2

I'm trying to use a single rotate3d function to rotate multiple axes of a cube at the same time, but I get a weird rotation.

If I use transform: rotateX(-30deg) rotateY(45deg) I get a nice looking angle, but if I do transform: rotate3d(-2, 3, 0, 15deg) the angle looks weird.

* { box-sizing: border-box; }

.scene {
  width: 50px;
  height: 50px;
}

.cube {
  width: 50px;
  height: 50px;
  margin: 50px;
  position: relative;
  transform-style: preserve-3d;
  transform: rotateX(-30deg) rotateY(45deg); /* works as expected */
  /* transform: rotate3d(-2, 3, 0, 15deg); */ /* looks very weird */
  transition: transform 1s;
}

.cube__face {
  position: absolute;
  width: 50px;
  height: 50px;
  border: 1px solid black;
}

.cube__face--front  {
  background: hsla(  0, 100%, 50%, 0.7);
  transform: rotateY(  0deg) translateZ(25px);
}
.cube__face--right  {
  background: hsla( 60, 100%, 50%, 0.7);
  transform: rotateY( 90deg) translateZ(25px);
}
.cube__face--back   {
  background: hsla(120, 100%, 50%, 0.7);
  transform: rotateY(180deg) translateZ(25px);
}
.cube__face--left   {
  background: hsla(180, 100%, 50%, 0.7);
  transform: rotateY(-90deg) translateZ(25px);
}
.cube__face--top    {
  background: hsla(240, 100%, 50%, 0.7);
  transform: rotateX( 90deg) translateZ(25px);
}
.cube__face--bottom {
  background: hsla(300, 100%, 50%, 0.7);
  transform: rotateX(-90deg) translateZ(25px);
}
<div class="cube">
  <div class="cube__face cube__face--front"></div>
  <div class="cube__face cube__face--back"></div>
  <div class="cube__face cube__face--right"></div>
  <div class="cube__face cube__face--left"></div>
  <div class="cube__face cube__face--top"></div>
  <div class="cube__face cube__face--bottom"></div>
</div>

I know I probably need to apply some math to make it work but I have no idea about linear algebra. Is there a formula to achieve what I want?

EDIT 1:

I found this question which is similar to mine, but I still can't figure it out. I tried transform: rotate3d(-30, 45, 0, 54deg) (sqrt(30² + 45²) = 54), which is closer to the expected result, but still not the formula I'm looking for.

EDIT 2:

I found a website to calculate a matrix3d based on any other transform functions. The resulting matrix for my initial transformation is matrix3d(0.707107, 0.353553, 0.612372, 0, 0, 0.866025, -0.5, 0, -0.707107, 0.353553, 0.612372, 0, 0, 0, -100, 1), and that works like a charm! But I need to do this dynamically.

I can get the current matrix3d of my element with window.getComputedStyle, and I found some functions to create rotation matrices and multiply 2 matrices on the mdn docs. But if I create a rotation matrix of 90º for the X axis and multiply it by the current matrix, and then apply the resulting matrix to the CSS transform, the cube goes crazy with the transformations. I'm doing it like this:

const matrix = window.getComputedStyle(cube).transform.slice(9, -1).split(', ').map(Number)
const rotation = rotateAroundXAxis(90)
const final = multiplyMatrices(matrix, rotation)
cube.style.transform = `matrix3d(${final.join(', ')})`

I would really appreciate any kind of help here.

SoKeT
  • 590
  • 1
  • 7
  • 16

1 Answers1

4

TL;DR rotate3d(0.853553, -1.319479, 0.353553, -0.936325rad)


Okay so I don't know where to start, but let me explain how transformations work. So any 3d transformation for example rotatation, translation, shear, scale, etc. can be expressed in the form of a 4x4 homogeneous matrix. (and thus css matrix3d takes 16 values)

Let's say we have a 4x4 matrix T and we want to transform a point (x, y, z) so that the new point is (x', y', z'). We can find out the new point by carring out the following matrix multiplication:

| x' |   | T11 T12 T13 T14 |   | x |
| y' | = | T21 T22 T23 T24 | x | y |
| z' |   | T31 T32 T33 T34 |   | z |
| 1  |   | T41 T42 T43 T44 |   | 1 |

Now, if the transformation doesn't involve any translations we can express such transformation in terms of 3x3 matrix also (afaik). In that case the the new point if found using the following matrix multiplication:

| x' |   | T11 T12 T13 |   | x |
| y' | = | T21 T22 T23 | x | y |
| z' |   | T31 T32 T33 |   | z |

Okay so now let's first express rotateX(-30deg) rotateY(45deg) in this matrix form. I'm going to use Rx(Θ) and Ry(Θ) as given here to find the net transformation matrix T. Also css rotates the axes/FOR instead of point so -30deg will be 30deg and 45deg will be -45deg for us as it also says here.

T = Ry(-45deg) x Rx(30deg) // order of multiplication is important, what happens first is rightmost then things are added on left

  = |  0.707107  -0.353553  -0.612372 |
    |         0   0.866025       -0.5 |
    |  0.707107   0.353553   0.612372 |

  ≈ |  0.707107  -0.353553  -0.612372  0 | // same as above but 4x4 version
    |         0   0.866025       -0.5  0 | // this is what getComputedStyle gives
    |  0.707107   0.353553   0.612372  0 |
    |         0          0          0  1 |

Calculations here on wolfram alpha

You can obtain the above T matrix from computed styles also. And use that from here onwards.

Now let's see what is rotate3d. rotate3d(ux, uy, uz, a) will rotate the point keeping an axis vector u (ux, uy, uz) with an angle a. We have the transformation we need to do that is T. So now we need to express the generalized T in form of rotate3d.

We'll use this formula to find out the axis.

| ux |   |  (0.353553) - (-0.5)      |   |  0.853553 |
| uy | = | (-0.612372) - (0.707107)  | = | -1.319479 |
| uz |   |         (0) - (-0.353553) |   |  0.353553 |

We'll use this formula to find out the angle.

0 = arccos((0.707107 + 0.866025 + 0.612372 - 1) / 2)
  = 0.936325 rad // ie -0.936325 rad according to CSS convention

So finally rotateX(-30deg) rotateY(45deg) is same as rotate3d(0.853553, -1.319479, 0.353553, -0.936325rad)

Demo:

[...document.querySelectorAll("[name='transform']")]
.forEach(radio => {
    radio.addEventListener("change", () => {
       let selectedTransform = document.querySelector("[name='transform']:checked").value;
       let cubeClasses = document.querySelector(".cube").classList;
       cubeClasses.remove("transform-a", "transform-b", "transform-c");
       cubeClasses.add(selectedTransform)
    })
})
* { box-sizing: border-box; }

.scene {
  width: 50px;
  height: 50px;
}

.cube {
  width: 50px;
  height: 50px;
  margin: 50px;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 1s;
}

.cube.transform-a {
  transform: rotateX(-30deg) rotateY(45deg);
}

.cube.transform-b {
  transform: rotate3d(0.853553, -1.319479, 0.353553, -0.936325rad);
}

.cube.transform-c {
  transform: matrix3d(0.707107, -0.353553, -0.612372, 0, 0, 0.866025, -0.5, 0, 0.707107, 0.353553, 0.612372, 0, 0, 0, 0, 1);
}

.cube__face {
  position: absolute;
  width: 50px;
  height: 50px;
  border: 1px solid black;
}

