10

I made CSS cube that I'm rotating using up/down and left/right keys but I'm having problems regarding rotation direction.

Attempt #1

DEMO

Using this article I managed to bind keys and apply rotation to the cube. My first problem was that CSS transform function rotates elements axes so when, ie. I press up, Y and Z axis change place. I adjusted original code for that case, but another problem is, since axes are vectors, when I press up 2 times X and Z are back in place but vectors are inverted (left key starts rotating cube to the right and vice-versa), so now I must rotate cube in opposite direction to get desired result and I don't have idea how to detect wheather axis are inverted.

JavaScript

var xAngle = 0,
    yAngle = 0,
    zAngle = 0,
    cube = $("#cube");

$(document).keydown(function(e) { //keyup maybe better?

  e.preventDefault();

  var key = e.which,
      arrow = {left: 37, up: 38, right: 39, down: 40},
      x = xAngle/90,
      y = yAngle/90;

    switch(key) {
      case arrow.left:
        if (x%2 == 0)
          yAngle -= 90;
        else
          zAngle += 90;
      break;
      case arrow.up:
        if (y%2 == 0)
          xAngle += 90;
        else
          zAngle -= 90;
      break;
      case arrow.right:
        if (x%2 == 0)
          yAngle += 90;
        else
          zAngle -=90;
      break;
      case arrow.down:
        if (y%2 == 0)
          xAngle -= 90;
        else
          zAngle += 90;
      break;
    }

    var rotate = "rotateX(" + xAngle + "deg) rotateY(" + yAngle + "deg) rotateZ(" + zAngle + "deg)";
    cube.css({"transform":rotate});  

});

Attempt #2

DEMO

I made another version using methods from this article which tries to solve same problem by decomposing and then updating css 3d matrix but it has other problems. After repeatedly pressing arrows in random directions cube changes viewing angle (more than one side is visible at once).

It would be great when I could get back rotated values or vector direction from 3d matrix but none of solutions I found seems to work. I'm guessing because 3d matrix is derived by multipling values from all functions that are passed (rotateX, rotateY and translateZ) and that kind of math is way over my head to figure out.

JavaScript

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

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);
};

function applyRotation(vector, angle) {

    var cube = $('#cube');

    var matrix = new WebKitCSSMatrix(cube.css('webkitTransform'));

    var vector = matrix.transformVector(vector);

    var newMatrix = matrix.rotateAxisAngle(vector.x, vector.y, vector.z, angle);

    cube.get(0).style.webkitTransform = newMatrix;
}

// rotate using arrow keys
$(document).keyup(function(e) {

    e.preventDefault();

    var key = e.which,
            arrow = {left: 37, up: 38, right: 39, down: 40},
            v,
            a;

        switch(key) {
            case arrow.left:
                v = new Vector(0,1,0),
                a = -90;
            break;

            case arrow.right:
                v = new Vector(0,1,0),
                a = 90;
            break;

            case arrow.up:
                v = new Vector(1,0,0),
                a = 90;
            break;

            case arrow.down:
                v = new Vector(1,0,0),
                a = -90;
            break;
        }

        applyRotation(v, a);

});

Attempt #3

DEMO

Third version I made rotates each side seperately and changes classes after rotation so I always just rotate X and Y in right direction, but while rotation is happening cube gets decomposed and I think up and down rotation is wrong (plus code is kinda bloated and ugly). Only plus side of this approach is bigger cross-browser compatibility for browsers that don't support preserve-3d property.

JavaScript

$(document).keyup(function(e) {

  e.preventDefault();

  var key = e.which,
      arrow = {left: 37, up: 38, right: 39, down: 40},
      front = "rotateX(0deg) translateZ(100px)",
      back = "rotateX(180deg) translateZ(100px)",
      right = "rotateY(90deg) translateZ(100px)",
      left = "rotateY(-90deg) translateZ(100px)",
      top = "rotateX(90deg) translateZ(100px)",
      bottom = "rotateX(-90deg) translateZ(100px)";

    switch(key) {
      case arrow.left:
        $(".front").css({"transform":left});
        $(".back").css({"transform":right});
        $(".left").css({"transform":back});
        $(".right").css({"transform":front});
        var front = $(".front");
        var back = $(".back");
        var left = $(".left");
        var right = $(".right");
        front.removeClass("front").addClass("left");
        back.removeClass("back").addClass("right");
        right.removeClass("right").addClass("front");
        left.removeClass("left").addClass("back");
      break;
      case arrow.up:
        $(".front").css({"transform":top});
        $(".back").css({"transform":bottom});
        $(".top").css({"transform":back});
        $(".bottom").css({"transform":front});
        var front = $(".front");
        var back = $(".back");
        var top = $(".top");
        var bottom = $(".bottom");
        front.removeClass("front").addClass("top");
        back.removeClass("back").addClass("bottom");
        top.removeClass("top").addClass("back");
        bottom.removeClass("bottom").addClass("front");
      break;
      case arrow.right:
        $(".front").css({"transform":right});
        $(".back").css({"transform":left});
        $(".left").css({"transform":front});
        $(".right").css({"transform":back});
        var front = $(".front");
        var back = $(".back");
        var left = $(".left");
        var right = $(".right");
        front.removeClass("front").addClass("right");
        back.removeClass("back").addClass("left");
        right.removeClass("right").addClass("back");
        left.removeClass("left").addClass("front");
      break;
      case arrow.down:
        $(".front").css({"transform":bottom});
        $(".back").css({"transform":top});
        $(".top").css({"transform":front});
        $(".bottom").css({"transform":back});
        var front = $(".front");
        var back = $(".back");
        var top = $(".top");
        var bottom = $(".bottom");
        front.removeClass("front").addClass("bottom");
        back.removeClass("back").addClass("top");
        top.removeClass("top").addClass("front");
        bottom.removeClass("bottom").addClass("back");
      break;
    }

});

