Source: wjbryant-1.1.1.js

/*! WJBRYANT JavaScript Library v1.1.1 | http://wjbryant.com/projects/wjbryant-javascript-library/
Copyright (c) 2013 Bill Bryant | http://opensource.org/licenses/mit */

/*jslint browser: true */
/*global ActiveXObject */

/**
 * @fileOverview The WJBRYANT JavaScript Library
 * @author       Bill Bryant
 * @version      1.1.1
 */

/*
 * NOTE
 * This library was written mainly for educational purposes and may contain
 * errors or bugs. It has not been fully tested and is not fully optimized.
 * New versions may change the API.
 */

(function (window, document) {
    'use strict';

    /**
     * The WJBRYANT namespace. If the WJBRYANT object is already defined, it
     * will not be overwritten. However, any properties or methods of the same
     * name will be overwritten.
     *
     * @namespace WJBRYANT
     */
    if (typeof window.WJBRYANT !== 'object') {
        window.WJBRYANT = {};
    }

    /**
     * Alias for the WJBRYANT namespace.
     *
     * @namespace WJB
     */
    window.WJB = window.WJBRYANT;

    var ns = window.WJB,
        // requestAnimationFrame polyfill by Erik Möller
        // ({@link http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating})
        // fixes from Paul Irish and Tino Zijdel
        // ({@link http://paulirish.com/2011/requestanimationframe-for-smart-animating/} and
        // {@link https://gist.github.com/1579671})
        i,
        vendors = ['ms', 'moz', 'webkit', 'o'],
        requestAnimationFrame = window.requestAnimationFrame,
        cancelAnimationFrame = window.cancelAnimationFrame,
        rAFLastTime = 0;

    for (i = 0; i < vendors.length && !requestAnimationFrame; i += 1) {
        requestAnimationFrame = window[vendors[i] + 'RequestAnimationFrame'];
        cancelAnimationFrame = window[vendors[i] + 'CancelAnimationFrame'] ||
            window[vendors[i] + 'CancelRequestAnimationFrame'];
    }

    if (!requestAnimationFrame) {
        requestAnimationFrame = function (callback) {
            var currTime = new Date().getTime(),
                timeToCall = Math.max(0, 16 - (currTime - rAFLastTime));

            rAFLastTime = currTime + timeToCall;

            return setTimeout(function () {
                callback(currTime + timeToCall);
            }, timeToCall);
        };
    }

    if (!cancelAnimationFrame) {
        cancelAnimationFrame = function (id) {
            clearTimeout(id);
        };
    }

    /**
     * A reference to the document head.
     *
     * @type Element
     * @name head
     * @memberOf WJBRYANT
     */
    ns.head = document.head || document.getElementsByTagName('head')[0];

    /**
     * Removes the leading and trailing whitespace from a string.
     * ({@link http://blog.stevenlevithan.com/archives/faster-trim-javascript})
     *
     * @param  {string} str  the string to trim
     * @return {string}      the trimmed string
     *
     * @method trim
     * @memberOf WJBRYANT
     */
    ns.trim = String.prototype.trim ?
            function (str) {
                return str.trim();
            } :
            function (str) {
                return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
            };

    if (document.addEventListener) {
        /**
         * Registers an event listener in a cross-browser compatible way. Note
         * that this only supports the DOM Level 2 and IE event registration
         * models. If this method is called in a browser that does not support
         * one of these models, it will do nothing. Based on the original code
         * by Dean Edwards and Tino Zijdel
         * ({@link http://therealcrisp.xs4all.nl/upload/addEvent_dean.html})
         * and also inspired by David Flanagan's Handler.js
         * ({@link http://www.davidflanagan.com/javascript5/display.php?n=17-2&f=17/Handler.js}).
         *
         * @param {Object}   obj   the object on which to register the event
         * @param {string}   type  the type of event - Example: 'load' or 'mouseover'
         * @param {Function} fn    the function to be executed when the event fires
         *
         * @method addListener
         * @memberOf WJBRYANT
         */
        ns.addListener = function (obj, type, fn) {
            obj.addEventListener(type, fn, false);
        };

        /**
         * Unregisters an event listener in a cross-browser compatible way.
         * Note that this only supports the DOM Level 2 and IE event
         * registration models. If this method is called in a browser that does
         * not support one of these models, it will do nothing. Based on the
         * original code by Dean Edwards and Tino Zijdel
         * ({@link http://therealcrisp.xs4all.nl/upload/addEvent_dean.html})
         * and also inspired by David Flanagan's Handler.js
         * ({@link http://www.davidflanagan.com/javascript5/display.php?n=17-2&f=17/Handler.js}).
         *
         * @param {Object}   obj   the object on which the event is registered
         * @param {string}   type  the type of event - Example: 'load' or 'mouseover'
         * @param {Function} fn    the function to be unregistered
         *
         * @method removeListener
         * @memberOf WJBRYANT
         */
        ns.removeListener = function (obj, type, fn) {
            obj.removeEventListener(type, fn, false);
        };
    } else if (document.attachEvent) {
        /** @ignore */
        ns.addListener = function (obj, type, fn) {
            var addListener = ns.addListener,
                guid = fn.guid,
                lid; // the listener id

            if (!guid) {
                guid = fn.guid = addListener.guid;
                addListener.guid += 1;
            }

            if (!obj.events) {
                obj.events = {};
            }

            // if this function was already added, don't add it again
            if (!obj.events[type + guid]) {
                obj.events[type + guid] = function () {
                    // fix the "this" keyword and the event argument
                    // propagate the return value of the fn function
                    return fn.call(obj, addListener.fixEvent(event));
                };
                lid = addListener.guid;
                addListener.guid += 1;
                obj.events[type + guid].lid = lid;

                obj.attachEvent('on' + type, obj.events[type + guid]);

                // keep track of all the event listeners
                addListener.listeners[lid] = {
                    obj: obj,
                    type: type,
                    fnguid: guid
                };

                // clean up to prevent memory leaks in IE
                if (!addListener.onunloadRegistered) {
                    addListener.onunloadRegistered = true;
                    window.attachEvent('onunload', ns.removeListener.removeAll);
                }
            }
        };
        // start at 1 so it always passes the truthy check in addListener
        ns.addListener.guid = 1;
        ns.addListener.listeners = {};

        // add some commonly used standard properties and methods to the event object
        // this only adds a very basic level of compatibility
        // the IE specific properties are not removed
        ns.addListener.fixEvent = function (e) {
            var addListener = ns.addListener;
            e.target = e.srcElement;
            e.preventDefault = addListener.preventDefault;
            e.stopPropagation = addListener.stopPropagation;
            return e;
        };

        ns.addListener.preventDefault = function () {
            this.returnValue = false;
        };
        ns.addListener.stopPropagation = function () {
            this.cancelBubble = true;
        };

        /** @ignore */
        ns.removeListener = function (obj, type, fn) {
            var guid = fn.guid,
                events = obj.events,
                func;

            if (events && guid) {
                func = events[type + guid];

                if (func) {
                    obj.detachEvent('on' + type, func);
                    delete events[type + guid];
                    delete ns.addListener.listeners[func.lid];
                }
            }
        };

        ns.removeListener.removeAll = function () {
            var lid,
                listeners = ns.addListener.listeners,
                curr,
                obj,
                type,
                fnguid,
                events,
                func;

            for (lid in listeners) {
                if (listeners.hasOwnProperty(lid)) {
                    curr = listeners[lid];
                    obj = curr.obj;
                    type = curr.type;
                    fnguid = curr.fnguid;
                    events = obj.events;
                    func = events[type + fnguid];

                    obj.detachEvent('on' + type, func);
                    delete events[type + fnguid];
                    delete listeners[func.lid];
                }
            }
        };
    } else {
        /**@ignore*/
        ns.removeListener = /**@ignore*/ ns.addListener = function () {};
    }

    /**
     * Dynamically loads a JavaScript file and executes the specified callback
     * function when complete. See discussion of this technique at
     * {@link http://www.nczonline.net/blog/2009/07/28/the-best-way-to-load-external-javascript/}
     *
     * @param {string}   url         the source location of the script to load
     * @param {Function} [callback]  the function to execute when the script is loaded
     *
     * @method loadScript
     * @memberOf WJBRYANT
     */
    ns.loadScript = function (url, callback) {
        if (typeof url !== 'string' || !url) {
            return;
        }

        var script = document.createElement('script');
        script.setAttribute('type', 'text/javascript');

        if (typeof callback === 'function') {
            if (script.readyState) { // IE
                script.onreadystatechange = function () {
                    if (script.readyState === 'loaded' || script.readyState === 'complete') {
                        script.onreadystatechange = null;
                        callback();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                };
            }
        }

        script.setAttribute('src', url);
        ns.head.appendChild(script);
    };

    /**
     * Gets the specified cookie's value.
     *
     * @param  {string} name  the name of the cookie to get
     * @return {string}       the cookie value
     *
     * @method getCookie
     * @memberOf WJBRYANT
     */
    ns.getCookie = function (name) {
        var start = document.cookie.indexOf(name + '='),
            len = start + name.length + 1,
            end = document.cookie.indexOf(';', len);
        if ((!start && name !== document.cookie.slice(0, name.length)) || start === -1) {
            return null;
        }
        if (end === -1) {
            end = document.cookie.length;
        }
        return decodeURIComponent(document.cookie.slice(len, end));
    };

    /**
     * Sets a cookie to the specified value.
     * If no cookie exists, then it is created.
     *
     * @param {string}  name       the name of the cookie
     * @param {string}  value      the value of the cookie
     * @param {number}  [expires]  the number of days in which the cookie will
     *                             expire
     * @param {string}  [path]     the cookie path
     * @param {string}  [domain]   the cookie domain
     * @param {boolean} [secure]   whether the cookie should only be sent over
     *                             a secure connection
     *
     * @method setCookie
     * @memberOf WJBRYANT
     */
    ns.setCookie = function (name, value, expires, path, domain, secure) {
        document.cookie = name + '=' + encodeURIComponent(value) +
            (expires ? ';max-age=' + (expires * 24 * 60 * 60) : '') +
            (path ? ';path=' + path : '') +
            (domain ? ';domain=' + domain : '') +
            (secure ? ';secure' : '');
    };

    /**
     * Deletes a cookie by setting its value to
     * nothing and its max-age to zero.
     *
     * @param {string} name      the name of the cookie to delete
     * @param {string} [path]    the path of the cookie to delete
     * @param {string} [domain]  the domain of the cookie to delete
     *
     * @method deleteCookie
     * @memberOf WJBRYANT
     */
    ns.deleteCookie = function (name, path, domain) {
        if (ns.getCookie(name)) {
            document.cookie = name + '=' +
                (path ? ';path=' + path : '') +
                (domain ? ';domain=' + domain : '') +
                ';max-age=0';
        }
    };

    /**
     * The WJBRYANT.Ajax namespace contains methods that provide common Ajax
     * functionality.
     *
     * @namespace Ajax
     * @memberOf WJBRYANT
     */
    ns.Ajax = {

        /**
         * Sends an Ajax request using the specified options. Ajax requests are
         * sent with a special 'X-Requested-With' header that can be used to
         * identify the requests on the server.
         *
         * @example
         * // these variables do not change with each request,
         * // so they are set outside of the function
         * var contactForm = document.getElementById('contactForm'),
         *     url = contactForm.getAttribute('action'),
         *     img = document.createElement('img'),
         *     imgContainer = document.getElementById('submitDiv'),
         *     submit = document.getElementById('submit');
         *
         * // preload loading animation image
         * img.setAttribute('src', '/img/loading.gif');
         * img.setAttribute('alt', 'Loading...');
         * img.className = 'loading';
         * img.setAttribute('width', '16');
         * img.setAttribute('height', '16');
         *
         * // send an Ajax request when the form is submitted
         * contactForm.onsubmit = function () {
         *     // disable the submit button so the user cannot submit the form
         *     // again while awaiting a response
         *     // if Ajax is used, it will be reenabled in the oncomplete callback
         *     // otherwise, it will remain disabled until the page is updated
         *     submit.disabled = true;
         *     imgContainer.appendChild(img);
         *
         *     return !WJB.Ajax.request({
         *         url: url,
         *         // the submit button will not be included with the data
         *         data: WJB.Ajax.scrape(contactForm),
         *         timeout: 4000,
         *         onsuccess: function (req) {
         *             var response;
         *             try {
         *                 // we are expecting data formatted as JSON
         *                 response = JSON.parse(req.responseText);
         *
         *                 // in this example, the server will set the
         *                 // message property in the response if the
         *                 // user should be notified of something
         *                 if (response.message) {
         *                     alert(response.message);
         *                 }
         *             } catch (err) {} // do nothing if text cannot be parsed
         *         },
         *         onfailure: function (req) {
         *             // check req.readyState
         *             // if it's not 0, check req.status for more info
         *             // otherwise, it was aborted (timed out) and req.status is not accessible
         *             if (req.readyState) {
         *                 alert('Error: Server returned status code ' + req.status);
         *             } else {
         *                 alert('Error: Request timed out');
         *             }
         *         },
         *         oncomplete: function (req) {
         *             imgContainer.removeChild(img);
         *             submit.disabled = false;
         *         }
         *     });
         * };
         * @param {Object} [options]  an object containing members specifying
         *                            the options of the request<br />
         *                            <br />
         *                            Valid members of this object are:
         * <pre>
         * {string}                 [url=location.href]   the url to send the request to
         *
         * {string}                 [method='GET']        the request method ('GET' or 'POST')
         *
         * {string|Object|FormData} [data=null]           the data to send with the request
         *
         * {number}                 [timeout]             the amount of time in milliseconds to wait
         *                                                for a response before canceling the request.
         *                                                The onfailure and oncomplete callbacks will
         *                                                be executed.
         *
         * {Function}               [onbeforesend]        the function to be executed before the
         *                                                request is sent. This function is passed the
         *                                                XMLHttpRequest object as a parameter.
         *
         * {Function}               [onuploadprogress]    the function to be executed everytime upload
         *                                                progress occurs (XHR Level 2). This function
         *                                                is passed the Event object as a parameter.
         *
         * {Function}               [ondownloadprogress]  the function to be executed everytime download
         *                                                progress occurs (XHR Level 2). This function
         *                                                is passed the Event object as a parameter.
         *
         * {Function}               [onsuccess]           the function to be executed when the response
         *                                                is received successfully. This function is
         *                                                passed the XMLHttpRequest object as a parameter.
         *
         * {Function}               [onfailure]           the function to be executed when the response
         *                                                is received and the status is not 200 or 304
         *                                                (an error occurred). This function is passed
         *                                                the XMLHttpRequest object as a parameter.
         *
         * {Function}               [oncomplete]          the function to be executed when the response
         *                                                is received whether or not it was successful.
         *                                                This function will always be executed after
         *                                                the onsuccess or onfailure function. This
         *                                                function is passed the XMLHttpRequest object
         *                                                as a parameter.
         * </pre>
         * @return {boolean}  whether or not the request was made successfully
         *
         * @method request
         * @memberOf WJBRYANT.Ajax
         */
        request: function (options) {

            options = options || {};

            /**
             * The XMLHttpRequest object.
             *
             * @memberOf WJBRYANT.Ajax.request
             * @private
             */
            var req = false,
                url = options.url || location.href,
                method = options.method ? options.method.toUpperCase() : 'GET',
                data = options.data || null,
                timeout = options.timeout || null,
                to,
                prop,
                dataPairs = [],
                space = /%20/g;

            // get the request object
            if (window.XMLHttpRequest) {
                req = new XMLHttpRequest();
            } else if (window.ActiveXObject) {
                try {
                    req = new ActiveXObject('Msxml2.XMLHTTP.6.0');
                } catch (e1) {
                    try {
                        req = new ActiveXObject('Microsoft.XMLHTTP');
                    } catch (e2) {
                        req = false;
                    }
                }
            }

            // if no request object could be obtained,
            // exit the method and return false
            if (!req) {
                return false;
            }

            // file contents will not be sent unless the browser supports FormData
            if (typeof data === 'object' && data !== null && data.constructor !== window.FormData) {
                for (prop in data) {
                    if (data.hasOwnProperty(prop)) { // just to be extra safe
                        dataPairs[dataPairs.length] = encodeURIComponent(prop).replace(space, '+') +
                            '=' + encodeURIComponent(data[prop].toString()).replace(space, '+');
                    }
                }
                data = dataPairs.join('&');
            }

            // progress event listeners must be set before open is called
            if (typeof options.onuploadprogress === 'function' && req.upload) {
                req.upload.onprogress = options.onuploadprogress;
            }

            if (typeof options.ondownloadprogress === 'function') {
                req.onprogress = options.ondownloadprogress;
            }

            // set up the request
            if (method === 'POST') {
                req.open(method, url, true);

                // if FormData is used, the Content-Type is set automatically
                // when the send method is called (multipart/form-data)
                if (typeof data === 'string' || data === null) {
                    req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                }
            } else {
                if (data) {
                    url += '?' + data;
                    data = null;
                }
                req.open(method, url, true);
            }

            // setting onreadystatechange after the call to open allows the same
            // XMLHttpRequest object to be reused by IE without a call to abort
            // (this code currently doesn't support that, but maybe in the future)

            /**
             * Called every time the readyState changes. If the readyState is 4
             * and the status is 200 (OK) or 304 (not modified), the
             * options.onsuccess function is called, otherwise the
             * options.onfailure function is called. Lastly, the options.oncomplete
             * function is called.
             *
             * @private
             * @function
             * @memberOf WJBRYANT.Ajax.request
             */
            req.onreadystatechange = function () {
                if (req.readyState === 4) {
                    if (to) {
                        clearTimeout(to);
                    }
                    if (req.status === 200 || req.status === 304) {
                        if (typeof options.onsuccess === 'function') {
                            options.onsuccess(req);
                        }
                    } else if (typeof options.onfailure === 'function') {
                        options.onfailure(req);
                    }
                    if (typeof options.oncomplete === 'function') {
                        options.oncomplete(req);
                    }
                }
            };

            // this special header is used to identify Ajax requests on the server
            req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

            if (typeof options.onbeforesend === 'function') {
                options.onbeforesend(req);
            }

            if (typeof timeout === 'number' && timeout > 0) {
                to = setTimeout(function () {
                    // prevent Firefox from calling onreadystatechange after abort
                    // IE will throw an error if it's set to null,
                    // so we set it to an empty function instead
                    req.onreadystatechange = function () {};

                    req.abort();

                    if (typeof options.onfailure === 'function') {
                        options.onfailure(req);
                    }
                    if (typeof options.oncomplete === 'function') {
                        options.oncomplete(req);
                    }
                }, timeout);
            }

            req.send(data);

            return true;
        },

        /**
         * Scrapes the data from a form and encodes it to be sent.
         *
         * @param  {Element}         form       the form to be scraped
         * @param  {Element}         [submit]   the submit button that was pressed
         * @param  {number[]}        [clickXY]  the x and y coordinates of the
         *                                      image submit that was pressed
         * @return {string|FormData}            the encoded data to be sent -
         *                                      returns false if unable to encode
         *
         * @method scrape
         * @memberOf WJBRYANT.Ajax
         */
        scrape: function (form, submit, clickXY) {

            var data = [],
                space = /%20/g,
                inputs = form.getElementsByTagName('input'),
                selects = form.getElementsByTagName('select'),
                textareas = form.getElementsByTagName('textarea'),
                qty = inputs.length,
                currElem,
                type,
                j,
                options,
                k,
                fdata;

            // use the FormData API if available
            if (window.FormData) {
                fdata = new window.FormData(form);

                // add the submit button if specified
                if (typeof submit === 'object') {
                    fdata.append(submit.name, submit.value);

                    if (submit.type === 'image' && clickXY) {
                        fdata.append(submit.name + '.x', clickXY[0].toString());
                        fdata.append(submit.name + '.y', clickXY[1].toString());
                    }
                }

                return fdata;
            }

            // check to make sure that components are able to be encoded,
            // if not - return false
            if (!encodeURIComponent) {
                return false;
            }

            /**
             * Encodes an Element into a string of this
             * format: 'name=value' and adds it to the
             * data array only if it is not disabled.
             *
             * @param {Element} elem  the element to encode
             *
             * @private
             */
            function encodeElem(elem) {
                // this check will fail if elements are disabled before the
                // form is scraped
                if (!elem.disabled && elem.name && typeof elem.value !== 'undefined') {
                    data[data.length] = encodeURIComponent(elem.name).replace(space, '+') +
                        '=' + encodeURIComponent(elem.value).replace(space, '+');
                }
            }

            // Not Submitted:
            // button and object elements (or anything besides input, select and textarea)
            // input types "button" and "reset"
            // input types "checkbox" and "radio" that are not checked
            // submit inputs that are not active (only one submit input can be used)
            // disabled elements

            // any non-standard or new input types will still be encoded and submitted

            // the submit input can be included if passed as the second argument

            // input type="image" normally sends the value and the x,y coordinates
            // coordinates of an image submit need to be taken from the click event
            // and passed in an array as the 3rd argument to this method

            // input type="file" is ignored if the FormData API is not available

            // loop through the various input types
            for (j = 0; j < qty; j += 1) {
                currElem = inputs[j];
                type = currElem.getAttribute('type');

                switch (type) {
                case 'checkbox':
                case 'radio':
                    // only "checked" input elements of these types are submitted
                    if (currElem.checked) {
                        encodeElem(currElem);
                    }
                    break;
                case 'button':
                case 'reset':
                case 'image':
                case 'submit':
                    break;
                default:
                    encodeElem(currElem);
                }
            }

            qty = selects.length;
            for (j = 0; j < qty; j += 1) {
                currElem = selects[j];
                if (currElem.type === 'select-multiple') {
                    if (!currElem.disabled) {
                        options = currElem.getElementsByTagName('option');
                        for (k = 0; k < options.length; k += 1) {
                            if (options[k].selected) {
                                encodeElem({
                                    name: currElem.name,
                                    value: options[k].value
                                });
                            }
                        }
                    }
                } else {
                    encodeElem(currElem);
                }
            }

            qty = textareas.length;
            for (j = 0; j < qty; j += 1) {
                currElem = textareas[j];
                encodeElem(currElem);
            }

            // add the submit button if specified
            if (typeof submit === 'object') {
                encodeElem(submit);

                if (submit.type === 'image' && clickXY) {
                    encodeElem({
                        name: submit.name + '.x',
                        value: clickXY[0].toString()
                    });

                    encodeElem({
                        name: submit.name + '.y',
                        value: clickXY[1].toString()
                    });
                }
            }

            return data.join('&');
        }
    };

    /**
     * The WJBRYANT.Anim namespace contains methods to animate Elements.
     *
     * @namespace Anim
     * @memberOf WJBRYANT
     */
    ns.Anim = {

        /**
         * Fades an element to and from any color.
         *
         * @param  {Element} elem       the element to be animated
         * @param  {Object}  [options]  an object containing members specifying
         *                              the animation options<br />
         *                              <br />
         *                              Valid members of this object are:
         * <pre>
         * {string[]} [properties=['backgroundColor']]  the style properties to animate. These are written
         *                                              in camelCase. Be careful to specify properties that
         *                                              only define a color, such as 'borderColor' instead
         *                                              of just 'border', because the property value will
         *                                              be overwritten when it is set to the new color.
         *
         * {string}   [from='#ffff66']                  the hex value to start at
         *
         * {string}   [to='#ffffff']                    the hex value to end at
         *
         * {number}   [dur=1500]                        the duration of the animation in milliseconds
         * {Function} [callback]                        the function to be executed when the animation is
         *                                              finished. This function is executed in the context
         *                                              of the element.
         * </pre>
         * @return {Object}  an object with an "end" method that sets all
         *                   properties to the final color and ends the animation
         *
         * @method fade
         * @memberOf WJBRYANT.Anim
         */
        fade: function (elem, options) {

            if (!elem || elem.nodeType !== 1) {
                return;
            }

            /**
             * Expands shorthand hex to a full 7 character hex number (including '#').
             * This method may return a hex number longer than 7 characters
             * if the argument is more than 4 characters long. However, only
             * the first 7 characters are considered by the fade algorithm.
             *
             * @param  {string} h  the hex number to expand (including '#')
             * @return {string}    the hex number expanded to full form (including '#')
             *
             * @private
             */
            function expand(h) {
                if (h.length < 7) {
                    h = h.replace(/#(\w)(\w)(\w)/, '#$1$1$2$2$3$3');
                }
                return h;
            }

            // set the defaults
            options = options || {};
            var elemStyle = elem.style,
                properties = options.properties || ['backgroundColor'],
                from = options.from ? expand(options.from) : '#ffff66',
                to = options.to ? expand(options.to) : '#ffffff',
                dur = options.dur || 1500,
                callback = options.callback,
                startTime,
                handle = true,

                // get to and from rgb values
                rf = parseInt(from.slice(1, 3), 16),
                gf = parseInt(from.slice(3, 5), 16),
                bf = parseInt(from.slice(5, 7), 16),
                rt = parseInt(to.slice(1, 3), 16),
                gt = parseInt(to.slice(3, 5), 16),
                bt = parseInt(to.slice(5, 7), 16);

            /**
             * Sets the next background color of the element.
             * Called after every interval until it's done (after the last "frame").
             *
             * @param {number} scheduledTime  the time the next frame is scheduled to be painted
             *
             * @private
             */
            function nextColor(scheduledTime) {
                var elapsedTime = scheduledTime - startTime,
                    fractionComplete = Math.min(elapsedTime / dur, 1),
                    r,
                    g,
                    b,
                    j,
                    len = properties.length;

                /**
                 * Calculates the next color based on the elapsed time
                 * and the "to" and "from" colors. This method is called
                 * for each individual r, g and b value separately.
                 *
                 * @param  {number} cf  the starting color value (0 - 255)
                 * @param  {number} ct  the target color value (0 - 255)
                 * @return {number}     the new color value (0 - 255)
                 *
                 * @private
                 */
                function calc(cf, ct) {
                    return Math.floor(cf * (1 - fractionComplete) + ct * fractionComplete);
                }

                r = calc(rf, rt);
                g = calc(gf, gt);
                b = calc(bf, bt);

                for (j = 0; j < len; j += 1) {
                    elemStyle[properties[j]] = 'rgb(' + r + ', ' + g + ', ' + b + ')';
                }

                if (fractionComplete < 1) {
                    handle = requestAnimationFrame(nextColor);
                } else {
                    handle = false;
                    if (typeof callback === 'function') {
                        callback.call(elem);
                    }
                }
            }

            startTime = new Date().getTime();
            // start the animation
            handle = requestAnimationFrame(function (scheduledTime) {
                // correct startTime for browsers that use the precision specification
                startTime = scheduledTime - (new Date().getTime() - startTime);
                nextColor(scheduledTime);
            });

            // return an object with an end method
            // to allow ending the animation
            return {
                end: function () {
                    var k,
                        len = properties.length;

                    // check if the animation is still going
                    if (handle) {
                        // stop the animation
                        cancelAnimationFrame(handle);
                        handle = false;

                        // set all properties to the final color
                        for (k = 0; k < len; k += 1) {
                            elemStyle[properties[k]] = to;
                        }

                        // execute the callback
                        if (typeof callback === 'function') {
                            callback.call(elem);
                        }
                    }
                }
            };
        },

        /**
         * Moves an element from one position to another.
         *
         * @example
         * WJB.Anim.move(document.getElementById('selectBox'), {
         *     from: [-200, -500],
         *     to: [35, 50],
         *     dur: 500,
         *     callback: function () {
         *         this.style.display = 'none';
         *     }
         * });
         * @param  {Element} elem       the element to be moved
         * @param  {Object}  [options]  an object containing members specifying
         *                              the animation options<br />
         *                              <br />
         *                              Valid members of this object are:
         * <pre>
         * {number[]} [from=curr. pos. || [0, 0]]  the starting position in [x, y]
         *
         * {number[]} [to=[0, 0]]                  the ending position in [x, y]
         *
         * {number}   [dur=400]                    the duration of the animation
         *                                         in milliseconds (must be > 0)
         *
         * {Function} [callback]                   the function to be executed when
         *                                         the animation is finished. This
         *                                         function is executed in the context
         *                                         of the element.
         * </pre>
         * @return {Object}  an object with an "end" method that moves the
         *                   element to the final position and ends the animation
         * @throws {Error}   if elem is not an Element
         *
         * @method move
         * @memberOf WJBRYANT.Anim
         */
        move: function (elem, options) {

            if (!elem || elem.nodeType !== 1) {
                throw new Error('Expected elem to be an Element');
            }

            options = options || {};

            var from = options.from,
                fromX = 0,
                fromY = 0,
                to = options.to,
                toX = 0,
                toY = 0,
                dur = options.dur,
                callback = options.callback,
                distX,
                distY,
                elemStyle = elem.style,
                startTime,
                handle = true,

                /**
                 * Determines if an object is an Array.
                 *
                 * @param  {Object}  obj  the object to test
                 * @return {boolean}      whether the object is an array
                 *
                 * @private
                 * @function
                 */
                isArray = Array.isArray ||
                    function (obj) {
                        return Object.prototype.toString.call(obj) === '[object Array]';
                    };

            if (!from) {
                // check for elemStyle.left and elemStyle.top to use as defaults for 'from'
                if (elemStyle.left && elemStyle.top) {
                    fromX = parseInt(elemStyle.left, 10);
                    fromY = parseInt(elemStyle.top, 10);
                }
            } else if (isArray(from) && typeof from[0] === 'number' && typeof from[1] === 'number') {
                // if 'from' is an array that contains numbers, use it
                fromX = from[0];
                fromY = from[1];
            }

            // if 'to' is an array that contains numbers, use it
            if (isArray(to) && typeof to[0] === 'number' && typeof to[1] === 'number') {
                toX = to[0];
                toY = to[1];
            }

            // check that dur is a positive number
            if (typeof dur !== 'number' || dur < 1) {
                dur = 400;
            }

            // the element cannot be animated without its position being set
            if ((window.getComputedStyle && window.getComputedStyle(elem, null).position === 'static') ||
                    (elem.currentStyle && elem.currentStyle.position === 'static')) {

                elemStyle.position = 'relative';
            }

            distX = toX - fromX;
            distY = toY - fromY;

            /**
             * Moves the element the appropriate distance for the next frame of animation.
             *
             * @param {number} scheduledTime  the time the next frame is scheduled to be painted
             *
             * @private
             */
            function next(scheduledTime) {
                var elapsedTime = scheduledTime - startTime,
                    fractionComplete = elapsedTime / dur;

                if (fractionComplete < 1) {
                    elemStyle.left = (fractionComplete * distX + fromX) + 'px';
                    elemStyle.top = (fractionComplete * distY + fromY) + 'px';
                    handle = requestAnimationFrame(next);
                } else {
                    elemStyle.left = toX + 'px';
                    elemStyle.top = toY + 'px';
                    handle = false;
                    if (typeof callback === 'function') {
                        callback.call(elem);
                    }
                }
            }

            startTime = new Date().getTime();
            // start the animation
            handle = requestAnimationFrame(function (scheduledTime) {
                // correct startTime for browsers that use the precision specification
                startTime = scheduledTime - (new Date().getTime() - startTime);
                next(scheduledTime);
            });

            // return an object with an end method
            // to allow ending the animation
            return {
                end: function () {
                    // check if the animation is still going
                    if (handle) {
                        // stop the animation
                        cancelAnimationFrame(handle);
                        handle = false;

                        // move the element to the final position
                        elemStyle.left = toX + 'px';
                        elemStyle.top = toY + 'px';

                        // execute the callback
                        if (typeof callback === 'function') {
                            callback.call(elem);
                        }
                    }
                }
            };
        },

        /**
         * Creates a slideshow that continuously replaces the target image element
         * with a new image from the list of sources every time the delay elapses.
         *
         * @param  {Element}  target   the image element this slideshow will replace
         * @param  {string[]} sources  the paths to each image in the slideshow
         * @param  {number}   delay    the time in milliseconds to wait between
         *                             changing slides
         * @return {Object}            an object with "start" and "stop" methods
         *                             used to control the slideshow
         *
         * @method makeSlideshow
         * @memberOf WJBRYANT.Anim
         */
        makeSlideshow: function (target, sources, delay) {
            var pics = [],
                currPic = 0,
                to, // the timeout
                j,
                len = sources.length,
                tempPic;

            /**
             * Changes to the next slide.
             *
             * @private
             */
            function changePic() {
                var nextPic = currPic < pics.length - 1 ? currPic + 1 : 0,
                    newTarget = pics[nextPic];

                target.parentNode.replaceChild(newTarget, target);
                target = newTarget;
                currPic = nextPic;

                // use recursive setTimeout instead of
                // setInterval because of a bug in Firefox
                to = setTimeout(changePic, delay);
            }

            // preload images
            for (j = 0; j < len; j += 1) {
                tempPic = target.cloneNode(true);
                tempPic.setAttribute('src', sources[j]);
                pics[j] = tempPic;
            }

            return {
                /**
                 * Starts the slideshow. If the slideshow was previously
                 * stopped, it will resume from its last position.
                 *
                 * @ignore
                 */
                start: function () {
                    if (!to) {
                        to = setTimeout(changePic, delay);
                    }
                },

                /**
                 * Stops the slideshow.
                 *
                 * @ignore
                 */
                stop: function () {
                    if (to) {
                        clearTimeout(to);
                        to = false;
                    }
                }
            };
        }
    };
}(window, document));