VulpesOS/gaia/apps/camera/js/controllers/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

445 lines
No EOL
12 KiB
JavaScript
Executable file

define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var cameraCoordinates = require('lib/camera-coordinates');
var debug = require('debug')('controller:viewfinder');
var ViewfinderView = require('views/viewfinder');
var FocusView = require('views/focus');
var FacesView = require('views/faces');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new ViewfinderController(app); };
module.exports.ViewfinderController = ViewfinderController;
/**
* Initialize a new `ViewfinderController`
*
* @param {App} app
*/
function ViewfinderController(app) {
bindAll(this);
this.app = app;
this.camera = app.camera;
this.activity = app.activity;
this.settings = app.settings;
this.createViews();
this.bindEvents();
this.configure();
debug('initialized');
}
/**
* Create and inject the views.
*
* @private
*/
ViewfinderController.prototype.createViews = function() {
this.views = {};
this.views.viewfinder = this.app.views.viewfinder || new ViewfinderView();
this.views.focus = this.app.views.focus || new FocusView();
this.views.faces = this.app.views.faces || new FacesView();
this.views.focus.appendTo(this.views.viewfinder.el);
this.views.faces.appendTo(this.views.viewfinder.el);
this.views.viewfinder.appendTo(this.app.el);
};
/**
* Initial configuration.
*
* @private
*/
ViewfinderController.prototype.configure = function() {
var settings = this.app.settings;
var zoomSensitivity = settings.viewfinder.get('zoomGestureSensitivity');
this.sensitivity = zoomSensitivity * window.innerWidth;
this.configureScaleType();
this.configureGrid();
};
/**
* Configure the viewfinder scale type,
* aspect fill/fit, depending on setting.
*
* @private
*/
ViewfinderController.prototype.configureScaleType = function() {
var scaleType = this.app.settings.viewfinder.get('scaleType');
this.views.viewfinder.scaleType = scaleType;
debug('set scale type: %s', scaleType);
};
/**
* Show/hide grid depending on currently
* selected option.
*
* @private
*/
ViewfinderController.prototype.configureGrid = function() {
var grid = this.app.settings.grid.selected('key');
this.views.viewfinder.set('grid', grid);
};
/**
* Hides the viewfinder frame-grid.
*
* @private
*/
ViewfinderController.prototype.hideGrid = function() {
this.views.viewfinder.set('grid', 'off');
};
/**
* Bind to relavant events.
*
* @private
*/
ViewfinderController.prototype.bindEvents = function() {
// View
this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible'));
this.views.viewfinder.on('fadedout', this.app.firer('viewfinder:hidden'));
this.views.viewfinder.on('click', this.app.firer('viewfinder:click'));
this.views.viewfinder.on('click', this.onViewfinderClicked);
// Tut tut, we shouldn't have direct coupling here.
// TODO: Camera events should be relayed through the app.
this.camera.on('zoomconfigured', this.onZoomConfigured);
this.camera.on('zoomchanged', this.onZoomChanged);
this.camera.on('preview:started', this.show);
// Camera
this.app.on('camera:autofocuschanged', this.views.focus.showAutoFocusRing);
this.app.on('camera:focusstatechanged', this.views.focus.setFocusState);
this.app.on('camera:focusconfigured', this.onFocusConfigured);
this.app.on('camera:shutter', this.views.viewfinder.shutter);
this.app.on('camera:facesdetected', this.onFacesDetected);
this.app.on('camera:configured', this.onCameraConfigured);
this.app.on('camera:previewactive', this.onPreviewActive);
this.app.on('busy', this.views.viewfinder.disable);
this.app.on('ready', this.views.viewfinder.enable);
this.app.on('camera:willchange', this.hide);
// Preview Gallery
this.app.on('previewgallery:opened', this.onGalleryOpened);
this.app.on('previewgallery:closed', this.onGalleryClosed);
// Settings
this.app.on('settings:closed', this.onSettingsClosed);
this.app.on('settings:opened', this.onSettingsOpened);
this.app.settings.grid.on('change:selected',
this.views.viewfinder.setter('grid'));
// Battery
this.app.on('battery:powersave', this.onPowerSave);
// App
this.app.on('pinch:changed', this.onPinchChanged);
this.app.on('hidden', this.stopStream);
};
/**
* Perform required viewfinder configuration
* once the camera has configured.
*
* @private
*/
ViewfinderController.prototype.onCameraConfigured = function() {
debug('configuring');
this.loadStream();
this.configurePreview();
};
/**
* Show the viewfinder.
*
* If the critical-path is not done we
* fade the viewfinder in straight away
* to make sure we have the quickest
* startup possible.
*
* We have to use a timeout for all other
* viewfinder showing actions conceal a Gecko
* rendering bug whereby the video element has
* not yet 'visually' switched to the new stream
* when we get the preview 'started' event from
* the camera. This means the user sees a flicker
* if we don't give it some time to adjust.
*
* We clear these timeouts to avoid multiple pending
* timeouts, which could cause pain.
*
* https://bugzilla.mozilla.org/show_bug.cgi?id=982230
*
* @private
*/
ViewfinderController.prototype.show = function() {
debug('show');
if (!this.app.criticalPathDone) {
this.views.viewfinder.fadeIn(true);
return;
}
clearTimeout(this.showTimeout);
this.showTimeout = setTimeout(this.views.viewfinder.fadeIn, 280);
debug('schedule delayed fade-in');
};
/**
* Fades the viewfinder in.
*
* We clear any pending timeouts here
* to prevent unusual behaviour ensuing.
*
* @private
*/
ViewfinderController.prototype.hide = function() {
debug('hide');
clearTimeout(this.showTimeout);
this.views.viewfinder.fadeOut();
};
/**
* Sets appropiate flags when the camera focus is configured
*/
ViewfinderController.prototype.onFocusConfigured = function(config) {
this.views.focus.setFocusMode(config.mode);
this.touchFocusEnabled = config.touchFocus;
this.views.faces.clear();
};
ViewfinderController.prototype.onFacesDetected = function(faces) {
var self = this;
var faceCircles = [];
var viewfinderSize = this.views.viewfinder.getSize();
var viewportHeight = viewfinderSize.height;
var viewportWidth = viewfinderSize.width;
var sensorAngle = this.camera.getSensorAngle();
var camera = this.app.settings.cameras.selected('key');
var isFrontCamera = camera === 'front';
faces.forEach(function(face, index) {
// Face comes in camera coordinates from gecko
var faceInPixels = cameraCoordinates.faceToPixels(
face.bounds, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
var faceCircle = self.calculateFaceCircle(faceInPixels);
faceCircles.push(faceCircle);
});
this.views.faces.show();
this.views.faces.render(faceCircles);
};
ViewfinderController.prototype.calculateFaceCircle = function(face) {
var diameter = Math.max(face.width, face.height);
var radius = diameter / 2;
return {
x: Math.round(face.left + face.width / 2 - radius),
y: Math.round(face.top + face.height / 2 - radius),
diameter: diameter
};
};
/**
* Start the viewfinder stream flowing
* with the current camera configuration.
*
* This indirectly enforces a screen wakelock,
* meaning the device is unable to go to sleep.
*
* We don't want the stream to start flowing if
* the preview-gallery is open, as this prevents
* the device falling asleep.
*
* @private
*/
ViewfinderController.prototype.loadStream = function() {
this.camera.loadStreamInto(this.views.viewfinder.els.video);
debug('stream started');
};
/**
* Stop the preview stream flowing.
*
* This indirectly removes the wakelock
* that is magically enforced by the
* flowing camera stream. Meaning the
* device is able to go to sleep.
*
* @private
*/
ViewfinderController.prototype.stopStream = function() {
this.views.viewfinder.stopStream();
debug('stream stopped');
};
/**
* Power save state changed. If entering power
* save mode, release the screen wake lock to
* allow it to timeout without user interaction,
* otherwise acquire the wake lock.
*
* @private
*/
ViewfinderController.prototype.onPowerSave = function(powerSave) {
this.views.viewfinder.els.video.mozUseScreenWakeLock = !powerSave;
};
/**
* Configure the size and postion
* of the preview video stream.
*
* @private
*/
ViewfinderController.prototype.configurePreview = function() {
var camera = this.app.settings.cameras.selected('key');
var isFrontCamera = camera === 'front';
var sensorAngle = this.camera.getSensorAngle();
var previewSize = this.camera.previewSize();
this.views.viewfinder.updatePreview(previewSize, sensorAngle, isFrontCamera);
};
/**
* Configures the viewfinder
* to the current camera.
*
* @private
*/
ViewfinderController.prototype.onZoomConfigured = function() {
var zoomSupported = this.camera.isZoomSupported();
var zoomEnabled = this.app.settings.zoom.enabled();
var enableZoom = zoomSupported && zoomEnabled;
if (!enableZoom) {
this.views.viewfinder.disableZoom();
return;
}
if (this.app.settings.zoom.get('useZoomPreviewAdjustment')) {
this.views.viewfinder.enableZoomPreviewAdjustment();
} else {
this.views.viewfinder.disableZoomPreviewAdjustment();
}
var minimumZoom = this.camera.getMinimumZoom();
var maximumZoom = this.camera.getMaximumZoom();
this.views.viewfinder.enableZoom(minimumZoom, maximumZoom);
};
/**
* Updates the zoom level on the camera
* when the pinch changes.
*
* @private
*/
ViewfinderController.prototype.onPinchChanged = function(deltaPinch) {
var zoom = this.views.viewfinder._zoom *
(1 + (deltaPinch / this.sensitivity));
this.views.viewfinder.setZoom(zoom);
this.camera.setZoom(zoom);
};
/**
* Responds to changes of the `zoom` value on the Camera to update the
* view's internal state so that the pinch-to-zoom gesture can resume
* zooming from the updated value. Also, updates the CSS scale transform
* on the <video/> tag to compensate for zooming beyond the
* `maxHardwareZoom` value.
*
* @param {Number} zoom
*/
ViewfinderController.prototype.onZoomChanged = function(zoom) {
var zoomPreviewAdjustment = this.camera.getZoomPreviewAdjustment();
this.views.viewfinder.setZoomPreviewAdjustment(zoomPreviewAdjustment);
this.views.viewfinder.setZoom(zoom);
};
ViewfinderController.prototype.onViewfinderClicked = function(e) {
if (!this.touchFocusEnabled) { return; }
this.views.faces.hide();
this.changeFocusPoint(e.pageX, e.pageY);
};
ViewfinderController.prototype.changeFocusPoint = function(x, y) {
var viewfinderSize = this.views.viewfinder.getSize();
var viewportHeight = viewfinderSize.height;
var viewportWidth = viewfinderSize.width;
var sensorAngle = this.camera.getSensorAngle();
var focusAreaSize = 10;
var focusAreaHalfSide = Math.round(focusAreaSize / 2);
// Top Left corner of the area and its size
var focusAreaPixels = {
left: x - focusAreaHalfSide,
top: y - focusAreaHalfSide,
right: x + focusAreaHalfSide,
bottom: y + focusAreaHalfSide,
width: focusAreaSize,
height: focusAreaSize
};
var camera = this.app.settings.cameras.selected('key');
var isFrontCamera = camera === 'front';
var focusArea = cameraCoordinates.faceToCamera(
focusAreaPixels, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
var focusPoint = {
x: x,
y: y,
area: focusArea
};
this.views.focus.setPosition(x, y);
this.app.emit('viewfinder:focuspointchanged', focusPoint);
};
ViewfinderController.prototype.onSettingsOpened = function() {
this.hideGrid();
// Make viewfinder invisible to the screen reader since it is behind settings
// overlay.
this.views.viewfinder.set('ariaHidden', true);
};
ViewfinderController.prototype.onSettingsClosed = function() {
this.configureGrid();
// Make viewfinder visible to the screen reader again when settings are
// closed.
this.views.viewfinder.set('ariaHidden', false);
};
/**
* Disables the viewfinder stream
* and pinch events.
*
* @private
*/
ViewfinderController.prototype.onGalleryOpened = function() {
this.views.viewfinder.disable();
// Make viewfinder invisible to the screen reader since it is behind gallery
// overlay.
this.views.viewfinder.set('ariaHidden', true);
};
/**
* Enables the viewfinder stream
* and pinch events.
*
* @private
*/
ViewfinderController.prototype.onGalleryClosed = function() {
this.views.viewfinder.enable();
// Make viewfinder visible to the screen reader again when gallery is closed.
this.views.viewfinder.set('ariaHidden', false);
};
ViewfinderController.prototype.onPreviewActive = function(active) {
if (!active) {
this.stopStream();
}
};
});