Статьи

Как создать приложение для обмена фотографиями в Instagram с HTML5: Часть 2

В первой части мы рассмотрели некоторые детали реализации макета интерфейса приложения InstaFuzz . Вы можете получить исходный код приложения здесь, если хотите запустить его локально. В этой статье мы рассмотрим некоторые другие моменты, такие как использование перетаскивания, File API, Canvas и Web Workers.

Перетаскивания

Одна из вещей, которую поддерживает InstaFuzz, — это возможность перетаскивать файлы изображений непосредственно на большой черно-синий прямоугольник. Поддержка этого активируется путем обработки события drop в элементе CANVAS. Когда файл помещается в элемент HTML, браузер запускает событие «drop» для этого элемента и передает объект dataTransfer, который содержит свойство files , содержащее ссылку на список файлов, которые были удалены. Вот как это обрабатывается в приложении («picture» — это идентификатор элемента CANVAS на странице):

 var pic = $ ("# picture");
 pic.bind ("drop", function (e) {
     suppressEvent (е);
     var files = e.originalEvent.dataTransfer.files;
     // больше кода здесь, чтобы открыть файл
 });

 pic.bind ("dragover", suppressEvent) .bind ("dragenter", suppressEvent);

 function suppressEvent (e) {
     e.stopPropagation ();
     e.preventDefault ();
 }

Свойство files представляет собой набор объектов File, которые затем можно использовать с File API для доступа к содержимому файла (рассматривается в следующем разделе). Мы также обрабатываем события перетаскивания и перетаскивания и в основном предотвращаем распространение этих событий в браузер, тем самым не давая браузеру обрабатывать удаление файла. Например, IE может выгрузить текущую страницу и в противном случае попытаться открыть файл напрямую.

Файловый API

После удаления файла приложение пытается открыть изображение и отобразить его на холсте. Это делается с помощью File API . Файловый API — это спецификация W3C, которая позволяет веб-приложениям программным образом защищать доступ к файлам из локальной файловой системы. В InstaFuzz мы используем объект FileReader для чтения содержимого файла в виде строки URL-адреса данных, например, используя метод readAsDataURL :

 var reader = new FileReader ();

 reader.onloadend = function (e2) {
     drawImageToCanvas (e2.target.result);
 };

 reader.readAsDataURL (файлы [0]);

Здесь files — это коллекция объектов File, полученных из функции, обрабатывающей событие drop в элементе CANVAS. Поскольку нас интересует только один файл, мы просто выбираем первый файл из коллекции и игнорируем остальные, если они есть. Фактическое содержимое файла загружается асинхронно, и после завершения загрузки происходит событие onloadend, где мы получаем содержимое файла в виде URL-адреса данных, который затем впоследствии рисуем на холсте.

Рендеринг фильтров

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

Рендеринг изображений на холсте

Элемент canvas поддерживает рендеринг объектов Image с помощью метода drawImage . Для загрузки файла изображения в экземпляре Image InstaFuzz использует следующую служебную программу:

 App.Namespace.define ("InstaFuzz.Utils", {
     loadImage: function (url, complete) {
         var img = новое изображение ();
         img.src = url;
         img.onload = function () {
             полный (IMG);
         };
     }
 });

Это позволяет приложению загружать объекты изображений с URL-адреса, используя следующий код:

 function drawImageToCanvas (url) {
     InstaFuzz.Utils.loadImage (url, function (img) {
         // сохранить ссылку на исходное изображение
         sourceImage = img;
         mainRenderer.clearCanvas ();
         mainRenderer.renderImage (IMG);
         // загрузка превью фильтров изображения
         loadPreviews (IMG);
     });
 }

Здесь mainRenderer — это экземпляр, созданный из функции конструктора FilterRenderer, определенной в filter-renderer.js . Приложение использует объекты FilterRenderer для управления элементами холста — как на панели предварительного просмотра, так и в основном элементе холста справа. Метод renderImage в FilterRenderer был определен так:

 FilterRenderer.prototype.renderImage = function (img) {
     var imageWidth = img.width;
     var imageHeight = img.height;
     var canvasWidth = this.size.width;
     var canvasHeight = this.size.height;
     вар ширина, высота;

     if ((imageWidth / imageHeight)> = (canvasWidth / canvasHeight)) {
         ширина = холст ширина;
         height = (imageHeight * canvasWidth / imageWidth);
     } еще {
         ширина = (imageWidth * canvasHeight / imageHeight);
         height = canvasHeight;
     }

     var x = (canvasWidth - ширина) / 2;
     var y = (canvasHeight - высота) / 2;
     this.context.drawImage (img, x, y, ширина, высота);
 };

