Ромка!eu

  • Отдых
  • Работа
Главная — Блоги — Ромка's blog

Это старая (Drupal 6) версия сайта romka.eu. Она больше не обновляется, комментирование материалов отключено. Обновленная версия сайта доступна по адресу http://romka.eu.

Проба пера в HTML5 + canvas. Эффект ластика

Ромка — Ср, 07/06/2011 - 15:42

Задача

Создать эффект "ластика" с помощью html5 тэга canvas. Суть эффекта простая: выводится картинка, поверх картинки выводится полупрозрачный фон, если пользователь нажимает на левую кнопку мыши и начинает двигать курсор по холсту, то полупрозрачный фон должен стираться. Конечный результат можно увидеть тут.

Задача будет разбита на 3 части:
1. сначала мы зальем картинку равномерным фоном и научимся стирать этот фон ластиком квадратной формы.
2. Затем мы зальем картинку равномерным фоном и научимся стирать фон ластиком круглой формы.
3. И в конце мы зальем картинку полупрозрачной текстурой и научимся стирать эту текстуру.

Прежде чем читать дальше, рекомендую ознакомиться вот с этой документацией: Обучение Canvas. Думаю, задачу проще было бы решить с использованием библиотек типа Libcanvas, но мне сначала интересно было поразбираться с голым канвасом.

Этап первый

Создаем html-страницу с холстом размером 800 на 600 и подключаем к ней файлы со стилями и скриптами (canvas3-1.html). На холсте с id "working-canvas" мы будем рисовать, холст с id "fog-canvas" будет выводиться поверх рабочего холста, на нем мы будем выводить полупрозрачный фон. Working-canvas я далее буду называть нижним холстом, а fog-canvas — верхним холстом.

canvas3-1.html:

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru">
  3. <head>
  4.   <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  5.   <title>Эксперименты с канвасом</title>
  6.   <link type="text/css" rel="stylesheet" media="all" href="./styles.css" />
  7.   <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js"></script>
  8.   <script type="text/javascript" src="./script.js"></script>
  9. </head>
  10. <body>
  11.   <div id="wrapper">
  12.     <canvas id="working-canvas" width="800" height="600">
  13.       Вы должны обновить ваш браузер
  14.     </canvas>
  15.    
  16.     <canvas id="fog-canvas" width="800" height="600">
  17.       Вы должны обновить ваш браузер
  18.     </canvas>
  19.   </div>  
  20. </body>
  21. </html>

На событие document.ready (canvas3-3.html) мы:

  1. создаем 2 канваса,
  2. для каждого канваса создаем по контексту,
  3. вызываем функцию draw(),
  4. на событие mouseDown "включаем" ластик, за работу которого отвечает функция eraser(),
  5. на событие mouseUp "выключаем" ластик.

Событие document.ready:

  1. $(document).ready(function() {
  2.   // Создаем холсты и контексты
  3.   var canvas = document.getElementById('working-canvas');
  4.   var fog_canvas = document.getElementById('fog-canvas');
  5.  
  6.   var context = canvas.getContext('2d');
  7.   var fog_context = fog_canvas.getContext('2d');
  8.  
  9.   if (canvas.getContext && fog_canvas.getContext){
  10.     // если все успешно создано, выводим изображения на холсты
  11.     draw(context, fog_context);
  12.   }
  13.  
  14.   // Биндим эффект квадратного ластика на маусдаун
  15.   $(fog_canvas).bind('mousedown', function(e) {
  16.     eraser(e, context, 40);
  17.     $(fog_canvas).bind('mousemove', function(e) {
  18.       eraser(e, context, 40);
  19.     });
  20.   });
  21.   // при маусапе отключаем ластик
  22.   $(fog_canvas).bind('mouseup', function() {
  23.     $(fog_canvas).unbind('mousemove');
  24.   });
  25. });

