XYK

3D Matrix Math for CSS Using Sylvester

July 26th, 2013

Sandbox

So recently I’ve been working on more CSS-based transformations and graphics. One of the problems with the current implementation of transform in webkit is that there is no way to individually assign values with JavaScript, making multi-dimensional animations impossible. This is because all of the transformations (like rotateX, translateZ, and scale) are grouped into a single property: -webkit-transform. For example, if you want to rotate 60 degrees in the X and Y axes, you would apply this piece of CSS:
-webkit-transform: rotateX(60deg) rotateY(60deg);

The problem occurs when you try and modify one or both of the values with JavaScript or JQuery. Since JQuery doesn’t support shorthand CSS for the .css() function, there is no way to change either of them without previously knowing the values.

Matrix3D()

The transform property also accepts a 4x4 transformation matrix that is calculated from all seven possible transformations.

matrix3d(0, 1,…15, 16)

Calculating it is pretty math-heavy, so I used a vector math library called Sylvester.js. The function below is heavily based on this tutorial.

The complete function returns a matrix3d value from seven parameters.

function returnMatrix3D(rX, rY, rZ, scale, tX, tY, tZ) {
var deg2rad, scale;
deg2rad = Math.PI / 180; // Degrees to radians constant

var rotationXMatrix, rotationYMatrix, rotationZMatrix, s, scaleMatrix, transformationMatrix, translationMatrix;
rotationXMatrix = $M([
[1, 0, 0, 0],
[0, Math.cos(rX * deg2rad), Math.sin(-rX * deg2rad), 0],
[0, Math.sin(rX * deg2rad), Math.cos(rX * deg2rad), 0],
[0, 0, 0, 1]]);

rotationYMatrix = $M([
[Math.cos(rY * deg2rad), 0, Math.sin(rY * deg2rad), 0],
[0, 1, 0, 0],
[Math.sin(-rY * deg2rad), 0, Math.cos(rY * deg2rad), 0],
[0, 0, 0, 1]]);

rotationZMatrix = $M([
[Math.cos(rZ * deg2rad), Math.sin(-rZ * deg2rad), 0, 0],
[Math.sin(rZ * deg2rad), Math.cos(rZ * deg2rad), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]);


s = scale;
scaleMatrix = $M([[s, 0, 0, 0], [0, s, 0, 0], [0, 0, s, 0], [0, 0, 0, 1]]);

transformationMatrix = rotationXMatrix.x(rotationYMatrix).x(rotationZMatrix).x(scaleMatrix);
transformationMatrix = transformationMatrix.transpose();
translationMatrix = $M([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [tX, tY, tZ, 1]]);
transformationMatrix = transformationMatrix.x(translationMatrix); // Apply transformation matrix AFTER transposing rotation and scale

s = "matrix3d(";
s += transformationMatrix.e(1, 1).toFixed(5) + "," + transformationMatrix.e(1, 2).toFixed(5) + "," + transformationMatrix.e(1, 3).toFixed(5) + "," + transformationMatrix.e(1, 4).toFixed(5) + ",";
s += transformationMatrix.e(2, 1).toFixed(5) + "," + transformationMatrix.e(2, 2).toFixed(5) + "," + transformationMatrix.e(2, 3).toFixed(5) + "," + transformationMatrix.e(2, 4).toFixed(5) + ",";
s += transformationMatrix.e(3, 1).toFixed(5) + "," + transformationMatrix.e(3, 2).toFixed(5) + "," + transformationMatrix.e(3, 3).toFixed(5) + "," + transformationMatrix.e(3, 4).toFixed(5) + ",";
s += transformationMatrix.e(4, 1).toFixed(5) + "," + transformationMatrix.e(4, 2).toFixed(5) + "," + transformationMatrix.e(4, 3).toFixed(5) + "," + transformationMatrix.e(4, 4).toFixed(5);
s += ")";

return s;
}

Sample Outputs

returnMatrix3D(60, 45, 60, 1, 0, 0, 0) - 60deg in X, 45deg in Y, and 60deg in Z. Scale of 1. The value returned is identical to transform: rotateX(60deg) rotateY(45deg) rotateZ(60deg).

matrix3d(0.35355,0.73920,0.57322,0.00000,-0.61237,-0.28033,0.73920,0.00000,0.70711,-0.61237,0.35355,0.00000,0.00000,0.00000,0.00000,1.00000)

Benchmarks

Benchmark ran with jsperf.com. Not sure how accurate this is.