REFERENCE MATERIAL:

Cœur
  • 37,241
  • 25
  • 195
  • 267
Teo Dragovic
  • 3,438
  • 20
  • 34
  • First demo is working fine for me (FF and Chrome) – Morpheus Dec 16 '13 at 15:51
  • @Morpheus It'll glitch sometimes when you alternate a vertical direction with a horizontal direction as he describes – Zach Saucier Dec 16 '13 at 16:16
  • Not sure if it'll help, but I [added some axis](http://codepen.io/Zeaklous/pen/aGjxm) to the first example – Zach Saucier Dec 16 '13 at 19:45
  • In example 3, the `$(".front").css({"transform":left});` lines are pointless, because after the first pass, `left` is a jQuery object, not a transform string! – Eric Dec 18 '13 at 20:59

2 Answers2

2

The problem with the attempt 2 is that rotateAxisAngle does the matrix multiplication in the oposite order of what you want. And, worse still, there is no function in the class to do the multiplication in the order that you want.

As an alternate way, I have choose to use the browser itself to do the math. I create a div that will be hidden, and where I will apply the transforms to get the new matrix.

With this approach, the javascript gets even shorter:

function applyTransform (transform) {

    var cubeCalculator = $('.cubecalculator');
    var cube = $('#cube');

    var matrix = cubeCalculator.css('webkitTransform');
    var composite = transform + ' ' + matrix;
    cubeCalculator.get(0).style.webkitTransform = composite;

    matrix = cubeCalculator.css('webkitTransform');
    cube.get(0).style.webkitTransform = matrix;
}

// rotate using arrow keys
$(document).keyup(function(e) {

    e.preventDefault();

    var key = e.which,
        arrow = {left: 37, up: 38, right: 39, down: 40},
        t;

    switch(key) {
        case arrow.left:
            t = 'rotateY(-90deg)';
        break;

        case arrow.right:
            t = 'rotateY(90deg)';
        break;

        case arrow.up:
            t = 'rotateX(90deg)';
        break;

        case arrow.down:
            t = 'rotateX(-90deg)';
        break;
    }

    applyTransform (t);

});

I think that the code is quite self explanatory: I apply the transform to the element as a composite of the new transform and the current transform (you don't need to extract the values from the matrix, can be applied as is)

demo

(I don't know why, it didn't work in codepen. have moved it to fiddle ...)

Finally I got the * Firefox to behave !

function applyTransform (transform1, transform2) {
    var matrix, composite1, composite2;
    var cubeCalculator = $('.cubecalculator');
    var cube = $('#cube');

    matrix = cubeCalculator.css('transform');
    composite1 = transform1 + ' ' + matrix;
    composite2 = transform2 + ' ' + matrix;
    cubeCalculator.get(0).style.transform = composite2;
    cube.get(0).style.transition = 'none';
    cube.get(0).style.transform = composite1;

    window.setTimeout (function() {
        cube.get(0).style.transform = composite2;
        cube.get(0).style.transition = 'transform 1s';
    }, 10   );
}

// rotate using arrow keys
$(document).keyup(function(e) {

    e.preventDefault();

    var key = e.which,
        arrow = {left: 37, up: 38, right: 39, down: 40},
        t1, t2;

    switch(key) {
        case arrow.left:
            t1 = 'rotateY(0deg)';
            t2 = 'rotateY(-90deg)';
        break;

        case arrow.right:
            t1 = 'rotateY(0deg)';
            t2 = 'rotateY(90deg)';
        break;

        case arrow.up:
            t1 = 'rotateX(0deg)';
            t2 = 'rotateX(90deg)';
        break;

        case arrow.down:
            t1 = 'rotateX(0deg)';
            t2 = 'rotateX(-90deg)';
        break;
    }

    applyTransform (t1, t2);

});

A little bit more complex code, but makes to the browser perfectly clear what you want it to do ... Works fine as long as you wait till the transition is over.

vals
  • 61,425
  • 11
  • 89
  • 138
  • _"And, worse still, there is no function in the class to do the multiplication in the order that you want._" - instead of `matrix.rotateAxisAngle(...)`, use `new WebKitCSSMatrix().rotateAxisAngle(...).multiply(matrix)` – Eric Dec 18 '13 at 20:24
  • yes, works perfectly! I adjusted your code a bit to get rid of vendor prefix http://codepen.io/teodragovic/pen/xKBqg now it works in Chrome, Opera and Safari but behaves very strange in FF (I guess Gecko doesn't handle multiplication properly). Thank you and @Eric both for some great insights – Teo Dragovic Dec 20 '13 at 16:13
  • Glad that it helped !. I have been investigating the FF issue. It's a problem only of the animation: see how, at the end of a bad movement, the face color changes (to the good one). So, the end position is ok, but the animation is wrong ... Kind of a bug, but I don't know if can be avoided somehow. – vals Dec 20 '13 at 20:22
0

Demo 2 is almost there. Your problem is that you're using the intermediate animation state of the cube to calculate the new position:

var matrix = new WebKitCSSMatrix(cube.css('webkitTransform'));

Instead, you should store and update an internal target matrix:

Additionally, it seems that this snippet of code:

var vector = matrix.transformVector(vector);
var newMatrix = matrix.rotateAxisAngle(vector.x, vector.y, vector.z, angle);

Doesn't work as intended. This does what you're looking for:

var newMatrix = new WebKitCSSMatrix().rotateAxisAngle(...).multiply(matrix)

http://codepen.io/eric-wieser/pen/BoeyD

Eric
  • 95,302
  • 53
  • 242
  • 374