Функция draw():

  1. на нижнем холсте выводит картинку,
  2. верхний холст заливает полупрозрачным фоном.

  1. function draw(context, fog_context) {
  2.   // Загружаем картинку, после ее загрузки выводим её на нижний холст, верхний холст заливаем полупрозрачным фоном
  3.   var img = new Image();
  4.   img.src = 'ya.jpg';
  5.   img.onload = function() {
  6.     // когда изображение загружено, выводим его на холст
  7.     context.drawImage(img, 200, 200);
  8.    
  9.     // заливаем изображение полупрозрачным фоном
  10.     fog_context.fillStyle = "rgba(0, 200, 200, 0.5)";
  11.     fog_context.fillRect (200, 200, 430, 400);
  12.   }
  13. }

Ход работы над этой задачей вы можете увидеть по ссылкам: canvas3-1.html, canvas3-2.html, canvas3-3.html, canvas3-4.html, canvas3-5.html, canvas3-6.html (тупиковая ветвь), canvas3-7.html (окончательная версия). На протяжении всей работы заметно будет меняться только содержимое функции eraser().

Нижний холст будет использоваться в качестве эталона: когда нам понадобится "стереть" определенную область на верхнем холсте мы скопируем нужные пикселы с нижнего холста и заменим ими ту же область верхнего холста.

Сначала мы реализуем простейший эффект стирания, с помощью метода clearRect. Для этого создаем функцию eraser() и вешаем её работу на событие onMouseDown. На OnMouseUp делаем анбинд:

  1. function eraser(e, context, fog_context, radius) {
  2.   /**
  3.    * Пока в эту функцию передаются только рабочий контекст, радиус (пока он используется для задания стороны квадрата ластика) и объект event.
  4.    * Позже, нам понадобится добавить сюда передачу второго контекста
  5.    */
  6.   var mouseX, mouseY;
  7.  
  8.   if(e.offsetX) {
  9.     mouseX = e.offsetX;
  10.     mouseY = e.offsetY;
  11.   }
  12.   else if(e.layerX) {
  13.     mouseX = e.layerX;
  14.     mouseY = e.layerY;
  15.   } else {
  16.     mouseX = -1000;
  17.     mouseY = -1000;
  18.   }
  19.  
  20.   // вот это и есть ластик:
  21.   fog_context.clearRect(mouseX, mouseY, radius, radius);
  22. }

Эффект ластика в таком виде не очень удобен (пример), так как невозможно изменить его форму. Для того, чтобы исправить этот недостаток, как я уже писал выше, необходимо скопировать нужную область из нижнего холста и заменить ею ту же область верхнего холста, а для этого нужно воспользоваться методами getImageData и putImageData. Из названий не трудно догадаться, что первый метод получает информацию о цветах пикселов области, воторой позволяет изменить заданную область холста.

Новая версия функции eraser():

  1. function eraser(e, context, fog_context, radius) {
  2.   /**
  3.    * Пока в эту функцию передаются только рабочий контекст, радиус (пока он используется для задания стороны квадрата ластика) и объект event.
  4.    * Позже, нам понадобится добавить сюда передачу второго контекста
  5.    */
  6.   var mouseX, mouseY;
  7.  
  8.   var diameter = radius * 2;
  9.  
  10.   if(e.offsetX) {
  11.     mouseX = e.offsetX;
  12.     mouseY = e.offsetY;
  13.   }
  14.   else if(e.layerX) {
  15.     mouseX = e.layerX;
  16.     mouseY = e.layerY;
  17.   } else {
  18.     mouseX = -1000;
  19.     mouseY = -1000;
  20.   }
  21.  
  22.   // Этот вариант ластика нам не подходит:
  23.   //context.clearRect(mouseX, mouseY, radius, radius);
  24.   // вместо него используем такой: сначала из нижнего холста получаем значения цветов пикселов, попавших под ластик
  25.   imagedata = context.getImageData(mouseX - radius, mouseY - radius, diameter, diameter);
  26.   // Затем заменяем этими пикселами пикселы на верхнем холсте:
  27.   fog_context.putImageData(imagedata, mouseX - radius, mouseY - radius);
  28. }

