CSS 2D/3D 变换

深入理解 CSS transform 的 2D 和 3D 函数、perspective 的视觉效果、transform-style 的 preserve-3d 和 flat、backface-visibility 的 3D 翻转卡片,以及 will-change 的使用和滥用问题。

CSS 2D/3D 变换

为什么 transform 是 CSS 动画的黄金属性

在前端性能优化领域,有一个广泛认可的原则:动画应该只改变 transformopacity。这个建议的背后原因是这两个属性的变化不会触发布局重计算,只在合成阶段处理,因此性能极高。

transform 是 CSS 中最强大的属性之一。它不仅能做位移、旋转、缩放、倾斜,还能创建 3D 效果。但 3D 变换也是最容易被误解和误用的 CSS 特性之一。transform-style: preserve-3dflat 的区别、perspective 的作用位置、backface-visibility 的实际行为——这些细节决定了你的 3D 效果是炫酷还是翻车。

面试定位:transform 在面试中主要考察对 3D 变换机制的理解。perspective 和 transform-style 的关系、backface-visibility 的实际效果、以及为什么 transform 适合做动画都是常见问题。


2D 变换函数

transform 的基本语法

.box {
  transform: translate(100px, 50px);     /* 位移 */
  transform: scale(1.5, 0.8);           /* 缩放 */
  transform: rotate(45deg);             /* 旋转 */
  transform: skew(10deg, -5deg);        /* 倾斜 */

  /* 组合变换:按顺序执行 */
  transform: translate(100px, 50px) rotate(45deg) scale(1.5);
}

translate —— 位移

.box {
  transform: translate(100px);           /* 仅 X 轴 */
  transform: translate(100px, 50px);    /* X 和 Y 轴 */
  transform: translateX(100px);         /* 仅 X 轴 */
  transform: translateY(50px);          /* 仅 Y 轴 */
  transform: translate3d(0, 0, 100px);  /* 3D 位移 */
}

百分比位移:相对于元素自身尺寸

.box {
  width: 200px;
  transform: translateX(50%); /* 向右移动 100px(200px * 50%) */
}

scale —— 缩放

.box {
  transform: scale(2);            /* X 和 Y 都放大 2 倍 */
  transform: scale(2, 1);         /* X 放大 2 倍,Y 保持 */
  transform: scaleX(0.5);         /* X 缩小一半 */
  transform: scaleY(1.5);         /* Y 放大 1.5 倍 */
  transform: scale3d(1, 2, 3);    /* 3D 缩放 */
}

/* 负值:镜像翻转 */
.flip {
  transform: scaleX(-1); /* 水平镜像 */
}

rotate —— 旋转

.box {
  transform: rotate(45deg);         /* 顺时针旋转 45 度 */
  transform: rotate(-90deg);       /* 逆时针旋转 90 度 */
  transform: rotateX(45deg);        /* 绕 X 轴旋转(3D) */
  transform: rotateY(45deg);        /* 绕 Y 轴旋转(3D) */
  transform: rotateZ(45deg);        /* 绕 Z 轴旋转(2D 效果同 rotate) */
  transform: rotate3d(1, 1, 1, 45deg); /* 绕自定义轴旋转 */
}

skew —— 倾斜

.box {
  transform: skew(10deg);           /* X 轴倾斜 10 度 */
  transform: skew(10deg, 5deg);      /* X 和 Y 轴分别倾斜 */
  transform: skewX(10deg);           /* 仅 X 轴 */
  transform: skewY(10deg);           /* 仅 Y 轴 */
}

3D 变换函数

perspective —— 透视效果

perspective 定义观察者与 z=0 平面的距离,产生近大远小的透视效果:

.scene {
  perspective: 500px; /* 观察者距离屏幕 500px */
}

.box {
  transform: translateZ(100px); /* 物体移近 100px,看起来更大 */
}

透视消失点perspective-origin 定义消失点的位置:

.scene {
  perspective: 500px;
  perspective-origin: center center;     /* 默认:中心 */
  perspective-origin: top left;          /* 左上角 */
  perspective-origin: 100% 100%;         /* 右下角 */
}

transform-style —— 3D 空间模式

/* preserve-3d:子元素保持 3D 空间位置 */
.parent-3d {
  transform-style: preserve-3d;
}

/* flat:子元素被扁平化到父元素平面 */
.parent-flat {
  transform-style: flat;
}

实际效果对比

.scene {
  perspective: 500px;
}

.card {
  transform: rotateY(45deg);
}

.card-front,
.card-back {
  position: absolute;
  backface-visibility: hidden;
}

.card-back {
  transform: rotateY(180deg); /* 初始时翻转朝后 */
}

/* 如果父元素没有 preserve-3d:
 * card-front 和 card-back 会被扁平化到同一平面
 * rotateY(180deg) 的背面会显示在正面"里面" */

.scene-3d {
  transform-style: preserve-3d;
}
/* 有了 preserve-3d:子元素在 3D 空间中正确排列 */

backface-visibility —— 背面可见性

