photo-sphere-viewer
Version:
A JavaScript library to display Photo Sphere panoramas
160 lines (141 loc) • 4.13 kB
JavaScript
import { EASINGS } from '../data/constants';
import { each } from './misc';
/**
* @callback OnTick
* @summary Function called for each animation frame with computed properties
* @memberOf PSV.utils.Animation
* @param {Object.<string, number>} properties - current values
* @param {float} progress - 0 to 1
*/
/**
* @summary Interpolation helper for animations
* @memberOf PSV.utils
* @description
* Implements the Promise API with an additional "cancel" method.
* The promise is resolved with `true` when the animation is completed and `false` if the animation is cancelled.
* @example
* const anim = new Animation({
* properties: {
* width: {start: 100, end: 200}
* },
* duration: 5000,
* onTick: (properties) => element.style.width = `${properties.width}px`;
* });
*
* anim.then((completed) => ...);
*
* anim.cancel()
*/
export class Animation {
/**
* @param {Object} options
* @param {Object.<string, Object>} options.properties
* @param {number} options.properties[].start
* @param {number} options.properties[].end
* @param {number} options.duration
* @param {number} [options.delay=0]
* @param {string} [options.easing='linear']
* @param {PSV.utils.Animation.OnTick} options.onTick - called on each frame
*/
constructor(options) {
this.__callbacks = [];
if (options) {
if (!options.easing || typeof options.easing === 'string') {
options.easing = EASINGS[options.easing || 'linear'];
}
this.__start = null;
this.options = options;
if (options.delay) {
this.__delayTimeout = setTimeout(() => {
this.__delayTimeout = null;
this.__animationFrame = window.requestAnimationFrame(t => this.__run(t));
}, options.delay);
}
else {
this.__animationFrame = window.requestAnimationFrame(t => this.__run(t));
}
}
else {
this.__resolved = true;
}
}
/**
* @summary Main loop for the animation
* @param {number} timestamp
* @private
*/
__run(timestamp) {
if (this.__cancelled) {
return;
}
// first iteration
if (this.__start === null) {
this.__start = timestamp;
}
// compute progress
const progress = (timestamp - this.__start) / this.options.duration;
const current = {};
if (progress < 1.0) {
// interpolate properties
each(this.options.properties, (prop, name) => {
if (prop) {
current[name] = prop.start + (prop.end - prop.start) * this.options.easing(progress);
}
});
this.options.onTick(current, progress);
this.__animationFrame = window.requestAnimationFrame(t => this.__run(t));
}
else {
// call onTick one last time with final values
each(this.options.properties, (prop, name) => {
if (prop) {
current[name] = prop.end;
}
});
this.options.onTick(current, 1.0);
this.__animationFrame = window.requestAnimationFrame(() => {
this.__resolved = true;
this.__resolve(true);
});
}
}
/**
* @private
*/
__resolve(value) {
this.__callbacks.forEach(cb => cb(value));
this.__callbacks.length = 0;
}
/**
* @summary Promise chaining
* @param {Function} [onFulfilled] - Called when the animation is complete (true) or cancelled (false)
* @returns {Promise}
*/
then(onFulfilled) {
if (this.__resolved || this.__cancelled) {
return Promise.resolve(this.__resolved)
.then(onFulfilled);
}
return new Promise((resolve) => {
this.__callbacks.push(resolve);
})
.then(onFulfilled);
}
/**
* @summary Cancels the animation
*/
cancel() {
if (!this.__cancelled && !this.__resolved) {
this.__cancelled = true;
this.__resolve(false);
if (this.__delayTimeout) {
window.clearTimeout(this.__delayTimeout);
this.__delayTimeout = null;
}
if (this.__animationFrame) {
window.cancelAnimationFrame(this.__animationFrame);
this.__animationFrame = null;
}
}
}
}