首页
/ freeCodeCamp 实战指南:用 animation-iteration-count: infinite 打造 CSS 心跳动画

freeCodeCamp 实战指南:用 animation-iteration-count: infinite 打造 CSS 心跳动画

2026-09-06 14:19:32作者:虞亚竹Luna

本篇围绕 freeCodeCamp Responsive Web Design 认证中 Applied Visual Design 模块的一道综合实战挑战展开:将前面几课积累的 @keyframestransformposition 知识组装起来,通过给两个动画元素追加 animation-iteration-count: infinite,让一个由纯 CSS 绘制的心形在持续变色的背景上"跳动"起来。读完并动手完成后,你能掌握多元素组合动画的时序设计、伪元素变换继承的底层机制,以及理解课程测试如何通过 getComputedStyle 验证动画属性。

挑战定位:课程脉络中的这一课

该挑战文件位于 587d78a8367417b2b2512ae4.md,元数据显示其标题为 "Make a CSS Heartbeat using an Infinite Animation Count",challengeType: 0dashedName: make-a-css-heartbeat-using-an-infinite-animation-count

从课程结构文件 applied-visual-design.jsonchallengeOrder 可以看到,它是动画系列中的收尾实战题之一,前面依次是:

  1. 学习 @keyframesanimation 属性的基本用法;
  2. 用动画改变按钮 hover 状态、创建位移运动、用淡入淡出营造视觉方向感;
  3. 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.jsonblockOrder 中得到印证。

原始骨架代码(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);
  }
}

这里有两个容易踩坑的细节:

  1. 每个关键帧都必须重复书写 rotate(-45deg)。因为 transform 是整体覆盖而非逐属性叠加——如果 50% 帧只写 scale(0.6),旋转会被丢掉,心形瞬间变回菱形方向,动画就会"跳帧"错乱。
  2. ::before / ::after 不需要自己的动画属性。伪元素本身没有设置 transform,它们作为 .heart 的后代内容跟随父元素一起被 scale 缩放——这正是文档 instructions 明确说明"heart:beforeheart:after 选择器不需要任何动画属性"的原因,从源码结构看,缩放发生在父级 .heart 的变换坐标系上,子内容自然被包含。

背景层:background 颜色动画

.back 是一个铺满视口的固定背景层(position: fixedtop/left: 0,宽高 100%),配合 padding: 0; margin: 0 抵消 body 默认间距。它的 @keyframes backdiv 只在 50% 关键帧把背景从白色切到淡粉 #ffe6f2,其余时间点保持起始色,于是每跳一下心,背景就闪一次粉红,与心跳节奏同步(两者 animation-duration 均为 1s)。

核心任务:追加 animation-iteration-count: infinite

CSS 动画默认 animation-iteration-count1,即只播放一遍就停在终态。前置挑战(弹球课)已经介绍过该属性:写 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,两者节奏会错开,可以自行实验观察效果。

小结与相关资源

一句话总结本课的技术要点:用 transformscale + 恒定的 rotate(-45deg))驱动心形缩放、用 background 关键帧驱动背景变色,最后给两条动画同时挂上 animation-iteration-count: infinite,就得到一个持续跳动的纯 CSS 心跳动画——全程无需 JavaScript。

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