以下是 Ana Tudor 的客座文章。如果您已经看过 Ana 的作品,您可能知道她将数学和代码结合起来创作艺术。最终作品看起来需要花费很长时间才能制作完成。但是,我亲眼目睹了 Ana 如何在制作的过程中快速思考构建这样的东西。在本篇文章中,她将通过逐步教程向我们解释整个思维过程。
8 月中旬,我决定尝试重现我在 12gon 上找到的一个好看的 GIF。

我认为我应该使用 CodePen 的 Professor Mode 实时编码供人们观看。30 分钟后,结果如下
查看 Pen Möbius 6hedrons (纯 CSS) 由 Ana Tudor (@thebabydino) 在 CodePen 上创建。
让我们来看看整个过程是如何工作的。它出奇地简单!
3D 坐标系
我们将使用 3D 坐标系。

x
、y
和 z
。x
轴从左 (-
) 到右 (+
)。y
轴从上 (-
) 到下 (+
)。z
轴从屏幕背面 (-
) 到屏幕正面,朝向我们 (+
)。这三条轴的交点是坐标系的原点。xy
平面(图中以蓝色表示)是屏幕的垂直平面。yz
平面是垂直平面(以绿色表示),它将屏幕分成左右两部分。zx
平面是水平平面(以红色表示),它将屏幕分成上下两部分。
重要概念:每个 HTML 元素都有一个局部 3D 坐标系,其原点最初位于元素的 50% 50% 0
点(水平方向 50%
,垂直方向 50%
以及元素所在的平面,因为所有 HTML 元素都是平面的,所有点都包含在同一个平面中)。它可以使用 transform-origin
属性进行更改,但不用担心,这里我们不需要这样做。
基本设置
元素在 3D 中的位置始终与其父元素的 3D 坐标系相关,因此我们将 覆盖整个视窗,并将所有后代元素绝对定位在其各自父元素的
50% 50%
点。我们还设置了 transform-style: preserve-3d
,因为我们希望允许 嵌套 3D 变换元素(条形将以 3D 变换,其子元素(条形面)也会以 3D 变换)。
body {
height: 100vh;
perspective: 40em;
background: #000;
}
body * {
position: absolute;
top: 50%; left: 50%;
transform-style: preserve-3d;
}
鉴于这是一个 3D 演示, 覆盖整个视窗,因此成为我们的场景,我们也设置了
perspective
。这使得离我们更近的一切看起来比更远的一切更大。perspective
值越小,前后之间的差异就越大。下面的演示展示了如何更改场景的 perspective
值使场景中的对象(在本例中是两个立方体)以不同的方式渲染。对于每个立方体,演示还显示了其局部坐标系的 xy
平面(以蓝色表示)。
查看 Pen 更改场景的透视效果 由 Ana Tudor (@thebabydino) 在 CodePen 上创建。
初始数据
现在让我们从图像中收集一些数据。由于图像在移动,使我们头晕目眩,因此让我们将其分割成帧。我讨厌安装东西,因此我使用 在线分割器 来处理这类事情,但您可以使用任何您最熟悉的方法。分割 GIF 会告诉我,共有 43 帧,帧之间的延迟为 0.04s
,这使得动画的持续时间大约为 1.75s
。
最重要的是,这是第一帧。

太好了,这是一张静态图像,它让我可以数出条形(或正方形直立棱柱体),而不会让我头晕目眩!没错,我用手指指着当前正在计数的条形,并记住我是从哪里开始计数的。如果我数对了,那么就有 24
根条形。如果我数错了,也没关系,我喜欢 24
。我有一个很好的理由喜欢它。条形的中心点分布在水平 zx
平面中围绕 y
轴的圆形上。

在一个圆形上,有 360°
,如下面的演示所示
查看 Pen 完整圆形 - 响应式 SVG 解释 由 Ana Tudor (@thebabydino) 在 CodePen 上创建。
24
恰好是 360°
的因数,因此,如果我们想要在圆形上均匀地分布条形,我们应该每隔 360°/24 = 15°
分布一个,这是一个很好的圆形数字,我喜欢圆形数字——这就是我将动画持续时间近似为 1.75s
的原因。让我们假设我的计数技能是准确的,并将条形数量保持在 24
,并将它们之间的基本角度保持在 15°
,因为整数很好……它们只是使我们的生活更轻松!
接下来,让我们从图像中选择四个主要色调。我相信我为此使用了开发者工具选择器,但您可以使用任何您喜欢的工具。

