在我之前发表的文章CSS 动画的时间统一(Time Uniform For CSS Animation)中,我提到了一种用时间刻度代替keyframes来制作 CSS 动画的方法。由于 CSS 缺乏复杂的数学计算能力,因此适用范围有限。
经过多年的等待,CSS 现在已经支持足够多的数学函数,尤其是 mod(), round(), 和 三角函数。是时候重新审视基于时间的动画方式了,希望这次会更有用。
基本理念
在着色器程序和其他各种程序中,使用时间来制作动画非常常见。CSS 无法像 JavaScript 那样启动计时器,但现在可以通过 CSS Houdini API 定义一个自定义变量,以毫秒为单位跟踪时间。
@property --t {
syntax: "<integer>";
initial-value: 0;
inherits: true
}
@keyframes tick {
from { --t: 0 }
to { --t: 86400000 }
}
:root {
animation: tick 86400000ms linear infinite
}
每过一毫秒,变量 --t
就会递增 1
,即一秒内递增 1000
。有一个使用 counter() 函数显示变量的技巧。
::after {
counter-reset: t var(--t);
content: counter(t);
}
其他基于 --t
的值也会随之变化。这就是我们获得动画效果的方法。
div {
/* 1 turn per second */
rotate: calc(var(--t) * .001turn);
}
控制帧频
将更新频率保持在每秒 60 帧(FPS)即可实现流畅的动画效果。浏览器通常会对渲染进行优化,因此更新频率高于 60 帧/秒不会有任何问题。但如果需要,也可以使用 step() 函数手动控制帧频。
/* ... */
:root {
animation: tick;
animation-duration: 86400000ms;
animation-iteration-count: infinite;
/* 8 fps */
animation-timing-function: step(calc(86400000/(1000/8)));
/* 24 fps */
animation-timing-function: step(calc(86400000/(1000/24)));
/* 60 fps */
animation-timing-function: step(calc(86400000/(1000/60)));
}
变化时间
--t
的值不断向一个方向增长。对于角度值大于 360deg
的情况,这样做没有问题,但并非所有 CSS 属性都将其值视为周期性的。
比方说,我想把一个方框从左到右做成动画,如果平移偏移量与 --t
相关联,它就会不停地增加。
translate: calc(var(--t) * .001px);
min()
一个预期的结果是,当偏移量达到一个特定值时,它会立即停止。这就是 min() 函数的作用所在。
translate: min(270px, calc(var(--t) * .5px));
为了精确控制动画的持续时间,我们可以限制 --t
的值。
/* 270px in 3s */
translate: calc(min(3000, var(--t)) * (270px / 3000));
mod()
在方框向右移动后,另一个选择是重新开始偏移。现在我们有了 mod() 函数来实现这一功能。
translate: calc(mod(var(--t)/4, 270) * 1px);
sin()
或者来回移动。
translate: calc(sin(mod(var(--t)/135, 270)) * 135px);
自定义 easing 函数
我们可以使用数学函数和 --t
变量创建自定义的 easing 函数,这可能是 cubic-bezier() 函数无法实现的。
ease-out-cubic
第一步是将 --t
值限定在 0 和 1 之间。
/* from 0 to 1 in 1s */
--t01: calc(min(1000, var(--t)) / 1000);
/* 1 - pow(1 - t, 3) */
--ease-out-cubic: calc(
1 - pow(1 - var(--t01), 3)
);
translate: calc(var(--ease-out-cubic) * 270px);
ease-out-elastic
/* from 0 to 1 in 1s */
--t01: calc(min(1000, var(--t)) / 1000);
/* pow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1 */
--ease-out-elastic: calc(
pow(2, -10 * var(--t01)) *
sin((var(--t01) * 10 - .75) * 2/3 * PI) + 1
);
translate: calc(var(--ease-out-elastic) * 270px);
尝试使用 CSS Doodle
随着表达式越来越复杂,var()
和 calc()
往往会降低代码的可读性。因此,我添加了 @t
函数来表示变量--t
。最新版本的 css-doodle 还可以直接在参数内接受简单的数学表达式。
/* rotate: calc(mod(var(--t) / 1000, 10) * 5deg); */
rotate: @t(/1000, %10, *5deg);
不编写keyframes,代码就很简短。
@grid: 20x1 / 280x 60px;
@gap: 1px;
@size: 100% 20%;
background: #000;
margin: auto;
translate: 0 calc(20px * sin(4*@t(/20, +@i(*6), %360deg)));
它还能快速试验新参数。
translate: 0 calc(20px * sin(3*@t(/50, *@i(*2), %360deg)));
函数 @T and @TS
除了 @t
函数外,大写函数 @T
还表示从一天开始的另一个时间刻度。函数 @TS
是 @t(/1000)
的简写,用于跟踪以秒为单位的时间。
这是一个用 css-doodle 制作的时钟。(CodePen link)
/* ... */
/* second */
rotate: @TS(*6, %360deg);
/* minute */
rotate: @TS(/60, *6, %360deg);
/* hour */
rotate: @TS(/60, /12, *6, %360deg);
round()
如何让秒针做跳跃运动呢?当然,直接的方法是使用 round() 函数,其中第三个参数指定了四舍五入的间隔。就时钟而言,每一步等于 360 / 60 = 6deg
。
rotate: round(down, @TS(*6, %360deg), 6deg);
再举一个例子
将颜色和位置一起制作成动画。 (CodePen link).
@grid: 100x1 / 100% auto (4/3) / #10153e;
@size: @rn(1vmin, 5vmin, 10);
margin: auto;
border-radius: 50%;
background: @p(
hsl(@t(/10, +@i(*2), %360), 90%, 80%)
);
box-shadow: @m5(
@r(±23vmin) @r(±23vmin) @r(2vmin) @r(-40px) @p
);
translate: @M2(
calc(100px * tan(6*cos(@t(/10, +@i(*10), /6, %360deg))))
);
结论
我对这种方法很感兴趣。虽然使用keyframes看起来更直接,但对于一个充满数学计算和输入变量的演示场景来说,使用时间作为变量更有可能获得多样化的结果。