VulpesOS/gaia/apps/camera/js/views/viewfinder.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

312 lines
7.9 KiB
JavaScript
Executable file

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 <video/> 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; }
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 '<div class="viewfinder-frame js-frame">' +
'<div class="viewfinder-video-container js-video-container" ' +
'aria-hidden="true">' +
'<video class="viewfinder-video js-video"></video>' +
'</div>' +
'<div class="viewfinder-grid">' +
'<div class="row"></div>' +
'<div class="row middle"></div>' +
'<div class="row"></div>' +
'<div class="column left">' +
'<div class="cell top"></div>' +
'<div class="cell middle"></div>' +
'<div class="cell bottom"></div>' +
'</div>' +
'<div class="column middle">' +
'<div class="cell top"></div>' +
'<div class="cell middle"></div>' +
'<div class="cell bottom"></div>' +
'</div>' +
'<div class="column right">' +
'<div class="cell top"></div>' +
'<div class="cell middle"></div>' +
'<div class="cell bottom"></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
}
});
});