# 机械：在任何画风里，把一个想法造出来讲清楚

机械是一种主题，不是一种观感。套件（`engine/src/canvas-core/mechanismKit.ts`）在 3D 中造出真实的零件，给它们排序，把它们拆开再收拢回去；一种**手笔**用某个画风自己的媒介给它们落下笔触。同一个八音盒可以画成蓝晒、彩色铅笔、点画或平涂的等轴平面。完整示例：`mechanism.ts`（18 音的机芯）、`mechanismHands.ts`、`mechanismExplorer.ts`。

## 1. 把想法变成机械

1. **先拿到事实。** 这个想法里真实的量：有多少、有多大、有多快、极限在哪里（[explainer.md](explainer.md)）。
2. **说出什么作用在什么上：** 动作（填满、等待、匹配、拒绝、衰减），以及这些动作作用的东西。
3. **找一个做这些动作的人造物件，** 一个普通读者认得出的东西：用旋转闸门表示「一次一个」，用筛子表示过滤器，用棘轮表示「只能向前」，用水闸表示队列。
4. **大声检验这个对应：** 先描述机械运转的样子，再描述这个想法；如果两段描述不能一步一步对上，就换一个物件。
5. **每个零件都有意义，** 并且要说出这个比喻从哪里开始不再是字面上成立的。
6. **一个零件承载故事。** 只有它得到强调（在它的材质上设 `accent: true`）。
7. **把数字展示出来：** 一个表盘、一把尺子、一个计数器，或者读数角实时读出这个量。
8. **把它转过来面向观众：** `Cam.fit(A, E, points, box)`。八音盒放在 36 的方位角上；大多数机械在直角的三分之一到三分之二之间的某个角度看起来是对的。

机械必须是一个人能叫得出名字的东西，每个零件都在干活。悬在空中的带标签方块，不给观众任何可以叫出名字的东西；一大块光秃秃的面板，上面只有一两道细节划痕，看起来就像一幅有人画到一半就停下了的画。

## 2. 先列零件清单，再做几何

先写清单，从地面往上：它所放置的工作台或支脚，承载运动的框架，运动部件，人会伸手去拿的把手和旋钮，最后是故事所讲的那一个零件。没有东西悬空，没有东西闲着：每个零件都搁在某样东西上，并且挣得了自己的位置。作为参照，八音盒有 68 个零件，大约占它卡片的 62%。如果你的机械想要的零件远多于此，就造一个保留核心的精简版。丰富感应该来自构造（轴与板相接处的一个凸台、一排钻孔、一根止动杆），而不是来自画在表面上的图案。当某个面看起来空时，问一问那里该有什么真实的零件。

## 3. 一个模型驱动一切

所有会动的东西、读数和每一条说明文字，都由一个以 `(frame)` 为参数（在交互作品里则以输入状态为参数）、没有副作用的函数计算出来。在八音盒里，`play(rot)` 放置销钉并弯曲音梳的齿，同一次调用还为读数给出销钉、音齿和音高的名字，所以没有什么需要写两遍。文字取自实时状态，绝不取自一份在旁边同步运行的脚本：读数说出刚刚拨响的是哪一根销钉，是因为它根据滚筒的角度算出来的，而不是因为一条定时字幕这么说。计数来自画零件的那份列表（「8 ASSEMBLIES OUT」就是 `PLAN.order.length`）。

## 4. 用套件来造

