30. Создаем графический редактор

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

Итак, давайте сделаем такое приложение. Сперва создадим папку imageEditor. Далее перенесем туда картинку, которая называется image-placeholder. Создадим файлы index.html, style.css и script.js.

В файле index.html напишем необходимые теги. Изменим язык сайта на русский. Изменим название сайта на Графический редактор. Подключим сразу наш файл css, а также подключим стороннюю библиотеку, откуда мы будем подгружать иконки поворотов, чтобы самим их не создавать. Эту ссылку я взял сайта Font Awesome – https://cdnjs.com/libraries/font-awesome

Также подключим библиотеку, чтобы подгрузить иконки отражения по горизонтали и вертикали. Эти иконки я нашёл на сайте boxicons – https://boxicons.com/usage#import-css

Конечно, можно эти иконки и самому сделать в любом графическом редакторе, но я решил их загрузить с помощью сторонних css библиотек.

Двигаемся дальше. А дальше начинаем набирать html код внутри тега бади.

Сперва создадим тег див внутри которого и будет размещаться наш графический редактор. Зададим ему классы container и disable. Класс дисейбл нужен нам, чтобы отключать возможность нажимать на кнопки редактора, пока не загружена картинка.

Первым тегом внутри данного тега div будет идти заголовок второго уровня в котором будет текст Графический редактор.

Далее снова создадим div где мы разместим панель редактирования и картинку.

Первым тегом внутри дива с айди эдитор пэнэл будет div с айди филтерз внутри которого будут кнопки яркость насыщенность и ползунок. Первым тегом пусть будет название этого раздела. Сделаем это с помощью тега label.

Затем создадим див с внутри которого будут находится кнопки. Создадим сами кнопки и зададим им id. Также зададим класс первой кнопке, чтобы по умолчанию выбрана была она.

Далее создадим див внутри которого будет ползунок и информация что мы редактируем.

Так как яркость у нас выбрана по умолчанию, то по умолчанию в информации, что мы сейчас редактируем укажем Яркость. Также в информации о процентах укажем 100%.

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

Напишем название этого раздела также с помощью тега label. Также создадим тег див с классом опшин где будут уже кнопки поворотов и отражений. Картинки добавим с помощью подключенных библиотек через атрибут класс. И каждой кнопке зададим id.

Дальше подключаем картинку по умолчанию которая будет отображаться пока мы не добавим свою картинку.

И нам осталось добавить кнопки, которые будут располагаться ниже.

Первая кнопка будет сбросить фильтры. остальные кнопки мы добавим в отдельный див чтобы расположить их справа. Зададим этому диву id rigfhtButtons

И в этом теге див первым делом создадим тег инпут с типом файл. Он нужен чтобы загрузить файлы на вашем компьютере. Зададим ему id и скроем его, чтобы отдельно эту кнопку не стилизовать. И за клик по этой кнопке для загрузке картинки будет отвечать тег button c текстом Выбрать картинку. Т.е. эти две кнопки у нас будут связанны с помощью Javascript. Просто у нас один стиль будет на все эти кнопки, и отдельно стилизовать кнопку инпут я не хочу.

На этом код html готов, переходим к коду css. Как обычно наберу весь код молча. Если у вас будут вопросы по каким-то свойствам, то пишите. Я обязательно отвечу.

Наконец-то с CSS всё. Переходим к коду JavaScript

Для начала подключимся к необходимым дум элементам.

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

Двигаемся дальше. Свяжем кнопку выбрать картинку с кнопкой формы, которая позволяет выбрать и загрузит картинку. Сделаем это через слушатель эдивент лисенер.

Вот так мы связали эти две кнопки. Давайте проверим. Все работает. Теперь нам надо создать функцию, которая отобразит загруженную картинку в нашей программе.

Только давайте сразу привяжем запуск нашей еще не созданной функции к кнопке инпут. Как вы помните элементы формы можно отследить с помощью события change.

Теперь создадим функцию loadImage. Будем использовать стрелочную функцию:

Когда мы выберем картинку, то её данные будут храниться в свойстве files объекта Dom элемента fileInput. Давайте выведем это свойство в консоль и посмотрим сто она возвращает.

Как видите данное свойство возвращает объект FileList, который является псевдомассивом и содержит внутри себя объекты File. Когда мы называем объект псевдомассивом, это означает, что мы в каких-то аспектах можем работать с этим объектом как с массивом. Например, обращаться к объектам файл, через порядковые номера. Но такие методы для массива как форич или push для данного объекта FileList недоступны.

Так как мы выбрали только один файл, то объект FileList содержит только один объект файл. Если бы мы хотели выбрать больше файлов, то для тега инпут, надо было бы указать атрибут multiply. Но нам для нашего графического редактора это не нужно.

Теперь давайте создадим константу file которой в качестве значения зададим этот объект файл, который содержится в объекте FileList.

Итак, теперь в константе файл будет находится объект файл, который по сути является нашей картинкой.

Дальше пропишем, если пользователь не выберет файл, т.е. в объекте файл будет храниться значение undefined, то мы выйдем из функции.

Если файл у нас есть то присвоим переменной fileName имя этого файла, который храниться в свойстве name

Далее нам надо добавить наш выбранный файл. Но как это сделать. Для этого нам поможет объект URL и его метод createObjectURL. Объект URL нужен для работы с веб-адресами, и у него много свойств и методов. Но нас здесь инстересует только один – createObjectURL. В его параметр нам надо добавить объект file и данный метод создаст специальную временную ссылку. Временная ссылка отличается от обычной тем что она действует только в течении текущего сеанса. Т.е. если вы создадите такую ссылку, и обновите страницу, и попробуете воспользоваться ею на другом сайте, то она перестанет действовать. Давайте выведем эту ссылку в консоль.

Слово перед ссылкой blob означает, что данная ссылка направляет на blop объект. Blop расшифровывается как binary large object, и по сути означает файл большого размера. Вы можете спросить, а почему я говорю о каком-то объекте Blop, если я в метод createObjectURL в качестве параметра добавлял объект File. В чем отличие Blop и File? На самом деле объект Blop является прототипом для объекта File. Т.е. объект файл наследует все свойство и методы объекта Blop. Но основное отличие между Blop и File заключается в том, что объект File предоставляет гораздо больше метаданных хранящегося в нем файла, например, имя файла, когда последний раз редактировался и т.д. Объект Blop может предоставить из метаданных только размер и тип файла.

А в остальном объект Blop и File идентичны друг другу. Метод createObjectURL может принять как Blop объект так и File, но ссылку возвращает на blop объект, так как этой ссылке не нужны метаданные файла, метод их игнорирует и по сути воспринимает ваш объект файл как Blop объект и создает соответствующую ссылку.

Теперь нам надо эту ссылку прописать в атрибуте src тега img. Тег img находится в константе привьюимдж, далее пишем свойство src и скопируем этот код.

Далее нам надо как загрузится картинка разблокировать кнопки и ползунок. Сделаем мы это с помощью метода эд ивент лисенер для тега имдж.

Загрузка картинки отслеживается с помощью события load. Далее пишем стрелочную функцию. Когда картинка загрузится, то удалим у тега div с классом контейнер, второй класс disable и все блокирующие css свойства перестанут действовать.

Проверяем. И как видите всё работает.

Но после того как мы выбрали картинку, и нажали на кнопку, например Яркость, то она не становится активной, т.е. синей. Нам надо сейчас поставить слушатели на эти кнопки, чтобы при клике на любую из них, она становилась активней. И также чтобы менялся текст над слайдером. Давайте начнем.

Эти все кнопки находятся в массиве FilterOption. Давайте с помощью метода массива forEach прикрепим к каждой кнопке слушатель.

Каждый элемент массива назовем option. В качестве второго параметра будем использовать стрелочную функцию. В теле функции проверим, имеет ли нажатая кнопка option класс active. Если нет, то удалим класс эктив у кнопки, которая уже имеет такой класс и присвоим нашей.

