sai-assets可复用网页资产库

three-fx · 三维特效两件(体波测地线盘 / 粒子黑洞)

three-fx v1.0.0 MIT(本包自产代码)+ MIT(upstream/three.module.js,Three.js Authors) 网页动效 16 文件 · 1381 KB
新窗口 ↗
载入预览…

需要一块三维、有物理感的空间效果:体波测地线盘(three.js 实时渲染体积薄盘,可拖转环视、面板 19 个参数)或粒子黑洞(Canvas2D 纯粒子吸积盘 + 引力吸入,零 WebGL 依赖,8 组盘样式预设)。适合展厅墙、deck 封面、科幻/暗色主题的首屏,以及需要「可被人拖着看」的三维玩具。取用面:体波 = src/blackhole-waves.js + upstream/three.module.js(1.3MB);粒子 = src/blackhole-particles.js(零依赖,两个文件各自独立,互不 import)。性能口径:体波 renderer.setPixelRatio 封顶 1.75、按「画质」参数降到 1.2;粒子 DPR 封顶 2;两者都是 rAF 循环,页隐藏时由浏览器自动停帧,帧间隔 dt 封顶 0.05s 防回前台跳帧;粒子密度/测地线步数(steps)是主要开销旋钮。无障碍口径:两个效果都没有内置 prefers-reduced-motion 分支(见 docs/usage.md §无障碍 的补丁片段),动效强烈,必须在页面层自己加停帧开关。

不适用场景

禁用:① 移动端首屏且首帧 LCP 敏感(体波要下载 1.3MB 的 three.js 并编译着色器,粒子要预建数万粒子);② 需要 reduced-motion 自动静态帧的场景(本包不内置,必须自己打补丁,见 usage.md);③ 长文阅读页的背景(体波是强光焦点,会和正文抢视线);④ 无 WebGL2 又要用体波(粒子版是 Canvas2D 兜底首选,但两者不共享 API);⑤ 把 three.module.js 再拷第二份进项目(要么整包引用本包 upstream/,要么用你们自己的 three.js 版本,别在仓里出现第二份 1.3MB 副本)。

元数据

类目
网页动效
版本
v1.0.0
许可
MIT(本包自产代码)+ MIT(upstream/three.module.js,Three.js Authors)
规模
16 文件 · 1381 KB
来源
sai-theme-lab · demos/{blackhole-void-three,blackhole-particles}

标签

  • webgl2
  • threejs
  • canvas2d
  • particles
  • raymarch
  • 3d
  • dark
  • hero

验证证据

2026-11-03 · python packs/three-fx/scripts/assert_tokens.py

PASS assert_tokens three-fx

归属与许可

  • three.js (mrdoob/three.js) · 来源 · MIT
    upstream/three.module.js —— r170 渲染器(Renderer/ShaderMaterial/Points/Raycaster/OrbitControls 等价逻辑),原样保留 @license + Copyright + SPDX-License-Identifier 头,全包只此一份,两个效果页共用
  • sai-assets 内部模板库 · demos/{blackhole-void-three,blackhole-particles}(自产,已脱敏) · MIT
    两个效果的机制代码与调参面板:体积薄盘/径向外推波的 shader、Canvas2D 粒子盘、8 组盘样式预设、params.json 默认值

取用

固定版本(CDN)
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/nakamotosai/sai-assets@pack/[email protected]/packs/three-fx/css/default.css">
raw 直取(跟随 main)
curl -O https://raw.githubusercontent.com/nakamotosai/sai-assets/main/packs/three-fx/SKILL.md
仓库内落地
python scripts/apply.py three-fx --to <项目目录>
整包下载
https://assets.ouuut.com/downloads/three-fx.zip

源码与规则

