В данном посте я хочу описать простую методику пиксельного искажения изображения на «чистом» 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. В других не могу пока проверить, если не работает, напишите в личку.
Спасибо за внимание.