Dave Rolsky - Animation.Resize-0.10

Documentation | Source

NAME

Animation.Resize - Animation for resizing an element

SYNOPSIS

  Animation.Resize.resizeElement( { elementId:    "some-element",
                                    targetWidth:  200,
                                    targetHeight: 150,
                                    anchors:      { top: true },
                                    onFinish:     someFunction,
                                  } );

DESCRIPTION

This library provides an animation effect for resizing elements by gradually changing the dimensions (and possibly position) of the element over a period of time. The animation can be anchored in various ways so that the animation moves in different ways.

FUNCTIONS

This library provides one function, resizeElement(). It requires an object with the following properties as its arguments.

  • elementId - required
  • The element should be resized.

  • targetWidth - required
  • The width, in pixels, to which the element should be resized.

  • targetHeight - required
  • The height, in pixels, to which the element should be resized.

  • anchorSides - optional
  • This parameter is itself an object with four possible keys, "top", "bottom", "left", "right". The anchors determine in which direction the element will appear to move as it is resized. You cannot anchor both the top and bottom or left and right at the same time. You can anchor only one side, or no sides.

  • frameCount - optional
  • The number of frames in the animation. Defaults to 20.

  • totalDuration - optional
  • The total length of the animation, in milliseconds. Defaults to 500, which makes for 40FPS with the default frameCount.

  • onFinish - optional
  • A callback to be executed when the animation is done.

LIMITATIONS

For the animation to work, the library must be able to get the width of the element being resized in pixels, as specified in CSS. In Firefox, the value of the style from getComputedStyle() is always in pixels, regardless of what units are used in the CSS, so resizing always works. In IE6, the value that can be retrieved is returned as is, so if you specify the value in percentage, points, or em units, the element cannot be resized.

The one special case where the value does not need to be retrieved is if the element has zero width and height, which can be calculated without looking at the CSS styles.

The reason this is all necessary is that the other ways of getting element sizes produce values that are not settable, for example using the offsetHeight or clientHeight properties. The only way to change the height or width of an element is to explicitly set its style attributes, so to calculate the animation, we need to start with those CSS values.

This is different from how I've seen other libraries do resizing, such as Scriptaculous, but the Scriptaculous method has the downside that you cannot start with an element of zero size, because it simply multiplies the current size of the element by a scaling factor for each frame of the animation. If the element size starts at zero, the size will never change. Scriptaculous also doesn't give fine control over the final size of the element, as it does not account for rounding errors in its math.

This is not really a criticism of Scriptaculous, as its doing something different. It lets you say "make this element about twice as big as it is right now", whereas Animation.Resize lets you say "resize this element to 300px by 200px".

AUTHOR

Dave Rolsky, <autarch@urth.org>.

COPYRIGHT

Copyright (c) 2006 Dave Rolsky. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as the Perl programming language (your choice of GPL or the Perl Artistic license).

JSAN.use("DOM.Ready");

if ( typeof Animation == "undefined" ) Animation = {};

Animation.Resize = function (params) {
    this._initialize(params);
}

Animation.Resize.DEBUG = 0;
Animation.Resize.VERSION = "0.10";

Animation.Resize.DEFAULT_FRAMES = 20;
Animation.Resize.DEFAULT_DURATION = 500;

Animation.Resize.resizeElement = function (params) {
    new Animation.Resize(params);
}

Animation.Resize.prototype._initialize = function (params) {
    this._setParams(params);

    var self = this;
    DOM.Ready.onIdReady( params.elementId,
                         function (elt) { self._startResize(elt) }
                       );
}

