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