В данном посте я хочу описать простую методику пиксельного искажения изображения на «чистом» javascript в 2D-Canvas без использования специальных библиотек и шейдеров, путём прямого доступа к пикселям изображения. Надеюсь, это будет интересно и полезно как для общего развития, так и для решения каких-то задач.
Canvas и пиксели
Я не буду описывать полностью объект Canvas, для этого есть документация. Остановимся на том, что нам нужно. Во-первых, это получение 2D-контекста:
var context = canvas.getContext('2d');
Этот контекст умеет многое делать с двухмерной графикой, в том числе получать прямой доступ к пиекселям в заданной области:
var pixels = context.getImageData(x, y, width, height); context.putImageData(pixels, x, y);
Вот эти пиксели нам и предстоит изменять. Мы будем рассматривать только 32-битные изображения. Каждый пиксель такого изображения представляет собой четыре байта, по байту на канал (R,G,B,A). Пиксели представляют собой одномерный массив из этих байт. Доступ к ним осуществляется через поле data (x,y — координаты, c — канал, b — значение):
pixels.data[(x+y*height)*4+c] = b;
Функция искажения
Искажение изображения, которое мы рассматриваем, представляет собой функцию, параметрами которой являются кооринаты получаемого изображения (далее будем называть их пиксели), а результатом — координаты исходного изображения (далее будем называть их текселы, так как фактически исходное изображение — это текстура, а координаты — это числа с плавающей точкой). Таким образом, функция для увеличения изображения имеет примерно следующий вид:
var zoom = function(px, py) { return { 'x': (px+width/2)*0.5, 'y': (py+height/2)*0.5 } }
Составим еще несколько функций для других искажений. Описывать каждый алгоритм я не вижу смысла, математика довольно простая и говорит сама за себя.
var twirl = function(px, py) { var x = px-width/2; var y = py-height/2; var r = Math.sqrt(x*x+y*y); var maxr = width/2; if (r>maxr) return { 'x':px, 'y':py } var a = Math.atan2(y,x); a += 1-r/maxr; var dx = Math.cos(a)*r; var dy = Math.sin(a)*r; return { 'x': dx+width/2, 'y': dy+height/2 } }
var reflect = function(px, py) { if (py<height/2) return { 'x': px, 'y': py } var dx = (py-height/2)*(-px+width/2)/width; return { 'x': px+dx, 'y': height-py } }
var spherize = function(px,py) { var x = px-width/2; var y = py-height/2; var r = Math.sqrt(x*x+y*y); var maxr = width/2; if (r>maxr) return { 'x':px, 'y':py } var a = Math.atan2(y,x); var k = (r/maxr)*(r/maxr)*0.5+0.5; var dx = Math.cos(a)*r*k; var dy = Math.sin(a)*r*k; return { 'x': dx+width/2, 'y': dy+height/2 } }
Хэш-таблица
Итак, мы получили возможность узнать, какие тексели брать для каждого пикселя. Но не рассчитывать же координаты каждый раз? Это будет слишком напряжно. Для этого на помощь приходит хэш-таблица. Таким образом, мы вычисляем всю карту преобразований однократно для каждого размера изображения, и в дальнейшем используем её для каждого преобразования:
// Параметром является функция искажения. Если это строка, функция устанавливается из имеющихся в объекте. var setTranslate = function(translator) { if (typeof translator === 'string') translator = this[translator]; for (var y=0; y<height; y++) { for (var x=0; x<width; x++) { var t = translator(x, y); map[(x+y*height)*2+0] = Math.max(Math.min(t.x, width-1), 0); map[(x+y*height)*2+1] = Math.max(Math.min(t.y, height-1), 0); } } }
Билинейная фильтрация
Чтобы при искажениях резкие границы нам не портили настроение, применим классический алгоритм билинейной фильтрации. Он подробно описан в Википедии. Суть алгоритма заключается в нахождении цвета пикселя в зависимости от четырёх ближайших текселей. В нашем случае, алгоритм выглядеть будет так:
var colorat = function(x, y, channel) { return texture.data[(x+y*height)*4+channel]; } for (var j=0; j<height; j++) { for (var i=0; i<width; i++) { var u = map[(i+j*height)*2]; var v = map[(i+j*height)*2+1]; var x = Math.floor(u); var y = Math.floor(v); var kx = u-x; var ky = v-y; for (var c=0; c<4; c++) { bitmap.data[(i+j*height)*4+c] = (colorat(x, y , c)*(1-kx) + colorat(x+1, y , c)*kx) * (1-ky) + (colorat(x, y+1, c)*(1-kx) + colorat(x+1, y+1, c)*kx) * (ky); } } }
Заключение
Вот, собственно и всё. Осталось обернуть это в отдельный объект, добавить его в код и посмотреть, что получится.
Поиграться в реальном времени на JSFiddle.
Работает в Chrome и Firefox. В других не могу пока проверить, если не работает, напишите в личку.
Спасибо за внимание.
Автор: Stdit