И поменяем текст над слайдером. Проверяем.

Далее я хочу чтобы у каждой кнопки было максимальное значение на слайдере. У кнопок яркость и насыщенность это было 200, а у инверсии и обесцветить 100. Также чтобы положения ползунка и значение здесь брались из соответствующей переменной. Для Яркости эта переменная будет brightness, для насыщенности – это переменная saturation и т.д.

Сделаем это с помощью условного оператора switch.

Проверим с помощью switch чему равен id у текущей кнопки option. Если оно равно brightness т.е. яркость, берем слайдер и ставим ему максимальное значение на 200. Максимальное значение можно установить с помощью атрибута max. Текущее значение ползунка возьмем у переменной brightness. ОНо у нас равно 100, значит ползунок будет располагаться в центре. Для процентов тоже возьмем значение из переменной. Значение в процентах у нас храниться в константе FilterValue.

В конце поставим оператор брейк. Теперь давайте поработаем с остальными значениями option id.

Для Инверсии максимальное значение будет 100, о значение ползунка и текст также будут брать значение из соответствующей для этого id переменной. А это переменная inversion и она равна 0, значит ползунок будет находится в самом начале. Проверяем. Все работает. Только программа не реагирует на наше перемещение ползунка. Проценты не меняются, так как они берут значение из соответствующих переменных, которые мы еще не меняли.

Давайте это исправим. Прикрепим к слайдеру слушатель. Для этой цели мы можем использовать два события. Сперва давайте как обычно используем событие “change”. Во втором параметре используем еще не созданную нами функцию updateFilter

И переходим теперь к её созданию. Функция будет стрелочной.

Значение для текста с процентами мы будем брать из текущего положения слайдера. Давайте проверим. Вроде работает. Но мы столкнулись вот с какой проблемой. Что если мы хотим уменьшить яркость на 20%. Так как здесь проценты обновляются после завершения контакта со слайдером, то попасть ровно в 20% становится проблематично. Так просто работает событие change. Наша стрелочная функция сработает тогда, кода мы завершаем взаимодействие с элементом формы.

Здесь лучше использовать другое событие – это input. Оно очень схоже с событием change, но с одним интересным отличием. Событие инпут срабатывает постоянно во время взаимодействия с элементом формы. Давайте это проверим. И теперь в 20% гораздо легче попасть.

Нам также надо во время перемещения слайдера постоянно обновлять значение соответствующей переменной. Для этого создадим константу, которая будет содержать кнопку с классом эктив.

Далее с помощью условного оператора свитч проверим, какой айди у выбранной кнопки. И в зависимости от этого айди будем менять значение у соответствующей переменной.

Проверяем. И как видите теперь значение слайдера для каждого эффекта сохраняется.

Далее настроим кнопку сбросить фильтры. Сперва прикрепим к этой кнопке слушатель.

Во втором параметре укажем еще не созданную нами функцию resetFilter. Теперь приступаем к её созданию.

Сперва зададим значения переменным по умолчанию.

И кликнем по первой кнопке, т.е. кнопке Яркость. Давайте проверим. Зададим для всех эффектов разные параметры и теперь кликнем по кнопке сбросить фильтры. Как видите все значения вернулись к исходным.

Теперь давайте создадим функцию, которая и будет применять фильтры к картинке. Назовем её applyFilters. На самом деле здесь не будет ничего сложного. Мы просто воспользуемся стилями CSS для картинки. Чтобы применить эффекты яркость, насыщенность, инверсия и обесцветить нам понадобится css-свойство filter. Давайте напишем необходимый код. Сперва набираем константу

Далле пишем свойство стайл, после которого мы можем написать необходимое css свойство. А в значении укажем необходимые фильтры со значениями из соответствующих переменных. Теперь запустим эту функцию в конце тела функции updateFilter. Т.е. когда мы будете изменять значения фильтров с помощью ползунка, то также будут применяться эффекты к картинке. Давайте проверим. И как видите все фильтры сработали.

