Поскольку HTML5 становится все более популярным, все больше основных браузеров начинают поддерживать его API. Сегодня, используя API-интерфейсы Canvas и File, мы можем создать полноценный графический редактор с функциями на уровне некоторых настольных приложений. Для этого мы будем использовать библиотеку EaselJS . Он использует синтаксис, аналогичный AS3, поэтому его будет легко понять как программистам Flash, так и JavaScript.
Окончательный результат предварительного просмотра
Давайте посмотрим на конечный результат, к которому мы будем стремиться:
Поиграйте с ним, чтобы почувствовать, на что он способен. Возможно, вы даже захотите скачать полный исходный код и осмотреться, прежде чем приступить к изучению этого руководства.
Вступление
Из-за большого количества кода в этом уроке я собираюсь пройтись по каждому файлу и объяснить каждую часть по очереди, вместо того, чтобы помочь вам перестроить его с нуля. Я постараюсь все комментировать как можно больше, и я верю, что вы все поймете.
Шаг 1: Стиль
Я начну необычным образом с файлов CSS. Сначала создайте файл style.css
:
* {font-family: Calibri, Sans-serif; контур: нет; } body, html {margin: 0; отступы: 0; переполнение: скрытое; background: url (background.gif); } холст {ясно: оба; дисплей: блок; z-индекс: -1; } вход {фон: белый; граница: 1px сплошной черный радиус границы: 4 пикселя; обивка: 3px 5px; } input [type = text] {padding: 5px; } button {padding: 6px 10px; цвет: rgb (255, 255, 255); фон: rgba (0, 0, 0, 0.3); граница: 1px solid rgb (40, 40, 40); радиус границы: 4 пикселя; } button: hover, button.hover {background: rgba (0, 0, 0, 0.2); } button: active, button.active {background: rgba (0, 0, 0, 0.4); } ul # mainmenu {стиль списка: нет; обивка: 2px; поле: 0; плыть налево; фон: RGB (150, 150, 150); размер шрифта: 1,2em; ширина: 778 пикселей; } ul # mainmenu li {float: left; поле: 0; отступы: 0 2px 0 0; положение: относительное; } ul # mainmenu li button {float: left; } ul # mainmenu li ul.submenu {стиль списка: нет; положение: абсолютное; слева: -2px; верх: 34px; фон: RGB (150, 150, 150); поле: 0; отступы: 0; дисплей: нет; плыть налево; ширина: 170 пикселей; border-radius: 0 0 4px 4px; } ul # mainmenu li ul.submenu li {margin: 0; отступы: 0; ясно: оба; ширина: 170 пикселей; } ul # mainmenu li ul.submenu li button {ширина: 162px; выравнивание текста: слева; поле: 4px; отступ слева: 20 пикселей; } ul # mainmenu li ul.submenu li button: hover {background: rgba (0, 0, 0, 0.2); } ul # mainmenu li ul.submenu li button: active {background: rgba (0, 0, 0, 0.4); } ul # mainmenu li ul.submenu li hr {margin: 4px; border-top: 1px solid rgba (0, 0, 0, 0.4); Граница внизу: 1px Solid RGB (205, 205, 205) } div # overlay {background: rgba (0, 0, 0, 0.6); положение: фиксированное; ширина: 100%; высота: 100%; верх: 0; слева: 0; z-индекс: 1; дисплей: нет; } ul # layer {width: 232px; положение: фиксированное; справа: 0; верх: 37px; фон: RGB (150, 150, 150); border-top: 1px пунктирный RGB (100, 100, 100); стиль списка: нет; поле: 0; отступы: 0 5px 5px; переполнение-у: авто; переполнение-х: скрыто; } ul # layer li {margin-top: 5px; обивка: 5px; фон: RGB (180, 180, 180); радиус границы: 4 пикселя; } ul # layer li.active {background: rgb (160, 160, 160); обивка: 3px; граница: 2px пунктирная черная; } ul # layer li img {width: 42px; высота: 42px; плыть налево; обивка: 2px; цвет: rgb (255, 255, 255); фон: rgba (0, 0, 0, 0.3); граница: 1px solid rgb (40, 40, 40); радиус границы: 4 пикселя; } ul # layer li h1 {font-size: 16px; отступы: 0 5px; поле: 3px 0; ширина: 132 пикселя; переполнение: скрытое; пустое пространство: nowrap; переполнение текста: многоточие; } ul # layer li span {padding: 0 5px; поле: 3px 0; ширина: 132 пикселя; } ul # layer li span button {padding: 3px 5px; поле: 0 3px 0 0; } button # button-move, # button-select, # button-text {background-repeat: no-repeat; background-position: 50% 50%; обивка: 6px 15px; } button # button-move {background-image: url (move.gif); } button # button-text {background-image: url (text.gif); } button # button-openfile input, button # button-importfile input {position: относительный; слева: -23px; вверху: -8px; ширина: 162 пикселя; непрозрачность: 0; } div # dialog-tooltext select {padding: 4px; ширина: 173 пикселей; цвет: rgb (0, 0, 0); граница: 1px solid rgb (40, 40, 40); радиус границы: 4 пикселя; } div.dialog {border: 1px solid black; фон: RGB (240, 240, 240); положение: фиксированное; радиус границы: 4 пикселя; z-индекс: 2; дисплей: нет; обивка: 40px; } div # cropoverlay {position: fixed; слева: 0; верх: 0; z-индекс: 2; фон: rgba (255, 255, 255, 0,15); ширина: 120 пикселей; высота: 120 пикселей; граница: 1px пунктирная черная; радиус границы: 0; отступы: 0; дисплей: нет; } div # cropoverlay div {width: 20px; высота: 20 пикселей; положение: абсолютное; z-индекс: 1000; справа: 0; низ: 0; граница сверху: 1px сплошной черный; рамка слева: 1px сплошной черный; } .ui-resizable {позиция: относительная;} .ui-resizable-handle {position: absolute; размер шрифта: 0,1px; z-index: 99999; дисплей: блок; } .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle {display: none; } .ui-resizable-se {курсор: se-resize; ширина: 12 пикселей; высота: 12 пикселей; справа: 1px; внизу: 1px; }
В первой строке мы меняем шрифт и отключаем контуры вокруг элементов. Далее есть только определения стилей: ul#mainmenu
— это элемент главного меню, div#overlay
— это заливка под всеми диалогами, а ul#layers
— это панель «Слои», которая будет отображаться в правой части холста. Затем мы определяем стиль для кнопок инструментов, и, наконец, у нас есть фрагмент стиля jQuery-UI, потому что эта часть нам понадобится для диалога обрезки слоя.
Затем следует файл print.css
который содержит только две строки, чтобы скрыть все, кроме холста, при печати изображения (этот стиль применяется только при печати страницы из-за ее объявления в файле HTML ).
тело * {видимость: скрыто; } холст {видимость: видимый; положение: абсолютное; верх: 0; слева: 0; }
Первая строка скрывает все элементы внутри секции body
, а вторая делает видимым только холст (а также выровненный по верхнему левому углу). Это потому, что когда кто-то хочет напечатать изображение, он обычно не хочет печатать интерфейс.
Шаг 2: структура HTML
Вы должны иметь общее представление об интерфейсе, взглянув на файлы CSS выше. Теперь создайте файл index.html
и введите следующие строки:
<! DOCTYPE html> <HTML> <Голова> <! - Стили CSS -> <link rel = "stylesheet" type = "text / css" href = "style.css" /> <link rel = "stylesheet" type = "text / css" media = "print" href = "print.css" /> <! - Выбор jQuery & jQuery-UI + перетаскивание -> <script type = "text / JavaScript" src = "jquery-1.7.1.min.js"> </ script> <script type = "text / JavaScript" src = "jquery-ui-1.8.18.custom.min.js"> </ script> <! - Интерфейс EaselJS Canvas -> <script type = "text / JavaScript" src = "easel.js"> </ script> <! - Встроенные фильтры EaselJS -> <script type = "text / JavaScript" src = "ColorFilter.js"> </ script> <script type = "text / JavaScript" src = "ColorMatrixFilter.js"> </ script> <! - Основная структура приложения -> <script type = "text / JavaScript" src = "main.js"> </ script> <! - Пользовательский интерфейс -> <script type = "text / JavaScript" src = "ui.js"> </ script> <! - Меню Файл -> <script type = "text / JavaScript" src = "file.js"> </ script> <! - Инструменты -> <script type = "text / JavaScript" src = "tools.js"> </ script> <! - Преобразования слоя -> <script type = "text / JavaScript" src = "layer.js"> </ script> <! - Преобразования изображения -> <script type = "text / JavaScript" src = "image.js"> </ script> <! - Пользовательские фильтры -> <script type = "text / JavaScript" src = "ConvolutionFilter.js"> </ script> <script type = "text / JavaScript" src = "filters.js"> </ script> <! - Простая система сценариев -> <script type = "text / JavaScript" src = "scripts.js"> </ script> </ HEAD> <Тело> <! - Тень для всех диалогов -> <div id = "overlay"> </ div> <! - Элемент слоя обрезки -> <div id = "cropoverlay" class = "dialog"> <DIV> </ DIV> <button style = "position: absolute; top: -33px;" class = "button-ok"> Crop </ button> <button style = "position: absolute; top: -33px; слева: 50px;" класс = "Кнопка-отменить"> Отмена </ кнопка> </ DIV> <! - Различные диалоги -> <div id = "dialog-openurl" class = "dialog"> Пожалуйста, введите URL, чтобы открыть: <br> <input type = "text" style = "width: 350px;" /> <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-scale" class = "dialog"> Установить масштаб: <br> X: <input class = "input-scaleX" type = "text" style = "width: 50px; text-align: right;" значение = "100" />% Y: <input class = "input-scaleY" type = "text" style = "width: 50px; text-align: right;" значение = "100" />% <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-rotate" class = "dialog"> Поворот: <br> <input type = "text" style = "width: 50px; text-align: right;" значение = "0" /> ° <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-skew" class = "dialog"> Косые: <br> X: <input class = "input-skewX" type = "text" style = "width: 50px; text-align: right;" значение = "100" /> ° Y: <input class = "input-skewY" type = "text" style = "width: 50px; text-align: right;" значение = "100" /> ° <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-layerrename" class = "dialog"> Переименовать слой: <br> <input type = "text" style = "width: 350px;" /> <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-tooltext" class = "dialog"> Добавить текстовый слой: <br> Шрифт: <Выберите> <option value = "Calibri"> Calibri </ option> <option value = "Times New Roman"> Times New Roman </ option> <option value = "Courier New"> Courier New </ option> </ Выберите> Размер: <input type = "text" class = "input-size" style = "width: 50px" value = "12px" /> Цвет: <input type = "text" class = "input-color" style = "width: 70px; фон: черный; цвет: silver;" значение = "черный" /> <br> <input type = "text" class = "input-text" style = "width: 318px" /> <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-filterbrightness" class = "dialog"> Установите яркость: <br> <input type = "text" style = "width: 50px;" />% <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-filterblur" class = "dialog"> Радиус размытия: <br> <input type = "text" style = "width: 50px;" /> px <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-filtercolorify" class = "dialog"> Colorify: <br> R: <input class = "r" type = "text" style = "width: 30px;" /> G: <input class = "g" type = "text" style = "width: 30px;" /> B: <input class = "b" type = "text" style = "width: 30px;" /> A: <input class = "a" type = "text" style = "width: 30px;" /> <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-filtergaussianblur" class = "dialog"> Радиус размытия: <br> <input type = "radio" class = "7" name = "radius" /> 3 пикселя <input type = "radio" class = "5" name = "radius" /> 2 пикселя <input type = "radio" class = "3" name = "radius" /> 1 пикс. & nbsp; <button class = "button-ok"> ОК </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <div id = "dialog-executetescript" class = "dialog"> Выполнить скрипт: <br> <textarea style = "width: 350px; высота: 200px;"> </ textarea> <br> <button class = "button-ok"> Выполнить </ button> <button class = "button-cancel"> Отмена </ button> </ DIV> <! - Главное меню -> <ul id = "mainmenu"> <Li> Кнопка <> Файл </ кнопка> <ul class = "submenu"> <li> <button id = "button-openfile"> <input type = "file" /> <span style = "margin-top: -32px; display: block;"> Открыть файл </ span> </ button> </ li> <li> <button id = "button-openurl"> Открыть URL-адрес </ button> </ li> <Литий> <ч /> </ литий> <li> <button id = "button-importfile"> <input type = "file" множественный = "true" /> <span style = "margin-top: -32px; display: block;"> Импортировать файл </ span > </ Button> </ li> <li> <button id = "button-importurl"> Импортировать URL-адрес </ button> </ li> <Литий> <ч /> </ литий> <li> <button id = "button-save"> Сохранить </ button> </ li> <li> <button id = "button-print"> Печать </ button> </ li> </ UL> </ Li> <Li> Кнопка <> Изменить </ кнопка> <ul class = "submenu"> <li> <button id = "button-undo"> Отменить </ button> </ li> <li> <button id = "button-redo"> Повторить </ button> </ li> </ UL> </ Li> <Li> Кнопка <> Слой </ Кнопка> <ul class = "submenu"> <li> <button id = "button-layercrop"> Обрезать </ button> </ li> <li> <button id = "button-layercale"> Масштаб </ button> </ li> <Литий> <ч /> </ литий> <li> <button id = "button-layerrotate"> Повернуть </ button> </ li> <li> <button id = "button-layerkew"> Наклон </ button> </ li> <li> <button id = "button-layerflipv"> Отразить по вертикали </ button> </ li> <li> <button id = "button-layerfliph"> Отразить по горизонтали </ button> </ li> </ UL> </ Li> <Li> Кнопка <> Изображение </ кнопка> <ul class = "submenu"> <li> <button id = "button-imagescale"> Масштаб </ button> </ li> <Литий> <ч /> </ литий> <li> <button id = "button-imagerotate"> Повернуть </ button> </ li> <li> <button id = "button-imageskew"> Наклон </ button> </ li> <li> <button id = "button-imageflipv"> Отразить по вертикали </ button> </ li> <li> <button id = "button-imagefliph"> Отразить по горизонтали </ button> </ li> </ UL> </ Li> <Li> <Кнопка> Фильтры </ кнопка> <ul class = "submenu"> <li> <button id = "button-filterbrightness"> Яркость </ button> </ li> <li> <button id = "button-filtercolorify"> Colorify </ button> </ li> <li> <button id = "button-filterdesaturation"> Desaturation </ button> </ li> <Литий> <ч /> </ литий> <li> <button id = "button-filterblur"> Размытие </ button> </ li> <li> <button id = "button-filtergaussianblur"> Размытие по Гауссу </ button> </ li> <li> <button id = "button-Filtergedetection"> Обнаружение края </ button> </ li> <li> <button id = "button-Filtergeenhance"> Edge Enhance </ button> </ li> <li> <button id = "button-filteremboss"> Тиснение </ button> </ li> <li> <button id = "button-filtersharpen"> Резкость </ button> </ li> </ UL> </ Li> <Li> <button id = "button-executetescript"> Выполнить скрипт </ button> </ Li> <li style = "float: right;"> <button id = "button-text" /> </ li> <li style = "float: right;"> <button id = "button-move" /> </ li> <li style = "float: right;"> <button id = "button-select" class = "active" /> </ li> </ UL> <! - Панель правого слоя -> <ul id = "layer"> </ ul> <! - Холст для рисования -> <Холст /> </ Body> </ Html>
Обратите внимание на спецификацию HTML5 типа документа. Нет бесполезных длинных спецификаций DTD; просто слово html
.
Вверху мы связываем необходимые библиотеки и файлы JS. Все они включены в исходный код загрузки ; конкретные библиотеки — это EaselJS , jQuery и jQuery UI .
(Для ознакомления с EaselJS ознакомьтесь с данным руководством .)
Далее мы строим всю структуру пользовательского интерфейса. Просто помните: каждый div
с классом dialog
— это просто диалог для пользователя, чтобы ввести данные, необходимые для выполнения какой-либо операции над изображением. Если вы запустите этот код в своем браузере сейчас, вы заметите несколько 404 ошибок в консоли и то, что меню не будет работать, но мы исправим это, когда создадим файл ui.js
Шаг 3: Основной объект приложения
Хорошей практикой является объединение всех функций и переменных, связанных с вашим приложением, в один объект, чтобы предотвратить их переопределение внешними библиотеками или даже вашими собственными сценариями. Наш объект будет выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
app = {
stage: null,
canvas: null,
layers: [],
tool: TOOL_SELECT,
callbacks: {},
selection: {
x: -1, y: -1
},
renameLayer: 0,
undoBuffer: [],
redoBuffer: []
}
|
(Я начинаю все имена переменных и функций с маленькой буквы, основываясь на условных обозначениях Дугласа Крокфорда для языка программирования JavaScript .)
В app.stage
мы будем хранить ссылку на объект Stage
для нашего приложения. Если вы что-то кодировали в ActionScript, подумайте об этой стадии как об AS3. Он имеет список отображения, который рисуется в элементе canvas при каждом обновлении. app.canvas
является переменной со ссылкой на элемент canvas внутри нашего HTML-документа. Мы будем использовать его для создания сцены и изменения ее размера вместе с окном.
Массив app.layers
будет содержать все слои изображения, а app.tool
содержит значение фактически выбранного инструмента. В app.callbacks
будет содержаться каждый обратный вызов события, который нам нужно будет указать (например, нажав кнопку меню), app.renameLayer
содержит номер фактически переименованного слоя, а app.undoBuffer
и app.redoBuffer
— это массивы для хранения резервной копии app.layers
состояние app.layers
чтобы заставить работать функции отмены и возврата.
Вам также нужно будет добавить эти четыре строки перед определением app
(это просто константы идентификатора инструмента):
1
2
3
4
|
const
TOOL_MOVE = 0,
TOOL_SELECT = 1,
TOOL_TEXT = 2;
|
Шаг 4: Полезные методы
Теперь мы определим методы этого объекта. Сначала добавьте следующие refreshLayers()
и sortLayers()
:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
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: tx — e1.stageX,
y: ty — e1.stageY
}
if (self.tool == TOOL_MOVE) self.addUndo();
e1.onMouseMove = function (e2) {
if (self.tool == TOOL_MOVE) {
tx = offset.x + e2.stageX;
ty = 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»>Delete</button><button class=»button-hide»>’ + (layer.visible ? ‘Hide’: ‘Show’) + ‘</button><button class=»button-rename»>Rename</button>
}
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();
}
|
Обратите внимание, что внутри объекта вам нужно объявлять переменные и функции с помощью «:» вместо «=».
Метод sortLayers()
вызывается, когда пользователь перетаскивает слои на панели «Слои»; refreshLayers()
вызывается очень часто, потому что он воссоздает app.stage
и app.stage
добавляет все слои на сцену, устанавливая их свойства и применяя обратные вызовы событий. Эти обратные вызовы позволяют перемещать слои и редактировать текст на текстовых слоях. Это очень важная функция, поскольку она также добавляет все слои на панель «Слои» в пользовательском интерфейсе и отключает кнопки инструментов в меню (если слоев нет), а также включает их (при наличии хотя бы одного слоя).
Перед refreshLayers()
вставьте другой набор вспомогательных функций (не забудьте добавить запятую после последней!):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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
и перед теми, которые вы добавили туда ранее:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
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()
, метод app.layers
toString()
переопределяется кодом, который подготавливает строковую версию всех слоев внутри. Конечно, хранить всю информацию о слоях было бы пустой тратой памяти, поэтому резервное копирование выполняется только для тех значений, которые могут быть изменены приложением.
Метод addUndo()
фактическое состояние слоев в массив app.undoBuffer
и очищает app.redoBuffer
— потому что, когда вы делаете действие, которое можно отменить, вы не можете восстановить все, что было отменено до этого действия. loadLayers()
принимает два аргумента (массив, из которого мы должны app.layers
состояние app.layers
и массив, в который мы должны app.layers
фактическое состояние этой переменной), и выполняет анализ резервных app.layers
.
Как показывает пример фильтра EaselJS:
«… фильтры отображаются только при кэшировании экранного объекта …»
(Из примеров EaselJS: фильтры .)
Это означает, что вам нужно вызвать метод cache()
Bitmap
чтобы применить фильтр. Кэширование выполняется для повышения производительности — фильтр применяется только один раз, и отрисовывается только отфильтрованное растровое изображение. EaselJS кеширует контент очень умным способом — он просто копирует его в другой элемент canvas, который не добавляется в документ (он скрыт). Я упоминаю об этом, потому что в конце метода loadLayers()
есть блок if
который проверяет, есть ли какие-либо фильтры, которые должны быть обновлены на этом слое — и, если они есть, обновляет элемент cache или cache
Шаг 5: Инициализация
Инициализация всего приложения проста; просто вставьте это после объявления app
:
01
02
03
04
05
06
07
08
09
10
11
12
|
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()
для перерисовки app.stage.update()
области.
В начале (сразу после загрузки документа) мы назначаем первый элемент canvas на странице, который функция $
( jQuery
) находит для app.canvas
, затем мы отключаем выбор чего-либо в документе (потому что в противном случае при перетаскивании мыши через холст есть эффект, как будто вы выбираете текст).
Мы устанавливаем скорость тикера в 30 (вам нужно всего 24 кадра в секунду, чтобы заставить человека думать, что он видит движение) и устанавливаем окно в качестве слушателя тикера.
Шаг 6: вспомогательные функции пользовательского интерфейса
Теперь пришло время воплотить в жизнь наше меню и весь пользовательский интерфейс. Файл ui.js
будет почти полностью состоять из функций jQuery, поэтому его действительно легко понять. Начнем с вспомогательных функций:
01
02
03
04
05
06
07
08
09
10
11
12
|
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()
и hideDialog()
говорят сами за себя — хотя одна интересная вещь в функции hideDialog()
— это то, как она проверяет, все ли диалоги скрыты с помощью псевдокласса jQuery ‘: visible’, только чтобы затем скрыть наложение. В конце концов, это оказалось бесполезным, потому что нет ситуации, когда на экране более одного диалога, но я оставил его для вашего будущего использования; Может быть, это пригодится.
Шаг 7: измени размер сцены
Теперь мы должны что-то сделать, когда пользователь изменит размер окна браузера. Это когда событие изменения размера window
вступает в игру. Он запускается каждый раз, когда пользователь изменяет размер окна браузера:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
$(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’
});
}
});
|
Сначала мы центрируем все диалоги с помощью функции each()
jQuery. Это вызывает обратный вызов для каждого элемента, которому соответствует селектор в функции $
.
Затем мы должны установить ширину и высоту холста — но не в CSS, потому что это растянет изображения внутри холста, а мы этого не хотим. Высота меню составляет 37 пикселей, поэтому мы устанавливаем высоту холста равной высоте окна минус 37 пикселей. То же самое для ширины, но на этот раз мы должны вычесть ширину панели слоев, которая составляет 232 пикселей. Мы также изменяем размеры меню и панели «Слои», чтобы соответствовать размеру окна (здесь мы можем использовать CSS).
После этого нам нужно обновить слои, чтобы они всегда были актуальными при изменении размера окна. Последнее, что нужно сделать, это переместить диалог обрезки, если пользователь изменил размер окна при обрезке слоя.
Шаг 8: связывая все вместе
Кнопки меню должны быть привязаны к обратным app.callbacks
, указанным в app.callbacks
, а также нам нужно привязать событие keydown
для входов и click
кнопки диалога. Последнее предложение может показаться сложным, но когда вы увидите код, оно станет понятным:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
|
$(document).ready(function () {
$(«ul#mainmenu li button»).click(function () {
$(this).focus();
$(this).parent().find(«ul.submenu:visible»).slideUp(‘fast’).show();
$(this).parent().find(«ul.submenu:hidden»).slideDown(‘fast’).show();
});
$(«ul#mainmenu li button»).blur(function () {
$(this).parent().find(«ul.submenu:visible»).delay(100).slideUp(‘fast’).show();
});
$(‘#button-openfile’).hover(
function () { $(this).addClass(‘hover’);
function () { $(this).removeClass(‘hover’);
);
$(‘#button-importfile’).hover(
function () { $(this).addClass(‘hover’);
function () { $(this).removeClass(‘hover’);
);
$(‘#button-openurl’).click(function () {
importFile = false;
showDialog(‘#dialog-openurl’);
$(‘#dialog-openurl input’).val(»).attr(‘disabled’, false).focus();
});
$(‘#button-importurl’).click(function () {
importFile = true;
showDialog(‘#dialog-openurl’);
$(‘#dialog-openurl input’).val(»).attr(‘disabled’, false).focus();
});
$(‘#button-undo’).click(function () { app.undo(); });
$(‘#button-redo’).click(function () { app.redo(); });
$(‘#button-layerscale’).click(function () {
affectImage = false;
showDialog(‘#dialog-scale’);
$(‘#dialog-scale input.input-scaleX’).val(‘100’);
$(‘#dialog-scale input.input-scaleY’).val(‘100’);
});
$(‘#button-layerskew’).click(function () {
affectImage = false;
showDialog(‘#dialog-skew’);
$(‘#dialog-skew input.input-scaleX’).val(‘100’);
$(‘#dialog-skew input.input-scaleY’).val(‘100’);
});
$(‘#button-layerrotate’).click(function () {
affectImage = false;
showDialog(‘#dialog-rotate’);
$(‘#dialog-rotate input’).val(‘0’);
});
$(‘#button-layercrop’).click(function () {
affectImage = false;
app.sortLayers();
app.refreshLayers();
var layer = app.getActiveLayer();
$(‘#overlay’).show();
$(‘#cropoverlay’).css({
left: Math.ceil(app.canvas.width / 2 + layer.x — layer.regX — 1) + ‘px’,
top: Math.ceil(app.canvas.height / 2 + layer.y — layer.regY + 38) + ‘px’,
width: (layer.text != null ? layer.getMeasuredWidth() * layer.scaleX: layer.image.width * layer.scaleX) + 2 + ‘px’,
height: (layer.text != null ? layer.getMeasuredLineHeight() * layer.scaleY: layer.image.height * layer.scaleY) + 2 + ‘px’
}).show();
});
$(‘#button-layerflipv’).click(app.callbacks.layerFlipV);
$(‘#button-layerfliph’).click(app.callbacks.layerFlipH);
$(‘#button-imagescale’).click(function () {
affectImage = true;
showDialog(‘#dialog-scale’);
$(‘#dialog-scale input.input-scaleX’).val(‘100’);
$(‘#dialog-scale input.input-scaleY’).val(‘100’);
});
$(‘#button-imageskew’).click(function () {
affectImage = true;
showDialog(‘#dialog-skew’);
$(‘#dialog-skew input.input-scaleX’).val(‘100’);
$(‘#dialog-skew input.input-scaleY’).val(‘100’);
});
$(‘#button-imagerotate’).click(function () {
affectImage = true;
showDialog(‘#dialog-rotate’);
$(‘#dialog-rotate input’).val(‘0’);
});
$(‘#button-imageskew’).click(function () {
affectImage = true;
showDialog(‘#dialog-skew’);
$(‘#dialog-skew input.input-skewX’).val(‘0’);
$(‘#dialog-skew input.input-skewY’).val(‘0’);
});
$(‘#button-filterbrightness’).click(function () {
showDialog(‘#dialog-filterbrightness’);
$(‘#dialog-filterbrightness input’).val(‘100’);
});
$(‘#button-filtercolorify’).click(function () {
showDialog(‘#dialog-filtercolorify’);
$(‘#dialog-filtercolorify input’).val(‘0’);
});
$(‘#button-filterblur’).click(function () {
showDialog(‘#dialog-filterblur’);
$(‘#dialog-filterblur input’).val(‘1’);
});
$(‘#button-filtergaussianblur’).click(function () {
showDialog(‘#dialog-filtergaussianblur’);
$(‘#dialog-filtergaussianblur input.7’).attr(‘checked’, true);
});
$(‘#button-executescript’).click(function () {
showDialog(‘#dialog-executescript’);
$(‘#dialog-executescript textarea’).val(»);
});
$(‘#button-select’).click(function () {
app.tool = TOOL_SELECT;
$(‘#mainmenu button’).removeClass(‘active’);
$(this).addClass(‘active’);
});
$(‘#button-move’).click(function () {
app.tool = TOOL_MOVE;
$(‘#mainmenu button’).removeClass(‘active’);
$(this).addClass(‘active’);
});
$(‘#button-text’).click(function () {
app.tool = TOOL_TEXT;
$(‘#mainmenu button’).removeClass(‘active’);
$(this).addClass(‘active’);
});
$(‘#button-imageflipv’).click(app.callbacks.imageFlipV);
$(‘#button-imagefliph’).click(app.callbacks.imageFlipH);
$(‘#dialog-openurl input’).keydown(app.callbacks.openURL);
$(‘#dialog-openurl button.button-ok’).click(app.callbacks.openURL);
$(' #dialog-scale input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.layerScale); $(' #dialog-scale button.button-ok').click(app.callbacks.layerScale); $(' #button-openfile input').change(app.callbacks.openFile); $(' #button-importfile input').change(app.callbacks.importFile); $(' #dialog-tooltext button.button-ok').click(app.callbacks.toolText); $(' #dialog-tooltext input').keydown(app.callbacks.toolText); $(' #dialog-layerrename button.button-ok').click(app.callbacks.layerRename); $(' #dialog-layerrename input').keydown(app.callbacks.layerRename); $(' #dialog-rotate button.button-ok').click(app.callbacks.layerRotate); $(' #dialog-rotate input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.layerRotate); $(' #dialog-skew button.button-ok').click(app.callbacks.layerSkew); $(' #dialog-skew input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.layerSkew); $(' #cropoverlay button.button-ok').click(app.callbacks.layerCrop); $(' #button-filterdesaturation').click(app.callbacks.filterDesaturation); $(' #button-filteredgedetection').click(app.callbacks.filterEdgeDetection); $(' #button-filteredgeenhance').click(app.callbacks.filterEdgeEnhance); $(' #button-filteremboss').click(app.callbacks.filterEmboss); $(' #button-filtersharpen').click(app.callbacks.filterSharpen); $(' #dialog-filterbrightness button.button-ok').click(app.callbacks.filterBrightness); $(' #dialog-filterbrightness input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterBrightness); $(' #dialog-filtergaussianblur button.button-ok').click(app.callbacks.filterGaussianBlur); $(' #dialog-filtergaussianblur input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterGaussianBlur); $(' #dialog-filterblur button.button-ok').click(app.callbacks.filterBlur); $(' #dialog-filterblur input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterBlur); $(' #dialog-filtercolorify button.button-ok').click(app.callbacks.filterColorify); $(' #dialog-filtercolorify input').keydown(app.callbacks.numberOnly).keydown(app.callbacks.filterColorify); $(' #dialog-executescript button.button-ok').click(app.callbacks.scriptExecute); $(' #button-save').click(app.callbacks.saveFile); $(' #button-print').click(app.callbacks.printFile); $(' #dialog-tooltext input.input-color').keyup(function (e) { $( this ).css({ backgroundColor: $( this ).val() }); });
$('ul #layers li').live('click', function () { app.activateLayer($( this ).attr('id ').replace(' layer- ', ' ') * 1); });
$(' ul #layers li button.button-delete').live('click', function () { app.layers.splice($( this ).parent().parent().attr('id ').replace(' layer- ', ' ') * 1, 1); this.undoBuffer = []; this.redoBuffer = []; app.refreshLayers(); });
$(' ul #layers li button.button-hide').live('click', function () { if ($( this ).text() == 'Hide ') { app.layers[$(this).parent().parent().attr(' id ').replace(' layer- ', ' ') * 1].visible = false; } else {
app.layers[$(this).parent().parent().attr(' id ').replace(' layer- ', ' ') * 1].visible = true; }
app.refreshLayers(); });
$(' ul #layers li button.button-rename').live('click', function () { $(' #dialog-layerrename').show(); $(' #overlay').show(); $(' #dialog-layerrename input').val(''); app.renameLayer = $( this ).parent().parent().attr('id ').replace(' layer- ', ' ') * 1; });
$(document).keydown(function (e) { if (e.keyCode == 27) { hideDialog(' .dialog '); }
});
$(' .dialog button.button-cancel ').each(function () { $(this).click(function () { hideDialog($(this).parent()); });
});
$(' canvas ').click(app.callbacks.toolText); $(' #cropoverlay').draggable().resizable({ handles: 'se ', resize: function (e, ui) { $(' #cropoverlay').css({ left: ui.position.left + 'px', top: ui.position.top + 'px' }); },
stop: function (e, ui) { $(' #cropoverlay').css({ left: ui.position.left + 'px', top: ui.position.top + 'px' }); }
});
$(window).resize(); });
|
Помните еще одну вещь: когда вы что-то делаете с jQuery и любыми элементами HTML, делайте это внутри document.ready
обратного вызова, потому что только тогда вы можете быть уверены, что все элементы, которые вы используете, уже отрисованы.
Приведенный выше код является длинным, но это потому, что есть много частей, которые похожи, но отличаются по частям, которые не позволяют нам обернуть его в какую-либо вспомогательную функцию. Выделенная часть — это место, где мы устанавливаем все обратные вызовы для диалоговых кнопок и входов.
Далее вы должны взглянуть на live()
функцию, которую мы используем для привязки событий щелчка к кнопкам слоя (на панели «Слой») — live()
функция добавляет обратный вызов для каждого элемента, который будет соответствовать этому селектору в будущем , что делает его очень полезным, так как мы генерируем новый список слоев на каждый app.refreshLayers()
звонок.
Последняя функция здесь, $(window).resize()
которая вручную запускает resize
событие окна. Вот почему важно связывать скрипты по порядку, потому что, если они ui.js
были добавлены в HTML раньше main.js
, слои будут обновляться до определения функции, что иногда может привести к неожиданным результатам, что еще больше затрудняет поиск ошибки.
Если вы запустите приложение сейчас, вы увидите хорошо работающее меню и правильное изменение размера пользовательского интерфейса, но все равно ни одна кнопка не сделает ничего, кроме выдачи ошибок в консоль при нажатии на нее.
Шаг 9: Открытие файлов
Теперь мы будем использовать другой API из спецификации HTML5: File API. Это позволяет нам открывать файлы с компьютера пользователя, но только тогда, когда он выбирает их в поле ввода файлов ОС (чтобы веб-приложения не могли украсть ваши личные данные).
Обратите внимание, что если вы будете запускать это приложение на локальном компьютере, вам необходимо настроить локальный сервер или добавить --allow-file-access-from-files
параметр при запуске Chrome, поскольку открытие файлов из локальных веб-страниц по умолчанию отключено.
В file.js
файл мы также поместим функции для сохранения и печати изображения, поэтому давайте начнем с этих четырех помощников:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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
Функция будет использоваться , чтобы открыть изображение и добавить его к слоям. Если мы выберем «Открыть файл» из меню, то старое содержимое будет стерто, а «Импорт файла» добавит новые слои к изображению. (В функции, если первый параметр, true
то мы открываем файл, в противном случае мы его импортируем).
openURL
открывает файл, используя ту же функцию, но из внешнего источника (и, насколько мне известно, Chrome отключает доступ к данным междоменных исходных пикселей, что делает эти изображения довольно бесполезными). Поскольку мы не можем сохранить файл на диске пользователя, мы просто открываем другое окно, содержащее только изображение, представляющее фактическую сцену; пользователь может затем щелкнуть правой кнопкой мыши, чтобы сохранить изображение.
Печать достигается по телефону window.print()
. Конечно, мы не можем печатать что-либо, не сообщая об этом пользователю, поэтому эта функция откроет диалоговое окно печати по умолчанию, где пользователь может выбрать параметры печати.
Теперь мы определим несколько обратных вызовов для кнопок внутри меню:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
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
обратном вызове first ( ) мы проверяем, является ли открытый файл изображением, и останавливаем его, если это не так. Затем мы создаем новый FileReader
, устанавливаем его onload
обратный вызов, чтобы открыть файл, и вызываемreadAsDataURL(file)
метод, который загружает файл и выводит результат в виде URL-адреса данных для чтения.
(Также обратите внимание, что мы очищаем массивы отмены и возврата; мы должны сделать это, потому что мы не можем восстановить изображение, если мы его удалим — пользователь должен вручную повторно выбрать файл из ввода.)
Сохраните файл, откройте приложение в своем браузере, и вы, наконец, сможете что-то сделать! Немного, но если вы делаете это в первый раз, возможно, очень интересно иметь возможность загружать некоторые изображения в браузер, даже если вы можете только перемещать их.
Шаг 10: текстовые слои
Теперь, когда вы можете открывать и импортировать изображения, вы можете добавить текст. Есть одна действительно полезная вещь с канвой — вы определяете текст так же, как в атрибуте шрифта CSS. И EaselJS полностью использует эту функцию.
Мы определим инструмент Text в tools.js
файле. Добавьте следующие строки:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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
одинаково; отличаются только их свойства.
Вторая функция — обратный вызов. Первое, что нам нужно сделать, это проверить, какой тип события мы получаем (потому что мы используем только один обратный вызов как для обработки нажатия кнопки, так и нажатия клавиши ввода внутри ввода). Тогда мы просто вызываем предыдущего помощника.
Шаг 11: Преобразование слоя
Простые преобразования слоев действительно просты с EaselJS. Все, что я покажу здесь, можно сделать, просто изменив свойства слоя ( Bitmap
или Text
).
Я начну с объяснения точки регистрации. В regX
и regY
свойства определяют точку , из которой рассчитываются поворот и положение — это как ручка. В main.js
файле мы устанавливаем эту точку в центр изображения, чтобы упростить преобразование слоев. Все функции преобразования слоя будут помещены в layer.js
файл.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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*()
функции.
Теперь поместите код обратного вызова в файл:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
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.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. Мы также проверяем тип события, потому что мы можем получить эти функции, вызываемые кнопкой или полем ввода. Каждая функция в приведенном выше коде практически одинакова: проверьте тип события, получите входное значение из диалога и вызовите функцию.
Обратный вызов в верхней части этой части кода привязан к входам, которые должны получать только цифры внутри них.
Шаг 12: Преобразования: Масштаб
Давайте начнем с добавления этого исходного кода; Я объясню это позже. Поместите код ниже в image.js
файл:
01
02
03
04
05
06
07
08
09
10
|
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
файл:
01
02
03
04
05
06
07
08
09
10
11
12
|
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
библиотеке принимают радианы в качестве параметров, а один радиан равен точно pi / 180 градусам).
Шаг 14: Преобразования: перекос
Перекос очень похож на вращение, потому что в основном это вращение, но с двумя разными углами для двух направлений. Посмотрите на код:
01
02
03
04
05
06
07
08
09
10
11
12
13
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
для позиции х и rady
у позиции. Это делает изображение искаженным должным образом (я должен сказать, что это дает довольно хороший эффект).
Шаг 15: Трансформации: Отразить
Это модификация imageScale()
функции. Он не последний, потому что это самая сложная из функций преобразования изображений, просто потому, что он находится на последней позиции в меню. Код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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
. Они изменяют изображение попиксельно, поэтому с большими изображениями и сложными фильтрами вы можете на некоторое время задержать браузер или даже полностью остановиться.
Прежде чем применять их, я объясню, что делает каждый фильтр.
ColorFilter
принимает восемь параметров при создании:
1
|
new ColorFilter(redMultiplier, greenMultiplier, blueMultiplier, alphaMultiplier, redOffset, greenOffset, blueOffset, alphaOffset); |
Когда фильтр применяется, он разбивает изображение на четыре канала (красный, зеленый, синий и альфа) и для каждого канала умножает каждое значение на соответствующий множитель и добавляет соответствующее смещение. (На самом деле, изображение на самом деле не разделено; это удобная метафора.)
ColorMatrixFilter
принимает только один параметр при создании:
1
|
new ColorMatrixFilter(matrix); |
Матрица имеет следующий формат:
[ р-р, рг, рб, ра, ро, gr, gg, gb, ga, иди, br, bg, bb, ba, bo, ар, аг, аа, аа, ао ]
Когда этот фильтр применяется, он также (в переносном смысле) разбивает изображение на каналы, а затем умножает каждое значение друг на друга. Например, уравнение для значения пикселя в красном канале после прохождения через фильтр:
1
|
newRed = (red * rr) + (green * rg) + (blue * rb) + (alpha * ra) + ro; |
То же самое относится и к зеленому, синему и альфа, только с разными переменными из матрицы ( gr,gg,gb,ga
для зеленого и т. Д.). Этот фильтр немного более продвинутый, чем ColorFilter
каждый, потому что каждый цвет зависит от других цветов пикселя.
Для получения более подробной информации см. Этот учебник .
Шаг 17: Простые фильтры: вспомогательная функция
Это одна из двух вспомогательных функций, которые будут использоваться здесь, но мы будем использовать ее для каждого фильтра, в том числе и для расширенных.
Поместите этот код в начало filters.js
файла:
01
02
03
04
05
06
07
08
09
10
11
12
|
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
файл):
1
2
3
4
|
filterBrightness = function (value) { applyFilter( new ColorFilter(value, value, value, 1)); hideDialog( '#dialog-filterbrightness' ); }
|
Как я упоминал ранее, наша вспомогательная функция делает все за нас, нам нужно только создать новый фильтр. Здесь мы создаем ColorFilter
с мультипликаторами красного, зеленого, синего, установленными на, value
и альфа, установленными на 1,0 (мы не хотим, чтобы этот фильтр касался альфы).
Ниже приведен пример результата этого фильтра:
Шаг 19: Простые фильтры: Colorify
Colorify увеличит или уменьшит значение некоторых каналов для изменения общего цвета изображения, поэтому мы снова будем использовать ColorFilter
.
Посмотрите на код:
1
2
3
4
|
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: Простые фильтры: десатурация
Десатурация — это процесс удаления насыщенности — простыми словами, делая изображение черно-белым. Для этого нам нужно рассчитать яркость каждого пикселя и установить для всех цветов это значение. Самое простое уравнение светимости включает в себя только добавление одинакового количества всех цветов, и для этого мы можем использовать ColorMatrixFilter
:
01
02
03
04
05
06
07
08
09
10
11
|
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
. Он также использует матрицу, но матрица свертки представляет множители пикселей, окружающих фактический пиксель.
Допустим, у нас есть пример матрицы свертки 3х3 (уже представленной в виде массива JavaScript):
1
2
3
4
5
|
[
[ 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
я создал этот код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
( 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()
вызывается, когда мы применяем фильтр к изображению. Сначала мы должны получить данные изображения из canvas, затем я использую трюк с JSON.parse(JSON.stringify(imageData.data))
— потому что мы хотим получить копию данных изображения, а imageData.data
объект не имеет clone()
илиslice()
метода чтобы достичь этого, поэтому мы используем этот хитрый способ, чтобы полностью скопировать объект и все его свойства.
Информация о цвете хранится в этих данных следующим образом:
[...] [красный] [зеленый] [синий] [альфа] [красный] [зеленый] [синий] [альфа] [...]
Таким образом, каждый пиксель занимает четыре элемента массива — по одному на каждый канал. Наконец, после перебора всех данных о пикселях мы вызываем putImageData()
цель, чтобы сохранить результат.
Шаг 22: сверточные фильтры: размытие
Это самый простой эффект свертки, поэтому я решил позволить пользователю установить радиус фильтра (что приведет к установке размера массива), чтобы сделать его более сложным. Вот функция, которую мы будем использовать:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
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: размытие по Гауссу
Этот сверточный фильтр называется так, потому что он использует значения из стандартного гауссова распределения (на рисунке выше), вставленные в матрицу свертки. Чтобы упростить задачу, мы позволяем пользователю выбрать только три значения радиуса, потому что применение фильтра для большего радиуса займет слишком много времени (радиус 3 пикселя уже является матрицей 7 на 7).
Вот функция:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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
. Все функции из этой точки будут иметь только разные матрицы:
01
02
03
04
05
06
07
08
09
10
11
12
|
filterEdgeDetection = function () { applyFilter( new ConvolutionFilter( [
[ 0, -1, 0 ], [ -1, 4, -1 ], [ 0, -1, 0 ] ],
1.0, 0.0
));
hideDialog( '#dialog-filteredgedetection' ); }
|
Ниже приведен результат этого фильтра:
Шаг 25: Улучшение края
Этот фильтр аналогичен предыдущему, но он усиливает края, не затемняя остальную часть изображения, что делает его идеальным для художественного использования. Это на самом деле матрица, которую я использовал, чтобы объяснить вам, как работают фильтры свертки:
01
02
03
04
05
06
07
08
09
10
11
12
|
filterEdgeEnhance = function () { applyFilter( new ConvolutionFilter( [
[ 0, 0, 0 ], [ -1, 1, 0 ], [ 0, 0, 0 ] ],
1.0, 0.0
));
hideDialog( '#dialog-filteredgeenhance' ); }
|
Ниже приведен результат этого фильтра:
Шаг 26: выбить
Фильтр «Тиснение» добавляет небольшой трехмерный эффект к изображению, выделяя левые нижние углы краев (так что это также фильтр обнаружения краев).
Функция:
01
02
03
04
05
06
07
08
09
10
11
12
|
filterEmboss = function () { applyFilter( new ConvolutionFilter( [
[ -1, -1, 0 ], [ -1, 1, 1 ], [ 0, 1, 1 ] ],
1.0, 0.0
));
hideDialog( '#dialog-filteremboss' ); }
|
Ниже приведен результат этого фильтра:
Шаг 27: Резкость
Мы все знаем, что такое заточка. Это достигается небольшой модификацией обнаружения края:
01
02
03
04
05
06
07
08
09
10
11
12
|
filterSharpen = function () { applyFilter( new ConvolutionFilter( [
[ 0, -1, 0 ], [ -1, 5, -1 ], [ 0, -1, 0 ] ],
1.0, 0.0
));
hideDialog( '#dialog-filtersharpen' ); }
|
Ниже приведен результат этого фильтра:
Вы видите разницу? Он действительно мал в матрице свертки, но в результате изображение получается более четким.
Это был последний из фильтров, но вы можете добавить больше, если хотите. Просто найдите некоторые в Интернете или экспериментируйте, чтобы создать свои собственные уникальные.
Шаг 28: Фильтрует обратные вызовы
Это будет следующая связка вызовов jQuery, но перед этим нам понадобится наша вторая вспомогательная функция в этом файле:
01
02
03
04
05
06
07
08
09
10
|
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
— функция для вызова с предыдущим значением.
Это создает сокращение, которое мы можем использовать в следующем коде обратного вызова; просто вставьте его под помощник:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
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()
как это:
1
2
3
4
|
var myEvilCondition = "someObjectPropertyButIDontKnowWhichOne" ; function evilEval () { return eval( 'myObject.' + myEvilCondition); }
|
Код выше очень плохой . Вы никогда не должны использовать eval()
как это. Приведенный выше пример можно исправить с помощью квадратных скобок:
1
2
3
4
|
var myNotEvilCondition = "someObjectPropertyButIDontKnowWhichOne" ; function goodNoEval () { return myObject[myNotEvilCondition]; }
|
В нашем случае все в порядке, потому что написание собственного интерпретатора было бы огромной тратой времени и ресурсов (то есть больше или больше файлов для загрузки пользователем).
Шаг 30: Сценарии: безопасный Eval
Поскольку мы выполняем пользовательские сценарии, мы должны быть уверены, что он случайно не уничтожит результат, над которым он работал, потому что он неправильно набрал имена функций или номера слоев. Вот почему он должен убедиться, что пользователь не может получить доступ к каким- window
либо app
методам напрямую.
По этой причине мы делаем нашу функцию похожей на это:
1
2
3
4
5
|
scriptExecute = function (code) { hideDialog( '#dialog-executescript' ); if ((code.match(/eval\(/g) != null ) && (!confirm( 'You used the eval function inside of your code. This may lead to unexpected effects, do you want to continue?' ))) return ; eval(code.replace(/(window\.|app\.)(.*?);/g, '' )); }
|
(Поместите этот код в scripts.js
файл.)
Сначала мы скрываем диалог, потому что в противном случае он просто зависнет там, пока скрипт не завершится, и, возможно, пользователь захочет увидеть свой скрипт на работе. Затем мы вызываем предоставленный код, но мы заменяем все window
и app
связанные вызовы, поэтому пользователь не может удалить все слои или закрыть окно по ошибке.
Небольшое предупреждение: пользователь все еще может что-то сделать с этими переменными, если он использует eval()
функцию в своем коде — но затем мы спрашиваем его, действительно ли он хочет это сделать.
Теперь мы должны добавить эту маленькую функцию обратного вызова:
1
2
3
|
app.callbacks.scriptExecute = function (e) { scriptExecute($( '#dialog-executescript textarea' ).val()); }
|
Это полная система сценариев. Идите и проверьте это, передав хороший код в диалог «Выполнение скрипта». Попробуйте использовать eval()
там, чтобы увидеть, что он спрашивает вас, действительно ли вы хотите это сделать.
Вывод
Как вы можете видеть, HTML5 Canvas — мощная вещь. Но мы только поцарапали поверхность того, что можно с этим сделать. Мы создали действительно продвинутое приложение, которое позволяет пользователям загружать свои фотографии и вносить в них некоторые изменения, а затем сохранять и распечатывать отредактированные фотографии — используя чистый JavaScript. Несколько лет назад это было бы шуткой.
Также вы можете расширить приложение, которое вы только что создали! Добавьте больше фильтров, измените интерфейс, добавьте больше полезных функций (например, вы можете добавить больше свойств к слою, может быть список всех фильтров с возможностью их удаления и редактирования или кнопку для преобразования буфера отмены в готовый скрипт для пользователь, чтобы поделиться). Просто будьте изобретательны, и, возможно, вы создадите действительно полезный материал!
Спасибо за чтение этого урока, надеюсь, я действительно научил вас чему-то, что вы будете использовать в каком-то большом проекте. Если вам нужна помощь в создании чего-то продвинутого с HTML5, не стесняйтесь спрашивать меня на мой контактный адрес электронной почты или добавив комментарий к этому учебнику. Я отвечу вам, как только получу ваше сообщение.