Hackery, Math & Design

Steven Wittens i

Abusing jQuery.animate for fun and profit (and bacon)

The days of static UIs that only have jarring transitions between pages are pretty much over. With frameworks like CoreAnimation or jQuery, it's easy to add useful animations to applications and webpages. In the case of jQuery, you can easily animate any CSS property, and you get free work-arounds for browser bugs to boot. You can run multiple animations (of arbitrary duration) at the same time, queue animations and even animate complex properties like colors or clipping rectangles.

But what if you want to go beyond mere CSS? You might have a custom widget that is drawn using <canvas>, whose contents are controlled by internal variables; maybe you're using 3D transformations to scale and position images on a page, and simple 2D tweening just doesn't cut it.

In that case, it would seem you are out of luck: jQuery's .animate() method can only be applied to a collection of DOM elements, and relies heavily on the browser's own semantics for processing CSS values and their units. However thanks to JavaScript's flexibility and jQuery's architecture, we can work around this, and re-use jQuery's excellent animation core for our own nefarious purposes.

Hackity hack hack

First, we need an object to store all the variables we wish to animate. We use an anonymous <div> outside of the main document, so that jQuery's DOM calls still work on it. We simply add our own properties to it:

var vars = $.extend($('<div>')[0], {
  foo: 1,
  bar: 2,

  customAnimate: true,
  updated: true
});

In this case, our properties are foo and bar. We also set customAnimate and updated to identify this object (see below).

Next we need to override jQuery's default step function, which gets called for every step of an animation, and applies new values to an element's CSS properties.

   // jQuery.fx.step._default
    _default: function(fx) {
      fx.elem.style[fx.prop] = fx.now + fx.unit;
    }

We can replace it using the following snippet:

var $_fx_step_default = $.fx.step._default;
$.fx.step._default = function (fx) {
  if (!fx.elem.customAnimate) return $_fx_step_default(fx);
  fx.elem[fx.prop] = fx.now;
  fx.elem.updated = true;
};

With the new step function, jQuery will check for the presence of a customAnimate property on any element it is animating. If present, it will assign the (unit-less) value to element.property rather than element.style.property and mark the element by setting element.updated to true.

Now we're ready to animate, using the normal $.animate syntax:

$(vars).animate({ foo: 5, bar: 10 }, { duration: 1000 });

The values vars.foo and vars.bar will now smoothly change over time. You can use any of jQuery's animation abilities as usual.

Now what about that updated variable? Well to actually use these animated values, you will need some kind of timer or step callback to read them back and draw them on the page. If you're using <canvas>, you need to redraw your entire widget for every change, but you don't want to be wasting CPU time by constantly refreshing it. Furthermore, if you're running multiple animations at the same time, you'll want to aggregate all your property changes into a single redraw per frame. This is easy with the updated property and a simple timer:

setInterval(function () {
  if (!vars.updated) return;
  vars.updated = false;
  
  drawWidget();
}, 30);

Now your widget will only refresh itself when its values are changed by the animation step function we defined earlier, and very few CPU cycles are wasted. As a plus, you can render updates as fast or as slow as you want, without affecting the duration of your animations.

Demo

I whipped up a quick demo which renders a cloud of bacon using <canvas>. All the motion in the demo is created through $.animate(), with a bunch of animations running at once.

This demo will not work in Internet Explorer, and has only been tested in Firefox 3 and Safari 3.

This is a rather esoteric example, but there are plenty of useful ways to apply this technique. I've used it to implement smooth, beautiful, usable widgets. You can combine multiple motion and opacity animations triggered by clicks and hovers without issues.

Final notes

While this technique works great, there is one big caveat. You should avoid animating any property that exists in CSS ('float', 'display', 'opacity', ...) because these have unexpected side effects depending on the browser.

There are also a couple of weaknesses:

  • jQuery does not support continued easing. That is, when you override an animation that is already in progress, the variable being animated will instantly stop and restart from its current position. The rate of change is not continuous between the two animations.
  • Animating angles is tricky. E.g. when animating from 350˚ to 0˚, you want it to animate across 10˚ and not the long way around. This requires manual correction.

And obviously, it would be cleaner if jQuery's animation core was refactored to separate out the CSS-specific code instead...

Canvas  Graphics  JavaScript  Usability  jQuery
This article contains graphics made with WebGL, which your browser does not seem to support.
Try Google Chrome or Mozilla Firefox. ×