HTML5 уже давно прочно вошел в нашу жизнь и уже все основные браузеры поддерживают его API. F А значит, уже сегодня, используя холст (Canvas) и API файлов (File API) мы можем создать полномасштабный редактор изображений.
Давайте, прежде чем браться за работу, посмотрим на конечный результат графического редактора на HTML5, к которому мы будем стремиться. Поиграйте с ним, чтобы почувствовать на что он способен.
Из-за большого количества кода в этом уроке, мы сразу пройдемся по всем файлам, вместо того, чтобы делать все с нуля. Не волнуйтесь, я постараюсь прокомменировать код насколько возможно, уверен, вы все поймете.
Шаг 1. Стили
Займемся стилевым оформлением нашего редактора. Скачайте архив с примером, откройте файл style.css
и изучите код.
На первой строке мы изменяем шрифт и отключаем контуры вокруг элементов. Теперь займемся определением стиля: ul#mainmenu
- главный элемент, div#overlay
отвечает за тень под всеми диалогами и ul#layers
- панель со слоями, которая будет отображаться на правой стороне холста. Следующее, что мы сделаем – определим стиль для кнопок инструментов. И наконец, у нас будет кусочек стиля jQuery-UI's (эта часть нужна нам для окна обрезания слоев.
Теперь посмотрим, что у нас написано в файле print.css
. В нем всего две строки - этого вполне достаточно чтобы скрыть все, кроме холста при печати изображения (этот стиль применяется только при печати страницы о чем свидетельствует директива media="print"
).
body * { visibility: hidden; }
canvas { visibility: visible; position: absolute; top: 0; left: 0; }
Первая строка скрывает все элементы внутри тега body, а вторая строка делает холст видимым (а также спозиционированным в верхнем левом углу). Это потому, что при печати людей интересует содержание, а не интерфейс.
Шаг 2: HTML структура
Вы должны уже иметь общее представление о интерфейсе, изучая наши CSS стили. Теперь взгляните на файл index.html
. Обратите внимание на HTML5 doctype спецификацию. Теперь больше никаких длинный надписей после html
, которые хрен запомнишь.
В верхней части, мы подключаем необходимые библиотеки и JS-файлы. Они включены в пример графического редактора на HTML5; а конкретно в библиотеки EaselJS, jQuery и jQuery UI.
Далее мы построим всю структуру пользовательского интерфейса. Просто помните: каждый тег DIV с классом dialog
- это просто диалоговое окно для пользователя чтобы ввести данные, необходимые для выполнения некоторых операций на изображении. Если вы сейчас запустите этот код в вашем браузере, то увидите несколько 404 ошибок в консоли и что меню не будет работать, но мы это исправим, когда создадим файл ui.js
.
Шаг 3: Главный объект приложения
Хорошей практикой является обертывание всех ваших приложений, связанных функции и переменных внутри одного объекта, чтобы предотвратить их от перекрывания внешними библиотеками или даже своими собственными сценариями. Наш объект будет выглядеть следующим образом:
app = {
stage: null,
canvas: null,
layers: [],
tool: TOOL_SELECT,
callbacks: {},
selection: {
x: -1, y: -1
},
renameLayer: 0,
undoBuffer: [],
redoBuffer: []
}
Я начинаю все имена переменных и функций с маленькой буквы, основанной на кодексе конвенций Дугласа Крокфорд для JavaScript.
Поместим ссылку на объект Stage
для нашего приложения в app.stage
. Если вы уже делали что-нибудь на ActionScript, представьте, что наш Stage написан на AS3. У него есть список отображения, который отрисовывается на канвасе при каждом обновлении. app.canvas
- переменная со ссылкой на холст элемента, расположенного внутри нашего HTML документа. Мы будем использовать его для создания рабочей области и изменять размеры его вместе с окном.
Массив app.layers
будет содержать все слои изображения, а app.tool
содержит значение текущего выбранного инструмента. В app.callbacks
будут содержаться все обратные вызовы, которые нам нужно будет уточнить(например, клик по кнопке меню), а app.renameLayer
будет содержать количество переименованных слоїв. app.undoBuffer
и app.redoBuffer
- массивы для хранения резервной копии. app.layers
нужен для того, чтобы работали функции отмены и повторения предыдущего действия.
Вы также должны будете добавить эти четыре строчки перед определением приложения
(это всего лишь константы-инструменты):
const
TOOL_MOVE = 0,
TOOL_SELECT = 1,
TOOL_TEXT = 2;
Шаг 4: полезные методы
Теперь, мы определим методы этого объекта. Сначала добавьте следующие refreshLayers()
и sortLayers()
методы:
refreshLayers: function () {
if ((this.getActiveLayer() == undefined) && (this.layers.length > 0)) this.layers[0].active = true;
this.stage = new Stage(this.canvas);
this.stage.regX = -this.canvas.width / 2;
this.stage.regY = -this.canvas.height / 2;
app.layers.toString = function () {
var ret = [];
for (var i = 0, layer; layer = this[i]; i++) {
ret.push('{"x":' + layer.x + ',"y":' + layer.y + ',"scaleX":' + layer.scaleX +
',"scaleY":' + layer.scaleY + ',"skewX":' + layer.skewX + ',"skewY":' +
layer.skewY + ',"active":' + layer.active + ',"visible":' + layer.visible +
',"filters":{"names":[' +
(layer.filters != null ? layer.filters.toString().replace(/(\[|\])/g, '"'): 'null') +
'],"values":[' + JSON.stringify(layer.filters) + ']}}');
}
return '[' + ret.join(',') + ']';
}
$('ul#layers').html('');
for (var i = 0, layer; layer = this.layers[i]; i++) {
var self = this;
self.stage.addChild(layer);
(function(t, n) {
layer.onClick = function (e) {
if ((self.tool != TOOL_TEXT) || (!t.text)) return true;
self.activateLayer(t);
editText = true;
}
layer.onPress = function (e1) {
if (self.tool == TOOL_SELECT) {
self.activateLayer(t);
}
var offset = {
x: t.x - e1.stageX,
y: t.y - e1.stageY
}
if (self.tool == TOOL_MOVE) self.addUndo();
e1.onMouseMove = function (e2) {
if (self.tool == TOOL_MOVE) {
t.x = offset.x + e2.stageX;
t.y = offset.y + e2.stageY;
}
}
};
})(layer, i);
layer.width = (layer.text != null ? layer.getMeasuredWidth() * layer.scaleX: layer.image.width * layer.scaleX);
layer.height = (layer.text != null ? layer.getMeasuredLineHeight() * layer.scaleY: layer.image.height * layer.scaleY);
layer.regX = layer.width / 2;
layer.regY = layer.height / 2;
$('ul#layers').prepend('<li id="layer-' + i + '" class="' + (layer.active ? 'active': '') +
'"><img src="' + (layer.text != undefined ? '': layer.image.src) + '"/><h1>' +
((layer.name != null) && (layer.name != '') ? layer.name: 'Unnamed layer') +
'</h1><span><button class="button-delete">Удалить</button><button class="button-hide">' +
(layer.visible ? 'Hide': 'Show') + '</button><button class="button-rename">Переименовать</button></span></li>');
}
this.stage.update();
$('ul#layers').sortable({
stop: function () {
app.sortLayers();
}
});
if (this.layers.length > 0) {
$('#button-layercrop').attr('disabled', false);
$('#button-layerscale').attr('disabled', false);
$('#button-layerrotate').attr('disabled', false);
$('#button-layerskew').attr('disabled', false);
$('#button-layerflipv').attr('disabled', false);
$('#button-layerfliph').attr('disabled', false);
$('#button-imagescale').attr('disabled', false);
$('#button-imagerotate').attr('disabled', false);
$('#button-imageskew').attr('disabled', false);
$('#button-imageflipv').attr('disabled', false);
$('#button-imagefliph').attr('disabled', false);
$('#button-filterbrightness').attr('disabled', false);
$('#button-filtercolorify').attr('disabled', false);
$('#button-filterdesaturation').attr('disabled', false);
$('#button-filterblur').attr('disabled', false);
$('#button-filtergaussianblur').attr('disabled', false);
$('#button-filteredgedetection').attr('disabled', false);
$('#button-filteredgeenhance').attr('disabled', false);
$('#button-filteremboss').attr('disabled', false);
$('#button-filtersharpen').attr('disabled', false);
} else {
$('#button-layercrop').attr('disabled', true);
$('#button-layerscale').attr('disabled', true);
$('#button-layerrotate').attr('disabled', true);
$('#button-layerskew').attr('disabled', true);
$('#button-layerflipv').attr('disabled', true);
$('#button-layerfliph').attr('disabled', true);
$('#button-imagescale').attr('disabled', true);
$('#button-imagerotate').attr('disabled', true);
$('#button-imageskew').attr('disabled', true);
$('#button-imageflipv').attr('disabled', true);
$('#button-imagefliph').attr('disabled', true);
$('#button-filterbrightness').attr('disabled', true);
$('#button-filtercolorify').attr('disabled', true);
$('#button-filterdesaturation').attr('disabled', true);
$('#button-filterblur').attr('disabled', true);
$('#button-filtergaussianblur').attr('disabled', true);
$('#button-filteredgedetection').attr('disabled', true);
$('#button-filteredgeenhance').attr('disabled', true);
$('#button-filteremboss').attr('disabled', true);
$('#button-filtersharpen').attr('disabled', true);
}
},
sortLayers: function () {
var tempLayers = [],
layersList = $('ul#layers li');
for (var i = 0, layer; layer = $(layersList[i]); i++) {
if (layer.attr('id') == undefined) break;
tempLayers[i] = this.layers[layer.attr('id').replace('layer-', '') * 1];
}
tempLayers.reverse();
this.layers = tempLayers;
this.refreshLayers();
}
Перед refreshLayers ()
, вставьте другой набор вспомогательных функций (не забудьте добавить запятую после последнего!):
getActiveLayer: function () {
var ret;
this.layers.forEach(function(v) {
if (v.active) ret = v;
});
if ((ret == undefined) && (this.layers.length > 0)) return this.layers[0];
return ret;
},
getActiveLayerN: function () {
for (var i = 0, layer; layer = this.layers[i]; i++) {
if (layer.active) return i;
}
},
activateLayer: function (layer) {
this.layers.forEach(function (v) {
v.active = false;
});
if (layer instanceof Bitmap) {
layer.active = true;
} else {
if (this.layers[layer] == undefined) return;
this.layers[layer].active = true;
}
this.refreshLayers();
},
Активный слой к которому вы обращаетесь при всех операция (трансформации, добавление фильтров и т.д.). Вы можете активировать слой, щелкнув по нему на панели Слои или с помощью инструмента Выбор на холсте.
Как видите, параметр метода activateLayer()
может быть либоBitmap
, либо цифрой. Если это Bitmap
- объект EaselJS для изображений – то его свойство active
стоит на true
, а если это цифра, активируется слой в этой позиции в массиве app.layers
. getActiveLayer()
возвращает слой, который является активным. getActiveLayerN ()
возвращает позицию активного слоя в пределах массива app.layers
.
Последний набор методов, относящихся к этому объекту, можно использовать сразу после декларации app.redoBuffer
и перед теми, которые вы использовали раньше:
addUndo: function () {
this.undoBuffer.push(this.layers.toString());
this.redoBuffer = [];
},
loadLayers: function (from, to) {
var json, jsonString = from.pop();
if (jsonString == undefined) return false;
to.push(this.layers.toString());
json = JSON.parse(jsonString);
for (var i = 0, layer, jsonLayer; ((layer = this.layers[i]) && (jsonLayer = json[i])); i++) {
for (value in jsonLayer) {
if (value != 'filters') {
layer[value] = jsonLayer[value];
} else {
var hadFilters = (layer.filters != null && layer.filters.length > 0);
layer.filters = [];
for (var j = 0; j < jsonLayer.filters.names.length; j++) {
if (jsonLayer.filters.names[j] == null) break;
layer.filters[j] = new window[jsonLayer.filters.names[j]];
for (value2 in jsonLayer.filters.values[0][j]) {
layer.filters[j][value2] = jsonLayer.filters.values[0][j][value2];
}
hadFilters = true;
}
if (hadFilters) {
if (layer.cacheCanvas) {
layer.updateCache();
} else {
layer.cache(0, 0, layer.width, layer.height);
}
}
}
}
}
this.refreshLayers();
},
undo: function () {
this.loadLayers(this.undoBuffer, this.redoBuffer);
},
redo: function () {
this.loadLayers(this.redoBuffer, this.undoBuffer);
},
Читая app.refreshLayers()
, вы наверное заметили, что метод toString()
, который мы используем для app.layers
, расширен кодом, который готовит строковую версию содержащихся внутри слоев. Конечно, это было бы пустой тратой памяти для хранения всей информации о слое, поэтому запоминаются только те значения, которые могут быть изменены приложением.
Метод addUndo()
помещает текущее состояние слоев в массив app.undoBuffer
и очищает app.redoBuffer
- потому что совершая действие, которое нельзя отемнить, вы не можете повторить ни одно из действий, совершенных перед ним. У loadLayers()
есть два аргумента (массив, из которого мы изымаем состояние app.layers
и массив, в который нам нужно поместить состояние этой переменной), и выполняет парсинг бэкапа app.layers
.
Как говорится в примере фильтра EaselJS:
"... фильтры отображаются только тогда, когда экранный объект в кеше ..."
Это значит, вам нужно использовать метод cache()
для Bitmap
, чтобы применить фильтр. Кеширование выполняется для улучшения работы приложения – фильтр применяется всего один раз, причем отрисовывается только отфильтрованный bitmap. EaselJS кеширует контент очень интересным способом – он просто копирует его на другой элемент канваса, которые не фигурирует в документе (он является скрытым). Я упомянул об этом, потому что в конце метода loadLayers()
есть блок if
, проверяющий документ на наличие каких-либо фильтров, которые должны быть обновлены в этом слое – и если они присутствуют, он обновляет кеш, или кеширует элемент.
Шаг 5: Инициализация
Инициализация всего приложения необычайно проста: просто вставьте этот код после объявления app
:
tick = function () {
app.stage.update();
}
$(document).ready(function () {
app.canvas = $('canvas')[0];
document.onselectstart = function () { return false; };
Ticker.setFPS(30);
Ticker.addListener(window);
});
Ticker
- реализованный в EaselJS таймер, который вызывает слушатель функции tick()
для поддержания стабильной FPS. Таким образом, мы можем автоматически вызывать app.stage.update()
, чтобы перерисовать сцену.
После загрузки документа, мы, с помощью jQuery
, находим первый элемент canvas и присваиваем app.canvas
ссылку на него и блокируем выбор любого элемента в документе (в ином случае при перемещении мыши по канвасу будет выделяться текст).
Мы устанавливаем FPS тикер на 30 (человеческому глазу необходимо только 24 кадров в секунду для восприятия плавного движение), а объект window устанавливаем как слушатель функции тикера.
Шаг 6: UI Helper Functions
Теперь настало время вдохнуть жизнь в наше меню и весь пользовательский интерфейс. Файл ui.js
будет почти полностью состоять из функций JQuery, так что вам будет легко в нем разобраться. Давайте начнем с вспомогательных функций:
importFile = false;
hideDialog = function (dialog) {
$(dialog).hide();
if ($('.dialog:visible').length == 0) $('#overlay').hide();
editText = false;
}
showDialog = function (dialog) {
$('#overlay').show();
$(dialog).show();
}
Переменная importFile
сообщит нам открыли ли мы файл или импортировали его. Именя функций showDialog()
and hideDialog()
говорят сами за себя. Что интересно в функции hideDialog()
- так это то, как она определяет наличие скрытых диалогов в псевдоклассе jQuery ':visible', чтобы потом скрыть подложку. В конце концов это оказалось бесполезным, потому что нет ситуации, когда на экране оставалось бы два и более диалога, но я оставил его на всякий случай. Может быть, вам он пригодится.
Шаг 7: Изменение размера окна
Теперь нам нужно предпринять какие-то действия, когда пользователь изменяет размер окна браузера. Это как раз тот случай, когда Jquery функция resize() вступает в игру. Она будет вызываться каждый раз, когда пользователь изменяет размер браузера:
$(window).resize(function () {
$('.dialog').each(function () {
$(this).css({ left: window.innerWidth / 2 - $(this).outerWidth() / 2 + 'px', top: window.innerHeight / 2 - $(this).outerHeight() / 2 + 'px' });
});
$('canvas').attr('height', $(window).height() - 37).attr('width', $(window).width() - 232);
$('ul#mainmenu').width($(window).width() - 4);
$('ul#layers').css({ height: $(window).height() - 37 });
app.refreshLayers();
if ($('#cropoverlay').css('display') == 'block') {
$('#cropoverlay').css({
left: Math.ceil(app.canvas.width / 2 - app.getActiveLayer().x - app.getActiveLayer().regX - 1) + 'px',
top: Math.ceil(app.canvas.height / 2 + app.getActiveLayer().y - app.getActiveLayer().regY + 38) + 'px'
});
}
});
В начале мы выравниваем по центру все сообщения с помощью Jquery функции each(). Это отменяет функцию для каждого элемента, для которого установлен селектор в функции $
.
Затем, мы должны установить ширину и высоту холста, - но не с помощью CSS, потому что это растянуло бы изображения внутри холста, а мы не хотим этого. Высота меню 37px поэтому мы зададит холсту "высоту окна браузера минус 37px. То же самое для ширины, но на этот раз мы должны вычесть ширину панели Layers, которая 232px. Мы также изменяем размер меню и панели Layers, чтобы они поместились в окно (здесь мы уже можем использовать CSS).
После этого мы должны обновить слои, чтобы убедиться, что они всегда актуальны когда меняется размера окна. Осталось только переместить окно «обрезания» на случай, если пользователь изменит размер окна when cropping the layer.
Шаг 8: Связываем все вместе
Кнопки в меню должны быть привязаны к функции обратного вызова, указанных в app.callbacks
, а также нам нужно связать событие KeyDown
для input и click
для кнопкок. Это может показаться сложным, но, когда вы увидите код в файле ui.js, заключенный между $(document).ready(function (){ });
, вам все станет ясно.
Важная вещь, которую вы должны помнить: когда вы делаете что-либо с помощью JQuery для любых HTML элементов, размещайте код внутри функции обратного вызова document.ready
, чтобы быть увереными, что все элементы, которые вы используете, уже отрендерены.
Затем вы должны посмотреть на функцию on(), которую мы использовали для привязки события нажатия на кнопки (на панели Слои). Функция on()
позволяет привязывать события ко всем элементам на странице, в том числе и к динамически добавляемым, что делает ее очень полезной, так как мы создаем новый список слоев при каждом вызове app.refreshLayers ()
.
Последняя функция, которая нам нужна – это $(window).resize()
, которая вручную запускает событие resize
для нашего окна. Поэтому важно присоединять скрипты по порядку, ведь если мы добавим ui.js
в документ HTML перед main.js
, слои будут обновляться перед определением функции, что может иногда приводить к неожиданным результатам, которые будут только усложнять поиск бага.
Если вы запустите приложение прямо сейчас, вы увидите красивое рабочее меню и правильное изменение размеров, но все же ни одна кнопка не будет работать.
Шаг 9: Открытие файлов
Теперь мы будем использовать другой API от спецификации HTML5: файловый программный интерфейс. Это дает нам возможность открывать файлы с компьютера пользователя, но только тогда, когда он выбирает их в поле ввода файла ОС в (для предотвращения кражи личных данных).
Пожалуйста, обратите внимание, что если вы будете запускать это приложение на локальном компьютере, необходимо настроить локальный сервер или добавить параметр --allow-file-access-from-files
при запуске Chrome, потому что открытие файлов из локальных веб-страниц по умолчанию отключено.
В файле file.js
мы также используем функции сохранения и распечатки изображения, поэтому давайте начнем с этих четырех элементов:
openFile = function (url, first) {
var img = new Image();
img.onload = function () {
var n = (first ? 0: app.layers.length);
if (first) app.layers = [];
app.layers[n] = new Bitmap(img);
app.layers[n].x = 0;
app.layers[n].y = 0;
app.activateLayer(n);
}
img.src = url;
this.undoBuffer = [];
this.redoBuffer = [];
}
openURL = function (self, url) {
$(self).attr('disabled', true);
openFile(url, !importFile);
hideDialog('#dialog-openurl');
}
saveFile = function () {
window.open(app.stage.toDataURL());
}
printFile = function () {
window.print()
}
Функция openFile
используетсяся, чтобы открыть изображение и добавить его к слоям..Выбрав в меню'Open File', мы сотрем старый контент, в то время как 'Import File' добавит в изображение новые слои. (Если параметр этой функции first стоит на true
, файл откроется, в противном случае он будет импортирован).
openURL
открывает файл, используя ту же самую функцию, но от внешнего источника (и, насколько мне известно, Chrome блокирует доступ к кросс-доменным данным о пикселях, что делает эти изображения практически бесполезными). Поэтому мы не можем сохранить файл на диске пользователя, мы просто открываем другое окно, содержащее только законченное изображение. Затем пользователь может щелкнуть правой кнопкой мыши, чтобы сохранить это изображение.
Печать осуществляется с помощью вызова window.print()
. Мы не cможем, конечно же, сразу начать печатать, потому эта функция откроет диалог печати по умолчанию, где пользователь может выбрать параметры печати.
Теперь мы определим некоторые функции обратного вызова для кнопок в меню:
app.callbacks.openFile = function (e) {
var file = e.target.files[0],
self = this;
if (!file.type.match('image.*')) return false;
var reader = new FileReader();
reader.onload = function(e) {
openFile(e.target.result, true);
};
reader.readAsDataURL(file);
};
app.callbacks.openURL = function (e) {
switch (e.type) {
case "click":
openURL($('#dialog-openurl input'), $('#dialog-openurl input').val());
break;
case "keydown":
if (e.keyCode == 13) openURL(this, $(this).val());
break;
}
}
app.callbacks.importFile = function (e) {
for (var i = 0, file; file = e.target.files[i]; i++) {
if (!file.type.match('image.*')) continue;
var reader = new FileReader();
reader.onload = function(e) {
openFile(e.target.result, false);
};
reader.readAsDataURL(file);
}
};
app.callbacks.saveFile = function () {
saveFile();
}
app.callbacks.printFile = function () {
printFile();
}
Как видите, код очень короткий, но очень мощный.. В первой функции обратного вызова (app.callbacks.openFile
) мы проверяем открыт ли файл изображения и останавливаемся, если это не так. Затем мы создаем новый FileReader
, устанавливаем для него функцию обратного вызова onload
для открытия файла и вызываем метод readAsDataURL(file)
, который загружает файл и выводит результат как ссылку на данные. Также обратите внимание, что мы очищаем массивы отмены (Undo) и возврата (Redo). Мы должны сделать это, потому что мы не можем восстановить изображение, если удаляем его - пользователь должен вручную повторно выбрать файл из input.
Сохраните файл, откройте приложение в браузере и попробуйте что-нибудь сделать! Не так много, но, если вы делаете это в первый раз, вам, вероятно, интересно будет загрузить несколько изображений в браузере, даже если вы можете только переместить их.
Шаг 10. Текстовый слой.
Теперь, вы можете открыть и импортировать изображения, а также добавить некоторый текст. Существует одна очень полезная вещь с холстом - вы определяете текст так же как и в CSS. С EaselJS нам не придется об этом беспокоиться.
Мы определим инструмент Текст в файле tools.js
. Добавьте следующие строки:
editText = false;
toolText = function (text, font, color, size, x, y) {
var n = (editText ? app.getActiveLayerN(): app.layers.length);
app.layers[n] = new Text(text, size + ' ' + font, color);
app.layers[n].x = x - app.canvas.width / 2;
app.layers[n].y = y - app.canvas.height / 2;
app.layers[n].name = text;
app.activateLayer(n);
hideDialog('#dialog-tooltext');
this.undoBuffer = [];
this.redoBuffer = [];
}
app.callbacks.toolText = function (e) {
switch (e.type) {
case "click":
if (e.target instanceof HTMLButtonElement) {
toolText($('#dialog-tooltext input.input-text').val(),
$('#dialog-tooltext select').val(),
$('#dialog-tooltext input.input-color').val(),
$('#dialog-tooltext input.input-size').val(),
(editText ? app.getActiveLayer().x: app.selection.x),
(editText ? app.getActiveLayer().y: app.selection.y));
} else {
if (app.tool != TOOL_TEXT) return true;
$('#dialog-tooltext').show();
$('#overlay').show();
app.selection.x = e.offsetX;
app.selection.y = e.offsetY;
$('#dialog-tooltext input.input-text').val((editText ? app.getActiveLayer().text: ''));
$('#dialog-tooltext input.input-size').val((editText ? app.getActiveLayer().font.split(' ')[0]: '12px'));
$('#dialog-tooltext select').val((editText ? app.getActiveLayer().font.split(' ')[1]: 'Calibri'));
$('#dialog-tooltext input.input-color').val((editText ? app.getActiveLayer().color: 'black'));
$('#dialog-tooltext input.input-color').css({ backgroundColor: $('#dialog-tooltext input.input-color').val() });
}
break;
case "keydown":
if (e.keyCode == 13) toolText($('#dialog-tooltext input.input-text').val(),
$('#dialog-tooltext select').val(),
$('#dialog-tooltext input.input-color').val(),
$('#dialog-tooltext input.input-size').val(),
(editText ? app.getActiveLayer().x: app.selection.x),
(editText ? app.getActiveLayer().y: app.selection.y));
break;
}
}
Переменная editText
равна true
когда мы меняем свойства существующего текстового слоя вместо создания нового.
Первая функция, как обычно, вспомогательная. Она проверяет редактируем ли мы существующий текстовый слой или добавляем новый, а затем создает новый объект Text
и добавляет его к уже имеющимся слоям. Поскольку все объекты в EaselJS расширяют базовый DisplayObject
, мы можем использовать как Text
, так и Bitmap
, только свойства их будут отличаться.
Вторая функция - функция обратного вызова. Первое, что мы должны сделать, это проверить, какой тип события мы получаем (потому что мы используем только одну функцию обратного вызова и для обработки нажатия кнопок и для обработки нажатия клавиш в input). Теперь мы просто активируем предыдущий хелпер.
Шаг 11. Трансформирование слоя.
Простые преобразования слоев делаются легко с помощью EaselJS. Все, что я покажу здесь, можно сделать просто изменив свойства слоя с изображением или текстом.
Я начну объяснения с опорной точки. Свойства regX
и regY
определяют точку, относительно которой рассчитываются вращение и смещение. В файле main.js
мы установили эту точку в центре изображения, чтобы легче трансформировать слои. Все функции преобразования слоев будут находиться в файле layer.js
affectImage = false;
layerScale = function (x, y) {
app.addUndo();
if (affectImage) return imageScale(x, y);
app.getActiveLayer().scaleX *= x / 100;
app.getActiveLayer().scaleY *= y / 100;
hideDialog('#dialog-scale');
}
layerRotate = function (deg) {
app.addUndo();
if (affectImage) return imageRotate(deg);
app.getActiveLayer().rotation += deg;
hideDialog('#dialog-rotate');
}
layerSkew = function (degx, degy) {
app.addUndo();
if (affectImage) return imageSkew(degx, degy);
app.getActiveLayer().skewX += degx;
app.getActiveLayer().skewY += degy;
hideDialog('#dialog-skew');
}
layerFlipH = function () {
app.addUndo();
app.getActiveLayer().scaleX = -app.getActiveLayer().scaleX;
}
layerFlipV = function () {
app.addUndo();
app.getActiveLayer().scaleY = -app.getActiveLayer().scaleY;
}
Как обычно, в этой части у нас есть вспомогательные функции. Я хочу кое-что сказать об этой части кода.
Во-первых, нам нужно активировать функцию addUndo()
перед тем, как что-либо менять. Почему? Потому что мы хотим вернуть над код в то состояние, в котором он находился перед тем, как мы нажали на кнопку отмены.
Обратите также внимание на переменную affectImage
. Нам нужно установить ее на true
, если мы хотим изменить изображение полностью; практически в каждой функции есть определение if
, которое проверяет, применяются ли изменения ко всему изображению и в случае, если это так, возвращает результат соответствующей функции image*()
.
Теперь поместите код функции обратного вызова в файл:
app.callbacks.numberOnly = function (e) {
if ((e.shiftKey) || ([8, 13, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 190, 189].indexOf(e.keyCode) < 0)) return false;
}
app.callbacks.layerRename = function (e) {
switch (e.type) {
case "click":
app.layers[app.renameLayer].name = $('#dialog-layerrename input').val();
app.refreshLayers();
hideDialog('#dialog-layerrename');
break;
case "keydown":
if (e.keyCode == 13) {
app.layers[app.renameLayer].name = $('#dialog-layerrename input').val();
app.refreshLayers();
hideDialog('#dialog-layerrename');
}
break;
}
}
app.callbacks.layerScale = function (e) {
switch (e.type) {
case "click":
layerScale($('#dialog-scale input.input-scaleX').val() * 1, $('#dialog-scale input.input-scaleY').val() * 1);
break;
case "keydown":
if (e.keyCode == 13) layerScale($('#dialog-scale input.input-scaleX').val() * 1, $('#dialog-scale input.input-scaleY').val() * 1);
break;
}
}
app.callbacks.layerRotate = function (e) {
switch (e.type) {
case "click":
layerRotate($('#dialog-rotate input').val() * 1);
break;
case "keydown":
if (e.keyCode == 13) layerRotate($(this).val() * 1);
break;
}
}
app.callbacks.layerSkew = function (e) {
switch (e.type) {
case "click":
layerSkew($('#dialog-skew input.input-skewX').val() / 100, $('#dialog-skew input.input-skewY').val() / 100);
break;
case "keydown":
if (e.keyCode == 13) layerSkew($('#dialog-skew input.input-skewX').val() / 100, $('#dialog-skew input.input-skewY').val() / 100);
break;
}
}
app.callbacks.layerFlipV = function () { layerFlipV(); }
app.callbacks.layerFlipH = function () { layerFlipH(); }
app.callbacks.layerCrop = function () {
var layer = app.getActiveLayer();
layer.cache(
Math.floor(app.canvas.width / 2 - $('#cropoverlay').position().left - layer.regX + layer.x - 1),
Math.floor(app.canvas.height / 2 - $('#cropoverlay').position().top - layer.regY + layer.y + 38),
$('#cropoverlay').width(),
$('#cropoverlay').height()
);
$(this).parent().find('.button-cancel').click();
}
Еще один ряд функций jQuery. Мы также проверяем тип события, т.к. эти фукнции можно активировать с помощью кнопки или поля для ввода. Все фукнции в вышеуказанном коде во многом похожи: проверить тип события, получить значение диалогового инпута и активировать фукнцию.
Callback-функция вверху этой части кода привязывается к инпутам, которые принимают только числа.
Шаг 12: Трансформирование: Масштабирование
Давайте начнем с добавления этотого исходного кода, я объясню его позже. Поместите код ниже, в файл image.js
:
imageScale = function (x, y) {
for (var i = 0, layer; layer = app.layers[i]; i++) {
layer.scaleX *= x / 100;
layer.scaleY *= y / 100;
layer.x *= x / 100;
layer.y *= y / 100;
}
hideDialog('#dialog-scale');
affectImage = false;
}
Мы просто пропускаем через цикл все слои, устанавливая им свойства scaleX
и scaleY
. Но изображение будет выглядеть странно, если мы станем только масштабировать слои. Поэтому при масштабировании мы должны также сдвигать каждый слой.
Шаг 13: Трансформирование изображения: Поворот
С поворотом будет немного сложнее, чем с масштабированием. Но сначала скопируйте этот код в image.js
:
imageRotate = function (deg) {
for (var i = 0, layer; layer = app.layers[i]; i++) {
layer.rotation += deg;
var rad = deg * Math.PI / 180,
x = (layer.x * Math.cos(rad)) - (layer.y * Math.sin(rad)),
y = (layer.x * Math.sin(rad)) + (layer.y * Math.cos(rad));
layer.x = x;
layer.y = y;
}
hideDialog('#dialog-rotate');
affectImage = false;
}
Все это, конечно, относится к вращению слоев но выделенный мною код является новым. В его основе лежат уравнения вращения точки в картезианской системе координат:
Где Φ - угол. В выделенном коде происходит перевод указанных уравнений в код JavaScript (плюс преобразование из градусов в радианы, потому что тригонометрическая функция Math
принимает в качестве параметра радианы, а один радиан равен числу пи, деленному на 180 градусов).
Шаг 14. Трансформирование: скос.
Перекос очень похож на вращение: это вращение, но с двух разных углов для двух направлений. Взгляните на код:
imageSkew = function (degx, degy) {
for (var i = 0, layer; layer = app.layers[i]; i++) {
layer.skewX += degx;
layer.skewY += degy;
var radx = degx * Math.PI / 180,
rady = degy * Math.PI / 180,
x = (layer.x * Math.cos(radx)) - (layer.y * Math.sin(radx)),
y = (layer.x * Math.sin(rady)) + (layer.y * Math.cos(rady));
layer.x = x;
layer.y = y;
}
hideDialog('#dialog-skew');
affectImage = false;
}
Вы видите разницу? Мы использовали только radx
для позиционирования по оси X и rady
для позиционирования по оси Y. За счет этого и достигается требуемый эффект.
Шаг 15. Трансформирование: переворачивание
Это модификация функции imageScale()
. Не расслабляйтесь, потому что это самая трудная из функций преобразования изображения, поэтому мы и оставили ее напоследок. Взгляните на код:
imageFlipH = function () {
app.addUndo();
for (var i = 0, layer; layer = app.layers[i]; i++) {
layer.scaleX = -layer.scaleX;
layer.x = -layer.x;
}
affectImage = false;
}
imageFlipV = function () {
app.addUndo();
for (var i = 0, layer; layer = app.layers[i]; i++) {
layer.scaleY = -layer.scaleY;
layer.y = -layer.y;
}
affectImage = false;
}
app.callbacks.imageFlipV = function () {
imageFlipV();
}
app.callbacks.imageFlipH = function () {
imageFlipH();
}
Давайте проигнорируем обратные вызовы - мы уже знаем, что они делают. Нам нужно сосредоточиться на первых двух функциях. В них мы просто пробегаем по слоям, поменяем значение scaleX
или scaleY
на противоположное - это и есть переворачивание изображения: мы просто перемещаем его в отрицательную область системы координат. Также координата x
или y
должна быть изменена, чтобы сделать изображение действительно выглядело перевернутым.
На этом мы заканчиваем с преобразованием изображения. Пришло время сделать что-то более продвинутое - фильтры!
Шаг 16. Простые фильтры: введение
Я называю их простыми потому, что мы используем фильтры, которые встроены в EaselJS: ColorFilter
и ColorMatrixFilter
. Прежде, чем мы применим их, я объясню, что каждый фильтр делает..
new ColorFilter(redMultiplier, greenMultiplier, blueMultiplier, alphaMultiplier, redOffset, greenOffset, blueOffset, alphaOffset);
Когда фильтр применяется он разбивает изображение на четыре канала (красный, зеленый, синий и альфа), умножает каждое значение на соответствующий множитель для каждого канала и добавляет соответствующие смещения. (На самом деле, изображение не раскалывается, это просто такая метафора.)
ColorMatrixFilter
принимает только один параметр:
new ColorMatrixFilter(matrix);
Матрица имеет следующий формат:
[
rr, rg, rb, ra, ro,
gr, gg, gb, ga, go,
br, bg, bb, ba, bo,
ar, ag, ab, aa, ao
]
Когда фильтр применяется, он также (образно) разбивает изображение на каналы, а затем умножает каждое значение друг на друга. Например, уравнение для значения пикселя в красном канале после прохождения через фильтр:
newRed = (red * rr) + (green * rg) + (blue * rb) + (alpha * ra) + ro;
То же самое для зеленого, синего и альфа-каналов также, только с разными переменных из матрицы (gr,gg,gb,ga
для зеленого, и так далее). Этот фильтр немного более продвинутый, чем ColorFilter
, потому что каждый цвет зависит от цвета других пикселей.
Шаг 17. Простые фильтры: вспомогательная функция
Это одна из двух вспомогательных функций, которые будут использованы здесь, но мы будем использовать ее для каждого фильтра. Поместите этот код в начале файла filters.js
:
applyFilter = function (filter) {
app.addUndo();
var layer = app.getActiveLayer();
layer.filters = (layer.filters ? layer.filters: []);
layer.filters.push(filter);
if (layer.cacheCanvas) {
layer.updateCache();
} else {
layer.cache(0, 0, layer.width, layer.height);
}
}
Он делает всю работу за нас: хватает активный слой; если нет никаких фильтров, то создает массив фильтров и добавляет фильтр.
После этого мы должны кешировать слой для для применения эффектов фильтра, поэтому мы проверяем, есть ли у нас уже в кеше этот слой (например, при его кадрировании) и вызываем updateCache()
или cache()
в случае необходимости.
Вот изображение, на котором я буду демонстрировать полученный результат после обработки изображения фильтром:
Шаг 18. Простые фильтры: яркость
Для этого эффекта мы будем использовать ColorFilter
, потому что изменение яркости изображения только изменяет все значения канала (красный, зеленый, синий) в слое на ту же величину.
Вот код (поместить его в файл filters.js
):
filterBrightness = function (value) {
applyFilter(new ColorFilter(value, value, value, 1));
hideDialog('#dialog-filterbrightness');
}
Как я уже упоминал ранее, наша вспомогательная функция делает все для нас, нам нужно только создать новый фильтр. Здесь мы создаем ColorFilter
с красным, зеленый, синий множителями со значениями value
и прозрачностью равной единице (мы не хотим чтобы она была затронута этим фильтром).
Ниже приведен результат действия этого фильтра:
Шаг 19. Простые фильтры: Colorify
Colorify изменяет значение каналов,делая его больше или меньше, чтобы изменить общий цвет изображения, так что мы снова будет использовать ColorFilter
. Взгляните на код:
filterColorify = function (r, g, b, a) {
applyFilter(new ColorFilter(1.0, 1.0, 1.0, 1.0, r, g, b, a));
hideDialog('#dialog-filtercolorify');
}
Опять же, все грязная работа ляжет на плечи applyFilter
, нам лишь нужно сосредоточиться на создании объекта фильтра. Здесь мы будем использовать последние четыре параметры конструктора ColorFilter
. Они добавляются к каналам, поэтому они идеально подходят под наши потребности.
Ниже приведен результат действия этого фильтра:
Шаг 20. Простые фильтры: уменьшение насыщенности
Снижение насыщенности (Desaturation) приводит к уменьшению насыщенности всех цветов снимка. Для этого нам нужно вычислить светимость каждого пикселя и установитm все цвета в этом значении. Простейшее уравнение светимости включает в себя только добавлением такого же количества всех цветов и для этого мы можем использовать ColorMatrixFilter
:
filterDesaturation = function () {
applyFilter(new ColorMatrixFilter(
[
0.33, 0.33, 0.33, 0.00, 0.00,
0.33, 0.33, 0.33, 0.00, 0.00,
0.33, 0.33, 0.33, 0.00, 0.00,
0.00, 0.00, 0.00, 1.00, 0.00
]
));
hideDialog('#dialog-filterbrightness');
}
Как я уже сказал раньше - мы должны взять такие же значения, что и в красном, зеленом и синем каналах и добавить их. Мы опять не трогаем альфа-канал, так как он не содержит каких-либо цветовых значений. Я не привожу результат действия фильтра, т.к. изображение стало черно-белым. Ничего интересного.
Шаг 21: Фильтры свертки
Фильтр Convolution Filter
уже немного более продвинутый, чем ColorMatrixFilter
. Он также использует матрицу, но матрица свертки представляет собой множители пикселей, окружающих актуальный пиксель.
Давайте предположим, что у нас (для примера) есть матрица свертки размером 3x3 (уже представлена в виде JavaScrip массива):
[
[ 0, 0, 0],
[-1, 1, 0],
[ 0, 0, 0]
]
И (к примеру), мы смотрим на те части изображения, где пиксели выглядят так: каждый номер представляет собой силу красного цвета канала (остальные мы игнорируем для простоты). Взгляните:
[ 00 ] [ 12 ] [ 43 ]
[ 12 ] [ 56 ] [ 62 ]
[ 63 ] [ 67 ] [ 92 ]
С помощью матрицы свертки мы модификацирум пиксел в центре (текущее значение: 56). Итак, мы умножаем каждое значение цвета вокруг пикселя на его множитель из массива свертки, а затем мы суммируем их. Мы получаем следующее уравнение:
newPixelValue = (00 * 0) + (12 * 0) + (43 * 0)
+ (12 * -1) + (56 * 1) + (62 * 0)
+ (63 * 0) + (67 * 0) + (92 * 0)
= (-12) + 56
= 44
Теперь установим значение красного канала пикселя на 44 – но в новоммассиве данных, потому что нам все еще надо держаться старого значения 56 для модификации других пикселей в изображении. Это означает, что при применении фильтра мы на самом деле создаем копию изображения, а не измененяем существующее на месте.
Как видите, при большой матрице формула станет более сложной, так как каждый пиксель зависит от данных соседних пикселей. По этой причине, большая матрица требует большего времени для обработки, так что не удивляйтесь, если ваш браузер "задумается", или рухнет.
Также вы должны помнить, что сумма всех значений внутри матрицы свертки должно быть равно либо нулю или единице - в противном случае вы можете получить непредсказуемый результат. Для упрощения мы можем использовать переменные factor
и offset
. После вычисления значения пикселя (уравнения выше) умножим все значения на factor
и offset
. Это упрощает создание, например, фильтра размытия, где все значения матрицы свертки являются одинаковыми.
К сожалению, ConvolutionFilter
не внедрен в EaselJS, так что нам придется его написать самим. Если взять за основу ColorFilter
, то у нас получится следующее:
(function (window) {
var ConvolutionFilter = function (matrix, factor, offset) {
this.initialize(matrix, factor, offset);
}
var p = ConvolutionFilter.prototype = new Filter();
p.matrix = null;
p.factor = 0.0;
p.offset = 0.0;
p.initialize = function (matrix, factor, offset) {
this.matrix = matrix;
this.factor = factor;
this.offset = offset;
}
p.applyFilter = function (ctx, x, y, width, height, targetCtx, targetX, targetY) {
targetCtx = targetCtx || ctx;
targetX = (targetX == null ? x: targetX);
targetY = (targetY == null ? y: targetY);
try {
var imageData = ctx.getImageData(x, y, width, height);
} catch (e) {
return false;
}
var data = JSON.parse(JSON.stringify(imageData.data));
var matrixhalf = Math.floor(this.matrix.length / 2);
var r = 0, g = 1, b = 2, a = 3;
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
var pixel = (y * width + x) * 4,
sumr = 0, sumg = 0, sumb = 0;
for (var matrixy in this.matrix) {
for (var matrixx in this.matrix[matrixy]) {
var convpixel = ((y + (matrixy - matrixhalf)) * width + (x + (matrixx - matrixhalf))) * 4;
sumr += data[convpixel + r] * this.matrix[matrixy][matrixx];
sumg += data[convpixel + g] * this.matrix[matrixy][matrixx];
sumb += data[convpixel + b] * this.matrix[matrixy][matrixx];
}
}
imageData.data[pixel + r] = this.factor * sumr + this.offset;
imageData.data[pixel + g] = this.factor * sumg + this.offset;
imageData.data[pixel + b] = this.factor * sumb + this.offset;
imageData.data[pixel + a] = data[pixel + a];
}
}
targetCtx.putImageData(imageData, targetX, targetY);
return true;
}
p.toString = function() {
return "[ConvolutionFilter]";
}
p.clone = function() {
return new ConvolutionFilter(this.matrix, this.factor, this.offset);
}
window.ConvolutionFilter = ConvolutionFilter;
}(window));
Вы можете пропустить все методы для applyFilter
, обратив внимание как они используются EaselJS для инициализации фильтра.
applyFilter()
вызывается, когда мы применяем фильтр к изображению. Во-первых, мы должны получить данные изображения из холста. Для этого я использую трюк с JSON.parse(JSON.stringify(imageData.data))
- потому что мы хотим копию данных изображения, а объект imageData.data
не имеет методов clone()
и slice()
для достижения нашей цели, поэтому мы используем это мудреное выражение чтобы полностью скопировать объект и все его свойства.
Информация о цвете хранится вот в таких данных:
[ ... ][ red ][ green ][ blue ][ alpha ][ red ][ green ][ blue ][ alpha ][ ... ]
Таким образом, каждый пиксель занимает четыре элемента массива - по одному для каждого канала. Наконец, после перебора всех пиксельных данных, мы используем putImageData()
, чтобы сохранить результат.
Шаг 22. Фильтры свертки: размытие.
Это простой эффект свертки, так что я решил позволить пользователю задать радиус размытия (что повлияет на размер массива), чтобы сделать фильтр более сложным. Вот функция, которую мы будем использовать:
filterBlur = function (radius) {
var matrix = [];
for (var y = 0; y < radius * 2; y++) {
matrix[y] = [];
for (var x = 0; x < radius * 2; x++) {
matrix[y][x] = 1;
}
}
applyFilter(new ConvolutionFilter(matrix, 1.0 / Math.pow(radius * 2, 2), 0.0));
hideDialog('#dialog-filterblur');
}
Это создает матрицу размытия, затем применяется фильтр.
Почему мы устанавливаем factor
на Math.pow(radius * 2, 2)
? Потому что, как я уже говорил ранее: сумма всех полей массива должна быть равна нулю или единице; если мы разделим их все на их сумму мы всегда будем получать 1.
Ниже показан результат применения этого фильтра:
Шаг 23. Размытие по Гауссу.
Это фильтр свертки, названный так, потому что он использует значения из стандартного распределения Гаусса (фото выше), вставленные в матрицу свертки. Для упрощения задачи мы позволяем пользователю выбрать только три значения радиуса и не более 3px, поскольку применение фильтра для большего радиуса займет слишком много времени на обработку (радиус 3px - уже матрица 7 на 7). Вот эта функция:
var gaussMatrix = [
[
[ 0.05472157, 0.11098164, 0.05472157 ],
[ 0.11098164, 0.22508352, 0.11098164 ],
[ 0.05472157, 0.11098164, 0.05472157 ]
],
[
[ 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633 ],
[ 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965 ],
[ 0.01330373, 0.11098164, 0.22508352, 0.11098164, 0.01330373 ],
[ 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965 ],
[ 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633 ]
],
[
[ 0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067 ],
[ 0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292 ],
[ 0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117 ],
[ 0.00038771, 0.01330373, 0.11098164, 0.22508352, 0.11098164, 0.01330373, 0.00038771 ],
[ 0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117 ],
[ 0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292 ],
[ 0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067 ]
]
];
filterGaussianBlur = function (radius) {
applyFilter(new ConvolutionFilter(gaussMatrix[radius], 1.0, 0.0));
hideDialog('#dialog-filtergaussianblur');
}
Зададим матрицы вне функции, чтобы не тратить время на присваивание каждый раз, когда пользователь выбирает радиус размытия из меню. В этой функции мы просто выбираем указанную матрицу и применяем фильтр. Вы, конечно, можете добавить большие значения радиуса, если хотите.
Ниже показан результат применения этого фильтра:
Шаг 24: Выделение краев
Фильтр Выделение краев выделяет границы цветовых и тоновых переходов, очерчивая их, а области сплошного цвета или плавного его изменения высветляет. В результате получается карандашная прорисовка изображения.
Для достижения этой цели мы используем приближение первых значений из распределения Лапласа (изображение выше) с b = 1/4
. All functions from this point will only have different matrices:
filterEdgeDetection = function () {
applyFilter(new ConvolutionFilter(
[
[ 0, -1, 0 ],
[ -1, 4, -1 ],
[ 0, -1, 0 ]
],
1.0,
0.0
));
hideDialog('#dialog-filteredgedetection');
}
Ниже показан результат применения этого фильтра:
Шаг 25: усиление контуров
Этот фильтр похож по сути на предыдущий, но он усиливает края без затемнения остальной части изображения, что делает его идеальным для художественного использования.
filterEdgeEnhance = function () {
applyFilter(new ConvolutionFilter(
[
[ 0, 0, 0 ],
[ -1, 1, 0 ],
[ 0, 0, 0 ]
],
1.0,
0.0
));
hideDialog('#dialog-filteredgeenhance');
}
Ниже показан результат применения этого фильтра:
Шаг 26: фильтр Рельеф
Фильтр Рельеф добавляет небольшой 3D-эффект к изображению, выделяя левые нижние углы границ (поэтому он также является фильтром определения границ).
filterEmboss = function () {
applyFilter(new ConvolutionFilter(
[
[ -1, -1, 0 ],
[ -1, 1, 1 ],
[ 0, 1, 1 ]
],
1.0,
0.0
));
hideDialog('#dialog-filteremboss');
}
Ниже показан результат применения этого фильтра:
Шаг 27: Резкость
Мы все знаем, что такое резкость. Она достигается небольшой корректировкой выделения границ:
filterSharpen = function () {
applyFilter(new ConvolutionFilter(
[
[ 0, -1, 0 ],
[ -1, 5, -1 ],
[ 0, -1, 0 ]
],
1.0,
0.0
));
hideDialog('#dialog-filtersharpen');
}
Ниже показан результат применения этого фильтра:
Вы видите разницу? Она небольшая в матрице свертки, но в результате изображение выглядит более резким.
Это был последний из фильтров, который мы поанироали разобрать вместе, но вы можете добавить больше, если вы хотите. Просто найдите некоторые в Интернете или экспериментируйте, чтобы создать свои собственные уникальные фильтры.
Шаг 28: обратные вызовы
Вот следующая группа вызовов jQuery, но прежде, чем мы займемся этим, нам понадобится вторая функция хелпера в этом файле:
filterSwitch = function (e, val, func) {
switch (e.type) {
case "click":
func(val);
break;
case "keydown":
if (e.keyCode == 13) func(val);
break;
}
}
Она принимает три параметра:
e
- объект события,
val
- значение, передаваемое функции
func
- функция, которая вызывается с предыдущим значением.
Это создает сокращенную запись, мы можем использовать следующий код обратного вызова; просто вставьте ее под хелпером:
app.callbacks.filterBrightness = function (e) {
var val = $('#dialog-filterbrightness input').val() / 100;
filterSwitch(e, val, filterBrightness);
}
app.callbacks.filterDesaturation = function () {
filterDesaturation();
}
app.callbacks.filterColorify = function (e) {
var r = $('#dialog-filtercolorify input.r').val() * 1,
g = $('#dialog-filtercolorify input.g').val() * 1,
b = $('#dialog-filtercolorify input.b').val() * 1,
a = $('#dialog-filtercolorify input.a').val() * 1;
switch (e.type) {
case "click":
filterColorify(r, g, b, a);
break;
case "keydown":
if (e.keyCode == 13) filterColorify(r, g, b, a);
break;
}
}
app.callbacks.filterBlur = function (e) {
var val = $('#dialog-filterblur input').val() * 1;
filterSwitch(e, val, filterBlur);
}
app.callbacks.filterGaussianBlur = function (e) {
var val = ($('#dialog-filtergaussianblur input.3').attr('checked') ? 2: $('#dialog-filtergaussianblur input.2').attr('checked') ? 1: 0);
filterSwitch(e, val, filterGaussianBlur);
}
app.callbacks.filterEdgeDetection = function (e) {
filterEdgeDetection();
}
app.callbacks.filterEdgeEnhance = function (e) {
filterEdgeEnhance();
}
app.callbacks.filterEmboss = function (e) {
filterEmboss();
}
app.callbacks.filterSharpen = function (e) {
filterSharpen();
}
Шаг 29: Скриптование (ведение)
Позволить пользователю использовать какой-нибудь язык сценариев в вашем приложении очень полезная возможность. Это позволит пользователю автоматизировать свою работу, или поделиться полученным эффектом с другим пользователем. А раз мы пишем все приложение на JavaScript, который сам по себе является языком сценариев, то нам ничего не мешает создать такую функцию.
Используя функцию eval()
, мы можем запустить JavaScript код из строки, и эта строка как раз и будет скриптом пользователя.
Вы, наверное, читали, что использование функции eval()
не рекомендуется на практике. Конечно, если вам надо использовать ее в вашем коде, это утверждение является верным, потому что эта функция отменяет любое кеширование или компиляцию, с помощью которых современные движки JavaScript разгоняют код. Она также создает дополнительный экземпляр парсера JavaScript, который зря занимает память, пока код не будет закончен. Поэтому избегайте подобного использования функцииeval()
:
var myEvilCondition = "someObjectPropertyButIDontKnowWhichOne";
function evilEval () {
return eval('myObject.' + myEvilCondition);
}
Честно говоря, код приведенный выше ужасен. Вы никогда не должны использовать eval()
таким образом. Приведенный выше пример может быть исправлен с помощью квадратных скобок:
var myNotEvilCondition = "someObjectPropertyButIDontKnowWhichOne";
function goodNoEval () {
return myObject[myNotEvilCondition];
}
В нашем случае все в порядке, поскольку создание собственного интерпретатора было бы напрасной тратой времени и ресурсов (это увеличило бы количество или размер файлов, загружаемых пользователями).
Шаг 30: Скриптование (безопасный eval)
Поскольку мы выполняем пользовательские скрипты, мы должны быть уверены, что он случайно не уничтожит результат, над которым он работал, из-за того, что он ошибся при вводе названия функций или номера слоев. Вот почему мы должены убедиться, что пользователь не может получить доступ к любым методам window
или app
напрямую. По этой причине, наша функция выглядит следующим образом (поместите этот код в файл scripts.js
):
scriptExecute = function (code) {
hideDialog('#dialog-executescript');
if ((code.match(/eval\(/g) != null) && (!confirm('Вы использовали eval функцию внутри вашего кода.' +
'Это может привести к неожиданным результатам. Хотите продолжить?'))) return;
eval(code.replace(/(window\.|app\.)(.*?);/g, ''));
}
Сначала мы спрячем диалог, иначе он так и будет висеть, пока не закончится скрипт, а пользователь, возможно, хочет увидеть этот скрипт в процессе работы. Потом мы вызываем соответствующий код, но заменяем при этом все вызовы, относящиеся к window
и app
, чтобы пользователь не мог удалить все слои или закрыть окно по ошибке.
Небольшое замечание: пользователь может сделать что-то с этими переменными, если он использует функцию eval()
- но тогда мы спросим его, действительно ли он хочет сделать это.
Теперь мы должны добавить небольшую функцию обратного вызова:
app.callbacks.scriptExecute = function (e) {
scriptExecute($('#dialog-executescript textarea').val());
}
Это все, что касается скриптов. Попробуйте и сами создать что-нибудь подобное, написав код для диалога 'Execute Script'. Если захотите использовать eval()
, система спросит вас, действительно ли вы собираетесь это сделать.
Заключение
Как вы можете видеть, HTML5 Canvas невероятно мощная вещь. Но мы лишь скользнули по поверхности того, что может быть сделано с ним. Мы создали действительно передовое приложение, которое позволяет пользователям загружать свои фотографии и делать некоторые изменения в них, а затем сохранить и распечатать отредактированную фотографию. И все это с использованием чистого JavaScript. Еще несколько лет назад такое было невозможным.
Также вы можете усовершенствовать приложение, которое вы только что создали! Добавьте несколько фильтров, поменяйте интерфейс, добавьте больше полезных функций (например, вы можете добавить дополнительные свойства, к слою, может быть, список всех фильтров с возможностью удаления и редактирования их или кнопку, которая будет конвертировать буфер отмененных действий в готовый для использования скрипт). Просто проявите воображение и, тогда вы будете создавать действительно полезные вещи!