Эффекты HTML5 canvas

16-06-2012

Раньше, до появления возможностей HTML5 с помощью JavaScript работать с изображениями, все делалось на PHP. Посмотрим как работать с изображениями на HTML <canvas>.

Работа с пикселями

Самый простой способ работать с изображениями, брать каждый пиксель и менять значение каналов: красный, зеленый, синий и прозрачность (red, green, blue, alpha). Также известный как R,G,B и A. Результаты могут получаться довольно таки бесполезными, но достаточно интересными.

Простым примером покажем что получится если поменять местами значения канала B (синий) и G (зеленый).

rgb(100, 50, 30, 255) //меняем
rgb(100, 30, 50, 255)

Callbacks

Подготовку изображения, выбор пикселей и другие части которые нужны выполнить до собственно до самого манипулирования каналами пикселя пока опустим. Функция для первого примера:

function (r, g, b)
{
  return [r, b, g, 255];
}

В данном случае прозрачность нам не нужна, и устанавливаем его строго на 255. Результат:

rbg

Имели:

original

Если нужно сделать изображение частично прозрачным нужна следующая функция:

function (r, g, b, a, factor)
{
  return [r, g, b, factor];
}

В этом случае мы позволяем указать factor, то есть возможность указать прозрачность. Пример при вызове с factor = 111:

Следующий пример будет немного сложнее, это градиент:

function (r, g, b, a, factor, i)
{
  var total = this.original.data.length;
  return [r, g, b, factor + 255 * (total - i) / total];
}

В итоге получаем следующее:

Вывод изображения на canvas

Для целей вывода изображения на канвас напишем небольшой конструктор:

function CanvasImage(canvas, src)
{
  // load image in canvas
  var context = canvas.getContext('2d');
  var i = new Image();
  var that = this;
  i.onload = function(){
    canvas.width = i.width;
    canvas.height = i.height;
    context.drawImage(i, 0, 0, i.width, i.height);

    // remember the original pixels
    that.original = that.getData();
  };
  i.src = src;

  // cache these
  this.context = context;
  this.image = i;
}	

Передается в этот конструктор ссылку на элемент canvas и адрес изображения. Изображение должно быть расположено на том же домене что и скрипт.

var transformador = new CanvasImage(
  $('canvas'),
  '/wp-content/uploads/2008/05/zlati-nathalie.jpg'
);

Конструктор создает новый объект Image и как только он загружается рисует его на canvasе. Еще конструктор запоминает некоторые данные для дальнейшего использования, такие как context, объект Image. `this` это тот же `this` к которому обращаются наши функции работающие с каналами пикселей которые были описаны выше.

Далее нам нужно три простых метода для выбора, установки и сбрасывания некоторых данных изображения на

canvas:

CanvasImage.prototype.getData = function()
{
  return this.context.getImageData(0, 0, this.image.width, this.image.height);
};

CanvasImage.prototype.setData = function(data)
{
  return this.context.putImageData(data, 0, 0);
};

CanvasImage.prototype.reset = function()
{
  this.setData(this.original);
}

“Мозгом” всего что мы делаем является метод transform(). Он берет функцию которая должна работать с каналами пикселя, одну из тех функций которые были описаны выше, и factor который по сути является параметром выбранной функции. Затем проходит по всем пикселям, передовая значения каналов rgba функции и заменяя их вернувшими значениями.

CanvasImage.prototype.transform = function(fn, factor)
{
  var olddata = this.original;
  var oldpx = olddata.data;
  var newdata = this.context.createImageData(olddata);
  var newpx = newdata.data
  var res = [];
  var len = newpx.length;
  for (var i = 0; i < len; i += 4)
  {
   res = fn.call(this, oldpx[i], oldpx[i+1], oldpx[i+2], oldpx[i+3], factor, i);
   newpx[i]   = res[0]; // r
   newpx[i+1] = res[1]; // g
   newpx[i+2] = res[2]; // b
   newpx[i+3] = res[3]; // a
  }
  this.setData(newdata);
};

Довольно таки просто не правда ли? Единственное место вызывающее вопросы это наверное инкремент i += 4. Все потому что getImageData().data является массивом в котором под каждый пиксель выделено 4 рядом стоящих элемента. Ниже пример изображения с 2мя пикселями:

[
  255, 0, 0, 255,
  0, 0, 255, 255
]

Объекты созданные конструктором ImageCanvas будем использовать примерно так:

transformador.transform(function(r, g, b, a, factor, i)
{
  // image magic here
  return [r, g, b, a];
}, factor);

Можно поиграться с этим объектом в консоли на странице с демкой.

Callbacks (продолжние)

Рассмотрим код остальных представленных на странице с примером реализации обработки изображения, возможностей.

Черно-белый

Серый равняется количеству красного, зеленого и синего. То есть мы можем просто посчитать среднее арифметическое трех цветов и получим нужное значение.

var agv = (r + g + b) / 3;

Этого в принципе достаточно, но существует небольшая секретная формула которая учитывает чувствительность людей на разные цвета. Результат:

function(r, g, b)
{
  var avg = 0.3  * r + 0.59 * g + 0.11 * b;
  return [avg, avg, avg, 255];
}

Сепия

Сепия получается преобразованием изображения в черно-белое, а после добавления некоторого определенного количества цвета каждому пикселю. Я добавляю 100 красного и 50 зеленого , но вы можете попробовать другие значения.

function(r, g, b)
{
  var avg = 0.3  * r + 0.59 * g + 0.11 * b;
  return [avg + 100, avg + 50, avg, 255];
}

А тут уже другие значения, но этот случай мне не очень таки нравится.

Инверсия

Отнимая 255 значения каждого канала получим инверсию.

function(r, g, b)
{
  return [255 - r, 255 - g, 255 - b, 255];
}

Шум

Последний эффект который будет рассмотрен это шум. Тоже получается довольно таки просто, достаточно случайным образом выбирать число из промежутка -factor и factor и добавлять это число к каждому каналу, ниже код:

function(r, g, b, a, factor)
{
  var rand =  (0.5 - Math.random()) * factor;
  return [r + rand, g + rand, b + rand, 255];
}

Шум с factor = 55: