FIELD NOTE
给静态博客做液态玻璃:换了三次方案,最后留下两条路线
记录本站液态玻璃从 DOM 垫底板、SVG 位移滤镜到 WebGL 折射的三次方案更替,以及超长面板、嵌套卡片和 Firefox 降级这些具体边界。
正文宽度
这个博客底栏右端有个「实验室」入口,里面挂着几个还没定稿的外观开关,其中一个叫「液态玻璃」。它默认关闭,打开后页面上的圆角面板会变成一块会折射背景的玻璃。
这篇文章记录它是怎么变成现在这样的。前后换过三次实现方案,中间有一次是因为面板上出现了一条本不该有的对角线,才发现问题。如果你也想在静态站点上加类似效果,下面这些约束和数字大概能省你一些时间。
先把边界划出来:玻璃不能接管页面
动手之前有一条线要先划:玻璃只是外观,页面本身仍然是一份普通的 HTML 文档。
液态玻璃的现成实现里,有一类要求把整个界面描述成一份画布元素列表——文字、按钮、滚动都在它自己的 canvas 里绘制。照那条路走,本站的 MDX 正文、目录、评论和搜索都得重写,还会丢掉搜索引擎能读到的文本、浏览器的页面内查找,以及文字的选中和复制。
本站选的是另一条路:只借渲染器,把它当成铺在内容下面的一层玻璃画布。
canvas(position: fixed,铺在内容下面)
└ 画「背景图 + 每块面板位置上的玻璃」
DOM 层(正常文档流)
└ 面板半透明,文字、图片、链接仍是活的 DOM,压在玻璃上渲染器会把背景图画进画布,所以玻璃铺好之后,body 上的 CSS 背景图要收起来,否则同一张图会叠两层。这个收起的动作挂在渲染成功之后:拿不到 WebGL 时画布上没有壁纸,这时候把 CSS 背景图藏掉,整页就只剩白底。
第一版:给每块面板垫一块底板
第一版用的是 @ybouane/liquidglass(MIT),它有个硬性要求:玻璃元素必须是根元素的直接子元素。
本站的面板是层层嵌套的——正文面板里有代码块、卡片和评论区,满足不了这个条件。当时的做法是给每块面板在同位置垫一块同尺寸的玻璃底板,再把面板自己的底色转成透明,文字仍旧留在原来的 DOM 里。为了底板对得齐,面板只能改颜色,不能动边框宽度和内边距,尺寸一变底板就会错位。
三个坑是照着现象一个个填的:
- 底板要加
isolation: isolate,否则注入的 canvas 带z-index: -1,会被背景图整个盖住。 - 倒角半径要按面板厚度缩放,薄条上两条倒角会撞在一起,接缝处看得见。
- 背景图用的是
background-attachment: fixed,滚动时底板相对背景在移动,库自己发现不了,只能主动重画,顺带做了滚动节流和视口剔除。
这一版能看出玻璃的样子,但观感和性能都不好:滚动会掉帧,玻璃块一多还得按屏幕预算往下砍。实验的说明里直接把「不保证体验和性能」写了进去。
第二版:用 SVG 位移滤镜,交给合成器
第二版换了条完全不同的路:不用画布,也不碰 WebGL,用一个 SVG 位移滤镜做折射。
先用 canvas 生成一张位移贴图。圆角矩形内部按有向距离场算「离边缘多近」,只有最外圈一条折射带把采样点往内偏折,中段是中性灰、不动。贴图交给 SVG 滤镜的 feImage,再让 feDisplacementMap 按贴图的红绿通道偏移背景。面板自己的 backdrop-filter 引用这个滤镜:
backdrop-filter: url(#lab-glass-cheap-0) blur(0.25px) contrast(1.2) saturate(1.1);这条路比第一版轻很多。没有根元素的约束,滤镜可以直接贴在面板上;位移由浏览器合成器执行,滚动、换页和动画都不用跑 JavaScript,也没有 GPU 上下文数量和帧缓冲的限制。
代价是它只有边缘折射。色散、边缘高光、厚度感都没有,观感和 WebGL 那套差一截——这句判断直接写进了实验室页面,和另一条路线并排显示。
这里还试过一次「把背景真正模糊起来」:把模糊半径换成亚克力那档的 14px。结果边缘那圈折射跟着一起糊掉,剩下的只是一块磨砂板,于是退回接近原版的 0.25px。
第三版:把 WebGL 渲染器搬进仓库
第三版换成了 liquid-glass-webgl 的 WebGL 渲染器。它是对 Kyant0/AndroidLiquidGlass 的 Web 移植,核心是更平滑的连续曲率圆角、可分离高斯与 Kawase 模糊,以及边缘把 RGB 分开折射的色差。这些是前两版都没有的厚度和层次。
搬进来的只有它的渲染器和 shader 两个目录,而且定下「不改动、只标注」:它的命名、注释语言和本站完全不一样,改掉之后就没法和上游对比、升级。ESLint 排除了这个目录,类型检查仍然覆盖它。上游发布时带着 56 个类型错误、靠 ignoreBuildErrors 关掉了类型检查,这里逐条修掉并记了清单。
接线上只实例化它的渲染器,不用它自带的宿主组件。元素用文档坐标,渲染器内部按 y = rect.y - scrollY 换算成视口坐标,页面滚动仍由浏览器负责;开发期还把渲染器挂到 window 上,方便验证滚动同步。
这个改动还有一个不轻的代价。liquid-glass-webgl 是 AGPL-3.0,按协议,通过网络提供服务时要向使用者提供完整源码,而这个义务覆盖整个服务,不能只标注引入的那一个组件。所以从这一版起,整站源码改成了 AGPL-3.0 发布。
两条路线怎么分工
WebGL 那套不是到处都能用,现在按面板分成几类:
| 面板 | 走哪条 | 原因 |
|---|---|---|
| 常规面板 | WebGL 画布 | 折射、厚度和色差只有这条有 |
| 正文面板 | 便宜版 SVG 滤镜 | 它可能高到几千甚至几万像素,WebGL 的几何会退化 |
| 嵌在玻璃里的卡片 | 便宜版 SVG 滤镜 | 画布在最底层,走 WebGL 会被父面板的薄纱压住 |
| 顶栏、底栏 | 保持原本的毛玻璃 | 一个图层没法同时待在正文下面、又盖住正文 |
| 短边小于 24px 的碎块 | 不上玻璃 | 分隔条这类元素根本画不出内容 |
「嵌在玻璃里的卡片」指的是文章末尾的上一篇/下一篇,以及评论区的每条留言。它们必须换路线,理由有一个具体数字:同一个位置,父面板的薄纱在时,卡片内部平均亮度是 33.2,把父面板去掉之后是 44.1,被压掉了大约四分之一。原因在层级——画布固定在最底层(z-index: -1),DOM 里的一切都压在它上面,包括父面板自己那层半透明底。便宜版是面板自己的 backdrop-filter,天然画在父面板之上,层级才是对的。
顶栏和底栏不参与,有两层原因,任何一层单独都够。第一是层级:画布铺在正文下面,正文才能压在玻璃上;而吸顶的顶栏要盖住滚上来的内容,必须画在正文之上,一个图层做不到两件事。第二是不划算:顶栏只有 57px 高,底栏 76px,本来就带 blur(14px) 的毛玻璃,为这两条窄边再养一层渲染器不值。
判断「谁不参与」用的是「在不在 <main> 里」,而不是标签名。一开始按标签名排除 <header>,结果页面的标题区也是 <header>,它们本该和别的面板一样有玻璃,却一直没有。
面板被画成三角形的那次
超长面板的问题,是在用户环境的 dpr 1.5 下才暴露出来的。
渲染器给每块玻璃生成一张方形遮罩纹理,把整个元素归一化到 texSize × texSize 的图里,圆角在这张图里能占到的像素数是 radius × (texSize / 元素最长边)。一块 832×10080 的正文面板,圆角只占 0.71 个像素。
| 面板尺寸 | 圆角在遮罩里占的像素 | 结果 |
|---|---|---|
| 832×31896 | 0.2 | 圆角被量化成直角 |
| 832×10080 | 0.71 | 整块玻璃画成三角形,面板上横着一条对角边 |
| 832×6381 | 1.12 | 正常 |
门槛原来定在 0.5,只考虑过「圆角变方」。三角形那个更难看的表现落在 0.5 之上,所以没被拦住。现在门槛提到 1:圆角连一个纹素都占不到,这套几何就不该再用,交给便宜版——它用元素像素算距离,没有这个问题。
这个现象只在 dpr 1.5 下出现,本地无头浏览器一直是 dpr 1,怎么都复现不出来。最后是按用户的 dpr 加生产构建(静态导出,也就是公网跑的那份)才确认的。
滚动、画质和降级
玻璃每帧的开销正比于画布像素数乘模糊开销,而滚动恰恰是最在意帧率、最不在意画质的时候。所以连续变化超过 80ms 就切到低画质,停手 320ms 再切回来。低画质是按比例降 dpr(0.65 倍)再加模糊降采样 2 倍,没有写死成 1——写死 1 在 dpr 1 的屏幕上等于没降,档位形同虚设。
滚动同步有一处延迟绕不开:正文由合成器线程滚动,画布必须回主线程重画,主线程一忙就会落在后面,玻璃会比正文慢一两帧。最早是在 scroll 事件里同步偏移,那会慢到两帧,现在改成常驻 rAF 每帧读一次滚动量,只慢一帧。
降级统一朝「看起来还是一块正常的板子」这个方向走:
- 拿不到 WebGL 上下文,或者壁纸解码失败:什么都不做,面板保持原本的亚克力外观。
- Firefox 不支持
backdrop-filter里的url():正文和卡片退回亚克力,实验室那一行会写明原因。
第二道判断要多说一句。CSS.supports 只看语法,而 Firefox 的 backdrop-filter 语法里恰好收 url(),却不会执行 SVG 滤镜——它能返回 true,实际却不生效。所以除了语法检测,还得补一道引擎判断。这里是少见的按引擎判断合理的场合,因为差别就在实现有没有做。
现在的样子
写这篇文章时,实验室里这个开关默认还是关的。已知的问题照实写:玻璃跟不上滚动和动画,会滞后一两帧;超长正文走的是另一套便宜实现;老设备上会掉帧。
留着的理由是它值得先占住这个位置。观感和性能都还不够好,但两条路线的边界已经摸清楚了——哪类面板适合实时折射、哪类只能交给合成器、从什么尺寸开始退化。这些结论换实现也成立,等以后再换更省的做法时可以接着往下走。
想自己看看的话,底栏右端那个「实验室」入口,打开「液态玻璃」就能试,关掉立刻回到默认样子。
DISCUSSION
留言
正在加载留言…