pack.json
{
  "schema": 1,
  "id": "three-fx",
  "title": "three-fx · 三维特效两件(体波测地线盘 / 粒子黑洞)",
  "kind": "fx",
  "category": "web-fx",
  "version": "1.0.0",
  "license": "MIT(本包自产代码)+ MIT(upstream/three.module.js,Three.js Authors)",
  "attribution": [
    {
      "name": "three.js (mrdoob/three.js)",
      "url": "https://github.com/mrdoob/three.js",
      "license": "MIT",
      "what": "upstream/three.module.js —— r170 渲染器(Renderer/ShaderMaterial/Points/Raycaster/OrbitControls 等价逻辑),原样保留 @license + Copyright + SPDX-License-Identifier 头,全包只此一份,两个效果页共用"
    },
    {
      "name": "sai-assets 内部模板库 · demos/{blackhole-void-three,blackhole-particles}(自产,已脱敏)",
      "license": "MIT",
      "what": "两个效果的机制代码与调参面板:体积薄盘/径向外推波的 shader、Canvas2D 粒子盘、8 组盘样式预设、params.json 默认值"
    }
  ],
  "origin": {
    "project": "sai-theme-lab",
    "path": "demos/{blackhole-void-three,blackhole-particles}",
    "derived_from": null
  },
  "entry": "SKILL.md",
  "files": {
    "src": [
      "src/blackhole-waves.js",
      "src/blackhole-particles.js",
      "src/param-panel.js",
      "src/param-panel.css",
      "src/stage.css",
      "effects/blackhole-waves/params.json",
      "effects/blackhole-particles/params.json"
    ],
    "upstream": [
      "upstream/three.module.js"
    ],
    "usage": "docs/usage.md",
    "assert": "scripts/assert_tokens.py",
    "preview": "preview/index.html"
  },
  "tags": [
    "webgl2",
    "threejs",
    "canvas2d",
    "particles",
    "raymarch",
    "3d",
    "dark",
    "hero"
  ],
  "when": "需要一块**三维**、有物理感的空间效果:体波测地线盘(three.js 实时渲染体积薄盘,可拖转环视、面板 19 个参数)或粒子黑洞(Canvas2D 纯粒子吸积盘 + 引力吸入,零 WebGL 依赖,8 组盘样式预设)。适合展厅墙、deck 封面、科幻/暗色主题的首屏,以及需要「可被人拖着看」的三维玩具。取用面:体波 = src/blackhole-waves.js + upstream/three.module.js(1.3MB);粒子 = src/blackhole-particles.js(零依赖,两个文件各自独立,互不 import)。性能口径:体波 renderer.setPixelRatio 封顶 1.75、按「画质」参数降到 1.2;粒子 DPR 封顶 2;两者都是 rAF 循环,页隐藏时由浏览器自动停帧,帧间隔 dt 封顶 0.05s 防回前台跳帧;粒子密度/测地线步数(steps)是主要开销旋钮。无障碍口径:两个效果**都没有**内置 prefers-reduced-motion 分支(见 docs/usage.md §无障碍 的补丁片段),动效强烈,必须在页面层自己加停帧开关。",
  "not_for": "禁用:① 移动端首屏且首帧 LCP 敏感(体波要下载 1.3MB 的 three.js 并编译着色器,粒子要预建数万粒子);② 需要 reduced-motion 自动静态帧的场景(本包不内置,必须自己打补丁,见 usage.md);③ 长文阅读页的背景(体波是强光焦点,会和正文抢视线);④ 无 WebGL2 又要用体波(粒子版是 Canvas2D 兜底首选,但两者不共享 API);⑤ 把 three.module.js 再拷第二份进项目(要么整包引用本包 upstream/,要么用你们自己的 three.js 版本,别在仓里出现第二份 1.3MB 副本)。",
  "verified": {
    "date": "2026-11-03",
    "how": "python packs/three-fx/scripts/assert_tokens.py",
    "evidence": "PASS assert_tokens three-fx"
  },
  "canonical_home": null
}
docs/usage.md
# three-fx · 用法与配对规则

两件**三维**特效,运行模型完全不同,各自独立:

| id | 机制 | 运行模型 | 依赖 |
|---|---|---|---|
| `blackhole-waves` | 体波测地线盘:体积薄盘 + 径向外推波 + 开普勒差速旋臂 + 多普勒 | three.js 实时渲染(WebGL2) | `upstream/three.module.js`(1.3MB,MIT) |
| `blackhole-particles` | 粒子黑洞:开普勒轨道吸积盘 + 1/r 引力吸入/重生 + 光子环 + 多普勒 | Canvas2D(2D 上下文 + 手写粒子系统) | 无 |

## 取用

**A · 体波测地线盘**(需要一个静态服务器,`import` + `fetch` 不能用本机文件协议直接开):

```html
<link rel="stylesheet" href="src/stage.css">
<link rel="stylesheet" href="src/param-panel.css">
<div id="magic" aria-label="体波测地线盘"></div>
<div class="hud" id="hud"></div>

<!-- 模块按 id 取这两段着色器源码,必须在 body 之前存在 -->
<script type="x-shader/x-vertex" id="pt-vert">…</script>
<script type="x-shader/x-fragment" id="pt-frag">…</script>
<script type="module" src="src/blackhole-waves.js"></script>
```

要拷的文件:`src/blackhole-waves.js` · `upstream/three.module.js` · `src/param-panel.js` · `src/param-panel.css` ·
`src/stage.css` · `effects/blackhole-waves/params.json`(页面里的 `fetch('./params.json')` 相对**页面**解析,所以 json 要放在页面同级)。

**B · 粒子黑洞**(零依赖,同样需要一个静态服务器才能读到 `params.json`,否则面板回落默认值):

```html
<link rel="stylesheet" href="src/stage.css">
<link rel="stylesheet" href="src/param-panel.css">
<canvas id="stage" aria-label="粒子黑洞"></canvas>
<div class="hud" id="hud"></div>
<script type="module" src="src/blackhole-particles.js"></script>
```

要拷的文件:`src/blackhole-particles.js` · `src/param-panel.js` · `src/param-panel.css` · `src/stage.css` ·
`effects/blackhole-particles/params.json`。**不要**为了粒子版去拷 three.js —— 它整个用不上。

两个模块都**不导出 API**:它们是"页面级成品"(自己拿 `#magic` / `#stage`、自己挂面板、自己跑 rAF)。
要嵌进 SPA 里,推荐用 `<iframe>` 或独立的静态页;要改行为,直接改模块里那几行(见 §无障碍 的补丁)。
调参面板底部有它的折叠按钮,拖动标题栏可换位置(默认左下角)。

## 参数

### `blackhole-waves`(面板 19 个,`effects/blackhole-waves/params.json`)

> `defaults` 里另有 4 个**未上面板**的进阶项:`inner` / `outer`(盘内外半径)、`beam`(盘面亮度增益)、
> `starDrift`(背景星漂移)——模块会读,但面板不生成控件,要改就直接改 json。

| key | 默认 | 范围 | 含义 / 怎么调 |
|---|---|---|---|
| `mode` | 测地线连续盘 | 4 档 | 测地线连续盘 / 测地线·转速加强 / 纯粒子盘 / 混合·底盘+飞尘 |
| `waveSpeed` | 4.5 | 0–12 | **动感主开关**:盘面纹理向外推的速度。0 = 冻结的静态盘 |
| `spin` | 5.5 | 0.5–14 | 角向转速:越大旋臂拧得越紧 |
| `wind` | 6.5 | 0–12 | 螺旋臂数/缠绕强度;配合 `spin` 出开普勒差速 |
| `contrast` | 1.55 | 0.3–2.4 | 盘面条纹对比;>2 会开始出硬边摩尔 |
| `waveFreq` | 5.0 | 1.5–8 | 条纹细度:大值=细密环纹,小值=厚带 |
| `diskThick` | 0.08 | 0.03–0.25 | 高斯厚度:决定侧视时盘是"薄盘"还是"厚环" |
| `temp` | 5200 | 2500–12000 K | 黑体色温:2500K 偏橙红,12000K 偏蓝白 |
| `opacity` | 0.88 | 0.2–1.0 | 盘浓度(累积不透明度) |
| `doppler` | 0.55 | 0–1 | 多普勒左右冷热差:0 = 对称盘,1 = 一侧明显发蓝 |
| `gain` | 2.1 | 0.5–3.5 | 盘增益(发射强度) |
| `exposure` | 1.4 | 0.5–2.5 | 全画面曝光 |
| `starGain` | 0.7 | 0–1.4 | 背景星亮度 |
| `steps` | 52 | 28–72 | **性能旋钮**:测地线步数,直接决定 GPU 时间 |
| `fov` | 40 | 28–70 | 视场角(相机) |
| `particleSpin` | 1.5 | 0.3–3.5 | 粒子盘转速倍率 |
| `particleDensity` | 1.0 | 0.4–1.5 | **性能旋钮**:粒子数量 |
| `dotScale` | 1.2 | 0.5–2.4 | 粒子点大小 |
| `quality` | 1.0 | 0.5–1.5 | **性能旋钮**:综合画质,直接改 `setPixelRatio`(1.2 + 0.55·q) |