我选择的四个色调是用于前面条形的端面 (1
上面的图中)、条形背面的端面 (2
)、条形前面的侧面 (3
) 和条形背面的侧面 (4
)。
在视觉上近似维度和距离后 (我承认我不擅长此,但我认为这些值有效),我们在 Sass 代码中设置了以下变量
$n-prisms: 24; // number of bars
$height: 6.25em; // height of a bar
$base: 1em; // base of a bar
$base-c: // base shades
#69f // base front (1)
#7e4b4c; // base back (2)
$lat-c: // lateral shades
#542252 // lateral front (3)
#7e301a; // lateral back (4)
$radius: 1.625*$height; // radius of circle we distribute the bars on
$base-angle: 360deg/$n-prisms; // base angle between two bars
$t: 1.75s; // animation duration
基本 HTML 结构
接下来,让我们确定 HTML 结构。每个条形都有四个侧面和两个端面(或底面),因此每个条形总共有六个面。所有条形都围绕其固定中点旋转,这些中点位于水平 zx
平面中已知 $radius
的圆形上。这意味着,在条形组合中,我们有 24
个定位元素将在该圆形上移动条形,并且在每个元素内部,我们都有一个带有六个面的条形。这使我们拥有以下 HTML 结构
但当然,我们不会多次复制粘贴定位器部分。有更智能、更紧凑的方法来编写此结构。例如,使用 Haml 或 Slim
.assembly
- 24.times do
.positioner
.prism
- 6.times do
.prism__face
条形面
现在我们有了 HTML 结构,让我们继续进行样式设置。我们从条形的正面开始,因为它们是唯一具有背景的元素,并且我们总是希望在屏幕上尽快看到一些东西(尤其是在实时编码时!)。我们为所有面赋予侧面面的尺寸(因为我们侧面的数量比底面的数量多,因此我们将底面视为特殊情况)。然后,我们将边距设置为负的侧面尺寸的一半,以便它们的 50% 50%
点保持在其容器的中间。当然,我们还需要为它们提供背景,以便我们可以看到它们。我们可以选择我们为侧面选择的两种色调中的任何一种;选择哪一个并不重要,当我们在水平 zx
平面中将条形分布在圆形上时,我们将用这两种色调的适当混合覆盖它。
.prism__face {
margin: -.5*$height (-.5*$base);
width: $base; height: $height;
backface-visibility: hidden;
background: nth($lat-c, 1);
}
我们还为面设置了 backface-visibility: hidden
,以便我们只有从正面观察它们时才能看到它们,而从背面观察它们时,它们对我们来说是不可见的。
查看 Pen backface-visibility
的作用 由 Ana Tudor (@thebabydino) 在 CodePen 上创建。
当我们想要检查它们是否朝向正确方向时,这非常有用。它还可以防止 Firefox 中出现错误的 3D 排序问题和闪烁。
接下来,我们处理底面的特殊情况。如果我们将侧面视为前四个面(即面 1
、2
、3
和 4
),那么底面将是面 5
和 6
,因此所有索引大于或等于 5
的面。我们借助 nth-child
伪类来表达这一点。
.prism__face:nth-child(n + 5) {
margin-top: -.5*$base;
height: $base;
background: nth($base-c, 1);
}
您可以在下面的 Pen 中看到我们目前已经完成的工作。还没有很多,但这是一个开始!
查看 CodePen 上 Ana Tudor(@thebabydino)的 Möbius 6hedrons – step 1。
创建条形
下一步是将面进行定位,使它们在 3D 空间中实际形成一条条形。为此,我们需要了解旋转和平移的工作原理。
绕某个轴正向旋转元素一个角度值,意味着从我们旋转轴的 +
号端看,该元素是顺时针旋转。绕 z
轴正向旋转,意味着从该轴的 +
号端看,该元素是顺时针旋转——就像我们平时眼睛在屏幕前一样。
查看 CodePen 上 Ana Tudor(@thebabydino)的 rotation around the z axis。
绕 y
轴正向旋转,意味着从该轴的 +
号端看,该元素是顺时针旋转,该 +
号端位于底部。在这种情况下,我们看到元素的左侧部分向前移动,而右侧部分向屏幕后面移动。例如,如果我们绕 y
轴将元素旋转 90°
,则其正面朝向右侧,而绕同一轴旋转 -90°
则使其正面朝向左侧。
查看 CodePen 上 Ana Tudor(@thebabydino)的 rotation around the y axis。
绕 x
轴正向旋转,意味着从该轴的 +
号端看,该元素是顺时针旋转——在这种情况下,位于屏幕右侧。因此,我们看到元素的底部部分向上向前移动,而顶部部分向下向后移动。例如,绕 x
轴旋转 90°
后,元素正面朝上,而绕同一轴旋转 -90°
则使其正面朝下。
查看 CodePen 上 Ana Tudor(@thebabydino)的 rotation around the x axis。
我们需要记住的一件非常重要的事情是,我们对元素应用的任何变换都会应用于其局部坐标系。
例如,如果我们绕元素的 y
轴将元素旋转 90°
,那么旋转后该元素不仅面向右侧,而且其 z
轴——在旋转之前指向我们的屏幕——现在也指向右侧。如果我们绕 x
轴将元素旋转 90°
,那么旋转后该元素不仅正面朝上,而且其 z
轴也指向向上。如果我们将它旋转 -90°
,那么元素将正面朝下,其 z
轴也将指向向下。
因此,我们需要记住,无论我们如何变换元素,z
轴始终指向元素的正面,y
轴始终指向元素的底部,而 x
轴始终指向元素的右侧。
查看 CodePen 上 Ana Tudor(@thebabydino)的 rotating an element also rotates its system of coordinates v2。
上面讲的是旋转,现在让我们看看平移。沿着某个轴正向平移一个值,表示将该面移向该轴的 +
号端。
例如,沿着 z
轴(指向我们)正向平移一个值,表示将元素向前移动,更靠近我们,而沿着同一轴反向平移一个值,表示将元素向后移动,远离我们。
查看 CodePen 上 Ana Tudor(@thebabydino)的 translating an element。
就像旋转一样,平移也会影响元素的局部坐标系——您可以在上面的演示中看到它随着元素一起移动。
任何变换都会影响元素的坐标系,这意味着它也会影响我们随后对该元素应用的任何变换的效果。
例如,如果我们在没有应用任何其他变换的情况下沿着 z
轴正向平移元素,这将使元素向前移动。但是,如果我们绕 y
轴将元素旋转 90°
,然后沿着 z
轴正向平移它,那么这种平移会使元素向屏幕的右侧移动(从我们在屏幕前的视角看),因为在旋转后,z
轴现在指向右侧。同样,如果我们首先绕 x
轴将元素旋转 90°
,然后沿着 z
轴正向平移它,那么这种平移会使元素向上移动,因为在旋转后,z
轴指向向上。如果我们绕 x
轴将它旋转 -90°
,然后沿着 z
轴正向平移它,那么这种平移会使元素向下移动,因为在旋转后,z
轴指向向下。
查看 CodePen 上 Ana Tudor(@thebabydino)的 transforms。
现在让我们看看面最初位于哪里,以及我们要将它们移动到哪里,以便它们实际形成一条条形。