Рабочий пример квадратного ластика на html5 + canvas.

Этап второй. Учимся стирать ластиком круглой формы

Для того чтобы изменить форму ластика, необходимо преобразовать содержимое объекта, возвращаемого методом getImageData. Этот объект содержит 3 свойства: width, height и data. Первые два элемента в посянении не нуждаются, последний элемент — это массив, содержащий информацию о цветах пикселов, входящих в выделенную область.

Формат этого массива имеет не очень удобную форму, это одномерный массив такого вида: [r1, g1, b1, a1, r2, g2, b2, a2, ... rN, gN, bN, aN], другими словами, за цвет пиксела M отвечают элементы массива от (M - 1) * 4 до (M - 1) * 4 + 3:
(M - 1) * 4 — красный
(M - 1) * 4 + 1 — зеленый
(M - 1) * 4 + 2 — синий
(M - 1) * 4 + 3 — альфа

При этом видно, что в этом массиве нет разбиения на строки, то есть массив, содержащий 1600 элементов, то есть информацию о 400 пикселах, может описывать как прямоугольник 10 на 40, так и квадрат 20 на 20.

Дальше немного тригонометрии:

  1. нам необходимо получить (x, y) координаты курсора мыши,
  2. те пикселы на верхнем холсте, которые попадают в круг определенного радиуса с центром (x, y) заменить пикселами с нижнего холста с теми же координатами,
  3. те пикселы на верхнем холсте, которые не попадают в круг, оставить без изменений.

Рабочий пример круглого ластика можно увидеть тут. А вот измененная часть функции eraser():

  1.  // Этот вариант ластика нам не подходит:
  2.   //context.clearRect(mouseX, mouseY, radius, radius);
  3.   // вместо него используем такой: сначала из нижнего холста получаем значения цветов пикселов, попавших под ластик
  4.   imagedata = context.getImageData(mouseX - radius, mouseY - radius, diameter, diameter);
  5.   fog_imagedata = fog_context.getImageData(mouseX - radius, mouseY - radius, diameter, diameter);
  6.  
  7.   //for(elem in imagedata) {
  8.   //  console.log(elem);
  9.   //}
  10.  
  11.   elem_count = diameter * diameter * 4;
  12.  
  13.   // Затем, воспользовавшись знаниями из геометрии за 7 класс, преобразовываем массив пикселов
  14.   i = 0;
  15.   while(i <= elem_count) {
  16.     /*
  17.      каждый элемент массива это не массив ргба, а отдельная компонетна цвета, то есть для нулевого элемента
  18.      0 - р
  19.      1 - г
  20.      2 - б
  21.      3 - а
  22.      
  23.      для m = i / 4 элемента:
  24.      i     - р
  25.      i + 1 - г
  26.      i + 2 - б
  27.      i + 3 - а
  28.      
  29.      
  30.      c
  31.      |
  32.      |\
  33.      | \
  34.      |  \
  35.      |   \
  36.      |____\
  37.      b     a
  38.      
  39.      ac должно быть меньше radius
  40.      
  41.      a — центр круга
  42.    
  43.      */
  44.  
  45.     // определяю координаты точки в матрице. m — номер в строке, n — номер строки
  46.     m = i / 4;
  47.     if (m < diameter) {
  48.       n = 0;
  49.     } else {
  50.       n = 0;
  51.       while(m >= diameter) {
  52.         m -= diameter;
  53.         n++;
  54.       }
  55.     }
  56.    
  57.     bc = radius - m;
  58.     if(bc < 0) {
  59.       bc = -bc;
  60.     }
  61.    
  62.     ab = radius - n;
  63.     if(ab < 0) {
  64.       ab = -ab;
  65.     }
  66.    
  67.     if(Math.sqrt(bc * bc + ab * ab) < radius) {
  68.       // Если пиксел попал в круг, то меняю его цвет как на нижнем холсте, иначе оставляю цвет на такой как на верхнем холсте
  69.       fog_imagedata['data'][i]     = imagedata['data'][i];     // r
  70.       fog_imagedata['data'][i + 1] = imagedata['data'][i + 1]; // g
  71.       fog_imagedata['data'][i + 2] = imagedata['data'][i + 2]; // b
  72.       fog_imagedata['data'][i + 3] = imagedata['data'][i + 3]; // a
  73.     }
  74.    
  75.     i += 4;
  76.   }
  77.  
  78.   // Затем заменяем этими пикселами пикселы на рабочем холсте:
  79.   fog_context.putImageData(fog_imagedata, mouseX - radius, mouseY - radius);

