
3 分钟#css #tooling
在浏览器里做「真折射」
backdrop-filter 的十个函数没有一个能位移像素。想在网页上做真的折射,只能把壁纸复制一层、用 feDisplacementMap 去推它——而且只在固定背景上成立。
backdrop-filter 很能干:模糊、亮度、对比度、饱和度、色相……但你把它支持的函数表从头看到尾,会发现没有一个能位移像素。
位移不了,就折射不了。这是浏览器给玻璃效果划的硬边界。
那就自己造一层
既然背景是固定的,我可以把它复制到窗口里,然后对这份副本做位移:
.wallpaper,
[data-surface="glass"] .window__backdrop {
background-image: var(--wallpaper-img), var(--wallpaper-image);
background-size: cover, auto;
background-position: center, center;
background-attachment: fixed, fixed;
}
关键在于 background-attachment: fixed:它让背景的定位基准变成视口,于是窗口里的副本和窗口背后的壁纸像素级对齐。
对齐之后再加位移:
<filter id="lg-refract" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.009 0.017" numOctaves="2" seed="11"/>
<feGaussianBlur in="noise" stdDeviation="1.1" result="field"/>
<feDisplacementMap in="SourceGraphic" in2="field" scale="18"
xChannelSelector="R" yChannelSelector="G"/>
<feGaussianBlur in="displaced" stdDeviation="7"/>
</filter>
feTurbulence 生成一张平滑的噪声场,feDisplacementMap 用它把每个像素推一点点,最后补一次模糊——玻璃的「毛」和「歪」就都在了。
边界必须写死
这套做法只在三种条件下成立,缺一个就会露馅:
- 背景必须是固定的。 壁纸贴了
position: fixed才谈得上「复制一份并对齐」。滚动内容不行。 - 窗口必须在壁纸之上、且不压别的内容。 副本里只有壁纸,压着图片或文字就会穿帮。
- 位移场不能动。
feTurbulence是 CPU 渲染的,每帧重算等于自杀。
所以我的实现里只有首页 Hero 和置顶卡用真折射,其余窗口用「同色层 + 高光边」——远看是玻璃,滚动不掉帧。
滚动时降级
这是设计里最舒服的一处取舍:
[data-surface="glass"].is-scrolling .window[data-mode="refract"] .window__backdrop {
filter: blur(26px) saturate(150%);
}
滚动中,位移场换成纯模糊(便宜),停手 140ms 后再切回折射。
视觉上像是玻璃「重新对焦」——而实际上,是浏览器替我做了它最擅长的事。
反射交给 Worker
折射是静态的,反射不是:光斑要跟着指针跑,还要随速度变亮。
这部分数学不重,但我还是塞进了 Web Worker:
- 主线程只做一件事:
pointermove时把坐标发给 Worker; - Worker 算出每扇窗的光斑位置(
--sx/--sy)与强度(--li); - 主线程收到结果,写三个 CSS 变量。
el.style.setProperty('--sx', data[i * 3].toFixed(1) + '%');
el.style.setProperty('--sy', data[i * 3 + 1].toFixed(1) + '%');
el.style.setProperty('--li', data[i * 3 + 2].toFixed(3));
这样滚动、移动指针时,主线程上没有任何逐帧计算——只有三次赋值。
想验证真的在折射
打开首页,滚动一下让 Hero 变成窗口,然后看窗口边缘:壁纸的斜向光带会在窗内被「推歪」一点点。那就是位移场在干活。
一句话总结
网页上的「真折射」不是魔法,是复制 + 对齐 + 位移三件事,前提是你得接受它只对固定背景成立。
接受这个前提之后,剩下的都是工程问题。[1]
脚注
顺带一提:
background-attachment: fixed在 iOS Safari 上有历史遗留的定位差异,所以我给移动端直接关掉了折射,只留模糊——反正屏幕小,看不出区别。 ↩︎