Это может показаться большим количеством кода, но все, что он в конечном итоге делает, — это выясняет лучший способ визуализации изображения в доступной области экрана с учетом соотношения сторон изображения. Ключевой фрагмент кода, который фактически отображает изображение на холсте, находится в последней строке метода. Элемент контекста ссылается на 2D-контекст, полученный из объекта canvas путем вызова его метода getContext .

Получение пикселей с холста

Теперь, когда изображение отрендерено, нам понадобится доступ к отдельным пикселям, чтобы применить все доступные фильтры. Это легко получить, вызвав getImageData для объекта контекста холста. Вот как это называет InstaFuzz из instafuzz.js .

 var imageData = renderer.context.getImageData (
     0, 0,
     renderer.size.width,
     renderer.size.height);

Объект, возвращаемый getImageData, обеспечивает доступ к отдельным пикселям через его свойство данных, которое, в свою очередь, представляет собой объект в виде массива, который содержит коллекцию байтовых значений, где каждое значение представляет цвет, отображаемый для одного канала одного пикселя. Каждый пиксель представлен 4 байтами, которые определяют значения для красного, зеленого, синего и альфа-каналов. Он также имеет длину свойство, которое возвращает длину буфера. Если у вас есть 2D-координата, вы можете легко преобразовать ее в индекс в этот массив, используя следующий код. Значения интенсивности цвета каждого канала варьируются от 0 до 255. Вот полезная функция от filters.js, которая принимает в качестве входных данных объект данных изображения вместе с 2D-координатами для пикселя, интересующего вызывающего, и возвращает объект, содержащий значения цвета:

 function getPixel (imageData, x, y) {
     var data = imageData.data, index = 0;
     // нормализуем x и y и вычисляем индекс
     х = (х <0)?  (imageData.width + x): x;
     у = (у <0)?  (imageData.height + y): y;
     index = (x + y * imageData.width) * 4;

     возвращение {
         r: данные [индекс],
         g: данные [индекс + 1],
         б: данные [индекс + 2]
     };
 }

Применение фильтров

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

 // Фильтр "Взвешенные оттенки серого"
 Filters.addFilter ({
     название: «Весовая шкала серого»,
     apply: function (imageData) {
         var w = imageData.width, h = imageData.height;
         var data = imageData.data;
         индекс var;

         для (var y = 0; y <h; ++ y) {
             для (var x = 0; x <w; ++ x) {
                 index = (x + y * imageData.width) * 4;
                 переменная яркость = parseInt ((данные [индекс + 0] * 0,3) +
                                          (данные [индекс + 1] + 0,59) +
                                          (данные [индекс + 2] * 0,11));
                 данные [индекс + 0] = данные [индекс + 1] =
                     данные [индекс + 2] = яркость;
             }

             Filters.notifyProgress (imageData, x, y, this);
         }

         Filters.notifyProgress (imageData, w, h, this);
     }
 });

После применения фильтра мы можем отразить это на холсте, вызвав метод putImageData, передав в измененный объект данных изображения. В то время как взвешенный фильтр в градациях серого довольно прост, большинство других фильтров используют технику обработки изображений, известную как свертка . Код для всех фильтров доступен в filters.js, и фильтры свертки были перенесены из кода C, доступного здесь .

Веб-работники

Как вы можете себе представить, выполнение всего этого перебора чисел для применения фильтров может занять много времени. Например, фильтр размытия в движении использует матрицу фильтра 9 × 9 для вычисления нового значения для каждого отдельного пикселя и фактически является наиболее интенсивным процессором среди всех этих фильтров. Если бы мы выполняли все эти вычисления в потоке пользовательского интерфейса браузера, то приложение по существу зависало бы каждый раз, когда применялся фильтр. Чтобы обеспечить отзывчивый пользовательский опыт, приложение делегирует основные задачи обработки изображений в фоновый скрипт, используя поддержку W3C Web Workers в современных браузерах.

