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

Submitted by Ромка on Ср, 06/07/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. }

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