Теперь нам надо настроить поведение для этих кнопок. Все они хранятся в константе rotateOption. Давайте ко всем этим кнопкам прикрепим слушатель.

Давайте когда ротейт будет равен -360, то мы сбросим значение до 0, чтобы переменная ротейт не хранила значения -430 градусов, – 520 и т.д.

Отслеживать мы будем клик. И при клике будет у нас запускаться стрелочная функция, которая как и мы делали с эффектами, проверит, чему равна айди кнопки, и в зависимости от айди поменяет значение переменной.

Проверять айди мы будем с помощью условного оператора свитч.

Если айди у кнопки равен left то мы отнимем у текущего значения переменной ротайт 90, ну т.е. 90 градусов. Если id будет райт то мы прибавим 90. Если айди будет хоризонтал, то мы переменой flip хоризонтал зададим следующее значение с помощью тернарного оператора. Если эта переменная равна 1, то присвоим ей значение -1, если нет, и переменная равна -1, то присвоим ей значение 1. Для CSS это значение будет означать нужно ли отражать картинку по горизонтали или нет. Для id вертикал сделаем тоже самое но уже с переменной flipVertical. И в конце этой стрелочной функции вызовем applyFilters.

А в самой функции эплайфилтерс добавим css стиль для поворота и отражения. Необходимое нам сиэсэс свойство называется трансформ.

В качестве значения укажем что мы хотим повернуть а также отразить изображения. А значения для поворота и отражения возьмем из необходимых переменных. Проверяем. Все работает. Осталось сохранить изображение при клике на кнопку Сохранить.

Давайте прикрепим слушатель к кнопке сохранить. Отслеживать мы будем конечно-же клик. А в качестве второго параметра напишем имя еще не созданной нами функции SaveImg.

Теперь переходим к созданию этой функции. Чтобы сохранить картинку, мы будем использовать тег canvas, куда мы отправим картинку, применим к ней эффекты и сохраним. Вот такой краткий план действий. Итак первым делом создаем тег canvas c помощью метода createElement

Далее мы должны задать канвасу ширину оригинальной картинки, т.е. не тот размер, который у нас отражается на сайте, а тот, который у самой картинки есть в пикселях. Так как когда пользователь будет редактировать картинку размером 400х200 пикселей, то и отредактированная картинка нужна ему будет таких же размеров.

А как получить доступ к оригинальным размерам картинки. Это можно сделать с помощью свойств naturalWidth и naturalHeight для нашей картинки previewImg. Зададим ей ширину и высоту.

Теперь, чтобы поместить туда картинку, нам надо создать внутри этого канваса 2d холст c помощью метода getContext.

Теперь добавим в него изображение с помощью метода drawImage.

И давайте отобразим этот канвас на нашей странице, чтобы видеть, что мы будем сохранять. И вот наша картинка отобразилась. Но естественно никакие эффекты мы пока применить не можем. Чтобы применить эффекты у канваса есть такое же свойство как filter которое мы применяли к изображению в функции эплай филтерс.

Давайте воспользуемся таким свойством.

Проверяем. Но ничего не происходит. Дело в том когда мы добавляем картинку, то канвас создает новый слой, как в фотошопе, на котором мы собираемся рисовать, добавлять изображение или применять к нему эффекты. Т.е. в нашем случае эффекты применяются к новому пустому слою, а не к нашей картинке. Поэтому нам надо применить эффекты до добавление картинки, чтобы они применились к слою куда будет добавлена картинка.

И как видите, все теперь работает.

Далее давайте применим к нашему изображению эффекты отражения. Для этого нам нужно свойство scale. Для того чтобы отразить изображение по вертикали или по горизонтали надо использовать значение -1 либо в первом параметре, тогда изображение отразится по горизонтали, или во втором параметре, тогда изображение отразится по вертикали. Если мы поставим в эти параметры единицы, то ничего не произойдет. Итак, необходимые значения мы храним в переменных флипгоризонтал и флипвертикал. Добавим их как параметры. И посмотрим на результат. Например, отразим по вертикали. И ничего не отображается.

