英文:
CSS transform rotate() X axis behaves differently from Y and Z
问题
在使用CSS制作立方体动画时,我发现在Y轴上进行90度旋转后,如果接下来在X轴上进行旋转,它会表现得好像在Z轴上旋转一样。
我在视觉上表示了空间轴,以便调试行为,看起来X轴实际上具有动态方向,这意味着旋转是根据元素自身的X轴发生的,而在Y轴和Z轴上,旋转始终相对于视口。
这种行为正常吗?我是否遗漏了什么?
我制作了这个 codepen 来调试这种行为。
HTML:
<div id="container">
<div id="square" class="square">
<h4 class="v v1">1</h4>
<h4 class="v v2">2</h4>
<h4 class="v v3">3</h4>
<h4 class="v v4">4</h4>
</div>
<div id="square2" class="square">
<h4 class="v v1">1</h4>
<h4 class="v v2">2</h4>
<h4 class="v v3">3</h4>
<h4 class="v v4">4</h4>
</div>
</div>
<div id="scrollElement"></div>
CSS:
body {
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
#container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform-style: preserve-3d;
perspective: 1000px;
display: flex;
justify-content: center;
align-items: center;
}
#square, #square2 {
width: 200px;
height: 200px;
background: red;
position: relative;
margin: 120px;
transform-style: preserve-3d;
}
.square h4 {
margin: 0;
color: #000;
font-size: 30px;
font-weight: bold;
background: #FFF;
padding: 3px;
width: 40px;
height: 40px;
border-radius: 5px;
border: 2px solid #000;
text-align: center;
}
.v {
position: absolute;
}
.v1 {
top: 10px;
left: 10px;
}
.v2 {
top: 10px;
right: 10px;
}
.v3 {
bottom: 10px;
right: 10px;
}
.v4 {
bottom: 10px;
left: 10px;
}
#scrollElement {
height: 10000px;
width: 100vw;
}
JavaScript:
gsap.registerPlugin(ScrollTrigger);
const tl = gsap.timeline();
ScrollTrigger.create({
animation: tl,
trigger: "#scrollElement",
start: "top top",
end: "100% 100%",
scrub: 0.2
});
tl.to("#square", { rotateY: "90deg", ease: "circ.out", duration: 5 })
.to("#square", { rotationZ: "90deg", ease: "circ.out", duration: 5 }, "<1")
.to("#square", {
rotateX: "90deg",
ease: "linear",
duration: 5
});
const el = document.querySelector("#square");
const { width, height } = el.getBoundingClientRect();
console.log(width, height);
const axes = {
x: {
rotate: "rotateZ(90deg)",
rotatePerp: "rotateZ(90deg) rotateY(90deg)",
top: "50%",
left: "50%",
translate: `translate(0, -${height}px)`,
translatePerp: `translate3d(0px, -${height}px, -${height}px)`,
color: "green"
},
y: {
rotate: "",
rotatePerp: "rotateY(90deg)",
top: "50%",
left: "50%",
translate: `translate(0, -${height}px)`,
color: "yellow"
},
z: {
rotate: "rotateZ(90deg) rotateX(90deg)",
rotatePerp: "rotateX(90deg)",
top: "50%",
left: "50%",
translate: `translate(0px, -${height}px)`,
color: "blue"
}
};
for (const key in axes) {
const axis = axes[key];
const div = document.createElement("div");
const div1 = document.createElement("div");
div.id = key;
div.style.transform = axis.translate + " " + axis.rotate;
div.style.top = axis.top;
div.style.left = axis.left;
div.style.backgroundColor = axis.color;
div.style.width = "5px";
div.style.height = height * 2 + "px";
div.style.position = "absolute";
div.innerHTML = `<h4>${key}</h4>`;
// Perpendicular axis
div1.id = key + "_perp";
div1.style.transform = axis.translate + " " + axis.rotatePerp;
div1.style.top = axis.top;
div1.style.left = axis.left;
div1.style.backgroundColor = axis.color;
div1.style.width = "5px";
div1.style.height = height * 2 + "px";
div1.style.position = "absolute";
div1.innerHTML = `<h4>${key}</h4>`;
el.appendChild(div);
el.appendChild(div1);
}
希望这些信息能够帮助你调试立方体旋转的行为。
英文:
While working on animating a cube in CSS, I found myself that after performing a 90° rotation on Y axis, if the next rotation happened on X axis, it behaved as if it were on Z axis.
I worked on a visual representation of the space axes to be able to debug the behaviour and it looks like the X axis effectively has a dynamic orientation, meaning the rotations happen accordingly to the element own X axis, while on Y and Z axis, the rotations are always relative to the viewport.
Is this behaviour normal?
Am I missing something?
I made this codepen to debug the behaviour.
Scroll down in the viewport to perform the rotations.
HTML:
<div id="container">
<div id="square" class="square">
<h4 class="v v1">1</h4>
<h4 class="v v2">2</h4>
<h4 class="v v3">3</h4>
<h4 class="v v4">4</h4>
</div>
<div id="square2" class="square">
<h4 class="v v1">1</h4>
<h4 class="v v2">2</h4>
<h4 class="v v3">3</h4>
<h4 class="v v4">4</h4>
</div>
</div>
<div id="scrollElement"></div>
body {
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
#container {
position:fixed;
top:0;
left:0;
width: 100%;
height:100%;
transform-style: preserve-3d;
perspective: 1000px;
display: flex;
justify-content: center;
align-items: center;
}
#square, #square2 {
width: 200px;
height: 200px;
background: red;
position:relative;
margin:120px;
transform-style: preserve-3d;
}
.square h4 {
margin:0;
color:#000;
font-size:30px;
font-weight: bold;
background: #FFF;
padding:3px;
width:40px;
height:40px;
border-radius: 5px;
border: 2px solid #000;
text-align: center;
}
.v {
position:absolute;
}
.v1 {
top:10px;
left:10px;
}
.v2 {
top:10px;
right:10px;
}
.v3 {
bottom:10px;
right:10px;
}
.v4 {
bottom:10px;
left:10px;
}
#scrollElement {
height: 10000px;
width: 100vw;
}
gsap.registerPlugin(ScrollTrigger);
const tl = gsap.timeline();
ScrollTrigger.create({
animation: tl,
trigger: "#scrollElement",
start: "top top",
end: "100% 100%",
scrub: 0.2
});
tl.to("#square", { rotateY: "90deg", ease: "circ.out", duration: 5 })
.to("#square", { rotationZ: "90deg", ease: "circ.out", duration: 5 }, "<1")
.to("#square", {
rotateX: "90deg",
ease: "linear",
duration: 5
});
////////////////////////////////////////
const el = document.querySelector("#square");
const { width, height } = el.getBoundingClientRect();
console.log(width, height);
const axes = {
x: {
rotate: "rotateZ(90deg)",
rotatePerp: "rotateZ(90deg) rotateY(90deg)",
top: "50%",
left: "50%",
translate: `translate(0, -${height}px)`,
translatePerp: `translate3d(0px, -${height}px, -${height}px)`,
color: "green"
},
y: {
rotate: "",
rotatePerp: "rotateY(90deg)",
top: "50%",
left: "50%",
translate: `translate(0, -${height}px)`,
color: "yellow"
},
z: {
rotate: "rotateZ(90deg) rotateX(90deg)",
rotatePerp: "rotateX(90deg)",
top: "50%",
left: "50%",
translate: `translate(0px, -${height}px)`,
color: "blue"
}
};
for (const key in axes) {
const axis = axes[key];
const div = document.createElement("div");
const div1 = document.createElement("div");
div.id = key;
div.style.transform = axis.translate + " " + axis.rotate;
div.style.top = axis.top;
div.style.left = axis.left;
div.style.backgroundColor = axis.color;
div.style.width = "5px";
div.style.height = height*2 + "px";
div.style.position = "absolute";
div.innerHTML = `<h4>${key}</h4>`;
// Perpendicular axis
div1.id = key + "_" + "perp";
div1.style.transform = axis.translate + " " + axis.rotatePerp;
div1.style.top = axis.top;
div1.style.left = axis.left;
div1.style.backgroundColor = axis.color;
div1.style.width = "5px";
div1.style.height = height*2 + "px";
div1.style.position = "absolute";
div1.innerHTML = `<h4>${key}</h4>`;
el.appendChild(div);
el.appendChild(div1);
}
答案1
得分: 1
这发生在所有三个轴上,不仅仅是 x
轴,这是规范规定的工作方式。对元素应用的变换始终是相对于其本地坐标系统的。
如果您围绕 z
轴旋转元素 90度
,然后围绕其 y
轴旋转,它看起来就好像第二次旋转是相对于视口的 x
轴。实际上,它始终是相对于自己的坐标系统。
初始时,元素的本地坐标系的 x
轴指向3点钟,y
轴指向下,指向6点钟,z
轴垂直指向屏幕外部,指向您。
围绕 z
轴的 90度
旋转(正旋转是顺时针方向)使本地坐标系的 x
轴指向下,指向6点钟,使本地坐标系的 y
轴指向9点钟。在这种情况下,我们绕其旋转的轴,即 z
轴,保持不变。
随后在 y
轴周围旋转 90度
(现在指向9点钟,与视口的 x
轴沿同一线),使本地坐标系的 x
轴从指向下,指向6点钟,变为指向屏幕背面垂直指向,使 z
轴从指向屏幕外部垂直指向您,变为指向下,指向6点钟。这使它看起来旋转是相对于视口的 x
轴。
第三次绕 z
轴旋转发生在现在指向下的 z
轴周围。这使它看起来就好像这次旋转是围绕视口的 y
轴发生的。
英文:
This happens for all three axes, not just the x
axis and it's how it should work, per spec. Transforms applied on an element are always relative to its local coordinate system.
If you rotate an element around the z
axis by 90deg
and then around its y
axis, it's going to look as if the second rotation is relative to the x
axis of the viewport. In fact, it's always relative to its own coordinate system.
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-css -->
div {
display: grid;
place-content: center;
margin: 2em;
width: 9em;
height: 9em;
box-shadow: 0 0 0 2px red;
transform:
perspective(25em)
rotatez(90deg) rotatey(45deg)
}
<!-- language: lang-html -->
<div>rotatez(90deg)<br>rotatet(45deg)</div>
<!-- end snippet -->
Initially, the x
axis of an element's local coordinate system points towards 3 o'clock, its y
axis points down, towards 6 o'clock and its z
axis points perpendicular out of the screen towards you.
A 90deg
rotation around the z
axis (positive rotations are clockwise) makes the x
axis of the local coordinate system point down, towards 6 o'clock and makes the y
axis of the local coordinate system point towards 9 o'clock. The axis we rotate around, in this case the z
axis, remains unchanged following a rotation.
A subsequent 90deg
around the y
axis (now pointing towards 9 o'clock, along the same line as the viewport's x
axis) makes the x
axis of the local coordinate system go from pointing down towards 6 o'clock to pointing perpendicular onto the screen, towards the back of it and the z
axis go from pointing perpendicular out of the screen towards you to pointing down, towards 6 o'clock. This makes it look like the rotation happens relative to the viewport's x
axis.
A third rotation around the z
axis happens around the z
axis that's now pointing down. This makes it look as if this rotation is happening around the viewport's y
axis.
答案2
得分: -1
这一部分内容的中文翻译如下:
这个问题在这里有更好的解释,然后得到了解决和回答。基本上:
是的,这是一个操作顺序的问题。3D 旋转可能会让人头晕,万向锁是棘手的。GSAP 的一个优点是其一致的操作顺序。这使您能够独立地动画化每个组件(x、y、z、scaleX、scaleY、rotation 等),并始终获得干净的结果。在99.9%的情况下,这正是每个人所想要的。但如果您有极其罕见的一种情况,实际上需要更改操作顺序,那么您需要在 onUpdate 中自己处理。当然,您仍然可以使用 GSAP 来动画化值,但您需要管理字符串。
英文:
This was explained better, and then solved and answered here. Basically:
> Yeah, it's an order of operation thing. 3D rotations can get mind-bending and gimbal lock is thorny. One of GSAP's strengths is its consistent order of operation. That's what allows you to animate each component (x, y, z, scaleX, scaleY, rotation, etc.) independently and always get clean results. In 99.9% of the cases, this is exactly what everyone wants. But if you've got one of those extremely rare edge cases where you actually want to CHANGE the order of operation, then you'd need to do that yourself in an onUpdate. You can still use GSAP to animate the values, of course, but you'd need to manage the strings.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论