freeCodeCamp 实战指南:用 animation-iteration-count: infinite 打造 CSS 心跳动画
本篇围绕 freeCodeCamp Responsive Web Design 认证中 Applied Visual Design 模块的一道综合实战挑战展开:将前面几课积累的 @keyframes、transform、position 知识组装起来,通过给两个动画元素追加 animation-iteration-count: infinite,让一个由纯 CSS 绘制的心形在持续变色的背景上"跳动"起来。读完并动手完成后,你能掌握多元素组合动画的时序设计、伪元素变换继承的底层机制,以及理解课程测试如何通过 getComputedStyle 验证动画属性。
挑战定位:课程脉络中的这一课
该挑战文件位于 587d78a8367417b2b2512ae4.md,元数据显示其标题为 "Make a CSS Heartbeat using an Infinite Animation Count",challengeType: 0,dashedName: make-a-css-heartbeat-using-an-infinite-animation-count。
从课程结构文件 applied-visual-design.json 的 challengeOrder 可以看到,它是动画系列中的收尾实战题之一,前面依次是:
- 学习
@keyframes与animation属性的基本用法; - 用动画改变按钮 hover 状态、创建位移运动、用淡入淡出营造视觉方向感;
- 在 Animate Elements Continually Using an Infinite Animation Count 一课中首次引入
animation-iteration-count: infinite,让一颗小弹球持续弹跳。
而"心形"本身并不是本课从零画的。搜索仓库可知,.heart 结构(一个 50px 正方形加 ::before/::after 两个圆形伪元素)出自更早的 Create a More Complex Shape Using CSS and HTML 挑战——本挑战的 description 原文即说明"使用了你在前一挑战中设计的心形"。整个模块隶属于 Responsive Web Design 认证,这一点可从 responsive-web-design.json 的 blockOrder 中得到印证。
原始骨架代码(Seed)完整解析
挑战给出的初始代码(seed)已经包含了完整的心形结构、两套 @keyframes 规则,唯独缺少让动画循环的属性。完整代码如下:
<style>
.back {
position: fixed;
padding: 0;
margin: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
animation-name: backdiv;
animation-duration: 1s;
}
.heart {
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: pink;
height: 50px;
width: 50px;
transform: rotate(-45deg);
animation-name: beat;
animation-duration: 1s;
}
.heart:after {
background-color: pink;
content: "";
border-radius: 50%;
position: absolute;
width: 50px;
height: 50px;
top: 0px;
left: 25px;
}
.heart:before {
background-color: pink;
content: "";
border-radius: 50%;
position: absolute;
width: 50px;
height: 50px;
top: -25px;
left: 0px;
}
@keyframes backdiv {
50% {
background: #ffe6f2;
}
}
@keyframes beat {
0% {
transform: scale(1) rotate(-45deg);
}
50% {
transform: scale(0.6) rotate(-45deg);
}
}
</style>
<div class="back"></div>
<div class="heart"></div>
这段代码由两部分动画构成,与文档 description 的描述一一对应:
心形主体:transform 缩放动画
.heart 是核心元素。它的定位写法值得注意:position: absolute 加上四边 top/right/bottom/left: 0 再加 margin: auto,这是经典的"绝对定位元素水平垂直双向居中"技巧,让心形无论视口多大都停在屏幕正中央。
心形的几何构造来自前一课:
- 主体是一个 50px × 50px 的粉色正方形,
transform: rotate(-45deg)旋转 45 度变成菱形; ::before是一个 50px 圆形(border-radius: 50%),top: -25px; left: 0把它叠在菱形左上角;::after是另一个 50px 圆形,top: 0; left: 25px叠在菱形右上角。
三个粉色块拼合后,视觉上就形成了标准的心形轮廓。
@keyframes beat 定义了心跳的"收缩"节奏:
@keyframes beat {
0% {
transform: scale(1) rotate(-45deg);
}
50% {
transform: scale(0.6) rotate(-45deg);
}
}
这里有两个容易踩坑的细节:
- 每个关键帧都必须重复书写
rotate(-45deg)。因为transform是整体覆盖而非逐属性叠加——如果 50% 帧只写scale(0.6),旋转会被丢掉,心形瞬间变回菱形方向,动画就会"跳帧"错乱。 ::before/::after不需要自己的动画属性。伪元素本身没有设置transform,它们作为.heart的后代内容跟随父元素一起被scale缩放——这正是文档 instructions 明确说明"heart:before和heart:after选择器不需要任何动画属性"的原因,从源码结构看,缩放发生在父级.heart的变换坐标系上,子内容自然被包含。
背景层:background 颜色动画
.back 是一个铺满视口的固定背景层(position: fixed,top/left: 0,宽高 100%),配合 padding: 0; margin: 0 抵消 body 默认间距。它的 @keyframes backdiv 只在 50% 关键帧把背景从白色切到淡粉 #ffe6f2,其余时间点保持起始色,于是每跳一下心,背景就闪一次粉红,与心跳节奏同步(两者 animation-duration 均为 1s)。
核心任务:追加 animation-iteration-count: infinite
CSS 动画默认 animation-iteration-count 为 1,即只播放一遍就停在终态。前置挑战(弹球课)已经介绍过该属性:写 animation-iteration-count: 3 动画播 3 次停止,而设为 infinite 则会无限循环。
本挑战的操作即:为 .back 和 .heart 两个类各追加一行 animation-iteration-count: infinite;。完整解法(solutions)为:
<style>
.back {
position: fixed;
padding: 0;
margin: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
animation-name: backdiv;
animation-duration: 1s;
animation-iteration-count: infinite;
}
.heart {
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: pink;
height: 50px;
width: 50px;
transform: rotate(-45deg);
animation-name: beat;
animation-duration: 1s;
animation-iteration-count: infinite;
}
.heart:after {
background-color: pink;
content: "";
border-radius: 50%;
position: absolute;
width: 50px;
height: 50px;
top: 0px;
left: 25px;
}
.heart:before {
background-color: pink;
content: "";
border-radius: 50%;
position: absolute;
width: 50px;
height: 50px;
top: -25px;
left: 0px;
}
@keyframes backdiv {
50% {
background: #ffe6f2;
}
}
@keyframes beat {
0% {
transform: scale(1) rotate(-45deg);
}
50% {
transform: scale(0.6) rotate(-45deg);
}
}
</style>
<div class="back"></div>
<div class="heart"></div>
与 seed 相比,实际改动只有两处:.back 和 .heart 各新增一行 animation-iteration-count: infinite;。此时效果为:心形以每秒一次的频率在 scale(1) 与 scale(0.6) 之间往复收缩(1s 时长内 0% → 50% 缩小、自动回放到 100% 放大),背景同步在白色与淡粉之间闪动,形成持续"搏动"的视觉。
测试如何验证你的答案
挑战 hints 部分给出的断言代码展示了课程平台的验证方式——用 window.getComputedStyle 读取运行时的计算样式,再比较 animationIterationCount 是否等于字符串 'infinite':
const heartElement = document.querySelector('.heart');
const heartStyle = window.getComputedStyle(heartElement);
assert.equal(heartStyle?.animationIterationCount, 'infinite');
const backElement = document.querySelector('.back');
const backStyle = window.getComputedStyle(backElement);
assert.equal(backStyle?.animationIterationCount, 'infinite');
两段断言分别对应 .heart 和 .back 两个类,与 instructions 的要求完全一致;::before/::after 则没有任何断言,印证了它们无需动画属性。这也提示一个通用的自查手段:在浏览器 DevTools 的 Computed 面板中,animation-iteration-count 若显示 infinite,即说明循环已生效。
延伸:从本课走向变化速率控制
从 applied-visual-design.json 的顺序看,本挑战之后紧接的是 Animate Elements at Variable Rates:让两个"闪烁星星"以不同的关键帧百分比变化,制造速率差异;其后还有用关键字(keywords)调整动画时序、贝塞尔曲线(Bezier curve)塑形运动等进阶内容。本课"双 1s 同步 + infinite"的组合是这些后续技巧的基线——先让两条动画线无限循环且同频,再去讨论如何刻意制造不同频。
另外两个与本课直接相关的知识点值得回顾:
animation-iteration-count可取正整数或infinite,数值表示循环次数,超出即停止;animation-duration决定单循环时长,两者相乘决定整体节奏。本课两条动画都设为 1s,保证心跳与背景闪烁逐帧对齐;若只改.heart的时长而保留.back为 1s,两者节奏会错开,可以自行实验观察效果。
小结与相关资源
- 挑战全文(含 seed 与 solutions 完整代码):587d78a8367417b2b2512ae4.md
- 心形几何构造来源课:587d78a6367417b2b2512ade.md
animation-iteration-count初学课(弹球循环):587d78a8367417b2b2512ae3.md- 本模块挑战顺序定义:applied-visual-design.json
一句话总结本课的技术要点:用 transform(scale + 恒定的 rotate(-45deg))驱动心形缩放、用 background 关键帧驱动背景变色,最后给两条动画同时挂上 animation-iteration-count: infinite,就得到一个持续跳动的纯 CSS 心跳动画——全程无需 JavaScript。
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