全部文章

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 用它把每个像素推一点点,最后补一次模糊——玻璃的「毛」和「歪」就都在了。

边界必须写死

这套做法只在三种条件下成立,缺一个就会露馅:

  1. 背景必须是固定的。 壁纸贴了 position: fixed 才谈得上「复制一份并对齐」。滚动内容不行。
  2. 窗口必须在壁纸之上、且不压别的内容。 副本里只有壁纸,压着图片或文字就会穿帮。
  3. 位移场不能动。 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]

脚注

  1. 顺带一提:background-attachment: fixed 在 iOS Safari 上有历史遗留的定位差异,所以我给移动端直接关掉了折射,只留模糊——反正屏幕小,看不出区别。 ↩︎

回到顶部