网页动画总是卡在第一步,这串CSS代码能让元素一个接一个动起来?

3,744字
16–24 分钟
in

搞过网页动画的小伙伴都懂,想让几个元素按顺序轮流动起来,那叫一个折腾。以前得算半天延迟时间,写一堆复杂的@keyframes,还动不动就翻车。要是元素数量一变,又得重新推倒重来,简直是噩梦。

目录

别急,最近CSS整了个新活,用几行代码就能搞定这个老大难问题。今天就来盘一盘,怎么让这些动画小精灵规规矩矩地排好队,一个接一个地表演,而且加多少个元素都不怕。

先来瞅瞅效果,到底有多丝滑

其实想实现的动画效果很简单:页面上一排卡片,第一个开始动,动完之后第二个接上,然后是第三个,全部轮完一圈后,又回到第一个,如此循环。这种需求在轮播图、步骤引导或者产品展示里太常见了。

以前要实现这个,得手动给每个元素加不同的动画延迟,比如第一个延迟0秒,第二个延迟2秒,第三个延迟4秒,以此类推。要是哪天老板说要加5个新功能点,或者删掉两个,那之前的延迟时间全得重新算,累不累?现在有了新方法,完全不用操这个心,无论元素数量是多是少,动画流程都能自动适应。

主角登场,linear()函数是个啥

要玩转这个顺序动画,得先认识CSS动画家族的新成员——linear()函数。这哥们儿可不是以前那个只能做匀速运动的linear关键词,它的本事大得多。

可以把它想象成一个能自定义曲线的“动画剧本”。以前用linear关键词,动画就是从起点直线跑到终点,中间不带拐弯的。但linear()函数允许随意定义好几个“剧情节点”,每个节点都决定了动画在某个时间点应该处于什么状态。

举个栗子,搞一个方块从左边移动到右边又弹回来的效果:

@keyframes 跑个来回 {
  0% { transform: translateX(0); }
  100% { transform: translateX(200px); }
}

.box {
  animation: 跑个来回 2s linear(0 0%, 1 50%, 0 100%);
}

看看这个linear(0 0%, 1 50%, 0 100%),它就像一个三幕剧的剧本:时间刚到0%,方块在起点(0);演到一半(50%时间点),方块已经跑到了终点(1);到了谢幕时间(100%),它又回到了起点(0)。这不就是过去要写好几句关键帧才能搞定的效果吗?现在一行linear()就解决,而且后面要改比例,改参数,完全不用去碰@keyframes本体,只改这个函数里的数值就行。

linear()变成排队神器

明白了linear()能自定义时间轴上的进度,那怎么让它实现排队动画呢?核心思路就是给每个元素分配一段“专属表演时间”,其余时间都在“后台候场”。

想象一下有N个演员在舞台上,每个人的戏份时长相同。第一个演员从开场演到第1/N场戏的时间点,然后退场,轮到第二个演员上场。第二个演员从第1/N场戏结束时开始,演到第2/N场戏结束时,再让第三个上场。以此类推,大家轮流上场。

把这个思路翻译成CSS代码,就需要知道两件事:舞台上一共有几个演员(元素总数),以及当前演员是第几个(当前元素的索引)。好消息是,CSS新出了两个神级函数——sibling-count()sibling-index(),刚好能解决这个问题。sibling-count()能数出来同一父级下有几个兄弟元素,sibling-index()能标出来当前元素是兄弟中的老几。目前这两个函数在Chrome和Edge里跑得飞起,火狐和Safari也已经在跟进的路上了。

有了这两个“侦察兵”,就可以写出一段通用的动画代码了:

/* 先让所有元素都套上动画 */
.container > * {
  /* 先算算每个元素的专属戏份开始和结束的时间节点 */
  --_start: calc(100% * (sibling-index() - 1) / sibling-count());
  --_end: calc(100% * sibling-index() / sibling-count());

  /* 动画总时长 = 每个元素的表演时间 * 元素总数 */
  animation: 炫酷特效 calc(0.5s * sibling-count()) infinite 
             linear(0, 0 var(--_start), 1, 0 var(--_end), 0);
}

/* 动画内容只要定义最终效果就行,不用管过程 */
@keyframes 炫酷特效 {
  to {
    background: #F8CA00;
    scale: 0.8;
  }
}

