Wednesday, September 19, 2012

Extending jQuery Animate() Beyond Basic CSS Animation

Using jQuery to enable DOM animation is very simple. The library does an excellent job of hiding all the details of managing timing and pace for you. The core animate() function is capable of handling most numeric CSS properties. However, I was interested in how you might extend the animation library to enable working with other CSS (or even non-CSS) properties. The jQuery documentation does not provide any details about extending the animation engine. So, I figured that I would have to dig through the code and spend some time researching what others had done:

http://onwebdev.blogspot.com/2011/02/jquery-fx-object.html - Talks about the jQuery animation internals. The code snippets seem outdated because the current code base doesn't look the same any more. However, you can get the basic point.

http://cdmckay.org/blog/2010/03/01/the-jquery-animate-step-callback-function/ - Discusses the FX object properties and what they represent.

Now that I have a better understanding of the internals, I decided to look at some actual implementations:

http://playground.benbarnett.net/jquery-animate-enhanced/ - Uses CSS3 Transitions to animate top/left CSS properties and opacity.  Proxies both animate() and stop(). Since this enhancement is intercepting properties already handled by jQuery, the proxying seemed appropriate. However, I'm more interested in adding properties not handled by default.

https://github.com/weepy/jquery.path - Adds a custom function to jQuery.fx.step to perform the custom animation along non-linear paths. The actual extension is quite simple:


$.fx.step.path = function(fx) {
var css = fx.end.css( 1 - fx.pos );
if ( css.prevX != null ) {
$.cssHooks.transform.set( fx.elem, "rotate(" + Math.atan2(css.prevY - css.y, css.prevX - css.x) + ")" );
}
fx.elem.style.top = css.top;
fx.elem.style.left = css.left;
};


http://www.bitstorm.org/jquery/shadow-animation/ - Adds to $.Tween.propHooks to enable animation of the boxshadow style. The $.Tween object seems to be relatively new and in jQuery 1.8, you can see that $.fx is now equal to the tween prototype.

Finally, I decided to see how the jQuery UI color animation worked:


jQuery.fx.step[ hook ] = function( fx ) {
if ( !fx.colorInit ) {
fx.start = color( fx.elem, hook );
fx.end = color( fx.end );
fx.colorInit = true;
}
jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) );
};


It seems that adding to the jQuery.fx.step object is currently the best approach.

So the next step is to test this out with a simple example to see it work. I decided to extend animate to allow animating a "size" property. This is not really necessary since I can simply animate height and width just fine. But its really simple and removes any extra code that would be necessary to implement something more complex. The goal is to see how it works. I'd like to trace through the code paths to see where and when certain things happen so I can understand how I might build a more useful extension.

I'd like my "size" animation to be able to accept a point to expand/contract around. This is similar to setting the transform-origin and using the transform scaling function. However, I'm going to implement it by adjusting the element's top/left to make it appear to be sizing around that point.

The implementation of the size property animation is below:

$.fx.step['size'] = function(fx)
{
if ( !fx._sizeInit )
{
var $el = $(fx.elem),
c = fx.end.center || {top: 0, left: 0};

fx.start = $el.offset();
$.extend(fx.start, {width: $el.width(), height: $el.height()});

fx._sizer = {};

fx._sizer.topDelta = c.top - (c.top * fx.end.height / fx.start.height);
fx._sizer.leftDelta = c.left - (c.left * fx.end.width / fx.start.width);
fx._sizer.widthDelta = fx.end.width - fx.start.width;
fx._sizer.heightDelta = fx.end.height - fx.start.height;

fx._sizeInit = true;
}

fx.elem.style.top = fx.start.top + Math.floor(fx._sizer.topDelta * fx.pos) + 'px';
fx.elem.style.left = fx.start.left + Math.floor(fx._sizer.leftDelta * fx.pos) + 'px';
fx.elem.style.width = fx.start.width + Math.floor(fx._sizer.widthDelta * fx.pos) + 'px';
fx.elem.style.height = fx.start.height + Math.floor(fx._sizer.heightDelta * fx.pos) + 'px';

}


There are several key aspects to note:

  • fx.elem is the DOM node not the jQuery object.

  • You can add properties to the fx object and use them in subsequent calls to the step function. The object persists between calls to the step function.

  • All the calculations have been done on the first step and saved so all future calls do the least amount work possible. Trying to do too much work on each animation step risks dropping frames in the animation which will make it look choppy. This is further illustrated by directly setting the style attribute on the element instead of using the jQuery css() function.



To animate this property, you would only need to give the "size" property in the animation call an appropriate object:


// myDiv is a 50 x 60 box. This will animate the size
// to 100 x 200 relative to the top/middle of the box
$('myDiv')
.animate({
size: {
center: {top: 0, left: 30},
height: 100,
width: 200
}
}, 'slow');


I created a demo that uses the above animation plugin to resize a box. The center coordinates are relative to the top/left of the element. Try entering a negative origin or an origin outside the boundary of the box. Essentially, all this size property did was do the math to find the different deltas - the harder part was move the top/left corner around to make the animation appear to be occurring around a different origin point. Like I said before, you could simply animate the transform scaling function with an appropriate transform-origin point set. jQuery animate can not do that natively but the Transit plugin extends jQuery to enable this functionality. However, if your interested in support, the approach I presented above will be backwards compatible with older browser versions.

I purposely kept this example simple so it was easy to see how to augment the animation fx object to create a new property handler. Some useful additions to this code would include relative resizing, percentages, etc. All of those require more parsing and logic which would detract from the example. As you can see, extending the capabilities of jQuery.animate() is relatively easy and enables building powerful extensions.