### `blackhole-particles`(11 个,`effects/blackhole-particles/params.json`)

| key | 默认 | 范围 | 含义 / 怎么调 |
|---|---|---|---|
| `style` | 经典(侧视薄盘) | 8 档 | 盘样式预设:一次改色温/倾角/内外半径/浓度/多普勒/增益/对比/风(其余参数之上还有一层全局缩放) |
| `density` | 1.0 | 0.4–2.0 | **性能旋钮**:粒子密度 |
| `holeSize` | 0.55 | 0.2–1.0 | 视界半径(画面占比) |
| `diskOpacity` | 0.85 | 0–1 | 光环浓度 |
| `doppler` | 0.55 | 0–1 | 左右冷热(轨道速度投影) |
| `spin` | 1.0 | 0.2–2.5 | 盘转速(开普勒 Ω ∝ r^-1.5 的全局倍率) |
| `gravity` | 1.0 | 0.2–2.5 | 星尘被吸入的强度;>2 星尘会被快速清空并重生 |
| `starGain` | 0.55 | 0.1–1.2 | 星空亮度 |
| `exposure` | 1.15 | 0.5–2.0 | 全画面曝光 |
| `followMouse` | true | 开关 | 视界跟随指针(关掉=固定居中) |
| `autoCycle` | false | 开关 | 每 ~5s 自动轮换样式预设(演示用,别在生产页开) |

面板用原生 `<input type="range">` / `<select>` / `<input type="checkbox">`,键盘可达;
缓存键是 `themelab.<slug>.params`(localStorage),换 slug 要清缓存,否则会读到旧默认值。

## 性能

- **体波**:`renderer.setPixelRatio(min(dpr, 1.75))`,并随 `quality` 降到 `1.2`;主开销是
  `steps`(光线步进)与 `particleDensity`(混合/纯粒子模式)。移动端建议 `quality 0.6 / steps 36 / particleDensity 0.6`。
- **粒子**:DPR 封顶 2;主开销是 `density`(粒子数)与绘制时的逐粒子变换。老设备用 `density 0.6`。
- 两者都是 `requestAnimationFrame` 循环,**页隐藏时浏览器自动停帧**(rAF 不触发),不需要额外处理;
  帧间隔已 clamp 到 0.05s,回前台不会"补帧"跳一下。
- 体波首次进入要**下载 1.3MB three.js + 编译着色器**,首帧明显晚于粒子版。首屏敏感的落地页用粒子版,
  或者把体波页做成"点击后加载"。
- 两个效果都**不共享**任何代码:同页同时打开 = 一个 WebGL 上下文 + 一个 2D 上下文,没有冲突,
  但两套 rAF 会各吃掉一部分 GPU/CPU,建议还是一页一个。

## 无障碍

