May 10, 2011 29

Handling 3D geometry with 3D CSS3 transforms

By

Cube3D Screenshot

View Demo (WebKit only) | Source @ GitHub

I haven’t done much 3D work except some OpenGL a number of years ago at university, so I had forgotten that:

3D is hardwork!

In terms of CSS3 3D transformations, its not so much that the actual CSS is any more difficult, its just that when you’re performing 3D rotations you very quickly lose track of important things such as orientation. Because of this, I decided to experiment with a 3D CSS cube that could be rotated via user input. For those that want to just dive in and look at the code, its available now on GitHub or take a look at the demo (WebKit only).

As a starting point, take a look at Paul Hayes very cool demo of a 3D cube. This is essentially what I wanted to create but it very elegantly highlights some of the difficulties with 3D work.

Try rotating the cube with the arrow keys, does the cube always rotate in the way you’d expect? If you’re anything like me it didn’t, I expect that when you press ‘left’ the cube would always visually spin left to right etc. But if you first press ‘up’ and then press ‘left’ the cube spins counter-clockwise and doesn’t show you a different cube face.

Why does this happen?

In Paul’s example the up/down keys have been hardwired to rotate around the x-axis and left/right to the y-axis. This seems logical, given that directly after initialising the cube this will create the correct result. But what is actually happening when the cube is rotated?

Rotation around x-axis

The above image shows what happens to the X, Y & Z axis when the user presses ‘up’. As you can see, after the first rotation the z-axis is now where the y-axis was. So for future rotations, in order to rotate the cube horizontally we need to rotate around the z-axis, instead of the y-axis. This is why you get the perculiar behaviour in Paul’s demo.

How do we work around this?

To work around this we need to delve a little deeper into the Javascript implementation of 3D transforms and forgo some of the neat helper functions like rotateX(deg) and rotateY(deg).

The initial assumption that for vertical rotations we want to rotate around X and for horizontal around Y is fundementally correct, its just we need to keep track of where these axes are after rotations are applied to the DOM.

To do this we can make use of the transformation matrix. For more information of matrices there is quite a good explanation with a focus towards CSS3 here.

In a WebKit based browser we can get the the current matrix for an element like this (using jQuery or Zepto frameworks for convenience):

var div = $('div#test');
var m = new WebKitCSSMatrix(div.css('webkitTransform'));

What we really need to understand about the matrix is that it defines the various transformations applied to our DOM elements, be they translates, rotations or scales. That is, the matrix tells us how to convert a point from its default position to where it currently is in 3D space given the current rotation.

With this knowledge what we need to do is use the matrix to work out where the original X and Y axis are currently situated. Each of the axes are actually just vectors describing a direction in 3D space, so can be described by:

x-axis => (1, 0, 0)
y-axis => (0, 1, 0)

In order to translate these vectors using the current transform matrix we need to perform some matrix maths. See here for a more detailed explaination of multiplying a matrix by a vector, or you can just trust that the following is true:

Matrix Vector Multiplication

Thats great in theory but how do we do that in Javascript? Start by creating a helper Vector object:

