﻿// Motion library from 82 Protons, a software design/build studio
// http://www.82protons.com

// This software is licensed. You are free to use it in your commerical or non-commercial
// product as long as the above lines of attribution are maintained.

// Version history
// 1.0 - initial implementation
// 1.1 - replaced nextAnimation with callback property on Animation object

// Set up the namespace

var eightyTwoProtons;
if (!eightyTwoProtons) eightyTwoProtons = {};
else if (typeof eightyTwoProtons != "object")
    throw new Error("eightyTwoProtons already exists and is not an object");
if (eightyTwoProtons.Motion) throw new Error("eightyTwoProtons.Motion already exists")
else eightyTwoProtons.Motion = {};

// Constants

eightyTwoProtons.Motion.VERSION = [1, 1];

// Private members

eightyTwoProtons.Motion._animations = [];
eightyTwoProtons.Motion._frameDurationInMilliseconds = 30;
eightyTwoProtons.Motion._animationTimerInterval = null;

// Accessor/setter functions for private or primitive values

// _frameDurationInMilliseconds, must be between 15 and 10000
eightyTwoProtons.Motion.getFrameDurationInMilliseconds = function() {
    return eightyTwoProtons.Motion._frameDurationInMilliseconds;
}
eightyTwoProtons.Motion.setFrameDurationInMilliseconds = function(newDuration) {
    if ((newDuration > 15) && (newDuration < 10000))
        eightyTwoProtons.Motion._frameDurationInMilliseconds = newDuration;
}

// Public members

eightyTwoProtons.Motion.deleteAnimationsForElement = function(element) {
    for (var i = eightyTwoProtons.Motion._animations.length - 1; i >= 0; i--)
    {
        if (eightyTwoProtons.Motion._animations[i].element == element)
        {
            eightyTwoProtons.Motion._animations.splice(i, 1);
        }
    }
}

eightyTwoProtons.Motion.isElementBeingAnimated = function(element)
{
    for (var i = 0; i < eightyTwoProtons.Motion._animations.length; i++)
    {
        if (eightyTwoProtons.Motion._animations[i].element == element) return true;
    }
    return false;
}

eightyTwoProtons.Motion.getLength = function() { return (eightyTwoProtons.Motion._animations.length); }

eightyTwoProtons.Motion.toString = function() { return (eightyTwoProtons.Motion._animations.length + " animations in list"); }

// Display the next frame in the animation and set isAnythingMovingNow
eightyTwoProtons.Motion.displayFrame = function()
{
    var completedAnimationsToDelete = [];
    for (var i = 0; i < eightyTwoProtons.Motion._animations.length; i++)
    {
        if (eightyTwoProtons.Motion._animations[i])
        {
            var anim = eightyTwoProtons.Motion._animations[i];
            if (!anim.isPaused)
            {
                var haveReachedTemporarySpeed = false; // tripped if speed hits target, temporary rate
                if (anim.temporarySpeed < anim.speed)
                {
                    anim.speed -= anim.acceleration;
                    haveReachedTemporarySpeed = (anim.speed < anim.temporarySpeed);
                }
                else if (anim.temporarySpeed > anim.speed)
                {
                    anim.speed += anim.acceleration;
                    haveReachedTemporarySpeed = (anim.speed > anim.temporarySpeed);
                }
                if (haveReachedTemporarySpeed) anim.speed = anim.temporarySpeed;
                
                var haveReachedEnd = false; // tripped if animation hits end point
                switch (anim.property)
                {
                case "left":
                    if (anim.element.offsetLeft < anim.targetValue) 
                    {
                        anim.element.style.left = anim.element.offsetLeft + anim.speed + "px";
                        haveReachedEnd = (anim.element.offsetLeft >= anim.targetValue); 
                    }
                    else
                    {
                        anim.element.style.left = anim.element.offsetLeft - anim.speed + "px";
                        haveReachedEnd = (anim.element.offsetLeft <= anim.targetValue);
                    }
                    
                    if (haveReachedEnd) // reached destination, so prevent overshooting and trim animation from list
                    {
                        anim.element.style.left = anim.targetValue + "px";
                        completedAnimationsToDelete.unshift(i);
                    }
                    break;
                
                case "top":
                    if (anim.element.offsetTop < anim.targetValue) 
                    {
                        anim.element.style.top = anim.element.offsetTop + anim.speed + "px";
                        haveReachedEnd = (anim.element.offsetTop >= anim.targetValue);
                    }
                    else
                    {
                        anim.element.style.top = anim.element.offsetTop - anim.speed + "px";
                        haveReachedEnd = (anim.element.offsetTop <= anim.targetValue);
                    }
                    
                    if (haveReachedEnd)
                    {
                        anim.element.style.top = anim.targetValue + "px";
                        completedAnimationsToDelete.unshift(i);
                    }
                    break;
                    
                case "alpha":
                    {
                        var currentAlpha = eightyTwoProtons.Motion.getAlpha(anim.element);
                        if (currentAlpha < anim.targetValue)
                        {
                            var newAlpha = currentAlpha + anim.speed;
                            eightyTwoProtons.Motion.setAlpha(anim.element, newAlpha);
                            haveReachedEnd = (newAlpha >= anim.targetValue);
                        }
                        else
                        {
                            var newAlpha = currentAlpha - anim.speed;
                            eightyTwoProtons.Motion.setAlpha(anim.element, newAlpha);
                            haveReachedEnd = (newAlpha <= anim.targetValue);
                        }
                    }
                    if (haveReachedEnd) completedAnimationsToDelete.unshift(i);
                    break;
                }
            }
        }
    }
    // indices of completed animations are added to completedAnimationsToDelete in reverse order, so we
    // splice them out of the list in that order
    for (var j = 0; j < completedAnimationsToDelete.length; j++)
    {
        // if there's a callback, use setTimeout so that it gets called once this function unwinds
        if (eightyTwoProtons.Motion._animations[completedAnimationsToDelete[j]].callback) 
            setTimeout(eightyTwoProtons.Motion._animations[completedAnimationsToDelete[j]].callback, 0);
        eightyTwoProtons.Motion._animations.splice(completedAnimationsToDelete[j], 1);
    }
    if (eightyTwoProtons.Motion._animations.length == 0) eightyTwoProtons.Motion.stopAnimationLoop();
}

