Monday, September 10, 2012

Diving into Matrices with CSS3 Transforms and jQuery

jQuery makes it easier to set the CSS3 transform style because it normalizes all the different browser specific variations for you. All you need to do is specify "transform: function(s)" and it does the rest. You can specify any set of functions you like: rotate, skew, translate, etc. However, if you get the transform style with jQuery.css(), you will always get a matrix transform back.

Consider the following code snippet:


<body style="margin: 10px">

<div id="myDiv">rotate this</div>

<br/><br/>

<pre>
<script>
$(function()
{

$('#myDiv').css({
transformOrigin: 'top left',
transform: 'rotate(27deg)'
});

$('pre').html($('#myDiv').css('transform'));

});

</script>
</pre>

</body>


When executed, this is what you will see:



This means that if you're relying on setting the CSS and then retrieving it later to modify it, you need to work with matrices. Your only other options are to maintain other variables, use data(), or store attributes on the elements via attr(). These are all less eloquent ways of working around the problem - the information is already stored on the element, you just need to work with the values as they are natively being represented.

Javascript does not have any built-in matrix functions. However, there are some libraries out there that provide the functionality. Matrix math alone is not enough to manage the transform style - these are specially crafted matrices that leverage the properties of linear algebra to perform these calculations. This isn't a new concept - 3D graphics have been doing this for a long time - we just get to use it in HTML now.

So I did some searching and found the W3 SVG spec which outlines how to construct all the different 2D transform matrices. Additionally, I found Sylvester to represent the matrices and do the actual math. Using this information, we should be able to build some simple extensions to build a transform, convert it to a CSS transform style string, and then do that in reverse to perform additional transforms.

First, we need to define some transform "recipes" that are matrices that can be combined together to form a final transform:


var _T = {
rotate: function(deg)
{
var rad = parseFloat(deg) * (Math.PI/180),
costheta = Math.cos(rad),
sintheta = Math.sin(rad);

var a = costheta,
b = sintheta,
c = -sintheta,
d = costheta;

return $M([
[a, c, 0],
[b, d, 0],
[0, 0, 1]
]);
},

skew: function(dx, dy)
{
var radX = parseFloat(dx) * (Math.PI/180),
radY = parseFloat(dy) * (Math.PI/180),
c = Math.tan(radX),
b = Math.tan(radY);


return $M([
[1, c, 0],
[b, 1, 0],
[0, 0, 1]
]);
},

translate: function(x, y)
{
var e = x || 0,
f = y || 0;

return $M([
[1, 0, e],
[0, 1, f],
[0, 0, 1]
]);
},

scale: function(x, y)
{
var a = x || 0,
d = y || 0;

return $M([
[a, 0, 0],
[0, d, 0],
[0, 0, 1]
]);
}
};


Here I've used Sylvester to represent the matrices. The Matrix class stores the matrix as a series of arrays. The class provides functions to perform matrix operations on these representations.

Next, we need to be able to convert from the CSS style string to a matrix and back again:


toString: function (m)
{
var s = 'matrix(',
r, c;

for (c=1;c<=3;c++)
{
for (r=1;r<=2;r++)
s += m.e(r,c)+', ';
}

s = s.substr(0, s.length-2) + ')';

return s;
},

fromString: function (s)
{
var t = /^matrix\((\S*), (\S*), (\S*), (\S*), (\S*), (\S*)\)$/g.exec(s),
a = parseFloat(!t ? 1 : t[1]),
b = parseFloat(!t ? 0 : t[2]),
c = parseFloat(!t ? 0 : t[3]),
d = parseFloat(!t ? 1 : t[4]),
e = parseFloat(!t ? 0 : t[5]),
f = parseFloat(!t ? 0 : t[6]);

return $M([
[a, c, e],
[b, d, f],
[0, 0, 1]
]);
}



Now let's do some real work with those functions. Let's say we have a div called "myDiv" and we want to iteratively rotate it by 25 degrees. First, we need to get the current transform and convert it to a matrix:


var t = _T.fromString($('#myDiv').css('transform'));


Next, we need to build our rotation matrix:


var r = _T.rotate(25);


Now, we can multiply the existing transform matrix by the rotation matrix to find the combined transform:


var n = t.x(r);


Finally, we convert the new transform matrix back to a string and set the style on the element:


$('#myDiv').css({
transform: _T.toString(n)
});


That's all there is to it. Any existing transforms are preserved and the 25 degree rotation is added to whatever is already there without us having to know what it is. There's a complete demo of the different transform functions on my sandbox.

When you try the demo, you'll notice I made a note about "gimble lock". This can occur when applying successive rotations on a transform that already contains a rotation. This effect happens when working with Euler angles and is generally fixed by using quaternions to manage the rotation calculations. This is math I did not want to dive into right now. That means I will probably need to use data() to record the current angle and use that to build a new transform. I still think using matrices to calculate the transforms is the correct approach. The simplicity in the representation is very appealing. Additionally, since this is the approach used throughout the rest of the transformation world, it will be a lot easier to find examples and understand concepts if we're using the same method.