lib/Animation.js
/*!
* The MIT License (MIT)
*
* Copyright (c) 2016 Mykhailo Stadnyk <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* jshint -W079 */
const vendorize = require('./vendorize');
/**
* @ignore
* @typedef {object} ns
*/
/* istanbul ignore next */
/**
* @type {function(
* callback: function(time: number): number,
* element?: HTMLElement,
* )}
* @access private
*/
const requestAnimationFrame = vendorize('requestAnimationFrame') ||
(callback => setTimeout(() => callback(new Date().getTime()), 1000 / 60));
/**
* Generic AnimationRule function interface
*
* @typedef {function(percent: number): number} AnimationRule
*/
/**
* Callback for animation step draw event.
* It will be called each time animation step is executed, bypassing
* as first argument a percent of animation completeness. It is expected
* that this callback will do an actual work of animating an elements or
* whatever, as far as animation engine is just calculating and executing
* animation steps without any knowledge about things under animation.
*
* @typedef {function(percent: number): *} DrawEventCallback
*/
/**
* Callback for animation complete event.
* It is called once each animation is complete.
*
* @typedef {function(): *} EndEventCallback
*/
/**
* Predefined known animation rules.
* It's a simple collection of math for some most used animations.
*
* @typedef {{
* linear: AnimationRule,
* quad: AnimationRule,
* dequad: AnimationRule,
* quint: AnimationRule,
* dequint: AnimationRule,
* cycle: AnimationRule,
* decycle: AnimationRule,
* bounce: AnimationRule,
* debounce: AnimationRule,
* elastic: AnimationRule,
* delastic: AnimationRule,
* }} AnimationRules
*/
/* istanbul ignore next: no reason covering this */
let rules = {
linear: p => p,
quad: p => Math.pow(p, 2),
dequad: p => 1 - rules.quad(1 - p),
quint: p => Math.pow(p, 5),
dequint: p => 1 - Math.pow(1 - p, 5),
cycle: p => 1 - Math.sin(Math.acos(p)),
decycle: p => Math.sin(Math.acos(1 - p)),
bounce: p => 1 - rules.debounce(1 - p),
debounce: p => {
let a = 0, b = 1;
for (; 1; a += b, b /= 2) {
if (p >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * p) / 4, 2) +
Math.pow(b, 2);
}
}
},
elastic: p => 1 - rules.delastic(1 - p),
delastic: p => {
let x = 1.5;
return Math.pow(2, 10 * (p - 1)) *
Math.cos(20 * Math.PI * x / 3 * p);
}
};
/* istanbul ignore next: private, not testable */
/**
* Evaluates animation step and decides if the next step required or
* stops animation calling a proper events.
*
* @access private
* @param {number} time
* @param {DrawEventCallback} draw
* @param {number} start
* @param {AnimationRule} rule
* @param {number} duration
* @param {EndEventCallback} end
* @param {Animation} anim
*/
function step(
time,
draw,
start,
rule,
duration,
end,
anim
) {
if (typeof rule !== 'function') {
throw new TypeError('Invalid animation rule:', rule);
}
let progress = time - start;
let percent = progress / duration;
let animationTransformed = 0;
if (percent > 1) {
percent = 1;
}
if (percent !== 1) {
animationTransformed = rule(percent);
// make sure we have correct number after applying animation
// transformation
if (isFinite(animationTransformed) && !isNaN(animationTransformed)) {
percent = animationTransformed;
}
}
draw && draw(percent);
if (progress < duration) {
anim.frame = requestAnimationFrame(time =>
step(time, draw, start, rule, duration, end, anim)
);
}
else {
end && end();
anim.inProgress = false;
}
}
/**
* Animation engine API for JavaScript-based animations.
* This is simply an animation core framework which simplifies creation
* of various animations for generic purposes.
*
* @example
* // create 'linear' animation engine, 500ms duration
* let linear = new Animation('linear', 500);
*
* // create 'elastic' animation engine
* let elastic = new Animation('elastic');
*
* // define animation behavior
* let bounced = new Animation('bounce', 500, percent => {
* let value = parseInt(percent * 100, 10);
*
* $('div.bounced').css({
* width: value + '%',
* height: value + '%'
* });
* });
*
* // execute animation
* bounced.animate();
*
* // execute animation and handle when its finished
* bounced.animate(null, () => {
* console.log('Animation finished!');
* });
*/
export default class Animation {
/**
* @constructor
* @param {string|AnimationRule} rule
* @param {number} duration
* @param {DrawEventCallback} [draw]
* @param {EndEventCallback} [end]
*/
constructor(rule = 'linear', duration = 250, draw = (()=>{}),
end = (()=>{}))
{
/**
* Overall animation duration in milliseconds.
* By default is equal to 250 ms.
*
* @type {number}
*/
this.duration = duration;
/**
* Animation rule. By default is linear animation.
* Animation rule is a subject to animation rules, which are
* a simple object containing math-based methods for calculating
* animation steps.
*
* @type {string|AnimationRule}
*/
this.rule = rule;
/**
* Callback function for the animation step draw event.
*
* @type {DrawEventCallback}
*/
this.draw = draw;
/**
* Callback for the animation complete event.
*
* @type {EndEventCallback}
*/
this.end = end;
if (typeof this.draw !== 'function') {
throw new TypeError('Invalid animation draw callback:', draw);
}
if (typeof this.end !== 'function') {
throw new TypeError('Invalid animation end callback:', end);
}
}
/* istanbul ignore next: non-testable */
/**
* Performs animation calling each animation step draw callback and
* end callback at the end of animation. Callbacks are optional to this
* method call. If them are not bypassed will be used that ones which
* was pre-set on constructing an Animation object or pre-set after
* construction.
*
* @example
* function draw(percent) {
* $('.my-animated-divs').css({
* width: parseInt(percent * 100, 10) + '%'
* });
* }
* function done() {
* console.log('Animation complete!');
* }
*
* // Define 'draw' and 'end' callbacks on construction
* var animation = new Animation('cycle', 500, draw, done);
* animation.animate();
*
* // Define 'draw' and 'end' callbacks after construction
* var animation = new Animation('cycle', 500);
* animation.draw = draw;
* animation.end = done;
* animation.animate();
*
* // Define 'draw' and 'end' callbacks at animation
* var animation = new Animation('cycle', 500);
* animation.animate(draw, done);
*
* @param {DrawEventCallback} [draw]
* @param {EndEventCallback} [end]
*/
animate(draw, end) {
this.frame && this.cancel();
// noinspection JSUnresolvedVariable
const start = window.performance && window.performance.now ?
window.performance.now() :
(vendorize('animationStartTime') || Date.now());
draw = draw || this.draw;
end = end || this.end;
this.draw = draw;
this.end = end;
/**
* Current requested animation frame identifier
*
* @type {number}
*/
this.frame = requestAnimationFrame(time =>
step(time, draw, start, rules[this.rule] || this.rule,
this.duration, end, this));
}
/**
* Cancels current animation if any
*/
cancel() {
if (this.frame) {
const cancelAnimationFrame = vendorize('cancelAnimationFrame') ||
/* istanbul ignore next */
((id) => {});
cancelAnimationFrame(this.frame);
this.frame = null;
}
}
/**
* Destroys this object properly
*/
destroy() {
this.cancel();
this.draw = null;
this.end = null;
}
}
/**
* Animation rules bound statically to Animation constructor.
*
* @type {AnimationRules}
* @static
*/
Animation.rules = rules;
module.exports = Animation;