HTML5 web приложение с фильтрами как в Instagram. Часть 2.

Посмотреть демо приложения Вы можете на моём аккаунте codepen: http://codepen.io/nikitakiselev/full/kXyBAP/.

HTML5 web приложение с фильтрами как в Instagram. Часть 1.

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

git clone https://github.com/mblog-repository/html5-insta-filters.git
cd html5-insta-filters
git checkout 05002ccda2eecbb24415d9ab323a107f331e2450

Теперь у Вас есть каркас приложения, для второй части статьи.

Содержание

  1. Техническое задание
  2. Введение
  3. Подготовка инструментов
  4. Добавление проекта на GitHub
  5. Написание HTML каркаса
  6. Написание Javascript кода
  7. Написание CSS кода
  8. Заключение

Написание HTML каркаса

Файл index.html был создан в предыдущей статье, поэтому открываем его и напишем там простой HTML каркас:

index.html

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>HTML5 web приложение с фильтрами как в Instagram</title>
</head>
<body>

</body>
</html>

Т.к. html код довольно объёмный и не представляет особого интереса, то здесь я опишу только ключевые моменты. Полностью посмотреть как выглядит файл index.html вы сможете в репозитории.

Следующим шагом будет написание html кода для приложения:

<div id="photo-box" class="photo-box">
        <div class="label">
                <img src="https://ucarecdn.com/c7aeb97e-a73f-40bb-8966-45d692d6dde1/upload.png" alt="Upload icon" width="100" height="100"/>
                <div>Нажмите или перетащите сюда изображение...</div>
        </div>
        <div id="loader" class="loader"></div>
</div>

<div class="download">
        <a href="#" id="download-link" class="download-link">
                Скачать изображение
        </a>
</div>

<input type="file" id="file-picker" class="file-picker"/>

<div id="filters" class="filters">
        <div class="preset preset_original" data-preset="original">
                <span class="preset-name">Original</span>
        </div>
        <div class="preset preset_vintage" data-preset="vintage">
                <span class="preset-name">Vintage</span>
        </div>
        <div class="preset preset_lomo" data-preset="lomo">
                <span class="preset-name">Lomo</span>
        </div>
</div>

Здесь в блоке div#photo-box будет находиться загружаемое пользователем изображение. Элемент <canvas>, на котором будет отрисоваваться выбранное изображение, создаётся динамически, поэтому сейчас его нет. Сейчас внутри блока мы видит блок .label, который выводит текст "Нажмите или перетащите сюда изображение..." и иконку. Блок #loader служит для индикации работы над изображением, это будет полезно на слабых компьютерах, когда эффект применяется к изображению не мгновенно. Без этого можно обойтить, но мы сделаем приложение немного "user friendly".

Далее в блоке #download находится ссылка для скачивания текущего изображения, отрисованного на <canvas>. Ссылка по умолчанию скрыта с помощью CSS стилей, и появляется когда применяется фильтр к изображению.

Элемент #file-picker так же скрыт с помощью CSS. Это стандартный элемент выбора файла. В техническом задании написано, что необохдимо предусмотреть возможность загрузки изображения с помощью стандартного диаголового окна выбора файла. Элемент скрыт, но при клике на область #photo-box, будет срабатывать событие click на #file-picker, открывая тем самым диплоговое окно выбора файла. Всё просто.

Ну а дальше идёт блок #filters, в котором выведены эффекты, которые можно применить к изображению. Этот блок появляется только тогда, когда пользователь выбрал изображение. Я не стал приводить в статье полный список всех доступных фильтров - их много и весь список вы найдёте в репозитории проекта на github. У каждого фильтра есть аттрибут data-preset="original", в котором прописано название фильтра. Это название мы и будем считывать по клику на фильтр, чтобы применить его к изображению.

На этом обзор основных моментов HTML структуры приложения закончен.

primer-raboty-prilozheniya.jpg

Написание Javascript кода

Наконец то мы подошли к самому интересному - написанию javascript.

Продумаем логику работы приложения:

  • Получить изображение путём перетаскивания его на определённую область или выбором с помощью системного диалогового окна.
  • Создать новый элемент canvas размером 500x500 px поместить его в DOM (Document Object Model — «объектная модель документа»).
  • Ожидать клика на какой нибудь фильтр.
  • Применить выбранный эффект с помощью библиотеки Caman.js
  • Если выбранный фильтр "original", вернуть начальное состояние изображения.
  • Добавить к выбранному фильтру класс .active.
  • При выборе другого изображения, удалить старый <canvas> и начать всё с первого пункта.

Теперь, когда мы знаем, что нужно делать, давайте же сделаем то, ради чего мы сдесь собрались - запрограммируем это. Писать весь код будем в файле src/js/app.js, но перед этим необходимо запустить сборщик проекта - gulp watch.

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

src/js/app.js

