dynamic-wallpaper · git:20261103.7cf0331 · 2026-11-03 · sha256 41fdb52be26039c2
dynamic-wallpaper git:20261103.7cf0331B
Immutable. This exact content never changes and is served at /api/v1/blob/41fdb52be26039c2.
---
name: dynamic-wallpaper
description: "做可在桌面常驻的交互式动态壁纸(Canvas 实时渲染、零依赖、零构建,可被 Lively Wallpaper / Wallpaper Engine 直接加载)。当用户要「动态壁纸 / 桌面壁纸 / 可交互壁纸 / 会动的桌面 / 桌面特效程序」,或需要长时间常驻、低占用、多场景可切换的全屏 Canvas 视觉程序时使用。含完整可运行案例(赛博朋克三场景,demo/demo.html 点开即跑)、渲染管线骨架、性能预算与七个实测视觉陷阱。"
---
# 动态桌面壁纸
做一类**常驻全屏、长时间运行、可交互、可切换主题**的 Canvas 程序。和"做个网页动画"的差别不在写法,在约束:它要在别人桌面上跑一整天,被 GPU 轮转、被休眠唤醒、被 4K 屏拉伸,而且加载方式通常是本地文件而不是 HTTP。
完整案例见 `demo/`(点 `demo.html` 或 `demo/index.html` 直接运行,无需服务器)。
## 一、交付形态是硬约束
壁纸宿主(Lively / Wallpaper Engine / Plash)都是**本地文件加载**,不是起服务器。所以:
- **单文件入口** `index.html`,`file://` 双击必须能跑
- **零构建**:不能有 `npm install`、不能有打包产物
- **零外部请求**:不能 `fetch` 本地 json、不能用 CDN 字体和库(`file://` 下会被 CORS 拦掉)
- **零依赖**:不用 Three.js / GSAP,多一个文件就多一个加载失败点
验证方式只有一条:`file:///…/index.html` 打开,控制台零报错,画面正常。开发期用 `python -m http.server` 只是图方便,别把它当成验收标准。
## 二、选型:Canvas 2D 还是 WebGL
默认选 **Canvas 2D**。理由不是性能,是稳定性:壁纸常驻意味着显存要还回去、驱动要兼容、休眠唤醒后 context 不能丢。WebGL 在这些场景上故障率高,而且调一块模糊、一个渐变的成本远高于 2D。
Canvas 2D 够用的前提是**把开销挪到初始化**:静态内容预渲染成离屏 canvas,每帧只做少量 `drawImage`。真正每帧重画几百个图元的写法才需要 WebGL。
什么时候该上 WebGL:需要真 3D 几何、实时体积光、上千个独立粒子的物理。案例里的"数据隧道"看着像 3D,其实是纯 2D 透视投影,代价极低。
## 三、渲染管线
四段,每帧一次:
```
输入(指针/键盘/时间) → 场景渲染(离屏层) → 合成(视差偏移) → 后处理(泛光/扫描线/暗角)
```
**1. 输入**:指针归一化成 `-1..1`,再用指数插值追目标值(`cur += (target-cur)*0.06`)。直接跟随会显得廉价,必须带阻尼。同时维护 `idle` 计时,用于让 UI 自动淡出。
**2. 场景**:每个场景实现 `resize(w,h)` / `update(dt,t)` / `render(ctx)`,通过 `engine.register(name, factory)` 注册。场景内部再分层——案例里的室外场景是 `天空+远景 / 中景 / 近景` 三张预渲染图,各带不同视差系数(12 / 24 / 42)。
**3. 合成**:按视差系数偏移 `drawImage`。全屏贴图一次比重画一遍建筑便宜两个数量级。
**4. 后处理**:两级降采样泛光(画面缩到 1/5 提亮部,再缩到 1/15 做大半径模糊,`lighter` 叠回),加缓存的扫描线 pattern 和暗角渐变。
## 四、七个实测视觉陷阱
这些是做完看着"廉价"、返工最多的点,按杀伤力排序。
**1. 发光变成实心圆盘。** 用 `createRadialGradient` 手写 stops(0→1、0.4→0.2、1→0)画出来边界很硬,霓虹全成了贴纸。改成高斯衰减,14 段采样 `alpha = exp(-t²/2σ²)`,σ 取 0.2 左右,才像真的光在散。
**2. 窗户全亮像圣诞树。** 窗格点亮概率如果直接乘一个放大系数(当年写了 `litRatio * 3.2`),77% 的窗都会亮。正确做法:按楼层分组给基础明暗(有些楼层整层暗),再乘原始比率,并且让亮度服从幂律分布而不是均匀分布。
**3. 隧道必然是同心圆靶子。** 环按 z **线性**分布时,远近环的半径差被压缩,看起来就是一堆同心圆。必须改成**对数分布**(`z = zMin * (zMax/zMin)^u`),透视才成立。
**4. 隧道弯曲量跟着半径等比缩放 = 白弯。** 如果每环的偏移量都乘自己的半径,屏幕上的偏移会等比放大,圆心反而没动。偏移必须随**深度 u** 变化(近处偏得多、远处偏得少),圆心的移动才可见。另外圆形截面天生像靶子,**改用切角矩形截面**(方框套方框)立刻有透视方向感。
**5. 全屏 fbm 噪声会变成大圆斑。** 拿噪声纹理铺满天空,每一个斑被拉宽到 200px 以上就是一团诡异的暗圆,而且我花了很久误判成"暗角"或"云层 bug"。结论:**大范围柔和气象不要用纹理噪声**,用横向线性渐变光雾带,更干净也更省。
**6. 每帧创建渐变对象。** `createLinearGradient` 在绘制循环里被调用几百次是性能杀手。全部提到 `resize()` 里缓存,或改成预渲染贴图。
**7. 4K 屏按 2 倍 dpr 渲染等于要求 8K 填充率。** 按 CSS 像素面积限 dpr:`>4.2e6 → 1`、`>2.6e6 → 1.5`、否则 `min(devicePixelRatio,2)`。
## 五、性能预算
目标:**1080p 稳定 60fps,CPU 占用别让人听见风扇**。
- 静态层预渲染,每帧 `drawImage` 次数控制在个位数
- 泛光 1/5 + 1/15 两级,别在全分辨率上模糊
- 光晕一律走预生成 sprite(按 `颜色|尺寸|软度` 缓存),不用 `shadowBlur` / `filter: blur`
- 同步降级:fps < 40 时 `quality = 0.7`(关泛光、减粒子),> 56 时恢复
- `document.hidden` 时直接 `return`,不渲染
- 窗口重新可见时重置时间基准,否则 `dt` 会跳一大帧
## 六、验收流程
1. `file://` 直接打开,零控制台报错
2. 三个场景各截一张图,**用眼睛看**——数值正常不代表好看
3. 分场景测帧率(`requestAnimationFrame` 计数 2.5 秒取平均)
4. 点击 / 移动 / 切场景 / 改窗口尺寸,回归一遍
5. 鼠标静止 2 秒后,UI 应该自动让出画面
第 2 步不可省。这个案例里所有严重缺陷(圆盘光晕、窗户过密、同心圆隧道)都是截图看出来的,没有一条是控制台报错。
## 七、案例:CyberNeon
`demo/` 是完整实现,4130 行,可直接拆改。
| 文件 | 作用 |
|------|------|
| `demo/index.html` | 入口,一个 canvas + 控制面板 |
| `demo/js/core/utils.js` | 数学 / 颜色 / 高斯发光贴图 / 光柱 |
| `demo/js/core/engine.js` | 主循环 / dpr / 输入 / 泛光 / 降级 |
| `demo/js/scenes/outdoor.js` | 雨夜霓虹城(分层预渲染 + 雨 + 车流 + 倒影) |
| `demo/js/scenes/indoor.js` | 控制中枢(一点透视 + 六块实时屏 + 全息) |
| `demo/js/scenes/tunnel.js` | 数据隧道(对数 z 分布 + 切角矩形截面) |
| `demo/js/ui/panel.js` | 控制面板(密度/辉光/速度/自动轮播) |
三个场景覆盖了三种不同的画面结构:**室外靠分层视差**、**室内靠透视建模**、**隧道靠参数化投影**。新场景照最接近的那个改。
## 八、新增一个场景
1. 复制一个 `js/scenes/xxx.js`,改 `CN.scenes.xxx = factory`
2. 在 `index.html` 加 `<script>`,在 `main.js` 里 `engine.register('xxx', ...)`
3. 实现 `resize / update / render`,静态部分塞进离屏 canvas
4. 在 `panel.js` 的场景列表和快捷键里加一项
## 装成壁纸
- **Lively Wallpaper**:`Add Wallpaper` → 选 `demo/index.html`
- **Wallpaper Engine**:新建 Web 类型壁纸,指向 `demo/` 目录
- **只想看看**:双击 `demo.html`