主题
空间转换与动画(一)
本页对应 空间转换与动画,共 8 段视频,约 59 分钟。
学习目标
- 能使用
@keyframes分别以from/to和百分比关键帧定义动画,并说明补间动画的中间状态由浏览器自动生成。 - 能使用
animation-name与animation-duration为元素应用动画,并独立完成颜色变化、缩放变化等基础动画效果。 - 能使用
animation-delay为多个元素设置不同延迟,做出有间隔、错落执行的同款动画效果。 - 能区分
fill-mode中forwards与backwards的作用,并验证动画结束状态和延迟等待状态的不同表现。 - 能使用
steps()速度曲线实现逐帧动画,并与linear匀速动画进行对比说明其差异。 - 能综合运用动画定义、时长、延迟和速度曲线,完成一个多元素动画小案例并进行效果调试。
知识点
空间转换是什么
为什么需要:平面 transform 只能做二维位移、旋转、缩放,想让盒子“立起来”、出现前后层次,就要引入空间转换。转写稿里也明确说,2D 应用更多,3D 空间转换更多是了解和体验,不必因为空间想象弱就焦虑。
怎么想:空间由 X、Y、Z 三条坐标轴构成,Z 轴位置与视线方向相同;空间转换也叫 3D 转换,属性仍是 transform。
怎么做:先建立“有 Z 轴”的意识,再配合视距、旋转和 transform-style 看效果。

易错点:不要把它当成网页 3D 游戏的全部。转写稿提醒,网页 3D 游戏通常不是用 CSS3 这套写的,而是调用 three.js 一类 3D 库;本节目标是知道 CSS3 能实现 3D 效果即可。
平移 translate3d
为什么需要:平面平移只有 X、Y,空间里还要能沿 Z 轴前后移动。
怎么想:取值与平面转换相同,只是多了一个 Z 方向。
怎么做:
css
transform: translate3d(x, y, z);
transform: translateX();
transform: translateY();
transform: translateZ();易错点:默认情况下,Z 轴平移没有效果。原因是电脑屏幕默认是平面,无法显示 Z 轴平移效果;要看到前后关系,需要后面的“视距”配合。
视距 perspective
为什么需要:屏幕是平的,要让元素产生近大远小、近实远虚的透视感,就要指定观察者与 Z=0 平面的距离。
怎么想:视距加给父级,取值范围常用 800—1200。
怎么做:
css
perspective: 视距;
易错点:不要把视距随便加在要动的子元素上;笔记明确说“添加给父级”。没有透视时,Z 轴平移和旋转的立体感会不明显。
旋转 rotateX / rotateY / rotateZ
为什么需要:空间转换不仅要移动,还要让元素绕某条轴转动。
怎么想:Z 轴旋转是 rotateZ(),X 轴旋转是 rotateX(),Y 轴旋转是 rotateY()。转写稿说这块想象不到也没关系,知道有这些能力即可。
怎么做:写法形如 transform: rotateZ(角度);,角度正负要结合方向判断。



易错点:正负方向不要死背,用左手法则判断:左手握住旋转轴,拇指指向正值方向,其他四个手指弯曲方向为旋转正值方向。

rotate3d 与立体呈现
为什么需要:有时要自定义旋转轴;有时父元素旋转后,子元素会被“压平”,需要决定子级在 3D 空间还是平面中。
怎么想:rotate3d(x, y, z, 角度度数) 用来设置自定义旋转轴及角度,x、y、z 取 0—1 之间的数字;transform-style 控制子元素处于 3D 空间还是平面。
怎么做:父级写 transform-style: preserve-3d; 让子级处于 3D 空间;flat 表示子级处于平面中。
易错点:transform-style 要加在“父级”上;如果一直是默认 flat,子元素各自旋转后仍可能被压回平面,立方体感出不来。
案例:3D 导航
为什么需要:把平移、旋转、立体呈现串起来,做出立方体导航。
怎么想:先搭立方体:绿色是立方体前面,橙色是立方体上面;再让鼠标悬停时立方体旋转。转写稿和代码注释都提醒,为了看到橙色和绿色的移动过程,可以临时给立方体加旋转观察。