Веб-работники позволяют веб-приложениям запускать сценарии в фоновой задаче, которая выполняется параллельно с потоком пользовательского интерфейса. Связь между рабочим и потоком пользовательского интерфейса осуществляется путем передачи сообщений с помощью API postMessage . На обоих концах (т. Е. Поток пользовательского интерфейса и рабочий) это проявляется как уведомление о событии, которое вы можете обработать. Вы можете только передавать «данные» между рабочими и потоком пользовательского интерфейса, то есть вы не можете передавать ничего, что связано с пользовательским интерфейсом — например, вы не можете передавать элементы DOM работнику из потока пользовательского интерфейса.

В InstaFuzz рабочий реализован в файле filter-worker.js . Все, что он делает в работнике — это обрабатывает событие onmessage и применяет фильтр, а затем передает результаты обратно через postMessage . Как выясняется, даже если мы не можем передать элементы DOM (что означает, что мы не можем просто передать элемент CANVAS работнику, чтобы применить фильтр), мы можем фактически передать объект данных изображения, возвращенный методом getImageData, который мы обсуждали ранее. , Вот код обработки фильтра из filter-worker.js :

 importScripts ("ns.js", "filters.js");

 var tag = null;

 onmessage = function (e) {
     var opt = e.data;
     var imageData = opt.imageData;
     вар фильтр;
     tag = opt.tag;
     filter = InstaFuzz.Filters.getFilter (opt.filterKey);
     var start = Date.now ();
     filter.apply (ImageData);
     var end = Date.now ();

     PostMessage ({
         тип: "изображение",
         imageData: imageData,
         filterId: filter.id,
         тег: тег,
         TimeTaken: конец - начало
     });
 }

Первая строка включает некоторые файлы сценариев, от которых зависит рабочий, вызывая importScripts . Это похоже на включение файла JavaScript в документ HTML с использованием тега SCRIPT. Затем мы устанавливаем обработчик для события onmessage, в ответ на который мы просто применяем рассматриваемый фильтр и передаем результат обратно в поток пользовательского интерфейса, вызывая postMessage . Достаточно просто!

Код, который инициализирует работника, находится в instafuzz.js и выглядит так:

  var worker = new Worker ("js / filter-worker.js"); 

Не много ли это? Когда сообщение отправляется рабочим в поток пользовательского интерфейса, мы обрабатываем его, определяя обработчик для события onmessage в рабочем объекте. Вот как это делается в InstaFuzz :

 worker.onmessage = function (e) {
     var isPreview = e.data.tag;
     switch (e.data.type) {
         case "image":
             if (isPreview) {
                 previewRenderers [e.data.filterId].
                     context.putImageData (
                         e.data.imageData, 0, 0);
             } еще {
                 mainRenderer.context.putImageData (
                     e.data.imageData, 0, 0);
             }
             перемена;
         // больше кода здесь
     }

 };

Код должен быть достаточно понятным. Он просто выбирает объект данных изображения, отправленный рабочим, и применяет его к контекстному объекту соответствующего холста, вызывая отображение измененного изображения на экране. Планирование фильтра для преобразования с рабочим одинаково просто. Вот процедура, которая выполняет эту функцию в InstaFuzz :

 функция scheduleFilter (filterId,
                              визуализатор,
                              img, isPreview,
                              resetRender) {
     if (resetRender) {
         renderer.clearCanvas ();
         renderer.renderImage (IMG);
     }

     var imageData = renderer.context.getImageData (
         0, 0,
         renderer.size.width,
         renderer.size.height);
     worker.postMessage ({
         imageData: imageData,
         ширина: imageData.width,
         высота: imageData.height,
         filterKey: filterId,
         tag: isPreview
 });

 }

Завершение

Источник для InstaFuzz доступен для скачивания здесь . Мы увидели, что довольно сложные пользовательские интерфейсы возможны сегодня с такими технологиями HTML5, как Canvas, Drag / Drop, File API и Web Workers. Поддержка всех этих технологий довольно хороша почти во всех современных браузерах. Одна вещь, которую мы не затронули, это вопрос совместимости приложения со старыми браузерами. По правде говоря, это нетривиальная, но необходимая задача, о которой я надеюсь рассказать в следующей статье.

Эта статья является частью технической серии HTML5 от команды Internet Explorer. Испытайте концепции этой статьи с тремя месяцами бесплатного кросс-браузерного тестирования BrowserStack @ http://modern.IE