let $photoBox = $('#photo-box');

$filePicker.on('change', event => {
  if (event.target.files.length) {
    uploadFile(event.target.files[0]);
  }
});

Здесь просто отлавливаем события клика на #photo-box, и если выбраны файлы, то передаём первый файл из массива в функцию uploadFile. Функция uploadFile не сложная, в ней мы выполняем провернку mime типов изображения и

// Индикатор работы
let $loader = $('#loader');

// Создаём новое изображение
let image = new Image();

// Имя загруженного изображения
// По умолчанию "image"
let uploadedFileName = 'image';

function uploadFile(file) {
  if (file.type.match('image/jpg') || file.type.match('image/png')) {
    $loader.show(); // Показываем loader, скрыть его надо по событию загрузки изображения
    image.src = URL.createObjectURL(file); // указываем src изображению
    uploadedFileName = file.name.split('.').shift(); // получаем имя файла и созраняем в переменную
  } else {
    alert('Поддерживаются только файлы jpg и png');
  }
}

Теперь остаётся отловить событие загрузки изображения image, создать элемент canvas и отрисовать на нём это изображение. Здесь немного сложнее, но мы рассмотрим этот код, плюс я оставил подробные комментарии.

// Получаем размеры #photoBox
// это будут максимально возможные для изображения
// т.е. 500x500px
let maxWidth = $photoBox.width(),
    maxHeight = $photoBox.height(),
    imageWidth = maxWidth,
    imageHeight = maxHeight;

// Событие загрузки изображения
image.onload = function() {
    // Оригинальные размеры изображения
    let width = this.width,
        height = this.height;

    // Получение новых размеров для изображения, 
    // чтобы оно влезло в заданную область в 500x500
    if (width >= maxWidth || height >= maxHeight) {
        if (width > height) {
            let ratio = width / maxWidth;
            imageWidth = maxWidth;
            imageHeight = height / ratio;
        } else {
            let ratio = height / maxHeight;
            imageHeight = maxHeight;
            imageWidth = width / ratio;
        }
    } else {
        imageWidth = width;
        imageHeight = height;
    }

    // Скрываем подсказку
    $label.hide();

    // Показываем блок с фильтрами
    $filters.addClass('active');
    // Выбираем первый фильтр (Original)
    $filters.find('.preset')
            .first()
            .trigger('click');

    // Ищем старый canvas, если он есть
    let $oldCanvas = $photoBox.find('canvas');

    // Создаём новый canvas и задаём ему размеры
    // либо старого канваса, либо 500х500, если старого канваса нет
    // Это нужно для плавной анимации изменения размера
    let $photo = $('<canvas>').addClass('photo').attr({
        id: 'photo',
        width: imageWidth,
        height: imageHeight
    }).css({
        width: $oldCanvas.width() || '0px',
        heigth: $oldCanvas.height() || '0px'
    });

    // Удаляем старый канвас
    $oldCanvas.remove();

    // Вставляем новый канвас в #photo-box
    $photoBox.append($photo);

    // Рисуем изображение на новом canvas
    let ctx = $photo[0].getContext('2d');
    ctx.drawImage(this, 0, 0, imageWidth, imageHeight);

    // Когда изображение отрисовано
    // плавно меняем его размеры до нужных
    $photo.css({
        width: imageWidth + 'px',
        height: imageHeight + 'px'
    });

    // Скрываем лоадер
    $loader.hide();
}

Написание CSS кода

Верстка проекта примитивная. Хочу обратить внимание на несколько моментов.

Изменение стиля полосы прокрутки

Изменение стилей полосы прокрутки поддерживают только браузеры, основанные на webkit. Можно было подключить js плагин, который сделает свою прокрутку с возможностью её кастомизации. Такой вариант будет работать во всех браузерах, но я решил не перегружать проект лишними плагинами, поэтому воспользовался кастомизацией только для webkit:

  &::-webkit-scrollbar {
    height: 4px;
    background-color: rgba(100,100,100,0.2);
    border-radius: 2px;
  }

  &::-webkit-scrollbar-thumb {
    border-radius: 1px;
    background: #fff;
    box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
    cursor: pointer;
  }

  &::-webkit-scrollbar-thumb:window-inactive {
    background: #aaa;
  }
### Вывод пресетов

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

presets.png

Выводятся эти изображения с помощью css:

.preset.preset_clarity { background-position: 0 0; } 
.preset.preset_concentrate { background-position: -100px 0; } 
.preset.preset_crossProcess { background-position: -200px 0; } 
.preset.preset_glowingSun { background-position: -300px 0; } 

Заключение

На этом описание разработки проекта закончено. Осталось добавить изменения в репозиторий.

git add .
git commit -m 'Complete project'
git push

Ссылка на репозиторий проекта: https://github.com/mblog-repository/html5-insta-filters

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