define(function(require, exports, module) { 'use strict'; /** * Dependencies */ var debug = require('debug')('view:viewfinder'); var bind = require('lib/bind'); var CameraUtils = require('lib/camera-utils'); var View = require('view'); /** * Locals */ var isZoomEnabled = false; var scaleSizeTo = { fill: CameraUtils.scaleSizeToFillViewport, fit: CameraUtils.scaleSizeToFitViewport }; var clamp = function(value, minimum, maximum) { return Math.min(Math.max(value, minimum), maximum); }; module.exports = View.extend({ name: 'viewfinder', className: 'js-viewfinder', fadeTime: 360, initialize: function() { this.render(); this.getSize(); }, render: function() { this.el.innerHTML = this.template(); this.els.frame = this.find('.js-frame'); this.els.video = this.find('.js-video'); this.els.videoContainer = this.find('.js-video-container'); // Clean up delete this.template; debug('rendered'); return this.bindEvents(); }, bindEvents: function() { bind(this.el, 'click', this.onClick); bind(this.el, 'animationend', this.onShutterEnd); return this; }, /** * Stores the container size. * * We're using window dimensions * here to avoid causing costly * reflows. * * @public */ getSize: function() { this.width = window.innerWidth; this.height = window.innerHeight; return { width: this.width, height: this.height }; }, onClick: function(e) { this.emit('click', e); }, enableZoom: function(minimumZoom, maximumZoom) { if (minimumZoom) { this._minimumZoom = minimumZoom; } if (maximumZoom) { this._maximumZoom = maximumZoom; } isZoomEnabled = true; }, disableZoom: function() { this._minimumZoom = 1.0; this._maximumZoom = 1.0; this.setZoom(1.0); isZoomEnabled = false; }, _minimumZoom: 1.0, setMinimumZoom: function(minimumZoom) { this._minimumZoom = minimumZoom; }, _maximumZoom: 1.0, setMaximumZoom: function(maximumZoom) { this._maximumZoom = maximumZoom; }, _zoom: 1.0, /** * Update the internal state of the view so that any future * pinch-to-zoom gestures can correctly adjust the current zoom * level in the event that the zoom level is changed outside of * the pinch-to-zoom gesture (e.g.: ZoomBar). This gets called * when the `Camera` emits a `zoomchanged` event. */ setZoom: function(zoom) { if (!isZoomEnabled) { return; } this._zoom = clamp(zoom, this._minimumZoom, this._maximumZoom); }, _useZoomPreviewAdjustment: false, enableZoomPreviewAdjustment: function() { this._useZoomPreviewAdjustment = true; }, disableZoomPreviewAdjustment: function() { this._useZoomPreviewAdjustment = false; }, /** * Adjust the scale of the tag to compensate for the inability * of the Camera API to zoom the preview stream beyond a certain point. * This gets called when the `Camera` emits a `zoomchanged` event and is * calculated by `Camera.prototype.getZoomPreviewAdjustment()`. */ setZoomPreviewAdjustment: function(zoomPreviewAdjustment) { if (this._useZoomPreviewAdjustment) { this.els.video.style.transform = 'scale(' + zoomPreviewAdjustment + ')'; } }, stopStream: function() { this.els.video.mozSrcObject = null; }, fadeOut: function() { debug('fade-out'); var self = this; this.hide(); clearTimeout(this.fadeTimeout); this.fadeTimeout = setTimeout(function() { self.emit('fadedout'); }, this.fadeTime); }, fadeIn: function(firstRun) { debug('fade-in'); this.show(); if (firstRun) { this.emit('fadedin'); // Delay to prevent the duration from applying too early requestAnimationFrame(() => { this.el.style.transitionDuration = this.fadeTime + 'ms'; }); } else { this.fadeTimeout = setTimeout(this.firer('fadedin'), this.fadeTime); } }, /** * Triggers a quick shutter style animation. * * @private */ shutter: function() { this.el.classList.add('shutter'); }, /** * Force a reflow before removing * the shutter class so that it * doesn't impact the animation. * * @private */ onShutterEnd: function() { this.reflow = this.el.offsetTop; this.el.classList.remove('shutter'); }, /** * Sizes and positions the preview stream. * * @param {Object} preview * @param {Number} sensorAngle * @param {Boolean} mirrored */ updatePreview: function(preview, sensorAngle, mirrored) { if (!preview) { return; } // An embedded app can initialize before its window is visible. this.getSize(); if (!this.width || !this.height) { return; } var aspect; // Invert dimensions if the camera's `sensorAngle` is // 0 or 180 degrees. if (sensorAngle % 180 === 0) { this.container = { width: this.width, height: this.height, aspect: this.width / this.height }; aspect = preview.height / preview.width; } else { this.container = { width: this.height, height: this.width, aspect: this.height / this.width }; aspect = preview.width / preview.height; } var shouldFill = aspect > this.container.aspect; var scaleType = this.scaleType || (shouldFill ? 'fill' : 'fit'); this.updatePreviewMetrics(preview, sensorAngle, mirrored, scaleType); }, /** * Calculates the correct sizing * depending on the chosen 'scaleType'. * * 'scale-type' attribute set as a styling hook. * * @param {Object} preview * @param {Number} sensorAngle * @param {Boolean} mirrored * @param {String} scaleType 'fill'|'fit' */ updatePreviewMetrics: function(preview, sensorAngle, mirrored, scaleType) { debug('update preview scaleType: %s', scaleType, preview); // Calculate the correct scale to apply to the // preview to either 'fill' or 'fit' the viewfinder // container (always preserving the aspect ratio). var landscape = scaleSizeTo[scaleType](this.container, preview); var portrait = { width: landscape.height, height: landscape.width }; // Set the size of the frame to match 'portrait' dimensions this.els.frame.style.width = portrait.width + 'px'; this.els.frame.style.height = portrait.height + 'px'; var transform = ''; if (mirrored) { transform += 'scale(-1, 1) '; } transform += 'rotate(' + sensorAngle + 'deg)'; // Set the size of the video container to match the // 'landscape' dimensions (CSS is used to rotate // the 'landscape' video stream to 'portrait') this.els.videoContainer.style.width = landscape.width + 'px'; this.els.videoContainer.style.height = landscape.height + 'px'; this.els.videoContainer.style.transform = transform; // CSS aligns the contents slightly // differently depending on the scaleType this.set('scaleType', scaleType); debug('updated preview size/position', landscape); }, template: function() { return '