Манипуляция пикселями на холсте с использованием Javascript

в 10:52, , рубрики: css, html, html5 canvas, javascript, перевод с английского, метки: , ,

В данной статье будет рассмотрено управление изображением с помощью Javascript и HTML тега canvas. Статья является переводом. Оригинал на phpied.com.

Управление пикселями

Простейший путь обрабатывать данные с изображения – это брать каждый пиксель и изменять значение одного или нескольких из его каналов: красный, зеленый, синий и альфа (прозрачность), для краткости будем называть их R, G, B и A.
Пример: изменим какие-нибудь значения, например поменяем B и G:
Было rgb(100, 50, 30, 255) станет rgb(100, 30, 50, 255)
Манипуляцию как таковую можно представить простейшей callback-функцией. Для приведенного выше примера:

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

Здесь мы игнорируем альфа-канал, он нам не нужен и будет установлен равным 255.

Результат:

image

из оригинала:

image

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

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

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

image

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

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

Если сделаем factor=111, получим:

image
this ссылается на созданный нами объект, который можно было немного увидеть. Он хранит в себе кое-какую нужную информацию, которая может пригодиться, как рассмотренном примере.

Холст

Рассмотрим структуру нашего холста. Начнем с конструктора:

function CanvasImage(canvas, src) {
// загрузка изображения на холст
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);
// запомним оригинальные пикселы
that.original = that.getData();
};
i.src = src;
// кешируем
this.context = context;
this.image = i;
}

Используем это, чтоб передать ссылку на элемент холста, находящийся где-нибудь на странице, а также url изображения.

Изображение должно быть в том же домене, где идет обработка его данных:

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

Конструктор создает объект new Image, и после загрузки изображение отрисовывается на холсте. Затем сохраняем некоторые вещи на будущее такие как context, объект image и оригинальные данные об изображении. ‘this’ – тот же самый, к которому манипулятор пикселями имеет доступ в примере выше.

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

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(). Он обрабатывает callback-вызов манипулятора пикселями и factor, который в сущности является конфигурационной настройкой для манипулятора. Затем он проходит по всем пикселям, передает значение olddata rgba-канала в callback-функцию и использует вернувшиеся значения как newdata. В конце newdata записывается на холст.

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
]

Callback

Дальнейший код просто показывает различные варианты callback-функции, и создает UI для их использования. Рассмотрим несколько из них:

Градации серого

Градации серого – это равное количество красного, синего, зеленого. Самый простой способ добиться этого – посчитать среднее значение:
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];
}

image

Сепия

Простейший вариант: сделать серую версию и добавить немного цвета на неё – равного количества rgb для каждого пикселя. Я добавил 100 красного, 50 зеленого, но вы можете выбрать другие значения.

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

image

Вот другой вариант, который возможно и лучше, но мне не очень нравится:

image

Негатив

Вычтем значение каждого канала из 255 для получения негатива

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

image

Шумы

Добавить шум. Это просто развлечение, берем случайное значение между –factor и factor и добавляем его к каждому каналу:
function(r, g, b, a, factor) {
var rand = (0.5 - Math.random()) * factor;
return [r + rand, g + rand, b + rand, 255];
}

image

Ваш ход

Сам пример опубликован по ссылке. Некоторые из манипуляций предлагает рассмотреть самостоятельно, используя исходный код и воображение.
В качестве шаблона берем следующее:

transformador.transform(function(r, g, b, a, factor, i) {
// здесь ваша магия...
return [r, g, b, a];
});

Попробуйте, например, сделать изображение черно-белым (не градациями серого, а черно-белым, где каждый пиксель либо 0,0,0, либо 255,255,255).

Или придумайте какой-то свой интересный пример.

Автор: luce_stellare


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js