Статьи

Доступ к камере пользователя с помощью JpegCamera и Canvas

Эта статья была рецензирована Дэном Принцем . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

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

В этой статье мы увидим, как с помощью JpegCamera вместе с возможностями элемента HTML canvas можно создать клон приложения Layout для Instagram:

Screenshot of the Layout-like app using JpegCamera and Canvas.
Демо-макет как приложение

Исходный код для демонстрации можно скачать с Github .

Библиотека JpegCamera

JpegCamera позволяет вам получить доступ к камере пользователя как часть вашего приложения, изящно getUserMedia() Flash до отказа, если браузер не поддерживает getUserMedia() .

Первый шаг — включить необходимые скрипты в ваш проект.

Библиотека зависит от библиотек объектов SWF и Canvas to Blob , которые входят в состав zip-загрузки со страницы проекта Github. Тем не менее, в том же zip-файле есть версия сценария с зависимостями , которая обеспечивает ту же функциональность, что и загрузка трех сценариев на странице.

Имея это в виду, вы можете включить три необходимых сценария.

 <script src="/jpeg_camera/swfobject.min.js" type="text/javascript"></script> <script src="/jpeg_camera/canvas-to-blob.min.js" type="text/javascript"></script> <script src="/jpeg_camera/jpeg_camera.min.js" type="text/javascript"></script> 

Или просто пойти с одной альтернативой сценария.

 <script type="text/javascript" src="js/libs/jpeg_camera/jpeg_camera_with_dependencies.min.js"></script> 

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

Как только библиотека включена, вы можете использовать глобальный объект JpegCamera, чтобы проверить доступность камеры, и выбрать, как управлять запасным вариантом, если нет.

Если доступ предоставлен, вы можете настроить прослушиватель, когда камера будет готова, с помощью конструктора JpegCamera() .

Конструктор JpegCamera() принимает селектор CSS в качестве аргумента, который должен идентифицировать контейнер, который будет использоваться для потока камеры.