Animation.Resize.prototype._setParams = function (params) {
    if ( ! params["elementId"] ) {
        throw new Error("Animation.Resize requires an elementId parameter");
    }

    if ( typeof params["targetWidth"] == "undefined" ) {
        throw new Error("Animation.Resize requires an targetWidth parameter");
    }

    if ( typeof params["targetHeight"]  == "undefined" ) {
        throw new Error("Animation.Resize requires an targetHeight parameter");
    }

    if ( ! params["anchorSides"] ) {
        params["anchorSides"] = {};
    }

    this._targetWidth  = params["targetWidth"];
    this._targetHeight = params["targetHeight"];
    this._anchors = params["anchorSides"];
    this._onFinish  = params["onFinish"];

    this._frameCount = params["frameCount"];
    this._totalDuration = params["totalDuration"];

    if ( typeof this._frameCount == "undefined" ) {
        this._frameCount = Animation.Resize.DEFAULT_FRAMES;
    }

    if ( typeof this._totalDuration == "undefined" ) {
        this._totalDuration = Animation.Resize.DEFAULT_DURATION;
    }

    this._intervalDuration =
        this._totalDuration / this._frameCount;

    if ( this._anchors.left && this._anchors.right ) {
        throw new Error("Cannot anchor both the left and right sides");
    }

    if ( this._anchors.top && this._anchors.bottom ) {
        throw new Error("Cannot anchor both the top and bottom sides");
    }
}

Animation.Resize.prototype._startResize = function (elt) {
    if ( Animation.Resize.DEBUG ) {
        alert( "Resizing: #" + elt.id
               + "\n"
               + "width: " + this._targetWidth
               + "\n"
               + "height: " + this._targetHeight
               + "\n"
               + "Anchors: " + this._anchorsAsString() );
    }

    this._elt = elt;
    this._calcFrames();

    var self = this;
    this._interval =
        setInterval( function () {
            self._doFrame()
        }, this._intervalDuration );
}

Animation.Resize.prototype._anchorsAsString = function () {
    var anchors = [];
    if ( this._anchors["top"] ) {
        anchors.push("top");
    }
    else if ( this._anchors["bottom"] ) {
        anchors.push("bottom");
    }

    if ( this._anchors["left"] ) {
        anchors.push("left");
    }
    else if ( this._anchors["right"] ) {
        anchors.push("right");
    }

    if ( anchors.length ) {
        return anchors.join(", ");
    }
    else {
        return "none";
    }
}

Animation.Resize.prototype._calcFrames = function () {
    var dims = this._calcInitialDimensions();

    var x_total = this._targetWidth  - this._currentWidth;
    var y_total = this._targetHeight - this._currentHeight;

    var x_direction = x_total >= 0 ? 1 : -1;
    var y_direction = y_total >= 0 ? 1 : -1;

    var x_step_base = parseInt( x_total / this._frameCount );
    var x_rem = Math.abs( x_total % this._frameCount );

    var y_step_base = parseInt( y_total / this._frameCount );
    var y_rem = Math.abs( y_total % this._frameCount );

    var frames = [];
    var debug = "";
    for ( i = 0; i < this._frameCount; i++ ) {
        var x_step = x_step_base;
        if ( x_rem-- > 0 ) {
            x_step += 1 * x_direction;
        }

        var y_step = y_step_base;
        if ( y_rem-- > 0 ) {
            y_step += 1 * y_direction;
        }

        frames[i] = { x: this._calcDeltas( i + 1, x_step, "left", "right" ),
                      y: this._calcDeltas( i + 1, y_step, "top", "bottom" ) };

        if ( Animation.Resize.DEBUG ) {
            debug =
                debug
                + "Frame #" + (i + 1) + ": "
                + "  x: { size: " + frames[i].x.size + ", move: " + frames[i].x.move + "}"
                + " - "
                + "y: { size: " + frames[i].y.size + ", move: " + frames[i].y.move + "}"
                + "\n\n";
        }
    }

    if ( Animation.Resize.DEBUG ) { alert(debug); }

    this._frames = frames;
}


Animation.Resize.prototype._calcDeltas = function (frame, step, anchor1, anchor2) {
    var deltas = { size: 0, move: 0 };

    if ( this._anchors[anchor1] ) {
        deltas.size = step;
    }
    else if ( this._anchors[anchor2] ) {
        deltas.move = step;
    }
    else {
        deltas.size = parseInt( step / 2 );
        deltas.move = deltas.size;

        if ( Math.abs(step) % 2 ) {
            /* It's important to alternate where the extra pixel is
               allocated or else by the end of the animation we'll
               have moved one side 25px (one per frame) more than the
               other */
            if ( frame % 2 ) {
                deltas.size += deltas.size < 0 ? -1 : 1;
            }
            else {
                deltas.move += deltas.move < 0 ? -1 : 1;
            }
        }
    }

    return deltas;
}

