Pull to refresh

Пиксельные искажения с билинейной фильтрацией в HTML5 canvas

Reading time 3 min
Views 8.6K

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

Articles