Фрагмент ниже показывает код, который делает это:

 (function() { if(!window.JpegCamera) { alert('Camera access is not available in your browser'); } else { JpegCamera('.camera') .ready(function(resolution) { // ... }).error(function() { alert('Camera access was denied'); }); } })(); 

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

Внутри функции ready обратному вызову разрешение камеры устройства передается в качестве первого аргумента. Это может быть полезно, если приложение, которое мы создаем, зависит от качества камеры устройства (т. Е. Сделать доступным захват HD или нет).

Между тем обратный вызов error получает в качестве аргумента string сообщение, объясняющее, что произошло. Если вам нужно показать пользователю объяснение в случае ошибки, вы можете использовать сообщение, которое предоставляет библиотека.

В дополнение к этому API JpegCamera предоставляет следующие методы:

  • capture() : это метод, который делает снимок. Он возвращает само изображение как объект Snapshot (класс, который JpegCamera использует для изображений).
  • show() : после того, как вы сделаете снимок, полученный вами объект Snapshot позволяет отображать изображение на странице, вызывая его метод show() . Изображение будет отображаться внутри того же контейнера, который вы указали при инициализации камеры.
  • showStream() : если в настоящий момент в контейнере отображается снимок, showStream() скрывает изображение и отображает поток.
  • getCanvas() : принимает функцию обратного вызова в качестве параметра, которая получает в качестве аргумента элемент canvas с захваченным изображением.

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

Сборка приложения

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

Структура приложения основана на шаблоне модуля . Этот шаблон дает нам несколько преимуществ:

  1. Это позволяет иметь четкое разделение между каждым из компонентов приложения.
  2. Он сохраняет нашу глобальную область чистым, предоставляя только те методы и свойства, которые строго необходимы другим. Другими словами, мы используем приватные атрибуты .

Вы заметите, что я передаю три параметра в самопризываемые функции:

 (window, document, jQuery) 

И эти аргументы получены:

 function(window, document, $) 

Причина прохождения window и document для целей минимизации . Если мы передадим их в качестве аргументов, то каждый из них может быть заменен на один символ. Если бы мы только ссылались на эти глобальные объекты напрямую, минификатор не смог бы заменить их более короткими именами.

С помощью jQuery мы делаем это, чтобы избежать конфликтов с другими библиотеками, которые также могут использовать $ качестве основной функции (например, Prototype ).

В верхней части модулей Layouts и Custom вы увидите что-то вроде этого:

 if(!window.LayoutApp) { window.LayoutApp = {}; } 

Это по двум причинам:

  1. Мы не дадим модулям генерировать ошибки в случае, если мы не включили скрипты в index.html .
  2. Мы сохраняем нашу глобальную область видимости, делая модули частью основного и доступными для него только после запуска приложения.

Логика приложения разделена на три модуля:

  • Модуль приложения
  • Модуль Макеты
  • Пользовательский модуль

Эти три модуля вместе с нашими библиотеками должны быть включены в наш index.html следующим образом:

 <!-- index.html --> <script type="text/javascript" src="js/libs/jquery-1.12.1.min.js"></script> <script type="text/javascript" src="js/libs/jpeg_camera/jpeg_camera_with_dependencies.min.js"></script> <script type="text/javascript" src="js/src/custom.js"></script> <script type="text/javascript" src="js/src/layouts.js"></script> <script type="text/javascript" src="js/src/app.js"></script> 

И есть еще один маленький кусочек кода для запуска приложения.

 <!-- index.html --> <script type="text/javascript"> (function() { LayoutApp.init(); })(); </script> 

Теперь давайте рассмотрим модули по одному.

Модуль приложения

Этот модуль содержит основную логику приложения. Он управляет взаимодействием пользователя с камерой, генерирует макеты на основе снятых изображений и позволяет пользователю загружать сгенерированные изображения.

Все начинается в модуле App , с метода init .

 // App module (app.js) initCamera = function () { if (!window.JpegCamera) { alert('Camera access is not available in your browser'); } else { camera = new JpegCamera('#camera') .ready(function (resolution) {}) .error(function () { alert('Camera access was denied'); }); } }, bindEvents = function () { $('#camera-wrapper').on('click', '#shoot', capture); $('#layout-options').on('click', 'canvas', download); }; init: function () { initCamera(); bindEvents(); } 

При вызове ìnit() приложение, вызывая следующие методы:

  1. initCamera() запускает камеру, если она доступна, или показывает предупреждение.
  2. bindEvents() устанавливает необходимые прослушиватели событий:
    1. Первый снимок, сделанный нажатием кнопки « Снять» .
    2. Второй для генерации загрузки при нажатии на одно из объединенных изображений.
 capture = function () { var snapshot = camera.capture(); images.push(snapshot); snapshot.get_canvas(updateView); }, 

Когда пользователь нажимает на Shoot , capture() вызывается. capture() использует метод класса Snapshot getCanvas() передаваемый в качестве функции обратного вызова updateView() .

 updateView = function (canvas) { canvas.selected = true; canvases.push(canvas); if (!measuresSet) { setCanvasMeasures(canvas); measuresSet = true; } updateGallery(canvas); updateLayouts(canvas); }, 

В свою очередь, updateView() кэширует новый объект canvas (см. updateGallery() ) и обновляет макеты новым изображением, вызывая updateLayouts() , который является методом, который делает магию .

updateLayouts() использует следующие три метода:

  • setImageMeasures() : этот определяет адекватную ширину и высоту для изображений, учитывая, сколько было сделано.
  • setSourceCoordinates() : проверяя измерения изображения, он возвращает координаты центра изображения.
  • setTargetCoordinates() : этот учитывает индекс изображения, которое будет нарисовано, и возвращает координаты, где изображения будут нарисованы на целевом холсте.

В дополнение к этому, метод calcCoeficient calculateCoeficient() позаботится о сохранении пропорций между оригинальным изображением и тем, которое будет сгенерировано, путем сравнения исходного и целевого показателей холста.

Наконец, updateLayout() рисует изображение на новом холсте, используя context.drawImage() с данными из четырех функций выше. Используемая реализация будет той, которая использует свои восемь параметров . Это означает, что мы указываем исходные координаты, исходные меры, целевые координаты и целевые меры.

Модуль Макеты

Модуль Layouts предоставляет основные данные макета вместе с некоторыми вспомогательными функциями.

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

 // Layouts module (layouts.js) var CANVAS_MAX_MEASURE = 200, LAYOUT_TYPES = { HORIZONTAL: 'horizontal', VERTICAL: 'vertical' }, LAYOUTS = [ { type: LAYOUT_TYPES.VERTICAL }, { type: LAYOUT_TYPES.HORIZONTAL } ]; return { getCanvasMaxWidth: function() { return CANVAS_MAX_MEASURE; }, getLayouts: function() { return LAYOUTS.concat(Custom.getCustomLayouts()); }, isHorizontal: function(layout) { return layout.type === LAYOUT_TYPES.HORIZONTAL; }, isVertical: function(layout) { return layout.type === LAYOUT_TYPES.VERTICAL; }, isAvailable: function(layout, totalImages) { return !layout.minImages || layout.minImages <= totalImages; } } 

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

Вот что каждый из этих методов вносит в приложение:

  • getCanvasMaxWidth() : чтобы сохранить изображения в чистоте, я определил для них ширину по умолчанию и назначил ее CANVAS_MAX_MEASURE . Это значение используется в модуле App для определения комбинированных мер изображения. См. Фрагмент ниже для фактической математики в модуле App .
 // App module (app.js) setCanvasMeasures = function (canvas) { measures.height = canvas.height * MAX_MEASURE / canvas.width; }, 

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

  • getLayouts() : возвращает макеты, которые генерируют объединенные изображения из изображений, сделанных пользователем. Он возвращает оба макета приложений по умолчанию , а также пользовательские макеты, которые можно добавить в Custom модуль (подробнее об этом позже).
  • isHorizontal() и isVertical() : макет по умолчанию в приложении определяется путем установки его атрибута type , который получает свои значения из LAYOUT_TYPES . Получая объект layout в качестве аргумента и полагаясь на эту константу, эти два метода оценивают layout.type === LAYOUT_TYPES.HORIZONTAL и layout.type === LAYOUT_TYPES.VERTICAL . На основе значений, возвращаемых этими функциями, модуль App определяет меры, исходные координаты и целевые координаты для комбинированных изображений.
  • isAvailable() : в зависимости от того, сколько изображений снял пользователь и с учетом атрибута minImages макета, эта функция определяет, должен ли макет отображаться или нет. Если пользователь взял столько изображений или больше, чем те, которые установлены как минимум, то макет будет обработан. В противном случае, если пользователь не сделал столько фотографий или макет не определил атрибут minImages , будет сгенерировано combined изображение.

Пользовательский модуль

Custom модуль позволяет добавлять новые макеты с собственной реализацией трех основных методов приложения: setImageMeasures() , setSourceCoordinates() и setTargetCoordinates() .

Это может быть достигнуто путем добавления нового объекта макета в массив CUSTOM_LAYOUTS Custom модуля с его собственной реализацией вышеупомянутых трех методов.

 // Custom module (custom.js) var CUSTOM_LAYOUTS = [ /** * Place your custom layouts as below */ // , // { // setImageMeasures: function (layout, targetCanvas, imageIndex) { // return { // height: 0, // width: 0 // } // }, // setSourceCoordinates: function (canvas, layout, imageWidth, imageHeight, imageIndex) { // return { // x: 0, // y: 0 // } // }, // setTargetCoordinates: function (targetCanvas, layout, imageWidth, imageHeight, imageIndex) { // return { // x: 0, // y: 0 // } // } // } ]; 

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

Посмотрите, как это делается в App.setImageMeasures() :

 // App module (app.js) setImageMeasures = function (layout, targetCanvas, imageIndex) { if (isFunction(layout.setImageMeasures)) { return layout.setImageMeasures(layout, targetCanvas, imageIndex); } else { if(Layouts.isVertical(layout)) { return { width: $(targetCanvas).width(), height: $(targetCanvas).height() / images.length }; } else if(Layouts.isHorizontal(layout)) { return { width: $(targetCanvas).width() / images.length, height: $(targetCanvas).height() }; } return { width: $(targetCanvas).width(), height: $(targetCanvas).height() }; } } 

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

Это достигается с isFunction() помощника isFunction() , который проверяет, является ли полученный аргумент на самом деле функцией, проверяя его тип.

 // App module (app.js) isFunction = function(f) { return typeof f === 'function'; } 

Если текущий модуль не содержит собственную реализацию setImageMeasures() , приложение setImageMeasures() и устанавливает меры в соответствии с типом макета ( HORIZONTAL или VERTICAL ).

За тем же потоком следуют setSourceCoordinates() и setTargetCoordinates() .

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

Одна важная деталь — помнить, что пользовательские методы макета должны возвращать объекты с теми же атрибутами, что и исходные методы.

Чтобы быть более понятным, ваша пользовательская реализация setImageMeasures() должна возвращать что-то в этом формате:

 { height: 0, // height in pixels width: 0 // width in pixels } 

Создание собственного макета

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

Как видно из раздела модуля « Layouts », макеты могут иметь определенный атрибут minImages . В этом случае давайте установим его на 3. Давайте также сделаем так, чтобы первое снятое изображение покрыло 60% целевого холста, а следующие два разделят оставшиеся 40%:

 { minImages: 3, imageData: [ { widthPercent: 60, heightPercent: 100, targetX: 0, targetY: 0 }, { widthPercent: 20, heightPercent: 100, targetX: 120, targetY: 0 }, { widthPercent: 20, heightPercent: 100, targetX: 160, targetY: 0 }, ], // ... 

Чтобы достичь этого, давайте применим простое правило из трех, используя меры targetCanvas :

 // Custom module (custom.js) setImageMeasures: function (layout, targetCanvas, imageIndex) { var imageData = this.imageData[imageIndex]; if( imageData) { return { width: imageData.widthPercent * $(targetCanvas).width() / 100, height: imageData.heightPercent * $(targetCanvas).height() / 100 }; } return { height: 0, width: 0 } }, 

Поскольку все функции получают в качестве аргумента номер изображения ( imageIndex ), обрабатываемого в настоящее время, мы можем произвольно определить размер, исходные координаты обрезки и координаты, где сечение исходного изображения будет размещено на целевом холсте для каждого фотографий.

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

Давайте переопределим две другие функции:

setSourceCoordinates ()
Учитывая, что мы хотим включить центр изображения со всем его вертикальным содержимым, мы вернем объект с x, установленным на 50, и y, установленным на 0.

 setSourceCoordinates: function (canvas, layout, imageWidth, imageHeight, imageIndex) { return { x: 50, y: 0 } }, 

setTargetCoordinates ()
Поскольку мы знаем меры холста, давайте просто вручную определим, где они будут размещены на целевом холсте.

 setTargetCoordinates: function (targetCanvas, layout, imageWidth, imageHeight, imageIndex) { var imageData = this.imageData[imageIndex]; if (imageData) { return { x: imageData.targetX, y: imageData.targetY } } return { x: 0, y: 0 } } 

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

Вывод

Как мы видели, JpegCamera избавляет от необходимости использовать камеру пользователя в своем приложении, не беспокоясь о кросс-браузерной совместимости.

Включить его в свой проект так же просто, как добавить необходимые скрипты на страницу, и для его использования требуется понимание всего лишь четырех методов API. Написание забавного приложения занимает всего несколько сотен строк кода!

Как насчет вас, вам когда-нибудь нужно было работать со средствами массовой информации пользователя? Вы хотите попробовать реализовать свой собственный макет? Дай мне знать в комментариях!