- 画布/挂载点都有 `aria-label`;HUD 只是状态文字,不承载唯一信息。
- 调参面板是原生表单控件(键盘可达);它的 `<label>` 与控件是**相邻**关系而非 `for` 关联,
  读屏会念控件类型与当前值,但不会念标签——要严格达标得在项目侧补 `htmlFor`。
- **两个效果都不内置 `prefers-reduced-motion`**:动效强度高(持续旋转 + 光晕),这是本包已知缺口。
  两种补法(都不需要动其它逻辑):

  ```js
  // 体波:src/blackhole-waves.js 的 frame() 里,把定时改成"定格"
  //   (第 749 行) uniforms.uTime.value = (now - t0) * 0.001;
  //   改成:
  const mq = matchMedia('(prefers-reduced-motion: reduce)');
  uniforms.uTime.value = mq.matches ? 6.0 : (now - t0) * 0.001;

  // 粒子:src/blackhole-particles.js 的 frame() 里,把 dt 归零(画面静止,状态不再演化)
  //   (第 439 行) const dt = Math.min(0.05, (now - lastT) / 1000);
  //   改成:
  const dt = mq.matches ? 0 : Math.min(0.05, (now - lastT) / 1000);
  ```

  更省事的替代:在页面层数 60 帧后停掉 rAF(`let n=0; const raf=requestAnimationFrame;
  window.requestAnimationFrame = cb => (n++ < 60 ? raf(cb) : 0)`),画面定格 —— 但面板改参数后
  不会立刻重绘,需要手动放行一帧。
- 这两个效果不适合长时间暴露的仪表盘场景(视野里一直有高速旋转),做看板请用 `bg-fx` 的静态帧模式。

## 反模式

1. **用体波当"背景图"**:它是强光焦点,占满屏会跟正文抢视线;要么缩小成一块画布,要么只用粒子版并压曝光。
2. **把 `upstream/three.module.js` 再拷一份进项目**,或反过来在项目里用别的 three.js 版本去 import
   这个模块(会拿到两个 three 实例,`instanceof` 与资源释放都会出错)。要么整包引用,要么只抄粒子版。
3. **直接双击本机文件打开页面**:`import` 与 `fetch('./params.json')` 都会被同源策略拦下,
   面板回落默认值、`#magic` 空白。必须过静态服务器。
4. **`autoCycle: true` 放进生产页**:每 5s 换一次盘样式,像幻灯片故障;它只是演示开关。
5. **把 `steps` / `density` / `quality` 都拉满**:三个性能旋钮同时顶格会让中端 GPU 掉到 20fps 以下,
   画面收益却接近零(步数超过 ~56 之后基本看不出差别)。
6. **指望面板参数被持久化到文件**:面板只写 localStorage;要固化默认值请改 `effects/<id>/params.json`。
7. **拿粒子版当"体波的降级方案"**:两者机制不同(一个真渲染体积盘,一个是 2D 粒子),
   视觉语言只在配色上相似;降级要提前决定用哪个,不要指望它们可以互相替换。
SKILL.md
---
name: three-fx
description: Two independent 3D effects — a three.js volumetric accretion disk with radial outward waves, Keplerian shear and Doppler asymmetry (20 live params, draggable camera), and a dependency-free Canvas2D particle black hole with Kepler orbits, 1/r infall, photon-ring crowding and 8 disk presets. Use when a hero/deck/scifi page needs a real 3D object with physics-ish motion, not a decorative loop.
---

# three-fx

两件三维特效,运行模型不同、互不 import,各自能用:

| id | 是什么 | 依赖 | 参数 |
|---|---|---|---|
| `blackhole-waves` | 体波测地线盘:体积薄盘(高斯厚度)+ 径向外推波 + 开普勒差速旋臂 + 多普勒左右冷热差;可拖转/滚轮/右键滚转环视 | `upstream/three.module.js`(r170,MIT,仅一份) | 20 |
| `blackhole-particles` | 粒子黑洞:开普勒轨道吸积盘 + 1/r 引力吸入与重生 + 光子环加密 + 多普勒黑体色偏 | 无(Canvas2D) | 11 |

