交错动画,也被称为 “跟进” 或 “重叠动作” 是由奥利·约翰斯顿和弗兰克·托马斯在他们 1981 年的著作《生命之幻觉》中定义的迪士尼动画十二原则之一。 其核心概念是通过延迟的顺序来动画化对象,以产生流畅的运动。
然而,这项技术并不仅仅适用于可爱的卡通人物动画。 数字界面的运动设计方面对 UX、用户感知和 “感受” 有 重大影响。 谷歌甚至在其 运动编排 页面中提到了交错动画,作为 Material Design 指南的一部分

虽然运动设计这个主题非常广泛,但我经常发现自己即使在最小的项目中也会应用一些片段。 在 Eko 上的 可交互可口可乐广告 的设计过程中,我被要求创建一些动画,在交互式视频加载时显示,因此这个模型诞生了
乍一看,这个动画似乎很简单就能用 CSS 实现,但事实并非如此! 虽然使用 GSAP 和全新的 Web 动画 API 可能更简单,但使用 CSS 则需要一些技巧,我将在本文中解释。 那为什么还要使用 CSS 呢? 在这种情况下——由于动画旨在在用户等待资产加载时运行,因此加载动画库仅仅为了显示加载动画是没有意义的。
首先,关于动画的结构。
有四个圆圈,绝对定位在一个容器内,该容器具有 overflow: hidden 属性,用于框定和裁剪最外侧两个圆圈的边缘。 为什么是四个而不是三个? 因为第一个圆圈在屏幕外,等待从左侧进入舞台,最后一个圆圈从右侧退出舞台。 其他两个始终在框架中。 这样,动画迭代的结束状态看起来与它的起始状态完全一样。 圆圈 1 取代圆圈 2,圆圈 2 取代圆圈 3,依此类推。
这是基本的 HTML
<div id="container">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
以及相应的 CSS
#container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 160px;
height: 40px;
display: block;
overflow: hidden;
}
span {
width: 40px;
height: 40px;
border-radius: 50%;
background: #4df5c4;
display: inline-block;
position: absolute;
transform: translateX(0px);
}
让我们用一个简单的动画来尝试一下,每个圆圈的 X 轴从 0 到 60 像素进行平移
查看 CodePen 上的示例 点加载器 - 无交错,由 Opher Vishnia (@OpherV) 创建。
看起来有点奇怪和机械,对吧? 那是因为我们缺少一个主要组件:交错动画。 也就是说,每个圆圈的动画需要在其前一个动画之后开始。 “没问题!”,你可能会想,“让我们使用 animation-delay 属性。 我们将第 4 个圆圈的值设置为 0s,第 3 个设置为 0.15s,依此类推”。 好吧,让我们试试
查看 CodePen 上的示例 点加载器 - 错误,由 Opher Vishnia (@OpherV) 创建。
嗯… 发生了什么? animation-delay 属性只影响动画开始前的初始延迟。 它不会在每次迭代之间添加额外的延迟,因此动画会像下面的图表一样失去同步

数学来拯救
为了克服这个问题,我将延迟烘焙到动画中。 CSS 关键帧动画以百分比表示,通过一些计算,您可以使用它们来定义动画应包含多少延迟。 例如,如果将 animation-duration 设置为 1s,并将起始关键帧设置为 0%,将相同的数值设置为 20%,结束关键帧设置为 80%,将相同的结束数值设置为 100%,动画将等待 0.2 秒,运行 0.6 秒,然后再次等待 0.2 秒。
在我的例子中,我希望每个圆圈在进行持续 0.5 秒的实际动画之前等待 0.15 秒的交错时间,整个过程持续 1 秒。 这意味着第 4 个圆圈动画等待 0 秒,然后动画 0.5 秒,并等待另外 0.5 秒。 第 2 个圆圈等待 0.15 秒,然后动画 0.5 秒,并等待 0.35 秒,依此类推。
为了实现这一点,您需要四个关键帧(或三个关键帧对):1 和 2 用于交错等待,2 和 3 用于实际动画时间,而 3 和 4 用于最终等待。 “技巧” 是理解如何将所需的时间转换为关键帧百分比,但这只是一个相对简单的计算。 例如,第 2 个圆圈需要等待 0.15 * 2 = 0.3 秒,然后动画 0.5 秒。 我知道动画的总时间是一秒钟,因此关键帧百分比的计算方式如下
0s = 0%
0.3s = 0.3 / 1s * 100 = 30%
0.8s = (0.3 + 0.5) / 1s * 100 = 80%
1s = 100%
最终结果看起来像这样

