---
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`