Этап третий. Теперь заменим однотонную заливку на заливку текстурой

Небольшая проблема вывода полупрозрачной текстуры состоит в том, что метод drawImage(), который мы используем для вывода изображения не позволяет сделать картинку полупрозрачной:

  1. метод globalAlpha() эту задачу не решает,
  2. эксперименты с createPattern() тоже ни к чему интересному не привели (пример canvas3-6.html), хотя во время этих экспериментов, я наткнулся на интересный пример: http://jsfiddle.net/UxDVR/7/.

Чтобы решить эту проблему мы прежде чем выводить картинку на верхний холст получим информацию об изображении с помощтю getImageData, каждый четвертый элемент массива data заменим, например, на 192 (это значение альфа-канала), а затем содержимое полученного массива перенесем на верхний холст (canvas3-7.html).

Ниже измененная версия функции draw():

  1. function draw(context, fog_context) {
  2.   // загружаем содержимое для верхнего слоя
  3.   var img_moroz = new Image();
  4.   img_moroz.src = 'moroz-small-2.png';
  5.   img_moroz.onload = function() {
  6.  
  7.     // загружаем содержимое нижнего слоя
  8.     var img = new Image();
  9.     img.src = 'ya.jpg';
  10.     img.onload = function() {
  11.       // когда нижнее изображение загружено, выводим его на холст
  12.       context.drawImage(img, 200, 200, 400, 400);
  13.      
  14.       // заливаем изображение полупрозрачным фоном
  15.       //fog_context.fillStyle = "rgba(0, 200, 200, 0.5)";
  16.       //fog_context.fillRect (200, 200, 430, 400);
  17.      
  18.       // выводим верхнее изображение, считываем его при помощи imageGetData и меняем альфу для всех пикселов
  19.       fog_context.drawImage(img_moroz, 200, 200);
  20.      
  21.       fog_imagedata = fog_context.getImageData(200, 200, 400, 400);
  22.      
  23.       elem_count = 429 * 400 * 4;
  24.       i = 3;
  25.       while(i <= elem_count) {
  26.         fog_imagedata['data'][i] = 192;
  27.         i += 4;
  28.       }
  29.  
  30.       // заменяем содержимое верхнего холста измененным содержимым
  31.       fog_context.putImageData(fog_imagedata, 200, 200);
  32.     }
  33.   }
  34. }

Конечный результат можно увидеть тут.

  • canvas
  • html5
  • ластик
  • Ромка's blog

Обо мне

Всем привет!
Меня зовут
Роман Архаров, я профессиональный веб-разработчик, программирую на языках PHP, Python и Action-Script, в работе использую фреймворки Drupal и Django. В этом блоге я размещаю заметки и статьи, связанные с моей работой, отдыхом и другими интересными мне темами.

Популярные заметки

  • Авторизация на Drupal-сайте с помощью аккаунта ВКонтакте
  • Перетаскивание строк таблицы. Table drag and drop — плагин для jQuery
  • Темизация Drupal. Часть 3. Основы Drupal Forms API и темизация форм
  • Темизация Drupal. Часть 4. Темизация Views
  • AJAX. Обмен данными между клиентом и сервером, закачка на сервер файлов без перезагрузки страницы при помощи библиотеки jQuery.
  • Кеширование на Drupal-сайте. Сравнение встроенного в Drupal кеша, статического файлового кеша (модуль Boost) и Varnish
  • Тестирование модуля "Inner poll"
  • Несколько панорам из путешествия по Перу и Боливии
  • Arduino + bluetooth, небольшая проблема
  • Пример разработки плагина для модуля CCK

