3D 变换与透视:翻牌与倾斜
把元素从二维平面抬进三维空间,只需三件 CSS 设施:父级的 perspective 给出观察者到屏幕的距离,rotateX 与 rotateY 让元素绕水平或垂直轴转动,transform-style: preserve-3d 让子元素真正活在同一个 3D 坐标系里而非被压扁回平面。
1 · 翻牌
绕 Y 轴转 180° 露出背面。背面预先旋了 180°,配合 backface-visibility: hidden——翻转过半时正面转开、背面转入,正反切换自然发生,不需要在代码里判断「该显示哪一面」。
perspective 是观察者眼睛到屏幕平面的距离:值小意味着离得近,近大远小的透视畸变强烈;值大趋近正交投影,几乎看不出立体。
注 · 高光用 描述: 或 时卡片正对屏幕,高光为 0;转到 侧面时最强。这不是物理光照模型,而是「越侧对越反光」这个直觉的最简表达——正好也是卡片在这个角度最窄、最需要一点东西提示它仍在转动的时刻。
2 · preserve-3d 这个开关
默认的 transform-style: flat 会把子元素先渲染成 2D 图像再贴到父级平面,子元素的 Z 坐标与自身旋转都被抹平。设为 preserve-3d 后子元素才保留各自的 3D 位置——翻牌的正反两面、倾斜卡片上凸起的 chip 与 badge 才能落在不同深度。
警示 · preserve-3d 与 overflow: hidden 互斥:设了非 visible 的 overflow,浏览器会强制把 transform-style 回退成 flat。倾斜卡片上那层 .gloss 靠
overflow: hidden 裁到圆角内,而卡片本身又需要 preserve-3d 让 badge 浮起来——两者能共存是因为两个属性挂在同一元素上时规范规定 overflow 优先,实际是 .gloss 被裁而 translateZ 仍生效(Chromium 的实现如此)。真要两者都严格保证,得把裁剪层与 3D
层拆成两级。
3 · 倾斜跟随
倾斜把光标的纵向偏移映射到 rotateX、横向偏移映射到 rotateY。光标每动一次就以当前值与速度为初始条件重建一对弹簧,移开时目标归零自动回正——与手势跟随用的是同一套接管。
import { createSpring, springEasing, Animation } from '@vega/anim';
/* 翻牌: 弹簧化成 easing + duration, 驱动 rotateY 0deg → 180deg */
const { easing, duration } = springEasing({ stiffness: 200, damping: 18 });
new Animation({
keyframes: [0, 180], duration, ease: easing,
onUpdate: (deg) => { card.style.transform = `rotateY(${deg}deg)`; },
});
// 背面预旋 180deg + backface-visibility: hidden, 翻过半自动换面
/* 倾斜: rAF 持续采样, 光标移动时用当前 value/velocity 重建弹簧接住运动 */
let sx = createSpring({ stiffness: 220, damping: 22, from: 0, to: 0 }), t0 = now();
function retarget(nx) {
const el = now() - t0, v = sx.velocity(el), cur = sx.value(el);
sx = createSpring({ stiffness: 220, damping: 22, from: cur, to: nx, velocity: v });
t0 = now();
}
翻牌的回弹质感来自把弹簧化成的缓动,见弹簧;倾斜的「平滑跟随并自然回正」与手势、中断与重定向同源。springEasing 会丢掉初速度这一点见弹簧的警示——翻牌总是从静止起步,故不受影响。