怎么做:
css
.nav li {
position: relative;
width: 100px;
height: 40px;
line-height: 40px;
transition: all 0.5s;
transform-style: preserve-3d;
/* 为了看到橙色和绿色的移动过程,给立方体添加旋转 */
/* transform: rotateX(-20deg) rotateY(30deg); */
}
.nav li a {
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 100%;
text-align: center;
text-decoration: none;
color: #fff;
}
/* 立方体每个面都有独立的坐标轴,互不影响 */
.nav li a:first-child {
background-color: green;
transform: translateZ(20px);
}
.nav li a:last-child {
background-color: orange;
transform: rotateX(90deg) translateZ(20px);
}
.nav li:hover {
transform: rotateX(-90deg);
}易错点:每个面都有独立坐标轴,互不影响;橙色面不是直接 translateZ,而是先 rotateX(90deg) 再 translateZ(20px);transform-style: preserve-3d 要放在 li 上。
缩放 scale3d
为什么需要:空间里也需要沿 X、Y、Z 缩放。
怎么想:与平面缩放类似,多了 Z 方向。
怎么做:
css
transform: scale3d(x, y, z);
transform: scaleX();
transform: scaleY();
transform: scaleZ();易错点:不要和 translate3d 混用参数含义;缩放写的是比例,不是位移距离。
动画与过渡:先分清“两个状态”和“多个状态”
为什么需要:过渡只能让元素从一个状态变到另一个状态,变完就结束;而页面里常有一直循环、可暂停、可控制最终画面的效果。
怎么想:把过渡理解成“1→2”,把动画理解成“1→2→3→…”,并且动画还能控制重复播放、最终画面、是否暂停。
怎么做:先定义关键帧,再给元素使用动画。
易错点:不要以为动画只能动一次;animation 的重点是“多状态 + 可控”。
定义动画:关键帧与百分比
为什么需要:动画要有多个状态,状态必须写在关键帧里。
怎么想:@keyframes 是固定写法,from/to 表示开始和结束两帧;百分比表示动画时间走到某个节点时的状态,更适合多状态。
怎么做:
css
/* 方式一 */
@keyframes 动画名称 {
from {}
to {}
}
/* 方式二 */
@keyframes 动画名称 {
0% {}
10% {}
......
100% {}
}易错点:@keyframes 不能拼错;from 不是表单的 form;大括号之间没有逗号。实际中先写红到蓝,只设两帧,浏览器会自动补出中间紫色过程,所以它叫补帧动画。
使用动画与复合、拆分属性
为什么需要:定义完动画,必须告诉元素“用哪个动画、播多久”。
怎么想:animation 复合属性里,动画名称和时长必须赋值;取值不分先后;两个时间值时,第一个是动画时长,第二个是延迟时间。
怎么做:
css
animation: 动画名称 动画花费时长;
提示:
- 动画名称和动画时长必须赋值
- 取值不分先后顺序
- 如果有两个时间值,第一个时间表示动画时长,第二个时间表示延迟时间
拆分写法如下:

易错点:只写名称不写时长,动画不会按预期播放;只写时长不写名称,也不知道播哪个动画。
延迟与状态控制:delay、fill-mode
为什么需要:多个元素做同一个动画时,如果同时开始会太整齐;给老二、老三加延迟,就能形成间隔感。
怎么想:animation-delay 控制“等多久再播”;fill-mode 控制动画结束或等待时的状态。实际中三个小绿块都做同一个上下跳动画,老大不延迟,老二延迟约 0.5 秒,老三延迟约 1 秒,于是出现依次跳动的效果。
怎么做:用 animation-delay 设置延迟;forwards 让动画结束后停在最后一帧,backwards 让延迟等待时固定在第一帧状态。
易错点:forwards 和 backwards 容易混:一个管结束后停在最后一帧,一个管等待时固定第一帧;这部分属于了解即可。
速度曲线与逐帧:linear、steps
为什么需要:动画默认速度不是匀速,课堂演示能感觉到“先快、后慢”;精灵动画则要求一格格切换,不能补得太细腻。
怎么想:linear 是匀速;steps(N) 是逐帧,让动画按 N 步跳过去。
怎么做:精灵动画先准备显示区域,盒子尺寸与一张精灵小图相同;再定义背景图位移动画,移动距离等于精灵图宽度;最后用 steps(N),N 与精灵小图个数相同。

