Проба пера в 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:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <title>Эксперименты с канвасом</title> <link type="text/css" rel="stylesheet" media="all" href="./styles.css" /> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js"></script> <script type="text/javascript" src="./script.js"></script> </head> <body> <div id="wrapper"> <canvas id="working-canvas" width="800" height="600"> Вы должны обновить ваш браузер </canvas> <canvas id="fog-canvas" width="800" height="600"> Вы должны обновить ваш браузер </canvas> </div> </body> </html>
На событие document.ready (canvas3-3.html) мы:
- создаем 2 канваса,
- для каждого канваса создаем по контексту,
- вызываем функцию draw(),
- на событие mouseDown "включаем" ластик, за работу которого отвечает функция eraser(),
- на событие mouseUp "выключаем" ластик.
Событие document.ready:
$(document).ready(function() { // Создаем холсты и контексты var canvas = document.getElementById('working-canvas'); var fog_canvas = document.getElementById('fog-canvas'); var context = canvas.getContext('2d'); var fog_context = fog_canvas.getContext('2d'); if (canvas.getContext && fog_canvas.getContext){ // если все успешно создано, выводим изображения на холсты draw(context, fog_context); } // Биндим эффект квадратного ластика на маусдаун $(fog_canvas).bind('mousedown', function(e) { eraser(e, context, 40); $(fog_canvas).bind('mousemove', function(e) { eraser(e, context, 40); }); }); // при маусапе отключаем ластик $(fog_canvas).bind('mouseup', function() { $(fog_canvas).unbind('mousemove'); }); });
Функция draw():
- на нижнем холсте выводит картинку,
- верхний холст заливает полупрозрачным фоном.
function draw(context, fog_context) { // Загружаем картинку, после ее загрузки выводим её на нижний холст, верхний холст заливаем полупрозрачным фоном var img = new Image(); img.src = 'ya.jpg'; img.onload = function() { // когда изображение загружено, выводим его на холст context.drawImage(img, 200, 200); // заливаем изображение полупрозрачным фоном fog_context.fillStyle = "rgba(0, 200, 200, 0.5)"; fog_context.fillRect (200, 200, 430, 400); } }
Ход работы над этой задачей вы можете увидеть по ссылкам: 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 делаем анбинд:
function eraser(e, context, fog_context, radius) { /** * Пока в эту функцию передаются только рабочий контекст, радиус (пока он используется для задания стороны квадрата ластика) и объект event. * Позже, нам понадобится добавить сюда передачу второго контекста */ var mouseX, mouseY; if(e.offsetX) { mouseX = e.offsetX; mouseY = e.offsetY; } else if(e.layerX) { mouseX = e.layerX; mouseY = e.layerY; } else { mouseX = -1000; mouseY = -1000; } // вот это и есть ластик: fog_context.clearRect(mouseX, mouseY, radius, radius); }
Эффект ластика в таком виде не очень удобен (пример), так как невозможно изменить его форму. Для того, чтобы исправить этот недостаток, как я уже писал выше, необходимо скопировать нужную область из нижнего холста и заменить ею ту же область верхнего холста, а для этого нужно воспользоваться методами getImageData и putImageData. Из названий не трудно догадаться, что первый метод получает информацию о цветах пикселов области, воторой позволяет изменить заданную область холста.
Новая версия функции eraser():
function eraser(e, context, fog_context, radius) { /** * Пока в эту функцию передаются только рабочий контекст, радиус (пока он используется для задания стороны квадрата ластика) и объект event. * Позже, нам понадобится добавить сюда передачу второго контекста */ var mouseX, mouseY; var diameter = radius * 2; if(e.offsetX) { mouseX = e.offsetX; mouseY = e.offsetY; } else if(e.layerX) { mouseX = e.layerX; mouseY = e.layerY; } else { mouseX = -1000; mouseY = -1000; } // Этот вариант ластика нам не подходит: //context.clearRect(mouseX, mouseY, radius, radius); // вместо него используем такой: сначала из нижнего холста получаем значения цветов пикселов, попавших под ластик imagedata = context.getImageData(mouseX - radius, mouseY - radius, diameter, diameter); // Затем заменяем этими пикселами пикселы на верхнем холсте: fog_context.putImageData(imagedata, mouseX - radius, mouseY - radius); }
Рабочий пример квадратного ластика на 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.
Дальше немного тригонометрии:
- нам необходимо получить (x, y) координаты курсора мыши,
- те пикселы на верхнем холсте, которые попадают в круг определенного радиуса с центром (x, y) заменить пикселами с нижнего холста с теми же координатами,
- те пикселы на верхнем холсте, которые не попадают в круг, оставить без изменений.
Рабочий пример круглого ластика можно увидеть тут. А вот измененная часть функции eraser():
// Этот вариант ластика нам не подходит: //context.clearRect(mouseX, mouseY, radius, radius); // вместо него используем такой: сначала из нижнего холста получаем значения цветов пикселов, попавших под ластик imagedata = context.getImageData(mouseX - radius, mouseY - radius, diameter, diameter); fog_imagedata = fog_context.getImageData(mouseX - radius, mouseY - radius, diameter, diameter); //for(elem in imagedata) { // console.log(elem); //} elem_count = diameter * diameter * 4; // Затем, воспользовавшись знаниями из геометрии за 7 класс, преобразовываем массив пикселов i = 0; while(i <= elem_count) { /* каждый элемент массива это не массив ргба, а отдельная компонетна цвета, то есть для нулевого элемента 0 - р 1 - г 2 - б 3 - а для m = i / 4 элемента: i - р i + 1 - г i + 2 - б i + 3 - а c | |\ | \ | \ | \ |____\ b a ac должно быть меньше radius a — центр круга */ // определяю координаты точки в матрице. m — номер в строке, n — номер строки m = i / 4; if (m < diameter) { n = 0; } else { n = 0; while(m >= diameter) { m -= diameter; n++; } } bc = radius - m; if(bc < 0) { bc = -bc; } ab = radius - n; if(ab < 0) { ab = -ab; } if(Math.sqrt(bc * bc + ab * ab) < radius) { // Если пиксел попал в круг, то меняю его цвет как на нижнем холсте, иначе оставляю цвет на такой как на верхнем холсте fog_imagedata['data'][i] = imagedata['data'][i]; // r fog_imagedata['data'][i + 1] = imagedata['data'][i + 1]; // g fog_imagedata['data'][i + 2] = imagedata['data'][i + 2]; // b fog_imagedata['data'][i + 3] = imagedata['data'][i + 3]; // a } i += 4; } // Затем заменяем этими пикселами пикселы на рабочем холсте: fog_context.putImageData(fog_imagedata, mouseX - radius, mouseY - radius);
Этап третий. Теперь заменим однотонную заливку на заливку текстурой
Небольшая проблема вывода полупрозрачной текстуры состоит в том, что метод drawImage(), который мы используем для вывода изображения не позволяет сделать картинку полупрозрачной:
- метод globalAlpha() эту задачу не решает,
- эксперименты с createPattern() тоже ни к чему интересному не привели (пример canvas3-6.html), хотя во время этих экспериментов, я наткнулся на интересный пример: http://jsfiddle.net/UxDVR/7/.
Чтобы решить эту проблему мы прежде чем выводить картинку на верхний холст получим информацию об изображении с помощтю getImageData, каждый четвертый элемент массива data заменим, например, на 192 (это значение альфа-канала), а затем содержимое полученного массива перенесем на верхний холст (canvas3-7.html).
Ниже измененная версия функции draw():
function draw(context, fog_context) { // загружаем содержимое для верхнего слоя var img_moroz = new Image(); img_moroz.src = 'moroz-small-2.png'; img_moroz.onload = function() { // загружаем содержимое нижнего слоя var img = new Image(); img.src = 'ya.jpg'; img.onload = function() { // когда нижнее изображение загружено, выводим его на холст context.drawImage(img, 200, 200, 400, 400); // заливаем изображение полупрозрачным фоном //fog_context.fillStyle = "rgba(0, 200, 200, 0.5)"; //fog_context.fillRect (200, 200, 430, 400); // выводим верхнее изображение, считываем его при помощи imageGetData и меняем альфу для всех пикселов fog_context.drawImage(img_moroz, 200, 200); fog_imagedata = fog_context.getImageData(200, 200, 400, 400); elem_count = 429 * 400 * 4; i = 3; while(i <= elem_count) { fog_imagedata['data'][i] = 192; i += 4; } // заменяем содержимое верхнего холста измененным содержимым fog_context.putImageData(fog_imagedata, 200, 200); } } }
Конечный результат можно увидеть тут.
Последние комментарии