| 需要 | 用 |
|---|---|
| 镜头、深度、屏幕上的世界偏移 | `Cam`、`Cam.fit`、`c.depth`、`c.dv` |
| 厚板、平板、带圆角平面的箱体、接缝、倒角 | `prism`、`box`、`rrect`、`circle` |
| 滚筒、立柱、旋钮、穹顶：任何回转面 | `turned` 配合 `cyl`、`cone`、`sphere`；明暗分带是精确的，绝不呈阶梯状 |
| 任意平面上的杠杆、叶片、支架 | `slab` 配合 `leverRing` 或任何凸轮廓 |
| 沿一条路线走的管子、电线、电缆 | `tubeAlong`（带圆角的弯折、端盖） |
| 螺丝、销钉、刻度、肋条，转开时渐渐淡去 | `screw`、`facingAt` + `smooth` 控制透明度 |
| 很多零件，按正确的顺序 | `paintAll(items)`；每一项都有 `at`、`bias`、`ride`（它所属的组件） |
| 爆炸图 | `ExplodePlan` + `sharesAt(plan, cues, t)`：错开着往外拆，最后出去的最先回家，反向操作会取消尚未执行的移动 |
| 一张给图形加框、并给每个角分派一项工作的卡片：标题、提示、实时读数 | `card` |

保持干净的规则：把包住另一个零件的零件拆开（后半、内部、前半）；每个零件都被某样东西托着；沿真实的方向拆开真实的组件，外面的先拆；沿每一条行进路径画一条点划线；在卡片上给图形四周留出宽裕的边距。

## 5. 用一种画风来画：手笔

套件从不选颜色。它带着含义去调用当前的手笔：一个区域和它的色调 `q`（0 是最暗的一侧，4 是顶面），一条线和它的角色（`out`、`lit`、`in`、`crease`、`seam`、`hair`、`light`、`chain`），一个点，卡片的纸面、分隔线和文字。画之前 `setHand(h)`，画完之后 `setHand(flatHand())`。已附带的手笔：`isometric`（快，默认）、`blueprint`、`colouredPencil`、`stipple`。要加一种，就去读 [`references/styles/<id>.md`](../styles/index.md) 里那个画风的配方，然后用代码回答五个问题：

1. **有光吗？** 蓝图没有（色调就是剖面线，只画在要看的那个零件上）；点画的光是点的密度；铅笔的光是压力和叠加的遍数。
2. **什么是边？** 一条尺规画的线、一条在暗部一侧更重的断续铅笔轮廓、一排更密的点，或者什么都没有（等轴平面以色调对色调相接）。
3. **强调怎么做？** 一种颜色、剖面线，或者密度。
4. **底是什么？** 把它一次性画进一个缓存图层；`knock` 把它重新画回到一个零件上面。
5. **笔触锚定了吗？** 把排线和点锚定到 `offset()` 上，绝不锚定到屏幕，并且用零件给它们做种子，这样静止的零件永远不会抖动（boil），移动的零件会带着它的笔触一起走。

## 6. 影片与交互作品

两部影片，而不是一部：制作过程（`kind: "drawing"`，每个零件按组装顺序先上墨、再上色调，用 `setReveal(ink, tone, paper)`，然后是文字）和运转过程（`kind: "explainer"`：贴近要看的那个零件播放，爆炸拆开，巡览，归位）。一部影片同时装下两者会过不了冷场闸门，因为构建过程里缓慢的笔画会被拿去和讲解片的下限比较。拆开期间，让每个组件沿着自己的路径轻轻呼吸，彼此错开节拍，这样巡览永远不会冻住。交互作品（[interactive.md](interactive.md)）：每个组件各坐一根自己的弹簧，用 `zeta` 0.8 到 0.9 做阻尼，让它带着分量落定，外层的组件用更快的弹簧；镜头固定，这样卡片和工作台只烘焙一次；离指针最近的零件亮起，读数说出它是做什么的；减弱动态效果时滚筒保持静止。

## 7. 检查

用 `--scale 2` 或 3 渲染，裁出每一个接合处（法兰、桥、曲柄、线头、齿尖）来看；要找的是：一个本该在某物前面却被画到了它后面的零件；两个本该接触却重叠或分开悬着的零件；一笔落在空处的笔画；一条画了两遍的边；一个在面中间戛然而止、而不是渐渐淡出的图案。然后跑 `node tools/test.mjs mechanism`，如果是页面，再跑 `emit-interactive.mjs` 和 `replay.mjs`（22 项检查，其中包括 DPR 2 下每帧 6 ms）。要实测，不要假设：说清楚这些你跑了哪几个。