css
div {
width: 140px;
height: 140px;
border: 1px solid #000;
background-image: url(./images/bg.png);
animation: run 1s steps(12) infinite;
}
@keyframes run {
from {
background-position: 0 0;
}
to {
background-position: -1680px 0;
}
}易错点:steps 的 N 不是随便写;移动距离要与精灵图宽度对应,否则会错位。
案例:走马灯与无缝原理
为什么需要:让一排图片持续移动,形成走马灯。
怎么想:用 ul 横向排列图片,整体位移动画;鼠标悬停暂停;结尾复制开头图片填补露白。无缝原理是复制开头图片到结尾位置,图片累加宽度等于区域宽度。
怎么做:

HTML 结构:
html
<li><img src="./images/1.jpg" alt="" /></li>
<li><img src="./images/2.jpg" alt="" /></li>
<li><img src="./images/3.jpg" alt="" /></li>
<li><img src="./images/4.jpg" alt="" /></li>
<li><img src="./images/5.jpg" alt="" /></li>
<li><img src="./images/6.jpg" alt="" /></li>
<li><img src="./images/7.jpg" alt="" /></li>
<!-- 替身:填补显示区域的露白 -->
<li><img src="./images/1.jpg" alt="" /></li>
<li><img src="./images/2.jpg" alt="" /></li>
<li><img src="./images/3.jpg" alt="" /></li>CSS 样式:
css
.box {
width: 600px;
height: 112px;
border: 5px solid #000;
margin: 100px auto;
overflow: hidden;
}
.box ul {
display: flex;
animation: move 6s infinite linear;
}
/* 定义位移动画;ul使用动画;鼠标悬停暂停动画 */
@keyframes move {
0% {
transform: translate(0);
}
100% {
transform: translate(-1400px);
}
}
.box:hover ul {
animation-play-state: paused;
}易错点:忘记 overflow: hidden 会露出多余图片;忘记复制开头图片,循环时容易露白;暂停要写 animation-play-state: paused。
多组动画
为什么需要:一个元素可能同时做位移、变色等多个动画。
怎么想:多组动画用逗号分隔,每组仍写名称、时长等。
怎么做:
css
animation:
动画一,
动画二,
... ...
;易错点:多组之间漏逗号;每组动画的名称和时长仍要写清楚。
课件图示



