diff options
| author | Andrej Mihajlov <and@codeispoetry.ru> | 2017-03-20 13:00:05 +0000 |
|---|---|---|
| committer | Andrej Mihajlov <and@codeispoetry.ru> | 2017-03-20 13:00:05 +0000 |
| commit | 0e3e9b765fa90f7beee7b7b89c884ebd7189f4e8 (patch) | |
| tree | a7706f06543e7ba5e6822d950faf4a41719e0a5e /app | |
| parent | 85f0ac9be65b0e81989e9d992e15b1f760b3b28d (diff) | |
| parent | 62b3e2784b8aab2c5a13b3eb0f18f2a158e6bb7f (diff) | |
| download | mullvadvpn-0e3e9b765fa90f7beee7b7b89c884ebd7189f4e8.tar.xz mullvadvpn-0e3e9b765fa90f7beee7b7b89c884ebd7189f4e8.zip | |
Merge branch 'feature/refactor-animations'
Diffstat (limited to 'app')
| -rw-r--r-- | app/lib/keyframe-animation.js | 349 | ||||
| -rw-r--r-- | app/lib/tray-animation.js | 264 | ||||
| -rw-r--r-- | app/lib/tray-animator.js | 113 | ||||
| -rw-r--r-- | app/lib/tray-icon-manager.js | 85 | ||||
| -rw-r--r-- | app/lib/tray-icon-provider.js | 47 | ||||
| -rw-r--r-- | app/main.js | 5 |
6 files changed, 386 insertions, 477 deletions
diff --git a/app/lib/keyframe-animation.js b/app/lib/keyframe-animation.js new file mode 100644 index 0000000000..b8f4a206d4 --- /dev/null +++ b/app/lib/keyframe-animation.js @@ -0,0 +1,349 @@ +import assert from 'assert'; +import { nativeImage } from 'electron'; + +/** + * Keyframe animation + * + * @export + * @class KeyframeAnimation + */ +export default class KeyframeAnimation { + + /** + * Set callback called on each frame update + * + * @type {function} + * @memberOf KeyframeAnimation + */ + set onFrame(v) { this._onFrame = v; } + + /** + * Get callback called on each frame update + * + * @readonly + * @type {function} + * @memberOf KeyframeAnimation + */ + get onFrame() { this._onFrame; } + + /** + * Set callback called when animation finished + * + * @type {function} + * @memberOf KeyframeAnimation + */ + set onFinish(v) { this._onFinish = v; } + + /** + * Get callback called when animation finished + * + * @readonly + * + * @memberOf KeyframeAnimation + */ + get onFinish() { this._onFinish; } + + /** + * Set animation pace per frame in ms + * + * @type {number} + * @memberOf KeyframeAnimation + */ + set speed(v) { this._speed = parseInt(v); } + + /** + * Get animation pace per frame in ms + * + * @readonly + * @type {number} + * @memberOf KeyframeAnimation + */ + get speed() { return this._speed; } + + /** + * Set animation repetition + * @type {bool} + * + * @memberOf KeyframeAnimation + */ + set repeat(v) { this._repeat = !!v; } + + /** + * Get animation repetition + * + * @readonly + * @type {bool} + * @memberOf KeyframeAnimation + */ + get repeat() { return this._repeat; } + + /** + * Set animation reversal + * @type {bool} + * @memberOf KeyframeAnimation + */ + set reverse(v) { this._reverse = !!v; } + + /** + * Get animation reversal + * + * @readonly + * @type {bool} + * @memberOf KeyframeAnimation + */ + get reverse() { return this._repeat; } + + /** + * Set animation alternation + * @type {bool} + * @memberOf KeyframeAnimation + */ + set alternate(v) { this._alternate = !!v; } + + /** + * Get animation alternation + * + * @readonly + * @type {bool} + * @memberOf KeyframeAnimation + */ + get alternate() { return this._alternate; } + + /** + * Source array of images + * + * @readonly + * @type {array} + * @memberOf KeyframeAnimation + */ + get source() { return this._source.slice(); } + + /** + * Array of NativeImage instances loaded based on source input + * + * @readonly + * @type {Electron.NativeImage[]} + * @memberOf KeyframeAnimation + */ + get nativeImages() { return this._nativeImages.slice(); } + + /** + * Flag that tells whether animation finished + * + * @readonly + * @type {bool} + * @memberOf KeyframeAnimation + */ + get isFinished() { return this._isFinished; } + + /** + * Create animation using file sequence + * + * @static + * @param {string} filePattern - file name pattern where {s} is replaced with index + * @param {number[]} range - sequence range [start, end] + * + * @memberOf KeyframeAnimation + * @return {KeyframeAnimation} + */ + static fromFileSequence(filePattern, range) { + assert(range.length === 2 && range[0] < range[1]); + + let images = []; + for(let i = range[0]; i <= range[1]; i++) { + images.push(filePattern.replace('{s}', i)); + } + + return new KeyframeAnimation(images); + } + + /** + * Creates an instance of KeyframeAnimation. + * @param {string[]} images + * + * @memberOf KeyframeAnimation + */ + constructor(images) { + assert(images.length > 0); + + this._source = images.slice(); + this._nativeImages = images.map((pathOrNativeImage) => { + if(typeof(pathOrNativeImage) === 'string') { + return nativeImage.createFromPath(pathOrNativeImage); + } else if((pathOrNativeImage + '') === '[object NativeImage]') { + return pathOrNativeImage; + } + return nativeImage.createEmpty(); + }); + + this._speed = 200; // ms + this._repeat = false; + this._reverse = false; + this._alternate = false; + + this._numFrames = images.length; + this._currentFrame = 0; + this._frameRange = [0, this._numFrames]; + this._isFinished = false; + + this._isFirstRun = true; + } + + /** + * Get current sprite + * + * @readonly + * @type {Electron.NativeImage} + * @memberOf KeyframeAnimation + */ + get currentImage() { + return this._nativeImages[this._currentFrame]; + } + + /** + * Prepare initial state for animation before running it. + * @param {object} [options = {}] - animation options + * @param {number} [options.startFrame] - start frame + * @param {number} [options.endFrame] - end frame + * @param {bool} [options.beginFromCurrentState] - continue animation from current state + * @param {string} [options.advanceTo] - resets current frame. (possible values: end) + * @memberOf KeyframeAnimation + */ + play(options = {}) { + let { startFrame, endFrame, beginFromCurrentState, advanceTo } = options; + + if(startFrame !== undefined && endFrame !== undefined) { + assert(startFrame >= 0 && startFrame < this._numFrames); + assert(endFrame >= 0 && endFrame < this._numFrames); + + if(startFrame < endFrame) { + this._frameRange = [ startFrame, endFrame ]; + } else { + this._frameRange = [ endFrame, startFrame ]; + } + } else { + this._frameRange = [ 0, this._numFrames - 1 ]; + } + + if(!beginFromCurrentState || this._isFirstRun) { + this._currentFrame = this._frameRange[this._reverse ? 1 : 0]; + } + + if(this._isFirstRun) { + this._isFirstRun = false; + } + + if(advanceTo === 'end') { + this._currentFrame = this._frameRange[this._reverse ? 0 : 1]; + } + + this._isFinished = false; + + this._unscheduleUpdate(); + + this._render(); + this._scheduleUpdate(); + } + + /** + * Stop animation + * @memberOf KeyframeAnimation + */ + stop() { + this._unscheduleUpdate(); + } + + _unscheduleUpdate() { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + } + + _scheduleUpdate() { + this._timeout = setTimeout(::this._onUpdateFrame, this._speed); + } + + _render() { + if(this._onFrame) { + this._onFrame(this._nativeImages[this._currentFrame]); + } + } + + _didFinish() { + if(this._onFinish) { + this._onFinish(); + } + } + + _onUpdateFrame() { + this._advanceFrame(); + + if(!this._isFinished) { + this._render(); + this._scheduleUpdate(); + } + } + + /** + * Advance animation frame + * @memberOf KeyframeAnimation + */ + _advanceFrame() { + // do not advance frame when animation is finished + if(this._isFinished) { return; } + + // advance frame + let didReachEnd = this._currentFrame === this._frameRange[this._reverse ? 0 : 1]; + + // did reach end? + if(didReachEnd) { + // mark animation as finished if it's not marked as repeating + if(!this._repeat) { + this._isFinished = true; + + this._didFinish(); + return; + } + + // change animation direction if marked for alternation + if(this._alternate) { + this._reverse = !this._reverse; + + this._currentFrame = this._nextFrame(this._currentFrame, this._frameRange, this._reverse); + } else { + this._currentFrame = this._frameRange[this._reverse ? 1 : 0]; + } + } else { + this._currentFrame = this._nextFrame(this._currentFrame, this._frameRange, this._reverse); + } + } + + /** + * Calculate next frame + * @private + * @param {number} cur - current frame + * @param {number[]} frameRange - frame range + * @param {bool} isReverse - reverse sequence direction? + * @returns {number} + * + * @memberOf KeyframeAnimation + */ + _nextFrame(cur, frameRange, isReverse) { + if(isReverse) { + if(cur < frameRange[0]) { + return cur + 1; + } else if(cur > frameRange[0]) { + return cur - 1; + } + } else { + if(cur > frameRange[1]) { + return cur - 1; + } else if(cur < frameRange[1]) { + return cur + 1; + } + } + return cur; + } + +}
\ No newline at end of file diff --git a/app/lib/tray-animation.js b/app/lib/tray-animation.js deleted file mode 100644 index 618d0383c0..0000000000 --- a/app/lib/tray-animation.js +++ /dev/null @@ -1,264 +0,0 @@ -import assert from 'assert'; -import { nativeImage } from 'electron'; - -/** - * Tray animation descriptor - * - * @export - * @class TrayAnimation - */ -export default class TrayAnimation { - - /** - * Set animation pace per frame in ms - * - * @type {number} - * @memberOf TrayAnimation - */ - set speed(v) { this._speed = parseInt(v); } - - /** - * Get animation pace per frame in ms - * - * @readonly - * @type {number} - * @memberOf TrayAnimation - */ - get speed() { return this._speed; } - - /** - * Set animation repetition - * @type {bool} - * - * @memberOf TrayAnimation - */ - set repeat(v) { this._repeat = !!v; } - - /** - * Get animation repetition - * - * @readonly - * @type {bool} - * @memberOf TrayAnimation - */ - get repeat() { return this._repeat; } - - /** - * Set animation reversal - * @type {bool} - * @memberOf TrayAnimation - */ - set reverse(v) { this._reverse = !!v; } - - /** - * Get animation reversal - * - * @readonly - * @type {bool} - * @memberOf TrayAnimation - */ - get reverse() { return this._repeat; } - - /** - * Set animation alternation - * @type {bool} - * @memberOf TrayAnimation - */ - set alternate(v) { this._alternate = !!v; } - - /** - * Get animation alternation - * - * @readonly - * @type {bool} - * @memberOf TrayAnimation - */ - get alternate() { return this._alternate; } - - /** - * Source array of images - * - * @readonly - * @type {array} - * @memberOf TrayAnimation - */ - get source() { return this._source.slice(); } - - /** - * Array of NativeImage instances loaded based on source input - * - * @readonly - * @type {Electron.NativeImage[]} - * @memberOf TrayAnimation - */ - get nativeImages() { return this._nativeImages.slice(); } - - /** - * Flag that tells whether animation finished - * - * @readonly - * @type {bool} - * @memberOf TrayAnimation - */ - get isFinished() { return this._isFinished; } - - /** - * Create animation using file sequence - * - * @static - * @param {string} filePattern - file name pattern where {s} is replaced with index - * @param {number[]} range - sequence range [start, end] - * - * @memberOf TrayAnimation - * @return {TrayAnimation} - */ - static fromFileSequence(filePattern, range) { - assert(range.length === 2 && range[0] < range[1]); - - let images = []; - for(let i = range[0]; i <= range[1]; i++) { - images.push(filePattern.replace('{s}', i)); - } - - return new TrayAnimation(images); - } - - /** - * Creates an instance of TrayAnimation. - * @param {string[]} images - * - * @memberOf TrayAnimation - */ - constructor(images) { - assert(images.length > 0); - - this._source = images.slice(); - this._nativeImages = images.map((pathOrNativeImage) => { - if(typeof(pathOrNativeImage) === 'string') { - return nativeImage.createFromPath(pathOrNativeImage); - } else if((pathOrNativeImage + '') === '[object NativeImage]') { - return pathOrNativeImage; - } - return nativeImage.createEmpty(); - }); - - this._speed = 200; // ms - this._repeat = false; - this._reverse = false; - this._alternate = false; - - this._numFrames = images.length; - this._currentFrame = 0; - this._isFinished = false; - } - - /** - * Get current sprite - * - * @readonly - * @type {Electron.NativeImage} - * @memberOf TrayAnimation - */ - get currentImage() { - return this._nativeImages[this._currentFrame]; - } - - /** - * Prepare initial state for animation before running it. - * @memberOf TrayAnimation - */ - prepare() { - this._currentFrame = this._firstFrame(this._reverse); - } - - /** - * Advance animation to the start. This method respects animation reversal - * - * @memberOf TrayAnimation - */ - advanceToStart() { - this._currentFrame = this._firstFrame(this._reverse); - } - - /** - * Advance animation to the end. This method respects animation reversal - * - * @memberOf TrayAnimation - */ - advanceToEnd() { - this._currentFrame = this._lastFrame(this._reverse); - } - - /** - * Advance animation frame - * @memberOf TrayAnimation - */ - advanceFrame() { - // do not advance frame when animation is finished - if(this._isFinished) { return; } - - // advance frame - let nextFrame = this._nextFrame(this._currentFrame, this._reverse); - - // did reach end? - if(nextFrame < 0 || nextFrame >= this._numFrames) { - // mark animation as finished if it's not marked as repeating - if(!this._repeat) { - this._isFinished = true; - return; - } - - // change animation direction if marked for alternation - if(this._alternate) { - this._reverse = !this._reverse; - - // clamp range - nextFrame = Math.min(Math.max(0, nextFrame), this._numFrames - 1); - - // skip corner frame when alternating by advancing frame once again - nextFrame = this._nextFrame(nextFrame, this._reverse); - } else { - nextFrame = this._firstFrame(this._reverse); - } - } - this._currentFrame = nextFrame; - } - - /** - * Calculate next frame - * @private - * @param {number} cur - current frame - * @param {bool} isReverse - reverse sequence direction? - * @returns {number} - * - * @memberOf TrayAnimation - */ - _nextFrame(cur, isReverse) { - return cur + (isReverse ? -1 : 1); - } - - /** - * Get first frame of animation - * - * @param {bool} isReverse reverse animation? - * @returns {number} - * - * @memberOf TrayAnimation - */ - _firstFrame(isReverse) { - return isReverse ? this._numFrames - 1 : 0; - } - - /** - * Get last frame of animation - * - * @param {bool} isReverse reverse animation? - * @returns {number} - * - * @memberOf TrayAnimation - */ - _lastFrame(isReverse) { - return isReverse ? 0 : this._numFrames - 1; - } - -}
\ No newline at end of file diff --git a/app/lib/tray-animator.js b/app/lib/tray-animator.js deleted file mode 100644 index 9fb03b5588..0000000000 --- a/app/lib/tray-animator.js +++ /dev/null @@ -1,113 +0,0 @@ -import assert from 'assert'; - -/** - * Tray icon animator - * @class TrayAnimator - */ -export default class TrayAnimator { - - /** - * Whether animator has started. - * @readonly - * @memberOf TrayAnimator - */ - get isStarted() { return this._started; } - - /** - * Creates an instance of TrayAnimator. - * @param {Electron.Tray} tray - an instance of Tray - * @param {TrayAnimation} animation - an instance of TrayAnimation - * - * @memberOf TrayAnimator - */ - constructor(tray, animation) { - assert(tray); - assert(animation); - - this._tray = tray; - this._animation = animation; - this._started = false; - this._timer = null; - } - - /** - * Advance animation to the start * - * @memberOf TrayAnimator - */ - advanceToStart() { - this._animation.advanceToStart(); - this._updateTrayIcon(); - } - - /** - * Advance animation to the end - * @memberOf TrayAnimator - */ - advanceToEnd() { - this._animation.advanceToEnd(); - this._updateTrayIcon(); - } - - /** - * Start animating - * @memberOf TrayAnimator - */ - start() { - if(this._started) { return; } - - this._timer = this._nextFrame(); - this._started = true; - - // prepare animation - this._animation.prepare(); - - // update from initial frame - this._updateTrayIcon(); - } - - /** - * Stop animating - * @memberOf TrayAnimator - */ - stop() { - if(!this._started) { return; } - - this._started = false; - - clearTimeout(this._timer); - this._timer = null; - } - - /** - * Schedules next animation frame - * @returns {number} timer ID - * @memberOf TrayAnimator - */ - _nextFrame() { - return setTimeout(::this._updateAnimationFrame, this._animation.speed); - } - - /** - * Updates animation frame - * @memberOf TrayAnimator - */ - _updateAnimationFrame() { - if(!this._started) { return; } - - this._animation.advanceFrame(); - this._updateTrayIcon(); - - if(!this._animation.isFinished) { - this._nextFrame(); - } - } - - /** - * Update tray icon with current frame - * @memberOf TrayAnimator - */ - _updateTrayIcon() { - this._tray.setImage(this._animation.currentImage); - } - -} diff --git a/app/lib/tray-icon-manager.js b/app/lib/tray-icon-manager.js index 987698a23d..82c226d3f6 100644 --- a/app/lib/tray-icon-manager.js +++ b/app/lib/tray-icon-manager.js @@ -1,7 +1,7 @@ import assert from 'assert'; -import TrayAnimator from './tray-animator'; -import TrayIconProvider from './tray-icon-provider'; +import path from 'path'; import { TrayIconType } from '../enums'; +import KeyframeAnimation from './keyframe-animation'; /** * Tray icon manager @@ -14,17 +14,19 @@ export default class TrayIconManager { /** * Creates an instance of TrayIconManager. * @param {Electron.Tray} tray - * @param {TrayIconProvider} iconProvider * * @memberOf TrayIconManager */ - constructor(tray, iconProvider) { + constructor(tray) { assert(tray); - assert(iconProvider); - this._tray = tray; - this._iconProvider = iconProvider; - this._animator = null; + const basePath = path.join(path.resolve(__dirname, '..'), 'assets/images/menubar icons'); + let filePath = path.join(basePath, 'lock-{s}.png'); + let animation = KeyframeAnimation.fromFileSequence(filePath, [1, 9]); + animation.onFrame = (img) => tray.setImage(img); + animation.speed = 100; + + this._animation = animation; this._iconType = null; } @@ -33,9 +35,9 @@ export default class TrayIconManager { * @memberOf TrayIconManager */ destroy() { - if(this._animator) { - this._animator.stop(); - this._animator = null; + if(this._animation) { + this._animation.stop(); + this._animation = null; } this._iconType = null; } @@ -67,64 +69,47 @@ export default class TrayIconManager { * * @memberOf TrayIconManager */ - _updateIconType(type) { + _updateIconType(type) { // no-op if same animator requested if(this._iconType === type) { return; } - // skip animation if: - // 1. there was no icon set before (which is usually when app starts) - // 2. unsecured -> securing - // 3. securing -> unsecured - const skip = this._iconType === null || - type === TrayIconType.securing || // unsecured -> securing - (type === TrayIconType.unsecured && this._iconType === TrayIconType.securing); // securing -> unsecured - - // do not animate if setting icon for the first time - this._updateType(type, skip); - } - - /** - * Get animation for iconType - * - * @param {TrayIconType} type - * @returns TrayIconAnimator - * - * @memberOf TrayIconManager - */ - _animationForType(type) { - switch(type) { - case TrayIconType.secured: return this._iconProvider.lockAnimation(); - case TrayIconType.unsecured: return this._iconProvider.unlockAnimation(); - case TrayIconType.securing: return this._iconProvider.unlockAnimation(); - } + this._updateType(type); } /** * Update icon animator with new type * * @param {TrayIconType} type - * @param {boolean} [skipAnimation=false] whether animation should be skipped * * @memberOf TrayIconManager */ - _updateType(type, skipAnimation = false) { + _updateType(type) { assert(TrayIconType.isValid(type)); - let animator = new TrayAnimator(this._tray, this._animationForType(type)); + let options = { beginFromCurrentState: true }; - // destroy existing animator - if(this._animator) { - this._animator.stop(); - this._animator = null; + switch(type) { + case TrayIconType.secured: + this._animation.reverse = false; + break; + case TrayIconType.securing: + case TrayIconType.unsecured: + this._animation.reverse = true; + break; } - if(skipAnimation) { - animator.advanceToEnd(); - } else { - animator.start(); + if(this._iconType === null) { + options.advanceTo = 'end'; } + + this._animation.play(options); + + // if(skipAnimation) { + // animator.advanceToEnd(); + // } else { + // animator.start(); + // } - this._animator = animator; this._iconType = type; } diff --git a/app/lib/tray-icon-provider.js b/app/lib/tray-icon-provider.js deleted file mode 100644 index aa50a71a71..0000000000 --- a/app/lib/tray-icon-provider.js +++ /dev/null @@ -1,47 +0,0 @@ -import path from 'path'; -import { EventEmitter } from 'events'; -import TrayAnimation from './tray-animation'; -import Enum from './enum'; - -const menubarIcons = { - base: path.join(path.resolve(__dirname, '..'), 'assets/images/menubar icons'), - lock: 'lock-{s}.png' -}; - -/** - * Tray icon provider - * - * @export - * @class TrayIconProvider - */ -export default class TrayIconProvider { - - /** - * Get lock animation - * - * @param {boolean} [isReverse=false] whether animation should be reversed - * @returns TrayIconAnimator - * - * @memberOf TrayIconProvider - */ - lockAnimation(isReverse = false) { - let filePath = path.join(menubarIcons.base, menubarIcons.lock); - let animation = TrayAnimation.fromFileSequence(filePath, [1, 9]); - animation.speed = 100; - animation.reverse = isReverse; - - return animation; - } - - /** - * Get unlock animation - * - * @returns TrayIconAnimator - * - * @memberOf TrayIconProvider - */ - unlockAnimation() { - return this.lockAnimation(true); - } - -}
\ No newline at end of file diff --git a/app/main.js b/app/main.js index 374ece212b..2534cfa089 100644 --- a/app/main.js +++ b/app/main.js @@ -1,7 +1,6 @@ import path from 'path'; import { app, crashReporter, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; import TrayIconManager from './lib/tray-icon-manager'; -import TrayIconProvider from './lib/tray-icon-provider'; // Override appData path to avoid collisions with old client // New userData path, i.e on macOS: ~/Library/Application Support/mullvad.vpn @@ -158,9 +157,9 @@ const showWindow = () => { const createTray = () => { tray = new Tray(nativeImage.createEmpty()); tray.on('click', toggleWindow); - tray.setHighlightMode('selection'); + tray.setHighlightMode('never'); - trayIconManager = new TrayIconManager(tray, new TrayIconProvider()); + trayIconManager = new TrayIconManager(tray); }; crashReporter.start({ |
