首页
/ freeCodeCamp 课程拆解:用 animation-duration 让多个元素以不同速率闪烁(Animate Multiple Elements at Variable Rates)

freeCodeCamp 课程拆解:用 animation-duration 让多个元素以不同速率闪烁(Animate Multiple Elements at Variable Rates)

2026-09-06 14:25:48作者:段琳惟

本篇基于 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)位于两个关键练习之间:

三道课环环相扣,共同覆盖了 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 行):要求每个挑战必须携带至少一条测试(公开挑战需含 texttestString),这正是文末三段断言的来源。

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>

这段代码里有几个值得注意的设计点:

  1. 公共样式抽到 .stars:尺寸、圆角、animation-iteration-count: infinite(无限循环)写在共享类上,三个星星元素在 HTML 中同时携带 stars 与各自的编号类(如 class="star-1 stars"),利用 CSS 多类叠加来"继承 + 差异化";
  2. 单一 @keyframes twinkle:所有星星引用同一条关键帧规则。规则内只声明了 20% 关键帧(缩小到 0.5 倍、透明度降到 0.5),其余关键帧(0% 和 100%)由浏览器隐式取元素的当前样式,因此动画呈现为"每周期前 20% 快速变暗变小、随后恢复"的闪烁效果;
  3. 三颗星星位置不同但节奏完全相同margin-top / margin-left 使用百分比控制布局位置,而 animation-duration 三者都是 1s——这正是题目要改变的地方:三颗星"齐步走"的闪烁看起来机械,真实夜空中的星星应有错落感。

挑战描述(--description--)指出:在前一课中你已经通过修改两个相似动画元素的 @keyframes 规则改变了它们的动画速率;而本课题目要求换一种等价但更简洁的手段——直接操纵多个元素的 animation-duration。指令(--instructions--)非常明确:

将类名分别为 star-1star-2star-3 的三个元素的 animation-duration 分别设置为 1s0.9s1.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');

三段测试的共同模式是:

  1. document.querySelector 按类名定位元素;
  2. window.getComputedStyle 读取浏览器实际计算后的样式(CSSStyleDeclaration 对象的 animationDuration 属性),而不是读取元素的 style 内联属性或源码文本;
  3. assert.equal 精确比较字符串形式的时长值。

getComputedStyle 而非检查源码字符串,意味着只要页面中任何层级的 CSS(类选择器、内联样式等)最终让元素的生效时长等于目标值,测试就能通过。这也解释了为什么答案中 star-11s 原值不动也能通过第一条断言。

从客户端源码看,这些测试如何到达执行环境:经典挑战页面 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 周期内的加减速曲线(linearease-out 等) 每元素一行属性

以本课场景为例,如果三颗星都想"周期相同但节奏不同",animation-timing-function 可以改变闪烁的起止快慢;而本课的 animation-duration 差异则是让"一个闪烁周期本身有多长"不同。二者可以叠加使用:先共享关键帧,再为每个元素设置不同的 durationdelayiteration-count,就能用最少代码构造出复杂的错峰动画。

小结

本挑战的核心知识点可以浓缩为一句话:多个元素引用同一份 @keyframes 时,通过为每个元素设置不同的 animation-duration(如 1s / 0.9s / 1.1s),即可让它们的循环动画以不同速率独立运转,无需复制关键帧规则。结合 课程结构文件挑战 schema挑战渲染入口,可以看出 freeCodeCamp 是如何把一段"可编辑 seed 代码 + 参考答案 + getComputedStyle 断言"组织成一道可自动批改、可独立复现的课程挑战的;这套结构本身就是学习 CSS 动画课程化组织方式的一个范例。

登录后查看全文
热门项目推荐
相关项目推荐