代码示例
示例一:3D 导航(立方体翻转)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>3D 导航</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { padding: 100px; background: #f2f2f2; }
.nav ul {
display: flex;
gap: 40px;
list-style: none;
}
.nav li {
position: relative;
width: 100px;
height: 40px;
line-height: 40px;
transition: all 0.5s;
/* 关键:让子级处于 3D 空间,而不是被压平 */
transform-style: preserve-3d;
/* 为了看到橙色和绿色的移动过程,给立方体添加旋转 */
/* transform: rotateX(-20deg) rotateY(30deg); */
}
.nav li a {
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 100%;
text-align: center;
text-decoration: none;
color: #fff;
}
/* 立方体每个面都有独立的坐标轴,互不影响 */
.nav li a:first-child {
background-color: green;
transform: translateZ(20px);
}
.nav li a:last-child {
background-color: orange;
transform: rotateX(90deg) translateZ(20px);
}
.nav li:hover {
transform: rotateX(-90deg);
}
</style>
</head>
<body>
<div class="nav">
<ul>
<li>
<a href="#">首页</a>
<a href="#">Home</a>
</li>
<li>
<a href="#">课程</a>
<a href="#">Course</a>
</li>
<li>
<a href="#">关于</a>
<a href="#">About</a>
</li>
</ul>
</div>
</body>
</html>关键行与易错点
transform-style: preserve-3d;必须加在li(父级)上,否则两个面会被"压平"到同一平面,悬停时看不到立方体效果。translateZ(20px)的 20px 是盒子高度 40px 的一半,两个面各推出去一半才能拼成立方体;写成 40px 就会分家。- 第二个面是
rotateX(90deg) translateZ(20px),顺序不能反:先旋转把坐标轴转过去,再沿新的 Z 轴平移;反过来写位置就完全不对。 :first-child/:last-child分别选中前后两个a,注意每个li里只能有这两个a,多一个就会错位。- 悬停是
rotateX(-90deg)(负值),根据左手法则,负角度才能让"上面"翻到"前面"。想调试过程可以把注释里那句rotateX(-20deg) rotateY(30deg)打开,能直观看到两个面的空间位置。
示例二:走马灯(无缝滚动 + 悬停暂停)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>走马灯</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.box {
width: 600px;
height: 112px;
border: 5px solid #000;
margin: 100px auto;
overflow: hidden; /* 只露出显示区域 */
}
.box ul {
display: flex;
list-style: none;
animation: move 6s infinite linear;
}
.box ul li { flex: 0 0 200px; height: 112px; }
.box ul li img {
display: block;
width: 200px;
height: 112px;
}
/* 定义位移动画;ul 使用动画;鼠标悬停暂停动画 */
@keyframes move {
0% {
transform: translate(0);
}
100% {
transform: translate(-1400px);
}
}
.box:hover ul {
animation-play-state: paused;
}
</style>
</head>
<body>
<div class="box">
<ul>
<li><img src="./images/1.jpg" alt="" /></li>
<li><img src="./images/2.jpg" alt="" /></li>
<li><img src="./images/3.jpg" alt="" /></li>
<li><img src="./images/4.jpg" alt="" /></li>
<li><img src="./images/5.jpg" alt="" /></li>
<li><img src="./images/6.jpg" alt="" /></li>
<li><img src="./images/7.jpg" alt="" /></li>
<!-- 替身:填补显示区域的露白 -->
<li><img src="./images/1.jpg" alt="" /></li>
<li><img src="./images/2.jpg" alt="" /></li>
<li><img src="./images/3.jpg" alt="" /></li>
</ul>
</div>
</body>
</html>关键行与易错点
- 无缝动画原理:把开头的图片复制一份放到末尾当"替身",图片累加宽度 = 显示区域宽度(600px 显示区,200px × 3 张替身正好补满)。
- 位移量
-1400px是 7 张原图的宽度(7 × 200),不是全部 10 张;算错就会出现"跳一下"的断裂感。 - 动画写在
ul上、overflow: hidden写在.box上,别搞反了;写在li上就各动各的。 linear匀速不能省,默认是慢快慢,滚动会一冲一停。- 图片必须给固定宽高(200 × 112),否则 flex 下图片按原尺寸撑开,位移量就对不上了。
- 悬停暂停用
animation-play-state: paused,注意是写在.box:hover ul上,状态是"父级悬停、子级暂停"。
示例三:精灵动画(steps 逐帧)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>精灵动画</title>
<style>
body { padding: 100px; }
div {
width: 140px;
height: 140px;
border: 1px solid #000;
background-image: url(./images/bg.png);
animation: run 1s steps(12) infinite;
}
@keyframes run {
from {
background-position: 0 0;
}
to {
background-position: -1680px 0;
}
}
</style>
</head>
<body>
<div></div>
</body>
</html>关键行与易错点
- 盒子尺寸必须和一张精灵小图尺寸相同(140 × 140),盒子大了会同时露出好几帧。
- 移动距离 = 精灵图总宽度 = 小图宽 × 小图个数 = 140 × 12 = 1680px,写成正数会反着跑,写少了会跳帧。
steps(12)里的 12 必须等于精灵小图个数,写成别的数字会漏帧或卡帧;steps()是逐帧(不补帧),和linear的平滑补帧完全不同。infinite让动画循环,否则只跑一遍就停在最后一帧的位置。- 背景图默认会平铺,如果精灵图有透明边或需要定位,记得配合
background-repeat: no-repeat使用。
常见错误
现象:给子元素写
transform: translateZ(50px)毫无反应,元素纹丝不动。 原因:屏幕是平面,Z 轴平移缺少透视参照;perspective没写,或错误地写在了子元素自己身上。 修法:perspective: 800~1200px加在父级(观察者到 Z=0 平面的距离),子元素自身不加。现象:
transform: rotate(45deg) translateX(100px)元素跑偏,方向与预期差 45°。 原因:复合 transform 是作用在坐标系上的,旋转写在前会把 X/Y 轴一起转掉,后面的位移方向随之改变。 修法:位移永远写在旋转/缩放之前;确实要独立控制时,拆成外层管位移、内层管旋转。现象:3D 导航的立方体怎么调都是一张纸片,橙色面始终不出现。 原因:
transform-style: preserve-3d加在了子元素a上,而不是容器li上;或容器链上有overflow: hidden把 3D 空间裁掉了。 修法:preserve-3d写在直接父级;做 3D 的容器不要加overflow: hidden。现象:
@keyframes定义好了,写上animation: move;却一动不动。 原因:动画名称和时长都是必填,缺时长默认0s,等于瞬间播完;也可能是@keyframes少写 s、名字拼错、from误写成form。 修法:animation: move 2s;两个值都给全,名字逐字核对。现象:精灵动画里小人不是"跑",而是整张精灵图平滑滑过去。 原因:没加
steps(),默认按补帧平滑过渡;或steps(N)的 N 与小图个数不符、位移量不等于精灵图总宽度。 修法:animation: run 1s steps(12) infinite;,N = 小图个数,to的background-position位移量 = 精灵图总宽度。现象:动画播完立刻弹回起点,最后一帧的效果留不住。 原因:默认不保留终态;配合
delay时又想让"等待期间就停在第一帧",两者混为一谈。 修法:留终态用animation-fill-mode: forwards;等待期就位第一帧用backwards。现象:走马灯循环到末尾会闪一下、露白。 原因:没有做替身,或位移距离按"单张宽"算而不是"已复制的原图总宽"。 修法:把开头几张图复制到末尾,位移量 = 单张宽 × 原图张数,位移距离与替身宽度严格对齐。
现象:想给同一元素加两个动画,写了两个
animation声明,只有后一个生效。 原因:animation是复合属性,后写的会整体覆盖前面的。 修法:一行内用逗号分隔:animation: 动画一 1s, 动画二 2s;,各自可带独立的时长、延迟、次数。
练习
3D 翻转卡片:一个容器内前后两张面,悬停时绕 Y 轴翻转 180° 露出背面。要求:父级加视距、
preserve-3d,两个面用backface-visibility: hidden互相遮挡,翻转有 0.6s 过渡。依次跳动:一排 4 个小球,共用同一段"上抛—回落"的三帧动画,用
animation-delay让它们依次起步形成波浪。验收:4 个球延迟值等差数列,整体节奏连贯不卡顿。无缝走马灯:8 张图横向排列,6s 匀速无限循环,鼠标悬停暂停。验收:循环衔接处看不出断点,
hover时停在当前帧而不是跳回起点。精灵跑动:给一张 12 帧的精灵图做循环跑动,
steps()步数与帧数一致。验收:小人脚不打滑、不出现半张图残影。综合小场景:做一屏"云彩 + 热气球",两者各用一段动画,速度不同、延迟不同,并让热气球在动画结束后停在终点位置。验收:两段动画互不覆盖,能同时运行。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回顾及今日目标 | 09:37 |
| 2 | 动画介绍-了解 | 03:12 |
| 3 | 初体验-1 | 13:03 |
| 4 | 初体验-2 | 05:35 |
| 5 | 属性-name及duration | 03:29 |
| 6 | 属性-delay | 09:35 |
| 7 | 属性-动画状态控制fill-mode | 07:44 |
| 8 | 属性-速度曲线steps | 07:16 |
小结
3D 转换是"看得懂就行"的知识点,后续课程不再承接;空间想象力弱的同学放弃细节不影响后续学习,但
perspective加父级、preserve-3d加父级这两条必须记牢。2D 的实际使用频率远高于 3D;
translate做居中(相对自身宽高自动计算)是必须掌握的一项,比 margin 负值方案更省心。动画与过渡的分水岭不在"能不能动",而在"状态数量"和"可控性"——只要需求里出现多状态、重复、延迟、暂停中的任意一个,就该用动画而不是过渡。
@keyframes是定义、animation是调用,两者解耦,所以定义写在调用之前或之后都能生效;真正决定能不能跑起来的是"名字 + 时长"这两个必填项。百分比关键帧比
from/to灵活得多,凡是需要三个及以上状态、或要精确卡某个时间点的,一律用百分比写。steps()是把"补帧"换成"逐帧",凡是素材本身是一格一格的(精灵图、数字翻牌、打字机),就该用它;反过来,凡是要顺滑的,绝不能加。复合属性
animation一行能写多个动画,但多个动画之间只能用逗号分隔在同一行,写两条声明等于自己覆盖自己。这些花哨动效在实际工作中落地机会不多——企业官网偏商务反而动画更少,趣味性强的项目也多半有前人写好的库。今天的目标是"会写、能改、看得懂",而不是把它当成核心竞争力。