freeCodeCamp 课程拆解:用 animation-duration 让多个元素以不同速率闪烁(Animate Multiple Elements at Variable Rates)
本篇基于 freeCodeCamp 开源课程库中的挑战文件 Animate Multiple Elements at Variable Rates,完整讲解"Applied Visual Design"课程中这道 CSS 动画练习的问题设定、参考答案、自动测试原理,以及它与前后章节(通过 @keyframes 变速、通过 animation-timing-function 变速)构成的技术脉络。读完后,你将掌握用单一 @keyframes 规则配合不同 animation-duration 让多个元素以不同速率独立循环动画的完整方案,并能看懂 freeCodeCamp 挑战文件的组织方式与测试运行机制。
挑战在课程中的位置与文件组织
这道挑战属于 Responsive Web Design 认证下的 applied-visual-design 课程块。课程块的挑战顺序由结构文件 applied-visual-design.json 声明,其中本挑战(id 为 587d78a8367417b2b2512ae6)位于两个关键练习之间:
- 前一课 Animate Elements at Variable Rates:通过为每个元素单独编写
@keyframes规则(修改关键帧百分比位置)来改变各自的动画节奏; - 本课 Animate Multiple Elements at Variable Rates:改用同一个
@keyframes规则 + 不同的animation-duration达到同样的"变速"效果; - 后一课 Change Animation Timing with Keywords:在
animation-duration相同的前提下,用animation-timing-function(linear/ease-out等)改变元素在时长内的加减速曲线。
三道课环环相扣,共同覆盖了 CSS 动画"速率控制"的三个正交维度:时长(duration)、关键帧形态(keyframes)、缓动曲线(timing function)。
从源码结构看,每个挑战是一个 Markdown 文件(文件名为挑战的 Mongo ObjectId),存放于 curriculum/challenges/english/blocks/<块名>/ 目录下。这些文件会被课程构建工具解析为结构化挑战数据,并由 challenge-schema.js 中的 Joi schema 校验。其中与本挑战 frontmatter 直接相关的字段约束包括:
dashedName(第 182 行):Joi.string().regex(slugRE),正则要求小写字母、数字、连字符,对应本文件的dashedName: animate-multiple-elements-at-variable-rates;challengeType(第 180 行):取值 0~33 的整数,本挑战为0(经典 HTML/CSS 挑战);tests(第 375-389 行):要求每个挑战必须携带至少一条测试(公开挑战需含text与testString),这正是文末三段断言的来源。
Markdown 文件内部以 # --description--、# --instructions--、# --hints--、# --seed--(含 ## --seed-contents-- 初始代码)、# --solutions--(参考答案)等标记分隔内容,--seed-- 与 --solutions-- 各包含一段完整的可运行 HTML。
题目设定:三颗同速闪烁的星星
原始挑战的初始代码(seed)如下,它模拟了一个夜空场景:#back 是一个固定定位、铺满视口的渐变背景,三颗 30×30px 的圆形白色"星星"以相同的 1s 周期无限循环闪烁:
<style>
.stars {
background-color: white;
height: 30px;
width: 30px;
border-radius: 50%;
animation-iteration-count: infinite;
}
.star-1 {
margin-top: 15%;
margin-left: 60%;
animation-duration: 1s;
animation-name: twinkle;
}
.star-2 {
margin-top: 25%;
margin-left: 25%;
animation-duration: 1s;
animation-name: twinkle;
}
.star-3 {
margin-top: 10%;
margin-left: 50%;
animation-duration: 1s;
animation-name: twinkle;
}
@keyframes twinkle {
20% {
transform: scale(0.5);
opacity: 0.5;
}
}
#back {
position: fixed;
padding: 0;
margin: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(black, #000099, #66c2ff, #ffcccc, #ffeee6);
}
</style>
<div id="back"></div>
<div class="star-1 stars"></div>
<div class="star-2 stars"></div>
<div class="star-3 stars"></div>
这段代码里有几个值得注意的设计点:
- 公共样式抽到
.stars上:尺寸、圆角、animation-iteration-count: infinite(无限循环)写在共享类上,三个星星元素在 HTML 中同时携带stars与各自的编号类(如class="star-1 stars"),利用 CSS 多类叠加来"继承 + 差异化"; - 单一
@keyframes twinkle:所有星星引用同一条关键帧规则。规则内只声明了20%关键帧(缩小到 0.5 倍、透明度降到 0.5),其余关键帧(0% 和 100%)由浏览器隐式取元素的当前样式,因此动画呈现为"每周期前 20% 快速变暗变小、随后恢复"的闪烁效果; - 三颗星星位置不同但节奏完全相同:
margin-top/margin-left使用百分比控制布局位置,而animation-duration三者都是1s——这正是题目要改变的地方:三颗星"齐步走"的闪烁看起来机械,真实夜空中的星星应有错落感。
挑战描述(--description--)指出:在前一课中你已经通过修改两个相似动画元素的 @keyframes 规则改变了它们的动画速率;而本课题目要求换一种等价但更简洁的手段——直接操纵多个元素的 animation-duration。指令(--instructions--)非常明确:
将类名分别为
star-1、star-2、star-3的三个元素的animation-duration分别设置为1s、0.9s、1.1s。
参考答案:只改两个时长值
完整参考答案(--solutions-- 区块)与 seed 代码几乎一致,唯一差异是两个时长声明:
<style>
.stars {
background-color: white;
height: 30px;
width: 30px;
border-radius: 50%;
animation-iteration-count: infinite;
}
.star-1 {
margin-top: 15%;
margin-left: 60%;
animation-duration: 1s;
animation-name: twinkle;
}
.star-2 {
margin-top: 25%;
margin-left: 25%;
animation-duration: 0.9s; /* 由 1s 改为 0.9s */
animation-name: twinkle;
}
.star-3 {
margin-top: 10%;
margin-left: 50%;
animation-duration: 1.1s; /* 由 1s 改为 1.1s */
animation-name: twinkle;
}
@keyframes twinkle {
20% {
transform: scale(0.5);
opacity: 0.5;
}
}
#back {
position: fixed;
padding: 0;
margin: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(black, #000099, #66c2ff, #ffcccc, #ffeee6);
}
</style>
<div id="back"></div>
<div class="star-1 stars"></div>
<div class="star-2 stars"></div>
<div class="star-3 stars"></div>
修改后三颗星星共享同一份关键帧形态,但各自一个完整闪烁周期分别为 1s、0.9s、1.1s。由于周期不同,三者的闪烁相位会随时间不断错开,形成"参差不齐"的自然闪烁感——这正是"variable rates"(可变速率)的视觉目标。
这里体现了 CSS 动画的时长与关键帧解耦原则:@keyframes 定义的是"一个周期内动画如何随时间百分比变化",而 animation-duration 决定"一个周期有多长"。改变周期长度不需要改写关键帧,只需调整 animation-duration 即可,这也是本课相对前一课(需要为每个元素单独复制一份 @keyframes)更优的原因——代码更少、维护更容易。
测试机制:getComputedStyle 读取运行时样式
挑战文件 --hints-- 区块中内置了三条自动测试(见 挑战文件),它们分别对三颗星星做计算样式断言:
const starOne = document.querySelector('.star-1');
const starOneStyle = window.getComputedStyle(starOne);
assert.equal(starOneStyle?.animationDuration, '1s');
const starTwo = document.querySelector('.star-2');
const starTwoStyle = window.getComputedStyle(starTwo);
assert.equal(starTwoStyle?.animationDuration , '0.9s');
const starThree = document.querySelector('.star-3');
const starThreeStyle = window.getComputedStyle(starThree);
assert.equal(starThreeStyle?.animationDuration, '1.1s');
三段测试的共同模式是:
- 用
document.querySelector按类名定位元素; - 用
window.getComputedStyle读取浏览器实际计算后的样式(CSSStyleDeclaration对象的animationDuration属性),而不是读取元素的style内联属性或源码文本; - 用
assert.equal精确比较字符串形式的时长值。
用 getComputedStyle 而非检查源码字符串,意味着只要页面中任何层级的 CSS(类选择器、内联样式等)最终让元素的生效时长等于目标值,测试就能通过。这也解释了为什么答案中 star-1 的 1s 原值不动也能通过第一条断言。
从客户端源码看,这些测试如何到达执行环境:经典挑战页面 classic/show.tsx 中的 GraphQL 查询会拉取挑战的 tests { text testString } 字段(第 586-589 行),即把每条测试的描述文案与可执行断言代码一起下发到前端;用户的代码与断言随后在隔离的沙箱环境中运行,全部断言通过挑战才算完成。这种"源码级 schema 校验 + 运行时断言"的双层验证,正是 freeCodeCamp 课程体系能自动批改 HTML/CSS 挑战的基础。
与前后两课的方法对比
把三道课的解法放在一起,可以更清晰地看到 CSS 动画变速的三个独立旋钮:
| 手段 | 对应课程 | 控制维度 | 代码代价 |
|---|---|---|---|
为每个元素复制并修改 @keyframes(改变关键帧百分比) |
Animate Elements at Variable Rates | 一个周期内变化的"时刻"与形态 | 需为每个元素维护一份独立的关键帧规则 |
共享 @keyframes + 不同 animation-duration(本课) |
本挑战 | 一个周期的总时长 | 只改一个属性值,关键帧只写一份 |
相同时长 + 不同 animation-timing-function |
Change Animation Timing with Keywords | 周期内的加减速曲线(linear、ease-out 等) |
每元素一行属性 |
以本课场景为例,如果三颗星都想"周期相同但节奏不同",animation-timing-function 可以改变闪烁的起止快慢;而本课的 animation-duration 差异则是让"一个闪烁周期本身有多长"不同。二者可以叠加使用:先共享关键帧,再为每个元素设置不同的 duration、delay、iteration-count,就能用最少代码构造出复杂的错峰动画。
小结
本挑战的核心知识点可以浓缩为一句话:多个元素引用同一份 @keyframes 时,通过为每个元素设置不同的 animation-duration(如 1s / 0.9s / 1.1s),即可让它们的循环动画以不同速率独立运转,无需复制关键帧规则。结合 课程结构文件、挑战 schema 与 挑战渲染入口,可以看出 freeCodeCamp 是如何把一段"可编辑 seed 代码 + 参考答案 + getComputedStyle 断言"组织成一道可自动批改、可独立复现的课程挑战的;这套结构本身就是学习 CSS 动画课程化组织方式的一个范例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00