由于我们从在屏幕平面(xy
)中绝对定位所有内容开始,并且正对着屏幕中间,因此面的初始 50% 50%
点(在移动它们以形成条形之前)与条形中间的点重合(上图中第一个面板)。一半的条形位于 xy
平面的后面(蓝色,第二个面板),另一半的条形位于前面。一半的条形位于 yz
平面的左侧(绿色,第三个面板),另一半的条形位于右侧。一半的条形位于 zx
平面的上方(红色,第四个面板),另一半的条形位于下方。从该点到前后面的面的距离为基底的一半。到左右两侧面的距离也是基底的一半,而到顶部和底部的面的距离为高度的一半。
我们已经将前四个面作为侧面的面,并将最后两个面作为底面的面。因此,我们将第一个面定位在条形的前面,然后按顺序进行定位——第二个位于右侧,第三个位于后面,第四个位于左侧。然后,我们将第五个面定位在顶部,并将第六个面定位在底部。

为了将第一个面定位在前面,我们所要做的就是沿着 z
轴向前平移基底的一半。这意味着沿着 z
轴平移 .5*$base
。
.prism__face:nth-child(1) {
transform: translateZ(.5*$base);
}
为了将第二个面定位在右侧,我们首先需要绕 y
轴旋转 90°
(一个直角),使它的 z
轴指向右侧。然后,我们沿着旋转后的 z
轴的正方向(屏幕右侧)平移基底的一半。
.prism__face:nth-child(2) {
transform: rotateY(90deg) translateZ(.5*$base);
}
为了将第三个面定位在后面,我们绕 y
轴旋转 180°
(比前面的面多 90°
),使它的 z
轴现在指向后面。然后,我们沿着 z
轴新的正方向(远离我们)平移基底的一半。
.prism__face:nth-child(3) {
transform: rotateY(180deg) translateZ(.5*$base);
}
您可能想知道为什么我们不是简单地将该面向后平移基底的一半——即 translateZ(-.5*$base)
。好吧,我们可以这样做,但这样会有很多问题
- 代码将不遵循任何模式
- 面的正面将在条形的内部
- 面将从条形的外部不可见,因为它的背面在外部,因为我们在上面设置了
backface-visibility: hidden
。
为了将第四个面定位在左侧,我们绕 y
轴旋转 270°
(比前面的面多 90°
),使它的 z
轴现在指向左侧。然后,我们向左(沿着 z
轴新的正方向)平移基底的一半。
.prism__face:nth-child(4) {
transform: rotateY(270deg) translateZ(.5*$base);
}
为了将第五个面定位在顶部,我们绕 x
轴旋转 90°
,使它的 z
轴指向向上,然后沿着 z
轴新的正方向(向上)平移高度的一半。
.prism__face:nth-child(5) {
transform: rotateX(90deg) translateZ(.5*$height);
}
为了将第六个面定位在底部,我们首先绕它的 x
轴旋转 -90°
,使它的 z
轴指向向下,然后沿着旋转后的 z
轴的正方向(向下)平移高度的一半。
.prism__face:nth-child(6) {
transform: rotateX(-90deg) translateZ(.5*$height);
}
下面的演示说明了它是如何工作的。
查看 CodePen 上 Ana Tudor(@thebabydino)的 position faces v#2。
好了,现在我们有了棱柱。但是,面的代码看起来不好;它过于重复。而且,如果我们稍微更改一下第一个面的代码,我们可以看到前四个面的模式。
.prism__face:nth-child(1) { /* 1 = 0 + 1 */
transform: rotateY( 0deg) translateZ(.5*$base); /* 0deg = 0*90deg */
}
.prism__face:nth-child(2) { /* 2 = 1 + 1 */
transform: rotateY( 90deg) translateZ(.5*$base); /* 90deg = 1*90deg */
}
.prism__face:nth-child(3) { /* 3 = 2 + 1 */
transform: rotateY(180deg) translateZ(.5*$base); /* 180deg = 2*90deg */
}
.prism__face:nth-child(4) { /* 4 = 3 + 1 */
transform: rotateY(270deg) translateZ(.5*$base); /* 270deg = 3*90deg */
}
一般来说,我们可以将这些第一个面(侧面的面)的代码写成
.prism__face:nth-child(#{$i + 1}) {
transform: rotateY($i*90deg) translateZ(.5*$base);
}
… 其中 $i
为 0
、1
、2
或 3
。
但是最后两个面呢?好吧,我们也可以注意到这些面的模式。
.prism__face:nth-child(5) { /* 5 = 4 + 1 */
transform:
rotateX( 90deg) translateZ(.5*$height); /* 90deg = 1*90deg = pow(-1, 4)*90deg */
}
.prism__face:nth-child(6) { /* 6 = 5 + 1 */
transform:
rotateX(-90deg) translateZ(.5*$height); /* -90deg = -1*90deg = pow(-1, 5)*90deg */
}
一般来说,我们可以将最后两个面(底面的面)的代码写成
.prism__face:nth-child(#{$i + 1}) {
transform: rotateX(pow(-1, $i)*90deg) translateZ(.5*$height);
}
… 其中 $i
为 4
或 5
。
现在,我们可以使用 Sass 的 if()
函数将这两个变体(侧面的面和底面的面)组合在一起。
.prism__face:nth-child(#{$i + 1}) {
transform:
if($i < 4, rotateY($i*90deg), rotateX(pow(-1, $i)*90deg)
translateZ(.5*if($i < 4, $base, $height));
}
或者,如果我们不想用同一个条件写两个三元运算符
$j: if($i < 4, 1, 0); // $j is 1 if $i is less than 4 and 0 otherwise
$k: 1 - $j; // $k is 0 if $j is 1 and 1 otherwise
.prism__face:nth-child(#{$i + 1}) {
transform:
rotate3d($k, $j, 0, ($j*$i + $k*pow(-1, $i))*90deg)
translateZ(.5*($j*$base + $k*$height));
}
现在,我们所要做的就是将这两个版本(通用面)中的一个放入一个循环中,该循环将为所有六个面生成代码。
.prism__face {
@for $i from 0 to 6 {
&:nth-child(#{$i + 1}) {
transform:
if($i < 4, rotateY($i*90deg), rotateX(pow(-1, $i)*90deg)
translateZ(.5*if($i < 4, $base, $height));
}
}
}
好了,我们已经完成了创建条形!我们可以在以下 Pen 中看到结果。
查看 CodePen 上 Ana Tudor(@thebabydino)的 Möbius 6hedrons – step2。
嗯,这看起来和我们之前的东西没什么不同。这是因为我们的视角。在这里,如果我们要把它连接到屏幕的中间,这条线将垂直于正面,也就是我们看到的最大的那个(所以它遮挡了所有其他的面),而在上面演示中解释分布的演示中,视角在上面一点,在右边一点,这就是为什么我们也能看到顶部和右边的面。然而,我们在这里**是** 3D。如果我们旋转条形(可以通过在上面的演示中拖动来实现),或者如果我们改变视角,这一点就会变得很明显。
使用 CSS 改变视角是通过perspective-origin
属性来完成的。就像perspective
属性一样,它是在场景元素(在本例中是)上设置的。它的默认值为
50% 50%
(场景元素的正中间)。在下面的演示中上下拖动会改变perspective-origin
的第二个值(y
)值,因此,我们的视角,所以顶部或底部的值变得可见。
查看笔Möbius 6hedrons – step2b (perspective-origin)
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
分布条形
这实际上非常类似于在条形上分布侧边。我们绕着y
轴旋转条形定位器,然后沿着z
轴在正方向上平移它们。只是现在我们不采用90°
步长。相反,我们的步长是$base-angle
(我们之前计算过它有一个很好的圆形值,为15°
),我们的z
平移距离是我们分布条形所在的圆的半径。所以代码是
.positioner {
@for $i from 0 to $n-prisms {
&:nth-child(#{$i + 1}) {
transform: rotateY($i*$base-angle) translateZ($radius);
}
}
}
这个演示说明了条形定位是如何工作的
查看笔position prisms
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
这个笔展示了我们用目前为止编写的代码所处的位置 - 它开始看起来像样了!
查看笔Möbius 6hedrons – step 3
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
然而,这里存在几个问题。首先,条形在原始 GIF 中不是垂直的。为了让它们处于正确的位置,我们需要围绕x
轴旋转定位器 - 假设旋转70°
。我们的通用变换链变成了
transform: rotateY($i*$base-angle) translateZ($radius) rotateX(70deg);
我们可以看到现在看起来更好了
查看笔Möbius 6hedrons – step 3b
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
但在原始 GIF 中,我们从上面稍微看到了条形。我们有两个选择可以实现这种效果。
第一个选择是简单地旋转整个组件,使得它的前半部分向下一点,后半部分向上一点。这意味着围绕x
轴以负角度旋转 - 假设为-30°
.assembly { transform: rotateX(-30deg); }
我们的第二个选择是在 上添加一个
perspective-origin
。从上面稍微看到组件意味着减少perspective-origin
值的y
分量 - 我们需要让它低于50%
。然而,%
值或任何其他从顶部测量的值都不太好,因为当我们使用它们时,我们看到条形的方式取决于场景的高度。为了更好地说明这一点,请看下面的图

这三种情况除了场景的高度之外,所有内容都相同(你可以在这个笔
中查看)。但是,由于perspective-origin
的y
分量是从顶部测量的,每个立方体都恰好位于其场景的正中间,因此我们的视角对于不同的场景高度来说是不同的。因此,我们看到立方体的方式取决于它所在的场景的高度。
在我们的例子中,场景高度不是固定的,而是视窗的高度。如果我们调整视窗大小,那么我们看到条形的方式就会根据场景顶部相对的perspective-origin
值而有所不同。
我在这种情况下经常使用的解决方案是在calc()
函数中从初始的50%
值中减去一个固定的px
或em
数 - 类似于这样
perspective-origin: 50% calc(50% - 32em);
下面的图片说明了使用这种解决方案后事物看起来的样子(你可以在这个笔
中查看)。

请注意,如果上面的例子中的立方体或我们正在进行的演示中的组件没有从顶部定位在50%
处,而是定位在30vmax
处,那么我们将拥有类似calc(30vmax - 32em)
的东西。这里关键的要点是我们需要设置一个相对于我们希望以相同方式看到的对象的中心点的perspective-origin
,而不管场景的尺寸如何。
现在让我们再看一下原始 GIF 的第一帧

我们现在将尝试调整这两个选项中的每一个,看看哪个选项可以更接近上面的图片。
是组件旋转方法吗?
查看笔Möbius 6hedrons – step 3c
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
还是改变视角的方法?不幸的是,这个演示在 Firefox 中无法正常工作。
查看笔Möbius 6hedrons – step 3d
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
看起来第一个选项 - 旋转整个组件 - 设法更接近,所以我们将选择它。
查看笔Möbius 6hedrons – step 3e
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
请注意,在两种情况下都可以调整更多内容,并且可能存在条形尺寸、场景上的perspective
和perspective-origin
的某种组合比简单地旋转组件更接近原始图像。我在现场编码时的主要目标是快速,所以我选择了当时看起来更好的那个,以及需要对以前设置的值进行最少更改的那个。
对条形进行阴影处理
还有一些其他看起来不对的地方。
首先,左右面应该比正面和背面更暗。这可以通过适当的nth-child
选择器来轻松修复,使用brightness()
过滤器降低所选面的亮度
.prism__face:nth-child(-n+4):nth-child(even) {
filter: brightness(.7); /* value < 1 decreases brightness */
}
其次,圆周周围的条形应该有不同的阴影,侧边从前面的紫色逐渐过渡到后面的某种橙色。因此,围绕圆周从0°
到180°
,条形的侧边从紫色到橙色,从180°
到360°
,它们从橙色又回到紫色。

这听起来像是 Sass mix()
函数的工作!在我们的例子中,权重将从100%
(100%
第一个阴影,紫色,其余部分直到100%
,所以100% - 100% = 0%
另一个阴影,橙色)到0%
(0%
紫色,100%
橙色)在[0°, 180°]
区间内,然后在[180°, 360°]
区间内从0%
增长到100%
。嗯,这就是余弦函数!有点像……
我们可以看到下面是余弦函数在[0°, 360°]
区间内的图形。当角度从0°
变化到180°
时,余弦值从1
变化到-1
。对于从180°
变化到360°
的角度,余弦值从-1
变化到1
。
查看笔cos(θ) graph
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
如果我们加上1
,整个图形向上移动一个单位,最大值为2
,最小值为0
。
查看笔1 + cos(θ) graph
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
接下来,如果我们将其全部乘以50%
,图形在[0°, 180°]
区间内从100%
变化到0%
,然后在[180°, 360°]
区间内再次回到100%
,这正是我们想要的。
查看笔(1 + cos(θ))*50% graph
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
所以条形$i
的混合函数中的权重是(1 + cos($i*$base-angle))*50%
。我们创建一个简单的 mixin 来简化操作
@mixin mix-me($c, $k) {
background: mix(nth($c, 1), nth($c, 2), $k);
}
然后我们在定位器循环中使用它
.positioner {
@for $i from 0 to $n-prisms {
$curr-angle: $i*$base-angle; // save this so we don't compute it twice
$k: (1 + cos($curr-angle))*50%;
&:nth-child(#{$i + 1}) {
transform: rotateY($curr-angle) translateZ($radius) rotateX(70deg);
.prism__face {
@include mix-me($lat-c, $k);
&:nth-child(n + 5) { @include mix-me($base-c, $k); }
}
}
}
}
我们在本节中所做更改的结果可以在这个笔中看到
查看笔Möbius 6hedrons – step 4 (shading)
,由 Ana Tudor (@thebabydino
) 在CodePen
上创建。
动画条形
这可能是最简单的一部分。每个条形都围绕着x
轴逆时针旋转半圈。然后,它保持静止一段时间,之后整个过程重复。需要稍微调整一下才能得到一个感觉上正确的百分比 - 我们这里使用75%
。对于定时函数,我们可以使用简单的ease-in-out
,也可以尝试来自easings.net的对称函数。我首选的对称缓动函数是easeInOutCubic
,因为它在我看来,在大多数情况下,它可以让动画感觉最接近自然运动。
@keyframes rot {
75%, 100% { transform: rotateX(-.5turn); }
}
.prism {
animation: rot $t ease-in-out infinite;
}
可以在以下 Pen 中看到此动画
查看 Pen Möbius 6hedrons – step 5 (animation) by Ana Tudor (@thebabydino) on CodePen.
然而,这里有一个问题:所有条形都同时旋转。这意味着我们需要为每个条形设置不同的animation-delay
。我们使用负延迟,以便所有动画在0
时刻就已经开始。
.positioner {
@for $i from 0 to $n-prisms {
&:nth-child(#{$i + 1}) {
.prism { animation-delay: -$i*$t/$n-prisms; }
}
}
}
这就得到了最终的结果!
查看 Pen Möbius 6hedrons (纯 CSS) 由 Ana Tudor (@thebabydino) 在 CodePen 上创建。
最后的话
现在你知道我如何在 30 分钟内编写出看起来极其复杂的代码了。你可能会更快,因为我从来没学会过用超过一根手指打字!
非常棒的深入帖子。虽然我不认为打字速度决定了你写代码的速度!
看起来这个练习作为视频录制会很棒,就像教授模式一样。它本质上提供了对 CSS 可以提供的各种 3D 可能性的实用解释。
只有我一个人现在感觉像个新手吗?
不,你并不孤单。:p
这实际上是非常复杂的(至少对于普通人来说)! XO
我不知怎么地勉强理解了各个步骤,但仅此而已。
我想这就是理解数学和知道数学之间的区别 - 那样使用余弦函数就是一个证明。我知道我就算用一千万年也无法达到这种水平。
但现在我感觉我更清楚地理解了 CSS 中的 3D 工作原理:例如,我一直以为
translateZ()
函数只能将物体向前或向后移动 - 这篇文章让我明白它可以与rotateX/Y
一起使用来将物体移动到其他方向。这也让透视属性和旋转场景的不同方法变得更加清晰。
一如既往,向 Ana 致以无限的赞赏。感谢您的百万个赞赏。
是的。我也是。
哈哈,只要学两年电子学课程,你甚至不需要太用心,它最终会通过重复而刻印在你的脑海中。不是开玩笑。很多人抱怨像 DSP 这样的课程在数学解题方面太难了,但当你反复学习同样的东西时,它就记住了……
哈哈,不,你并不孤单。“出奇地简单”我的屁股。smh。
绝对不孤单。
读完这篇文章后,我甚至不能自称是 web 开发人员,干得漂亮,文章很棒!
非常感谢这篇文章。它确实详细地解释了所有内容。现在我只需要去学习更多关于 SASS 函数的知识。
很荣幸我的旧 GIF 能够出现在 CSS tricks 上!这是一篇非常有趣的文章,我以前不知道 CSS 可以实现这种复杂性。感谢您的发布。
太棒了 >_<
太棒了!分享了!
感谢分享。这很有启发性!
这简直是史诗级的,也是非常好的详细教程。
继续努力。
太棒了!我希望我能更擅长数学、SASS、CSS,以及……
继续努力!
非常棒的帖子!感谢您花时间如此详细地解释所有内容(我相信这花的时间超过 30 分钟:-) 您真正展现了三项了不起的技能:您对 CSS 的实现、您对数学和 3D 坐标系的理解,以及您清楚地解释复杂概念的能力。
这真的很棒!当您第一次提到 30 分钟时,我持怀疑态度,但当您分解它之后,我就能明白为什么它能在 30 分钟内完成。
你看,使用 HTML 和 CSS 进行 3D 操作最有趣的部分在于 DOM 基本上变成了你的场景图,而 CSS 成了你用来变换该场景图的方式,这与你在其他任何 3D 框架中处理场景图的方式类似。当然,这是一个有点有限的场景图,其中每个多边形都必须由一个节点(一个 HTML 元素)表示,但它毕竟是一个场景图。(嗯,我想你可以使用带有 :before 和 :after 的单个元素来同时表示 3 个面。)
然后你会获得所有很酷的、有用的场景图优势,比如父子坐标系。需要一堆移动的立方体?只需在局部坐标系中构建一个立方体(6 个元素,或者 2 个带有 :before 和 :after),将它复制到一堆父元素中,然后移动父元素来移动立方体。需要旋转整个移动立方体组件?将所有父元素放入另一个祖先元素中,然后旋转它!
有趣的是,如果你找来一个 3D 游戏开发者和一个前端 web 开发者,他们可能根本看不到任何技术上的重叠。然而,实际上有很多重叠:用于游戏内脚本、UI 编码的 JavaScript,甚至是 3D 场景图的概念。
现在我们需要的是有人使用 JavaScript 3D 引擎(例如 cannon.js)构建一个简单的基于 web 的 3D 物理游戏,而不使用 WebGL!我找到一个简单的示例:http://davetayls.me/blog/2013/06/04/3d-css-and-physics-with-cannonjs/
总之,我跑题了。演示真的很酷!
three.js 也是一个可以处理使用变换属性的 3D 引擎!
查看这个惊人的演示:http://mrdoob.github.io/three.js/examples/css3d_periodictable.html
非常棒的教程,一如既往 :)
由于我比较懒,如果我需要做 gif 动画,我会用 Blender 的“动画节点插件”https://www.youtube.com/watch?v=r-c7B0cBqEE&list=LLHvd290AgsBrWEUu_mYzM9g&index=19
很棒的演示。我不知道我对最终产品更印象深刻,还是对您解释所有内容的清晰度更印象深刻。
我喜欢您如何简化/抽象重复的声明。我一直试图做得更好,看到其他开发者的例子很棒。
哇!太棒了!
真的,非常棒。立即在 Codepen 上关注你。
太棒了!一篇很棒的文章,知识量很大。我喜欢你为每个概念提供了简化的工作示例。远超我的技能水平,但仍然值得欣赏。
你是大师!
精彩的文章,让人很谦虚。
我花了超过30分钟才读完它!
我认为侧面位置的算法化部分缺少一个右大括号。
另外,我在颜色阴影部分有点迷路了。
我仍在尝试解码和理解这篇很棒的文章。
哎呀,是真的!感谢,现在已经修复了。
哇,太惊艳了。感谢详细的解释。
太令人印象深刻了……莫比乌斯活了……太令人印象深刻了
8-O
干得好!我喜欢!
Cheera,太棒了!让我很想更深入地了解如何以3D方式显示DOM元素。
这真的很棒,很快我们将拥有语义正确的、与RDF兼容的页面,它们可以在台式机上以3D方式显示,在移动设备上以2D方式显示。反之亦然。这是一个多么美好的时代;)
Ana,真是太棒了!
这就是我热爱数学的原因!
哇!太棒了!