.card {
  backface-visibility: visible;  /* 背面可见 */
  backface-visibility: hidden;   /* 背面隐藏(常用) */
}

/* 3D 翻转卡片 */
.flip-card {
  position: relative;
  transform-style: preserve-3d;
}

.flip-card-front,
.flip-card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
}

.flip-card-front {
  /* 正面 */
}

.flip-card-back {
  transform: rotateY(180deg); /* 初始时翻转朝后 */
}

.flip-card:hover {
  transform: rotateY(180deg); /* hover 时翻转 */
}

变换的顺序与原点

transform-origin —— 变换原点

默认变换原点是元素中心,但可以自定义:

.box {
  transform-origin: center center;       /* 默认:中心 */
  transform-origin: top left;           /* 左上角 */
  transform-origin: 50px 50px;          /* 具体坐标 */
  transform-origin: 100% 100%;         /* 右下角 */
  transform-origin: bottom center;       /* 底部中心 */
}

/* 围绕右下角旋转 */
.rotate-corner {
  transform-origin: right bottom;
  transform: rotate(45deg);
}

变换顺序的影响

/* 顺序不同,结果不同 */
.box1 {
  transform: translate(100px, 0) rotate(90deg);
  /* 先向右移动 100px,再绕新位置的中心旋转 */
}

.box2 {
  transform: rotate(90deg) translate(100px, 0);
  /* 先旋转,再向当前 right 方向(原来的下方)移动 100px */
}

will-change 与性能

will-change 的作用

will-change 提示浏览器提前为某些属性变化做准备:

.box {
  will-change: transform;
  /* 浏览器会:
   * 1. 为这个元素创建一个新的层叠上下文
   * 2. 将这个元素的渲染提升到新的 Layer
   * 3. 提前准备好变换矩阵计算
   */
}

滥用 will-change 的问题

/* 滥用示例 */
.bad {
  will-change: transform, opacity, left, top, width, height;
  /* 问题:
   * 1. 创建太多 Layer,消耗 GPU 内存
   * 2. 每个 Layer 都需要独立管理
   * 3. 可能导致层爆炸(Layer explosion)
   */
}

正确使用模式

/* 模式一:动画开始前声明 */
.animated-box {
  will-change: transform;
}

.animated-box:hover {
  transform: scale(1.1);
}

/* 模式二:JavaScript 控制的动画 */
element.style.willChange = 'transform';
// 动画开始
element.style.transform = 'translateX(100px)';
// 动画结束
element.addEventListener('transitionend', () => {
  element.style.willChange = 'auto';
});

实战场景

场景一:3D 翻转卡片

.flip-card {
  width: 200px;
  height: 260px;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 0.6s;
  cursor: pointer;
}

.flip-card-front,
.flip-card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.flip-card-front {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

.flip-card-back {
  background: linear-gradient(135deg, #f093fb, #f5576c);
  color: white;
  transform: rotateY(180deg);
}

.flip-card:hover {
  transform: rotateY(180deg);
}

场景二:透视卡片堆叠

.card-stack {
  perspective: 1000px;
}

.card {
  transform-style: preserve-3d;
  transition: transform 0.3s ease;
}

.card:nth-child(1) { transform: translateZ(0); }
.card:nth-child(2) { transform: translateZ(-20px); }
.card:nth-child(3) { transform: translateZ(-40px); }

.card-stack:hover .card:nth-child(1) { transform: translateZ(20px) rotateY(10deg); }
.card-stack:hover .card:nth-child(2) { transform: translateZ(0) rotateY(5deg); }
.card-stack:hover .card:nth-child(3) { transform: translateZ(-20px); }

场景三:按钮点击效果

.btn-press {
  transition: transform 100ms ease;
  will-change: transform;
}

.btn-press:active {
  transform: scale(0.95);
  /* 性能好,因为只触发 Composite */
}

面试高频问题

Q: transform-style: preserve-3d 和 flat 有什么区别?

回答要点preserve-3d 保持子元素的 3D 位置,子元素可以出现在父元素的"前面"或"后面"。flat 将子元素扁平化到父元素所在平面,子元素的 3D 变换会被投影到 2D 平面。对于需要多层叠加的 3D 效果,必须使用 preserve-3d

Q: perspective 应该设置在哪个元素上?

回答要点perspective 应该设置在包含 3D 变换子元素的父容器上,而不是直接应用 3D 变换的元素上。perspective 创造的是一个"观察者视角",决定了子元素 3D 变换的透视效果。

Q: 为什么说 transform 和 opacity 适合做动画?

回答要点:因为这两个属性的变化只触发 Composite(合成)阶段,不触发布局(Layout)和绘制(Paint)阶段。浏览器可以将这些动画提升到独立的 Layer(层),使用 GPU 进行合成,效率极高。

Q: backface-visibility: hidden 的实际行为是什么?

回答要点:当元素背面朝向观察者时(通过 rotateY 180deg 或 rotateX 180deg 等),该面不可见。这常用于 3D 翻转卡片,正面和背面各占一面,背面初始时翻转朝后,hover 时翻转朝前。


延展阅读