// Stop the timer for the animation loop, primarily used when the animation list is empty
eightyTwoProtons.Motion.stopAnimationLoop =
function()
{
    clearInterval(eightyTwoProtons.Motion._animationTimerInterval);
    eightyTwoProtons.Motion._animationTimerInterval = null;
}

eightyTwoProtons.Motion.startAnimationLoop =
function()
{
    if (!eightyTwoProtons.Motion._animationTimerInterval)
        eightyTwoProtons.Motion._animationTimerInterval = 
            setInterval(eightyTwoProtons.Motion.displayFrame, eightyTwoProtons.Motion._frameDurationInMilliseconds);
}


// Animation object

eightyTwoProtons.Motion.Animation = function(
    element,            // HTMLElement to animate
    property,           // property to animate, which maps loosely onto CSS properties of the same name, or "none,"
                        // which animates nothing that starts and stops with the parent animation list
    targetValue,        // the value the property should have at the end of the animation
    speed,              // how much that value should change each frame
    isPaused,           // if true, do not change property during displayFrame
    callback            // function to call when animation is complete
)
{
    if ("left|top|alpha|none".indexOf(property) == -1)
        throw new Error("property must be one of left, top, alpha, or none")
    else
        this.property = property;
    
    if ((this.property != "none") && !element)
        throw new Error("element not found")
    else
        this.element = element;
    
    if (-(-targetValue) != targetValue)
        throw new Error("targetValue must be a number")
    else
        this.targetValue = targetValue;
    
    if (-(-speed) != speed)
        throw new Error("speed must be a number")
    else
        this.speed = Math.abs(speed);
    
    this.isPaused = (isPaused == true);
    
    if (callback) 
        this.callback = callback 
    else 
        this.callback = null;
        
    this.defaultSpeed = this.speed;
    this.temporarySpeed = this.speed;
    this.acceleration = (this.property == "alpha" ? 0.1 : 1); // default acceleration rates for changes in speed
    
    eightyTwoProtons.Motion._animations.push(this);
    eightyTwoProtons.Motion.startAnimationLoop();
}

eightyTwoProtons.Motion.Animation.prototype.toString = function() { return (this.property + " to " + this.targetValue + " for element " + this.element.id); }

eightyTwoProtons.Motion.Animation.prototype.pause = function() { this.isPaused = true; }

eightyTwoProtons.Motion.Animation.prototype.unpause = function() { this.isPaused = false; }

eightyTwoProtons.Motion.Animation.prototype.setTemporarySpeed = function(newSpeed, accel)
{
    if (-(-newSpeed) != newSpeed)
        throw new Error("newSpeed must be a number")
    else
        this.temporarySpeed = Math.abs(newSpeed);
        
    if (accel)
    {
        if (-(-accel) != accel)
            throw new Error("accel must be a number")
        else
            this.acceleration = Math.abs(accel);
    }
}

eightyTwoProtons.Motion.Animation.prototype.resetToDefaultSpeed = function() { this.speed = this.defaultSpeed; this.temporarySpeed = this.defaultSpeed; }

eightyTwoProtons.Motion.Animation.prototype.deleteAnimation = function()
{
    for (var i = 0; i < eightyTwoProtons.Motion._animations.length; i++)
    {
        if (eightyTwoProtons.Motion._animations[i] == this) 
        {
            eightyTwoProtons.Motion._animations.splice(i, 1);
            break;
        }
    }
}

eightyTwoProtons.Motion.setIdealLeft = function(element, position)
{
    element.idealLeft = position;
    element.style.left = Math.round(position) + "px";
}

eightyTwoProtons.Motion.setIdealTop = function(element, position)
{
    element.idealTop = position;
    element.style.top = Math.round(position) + "px";
}

eightyTwoProtons.Motion.setAlpha = function(element, percent)
{
    if (percent < 0) percent = 0;
    if (percent > 1) percent = 1;
    if (-(-percent) != percent) percent = 1;
    
    element.style["filter"] = "alpha(opacity=" + Math.floor(percent * 100)+ ")";
    element.style["opacity"] = percent;
    element.style["-moz-opacity"] = percent;
}

eightyTwoProtons.Motion.getAlpha = function(element) 
{ 
    var opacity = 1.0;
    
    if (element.style.opacity)
    {
        opacity = Math.abs(element.style.opacity);
    }
    else
    {
        var sz = element.style.filter;
        opacity = Math.abs(sz.substring(sz.indexOf("=") + 1, sz.indexOf(")")));
    }
    return opacity;
}

