动机
等高线背景(上一篇)用了一阵子,想换个气质。SPlayer-Next 这个播放器的界面一直让我印象很深:专辑封面铺开当背景,大片模糊,卡片浮在毛玻璃上。博客没有封面可以借,那就自己生成一张。
这次的需求清单:
- 每次访问随机,窗口缩放时保持同一张图(沿用种子方案)
- 色调限定深灰、藏青,气质要沉
- 正文对比度有硬指标:文字落在画布上最差也要过 AA(4.5:1)
- 亮暗主题都要成立,切换时背景整幅跟着换
- 光标要有反馈,但要克制
- 依旧零依赖、单文件
方案:自己生成一张「封面」
播放器的模糊背景 = 封面大图 + blur。博客没有现成大图,逐像素生成一整屏再模糊又太重。实际用的是一条廉价的「模糊链」:
低分辨率 canvas(视口的 1/4)→ CSS 拉伸放大 → filter: blur(48px) → 固定底层
↑ 上采样自带柔焦 ↑ 抹平最后的痕迹
| 做法 | 像素量 | 模糊代价 |
|---|---|---|
| 逐像素生成整屏 + 模糊 | ~130 万 | 每帧全屏运算 |
| 1/4 分辨率生成 + 拉伸 + blur | ~8 万 | 低一个数量级 |
拉伸放大本身就是一次免费的模糊,云雾感反而比精确计算更好。
生成:种子 → 噪声 → 光斑
随机部分原封不动搬自等高线那篇:mulberry32 种子随机 + simplex 噪声,Date.now() ^ Math.random() 取种子,缩放重绘不换图。
先离线生成一张静态「底纹」:每个像素上采 4 层 fBm,存进 Float32Array,之后每帧只查表不再重算。然后铺 7 团光斑,圆心、半径、摆动幅度、相位全部随机,每团再抽一个 0~1 的「色相倾向」:
| |
绘制时每像素对 7 团光斑做二次曲线衰减的加权平均,颜色在「灰底 ↔ 藏青」之间按各团倾向调和,再用静态噪声场调制亮度,叠出云絮纹理:
| |
亮色模式下调制幅度几乎减半(0.35):亮色下正文是深色字、直接压在画布上,云不能太深。
最后把「边缘压暗」直接画进像素里——从中心向外线性混向深色,给玻璃卡留出高对比区域,省掉一层独立的遮罩 DOM。
光标跟随:目标点 + 帧率无关缓动
pointermove 只更新目标点,真正绘制放在 rAF 里做指数趋近:
| |
三段行为:鼠标在窗口内,光斑向光标方向缓慢牵引,叠一层贴着光标走的柔光晕,玻璃卡背后有淡淡的蓝光流动;鼠标离开窗口,回到一个很慢的自动漂移(两个不同频率的正弦叠加);prefers-reduced-motion 只关掉自动漂移和初始漂移这类自发动画,用户自己移动光标触发的跟随保留——不少桌面环境关掉系统动画后这个媒体查询就是 reduce,全关掉的话效果会完全消失。
一个值得记下来的坑:第一版的首次绘制依赖 rAF 循环,而循环要等第一次 pointermove 才启动——触屏设备和没有鼠标事件的浏览器里,背景就是一张透明图。修法是 init 里无条件先画一帧,动画才交给循环。
主题切换:监听 data-theme
PaperMod 切主题只是改 html[data-theme],CSS 变量立即生效,但烤进 canvas 里的颜色不会自己变。补一个观察器:
| |
这里有个真实的翻车记录:观察器最初挂在动效初始化的后面,而 prefers-reduced-motion 的用户会提前 return——结果开了「减少动效」的人一换主题,面板全变了、背景纹丝不动。教训是:无论动效偏好如何,「背景跟随主题」这件事都必须先挂上。现在主题观察器和 resize 监听都在动效分支之前。
玻璃层与降级链
CSS 端定义两套变量(:root 默认暗色,:root[data-theme="light"] 覆盖),玻璃容器三件套:半透明底 + backdrop-filter: blur() + 内外阴影。
玻璃只给该给的东西:首页列表卡、顶栏、页脚、目录卡、标签 chip、上下篇导航;文章正文、归档、标签、搜索页保持透明,直接浮在噪声场上。范围用一条 :has() 收窄——首页 main 的特征是有一个 .first-entry:
| |
对比度用数字验收:对生成结果做像素采样,取最差情况下的对比度——暗色约 6:1,亮色约 5.4:1,都过了 AA。
降级链三层兜底:
| 场景 | 处理 |
|---|---|
浏览器不支持 backdrop-filter | 底色不透明度提到 0.94 |
| 用户偏好减少透明度 | 同上,并关掉 blur |
| 打印 | 玻璃全部还原成纸面 |
成本
- 代码量:单文件约 355 行,压缩后 5KB,零依赖
- 首次生成:1440×900 视口下噪声场约 16ms,之后每帧光斑循环约 3ms,动画无压力
- 触屏设备没有指针事件,永远只有一帧静态图,几乎零开销
- 无网络请求,构建产物只多一个 JS 文件
小结
和等高线那篇用的是同一套噪声工具箱(mulberry32 + simplex + fBm),只是渲染方式换了:等高线是「从场里抽线」,这次是「把场直接画出来」。生成艺术的底层数学就那几个,变的只是最后一步的呈现。
本文背景的完整实现是 assets/js/noise-glass-bg.js(噪声场与交互)和 assets/css/extended/noise-glass-bg.css(玻璃层样式),欢迎自取魔改。