## When to use

- 需要**空间感 / 物理感**的主视觉:科幻、暗色宇宙、数据"引力井"隐喻、展厅墙、deck 封面
- 需要"可被人拖着看"的三维玩具(体波版有轨道相机)
- 需要同一题材的两种成本档:体波(重、真渲染)↔ 粒子(轻、零依赖)

## When not to use

- 移动端首屏且 LCP 敏感(体波要 1.3MB three.js + 着色器编译)
- 要求 `prefers-reduced-motion` 自动静帧的场景:**本包不内置**,必须按 `docs/usage.md` §无障碍 打补丁
- 长文阅读页背景、仪表盘背景(持续旋转会疲劳)
- 必须直接双击本地文件打开的环境(`import` / `fetch` 需要 http 服务)
- 想要一个能被 `import` 进组件树、可销毁可复用的库(这两个是页面级成品,自己抓 DOM)

## Files

| path | 是什么 |
|---|---|
| `src/blackhole-waves.js` | 体波测地线盘(单文件成品:场景/材质/相机/环绕控制/面板挂载/主循环) |
| `src/blackhole-particles.js` | 粒子黑洞(单文件成品:粒子系统/引力/绘制/面板挂载/主循环) |
| `src/param-panel.js` · `src/param-panel.css` | 浮动调参面板(按 `params.json` 的 schema 生成原生控件) |
| `src/stage.css` | 两个效果页共用的舞台样式(`#magic` / `#stage` / HUD / 提示 / 返回链) |
| `upstream/three.module.js` | three.js r170,**只此一份**,保留 `@license` + Copyright + SPDX 头 |
| `effects/blackhole-waves/index.html` | 体波页(含两段内联着色器源码,模块按 `id="pt-vert"/"pt-frag"` 取) |
| `effects/blackhole-particles/index.html` | 粒子页(挂 `#stage`) |
| `effects/<id>/params.json` | 该效果的参数默认值 + 面板 schema |
| `preview/index.html` | 两件效果的索引墙(链接,无 iframe) |
| `docs/usage.md` | 取用片段、全部参数表、性能与无障碍口径、反模式 |
| `scripts/assert_tokens.py` | 断言:pack.json / 产物 / 相对引用 / effect↔模块对齐 / SPDX 头 / 脱敏残留 |

## Agent apply steps

1. 读 `pack.json` 的 `when` / `not_for` 选版本:重(体波)还是轻(粒子)。
2. 按 `docs/usage.md` §取用 拷**对应那一组**文件(粒子版**不要**带 three.js)。
3. 页面里给出模块要的 DOM:`#magic`+`#hud`+两段内联着色器(体波),或 `#stage`+`#hud`(粒子)。
4. `fetch('./params.json')` 相对**页面**解析:把 `effects/<id>/params.json` 放到页面同级。
5. 必须走静态服务器验证(`python -m http.server` 或项目 dev server),别用本机文件协议直接开。
6. 交付前自检:`python packs/three-fx/scripts/assert_tokens.py` 必须打印 `PASS assert_tokens three-fx`。

## Hard rules

- **`upstream/three.module.js` 只此一份**;项目里已有 three.js 时,别让两个 three 实例同时存在。
- 体波页的 `#pt-vert` / `#pt-frag` 内联着色器是**模块的输入**(按 id 读取),删了页面就是全黑 + 报错。
- `params.json` 里的 `style` 选项字符串就是 `STYLE_TABLE` 的键,改名要两边一起改(断言会查)。
- 面板缓存键 = `themelab.<slug>.params`;改 slug 必须同时改模块里的 `localStorage.removeItem(...)`。
- 本包**不含** `prefers-reduced-motion` 分支:上线到公共站点前按 `docs/usage.md` §无障碍 的补丁处理,
  不要假装引擎已经帮你做了。
- 一页一个效果;两个都想要就做索引墙链接,或分两个 section 并各自按需加载。

← 回索引