.cube__face--front  {
  background: hsla(  0, 100%, 50%, 0.7);
  transform: rotateY(  0deg) translateZ(25px);
}
.cube__face--right  {
  background: hsla( 60, 100%, 50%, 0.7);
  transform: rotateY( 90deg) translateZ(25px);
}
.cube__face--back   {
  background: hsla(120, 100%, 50%, 0.7);
  transform: rotateY(180deg) translateZ(25px);
}
.cube__face--left   {
  background: hsla(180, 100%, 50%, 0.7);
  transform: rotateY(-90deg) translateZ(25px);
}
.cube__face--top    {
  background: hsla(240, 100%, 50%, 0.7);
  transform: rotateX( 90deg) translateZ(25px);
}
.cube__face--bottom {
  background: hsla(300, 100%, 50%, 0.7);
  transform: rotateX(-90deg) translateZ(25px);
}
<div class="cube transform-b">
  <div class="cube__face cube__face--front"></div>
  <div class="cube__face cube__face--back"></div>
  <div class="cube__face cube__face--right"></div>
  <div class="cube__face cube__face--left"></div>
  <div class="cube__face cube__face--top"></div>
  <div class="cube__face cube__face--bottom"></div>
</div>
<label>
  <input type="radio" name="transform" value="transform-a"/>
  <code>rotateX(-30deg) rotateY(45deg)</code>
</label>
<label><br>
  <input type="radio" name="transform" value="transform-b" checked/>
  <code>rotate3d(0.853553, -1.319479, 0.353553, -0.936325rad)</code>
</label>
<label><br>
  <input type="radio" name="transform" value="transform-c"/>
  <code>matrix3d(0.707107, -0.353553, -0.612372, 0, 0, 0.866025, -0.5, 0, 0.707107, 0.353553, 0.612372, 0, 0, 0, 0, 1)</code>
</label>

All this complexity and maths and then they say "cSs iS nOt PrOgRaMmInG" "cSs iS eAsY" xD :P

Here's a vanilla JS implementation to calculate rotate3d:

class Matrix {
  
  constructor(raw) {
    this.raw = raw;
  }
  
  static ofRotationX(a) {
    return new Matrix([
      [1, 0, 0],
      [0, Math.cos(a), -Math.sin(a)],
      [0, Math.sin(a), Math.cos(a)]
    ])
  }
  
  static ofRotationY(a) {
    return new Matrix([
      [Math.cos(a), 0, Math.sin(a)],
      [0, 1, 0],
      [-Math.sin(a), 0, Math.cos(a)]
    ])
  }
  
  static ofRotationZ(a) {
    return new Matrix([
      [Math.cos(a), -Math.sin(a), 0],
      [Math.sin(a), Math.cos(a), 0],
      [0, 0, 1],
    ])
  }
  
  get trace() {
    let { raw } = this;
    return raw[0][0] + raw[1][1] + raw[2][2];
  }
  
  multiply(matB) {
    let { raw: a } = this;
    let { raw: b } = matB;
    return new Matrix([
      [
        a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0],
        a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1],
        a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2]
      ],
      [
        a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0],
        a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1],
        a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2]
      ],
      [
        a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0],
        a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1],
        a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2]
      ]
    ]);
  }
}


function getRotate3d(transMat) {
    let { raw: t } = transMat;
    return {
      axis: [t[2][1] - t[1][2], t[0][2] - t[2][0], t[1][0] - t[0][1]],
      angle: -1 * Math.acos((transMat.trace - 1)/2)
    }
}

