Animating CSS3 2D Transforms
There was a little "poke fun at the boss" mail trail at work recently (yep, we do that sometimes and everybody just takes it in stride; it just so happened that boss was the target this time but it might just as well have been yours truly or anybody else) and I contributed to the joint exercise with a little video cooked up with Windows Live Movie Maker (nope, I am not posting the video here!). Somebody wondered if that video could have been built with a HTML5, JavaScript and CSS3 solution and I figured I'd give it a shot.
The basic idea was to show a sequence of "scenes" where each scene animates one or more HTML elements in some fashion. My first choice was to consider using CSS3 transitions but given that I wanted this to work in Internet Explorer 9 (IE9) which does not support CSS3 transitions, I decided to go with a JavaScript based animation approach.
Being an ardent jQuery user I thought I'd extend jQuery's effects system (primarily via the animate
function) by writing a little plugin that would let me animate CSS3 2D transform matrices. I couldn't use jQuery.animate
as is because it requires the property being animated to be a numeric value (width, opacity, left, top etc.) and CSS3 2D transform values are essentially 3x2 matrices.
So what are CSS3 2D Transforms?
Before we talk about the plugin though let's quickly review how CSS3 2D transforms work. Here's a div
for example that has been rotated clock-wise about its center by 45 degrees (you'll need to use IE9 or a recent version of Firefox, Chrome, Safari or Opera to see the rotation):
:-)
The rotation was achieved by a bit of CSS3 code like so:
transform: rotate(45deg);
Since the CSS3 2D transform spec is a working draft standard at the time of this writing, all browsers currently provide support for this only through vendor prefixed CSS3 attributes. So to make this work today you'd have to actually write all of the following:
-o-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
The vendor prefixes given above are for Opera, Firefox (Mozilla), WebKit based browsers (Chrome and Safari) and IE - in that order. Note that I have supplied the standard name for the attribute - "transform
" - last in the list. This has been deliberately done so that when these browsers do start supporting the standard name for the attribute, that behavior (i.e., the standard behavior) will take precedence over the vendor prefixed versions by virtue of it showing up last in the rule.
Apart from rotation, the spec also supports the specification of translation, scaling and skew transforms. Multiple transforms can be applied to a single element at the same time by separating each transform by a space. Here's another div
that has been rotated about its center by 45 degrees and skewed along the X axis by 15 degrees.
:-)
And here're the CSS3 rules that produced this output:
-o-transform: rotate(45deg) skewX(15deg);
-moz-transform: rotate(45deg) skewX(15deg);
-webkit-transform: rotate(45deg) skewX(15deg);
-ms-transform: rotate(45deg) skewX(15deg);
transform: rotate(45deg) skewX(15deg);
Reading transform data back from the element
Now that we have the basic transform applied, let's see what we get back if we query the transform via jQuery. We use jQuery's css
function to retrieve the value. On IE9 for instance here's the code you'd use to get the transform:
var transform = $("div").css("msTransform");
And here's the value IE9 returns for the second div
above (the one which has both a rotate and a skew applied):
matrix(0.707107, 0.707107, -0.517638, 0.896575, 0, 0)
As you can tell, that doesn't look anything like the way we specified the transform. It turns out that the browser is free to convert our transform specification into a matrix representation and discard the original string given in the style sheet. As you might expect we can also specify the transform in this format, should we choose to do so, instead of using the the syntax we used earlier. While we are reviewing matrices, it might be useful to consider the following matrix - known as the identity matrix. This is a transform matrix applying which produces basically no change in the source matrix - similar to the effect that multiplying a number by one produces on it.
matrix(1, 0, 0, 1, 0, 0)
Since we'll need to work with these numbers individually, it might be useful to write a helper routine that can parse a string in this form and produce an array of 6 numbers.
(function ($) {
//
// regular expression for parsing out the matrix
// components from the matrix string
//
var matrixRE = /\([0-9epx\.\, \t\-]+/gi;
//
// parses a matrix string of the form
// "matrix(n1,n2,n3,n4,n5,n6)" and
// returns an array with the matrix
// components
//
var parseMatrix = function (val) {
return val.match(matrixRE)[0].substr(1).
split(",").map(function (s) {
return parseFloat(s);
});
}
})(jQuery);
We basically use a regular expression to extract the relevant portion and convert it into an array of numbers. You'll note that I've used the self-calling anonymous function technique above. This was done to prevent polluting the global namespace with variables such as matrixRE
.
Another factor to consider while reading transform matrix values back from the DOM is the fact that browsers today implement CSS3 2D transforms through vendor prefixes. This means that code such as the following, where we use the standard name for the CSS attribute, simply won't work!
var transform = $("div").css("transform");
We're going to have to use the appropriate vendor prefix while setting and retrieving transform matrices from script. As this can quickly get quite cumbersome to do, it makes sense to wrap this activity up in a couple of utility routines. Here goes:
(function ($) {
//
// transform css property names with vendor prefixes;
// the plugin will check for values in the order the
// names are listed here and return as soon as there
// is a value; so listing the W3 std name for the
// transform results in that being used if its available
//
var transformPropNames = [
"transform",
"msTransform",
"-webkit-transform",
"-moz-transform",
"-o-transform"
];
var getTransformMatrix = function (el) {
//
// iterate through the css3 identifiers till we
// hit one that yields a value
//
var matrix = null;
transformPropNames.some(function (prop) {
matrix = el.css(prop);
return (matrix !== null && matrix !== "");
});
//
// if "none" then we supplant it with an identity matrix so
// that our parsing code below doesn't break
//
matrix = (!matrix || matrix === "none") ?
"matrix(1,0,0,1,0,0)" : matrix;
return parseMatrix(matrix);
};
//
// set the given matrix transform on the element; note that we
// apply the css transforms in reverse order of how its given
// in "transformPropName" to ensure that the std compliant prop
// name shows up last
//
var setTransformMatrix = function (el, matrix) {
var m = "matrix(" + matrix.join(",") + ")";
for (var i = transformPropNames.length - 1; i >= 0; --i) {
el.css(transformPropNames[i], m);
}
};
})(jQuery);
With these functions handy, we can set and retrieve transforms like so and it should work well on all modern browsers:
setTransformMatrix($("div"),
[0.707107, 0.707107, -0.517638, 0.896575, 0, 0]);
var matrix = getTransformMatrix($("div"));
The jQuery plugin
The jQuery plugin uses jQuery's animation function to take advantage of its "easing" system. The animation is driven by having jQuery animate a phantom property called "percentAnim" from 0 through 100 on the element(s) in question and uses the "step" callback function to guide the actual animation of the transform matrix. Here's the source of the jQuery plugin (assuming the functions defined above are available):
(function ($) {
//
// interpolates a value between a range given a percent
//
var interpolate = function (from, to, percent) {
return from + ((to - from) * (percent / 100));
}
$.fn.transformAnimate = function (opt) {
//
// extend the options passed in by caller
//
var options = {
transform: "matrix(1,0,0,1,0,0)"
};
$.extend(options, opt);
//
// initialize our custom property on the element
// to track animation progress
//
this.css("percentAnim", 0);
//
// supplant "options.step" if it exists with our own
// routine
//
var sourceTransform = getTransformMatrix(this);
var targetTransform = parseMatrix(options.transform);
options.step = function (percentAnim, fx) {
//
// compute the interpolated transform matrix for
// the current animation progress
//
var $this = $(this);
var matrix = sourceTransform.map(function (c, i) {
return interpolate(c, targetTransform[i],
percentAnim);
});
//
// apply the new matrix
//
setTransformMatrix($this, matrix);
//
// invoke caller's version of "step" if one
// was supplied;
//
if (opt.step) {
opt.step.apply(this, [matrix, fx]);
}
};
//
// animate!
//
return this.animate({ percentAnim: 100 }, options);
};
})(jQuery);
There isn't a whole lot going on there. The "step" function is called by jQuery every time a frame update is needed. It passes the current value of the property jQuery is animating over the given duration as an argument to the "step" function. In our case this will be a number that ranges between 0 and 100 and we take it as an indication of how far we are into the animation and use it to appropriately interpolate the values in the transform matrix. Here's an example of how we might use this function to translate and rotate an element over 1 second:
var box = $("#box");
box.transformAnimate({
transform: "matrix(-0.707107, 0.707107, " +
"-0.707107, -0.707107, 150, 0)",
duration: 1000
});
If you're wondering how I got those numbers in the matrix, well, I used the JavaScript console that ships as a part of IE9's debugging tools (just hit F12 if you're running IE) and then manually applied the said transform and then retrieved the resulting matrix via jQuery! Not exactly rocket science, I know!
The jQuery plugin has been used to trigger an animation on the box shown below when you click on it. It uses the animation plugin defined above to translate the box 150 pixels along the X axis and also rotate it 135 degrees around the center in a clock-wise direction over 1 second and then does the reverse. Go ahead, give it a shot - try clicking the smiley!
:-)
Great, now make it rotate all the way 5 times!
At one point I needed to make a certain element rotate about 5 times around the center over a few seconds. So I figured I'll just use my plugin to get it done. Turns out it isn't quite as simple as that! The problem you see is that the plugin works purely by interpolating the values of the transform matrix from the source to the target matrix over the given duration. Interpolation does not imply that things will progress in a linear fashion. For example, imagine that I wish to rotate an element from 0 degrees through 270 degrees. One can achieve this by incrementing the rotation angle from 0 to 270. However, when represented as a transform matrix it turns out that we can just as easily achieve this by rotating counter-clockwise from 0 degrees to -90 degrees! Not quite what we want! To make this kind of mindless animation work I had to put together a mini-animation framework (if you can call it that) myself.
First, I copied the setTransformMatrix
function from the transform jQuery plugin:
var Utils = {
//
// utility for applying transform matrix
//
applyTransform: (function () {
var transformPropNames = [
"transform",
"msTransform",
"-webkit-transform",
"-moz-transform",
"-o-transform"
];
return function (el, transform) {
for (var i = transformPropNames.length - 1;
i >= 0; --i) {
el.css(transformPropNames[i], transform);
}
};
})()
};
And then implemented a simple animate routine whose job is to simply invoke a callback function at a predefined interval for a specified duration.
var Utils = {
//
// simple custom animation
//
animate: (function () {
var ANIMATION_INTERVAL = 17;
return function (duration, frame, done) {
var start = Date.now();
//
// our animation routine
//
var anim = function () {
//
// compute animation progress
//
var elapsed = (Date.now() - start);
var percent = elapsed / duration;
//
// invoke callback
//
frame(percent);
//
// schedule next frame update if need be
//
if (elapsed < duration) {
window.setTimeout(anim, ANIMATION_INTERVAL);
} else {
done();
}
};
//
// schedule first frame update
//
window.setTimeout(anim, ANIMATION_INTERVAL);
};
})()
};
Finally, I put together a routine to animate the transform by animating the transform options individually, i.e., you can for instance, translate along Y axis from 10 pixels to 200 pixels and skew along X axis from 20 degrees to 0 degrees and so forth. Here's the function definition:
var Utils = {
//
// wrapper for animating transforms; "opt" might look
// like this:
// opt: {
// scale: {
// from: 0.01, to: 1.0
// },
// rotate: {
// from: 0.0, to: 360.0 * 2.0
// },
// translateX: {
// from: 10, to: 300
// }
// }
//
transformAnimate: (function () {
var suffix = {
scale: "",
scaleX: "",
scaleY: "",
rotate: "deg",
translate: "px",
translateX: "px",
translateY: "px",
skew: "deg",
skewX: "deg",
skewY: "deg"
};
return function (duration, el, opt, done) {
Utils.animate(duration, function (percent) {
var transform = "";
for (var p in opt) {
var val = opt[p].from +
((opt[p].to - opt[p].from) * percent);
transform += sprintf("%s(%f%s) ",
p, val, suffix[p]);
}
Utils.applyTransform(el, transform);
}, done);
};
})()
};
If I wanted to implement the same animation I did earlier, i.e. translate the box 150 pixels along the X axis and also rotate it 135 degrees around the center in a clock-wise direction over 1 second using this new framework, then I'd do something like this:
Utils.transformAnimate(1000, $("#box1"), {
translateX: { from: 0.0, to: 150.0 },
rotate: { from: 0.0, to: 135.0 }
}, function() {});
Here's another box for you that you can click to make it rotate and translate a bit.
:-)
Pretty cool don't you think?