teardown 是做什么的?
一台机器,一个页面。访客看到机器运行,按下按钮,执行工作的部件便会展开,同时周围仪表显示真实数据。若无人操作,页面会通过光标自动导览。同一段代码会逐帧录制导览的影片,并使用同一时间轴生成配乐。
下面的 SKILL_DIR 指的是该文件所在的文件夹(对于 Cursor 安装,通常是 ~/.cursor/skills/teardown、~/.agents/skills/teardown 或项目内的 .cursor/skills/teardown)。构建和渲染结果将输出到您运行命令的文件夹下的 ./teardown-out 目录。
首次运行
cd "$SKILL_DIR" && npm install && npx playwright install chromium
页面不需要其他依赖。视频录制需要 ffmpeg 在 PATH 环境变量中,配乐生成需要 Python 3(仅使用标准库)。您可以通过设置 FFMPEG、PYTHON 或 CHROMIUM 环境变量来指定其他二进制文件路径。
3个示例
构建一个,打开HTML,按下按钮。开始新机器时,从最接近的示例复制代码。
| 机器 | 驱动方式 | 值得借鉴的部分 | |
|---|---|---|---|
v8-* | 带玻璃缸套的十字曲轴V8发动机:您追踪的汽缸内燃烧过程清晰可见,气门在凸轮上抬起,8个活塞通过精确的曲柄滑块机构运动。 | 曲轴角度。点火顺序1-8-4-3-6-5-7-2,以及用于示功图的缸压模型。 | 剖切模式(玻璃、截面、实体),点火顺序作为气缸选择器,与曲轴同步的实时图表。 |
cessna-* | Cessna 337飞机的主起落架,在起飞时收进机身,降落时放下。 | 行程0..1:一个201毫米的齿条驱动一个32齿的齿轮(模数4毫米),精确旋转180°。 | 一个自由度驱动整个机构,飞行阶段作为时间的函数,在模型下方滚动的跑道。 |
strandbeest-* | Theo Jansen的Strandbeest,一个曲轴上连接20条腿,在沙地上横向行走。 | 风力。曲轴速度由风力积分得出;每个关节通过Jansen的11个连杆长度的圆交点计算得出。 | 一个仅67行的精确连杆求解器,实例化管道,脚部轨迹,所有20只脚的接触图表。 |
每个示例包含3个文件夹:<name>-shared/ 存放机器,<name>-film/ 存放录制的导览,<name>-live/ 存放可驱动的页面。
node "$SKILL_DIR/sims/build.mjs" v8-live # → teardown-out/dist/v8-live.html,离线,约800 KB
node "$SKILL_DIR/sims/build.mjs" # 构建全部6个
如何构建一台机器
每个机器一个文件夹,按职责拆分。示例严格遵循以下结构:
| 文件 | 职责 |
|---|---|
kin.js / gear.js / jansen.js | 数学计算。使用真实单位和真实尺寸,一个输入(曲轴角度、行程、风力),所有姿态和读数都作为该输入的函数。这里没有 three.js 代码。 |
engine.js / gear.js / beast.js | 几何体,通过代码构建:车削部件、管道、实例化连杆。update(input) 方法用于更新姿态。 |
stage.js | 场景、灯光、阴影、后期处理(GTAO、泛光)。frame(state, cam) 方法绘制一帧。 |
console.js | 叠加层:固定的 1600 x 900 CSS 布局,包含面板、图表、开关、带引出线的标注。update(state) 方法根据当前帧的状态重新绘制。 |
film.js | 一个操作者动作的剧本(视角切换、隔离部件、剖切、油门控制)以及 render(t) 作为时间的纯函数。 |
live.js | 同一机器的实时交互版本:按钮、滑块、轨道控制、缩放、键盘。若无输入,则播放 film.js 的剧本作为带有光标的自动导览;任何点击、拖拽、滚轮或按键操作都会接管控制权,静默25秒后交回控制权。 |
score.py | 影片时间轴上的配乐。 |
对于 SKILL_DIR 之外的机器,创建一个包含 main.js 的文件夹,并通过名称导入工具包:import {runSim} from 'teardown-kit/runtime.js'。使用命令构建:node "$SKILL_DIR/sims/build.mjs" ./my-machine。
决定成败的规则
- 从参考开始,而非凭空想象。 一张照片、一张手册里的剖视图、一段真实机器的视频片段。以此为参考进行构建并逐帧对比。凭空想象出来的机器总是显得千篇一律;而拥有对照物的模型则可以精确地重建真实机器。
- 使用真实数字,单一来源。 首先获取真实尺寸(缸径、行程、齿数、连杆长度)。每个仪表、图表和标签都读取与驱动部件姿态相同的函数,因此屏幕上没有任何内容会与运动相矛盾。手动验证一个:在 Cessna 示例中,32齿 × 4毫米模数 = 128毫米节圆直径,其半周长即为201毫米的行程。
- 单一输入驱动一切。 根据曲轴角度、行程或风力进行姿态调整。没有任何手动关键帧,因此影片、实时页面和配乐永远不会漂移不同步。
- 执行工作的部件应当展开。 隔离它、剖切它、透视它。一个能进入内部的摄像机镜头胜过十个外部轨道镜头。
- 灯光工作室,一种强调色。 浅色背景在信息流中更易阅读;深色背景会丢失最初几秒的注意力。强调色用于标记正在变化的部分。注意浅色背景上的近白色零件:为其添加墨线轮廓或更暗的地面。
- 根据机构选择摄像机角度。 正视视角下,平面连杆机构会折叠成网状;一个沿其脊柱的3/4视角则能让每条腿都清晰可辨。
- 用数字修复碰撞。 当一个零件穿透另一个零件时,在参数块中缩放或移动它。切勿用摄像机视角来隐藏它。
实时页面规则
- 在集成显卡上测试。 在台式机显卡上能以60fps运行的页面,在笔记本电脑的内置显卡上可能降至15fps。示例中设置了质量等级(
live.js中的TIERS):禁用环境光遮蔽、降低像素比、使用更小的阴影贴图、隔帧重绘面板。首先找到最耗性能的着色器:一个每个顶点计算5次的置换位移,比所有质量等级加起来都更耗费性能。 - 预热着色器。
kit/warm.js在第一帧渲染前并行编译所有程序,确保页面在首次绘制时不会卡顿。 - 保持页面小于1 MB。 链接爬虫(如X、Slack)会从HTML中读取分享卡片,对于一个15MB的页面会选择放弃。大型模型应作为独立文件放在页面旁边:
build.json→{"external": {"heavy.glb": "light.glb"}}。
render(t) 契约
import {runSim} from '../kit/runtime.js';
const DEFAULTS = {duration: 20, fps: 60, hud: {enabled: false}, director: {enabled: false}};
runSim(DEFAULTS, async ({cfg, canvas, capture}) => {
// 构建场景,预计算任何有状态的内容
return {render(t) { /* 从头绘制第t帧 */ return {}; }, resize() {}};
});
render(t)是纯函数:没有Math.random(在设置时使用kit/util.js中的rng(seed)),没有Date,帧之间不携带状态。捕获系统会在任意时间点调用它。- 任何按自身规则运动的对象(智能体、粒子)在加载时被模拟到类型化数组中;
render只读取它们。随时间积分变化的量(如Strandbeest在变化风力下的曲轴)以闭合形式积分或在加载时制成表格。 - 每个节拍都写入
<name>-film/timeline.json,单位是秒。配乐读取相同的文件。 - 第0帧是缩略图。它必须已经展示出机器。
命令
node "$SKILL_DIR/sims/build.mjs" v8-film
node "$SKILL_DIR/sims/capture.mjs" v8-film --size 1600x900 --dpr 1.2 --stills beats --cols 4 --out teardown-out/v8-beats.png
node "$SKILL_DIR/sims/capture.mjs" v8-film --size 1600x900 --dpr 1.2 --still 12.5 --out teardown-out/v8-12.5.png
node "$SKILL_DIR/sims/capture.mjs" v8-film --size 1600x900 --dpr 1.2 --out teardown-out/v8-silent.mp4
python "$SKILL_DIR/run_sfx.py" "$SKILL_DIR/sims/v8-shared/score.py"
ffmpeg -y -i teardown-out/v8-silent.mp4 -i teardown-out/v8-score.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 256k -shortest -movflags +faststart teardown-out/v8.mp4
node "$SKILL_DIR/wave.mjs" teardown-out/v8.mp4 --timeline "$SKILL_DIR/sims/v8-film/timeline.json"
--size 是CSS布局尺寸,--dpr 是倍率:1600 x 900 在 1.2 dpr 下输出 1920 x 1080。捕获时会打印所使用的GPU;如果显示为软件渲染器,则表明 paths.mjs 中的GPU标志未生效。
审查循环
您无法观看视频,但可以查看帧。切勿在未检查当前渲染的多帧图之前就报告机器已完成。
--stills beats:每个时间轴节拍生成一张平铺图,节拍后0.3秒,每张标注t=。- 对照下面的检查清单逐一检查每张图。
- 放大查看:
--stills every:0.2 --from A --to B用于检查过渡动画,--still T用于查看一整帧。 - 修复,重建,返回步骤1。当完整一轮检查未发现任何问题时停止。
- 最终检查:
node "$SKILL_DIR/sheet.mjs" --video <final.mp4> --timeline <timeline.json>,并再次审查该输出:它能显示编码器实际写入了什么。
检查清单:
- 机械机构。 每个应接触的部件都接触了;没有部件穿过其他部件;面板上的数字与姿态匹配。
- 构图。 每帧一个焦点。没有重要内容距离边缘60像素以内。标签永远不会遮盖其命名的部件。
- 文字。 无文字被裁剪。关键读数在手机宽度屏幕上也可辨认。
- 3D。 泛光会吞噬细节:保持自发光层强度低于1.0。近白色表面容易过曝。弱光下的光泽金属会产生条纹:大面积表面的粗糙度应 ≥ 0.45,环境贴图强度 ≈ 0.5。
- 节奏。 每个状态停留时间足够长以被阅读:短句1秒,超过三个单词后每多一个词增加0.25秒。
示例已踩过的 three.js 陷阱
smoothstep(a, b, x)在a > b时,Windows系统(ANGLE/D3D)会返回0。请写成1.0 - smoothstep(b, a, x)。- 离地面仅零点几毫米的
ShadowMaterial或贴花,在后处理渲染目标内会丢失深度测试。请使用polygonOffset。 - 放置在地面上带有镜面映射的四边形,会被作为背面剔除。请使用
DoubleSide。 - 一个NaN像素会通过泛光效应导致整帧变黑。请确保在着色器中钳制所有
pow()函数的底数。 - 对于被裁剪(剖切)的模型使用GTAO时,需要在
gtao.normalMaterial上设置相同的裁剪平面,否则被切掉的部分会以浅色鬼影形式出现。
音效
sfx/ 文件夹使用纯Python(48kHz,可重现)合成所有声音,并读取影片的时间轴,确保音效与画面精确同步。python "$SKILL_DIR/run_sfx.py" --list 可列出可用的发生器。sample() 函数用于添加 sfx/samples/ 中的CC0录音。sfx/score_kit.py 包含三个配乐示例使用的基础音色和强调音:和弦背景、每个作功行程的柔和重击声、马达声、气流风声。
- 所有时间点均来自时间轴,绝不手动输入数字。
- 母带处理是自动的:响度 −14 LUFS,真实峰值 ≤ −2 dBFS。
- 强调而非叙述:连续运动保持静音。每个音调声音只使用一个键。
品味,以数字量化
- 缓动:进入动画
cubic-bezier(0.16, 1, 0.3, 1),退出动画cubic-bezier(0.6, 0, 0.9, 0.35)时长是进入动画的一半,状态切换cubic-bezier(0.65, 0, 0.35, 1)。仅在连续运动中使用线性缓动。 - 机器应占据视口的60-70%。在1080画布上,标签字体不小于34px。
- 一种强调色,且必须具有意义:标记当前正在工作的部件。
- 不能有任何元素是完全静止的:至少有一个元素始终处于运动状态。
致谢
Cursor 移植版由 bugeng777/teardown-cursor 基于 elvarofficial/teardown (MIT 许可证) 完成。工具包、帧捕获、音效引擎和审查规则来自 whaleyxbt/claude-motion (MIT 许可证)。录制的样本为 Freesound 上的 CC0 素材,由 Barty-Bart/motion-graphics (MIT 许可证)剪辑;来源见 sfx/samples/SOURCES.md。