console.log(getRotate3d(
  Matrix.ofRotationY(-45 * Math.PI/180)
  .multiply(Matrix.ofRotationX(30 * Math.PI/180))
));
Devansh J
  • 4,006
  • 11
  • 23
  • Thank you so much for this, I didn't expect such a long and detailed response. I think I'm getting really close to what I need but I'm still not there. Basically, I want the cube to rotate based on user input, while keeping the previous rotations, but the axes change every time I do a rotation even though I'm multiplying the rotation matrices. Could you take a look at this pen? I'd really appreciate it: https://codepen.io/anon/pen/rbKxpy – SoKeT Apr 20 '19 at 13:45
  • @SoKeT You're welcome! :) Actually that's pretty simple if you are fine with appending transform-functions like [this](https://codepen.io/devanshj/pen/mgKVQo?editors=0010). Also could you please also accept the answer because I think it answers the question. – Devansh J Apr 20 '19 at 13:56
  • @SoKeT also probably that's the only way to do it I guess, because when you replace the whole transform the browser is not sure what properties to transition and how. Appending new transform makes the transitions very explicit. – Devansh J Apr 20 '19 at 13:59
  • That's the first thing I tried, but notice how when you rotate an axis, the others change. For example if you rotate X then the next Y rotation should actually rotate the Z axis. I've also tried to keep track of this manually in order to rotate the correct axis every time but it gets out of hand really soon. – SoKeT Apr 20 '19 at 14:01
  • @SoKeT Ohhhh now I get what you want, well let me see what I can do. I think I have a solution in my mind which will work only if the rotations are going to be 90deg? Is that the case? – Devansh J Apr 20 '19 at 14:03
  • They should always be 90deg, but they can be clockwise or counter-clockwise. Does that change what you have in mind? – SoKeT Apr 20 '19 at 14:07
  • @SoKeT Yes a little, but nevermind I figured out how I'll do it with arbitary angles too. But will take some time to implement. – Devansh J Apr 20 '19 at 14:09
  • I really appreciate your effort, but you don't have to implement the whole thing. I just need some pointers here and there ^^ – SoKeT Apr 20 '19 at 14:12
  • @SoKeT For +90deg it's pretty simple, I'm also rotating the axes basically I storing what is the apparent axis currently. For eg after rotateX(90deg), z becomes y and y becomes z. Here's a [demo](https://codepen.io/devanshj/pen/mgKVQo?editors=0010). Haven't rigorously tested but works fine I guess. – Devansh J Apr 20 '19 at 14:15
  • @SoKeT Hold on isn't really accurate, after rotateX(90deg), current y is z and current z is -y. But probably that won't matter bcoz rotate(Y, 90deg) and rotate(-Y, 90deg) is same. – Devansh J Apr 20 '19 at 14:27
  • That does indeed work. I've updated your pen to include counter-clockwise turns: https://codepen.io/anon/pen/dLKMNL. My initial solution was pretty much identical except that I wasn't appending rotations, instead I would keep track of them and only apply a `rotateX`, `rotateY` and `rotateZ`. I'm not sure of the performance implications of appending rotations, but I'd still like to do it as efficiently as possible. [This guy](https://stackoverflow.com/questions/44860043/rotating-css-cube-on-fixed-axes) had pretty much the same problem, but I don't know how to extrapolate his solution – SoKeT Apr 20 '19 at 14:33
  • @SokeT The thing is you are when you are replacing the whole transform the axis and the angle both change. So the browser isn't sure what to do. Appending is the only way imo and I don't think there would be any performance issues. If you are talking about [this](https://stackoverflow.com/a/44933252/9591609) the sqrt(x^2 + y^2) being the angle is totally wrong. If you are taking about [this](https://stackoverflow.com/a/44860880/9591609) then only the angle is changing. – Devansh J Apr 20 '19 at 15:10
  • @SoKeT also I won't force you to accept my answer but it answers the question (convert multiple rotations to one rotate3d) the best way possible. – Devansh J Apr 20 '19 at 15:12
  • @SoKet Also 1. use `toFixed` as the string sometimes contains exponential form eg `1.161168e-16` 2. `acos` returns angle between -90deg and 90deg and sometimes you might want to have the actual angle. 3. you are not negating angle before passing to `rotateX` matrix function (our convention is different) – Devansh J Apr 20 '19 at 15:41
  • Well then, I guess this is good enough since this is only a side project of mine and not actual work. Thank you so much for all the time and effort you've spent on this. And sorry for not accepting it earlier, I was planning to as soon as we came to a conclusion :p – SoKeT Apr 20 '19 at 16:36
  • @SoKeT Welcome! and yeah no problem for the delay xD – Devansh J Apr 20 '19 at 16:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/192153/discussion-between-soket-and-devansh-j). – SoKeT Apr 20 '19 at 18:29