# Tephra

> 把一张图重画成黑底像素画，两种正典画法按题材二选一：**纯粒子**（会动的短笔触沿图像自身 的结构方向流动，适合浪／烟／光柱这类本来就在流动的东西），或**网点实体**（把明暗翻译成 逐帧静止的有序点阵，适合人像／雕塑／动物这类要读出轮廓和体积的东西，并且只有它能把形体 梳成硬边碎裂条）。动效按画面的物理规律分区规划；可选地让一部分形体溃散成飞灰，同时锚定 另一部分保持实体与原色。输出静态 PNG 与无缝循环 GIF/MP4。 触发：/tephra、「粒子流动效果」「把这张图重绘成笔触」「黑底像素画」「网点碎裂」。 算法与代码全部原创，输出不基于任何模型，也不复制任何具体作品。

- Skill: `haorantang97/tephra` (Agent Skill, multi-file: 15 files)
- Install (CLI): `npx skillmds@latest add haorantang97/tephra`
- Raw SKILL.md: https://api.skillmd.com/api/skills/haorantang97/tephra/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: PolyForm-Noncommercial-1.0.0
- Author: haorantang97 (https://skillmd.com/u/haorantang97)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/haorantang97/tephra

---


# Tephra v8

核心脚本 `scripts/tephra.py`，确定性（同参数同 seed 逐字节相同）。

> **改代码时的铁律：不要往共享 `rng` 上加无条件的抽取。**
> 每次渲染都会跑到的新噪声场，必须自己开一条流（`np.random.default_rng(seed * 常数)`）。
> 从共享流上抽会把后面所有抽取（笔触播种、雨点）整体挪位，同一条命令换个版本就渲不出
> 同一张图了。0.2.0 修的就是这个（湍流卷曲场 `_n1`）。
**交付时必须报出完整命令**——那就是版本号。

---

## 先读：工作流（七步，不要跳）

这套东西最常见的失败不是参数调错，是**跳步**。按顺序走：

1. **叠网格**　在源图上叠 0.1 间距的归一化网格，所有坐标参数（漩涡中心／分区／锚定）从图上**读**出来，不要估。
2. **体检**　`python3 scripts/dp_fitness.py 图.png [--invert]`。不过就别硬渲，回第 3 步。
3. **前处理**　见下「M0.5」。改的是明暗，不是几何。
4. **列元素 → 逐个判物理**　画面里有哪几样东西，每样在物理上往哪走。见「M3 物理对照表」。
5. **写成分区**　一个元素一条 `--flow-region`；天然同心的结构才用 `--swirl`。
6. **渲**
7. **验**　相位互相关逐区量位移，与第 4 步的判断核对。**方向靠眼睛看会错。**

---

## 两种正典画法（先选一种，再往下调）

这套东西有**两条都成立的正典**，各自对应一批不同的参考作品。**选错了后面全是白调。**

![两种画法](../../assets/examples/canon-modes.jpg)

*同一张源图、同一个主体，左 A 右 B（动图：`assets/examples/canon-modes.gif`）。
两边都是各自路线调到最好的版本，不是「同参数开关 mass」——把 B 的参数原样关掉 mass，
画面会直接空掉（B 的 `--density 0.02` 几乎没有笔触，整张画都靠网点层撑着）。*


| | **A · 纯粒子** | **B · 网点实体** |
| --- | --- | --- |
| 一句话 | 用会动的短笔触把图重画一遍 | 把明暗翻译成一层静止的有序点阵 |
| 参考作品 | 样本J 的「尘」那一层 | 样本A / 样本K |
| 开关 | `--mass 0`（preset `flux` 的默认） | `--mass 2.6` ＋ 原生网格渲染 |
| 主体逐帧 | 一直在动（帧间留存 ≈ 0.83） | **完全静止**（≈ 0.999） |
| 落点怎么定 | 按亮度加权**随机采样** | 跟固定 8×8 Bayer 表**阈值判定** |
| 质感 | 密集绒毛状笔触，边缘硬 | 规则点阵，能读出连续调子 |
| 擅长 | 浪、烟、光柱、火、水花——**本来就在流动的东西** | 人像、雕塑、动物、建筑——**要读出轮廓和体积的东西** |
| 不擅长 | 人像（糊成一团毛，读不出结构） | 流体（点阵钉死，动感全没了） |

### A · 纯粒子——裸跑即正典

```bash
python3 scripts/tephra.py in.png -o out --size 1400 --seconds 1.6 --fps 25 --mp4
```

默认 preset `flux` 就是它，`--floor auto` 从这张图自己的直方图读。
**不要为了「用上正典」手打一长串参数**——那串就是默认值。

### B · 网点实体——必须走原生网格

```bash
python3 scripts/tephra.py in.png -o nat --size 768 --supersample 1 --palette ash \
  --mass 2.6 --density 0.02 --sparsity 6.0 --floor 0.008 --contrast 1.15 --coherence 16 --stroke 3 \
  --dissolve 溃散方向,0.45,0.40 --dissolve-patch 0.35 --escape 0 \
  --comb-life 0.04 --comb-emit 0.2 --comb=梳向,2.3,30,1.45,0.16,5,0.70,66,0.24,7.0,0.26,3.6 \
  --seconds 0.96 --fps 25 --mp4
python3 scripts/upscale_nearest.py nat.png out.png 1400
```

细节与实测验收表见 **M4**。这一路**必须在 512–768 的原生网格上渲再 NEAREST 放大**——
直接渲 1400 会把碎料糊成烟，这是最容易踩的一个坑。

> **`--comb` 的碎裂条只能从网点上梳出来。**
> 没有 mass 层就没有点阵可梳，梳笔触只会得到一团运动模糊。
> 所以 B 那种硬边碎裂条在 A 里做不出来——**不是参数没调对，是缺一层。**

> **`--mass` 不是风格开关，是亮度门槛。** 它只决定「多暗算暗到不画」。
> 画出来长什么样由**源图的明暗分布形状**决定，不由这个数决定：
> 侧光雕塑的调子摊在中间段 → 网点连续过渡，读得出体积；
> 逆光剪影被相机压死、内部几乎一个值 → 近三成面积顶到 90% 以上，读起来是一块剪纸。
> （实测：同一套参数，雕像只有 8.5% 的面积超过 80% 占空比，马是 29.8%。）
> 想让剪影也有层次，要在 **M0.5 前处理**里造体积明暗，改 `--mass` 没用。

### 共用的加料（两条路都适用）

| 想要 | 加什么 |
| --- | --- |
| 换色 | `--palette gold / ember / blood / violet / phosphor / neon` |
| 虚空不够／主体发碎 | `--floor 0.05` 覆盖自动值 |
| 白底图或暗主体 | `--invert`（**注意副作用，见 M0.5 P1**） |
| 焦点星爆 | `--burst-at X,Y,强度` |
| 三维漩涡 | `--swirl X,Y,S,R,RATIO,TILT,PITCH` |
| 保留一个实体物 | `--anchor X,Y,RX,RY[,S][,#色][,块][,曝光][,黑点分位][,彩度]` |
| 一部分溃散 | `--dissolve DEG,START,SOFT` + `--dissolve-patch 0.5` |

---

## 模块（一个模块只负责一件事）

**铁律：模块之间不许偷偷改对方的参数。**
历史教训：`--dissolve` 曾经把 `--mass` 从 0 改成 1，于是「只加一点溃散」实际换掉了整张图的画法。

### M0 输入判定
`scripts/dp_fitness.py`。判据：**中位亮度 < 0.20 直接过；否则要求可分离性 > 0.55**。
（依据：4 张成功/失败样本实测。中位亮度低 = 虚空是图自带的；可分离性高 = 删哪儿由图决定不由你猜。）

### M0.5 前处理（只改明暗，不改几何）

| 规则 | 触发 | 手段 |
| --- | --- | --- |
| P1 反相 | 主体比背景暗 | `--invert`。**副作用：主体自己的高光会变成 floor 首先删除的对象**，造型强的物体会掉一截 |
| P2 去梯度 | 大尺度亮度梯度（大气/景深/渐晕） | 减一个超大尺度高斯背景再归一化 |
| P3 裁非画面物 | 水印/字幕/拼接缝 | **裁掉，不要修补**——修补留下的直角缝会被忠实渲染成方块 |
| P4 换轴 | 主体与背景亮度接近（差 < 0.05）但纹理差 1.5 倍以上 | 用局部对比度当门槛 |
| P5 重打光 | 前四条都救不回来 | img2img，denoise 0.25–0.35，**几何锁死**，结构保真 > 0.55 |

**什么时候必须上 P4**：主体上某个你要保住的部件（耳朵、下颌、发丝）与背景**中位亮度差 < 0.02**。
量一下再决定——雕像的耳朵和背景烟都是 0.055，试多少个 `--floor` 都是白试。
现成脚本：`scripts/p4_mask.py 源图 输出.png`（小而亮的东西如余烬会被纹理判据误杀，脚本单独保留）。

**P4 的陷阱（必读）**：纹理能量本质上是边缘检测，直接拿它当形体会把实心物体变成**铁丝网**——
只有一圈亮边、内部是空的，粒子化之后糊成一团。正确用法是：
**纹理只用来造「区域掩膜」（闭运算→填洞→大尺度平滑），形体交回给主体自己的明暗。**
这样还有个好处：背景已由掩膜杀掉，就**不需要反相**，主体内部直接用源图明暗——
受光处密、背光处疏，「张弛有度」是光影本身给的，不用另外发明规则。

### M1 形体
互斥三选一，前两个都是正典，按上面那张表按题材选：
**A 纯粒子 `--mass 0`** ／ **B 网点实体 `--mass 2.6`（要碎裂条就只能选它）** ／ 局部锚定 `--anchor`。

`--anchor` **不做主体分割**：逐 RGB 通道对源图抖动，暗的自己不印、彩的自己出色，
黑点分位从椭圆区内自取（区内多数面积是背景 → 背景自动归零）。
彩度提升必须**按已有彩度开门**，否则中性色的微弱色偏会被放大成假色（白浪变青）。

### M2 笔触质感
`--stroke` 最长笔长 / `--length-mix` 长短偏斜 / `--aniso-len` 各向异性区才给长笔 /
`--fray` 子段错位 / `--gap` 断口 / `--jitter` `--curve` 歪与弯 / `--density` `--sparsity`。

**笔长必须小于你要读出来的最小部件**。一根 22px 的笔画不出一个 25px 的蹄子，只会盖过去。
细节多的图：`--stroke 10 --coherence 18`；大形为主：`--stroke 22 --coherence 45`。

### M3 流动（三层，各自独立、都可局部化，权重最强者赢）

| 层 | 参数 | 何时用 |
| --- | --- | --- |
| 全局朝向 | `--flow-bias` / `--flow-from-burst` | 兜底 |
| **分区朝向** | `--flow-region X,Y,RX,RY,ANGLE[,S]` | **默认手段**，一个元素一条 |
| 分区旋转 | `--swirl X,Y,S,R[,RATIO,TILT,PITCH]` | 只给天然同心的结构 |

**物理对照表**：光柱/雨/瀑布/坠落=90° ｜ 火焰/余烬/上升烟=−90° ｜ 破碎浪的浪背≈−40° ｜
浪的桶内/发髻/年轮/星系=`--swirl` ｜ 爆炸碎片=`--flow-from-burst` ｜ **岩石/建筑不动，什么都不给**。
拿不准就问：关掉重力和风，它还会往那个方向动吗？不会就是编的。

**M3.a 三维漩涡**：`RATIO` = 投影椭圆的短长轴比（**必须实测**，拟合泡沫环主轴），`TILT` 长轴倾角，`PITCH` 径向内收。
在水面平面里旋转再把速度投影回画面——透视压缩自动产生（端部慢、鳍部快），速度按 1/r。
**平面圆形漩涡会把水推着横切过所有的圈**，读起来就是一个转盘。

`--conserve 1.5` **必开**：消除吸引子造出的假轮廓。

### M4 溃散（可选，非每图必用）

> ### 最关键的一条：**在原生网格上渲，再 NEAREST 放大**
> 样本A 的原生画幅是 **512×384**（GIF 容器 2048×1536，4×4 块 100% 一致，实测），
> 墨严格二值（只有 0 和 210 两个值），点亮率 10.2%。
> **一个"像素"= 画幅宽度的 1/512。** 直接在 1400 宽上抖动，格子就细了 2.7 倍——
> 单个像素永远读不出来，整片碎料糊成烟。这是"感觉像烟不像破损"的唯一根因。
> 做法：`--size 512 --supersample 1` 渲，再用 `scripts/upscale_nearest.py` 放到交付尺寸。
> 验收用 `scripts/native_check.py 成品.gif`，逐项对照下表实测值。
>
> | 指标（512 网格） | 样本A 实测 |
> | --- | --- |
> | 点亮率 | 10.21% |
> | 实体（≥80% 帧点亮） | 8.18%，**逐帧完全静止 1.000** |
> | 碎料 | 2.03%，**1101 个独立块/帧** |
> | 单块面积 | 中位 **2**，p90 8，p99 25，max 36（最大约 15×10） |
> | 碎料运动 | 每帧漂移 **+1,+1 px**，最佳平移后仍**每帧死掉 61%** |
> | 碎料射程 | 184 px ≈ **1.0 S** |
>
> **漂移与消亡是两件事**：慢漂移给方向感，逐帧重掷给闪烁感。只做其中一个都不对。

> **人像先看这条。**
> 这一路的实体半边是**平滑的有序网点**（画法 B，`--mass 2.6`），碎的半边是**同一块网点被朝一个方向梳出去**的
> 硬边条纹——不是粒子、不是脉络、不是喷射。用 `--comb ANGLE,LEN[,SPREAD,THIN,WOBBLE]`。
> 梳出去多少，原处就少多少（`(1-R)^0.85`），质量守恒是机制自带的，不用另外"咬"。
> **纯粒子 `--mass 0` 画人像是错的**（MODULES E-8 早写过，我在这条上连续走了很多轮弯路）。
`--dissolve DEG,START,SOFT` + `--escape` + `--turbulence`。**与 M1 完全解耦**，不会替你开 mass。

**溃散是局部侵蚀，不是整体擦除。** 原作实测（4 件，PROPOSALS_EDGE）：
- 灰烬**不是飞出去的，是生在那儿的**。逃逸粒子寿命 2–3 帧、速度 1–5 px/帧，一生只挪 0.02–0.10 S。
  远场密度是**出生分布**：近场指数（e 折减 0.05–0.10 S）+ 约 1/d 的肥尾，延伸到 ~0.9 S。
  旧实现让每颗灰一生飞 0.22×画幅（长 10–30 倍），结果不是"崩解"而是**一层均匀灰雾**。
- 溃散侧的**实体本身也要变稀**：同等深度只有完好侧的 **0.46 倍**，从边界往里渐变。
- **散出去的东西要变成画面的一部分，不是消失。** 实测 样本S 单帧：核内填充 0.16，
  出核后掉到 0.05，然后**从 0.25 S 一直到 3 S 保持 0.017–0.020 的平台不再衰减**（是核的 0.10–0.12 倍）。
  没有"淡出"这回事。材料一路铺到画外，靠**结构**——分叉的丝状脉络——继续成形。
  连通笔画最长到 **0.35 S**（约单笔的 10 倍），这就是碎片长成翅膀/根系/神经的物理。
- ⚠️ **不要用"绕核心质心的径向平均"去判断飞得远不远**。翅膀是一个窄的定向叶：
  在 ±15° 扇区里 d=1 S 处可能有 0.10 填充，除以 360° 平均下来只剩 0.008，和"什么都没有"无法区分。
  本项目就是这么得出过"原作飞得不远"的错误结论（第 4 次栽在自选指标上）。要用扇区剖面或连通长度。
- 前沿是**咬出来的**：核会裂成 ≥2 块，轮廓粗糙度 2.3–6.0（光滑圆 ≈1.3），前方有脱离主体的离岛。
- 完好侧是**真空崖**：0.03 S 内从 0.86 掉到 0.006，崖外纯黑。
- 墨是**二值**的：四件各只有一个亮度值。淡出靠存活抽签，不靠 alpha。

| 参数 | 默认 | 含义 |
| --- | --- | --- |
| `--dissolve-life` | 0.10s | 溃散粒子寿命；每个周期在新位置重生（模周期数保无缝） |
| `--dissolve-near` | 0.08 | 近场 e 折减，单位 = S。**不要拿它当"飞多远"的旋钮**——调大它是把整朵云一起加厚，出来是一张均匀灰毯（实测：调到 0.26 后断口只有参照的 0.3 倍、远场是参照的 3.8 倍，contrast 从 63:1 塌到 5:1）。要飞得远，调 `--dissolve-reach` |
| `--dissolve-reach` | 0.9 | 肥尾能到多远（单位 S）。肥尾只带少量粒子，拉远它不加厚 |
| `--dissolve-floor` | 0 | 按**面积均匀**撒出去的比例 → 远场是**平台**而不是衰减尾。要"散出去的成为画面一部分"就开 0.3–0.5 |
| `--dissolve-strand` | 0.6 | 逃逸位移沿图像自己的流场**走**而不是直线弹射；越小越跟着场走 |
| `--spray` | — | `N,ANGLE,FAN[,SPEED,SPAN]` **把碎掉的像素推出去**。和雨点是同一套引擎（短命闪现、小块、逐条抽角度），区别只有两点：**发射点是碎块本身**（按溃散场播种），角度是围绕 ANGLE、半角 FAN 的**扇形**。碎片于是离开形体、越飞越开，而不是待在原地 |
| `--filament` | — | `N,LEN[,BRANCH,WIDTH,GAIN]` **让碎片长成脉络**。在溃散前沿播 N 根，顺流场积分 LEN×S 长，逐级分叉，画进 dust 权重里让笔触沿着它排队。这是"翅膀/根系"的唯一来源——散点无论多少都长不出结构 |
| `--dissolve-min` | 0.05 | 最小逃逸位移（不设下限的话灰烬落回自己核里，溃散侧永远稀不下来） |
| `--dissolve-tail` | 0.25 | 走肥尾（远场基座）的比例 |
| `--dissolve-patch` | 0.35 | **局部性**。0 = 旧的单调半平面梯度（像扫描线擦除）；0.45–0.6 出咬痕和离岛 |
| `--escape` | 1.0 | 现在的含义是**逃逸概率倍数**（P(出走)=clip(DIS×escape)），不是"飞多远" |
| `--dissolve-keepout` | — | `X,Y,RX,RY[,S]` 软椭圆：灰烬不许落进去。给画面里**另一个要自己成立的元素**留真空（余烬、亮窗、锚定物）。落进去的位移被拉短而不是删除，墨量不变；椭圆半径逐颗抖动 ±15%，免得出现一条一眼可辨的遮罩边 |
| `--escape-legacy` | — | 回到旧的长途飞行，只为复现老图 |

**三条硬规则**：
1. **必须留一块不散的。** `--dissolve START` 要让 ramp<0.05 的区域覆盖主体的 **≥25%**。
   全散 = 每颗都出走 = 整体平移+糊，不是崩解（实测：马 START 0.38 时主体只剩 4% 不散，结果是一坨模糊）。
2. **DEG 要让识别特征排在最后。** 沿 DEG 轴投影主体，识别靠哪儿哪儿就得投影值最小。
   （雕像仰头，脸在"上"——用 −90° 会**先吃眉骨和眼窝**；换成 −30° 才是"脸留住、后脑散"。先算再定。）
3. **`--conserve` 会抵消侵蚀**：它按"home 密度/当前密度"补亮，而溃散造成的变稀正是它要修的东西。
   home 密度必须按逃逸后的周期平均位置算（脚本已改）；别用旧版跑新参数。

**M4.a 断口要切在哪：切过识别特征，不是绕开它。**
参照 样本A 是「脸的**左半完整、右半崩解**」——溃散穿过脸，不是从后脑开始。
我在这个项目里连续几轮把断口放在后脑以「保住脸」，方向反了：
被打碎的那部分正是延伸并充实画面的材料，全绕开就只剩「一个完整的头 + 旁边一点灰」。

做法：把主体的识别地标沿 DEG 轴投影，读出各自的 t，再挑 START 落在**地标之间**。
雕像实测（DEG=0，t 越大越先碎）：

| 耳 | 眼窝 | 鼻梁 | 颊 | 颅顶 | 后脑 |
| --- | --- | --- | --- | --- | --- |
| 0.06 | 0.28 | 0.39 | 0.67 | 0.64 | 1.06 |

`START 0.20` → 耳与眼窝守住（ramp 0 / 0.20），鼻梁半碎（0.47），颊与颅顶全碎。
一句话：**留住一只眼 + 一条轮廓就够认了，剩下的交给画面。**
锚区仍要 ≥25%（见上文硬规则 1）。

**溃散方向 = 该作品的全局流向下游**（实测 4/4 件：溃散侧、粒子漂移、独立雨层三者同向，差 <30°）。
`--dissolve DEG` 应等于 M3 主 flow-region 的 ANGLE，不要独立拍——除非撞上规则 2，那时以规则 2 为准。
溃散比例有上限：辨识度靠轮廓的（马）不能多散；靠亮部脊线的（人像）可以散到 55%。

**验收**（`scripts/m4_erode.py 源图 成品图 DEG START SOFT FLOOR [--invert]`）：
按**源图算出的 ramp** 给成品分箱（不是按成品自己的密度分——那是自指的），
fill 应随 ramp 单调下降，最深箱 ≈ 0.33–0.50 倍。
`scripts/m4_check.py 成品图 DEG`：完好侧 0.05 S 外 <0.008、粗糙度 2.3–6.0、核 ≥2 块，
并**逐箱对照 样本A 实测剖面**（偏离 >1.8× 或 <0.55× 会标出来）。断口/远场比值要在 20:1 以上。

### M5 空域层（与 M1–M4 无关的独立层）

雨点的模型是照原作动画逐帧实测标定的，**不要凭感觉改**：

| 参数 | 值 | 实测依据 |
| --- | --- | --- |
| `--rain-block` | 1.1 | 原作网点面积中位 4px²@2560 ≈ 边长 1.1 输出 px |
| `--rain-short` | 4 | 长度分位 4/4/5/7/17，**中位只有 5px**，长条只占 5% |
| `--rain-spread` | 25 | 链间角差在 200px 和 1200px 上**相同** → 逐条独立抽，不是平滑场 |
| `--rain-flow` | 0 | 同上；平滑角度场是错的结构 |
| `--rain-life` | 0.16s | 原位残留为零、像素存活 14.7%、轨迹从不超过 4 步 → **短命闪现不是巡航** |
| `--rain-px-per-sec` | 0.6 | 位移中位 1600px/s @2560 = 0.62 画幅/秒 |
| `--rain-speed-len` | 0.5 | 位移与长度相关 +0.36 |
| `--rain-taper` / `--rain-spacing` | 1.0 / 1.0 | 链内**无梯度**。调成非 1 会出现流星尾，那不是这个语言里的东西 |
| `--rain-keepout X,Y,RX,RY` | 视构图 | 软椭圆禁入：不生成也不飞入，边缘淡出 |

`--rain-wild` 一个数放大四个维度的离散度。**重尾分布调大方差时必须同时封顶**，否则调的不是「更乱」而是「偶尔炸一个巨块」。

### M6 输出
`--palette` `--size` `--seconds` `--fps` `--mp4` `--seed`。
无缝循环校验：|末帧−首帧| ≈ 相邻帧差。参照的节奏是 **0.48s / 12 帧 / 25fps** 的快循环。

---

## 调参纪律

- **不要用静帧调流动。** `--static-only` 时 frames=1，粒子完全不推进——凡是涉及 flow/boil/conserve 的调参，静帧测的是另一张画。
- **不要相信自己临时写的分析程序。** 本项目已三次栽在这上面：聚类阈值造出了原图没有的「彗星尾」；手放的取样框让同一张图得出 0.16σ 与 0.59σ 两个相反判断；「墨在核内的份额」这个指标是自指的（核由渲染图自己的密度定义），越加外围墨它反而越高。**凡用到阈值/网格/聚类半径的地方，先换两三个取值看结论翻不翻。**
- **参考是动画就必须按动画取证。** 静帧给不出速度、寿命、相位。而且要确认抓到的是**相邻帧**——间隔 1 秒的截屏落在 0.48s 循环的随机相位上，测出来的位移全是假的。

## 常见失败

- 蚯蚓状意义不明的亮线 → `--conserve 1.5` 没开
- 光柱向上流 / 方向违反物理 → 用了全局 flow-bias 兜所有区，改逐元素 `--flow-region`
- 满屏中长线发土 → `--length-mix` 调到 3，`--density` 降到 1.5 以下
- 主体像剪纸／贴纸 → **先分清是哪一种**。画法 A（纯粒子）出现这个，是做了无差别的边缘填充，去掉它；
  画法 B（网点）出现这个，是源图内部被压死（近三成面积顶到 80% 占空比以上），
  要回 M0.5 造体积明暗，**不是回纯粒子**——人像回了纯粒子只会更糊。量一下再决定：
  按 8×8 格统计占空比，中间段（35–65%）占比低于 15% 就是压死了。
- 主体成了铁丝网（只有轮廓、内部空） → 拿纹理能量当形体了。见 M0.5 P4 的陷阱
- 雨点像蛆 → 给单根加了曲率。原作每根都是**直的**，活力来自多条不同角度的短命闪现交错
- 雨点像流星有尾巴 → `--rain-taper` 不是 1

## 边界与伦理

细节（五官、文字、小窗）必然消失；平缓明暗过渡会变均匀绒毛。适合「一个大形 + 强对比」。

算法与代码全部原创，输出由确定性脚本渲出，不含任何模型。参数中的目标常数来自对一批
公开发布的粒子动画作品的逐帧实测——测量的是可量化的统计特征（网格尺度、点亮率、
块面积分布、帧间留存），不复制任何具体作品的画面、构图或主体。不得以他人名义署名或宣传。

