'use strict'; (function(exports) { /* * constructor */ var ScreenAutoBrightness = function ScreenAutoBrightness() { // does nothing }; /* * callback function to set screen brightness. */ ScreenAutoBrightness.prototype.onbrightnesschange = null; /* * We won't set a new brightness value if the difference between the current * and target brightness values is lower than this constant. */ ScreenAutoBrightness.prototype.AUTO_BRIGHTNESS_MIN_DELTA = 0.1; ScreenAutoBrightness.prototype.SYNTHETIC_BRIGHTNESS_DELAY = 250; ScreenAutoBrightness.prototype._previousBrightnessValues = null; // Start with half of max brightness. ScreenAutoBrightness.prototype._currentBrightness = 0.5; // The interval for stepping up in milliseconds. We use a constant speed for // moving the brightness upward. ScreenAutoBrightness.prototype.STEP_INTERVAL_MS_UP = 30; // We use a decelerated speed for moving the brightness downward. The interval // is STEP_INTERVAL_MS_DOWN * a variable multiplier (with a maximum of // MAX_STEP_DOWN_INTERVAL_MULTIPLIER). ScreenAutoBrightness.prototype.STEP_INTERVAL_MS_DOWN = 150; ScreenAutoBrightness.prototype.MAX_STEP_DOWN_INTERVAL_MULTIPLIER = 20; ScreenAutoBrightness.prototype.STEP_DELTA = 0.005; ScreenAutoBrightness.prototype.COOL_DOWN_MS_UP = 2000; ScreenAutoBrightness.prototype.COOL_DOWN_MS_DOWN = 5000; // The current state of the state machine. ScreenAutoBrightness.prototype._state = undefined; // State machine constants. ScreenAutoBrightness.prototype.STATE_COOLING_DOWN = 0; ScreenAutoBrightness.prototype.STATE_MOVING_UP = 1; ScreenAutoBrightness.prototype.STATE_MOVING_DOWN = 2; ScreenAutoBrightness.prototype.STATE_PAUSED = 3; /* * id of delay timeout */ ScreenAutoBrightness.prototype._delayTimerID = undefined; ScreenAutoBrightness.prototype._transitionTimerID = undefined; // The timestamp we set the screen brightness. ScreenAutoBrightness.prototype._brightnessSetTimestamp = 0; ScreenAutoBrightness.prototype.pause = function() { this._state = this.STATE_PAUSED; }; ScreenAutoBrightness.prototype.resume = function() { // Screen brightness might be set by another module. Reget the cached // brightness value. this._currentBrightness = navigator.mozPower.screenBrightness; this._state = this.STATE_COOLING_DOWN; }; // reinitialize to start state ScreenAutoBrightness.prototype.reset = function() { this._previousBrightnessValues = new ScreenAutoBrightnessValues(); if (this._delayTimerID) { clearTimeout(this._delayTimerID); } this._delayTimerID = undefined; if (this._transitionTimerID) { clearTimeout(this._transitionTimerID); } this._transitionTimerID = undefined; this._state = this.STATE_COOLING_DOWN; }; ScreenAutoBrightness.prototype.delayedSynthesizeBrightness = function() { this._delayTimerID = undefined; // Check again whether a new value has been pushed. if (Date.now() - this._previousBrightnessValues.getLatestTimestamp() >= this.SYNTHETIC_BRIGHTNESS_DELAY) { // No new value from the sensor. Synthesize one value. this._previousBrightnessValues.synthesizeWithLatest(); } this.maybeSynthesizeBrightness(); this.doAutoAdjust(); }; ScreenAutoBrightness.prototype.maybeSynthesizeBrightness = function() { if (this._delayTimerID) { clearTimeout(this._delayTimerID); this._delayTimerID = undefined; } // When the user enters a dark room, we might sensed the brightness values // like [0.5, 0.49, 0.45, 0.1], and then the sensor stops reporting new // values. We synthesize the sensed brightness value with a delayed timer // and adjust the brightness accordingly. if (Math.abs(this._previousBrightnessValues.getLatest() - this._currentBrightness) > this.AUTO_BRIGHTNESS_MIN_DELTA) { this._delayTimerID = setTimeout(this.delayedSynthesizeBrightness.bind(this), this.SYNTHETIC_BRIGHTNESS_DELAY); } }; /* * Automatically adjust the screen brightness based on the ambient * light (in lux) measured by the device light sensor */ ScreenAutoBrightness.prototype.autoAdjust = function(lux) { this._previousBrightnessValues.pushLuxValue(lux); this.maybeSynthesizeBrightness(); this.doAutoAdjust(); }; /** * Dispatch to the state handling function. */ ScreenAutoBrightness.prototype.doAutoAdjust = function() { switch (this._state) { case this.STATE_COOLING_DOWN: this.doAutoAdjustOnCoolingDown(); break; case this.STATE_MOVING_UP: this.doAutoAdjustOnMovingUp(); break; case this.STATE_MOVING_DOWN: this.doAutoAdjustOnMovingDown(); break; case this.STATE_PAUSED: this.doAutoAdjustOnPaused(); break; default: throw new Error('ScreenAutoBrightness: ' + 'unexpected state: ' + this._state); } }; /** * State machine. */ ScreenAutoBrightness.prototype.doAutoAdjustOnCoolingDown = function() { var now = Date.now(); var targetBrightness = this._previousBrightnessValues.getAverage(); if (targetBrightness > this._currentBrightness + this.AUTO_BRIGHTNESS_MIN_DELTA) { if (now - this._brightnessSetTimestamp < this.COOL_DOWN_MS_UP) { return; } this._state = this.STATE_MOVING_UP; this._transitionTimerID = setTimeout(this.doAutoAdjust.bind(this), this.STEP_INTERVAL_MS_UP); } else if (targetBrightness < this._currentBrightness - this.AUTO_BRIGHTNESS_MIN_DELTA) { if (now - this._brightnessSetTimestamp < this.COOL_DOWN_MS_DOWN) { return; } this._state = this.STATE_MOVING_DOWN; this._nextStepDownTimeout = 0; this._transitionTimerID = setTimeout(this.doAutoAdjust.bind(this), this.computeStepDownTimeout()); } }; ScreenAutoBrightness.prototype.doAutoAdjustOnMovingUp = function() { var now = Date.now(); var targetBrightness = this._previousBrightnessValues.getAverage(); if (!this._transitionTimerID || now - this._brightnessSetTimestamp < this.STEP_INTERVAL_MS_UP) { // We are no kicked by the transition timeout. return; } this._transitionTimerID = null; if (this._currentBrightness >= targetBrightness) { // Don't use Math.abs(): we may need to brake. this._state = this.STATE_COOLING_DOWN; return; } this.setBrightness(this._currentBrightness + this.STEP_DELTA); // Constant speed for moving up. this._transitionTimerID = setTimeout(this.doAutoAdjust.bind(this), this.STEP_INTERVAL_MS_UP); }; ScreenAutoBrightness.prototype.doAutoAdjustOnMovingDown = function() { var targetBrightness = this._previousBrightnessValues.getAverage(); var now = Date.now(); if (!this._transitionTimerID || now < this._nextStepDownTimeout) { // We are not kicked by the transition timeout. return; } this._transitionTimerID = null; if (this._currentBrightness <= targetBrightness) { this._state = this.STATE_COOLING_DOWN; return; } this.setBrightness(this._currentBrightness - this.STEP_DELTA); // Decelerated speed for moving down. var timeout = this.computeStepDownTimeout(); this._nextStepDownTimeout = now + timeout; this._transitionTimerID = setTimeout(this.doAutoAdjust.bind(this), timeout); }; ScreenAutoBrightness.prototype.doAutoAdjustOnPaused = function() { // Do nothing. }; ScreenAutoBrightness.prototype.computeStepDownTimeout = function() { var multiplier = Math.min(1 / (Math.abs(this._currentBrightness - this._previousBrightnessValues.getAverage())), this.MAX_STEP_DOWN_INTERVAL_MULTIPLIER); return this.STEP_INTERVAL_MS_DOWN * multiplier; }; // Adjust the screen brightness ScreenAutoBrightness.prototype.setBrightness = function(brightness) { if (typeof this.onbrightnesschange === 'function') { this._brightnessSetTimestamp = Date.now(); this._currentBrightness = Math.min(1.0, Math.max(0.1, brightness)); this.onbrightnesschange(this._currentBrightness); } else { console.log('ScreenAutoBrightness: ' + 'Brightness should change but no callback attached.'); } }; /** * Utility class for collecting and processing brightness values. */ function ScreenAutoBrightnessValues() { this._data = []; } // The size of the sliding window. ScreenAutoBrightnessValues.prototype.KEEP_BRIGHTNESS_VALUES = 5; /* * The auto-brightness algorithm will never set the screen brightness * to a value smaller than this. 0.1 seems like a good screen brightness * in a completely dark room on a Unagi. */ ScreenAutoBrightnessValues.prototype.AUTO_BRIGHTNESS_MINIMUM = 0.1; ScreenAutoBrightnessValues.prototype.DEFAULT_BRIGHTNESS_VALUE = 0.5; /* * This constant is used in the auto brightness algorithm. We take * the base 10 logarithm of the incoming lux value from the light * sensor and multiplied it by this constant. That value is used to * compute a weighted average with the current brightness and * finally that average brightess is and then clamped to the range * [AUTO_BRIGHTNESS_MINIMUM, 1.0]. * * Making this value larger will increase the brightness for a given * ambient light level. At a value of about .25, the screen will be * at full brightness in sunlight or in a well-lighted work area. * At a value of about .3, the screen will typically be at maximum * brightness in outdoor daylight conditions, even when overcast. */ ScreenAutoBrightnessValues.prototype.AUTO_BRIGHTNESS_CONSTANT = 0.27; /* * The sliding window keeping previous brightness values converted from lux * value. * The array elements are of object * { * _timestamp: (timestamp in ms), * _value: (brightness value [0.1, 1] * } */ ScreenAutoBrightnessValues.prototype._data = null; ScreenAutoBrightnessValues.prototype.getLatestTimestamp = function(lux) { if (this._data.lenght === 0) { return 0; } return this._getLatestObject()._timestamp; }; ScreenAutoBrightnessValues.prototype.synthesizeWithLatest = function() { if (this._data.length === 0) { return; } var latest = this._getLatestObject(); // Push the latest brightness object again and remove the head. this._data.push(latest); this._data.shift(); }; // Push a brightness value to the sliding window using the sensed lux value. ScreenAutoBrightnessValues.prototype.pushLuxValue = function(lux) { var self = this; var latest = { _timestamp: Date.now(), _value: self.computeBrightnessFromLux(lux) }; this._data.push(latest); if (this._data.length > this.KEEP_BRIGHTNESS_VALUES) { this._data.shift(); } }; ScreenAutoBrightnessValues.prototype._getLatestObject = function() { var lastIndex = this._data.length - 1; return this._data[lastIndex]; }; ScreenAutoBrightnessValues.prototype.getLatest = function() { if (this._data.length === 0) { return this.DEFAULT_BRIGHTNESS_VALUE; } return this._getLatestObject()._value; }; ScreenAutoBrightnessValues.prototype.getAverage = function() { if (this._data.length === 0) { return this.DEFAULT_BRIGHTNESS_VALUE; } var sum = 0; for (var i = 0; i < this._data.length; i++) { sum += this._data[i]._value; } var brightness = sum/this._data.length; return Math.max(this.AUTO_BRIGHTNESS_MINIMUM, Math.min(1.0, brightness)); }; // Brightness is log(lux), bounded in the range [0.1, 1]. ScreenAutoBrightnessValues.prototype.computeBrightnessFromLux = function(lux) { if (lux < 1) { // Can't take the log of 0 or negative numbers lux = 1; } var computedBrightness = Math.log10(lux) * this.AUTO_BRIGHTNESS_CONSTANT; var clampedBrightness = Math.max(this.AUTO_BRIGHTNESS_MINIMUM, Math.min(1.0, computedBrightness)); return clampedBrightness; }; exports.ScreenAutoBrightnessValues = ScreenAutoBrightnessValues; exports.ScreenAutoBrightness = ScreenAutoBrightness; }(window));