由于整个动画(包括交错时间和等待时间)烘焙到 CSS 关键帧中,恰好持续一秒钟,因此动画不会失去同步。
幸运的是,Sass 允许我们使用一个简单的 for 循环和一些内联数学来自动化这个过程,最终编译成一系列关键帧动画。 这样,您可以操作时间变量来实验和测试对您的动画最有效的方法
@mixin createCircleAnimation($i, $animTime, $totalTime, $delay) {
@include keyframes(circle#{$i}) {
0% {
@include transform(translateX(0));
}
#{($i * $delay)/$totalTime * 100}% {
@include transform(translateX(0));
}
#{($i * $delay + $animTime)/$totalTime * 100}% {
@include transform(translateX(60px));
}
100% {
@include transform(translateX(60px));
}
}
}
$animTime: 0.5s;
$totalTime: 1s;
$staggerTime: 0.15s;
@for $i from 0 through 3 {
@include createCircleAnimation($i, $animTime, $totalTime, $staggerTime);
span:nth-child(#{($i + 1)}) {
animation: circle#{(3 - $i)} $totalTime infinite;
left: #{$i * 60 - 60 }px;
}
}
瞧——这是最终的结果
<
p data-height=”450 data-theme-id=” 1″=”” data-slug-hash=”bEydYo” data-default-tab=”result” data-user=”OpherV” data-embed-version=”2″ data-pen-title=”dot loading animation – SASS stagger” class=”codepen”>查看 CodePen 上的示例 点加载动画 - SASS 交错,由 Opher Vishnia (@OpherV) 创建。
这种方法有两个主要的注意事项
首先,您需要确保定义的交错时间/动画时间不要过长,以免与动画总时间重叠,否则数学(和动画)将失效。
其次,这种方法会生成大量的 CSS 代码,尤其是在使用 Sass 来生成所有浏览器兼容性前缀时。 在我的示例中,我只对四个项目进行了动画处理,但如果您的项目包含更多项目,生成的代码量可能不值得付出努力,您可能希望坚持使用基于 JS 的动画库,例如 GSAP。 尽管如此,完全用 CSS 来完成这件事还是非常酷的。
让生活更轻松
为了对比 Sass 解决方案的冗长,我想向您展示如何使用 GSAP 的 Timeline 和 staggerTo 函数轻松实现相同的效果
查看 CodePen 上的示例 点加载动画 - GSAP,由 Opher Vishnia (@OpherV) 创建。
这里有两个有趣的地方。 首先,staggerTo 的最后一个参数定义了动画元素之间的等待时间,它被设置为负值 (-0.15)。 这使得元素能够以相反的顺序交错(圆圈 4-3-2-1 而不是 1-2-3-4)。 很酷,对吧?
其次,请查看 tl.set({}, {}, “1”); 这段代码。 这段代码有什么奇怪的语法? 这是一个实现每个圆圈动画结束时的等待时间的巧妙技巧。 本质上,通过在时间 1 将一个空对象设置为一个空对象,Timeline 动画将从 1 秒标记后开始重复,而不是在圆圈动画结束之后。
展望未来
Web 动画 API 是新一代令人兴奋的技术,但它超出了本文的范围。 我还是忍不住为您提供一个示例实现,它使用了与 CSS 实现相同的数学方法
查看 CodePen 上的示例 点加载动画 - WAAPI,由 Opher Vishnia (@OpherV) 创建。
这有帮助吗? 您是否使用过这种技术创建了一些流畅的动画? 告诉我!
非常好,Opher!
我只是想指出,在 GSAP 版本中,你完全不需要那个额外的 .set({}, {}, “1”) - 它没有任何作用,因为之前的缓动动画在 1.1 秒结束(时间线在末尾重复)。set() 不会告诉它提前重复。
事实上,你可以稍微简化你的代码 - 这里有一个使用 fromTo() 的分支
如果你的目标是让时间线在达到 1 秒位置时重新开始,你可以使用类似这样的 call()
或者,你也可以使用不同的缓动动画来动画播放头的位置!就像
如果大家有任何与 GSAP 相关的问题,请随时访问我们的论坛 https://greensock.com/forums/
快乐缓动!
看来我忘记了如何做数学 - 持续时间不是 1.1 秒,而是 0.95 秒(第一个缓动动画没有延迟)。抱歉。但你仍然不需要那个 .set({}, {}, “1”) 调用,因为你可以简单地在时间线上设置 repeatDelay 为 0.05 来获得相同的结果(但更易读)。演示
很棒,谢谢 :)
我相信你可以在你的 mixin 中折叠 @keyframe 规则(0%, x% { … }),因为 keyframe 1/2 和 3/4 分别是等效的
似乎在输出的前缀属性的情况下,这可能会产生显着影响。
绝对的。在文章中,我为了简洁使用了长格式 - 但这是一个很好的优化建议。谢谢!