Animation.Resize.prototype._calcInitialDimensions = function () {
    var dims;

    var width  = this._sizeFromStyle("width");
    var height = this._sizeFromStyle("height");

    if ( typeof width == "undefined" || typeof height == "undefined" ) {
        if ( this._eltIsZeroSize() ) {
            width = 0;
            height = 0;
        }
        else {
            throw Error("The element to be resized must have an explicit width and height style in pixels, or be zero size");
        }
    }

    this._currentWidth  = width;
    this._currentHeight = height;
}

Animation.Resize.prototype._sizeFromStyle = function (propName) {
    var styleText = Animation.Resize._textForStyle( this._elt, propName );

    var match = styleText.match( /(\d+)(%|em|pt|px)?/ );

    if ( ! match ) {
        return undefined;
    }

    var units = match[2];
    if ( typeof units == "undefined" || units == "px" ) {
        return parseInt( match[1] );
    }
    else {
        return undefined;
    }
}

Animation.Resize._textForStyle = function ( elt, propName ) {
    if ( document.defaultView ) {
        return document.defaultView.getComputedStyle( elt, null ).getPropertyValue(propName);
    }
    else {
        return eval( "elt.currentStyle." + propName );
    }
}

Animation.Resize.prototype._eltIsZeroSize = function () {
    var width;
    var height;

    if ( this._elt.style.display != "none" ) {
        width  = this._elt.clientWidth;
        height = this._elt.clientHeight;
    }
    else {
        /* Taken from Prototype, which comments that when display is
         * none, clientWidth and clientHeight are always 0 */
        var vis = this._elt.style.visibility;
        var pos = this._elt.style.position;

        this._elt.style.visibility = 'hidden';
        this._elt.style.position = 'absolute';

        width  = this._elt.clientWidth;
        height = this._elt.clientHeight;

        this._elt.style.display = 'none';
        this._elt.style.position = pos;
        this._elt.style.visibility = vis;
    }

    if ( width == 0 && height == 0 ) {
        return true;
    }
    else {
        return false;
    }
}

Animation.Resize.prototype._doFrame = function () {
    if ( this._frames.length ) {
        this._applyStep( this._frames.shift() );

        if ( this._isLastFrame() ) {
            this._finish();
        }
    }
}

/* The animation looks better to me if the move part comes before the
 * resize. */
Animation.Resize.prototype._applyStep = function (step) {
    if ( step.x.size || step.x.move ) {
        if ( step.x.move ) {
            var left = this._elt.offsetLeft;
            left -= step.x.move;

            this._elt.style.left = left + "px";
        }

        this._currentWidth = this._currentWidth + step.x.size + step.x.move;
        this._elt.style.width = this._currentWidth + "px";
    }

    if ( step.y.size || step.y.move ) {
        if ( step.y.move ) {
            var top = this._elt.offsetTop;
            top -= step.y.move;

            this._elt.style.top = top + "px";
        }

        this._currentHeight = this._currentHeight + step.y.size + step.y.move;
        this._elt.style.height = this._currentHeight + "px";
    }

    if ( Animation.Resize.DEBUG ) {
        alert( "Current width: " + this._elt.style.width
               + "\n"
               + "Current height: " + this._elt.style.height
               + "\n"
               + "Current top: " + this._elt.style.top
               + "\n"
               + "Current left: " + this._elt.style.left );
    }
}

Animation.Resize.prototype._isLastFrame = function () {
    if ( this._frames.length ) {
        return false;
    }
    else {
        return true;
    }
}

Animation.Resize.prototype._finish = function () {
    this._clearInterval();
    if ( this._onFinish ) {
        this._onFinish();
    }
}

Animation.Resize.prototype.cancel = function () {
    this._clearInterval();
}

Animation.Resize.prototype._clearInterval = function () {
    clearInterval( this._interval );
}


/*

*/