来看这段代码是怎么“排班”的。假设有3个元素,sibling-count()是3。第一个元素(索引是1),它的--_start就是calc(100%*(1-1)/3),结果是0%;--_endcalc(100%*1/3),大约是33.33%。第二个元素(索引是2),--_startcalc(100%*(2-1)/3),大约是33.33%;--_endcalc(100%*2/3),大约是66.67%。第三个元素以此类推。

再看linear(0, 0 var(--_start), 1, 0 var(--_end), 0)这句“剧本”:

  • 从0开始(动画初始状态)。
  • 一直保持到--_start时间点之前,都是0(不动),这就相当于“候场”。
  • 到达--_start时,开始往1走(动画表演中)。
  • 走到--_end时,完成表演,然后立刻回到0(退场恢复原状)。
  • --_end一直到结束,都保持0,继续“候场”等下一轮。

就这么巧妙,每个元素都只在自己的那一段“戏份区间”里表演,其他时间都乖乖候着,动画自然就连贯起来了。而且因为--_start--_end是动态算出来的,加一个元素、减一个元素,都不用改动画代码,自动适配,简直不要太爽。

照着抄作业,分步实操

理论知识差不多了,直接上手操作。假设要在页面上实现一列卡片按顺序逐个放大又缩小的效果。

第一步:搭好HTML结构

搞个父级容器,里面想放多少卡片就放多少。

<div class="card-group">
  <div class="card">第一张牌</div>
  <div class="card">第二张牌</div>
  <div class="card">第三张牌</div>
  <div class="card">第四张牌</div>
</div>

第二步:写CSS动画核心

先定义好动画内容,这次让卡片变个色加个缩放。

@keyframes 闪现 {
  to {
    background-color: #FFB347;
    transform: scale(1.1);
  }
}

第三步:套用排队逻辑

给所有卡片加上动画,用上前面那一套动态计算公式。

.card-group > .card {
  --duration: 0.4s;  /* 每个卡片的表演时长 */
  --_start: calc(100% * (sibling-index() - 1) / sibling-count());
  --_end: calc(100% * sibling-index() / sibling-count());

  animation: 闪现 calc(var(--duration) * sibling-count()) infinite
             linear(0, 0 var(--_start), 1, 0 var(--_end), 0);
}

写到这里,刷新页面,会发现动画已经跑起来了,卡片们乖乖地排队表演。但有个小细节,火狐和Safari里sibling-index()sibling-count()还没完全支持,为了兼容性,可以给不支持的情况加个降级方案。比如用@supports判断一下,如果浏览器不认识这两个函数,就退回到传统的加延迟的写法,虽然麻烦点,但总比不动好。

另外,动画里如果想改点别的属性,比如透明度、边框什么的,直接在@keyframesto里加就行。linear()只管时间轴上的进度,动画内容还是由关键帧决定。

还能怎么玩,进阶玩法一瞥

排队动画的基础版玩熟了,还能整点花的。比如不让动画完全错开,而是让前一个没结束,后一个就开始,制造一种重叠的、更连贯的效果。

这个思路得重新画“剧本”。假设有N个元素,但这次定义N+1个时间节点,每个元素在相邻的两个时间段里都有戏份,这样就会有动画重叠的效果。

代码稍微变一下,把linear()函数里的“表演区间”从一段改成两段,从[--_start, --_end]改成两段区间[--_start, --_mid][--_mid, --_end]。公式得重新推导,但原理还是一样,都是通过动态计算控制点来分配每个元素的活跃时间。这种玩法能让动画看起来更紧凑,有点像多米诺骨牌一张推一张的效果。

还有一种是让动画在元素间“接力”,第一个动完直接变成最终状态,然后第二个开始动,这样最后所有元素都处于结束状态,看起来更有层次感。这个只要调整linear()函数里的“输出”值,把“退场”改成“保持”就行。比如把最后一个0改成1,那就意味着每个元素表演完就不再恢复原状,而是定格在最终状态。

CSS的linear()函数就像是给了动画设计师一把时间线手术刀,以前只能做线性切割,现在可以任意雕刻每个时间片段里的动画进度。配合上sibling-count()sibling-index()这两个元素计数器,以前那些繁琐的手动计算全都可以交给CSS自动完成。以后再遇到“让这一堆元素按顺序动起来”的需求,直接把这套代码甩过去,稳得一批。