Skip to content

空间转换与动画(一) ​

本页对应 空间转换与动画,共 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(角度);,角度正负要结合方向判断。

Z轴旋转示意图

X轴旋转示意图

Y轴旋转示意图

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

左手法则示意图

rotate3d 与立体呈现 ​

为什么需要:有时要自定义旋转轴;有时父元素旋转后,子元素会被“压平”,需要决定子级在 3D 空间还是平面中。

怎么想:rotate3d(x, y, z, 角度度数) 用来设置自定义旋转轴及角度,x、y、z 取 0—1 之间的数字;transform-style 控制子元素处于 3D 空间还是平面。

怎么做:父级写 transform-style: preserve-3d; 让子级处于 3D 空间;flat 表示子级处于平面中。

易错点:transform-style 要加在“父级”上;如果一直是默认 flat,子元素各自旋转后仍可能被压回平面,立方体感出不来。

案例:3D 导航 ​

为什么需要:把平移、旋转、立体呈现串起来,做出立方体导航。

怎么想:先搭立方体:绿色是立方体前面,橙色是立方体上面;再让鼠标悬停时立方体旋转。转写稿和代码注释都提醒,为了看到橙色和绿色的移动过程,可以临时给立方体加旋转观察。

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: 动画名称 动画花费时长;

animation复合属性

提示:

  • 动画名称和动画时长必须赋值
  • 取值不分先后顺序
  • 如果有两个时间值,第一个时间表示动画时长,第二个时间表示延迟时间

拆分写法如下:

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: 
  动画一,
  动画二,
  ... ...
;

易错点:多组之间漏逗号;每组动画的名称和时长仍要写清楚。

课件图示 ​

课件第6页:l transform: rotateZ(值); l transform: ro

课件第9页:作用:设置元素的子元素是位于 3D 空间中还是平面中 属性名:transform

课件第10页:l 呈现立体图形步骤 1. 父元素添加transform-style: pres

代码示例 ​

示例一: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 使用。

常见错误 ​

  1. 现象:给子元素写 transform: translateZ(50px) 毫无反应,元素纹丝不动。 原因:屏幕是平面,Z 轴平移缺少透视参照;perspective 没写,或错误地写在了子元素自己身上。 修法:perspective: 800~1200px 加在父级(观察者到 Z=0 平面的距离),子元素自身不加。

  2. 现象:transform: rotate(45deg) translateX(100px) 元素跑偏,方向与预期差 45°。 原因:复合 transform 是作用在坐标系上的,旋转写在前会把 X/Y 轴一起转掉,后面的位移方向随之改变。 修法:位移永远写在旋转/缩放之前;确实要独立控制时,拆成外层管位移、内层管旋转。

  3. 现象:3D 导航的立方体怎么调都是一张纸片,橙色面始终不出现。 原因:transform-style: preserve-3d 加在了子元素 a 上,而不是容器 li 上;或容器链上有 overflow: hidden 把 3D 空间裁掉了。 修法:preserve-3d 写在直接父级;做 3D 的容器不要加 overflow: hidden。

  4. 现象:@keyframes 定义好了,写上 animation: move; 却一动不动。 原因:动画名称和时长都是必填,缺时长默认 0s,等于瞬间播完;也可能是 @keyframes 少写 s、名字拼错、from 误写成 form。 修法:animation: move 2s; 两个值都给全,名字逐字核对。

  5. 现象:精灵动画里小人不是"跑",而是整张精灵图平滑滑过去。 原因:没加 steps(),默认按补帧平滑过渡;或 steps(N) 的 N 与小图个数不符、位移量不等于精灵图总宽度。 修法:animation: run 1s steps(12) infinite;,N = 小图个数,to 的 background-position 位移量 = 精灵图总宽度。

  6. 现象:动画播完立刻弹回起点,最后一帧的效果留不住。 原因:默认不保留终态;配合 delay 时又想让"等待期间就停在第一帧",两者混为一谈。 修法:留终态用 animation-fill-mode: forwards;等待期就位第一帧用 backwards。

  7. 现象:走马灯循环到末尾会闪一下、露白。 原因:没有做替身,或位移距离按"单张宽"算而不是"已复制的原图总宽"。 修法:把开头几张图复制到末尾,位移量 = 单张宽 × 原图张数,位移距离与替身宽度严格对齐。

  8. 现象:想给同一元素加两个动画,写了两个 animation 声明,只有后一个生效。 原因:animation 是复合属性,后写的会整体覆盖前面的。 修法:一行内用逗号分隔:animation: 动画一 1s, 动画二 2s;,各自可带独立的时长、延迟、次数。

练习 ​

  1. 3D 翻转卡片:一个容器内前后两张面,悬停时绕 Y 轴翻转 180° 露出背面。要求:父级加视距、preserve-3d,两个面用 backface-visibility: hidden 互相遮挡,翻转有 0.6s 过渡。

  2. 依次跳动:一排 4 个小球,共用同一段"上抛—回落"的三帧动画,用 animation-delay 让它们依次起步形成波浪。验收:4 个球延迟值等差数列,整体节奏连贯不卡顿。

  3. 无缝走马灯:8 张图横向排列,6s 匀速无限循环,鼠标悬停暂停。验收:循环衔接处看不出断点,hover 时停在当前帧而不是跳回起点。

  4. 精灵跑动:给一张 12 帧的精灵图做循环跑动,steps() 步数与帧数一致。验收:小人脚不打滑、不出现半张图残影。

  5. 综合小场景:做一屏"云彩 + 热气球",两者各用一段动画,速度不同、延迟不同,并让热气球在动画结束后停在终点位置。验收:两段动画互不覆盖,能同时运行。

原视频对照 ​

#视频时长
1回顾及今日目标09:37
2动画介绍-了解03:12
3初体验-113:03
4初体验-205:35
5属性-name及duration03:29
6属性-delay09:35
7属性-动画状态控制fill-mode07:44
8属性-速度曲线steps07:16

小结 ​

  1. 3D 转换是"看得懂就行"的知识点,后续课程不再承接;空间想象力弱的同学放弃细节不影响后续学习,但 perspective 加父级、preserve-3d 加父级这两条必须记牢。

  2. 2D 的实际使用频率远高于 3D;translate 做居中(相对自身宽高自动计算)是必须掌握的一项,比 margin 负值方案更省心。

  3. 动画与过渡的分水岭不在"能不能动",而在"状态数量"和"可控性"——只要需求里出现多状态、重复、延迟、暂停中的任意一个,就该用动画而不是过渡。

  4. @keyframes 是定义、animation 是调用,两者解耦,所以定义写在调用之前或之后都能生效;真正决定能不能跑起来的是"名字 + 时长"这两个必填项。

  5. 百分比关键帧比 from/to 灵活得多,凡是需要三个及以上状态、或要精确卡某个时间点的,一律用百分比写。

  6. steps() 是把"补帧"换成"逐帧",凡是素材本身是一格一格的(精灵图、数字翻牌、打字机),就该用它;反过来,凡是要顺滑的,绝不能加。

  7. 复合属性 animation 一行能写多个动画,但多个动画之间只能用逗号分隔在同一行,写两条声明等于自己覆盖自己。

  8. 这些花哨动效在实际工作中落地机会不多——企业官网偏商务反而动画更少,趣味性强的项目也多半有前人写好的库。今天的目标是"会写、能改、看得懂",而不是把它当成核心竞争力。