我知道有很多纯 CSS 立方体教程。 我自己也做过几个。 但到了 2017 年中期,当 CSS 自定义属性在所有主要桌面浏览器中都得到支持时,它们都感觉……过时了,而且很 WET。 我认为我应该做点什么来解决这个问题,所以这篇文章就诞生了。 它将向您展示当今构建 CSS 立方体的最有效路径,同时还解释了您应该避开的一些常见但不太理想的立方体编码模式。 让我们开始吧!
HTML 结构
HTML 结构如下:一个 .cube
元素,它包含 .cube__face
子元素(6
个)。 我们使用 Haml,这样就可以尽可能少地编写代码
.cube
- 6.times do
.cube__face
我们不使用 .front
、.back
和类似的类。 它们没有用,因为它们会使代码膨胀,并使其逻辑性降低。 相反,我们将使用 :nth-child()
来定位面。 我们不需要担心浏览器的支持,因为我们在这里构建的是使用 3D 变换的东西,这需要更新得多的浏览器支持!
基本样式
所有这些元素都是绝对定位的
[class*='cube'] { position: absolute }
.cube
是场景元素的子元素,在这种情况下是 body
,因为我们希望尽可能保持简单。 如果我们在场景中有多个 3D 形状,并且我们希望它们以 3D 方式交互,那么我们的立方体将是该组合的子元素,而该组合将是场景的子元素。
我们使 body
覆盖整个视窗并设置 perspective
,以便更近的东西看起来更大,而更远的东西看起来更小。
body {
height: 100vh;
perspective: 25em
}
当我经常想要做的事情是,当全高 body
是场景时,在 .cube
上设置 font-size
,使其依赖于最小视窗尺寸。 如果我随后以 em
单位设置立方体尺寸,这将使我们的整个立方体随着视窗很好地缩放。
.cube { font-size: 8vmin }
我不直接在 vmin
单位中设置立方体尺寸的原因是 Edge 浏览器中的一个错误。
然后我们给 .cube
元素一个 transform-style
为 preserve-3d
,这样它的立方体子元素就不会被展平到它的平面中,以防我们决定对其进行动画处理,并将它放在场景的中间,使用 top
和 left
偏移量。 这是立方体的初始定位,最好使用偏移量,而不是为此使用 translate()
变换。 我已经看到,有时人们对此感到困惑,因为他们听说,出于性能原因,最好使用变换而不是偏移量……这是真的,但这适用于对位置进行动画处理,而不是对初始位置进行动画处理。 这里非常简单的规则是:使用偏移量或边距,在初始定位时,无论哪种方法更方便,从该初始位置开始,使用变换对位置进行动画处理。
.cube {
top: 50%; left: 50%;
transform-style: preserve-3d;
}
然后我们选择一个立方体边长,并将其设置为立方体面的 width
和 height
。 我们还给面赋予一个负边距,等于立方体边长的一半,这样它们就位于正中间。 同样,这与立方体面的初始定位有关。 我们还赋予它们一个 box-shadow
,只是为了让我们可以看到它们。
$cube-edge: 8em;
.cube__face {
margin: -.5*$cube-edge;
width: $cube-edge; height: $cube-edge;
box-shadow: 0 0 0 2px;
}
我经常看到代码中在所有东西上都设置了 transform-style: preserve-3d
。 这是不必要的,是对 preserve-3d
工作原理的误解。 它只需要在将要应用 3D 变换的东西上设置(立即、在用户交互之后、通过自动运行的动画……无论如何)并且 具有 3D 变换的子元素。 在我们的特定情况下,这仅仅是 .cube
元素。 场景不会在 3D 中进行变换,.cube__face
元素没有子元素。
我看到的另一个不必要的事情是在 .cube
元素上设置显式尺寸。 此元素不可见。 我们没有在其中设置任何文本,我们没有设置和背景、边框或阴影。 它在这里的唯一目的是作为一个容器,我们可以对它的位置进行动画处理,以便以相同的方式同时轻松地移动所有面子元素。 在这个绝对定位的 .cube
元素上不设置任何尺寸意味着它的尺寸被计算为 0x0
,因此在它的面子元素上设置任何 %
值偏移也是没有意义的。 top: 0
与 top: 50%
或任何其他百分比值对于父元素具有 0x0
尺寸的元素来说是完全相同的。 同样适用于所有其他偏移量(right
、bottom
、left
)。
有人问我,为什么不将 .cube
的 top
和 left
设置为 calc(50% - #{.5*$cube-edge})
并完全从 .cube__face
中删除 margin
,如果我这么关心压缩代码的话。 嗯,这是因为两者并没有真正产生相同的结果,即使 .cube__face
元素最终都在屏幕的中间。 为了说明这一点,让我们给 .cube
元素一个红色的 box-shadow
,以便我们能看到它,并并排查看这两种情况
请参阅 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
在上面的演示中,我们的 .cube
元素在两种情况下定位不同。 当对它的偏移量使用 calc()
值并跳过其子元素的边距时,它的位置不再与场景的中心重合,而是与它的面子元素的左上角重合。 那又怎样? 反正它在我们实际的演示中是不可见的……
虽然那是真的,但不同的位置也意味着不同的 transform-origin
。 而且如果我们决定旋转或缩放我们的 .cube
(这是我们决定要做的),这就会改变事情。 所以考虑以下立方体的关键帧动画
@keyframes rot { to { transform: rotateY(1turn) } }
这是绕立方体的 y
轴旋转。 这两种情况的结果并不相同
请参阅 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
在这两种情况下,面都围绕其父立方体的 y
轴旋转,但这个 y
轴相对于面的位置不同。 它在初始情况下与面的 y
轴重合,而在第二种情况下与面的左边缘重合。 这就是我不将立方体面的负边距引入父立方体的偏移量的原因:它会影响立方体在 3D 中的动画。
用变换构建立方体
我们在上面的演示中还没有得到一个立方体。 为了做到这一点,我们需要在 3D 中定位面。 有多种变换组合可以实现相同的效果,但最有效和最合乎逻辑的方法是从以 90°
的增量围绕它们平面中的一个轴(x
或 y
)旋转前四个面开始,然后围绕同一平面中的另一个轴旋转剩余的两个面 ±90°
。 然后我们沿着垂直于它们平面的轴(它们的 z
)轴链式地进行一个半立方体边长的平移。
关于平移和旋转以及如何获得用于创建长方体的 transform
链的详细解释可以在 这篇文章中找到。 立方体的情况是一个简化版本,其中沿三个轴的所有尺寸都相等。
考虑到我们选择围绕它们的 y
轴旋转前四个面,我们的 transform
链看起来如下
.cube__face:nth-child(1) {
transform: rotateY( 0deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(2) {
transform: rotateY( 90deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(3) {
transform: rotateY(180deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(4) {
transform: rotateY(270deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(5) {
transform: rotateX( 90deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(6) {
transform: rotateX(-90deg) translateZ(.5*$cube-edge)
}
现在我们用它们的 rotate3d(i, j, k, a)
等效项替换 rotateY(ay)
和 rotateX(ax)
组件。 rotate3d()
函数中的 i
、j
和 k
是旋转轴在坐标系的 x
、y
和 z
轴上的单位向量的分量,而 a
是绕该旋转轴的旋转角度。
由于 rotateY()
中的旋转轴是 y
轴,因此单位向量在另外两个轴(i
在 x
轴上,k
在 z
轴上)上的分量是 0
,而在 y
轴上的分量(j
)是 1
。 另外,在这种情况下 a
是 ay
。
类似地,在 rotateX()
的情况下,我们有 i
是 1
,j
和 k
是 0
,而 a
是 ax
。 因此,我们使用 rotate3d
的等效链将是
.cube__face:nth-child(1) {
transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 0deg /* 0*90° */)
translateZ(.5*$cube-edge)
}
.cube__face:nth-child(2) {
transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 90deg /* 1*90° */)
translateZ(.5*$cube-edge)
}
.cube__face:nth-child(3) {
transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 180deg /* 2*90° */)
translateZ(.5*$cube-edge)
}
.cube__face:nth-child(4) {
transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 270deg /* 3*90° */)
translateZ(.5*$cube-edge)
}
.cube__face:nth-child(5) {
transform: rotate3d(1 /* i */, 0 /* j */, 0 /* k */, 90deg /* 1*90° */)
translateZ(.5*$cube-edge)
}
.cube__face:nth-child(6) {
transform: rotate3d(1 /* i */, 0 /* j */, 0 /* k */, -90deg /* -1*90° */)
translateZ(.5*$cube-edge)
}
我们在上面的代码中注意到了一些事情。 首先,k
分量始终为 0
。 然后,i
分量对于前四个面为 0
,对于剩余的两个面为 1
,而 j
分量对于前四个面为 1
,对于最后两个面为 0
。 最后,角度值始终可以写成一个乘数乘以 90°
。
这意味着我们可以在代码中引入 CSS 变量,这样我们就不必重复这些变换函数
.cube__face {
transform: rotate3d(var(--i), var(--j), 0, calc(var(--m)*90deg))
translateZ(.5*$cube-edge);
&:nth-child(1) { --i: 0; --j: 1; --m: 0; }
&:nth-child(2) { --i: 0; --j: 1; --m: 1; }
&:nth-child(3) { --i: 0; --j: 1; --m: 2; }
&:nth-child(4) { --i: 0; --j: 1; --m: 3; }
&:nth-child(5) { --i: 1; --j: 0; --m: 1; }
&:nth-child(6) { --i: 1; --j: 0; --m: -1; }
}
由于--i
和--j
在最初四个面都保持相同的值,而只有在最后两个面才改变,我们可以将它们的默认值分别设置为0
和1
,然后在第5
和6
个面时,将它们分别切换为1
和0
。这两个面可以通过:nth-child(n + 5)
选择。此外,我们可以将--m
的默认值设置为0
,从而完全消除对:nth-child(1)
规则的需要。
.cube__face {
transform: rotate3d(var(--i, 0), var(--j, 1), 0, calc(var(--m, 0)*90deg))
translateZ(.5*$cube-edge);
&:nth-child(n + 5) { --i: 1; --j: 0 }
&:nth-child(2 /* 2 = 1 + 1 */) { --m: 1 }
&:nth-child(3 /* 3 = 2 + 1 */) { --m: 2 }
&:nth-child(4 /* 4 = 3 + 1 */) { --m: 3 }
&:nth-child(5 /* 5 = 4 + 1 */) { --m: 1 /* 1 = pow(-1, 4) */ }
&:nth-child(6 /* 6 = 5 + 1 */) { --m: -1 /* -1 = pow(-1, 5) */ }
}
更进一步,我们注意到,无论是1
还是0
,--j
都可以用calc(1 - var(--i))
替换,而--m
要么是前四个面的面索引,要么是最后两个面的面索引的-1
次方。这使我们能够消除--j
变量,并在循环中设置乘数--m
.cube__face {
--i: 0;
transform: rotate3d(var(--i), calc(1 - var(--i)), 0, calc(var(--m, 0)*90deg))
translateZ(.5*$cube-edge);
&:nth-child(n + 5) { --i: 1 }
@for $f from 1 to 6 {
&:nth-child(#{$f + 1}) { --m: if($f < 4, $f, pow(-1, $f)) }
}
}
结果可以在下面看到

这里最大的区别在于编译后的代码。使用这种 CSS 变量方法,我们只需编写一次变换函数
.cube__face {
--i: 0;
transform: rotate3d(var(--i), calc(1 - var(--i)), 0, calc(var(--m, 0)*90deg))
translateZ(4em);
}
.cube__face:nth-child(n + 5) { --i: 1 }
.cube__face:nth-child(2) { --m: 1 }
.cube__face:nth-child(3) { --m: 2 }
.cube__face:nth-child(4) { --m: 3 }
.cube__face:nth-child(5) { --m: 1 }
.cube__face:nth-child(6) { --m: -1 }
如果没有 CSS 变量,我们所能做到的最好的方法仍然需要为每个面重复编写变换函数
.cube__face:nth-child(1) {
transform: rotateY(0deg) translateZ(4em)
}
.cube__face:nth-child(2) {
transform: rotateY(90deg) translateZ(4em)
}
.cube__face:nth-child(3) {
transform: rotateY(180deg) translateZ(4em)
}
.cube__face:nth-child(4) {
transform: rotateY(270deg) translateZ(4em)
}
.cube__face:nth-child(5) {
transform: rotateX(90deg) translateZ(4em)
}
.cube__face:nth-child(6) {
transform: rotateX(-90deg) translateZ(4em)
}
动画立方体
我们可以向.cube
元素添加一个关键帧animation
.cube { animation: ani 2s ease-in-out infinite }
@keyframes ani {
50% { transform: rotateY(90deg) rotateX(90deg) scale3d(.5, .5, .5) }
100% { transform: rotateY(180deg) rotateX(180deg) }
}
结果可以在下面看到

当前支持状态和跨浏览器版本
那些没有使用 WebKit 浏览器的人可能已经注意到上面的演示不工作。这是因为,目前,Firefox 和 Edge 不支持将calc()
值替换为除长度值之外的任何其他值。这包括rotate3d()
中的无单位值和角度值。一个使跨浏览器的方法是不将--j
替换为calc(1 - var(--i))
的等效值,而是使用一个角度--a
自定义属性,而不是calc(var(--m)*90deg)
.cube__face {
transform: rotate3d(var(--i, 0), var(--j, 1), 0, var(--a))
translateZ(.5*$cube-edge);
&:nth-child(n + 5) { --i: 1; --j: 0 }
@for $f from 1 to 6 {
&:nth-child(#{$f + 1}) { --a: if($f < 4, $f, pow(-1, $f))*90deg }
}
}
这确实意味着我们现在有一点冗余,但并不糟糕,而且我们的结果现在是跨浏览器的。
添加文本和背景
接下来,我们可以向立方体的每个面添加文本。要么对所有面都一样
.cube
- 6.times do
.cube__face Boo!
…要么每个面都不同(我们在这里切换到 Pug,因为它允许我们编写比 Haml 在这种情况下更少的代码)
- var txt = ['ginger', 'anise', 'nutmeg', 'cinnamon', 'vanilla', 'cloves'];
- var n = txt.length;
.cube
while n--
.cube__face #{txt[n]}
在这种情况下,我们还设置了text-align: center
,将line-height
设置为$cube-edge
,并调整$cube-edge
和font-size
的值,以获得最佳的文本匹配
$cube-edge: 5em;
.cube {
font: 8vmin/ #{$cube-edge} cookie, cursive;
text-align: center;
}
我们得到以下结果

我们也可以为我们的每个面添加一些柔和的渐变背景
$pastels: (#feffaa, #b2ff90) (#fbc2eb, #a6c1ee) (#84fab0, #8fd3f4) (#a1c4fd, #c2e9fb)
(#f6d365, #fda085) (#ffecd2, #fcb69f);
.cube__face {
background: linear-gradient(var(--ga), var(--gs));
@for $f from 0 to 6 {
&:nth-child(#{$i + 1}) {
--ga: random(360)*1deg; /* gradient angle */
--gs: nth($pastels, $f + 1); /* gradient stops */
}
}
}
上面的代码为我们提供了漂亮的柔和立方体

一个用例
我使用这种创建长方体的方法,在一个由Dave Whyte 的动画循环启发的演示中。

拖动时旋转立方体
在此之后,还有一个需要解决的问题:如果立方体不是使用 CSS 关键帧自动动画,而是通过拖动来旋转,该怎么办?让我们看看如何做到这一点!
我们首先选择.cube
元素,并确定拖动过程中各个阶段将要发生的事情。在mousedown
/touchstart
事件中,我们将所有内容锁定到位,以进行立方体旋转。这意味着将拖动标志设置为true
,并读取事件发生的点的坐标,这些坐标也是mousemove
/touchmove
检测到的第一个移动的起点坐标。在mousemove
/touchmove
事件中,如果拖动标志为true
,我们将旋转立方体。在mouseup
/touchend
事件中,同样,只有在拖动标志为true
时,我们执行释放动作:我们将拖动标志再次设置为false
,并清除初始坐标。
const _C = document.querySelector('.cube');
let drag = false, x0 = null, y0 = null;
/* helper function to handle both mouse and touch */
function getE(ev) { return ev.touches ? ev.touches[0] : ev };
function lock(ev) {
let e = getE(ev);
drag = true;
x0 = e.clientX;
y0 = e.clientY;
};
function rotate(ev) {
if(drag) { /* rotation happens here */ }
};
function release(ev) {
if(drag) {
drag = false;
x0 = y0 = null;
}
};
addEventListener('mousedown', lock, false);
addEventListener('touchstart', lock, false);
addEventListener('mousemove', rotate, false);
addEventListener('touchmove', rotate, false);
addEventListener('mouseup', release, false);
addEventListener('touchend', release, false);
现在剩下的就是填写rotate()
函数的内容了!
对于mousemove
/touchmove
监听器捕获的每一个微小移动,我们都有一个起点和一个终点。终点坐标(x,y
)是每次mousemove
/touchmove
触发时通过clientX
和clientY
读取的坐标。起点的坐标(x0,y0
)要么与先前微小移动的终点坐标相同,要么,如果之前没有移动,则与mousedown
/touchstart
触发的点的坐标相同。这意味着,在rotate()
函数中完成所有其他操作后,我们将x0
设置为x
,将y0
设置为y
function rotate(ev) {
if(drag) {
let e = getE(ev),
x = e.clientX, y = e.clientY;
/* rotation code here */
x0 = x;
y0 = y;
}
};
接下来,我们计算当前微小移动的终点和起点沿两个轴(dx
和dy
)以及对角线(d
)的坐标差。如果d
为0
,则我们没有真正移动(也许不应该触发任何事件,但以防万一),因此我们退出函数,不做任何其他操作,甚至不将x0
和y0
分别设置为x
和y
- 在这种情况下它们是相同的。
function rotate(ev) {
if(drag) {
let e = getE(ev),
x = e.clientX, y = e.clientY,
dx = x - x0, dy = y - y0,
d = Math.hypot(dx, dy);
if(d) {
/* actual rotation happens here */
x0 = x;
y0 = y;
}
}
};
我们处理从可能以某种方式变换的先前状态开始的拖动旋转的方式如下:我们将对应于当前微小移动的rotate3d()
与当前微小移动开始时计算的立方体transform
值进行链式连接。也就是说,除非计算的transform
值为none
,在这种情况下,我们将它与空值进行链式连接。我们可以将整个transform
链写入样式表或作为内联样式,或者…我们可以再次使用 CSS 变量!
在 CSS 中,我们将.cube
元素的transform
属性设置为rotate3d(var(--i), var(--j), 0, var(--a))
,它与变换链的先前值var(--p)
进行链式连接。为了简化操作,我们将旋转轴单位向量沿z
轴的成分保持为0
。
.cube {
transform: rotate3d(var(--i), var(--j), 0, var(--a)) var(--p);
}
因为我们已经完成了以上操作,并且 CSS 变量是继承的,所以现在我们需要明确地为.cube__face
元素设置--i
和--j
,分别设置为0
和1
。否则,将应用从.cube
元素继承的值,而不是在var()
中指定的默认值。
.cube__face {
--i: 0; --j: 1;
transform: rotate3d(var(--i), var(--j), 0, var(--a))
translateZ(.5*$cube-edge);
}
回到 JavaScript,我们读取计算的transform
值,并将其设置为--p
变量。旋转角度取决于当前微小移动的起点和终点之间的距离d
和一个常数A
。我们将结果限制到两位小数。对于向上移动的方向,即沿y
轴的负方向,我们将立方体绕x
轴顺时针旋转。这意味着我们将--i
成分设置为-dy
。对于向右移动的方向,即沿x
轴的正方向,我们将立方体绕y
轴顺时针旋转,这意味着我们将--j
成分设置为dx
。
const A = .2;
function rotate(ev) {
if(drag) {
let e = getE(ev),
x = e.clientX, y = e.clientY,
dx = x - x0, dy = y - y0,
d = Math.hypot(dx, dy);
if(d) {
_C.style.setProperty('--p', getComputedStyle(_C).transform.replace('none', ''));
_C.style.setProperty('--a', `${+(A*d).toFixed(2)}deg`);
_C.style.setProperty('--i', +(-dy).toFixed(2));
_C.style.setProperty('--j', +(dx).toFixed(2));
x0 = x;
y0 = y;
}
}
};
最后,我们可以为这些自定义属性设置一些任意的默认值,以便立方体的初始位置看起来比从正面看更像 3D。
.cube {
transform: rotate3d(var(--i, -7), var(--j, 8), 0, var(--a, 47deg))
var(--p, unquote(' '));
}
unquote(' ')
值是由于使用 Sass 造成的。虽然在纯 CSS 中,空空格是 CSS 自定义属性的有效值,但当 Sass 看到类似var(--p, )
的内容时,它会抛出一个错误,因此我们需要使用unquote()
来引入那个“无值”默认值。
所有上述操作的结果是一个可以使用鼠标和触摸拖动的立方体
查看 thebabydino 在 CodePen 上的Pen (@thebabydino)。
非常棒,我建议最后再添加一个功能
改变立方体的方向,不仅可以使用拖动,还可以点击每个面,使其与查看者对齐,以便将该面移到前面
这既绝对棒极了,又毫无用处。就像看着人们用手钉钉子一样。这是否是对技巧的良好演示?当然。但我们有锤子和 WebGL。
呵呵,WebGL 很棒。
对我来说,用 WebGL 做一个立方体就像用手钉钉子一样。我知道怎么做,但这并不意味着我知道为什么,我有一种感觉,我真的理解它是如何工作的,或者我发现它比 CSS 更容易(我甚至发现用 2D 画布模拟 3D 比用 WebGL 更容易)……这只是一件猴子看到,猴子做的事。我无法在某些非常狭窄的限制之外进行调整。我绝对无法写一篇关于它的文章,它将全部是“我不太理解我在这里做了什么,我只是得出结论说它有效”。
是的,CSS 限制了我可以用 3D 做的事情。很多。但我不能用 CSS 做的事情正是我在 WebGL 中无法理解的事情,比如光照、材质,这些东西相互作用的方式。然后我就可以专注于我能理解的东西,也就是几何。特别是由于 CSS 处理这些的方式对我来说比 WebGL 更自然。在做 CSS 3D 时,我用构成 3D 的 2D 形状。使用 WebGL,首先是顶点。这对一个来自电子/数学背景而不是计算机图形背景的人来说感觉不太自然,而且感觉像是额外的工作。
Ana,我发现你的回复真的很吸引人。考虑到在进行 HTML/CSS 3D 时所涉及的所有数学知识,以及你关于该主题的极其优雅的文章,我一直以为你具有图形编程背景。了解你实际上更喜欢 HTML/CSS 3D 比建立在 OpenGL 或其变体(在本例中为 WebGL)之上的更“传统” 3D 非常有趣。酷!
哦,css 立方体,没用但有趣。我分享一下我自己的立方体吧。它可以点击和调整大小。
https://codepen.io/Velenir/full/KNLwNB/
嗨,Velenir,
非常有趣的例子。如果也可以用鼠标拖动,而不仅仅是点击面,那就完美了。你能不能改进一下?:-)
像往常一样,喜欢其中涉及的数学。也是极简代码的粉丝,代码零重复。不过我不得不说,我可能会保留带有三个变量的详细规则。它比依赖于默认值的规则多一点点代码,这与我的第二句话相矛盾,但在我看来,它乍一看更容易理解。
哦,
lock()
中误导性的缩进让我以为一秒钟出现了一个错误;)哎呀,真不敢相信我做了那样的事。已修复,谢谢!
代码块实际上包含阻止整个程序运行的错误的可能性非常低,因为代码是复制粘贴的。我要么先在文章中写出来,然后在 CodePen 上复制粘贴(如果它不能工作,那么我知道我需要修复一些东西),要么在确定它能工作之后反过来做。
但缩进是一个不同的野兽,因为它在 WordPress 编辑器中的工作方式与 CodePen 中不同。我知道我在这里经常搞砸它。知道我经常搞砸它确实让我在寻找潜在问题时更加小心。但我还是错过了它!
至于什么更容易理解……对我来说,在生成的代码中没有重复这些转换是有用的,因为我并没有真正做一些只限于一个立方体的事情,但我可能有很多其他带有转换的东西。因此,当其他转换无法按预期工作时,更容易扫描生成的代码以查找这些转换。