var Vector = function(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

We then need a way to enable matrix/vector multiplications, I decided to extend the WebKitCSSMatrix object here and add a helper function to do this:

WebKitCSSMatrix.prototype.transformVector = function(v) {
    var xOut = this.m11*v.x + this.m12*v.y + this.m13*v.z;
    var yOut = this.m21*v.x + this.m22*v.y + this.m23*v.z;
    var zOut = this.m31*v.x + this.m32*v.y + this.m33*v.z;

    return new Vector(xOut, yOut, zOut);
};

With the Vector helper object and the transformVector() function we can now work out the correct vector to rotate around:

// Rotate 90 degrees around the original x-axis
var xAxis = new Vector(1, 0, 0);
var angle = 90;

// Get the current Matrix
var currentMatrix = new WebKitCSSMatrix(div.css('webkitTransform'));

// Work out what the vector looks like under the current Matrix
var xAxisTranslated = currentMatrix.transformVector(xAxis);

With the new x-axis vector we can now use the built in rotateAxisAngle() function of the WebKitCSSMatrix object to perform the rotation:

// Rotate the Matrix around the transformed vector
var newMatrix = m.rotateAxisAngle(xAxisTranslated.x, xAxisTranslated.y, xAxisTranslated.z, angle);

At this point we’ve worked out the new matrix of the DOM element so we just need to update the element with the new matrix:

// Setup CSS Transitions
div.css({
    '-webkit-transition-duration': '400ms',
    '-webkit-transition-timing-function': 'linear',
    'transition-property': 'all',
});

setTimeout(function(){
    div.get(0).style.webkitTransform = newMatrix;
}, 5);

If you want to have a play with this code for yourself then you can grab it from GitHub.

  • Pingback: Дайджест недели, 13 мая | Софт Буум

  • Anonymous

    Thanks for explaining the subtleties of cube rotation. I struggled with this very problem when playing with Paul Hayes example but gave up on solving it. You did it!

  • http://www.joelambert.co.uk Joe Lambert

    No probs, glad it was helpful!

  • http://twitter.com/freddywang freddywang

    The difference between this demo and Paul’s demo lies on how rotation should happen. Joel’s demo involves additional matrix calculation which take care of the corresponding axis for rotation anchor. It fixes the weird cases in Paul’s demo. When you press left/right arrow, sometime you will get y-axis rotation, sometime you will get z-axis rotation depending of which cube face is in front. In Joel’s demo, rotation is consistent at any situations regardless of which cube face is in front. You should never get z-axis rotation.Yeap, 3D is hardwork if you really want a quick tiny solution without involving a huge js3D library. Well done.

  • http://www.joelambert.co.uk Joe Lambert

    Thanks!

  • Pingback: 20 Stunning Examples CSS 3d Transforms | O2 Interactive

  • Pingback: CSS3 technology in action: Design examples | TechRepublic

  • Jbx2003

    Downloaded it and guess what, this one actually works!!!!

  • http://www.markadrake.com/ Mark Drake

    Just came across this example today, really enjoyed it! What I really like most about it was accidentally hitting the down and right arrow key and the cube rotating accordingly. I’ve seen a few demos here and there that only handle one key / direction at a time.

  • http://www.joelambert.co.uk Joe Lambert

    Thanks Mark, CSS transitions really made it easy to get the multi direction support.

  • Rohan Madtha

    This is a great adaptation, thanks. I do have a question about Paul’s demo though. I’m trying to incorporate it into my code and I’m having trouble expanding the size of the faces. While I can get the faces to line up along the edges, as soon as I start rotating, the cube moves to different place (presumably due to a skewed origin). How can I understand this particular coordinate system better?

  • http://www.joelambert.co.uk Joe Lambert

    Hi Rohan,

    Pauls demo does have the “skewed origin” as you mention, I think this also happens at the normal size. My blog post however discusses why that happens and how you can work around it. 

    If there is an difference in behaviour between Pauls example and your code then I can only suggest you drop him a message and see if he can help out. He’s @fofr:twitter on Twitter

  • Pingback: Amazing CSS 3D Animation Tutorials : Daily Syntax

  • Pingback: GNC Designstudio

  • Jonas

    One problem I have is, that I can’t interact with elements like links on other faces than the front and the “back” of the cube. Check it here: http://fundstuecke.jonasbach.de/ How can I make the links on the other sides of the cube clickable as well?

  • http://www.joelambert.co.uk Joe Lambert

    What browser are you using? I think this is probably a result of the browser using hardware acceleration and pre-rendering the tiles on the GPU.

  • Jonas

    On Chrome 20 / Mac, only the front and back are fully clickable. The left side isn’t clickable at all, while the other sides seem to react precisly on 50% of their surface.

    In Safari 5.1 all sides work. Only the left side doesn’t react. I’m not sure how to work around this, if it’s due to pre-rendered tiles. Does that mean, you can’t use 3D transformations which feature interactive elements?

  • Pingback: Continuing Da Cube | Erti Ori Sami

  • Pingback: 20 exemplos extraordinários de CSS 3D « wiliamluis

  • http://www.facebook.com/profile.php?id=612867245 Dave Watts

    you should take out this check

    if(!flux.browser.supports3d)because I have had the 3D css code like this working in my browser, however my browser fails this checkCheersDave

  • Pingback: 20 stunning examples of CSS 3D transforms » Web Design & Web Development: HTML, CSS, JQUERY, PHP

  • Alex_under

    Hello! Check out my implementation of the cube. It doesn’t relly only on Webkit, so the project works even in Firefox and yet the cube animation works as it should. http://qbizm.ru/en

  • Bernard Latanowicz

    Hi Joe, first I’d like to thank you for great explanation. I’ve been using your code as an example to learn transforms by writing my own cube. I can’t figure out one thing and I’d like ask you to help me with it.

    When I press left arrow my matrix3d in the beginning of applyRotation has values: matrix3d(0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000) and this is ok.

    But when I press, for instance, left and up soon, my matrix3d is: matrix3d(-0.278991, -0.960294, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, -0.960294, 0.278991, -0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000) and my cube is rotated by some angle.

    Your function works different. When I press left and then up, your matrix3d has only zeros and ones in itsself. How do you save/load these values.

    I know I get the current values and you get the values of “next front” side, but I can’t find where and how you’re doing this. Could you help me, please?

  • Pingback: 30 Amazing running 3D effect with CSS3 & HTML5 - Magazinein

  • Bernard Latanowicz

    Hi Joe, it’s me again :-)

    I’ve just figured out my problem. You use zepto.js and I use jQuery.js Function css() is different in these two libraries. I don’t want to attach zepto, so I’ll write my own one.

  • Mohit Panda45

    can we design 3d animations or any sites in css3 without using

    <

    div>

  • Pyromaster

    can this be made to work on iOS?

  • disqus_sYoIgdVeok

    wtf bbq

  • Dan

    Two years old, but still interesting, thanks! :) There seems to be a bug in Chrome 31/Mac, try this sequence of keystrokes: right-right-down Animates seemingly correct but then jumps to a different side. Safari is ok though.