背景
在为 ACMUSIC(网易云第三方客户端)的私人FM模块添加镜面倒影效果时,遇到了一个有趣的技术挑战:镜面倒影明明 DOM 结构正确、transform 也没问题,但就是不可见。这篇文章记录了排查过程和最终的解决方案。
1. 三层结构设计
倒影系统采用三层嵌套结构:
.fm-card-wrapper(3D 变换容器)
├─ .fm-card(封面卡片,340×340, border-radius 28px)
└─ .fm-reflection-plane(倒影裁剪平面)
└─ .fm-reflection(倒影元素,scaleY(-1) 翻转)
└─ img(封面图片,340×340)
关键设计思路:
- plane:position 在卡片正下方,
overflow: hidden+border-radius: 28px负责裁剪倒影可见区域和圆角 - reflection:position 在 plane 内部,通过
transform: scaleY(-1)实现垂直镜像 - img:复用封面图片,
object-fit: cover填充
2. 核心 CSS
2.1 Plane——裁剪平面
|
|
2.2 Reflection——镜像元素
|
|
3. 核心问题:浏览器 culling 导致倒影不可见
3.1 问题现象
DOM 结构正确,CSS transform 也正确,但倒影完全不可见。即使用纯红色背景(opacity:1, mask:none)替换图片,plane 区域内仍然看不到任何内容。
3.2 根因分析
问题出在浏览器的**渲染裁剪优化(culling)**机制上。
.reflection 元素在 plane 内部定位为 top: 340px,它的布局盒子(layout box)在 plane 坐标系中占据 [340, 680] 的范围。而 plane 的 overflow: hidden 裁剪区域是 [0, 410](410px 是 plane 的高度)。
|
|
浏览器在渲染前会做一次粗略裁剪判断:如果某个子元素的布局盒子与父元素的裁剪区域重叠不足一定阈值(约 20-40px),Chrome 会直接跳过该元素的渲染,认为它"完全不可见"。
问题就出在这里。虽然 scaleY(-1) 会把 reflection 的视觉内容翻转到 plane 裁剪区域 [0, 110],但浏览器在进行 culling 决策时只看布局盒子,不看 transform 的结果。
3.3 验证实验
通过 CDP 测试不同 top 值的渲染情况:
| refletion top | 布局盒在 plane 内重叠 | 是否渲染 |
|---|---|---|
| 340px | 0-40px | ❌ 被 culling |
| 60px | ~30px | ❌ 临界值 |
| 40px | ~40px | ✅ 可见 |
| 0px | 70px | ✅ 可见 |
实验证明:当 layout box 在父级 clip 区域内重叠超过阈值时,浏览器才会真正渲染;否则即使 transform 会把它移入可视区,也会被跳过。
3.4 解决方案
方案一:增加 plane 高度(曾尝试)
- 将 plane 高度从 70px 提升到 410px
- 布局重叠达到 70px,浏览器不再 culling
- 但 plane 太高,倒影延伸过长
方案二:clip-path 修剪(最终采用)
- plane 保持 410px 高度(满足 culling 重叠阈值)
- 使用
clip-path: inset(0 0 300px 0)将视觉显示区域修剪到 110px - clip-path 作用在渲染后期,不影响浏览器初始的 culling 判断
overflow: hidden负责布局裁剪判断(决定是否渲染)clip-path负责视觉裁剪(控制实际显示多少)
|
|
4. mask 渐隐方向陷阱
4.1 问题
在 scaleY(-1) + top: 340px 的几何配置下,plane 的可见窗口(110px)对应 reflection 元素的底部 110px(元素局部 y 坐标 [230, 340])。
如果使用 mask-image: linear-gradient(to bottom, ...):
- mask 的
to bottom方向:0% = 元素顶部.78,100% = 元素底部0 - 可见窗口在元素底部,mask 值 ≈
0→ 完全被 mask 掉
4.2 解决
使用 mask-image: linear-gradient(to top, ...):
- mask 的
to top方向:0% = 元素底部.78,100% = 元素顶部0 - 可见窗口在元素底部,mask 值从
.78开始 → 倒影可见且靠近卡片最清晰
|
|
4.3 断层问题
另一个细节:mask 梯度的 transparent 必须设置在 clip-path 边界之前。
clip-path 在 110px 处(梯度 32.4%)做硬切。如果此时 mask 还有残留值(如 .26),就会看到明显的「断层」——倒影突然消失。将 transparent 设在 35%,使 clip 边界处 mask 已衰减到 .017,切边几乎不可见。
5. 侧卡(3D 旋转卡)的特殊处理
previous/next 卡片有 rotateY(±48deg) 3D 变换,倒影需要额外处理:
- hover 动画:
translateZ(-90→-45)、rotateY(48→38)、scale(1→1.03) - 倒影增强:hover 时 opacity +30%,blur 降低到 0
- 宽度自适应:plane 和 reflection 使用
width: 100%继承 3D 变换后的视觉宽度 - position 补偿:rotateY 导致视觉 gap 与 current 不一致,需额外
+2px补偿
6. 总结
| 问题 | 根因 | 解决 |
|---|---|---|
| 倒影不可见 | 浏览器 culling 只看布局盒不看 transform | plane 增高 + clip-path 修剪 |
| mask 方向错误 | to bottom 使可见区域 mask≈0 |
改为 to top |
| 倒影底部断层 | mask 未在 clip 边界前归零 | transparent 设在 35%(边界 32.4%) |
| 侧卡 gap 不一致 | rotateY 透视投影 | 侧卡 plane 补偿 +2px |
这次开发让我深刻理解了一个经常被忽略的浏览器渲染行为:culling 基于布局盒而非视觉位置。在涉及 transform 将内容移入可视区域时,需要确保布局盒本身与裁剪区域有足够重叠,否则内容会被浏览器直接跳过渲染。
最终效果
|
|