Подписка


Последние комментарии

  • drupal-admin → Кеширование на Drupal-сайте. Сравнение эффективности встроенного в Drupal кеша, статического файлового кеша (модуль Boost) и Var → Отличная статья
  • Меховщиков Руслана → Проверка имени пользователя на наличие букв из разных алфавитов → Re: Читал про это уже на
  • Вероника → Куба. Лето 2008. Часть 3 → По поводу "приветливых жителей" Тринидада
  • Sfero → Статья в PC Magazine/RE. Drupal: разработка модуля → Немного о контенте
  • RemaGe → Статья в PC Magazine/RE. Drupal: разработка модуля → -)
  • Ромка → Статья в PC Magazine/RE. Drupal: разработка модуля → Да, будет сборка, та которую
  • RemaGe → Статья в PC Magazine/RE. Drupal: разработка модуля → -)
  • anatinge → Поездка в Киев → спасибо :)
  • Ромка → Поездка в Киев → Да, в общем-то, ничего
  • GogA → Поездка в Киев → Заинтриговал, что там за

Новые заметки

  • Кеширование на Drupal-сайте. Сравнение встроенного в Drupal кеша, статического файлового кеша (модуль Boost) и Varnish
  • Любопытное поведение модуля syslog в шестом Друпале (баг?)
  • Arduino + bluetooth, небольшая проблема
  • Гугл 1 марта прекратил поддержку сервиса Google Friend Connect
  • Проба пера в HTML5 + canvas. Эффект ластика
  • Шесть рукопожатий
  • Доклад на DrupalConfMoscow 2011
  • Статусы разных сущностей в Друпале 6
  • Футер, прибитый к низу страницы
  • Путешествие по Перу и Боливии. Часть 2

Тэги

Drupal jquery Куба Лето 2007 Лето 2008 Отдых Работа Штуки-дрюки лето модуль статья цитата
еще тэги

Навигация

  • Exchange rate
  • Переход по внешней ссылке
  • Последние сообщения
  • Фотогалереи

Курсы валют

  • 100 Казахстанских тенге — 21,3687 (-0.43)
  • 10 Украинских гривен — 27,2852 (-13.04)
  • 1 Китайский юань — 10,2706 (-40.98)
  • 100 Японских иен — 53,5332 (+12.67)
  • 1 Доллар США — 65,6210 (+33)
  • 1 Евро — 69,8076 (+29.42)
  • 10000 Белорусских рублей — 36,3310 (-2.51)
  • 1 Турецкая лира — 22,9540 (+4.99)

10 случайных заметок

  • Разработка сайта на Drupal. Часть 4. Пример разработки корпоративного интранет-сайта на Drupal
  • Модуль "Курсы валют" для Drupal 6
  • Панорама Гаваны
  • Разработка модуля для Drupal. Часть3. Cron и темизация
  • Несколько статей о Друпале
  • Тестовое письмо из Копенгагена.
  • Новый год 2008. Часть 5. Осло
  • Home sheep home
  • Шесть рукопожатий
  • Доклад на DrupalConfMoscow 2011

Случайные фото

Тринидадские спортсменчики
На подходе к Красной поляне...
Море
Оригинальная дорожная разметка в Гаване
Копенгаген, Кристиания, Граффити 7
Дорожка
Фонтаны в Осло работают даже зимой. Карл Юхан Гате
Старая и новая наши машины — пыжик и сузуки джимни
Я и бамбук
Красная поляна
Здание замка Акерхус
Без имени
Взгляд в будущее
Буквы
Сочи ночью
Еще горы
  • Отдых
  • Работа

При использовании материалов с сайта, пожалуйста, ставьте ссылку на источник. E-mail для связи: mne@romka.eu.