sebastian-software/core

View on GitHub
source/class/core/effect/Animate.js

Summary

Maintainability
B
4 hrs
Test Coverage
/*
==================================================================================================
  Core - JavaScript Foundation
  Copyright 2010-2012 Zynga Inc.
  Copyright 2012-2014 Sebastian Werner
==================================================================================================
*/

"use strict";

(function()
{
    var time = Date.now;
    var desiredFrames = 60;
    var millisecondsPerSecond = 1000;
    var running = {};
    var counter = 1;
    var AnimationFrame = core.effect.AnimationFrame;

    /**
     * Generic animation class with support for dropped frames both optional easing and duration.
     *
     * Optional duration is useful when the lifetime is defined by another condition than time
     * e.g. speed of an animating object, etc.
     *
     * Dropped frame logic allows to keep using the same updater logic independent from the actual
     * rendering. This eases a lot of cases where it might be pretty complex to break down a state
     * based on the pure time difference.
     */
    core.Module("core.effect.Animate",
    {
        /**
         * {Integer} Start the animation. Returns the identifier of animation. Can be used to stop it any time.
         *
         * - @stepCallback {Function} Pointer to function which is executed on every step.
         *   Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
         * - @verifyCallback {Function} Executed before every animation step.
         *   Signature of the method should be `function() { return continueWithAnimation; }`
         * - @completedCallback {Function}
         *   Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
         * - @duration {Integer} Milliseconds to run the animation
         * - @easingMethod {Function} Pointer to easing function
         *   Signature of the method should be `function(percent) { return modifiedValue; }`
         * - @root {Element ? document.body} Render root, when available. Used for
         *   optimizing native requestAnimationFrame.
         */
        start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root)
        {
            var start = time();
            var lastFrame = start;
            var percent = 0;
            var dropCounter = 0;
            var id = counter++;

            if (!root) {
                root = document.body;
            }

            // Compacting running db automatically every few new animations
            if (id % 20 === 0)
            {
                var newRunning = {};
                for (var usedId in running) {
                    newRunning[usedId] = true;
                }

                running = newRunning;
            }

            // This is the internal step method which is called every few milliseconds
            var step = function(virtual)
            {
                // Normalize virtual value
                var render = virtual !== true;

                // Get current time
                var now = time();

                // Verification is executed before next animation step
                if (!running[id] || (verifyCallback && !verifyCallback(id)))
                {
                    running[id] = null;
                    completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
                    return;
                }

                // For the current rendering to apply let's update omitted steps in memory.
                // This is important to bring internal state variables up-to-date with progress in time.
                if (render)
                {
                    var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
                    for (var j = 0; j < Math.min(droppedFrames, 4); j++)
                    {
                        step(true);
                        dropCounter++;
                    }
                }

                // Compute percent value
                if (duration)
                {
                    percent = (now - start) / duration;
                    if (percent > 1) {
                        percent = 1;
                    }
                }

                // Execute step callback, then...
                var value = easingMethod ? easingMethod(percent) : percent;
                if ((stepCallback(value, now, render) === false || percent === 1) && render)
                {
                    running[id] = null;
                    completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
                }
                else if (render)
                {
                    lastFrame = now;
                    AnimationFrame.request(step, root);
                }
            };

            // Mark as running
            running[id] = true;

            // Init first step
            AnimationFrame.request(step, root);

            // Return unique animation ID
            return id;
        },


        /**
         * {Boolean} Stops the given animation via its @id {Integer}. Returns whether the animation was stopped.
         */
        stop: function(id)
        {
            var cleared = running[id] != null;
            if (cleared) {
                running[id] = null;
            }

            return cleared;
        },


        /**
         * {Boolean} Whether the given animation via its @id {Integer} is still running.
         */
        isRunning: function(id) {
            return running[id] != null;
        }
    });
})();