我使用 CSS 变换已有五年多的时间了,一直困扰我的一件事是无法单独为 transform
链中的组件设置动画。 本文将解释问题、旧的解决方法、新的神奇的 Houdini 解决方案,最后,将通过比用于说明概念的示例更美观的示例为您提供视觉盛宴。
问题
为了更好地理解手头的问题,让我们考虑一个我们将盒子水平移动到屏幕上的示例。 就 HTML 而言,这意味着一个 div
<div class="box"></div>
CSS 也非常简单。 我们为这个盒子设置尺寸、背景
并使用 margin
将其水平居中。
$d: 4em;
.box {
margin: .25*$d auto;
width: $d; height: $d;
background: #f90;
}
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
接下来,借助沿 x 轴的平移,我们将其向左移动半个视口 (50vw
)(在 x 轴的负方向,正方向朝右)。
transform: translate(-50vw);
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
现在,盒子的左半部分位于屏幕之外。 将平移的绝对量减少到其边缘长度的一半会使其完全位于视口中,而将其减少到更多,例如一个完整的边缘长度(即 $d
或 100%
——请记住,translate()
函数中的 %
值相对于 正在平移的元素的尺寸),则会使其不再接触视口的左边缘。
transform: translate(calc(-1*(50vw - 100%)));
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
这将是我们的初始动画位置。
然后,我们创建一组 @keyframes
将盒子移动到相对于初始位置的对称位置,并且没有平移,并在设置 animation
时引用它们。
$t: 1.5s;
.box {
/* same styles as before */
animation: move $t ease-in-out infinite alternate;
}
@keyframes move {
to { transform: translate(calc(50vw - 100%)); }
}
所有这些都按预期工作,为我们提供了一个从左到右再返回的盒子。
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
但这是一个相当无聊的动画,所以让我们使它更有趣。 假设我们希望盒子在中间时缩放到 .1
的因子,并在两端具有其正常大小。 我们可以添加另一个关键帧。
50% { transform: scale(.1); }
盒子现在也缩放了 (演示),但是,由于我们添加了一个额外的关键帧,因此时间函数不再应用于整个动画——仅应用于关键帧之间的那部分。 这使得我们的平移在中间 (50%
) 较慢,因为我们现在在那里也有一个关键帧。 因此,我们需要调整时间函数,包括在 animation
值和 @keyframes
中。 在我们的例子中,因为我们希望整体具有 ease-in-out
,所以我们可以将其分成一个 ease-in
和一个 ease-out
。
.box {
animation: move $t ease-in infinite alternate;
}
@keyframes move {
50% {
transform: scale(.1);
animation-timing-function: ease-out;
}
to { transform: translate(calc(50vw - 100%)); }
}
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
现在一切正常,但是如果我们想要为平移和缩放设置不同的时间函数呢? 我们设置的时间函数意味着 animation
在开始时较慢,在中间较快,然后在结束时再次变慢。 如果我们希望这仅适用于平移,而不适用于缩放怎么办? 如果我们希望缩放在开始时快速发生,当它从 1
变为 .1
时,在中间时缓慢,当它在 .1
附近时,然后在结束时再次快速发生,当它回到 1
时怎么办?
好吧,不可能为同一链中的不同变换函数设置不同的时间函数。 我们无法使平移在开始时变慢,缩放在中间变快,反之亦然。 至少,在我们动画化的是 transform
属性并且它们是同一 transform
链的一部分时,这是不可能的。
旧的解决方法
当然,有一些方法可以解决这个问题。 传统上,解决方案是将 transform
(以及 animation
)拆分为多个元素。 这给了我们以下结构。
<div class="wrap">
<div class="box"></div>
</div>
我们在包装器上移动 width
属性。 由于 div
元素默认情况下是块级元素,因此这也会确定其 .box
子元素的 width
,而无需我们显式设置它。 但是,我们保留 .box
上的 height
,因为子元素 (本例中的 .box
) 的 height
也决定了其父元素 (本例中的包装器) 的 height
。
我们还向上移动 margin
、transform
和 animation
属性。 除了这个之外,我们切换回此 animation
的 ease-in-out
时间函数。 我们还将 move
的 @keyframes
集修改为其初始状态,以便去除 scale()
。
.wrap {
margin: .25*$d calc(50% - #{.5*$d});
width: $d;
transform: translate(calc(-1*(50vw - 100%)));
animation: move $t ease-in-out infinite alternate;
}
@keyframes move {
to { transform: translate(calc(50vw - 100%)); }
}
我们创建了另一组 @keyframes
,用于实际的 .box
元素。 这是一个交替的 animation
,其持续时间是产生振荡运动的那个的一半。
.box {
height: $d;
background: #f90;
animation: size .5*$t ease-out infinite alternate;
}
@keyframes size { to { transform: scale(.1); } }
现在我们得到了我们想要的结果。
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
这是一个可靠的解决方法,不会添加太多额外的代码,更不用说在这种情况中,我们实际上不需要两个元素,我们可以只使用一个元素及其伪元素之一。 但是,如果我们的变换链变长,我们别无选择,只能添加额外的元素。 而且,在 2018 年,我们可以做得更好!
Houdini 解决方案
你们中的一些人可能已经知道 CSS 变量 无法设置动画(我想任何不知道的人刚刚知道了)。 如果我们尝试在 animation
中使用它们,当中间时间的一半过去时,它们只会从一个值翻转到另一个值。
考虑振荡盒子的初始示例(不涉及缩放)。 假设我们尝试使用自定义属性 --x
为其设置动画。
.box {
/* same styles as before */
transform: translate(var(--x, calc(-1*(50vw - #{$d}))));
animation: move $t ease-in-out infinite alternate
}
@keyframes move { to { --x: calc(50vw - #{$d}) } }
遗憾的是,这只会导致在 50%
处翻转,官方原因是浏览器无法知道自定义属性的类型(这对我来说没有意义,但我猜这并不重要)。
查看 thebabydino 在 CodePen 上的 Pen (@thebabydino)。
但是我们可以忘记所有这些,因为现在 Houdini 已经登场,我们可以注册此类自定义属性,以便我们显式地为其指定类型 (syntax
)。
有关此内容的更多信息,请查看 演讲 和 幻灯片,作者为 Serg Hospodarets。
CSS.registerProperty({
name: '--x',
syntax: '<length>',
initialValue: 0,
inherits: false
});
在规范的早期版本中,inherits
是可选的,但后来它变成了强制性的,因此,如果您发现旧的 Houdini 演示不再起作用,很可能是因为它没有显式设置 inherits
。
我们将 initialValue
设置为 0
,因为我们必须将其设置为某个值,并且该值必须是 计算独立 的值——也就是说,它不能依赖于我们在 CSS 中设置或更改的任何内容,并且,鉴于初始和最终平移值取决于我们在 CSS 中设置的盒子尺寸,因此 calc(-1*(50vw - 100%))
在这里无效。 将 --x
设置为 calc(-1*(50vw - 100%))
甚至不起作用,我们需要改为使用 calc(-1*(50vw - #{$d}))
。
$d: 4em;
$t: 1.5s;
.box {
margin: .25*$d auto;
width: $d; height: $d;
--x: calc(-1*(50vw - #{$d}));
transform: translate(var(--x));
background: #f90;
animation: move $t ease-in-out infinite alternate;
}
@keyframes move { to { --x: calc(50vw - #{$d}); } }

目前,这仅在 实验性 Web 平台功能 标志后面的 Blink 浏览器中有效。 这可以通过 chrome://flags
(或者,如果您使用的是 Opera,则为 opera://flags
)启用。

在所有其他浏览器中,我们仍然看到在 50%
处翻转。
将其应用于我们的振荡和缩放演示意味着我们引入了两个我们注册和设置动画的自定义属性——一个是沿 x 轴的平移量 (--x
),另一个是统一缩放因子 (--f
)。
CSS.registerProperty({ /* same as before */ });
CSS.registerProperty({
name: '--f',
syntax: '<number>',
initialValue: 1,
inherits: false
});
相关的 CSS 如下所示
.box {
--x: calc(-1*(50vw - #{$d}));
transform: translate(var(--x)) scale(var(--f));
animation: move $t ease-in-out infinite alternate,
size .5*$t ease-out infinite alternate;
}
@keyframes move { to { --x: calc(50vw - #{$d}); } }
@keyframes size { to { --f: .1 } }

更好看的样式
不过,一个简单的振荡和缩放的正方形并不是最令人兴奋的事情,所以让我们看看更漂亮的演示!

3D 版本
从 2D 到 3D,正方形变成了立方体,并且,由于只有一个立方体不够有趣,所以让我们用一整个网格来表示它们!
我们将 body
视为我们的场景。在这个场景中,我们有一个立方体的 3D 组合(.a3d
)。这些立方体分布在一个具有 nr
行和 nc
列的网格上。
- var nr = 13, nc = 13;
- var n = nr*nc;
.a3d
while n--
.cube
- var n6hedron= 6; // cube always has 6 faces
while n6hedron--
.cube__face
我们首先进行一些基本的样式,以创建一个具有透视效果的场景,将整个组合置于中间,并将每个立方体的面放置到其位置。我们不会详细介绍如何构建 CSS 立方体,因为我已经为此主题撰写了一篇非常详细的文章,所以如果您需要回顾,可以查看那篇文章!
目前的结果可以在下面看到——所有立方体堆叠在场景的中间

对于所有这些立方体,它们的前半部分位于屏幕平面的前面,后半部分位于屏幕平面的后面。在屏幕平面上,我们有一个立方体截面的正方形。这个正方形与表示立方体面的正方形相同。
查看 thebabydino 在 CodePen 上创作的 Pen(@thebabydino)。
接下来,我们在立方体组上设置列(--i
)和行(--j
)索引。最初,我们为所有立方体将这两个索引都设置为 0
。
.cube {
--i: 0;
--j: 0;
}
由于每一行都有数量等于列数 (nc
) 的立方体,因此我们为前 nc
个立方体之后的全部立方体将行索引设置为 1
。然后,对于前 2*nc
个立方体之后的全部立方体,我们将行索引设置为 2
。依此类推,直到我们覆盖所有 nr
行。
style
| .cube:nth-child(n + #{1*nc + 1}) { --j: 1 }
| .cube:nth-child(n + #{2*nc + 1}) { --j: 2 }
//- and so on
| .cube:nth-child(n + #{(nr - 1)*nc + 1}) { --j: #{nr - 1} }
我们可以在循环中压缩它
style
- for(var i = 1; i < nr; i++) {
| .cube:nth-child(n + #{i*nc + 1}) { --j: #{i} }
-}
之后,我们继续设置列索引。对于列,在我们遇到另一个具有相同索引的立方体之前,我们始终需要跳过数量等于 nc - 1
的立方体。因此,对于每个立方体,它之后的第 nc
个立方体将具有相同的索引,我们将有 nc
个这样的立方体组。
(我们只需要将索引设置为最后 nc - 1
个,因为所有立方体的列索引最初都设置为 0
,因此我们可以跳过包含列索引为 0
的立方体的第一个组——无需再次将 --i
设置为它已有的相同值。)
style
| .cube:nth-child(#{nc}n + 2) { --i: 1 }
| .cube:nth-child(#{nc}n + 3) { --i: 2 }
//- and so on
| .cube:nth-child(#{nc}n + #{nc}) { --i: #{nc - 1} }
这也可以在循环中压缩
style
- for(var i = 1; i < nc; i++) {
| .cube:nth-child(#{nc}n + #{i + 1}) { --i: #{i} }
-}
现在我们已经设置了所有行和列索引,我们可以使用 2D translate()
变换将这些立方体分布在屏幕平面的 2D 网格上,如下面的插图所示,其中每个立方体由其在屏幕平面上的正方形截面表示,并且距离是在 transform-origin
点之间测量的(默认情况下,它们位于 50% 50% 0
,因此正好位于屏幕平面上的正方形立方体截面的中间)
/* $l is the cube edge length */
.cube {
/* same as before */
--x: calc(var(--i)*#{$l});
--y: calc(var(--j)*#{$l});
transform: translate(var(--x), var(--y));
}
这给了我们一个网格,但它不在屏幕的中间。

现在,是左上立方体的中心点位于屏幕的中间,如上面的演示中突出显示的那样。我们想要的是网格位于中间,这意味着我们需要将所有立方体向左和向上(在 x 和 y 轴的负方向上)移动,移动距离为网格尺寸的一半(分别为 calc(.5*var(--nc)*#{$l})
和 calc(.5*var(--nr)*#{$l})
)与网格左上角和左上立方体在屏幕平面上的垂直横截面的中点之间的距离(这些距离均为立方体边长的一半,或 .5*$l
)之间的水平和垂直差值。
从先前的数量中减去这些差异,我们的代码变为
.cube {
/* same as before */
--x: calc(var(--i)*#{$l} - (.5*var(--nc)*#{$l} - .5*#{$l}));
--y: calc(var(--j)*#{$l} - (.5*var(--nr)*#{$l} - .5*#{$l}));
}
或者更好的是
.cube {
/* same as before */
--x: calc((var(--i) - .5*(var(--nc) - 1))*#{$l}));
--y: calc((var(--j) - .5*(var(--nr) - 1))*#{$l}));
}
我们还需要确保设置了 --nc
和 --nr
自定义属性
- var nr = 13, nc = 13;
- var n = nr*nc;
//- same as before
.a3d(style=`--nc: ${nc}; --nr: ${nr}`)
//- same as before
这给了我们一个位于视口中间的网格

我们还使立方体边长 $l
更小,以便网格适合视口。
或者,我们可以使用 CSS 变量 --l
,以便我们可以根据列数和行数控制边长。这里的第一步是将两者中的最大值设置为 --nmax
变量
- var nr = 13, nc = 13;
- var n = nr*nc;
//- same as before
.a3d(style=`--nc: ${nc}; --nr: ${nr}; --max: ${Math.max(nc, nr)}`)
//- same as before
然后,我们将边长 (--l
) 设置为最小视口尺寸除以该最大值 (--max
) 的大约 80%
(完全是任意值)
.cube {
/* same as before */
--l: calc(80vmin/var(--max));
}
最后,我们更新立方体和面的变换、面的尺寸和 margin
以使用 --l
而不是 $l
.cube {
/* same as before */
--l: calc(80vmin/var(--max));
--x: calc((var(--i) - .5*(var(--nc) - 1))*var(--l));
--y: calc((var(--j) - .5*(var(--nr) - 1))*var(--l));
&__face {
/* same as before */
margin: calc(-.5*var(--l));
width: var(--l); height: var(--l);
transform: rotate3d(var(--i), var(--j), 0, calc(var(--m, 1)*#{$ba4gon}))
translatez(calc(.5*var(--l)));
}
}
现在我们有了一个漂亮的响应式网格!

但它很丑,所以让我们通过使每个立方体的 color
取决于其列索引 (--i
) 来将其变成彩虹。
.cube {
/* same as before */
color: hsl(calc(var(--i)*360/var(--nc)), 65%, 65%);
}

我们还使场景背景变暗,以便我们与现在更浅的立方体边缘形成更好的对比。
为了进一步增强效果,我们添加了一个围绕 y 轴的行旋转,旋转角度取决于行索引 (--j
)
.cube {
/* same as before */
transform: rotateY(calc(var(--j)*90deg/var(--nr)))
translate(var(--x), var(--y));
}

我们还减小了立方体边长 --l
并增加了 perspective
值,以便允许此扭曲的网格适应。
现在是乐趣开始的时候了!对于每个立方体,我们将其位置沿 z 轴来回动画,动画距离为网格宽度的一半(我们将 translate()
更改为 translate3d()
并使用一个额外的自定义属性 --z
,该属性在 calc(.5*var(--nc)*var(--l))
和 calc(-.5*var(--nc)*var(--l))
之间变化)以及它的尺寸(通过一个缩放因子为 --f
的统一 scale3d()
,该因子在 1
和 .1
之间变化)。这与我们在原始示例中对正方形所做的操作基本相同,只是运动现在发生在 z 轴上,而不是 x 轴上,并且缩放发生在 3D 中,而不仅仅是在 2D 中。
$t: 1s;
.cube {
/* same as before */
--z: calc(var(--m)*.5*var(--nc)*var(--l));
transform: rotateY(calc(var(--j)*90deg/var(--nr)))
translate3d(var(--x), var(--y), var(--z))
scale3d(var(--f), var(--f), var(--f));
animation: a $t ease-in-out infinite alternate;
animation-name: move, zoom;
animation-duration: $t, .5*$t;
}
@keyframes move { to { --m: -1 } }
@keyframes zoom { to { --f: .1 } }
在我们注册乘数 --m
和缩放因子 --f
以赋予它们类型和初始值之前,这不会有任何作用
CSS.registerProperty({
name: '--m',
syntax: '<number>',
initialValue: 1,
inherits: false
});
CSS.registerProperty({
name: '--f',
syntax: '<number>',
initialValue: 1,
inherits: false
});

此时,所有立方体同时动画。为了使事情更有趣,我们添加了一个延迟,该延迟取决于列和行索引
animation-delay: calc((var(--i) + var(--j))*#{-2*$t}/(var(--nc) + var(--nr)));

最后的润色是向 3D 组合添加旋转
.a3d {
top: 50%; left: 50%;
animation: ry 8s linear infinite;
}
@keyframes ry { to { transform: rotateY(1turn); } }
我们还通过为面设置黑色背景使它们不透明,并且我们得到了最终结果

这方面的性能非常糟糕,从上面的 GIF 记录可以看出,但仍然很有趣,可以看看我们能将事情推到多远。
跳跃的正方形
我在另一篇文章的评论中偶然发现了原始代码,并且,当我看到代码时,我认为它是使用一些 Houdini 魔法进行改造的完美候选者!
让我们首先了解原始代码中发生了什么。
在 HTML 中,我们有九个 div。
<div class="frame">
<div class="center">
<div class="down">
<div class="up">
<div class="squeeze">
<div class="rotate-in">
<div class="rotate-out">
<div class="square"></div>
</div>
</div>
</div>
</div>
</div>
<div class="shadow"></div>
</div>
</div>
现在,此动画比我所能想到的任何动画都要复杂得多,但即使如此,九个元素似乎也太多了一些。因此,让我们看一下 CSS,看看它们各自用于什么,以及在准备切换到 Houdini 支持的解决方案之前,我们可以如何简化代码。
让我们从动画元素开始。.down
和 .up
元素都具有与垂直移动正方形相关的 animation
/* original */
.down {
position: relative;
animation: down $duration ease-in infinite both;
.up {
animation: up $duration ease-in-out infinite both;
/* the rest */
}
}
@keyframes down {
0% {
transform: translateY(-100px);
}
20%, 100% {
transform: translateY(0);
}
}
@keyframes up {
0%, 75% {
transform: translateY(0);
}
100% {
transform: translateY(-100px);
}
}
使用 @keyframes
和两个元素上的动画具有相同的持续时间,我们可以实现一个二合一的效果。
在第一组 @keyframes
的情况下,所有操作(从 -100px
到 0
)都发生在 [0%, 20%]
区间内,而在第二个区间内,所有操作(从 0
到 -100px
)都发生在 [75%, 100%]
区间内。这两个区间没有交集。由于这个原因以及两个动画具有相同的持续时间,我们可以将每个关键帧处的平移值加起来。
- 在
0%
时,我们从第一组@keyframes
获得-100px
,从第二个获得0
,这给了我们-100px
- 在
20%
时,我们从第一组@keyframes
得到0
,从第二组得到0
(因为从0%
到75%
的任何帧我们都有0
),最终得到0
。 - 在
75%
时,我们从第一组@keyframes
得到0
(因为从20%
到100%
的任何帧我们都有0
),从第二组得到0
,最终得到0
。 - 在
100%
时,我们从第一组@keyframes
得到0
,从第二组得到-100px
,最终得到-100px
。
我们的新代码如下。我们去掉了简写中的animation-fill-mode
,因为在这种情况下它没有任何作用,因为我们的动画无限循环,持续时间不为零且没有延迟。
/* new */
.jump {
position: relative;
transform: translateY(-100px);
animation: jump $duration ease-in infinite;
/* the rest */
}
@keyframes jump {
20%, 75% {
transform: translateY(0);
animation-timing-function: ease-in-out;
}
}
请注意,两个动画的计时函数不同,因此我们需要在@keyframes
中切换它们。我们仍然得到相同的效果,但我们去掉了其中一个元素和一组@keyframes
。
接下来,我们对.rotate-in
和.rotate-out
元素及其@keyframes
执行相同的操作。
/* original */
.rotate-in {
animation: rotate-in $duration ease-out infinite both;
.rotate-out {
animation: rotate-out $duration ease-in infinite both;
}
}
@keyframes rotate-in {
0% {
transform: rotate(-135deg);
}
20%, 100% {
transform: rotate(0deg);
}
}
@keyframes rotate-out {
0%, 80% {
transform: rotate(0);
}
100% {
transform: rotate(135deg);
}
}
与之前的情况类似,我们对每个关键帧的旋转值进行累加。
- 在
0%
时,我们从第一组@keyframes
得到-135deg
,从第二组得到0deg
,最终得到-135deg
。 - 在
20%
时,我们从第一组@keyframes
得到0deg
,从第二组得到0deg
(因为从0%
到80%
的任何帧我们都有0deg
),最终得到0deg
。 - 在
80%
时,我们从第一组@keyframes
得到0deg
(因为从20%
到100%
的任何帧我们都有0deg
),从第二组得到0deg
,最终得到0deg
。 - 在
100%
时,我们从第一组@keyframes
得到0deg
,从第二组得到135deg
,最终得到135deg
。
这意味着我们可以将代码压缩成以下形式:
/* new */
.rotate {
transform: rotate(-135deg);
animation: rotate $duration ease-out infinite;
}
@keyframes rotate {
20%, 80% {
transform: rotate(0deg);
animation-timing-function: ease-in;
}
100% { transform: rotate(135deg); }
}
我们只有一个具有缩放transform
的元素,它扭曲了我们的白色正方形。
/* original */
.squeeze {
transform-origin: 50% 100%;
animation: squeeze $duration $easing infinite both;
}
@keyframes squeeze {
0%, 4% {
transform: scale(1);
}
45% {
transform: scale(1.8, 0.4);
}
100% {
transform: scale(1);
}
}
这里在代码压缩方面我们能做的不多,除了删除animation-fill-mode
并将100%
关键帧与0%
和4%
关键帧组合在一起。
/* new */
.squeeze {
transform-origin: 50% 100%;
animation: squeeze $duration $easing infinite;
}
@keyframes squeeze {
0%, 4%, 100% { transform: scale(1); }
45% { transform: scale(1.8, .4); }
}
最里面的元素(.square
)仅用于显示白色方块,并且没有设置任何transform
。
/* original */
.square {
width: 100px;
height: 100px;
background: #fff;
}
这意味着如果我们将它的样式移动到其父元素,我们就可以去掉它。
/* new */
$d: 6.25em;
.rotate {
width: $d; height: $d;
transform: rotate(-135deg);
background: #fff;
animation: rotate $duration ease-out infinite;
}
到目前为止,我们已经去掉了三个元素,我们的结构变成了以下形式:
.frame
.center
.jump
.squeeze
.rotate
.shadow
最外面的元素(.frame
)充当场景或容器。这是一个大的蓝色正方形。
/* original */
.frame {
position: absolute;
top: 50%;
left: 50%;
width: 400px;
height: 400px;
margin-top: -200px;
margin-left: -200px;
border-radius: 2px;
box-shadow: 1px 2px 10px 0px rgba(0,0,0,0.2);
overflow: hidden;
background: #3498db;
color: #fff;
font-family: 'Open Sans', Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
此演示中没有文本,因此我们可以删除与文本相关的属性。我们还可以删除color
属性,因为不仅在此演示中任何地方都没有文本,而且我们也没有将其用于任何边框、阴影、背景(通过currentColor
)等。
我们还可以通过在body
上使用flexbox布局来避免将此容器元素移出文档流。这也消除了偏移量和margin
属性。
/* new */
$s: 4*$d;
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.frame {
overflow: hidden;
position: relative;
width: $s; height: $s;
border-radius: 2px;
box-shadow: 1px 2px 10px rgba(#000, .2);
background: #3498db;
}
我们还将此元素的尺寸与跳跃正方形的尺寸关联。
.center
元素仅用于定位其直接子元素(.jump
和.shadow
),因此我们可以完全将其移除,并在这些子元素上直接使用其偏移量。
我们在所有.frame
的后代元素上使用绝对定位。这使得.jump
和.squeeze
元素成为0x0
的盒子,因此我们调整了挤压transform
的transform-origin
(0
的100%
始终为0
,但我们想要的值是正方形边长的一半.5*$d
)。我们还在.rotate
元素上设置了等于正方形边长一半的负边距(-.5*$d
)(以补偿我们在已移除的.center
元素上设置的translate(-50%, -50%)
)。
/* new */
.frame * { position: absolute, }
.jump {
top: $top; left: $left;
/* same as before */
}
.squeeze {
transform-origin: 50% .5*$d;
/* same as before */
}
.rotate {
margin: -.5*$d;
/* same as before */
}
最后,让我们看一下.shadow
元素。
/* original */
.shadow {
position: absolute;
z-index: -1;
bottom: -2px;
left: -4px;
right: -4px;
height: 2px;
border-radius: 50%;
background: rgba(0,0,0,0.2);
box-shadow: 0 0 0px 8px rgba(0,0,0,0.2);
animation: shadow $duration ease-in-out infinite both;
}
@keyframes shadow {
0%, 100% {
transform: scaleX(.5);
}
45%, 50% {
transform: scaleX(1.8);
}
}
当然,我们去掉了position,因为我们已经为.frame
的所有后代设置了它。如果我们将.shadow
元素放在DOM中.jump
元素之前,我们还可以删除z-index
。
接下来,我们有偏移量。阴影的中点在水平方向上偏移$left
(就像.jump
元素一样),在垂直方向上偏移$top
加上正方形边长的一半(.5*$d
)。
我们看到height
设置为2px
。在另一个轴上,width
计算为正方形的边长($d
)加上来自left
的4px
和来自right
的4px
。总共加上8px
。但我们注意到,具有8px
扩展且没有模糊的box-shadow
只是background
的扩展。因此,我们只需沿两个轴将元素的尺寸增加扩展的两倍,并完全删除box-shadow
。
就像其他元素一样,我们还从animation
简写中删除了animation-fill-mode
。
/* new */
.shadow {
margin: .5*($d - $sh-h) (-.5*$sh-w);
width: $sh-w; height: $sh-h;
border-radius: 50%;
transform: scaleX(.5);
background: rgba(#000, .2);
animation: shadow $duration ease-in-out infinite;
}
@keyframes shadow {
45%, 50% { transform: scaleX(1.8); }
}
我们现在已经将原始演示中的代码减少了大约40%,同时仍然获得相同的结果。
查看thebabydino在CodePen上创建的Pen (@thebabydino)。
我们的下一步是将.jump
、.squeeze
和rotate
组件合并为一个,以便我们将三个元素合并为一个。作为提醒,此时我们相关的样式是:
.jump {
transform: translateY(-100px);
animation: jump $duration ease-in infinite;
}
.squeeze {
transform-origin: 50% .5*$d;
animation: squeeze $duration $easing infinite;
}
.rotate {
transform: rotate(-135deg);
animation: rotate $duration ease-out infinite;
}
@keyframes jump {
20%, 75% {
transform: translateY(0);
animation-timing-function: ease-in-out;
}
}
@keyframes squeeze {
0%, 4%, 100% { transform: scale(1); }
45% { transform: scale(1.8, .4); }
}
@keyframes rotate {
20%, 80% {
transform: rotate(0deg);
animation-timing-function: ease-in;
}
100% { transform: rotate(135deg); }
}
这里唯一的问题是缩放transform
的transform-origin
与默认的50% 50%
不同。幸运的是,我们可以解决这个问题。
任何具有非默认transform-origin
的transform
等效于一个具有默认transform-origin
的transform
链,该链首先平移元素,使其默认transform-origin
点(对于HTML元素,为50% 50%
点;对于SVG元素,为viewBox
的0 0
点参见SVG元素上的变换)移动到所需的transform-origin
,应用我们想要的实际变换(缩放、旋转、倾斜、这些变换的组合……无关紧要),然后应用反向平移(坐标每个轴的值乘以-1
)。
transform-origin
的transform
等效于一个链,该链将默认transform-origin
点平移到自定义点的transform-origin
,执行所需的transform
,然后反转初始平移(在线演示)。将此转换为代码意味着,如果我们有任何具有transform-origin: $x1 $y1
的transform
,则以下两者是等效的:
/* transform on HTML element with transform-origin != default */
transform-origin: $x1 $y1;
transform: var(--transform); /* can be rotation, scaling, shearing */
/* equivalent transform chain on HTML element with default transform-origin */
transform: translate(calc(#{$x1} - 50%), calc(#{$y1} - 50%))
var(--transform)
translate(calc(50% - #{$x1}), calc(50% - $y1);
在我们的特定情况下,我们在x轴上具有默认的transform-origin
值,因此我们只需要沿y轴执行平移即可。通过用变量替换硬编码的值,我们得到以下变换链:
transform: translateY(var(--y))
translateY(.5*$d) scale(var(--fx), var(--fy)) translateY(-.5*$d)
rotate(var(--az));
我们可以通过合并前两个平移来稍微压缩一下:
transform: translateY(calc(var(--y) + #{.5*$d}))
scale(var(--fx), var(--fy)) translateY(-.5*$d)
rotate(var(--az));
我们还将三个元素上的三个动画合并为一个:
animation: jump $duration ease-in infinite,
squeeze $duration $easing infinite,
rotate $duration ease-out infinite;
我们修改@keyframes
,以便现在动画化新引入的自定义属性--y
、--fx
、--fy
和--az
。
@keyframes jump {
20%, 75% {
--y: 0;
animation-timing-function: ease-in-out;
}
}
@keyframes squeeze {
0%, 4%, 100% { --fx: 1; --fy: 1 }
45% { --fx: 1.8; --fy: .4 }
}
@keyframes rotate {
20%, 80% {
--az: 0deg;
animation-timing-function: ease-in;
}
100% { --az: 135deg }
}
但是,除非我们注册我们已引入并想要动画化的这些CSS变量,否则这将不起作用。
CSS.registerProperty({
name: '--y',
syntax: '<length>',
initialValue: '-100px',
inherits: false
});
CSS.registerProperty({
name: '--fx',
syntax: '<number>',
initialValue: 1,
inherits: false
});
/* exactly the same for --fy */
CSS.registerProperty({
name: '--az',
syntax: '<angle>',
initialValue: '-135deg',
inherits: false
});
现在我们有一个使用动画化CSS变量的方法的有效演示。但是,鉴于我们的结构现在是一个包装器和两个子元素,我们可以将其进一步减少到一个元素和两个伪元素,从而获得最终版本,如下所示。值得注意的是,这仅在启用了实验性Web平台功能标志的Blink浏览器中有效。

Ana,你的文章非常有趣且全面,阅读它们是一种享受。
写这些文章需要多长时间?
这取决于。
我特别是在1月22日开始撰写这篇文章,并在2月27日完成。可能花了大约100个小时,其中很大一部分只是打磨内容。打磨实际文本,制作和重做交互式演示,解释与具有
transform-origin
的transform
等效的链接,因为有时我只是觉得需要用新的思路和新的代码重新开始处理某些事情,这样它就不会被旧的思路污染。但这确实取决于文章。关于模态框的评论是我在一周内写完并打磨的。关于计时函数或
background-clip
的文章花费的时间比这篇文章长得多。一般来说,带有插图或交互式演示的文章花费的时间更长。这就是为什么在经历了一系列我最终在两到八个月后放弃的文章的糟糕经历之后,我减少了需要那种东西的话题。Ana,这完全令人难以置信且才华横溢。