Дело в том, что когда мы отражаем картинку по вертикали, наша картинка буд-то переворачивается вниз и находится теперь здесь относительно канваса. Т.е. нам надо после отражения картинки по вертикали переместить картинку вверх. Перемещать мы будет на высоту картинки. О чтобы переместить картинку, а точнее текущий слой нам понадобится метод translate, где в пером параметре мы указываем насколько переместить слой по х, а во втором параметре мы указываем на сколько переместить по y.

Давайте теперь создадим условие, если flip vertical равен -1, то переместим картинку вверх на размер высоты картинки. Чтобы переместить вверх по y нам надо использовать знак минус впереди.

Давайте проверим. Все работает. Теперь надо почти тоже самое сделать и для отражения по горизонтали.

Только при отражении по горизонтали наша картинка сместится сюда, а значит нам надо переместить картинку влево на размер ширины картинки.

Проверяем. Все работает.

Теперь нам надо настроить разворот на 90, 270, -90 и -270 градусов. Т.е. когда наша картинка будет вертикальной. Давайте напишем новое условие иф.

Если наша картинка примет вертикальное положение, то нам необходимо изменить размеры канваса, где ширина канваса теперь будет равна высоте картинке, а высота канваса будет равна ширине картинке.

Далее обработаем вариант, когда переменная ротейт будет равна либо 90, либо -270 градусов. Для картинке это одно и тоже положение. Итак если ротейт равен 90 или -270, то мы должны развернуть картинку на соответствующее количество градусов. Разворачивать слой или картинку мы будем с помощью свойства rotate. В параметре которого мы должны указать значение радианы. Но мы же привыкли к градусам. Как перемести градусы в радианы. Все просто, необходимо значение градусов умножить на число Пи и разделить на 180. Давайте это напишем. Число Pi есть у математического объекта Math, и его свойства PI.

Вращаться картинка будет относительно этой точки, а значит если мы развернем картинку на 90 или -270 градусов то она окажется здесь. Т.е. нам надо затем ее переместить сюда. И вы можете предположить, что нам просто в методе транслейт надо указать по x размер высоты картинки. На самом деле нет. Дело в том когда мы вращаем слой, мы также вращаем его систему координат. Т.е. теперь система координат слоя выглядит так и нам надо как бы поднять картинку по y на размер высоты картинки. Пишем.

Теперь давайте сразу напишем условие если переменная ротейт равна -90 или 270. Ну точнее здесь вместо условия можно указать просто else, так как в этом условии из четырех значений мы обработали значения 90 и -270, и в остатке у нас остаются значения -90 и 270.

Итак если переменная ротейт равна -90 или 270 то мы развернем картинку на соответствующее значение градусов и наша картинка окажется здесь. А ее система координат будет выглядеть так. Т.е. мы должны переместить картинку на ширину картинки по иксу вниз. Пишем. Теперь проверим.

Как видите всё работает. Нам осталось сохранить картинку.

Для начала внутри этой функции создадим ссылку и присвоим ее константе с именем link.

Далее у этой ссылке мы должны указать для атрибута download имя картинки. Так мы укажем что по клику на эту ссылку мы хотим скачать файл и чтобы у него было такое имя файла. Далее мы должны указать ссылку на наш файл. Что получить ссылку на наш канвас мы должны использовать его метод toDataURL. Этот метод преобразует наш канвас и все его слои в одну картинку и предоставит ссылку на его скачивание.

И нам осталось кликнуть по тегу ссылке, чтобы начать скачивание. Кликнуть мы можем с помощью метода клик. Пишем. Уберем строчку где мы добавляем канвас на нашу страницу. И проверяем. Всё работает!

Домашнее задание. Добавить в программу эффекты размытия и контрастности.