一次完整的 AI 编程实验记录:从一段 8 行的英文提示词出发,让 Claude Code 生成一个单文件
index.html------一台正在运转的 V8 发动机 3D 剖视动画,再由它自己截图自检、修 bug,最后逐帧渲染导出 GIF 与 MP4 视频。
缩写对照表
| 缩写 | 英文全称 | 中文 |
|---|---|---|
| RPM | Revolutions Per Minute | 每分钟转数(转速) |
| TDC | Top Dead Center | 上止点 |
| BTDC | Before Top Dead Center | 上止点前 |
| SOHC | Single Overhead Camshaft | 单顶置凸轮轴 |
| GM | General Motors | 通用汽车 |
| HUD | Heads-Up Display | 抬头显示(界面叠加信息层) |
| CDN | Content Delivery Network | 内容分发网络 |
| PBR | Physically Based Rendering | 基于物理的渲染 |
| PMREM | Prefiltered Mipmapped Radiance Environment Map | 预过滤 Mipmap 辐射环境贴图 |
| ACES | Academy Color Encoding System | 学院色彩编码系统(色调映射曲线) |
| SVG | Scalable Vector Graphics | 可缩放矢量图形 |
| fps | frames per second | 每秒帧数 |
| GIF | Graphics Interchange Format | 图形交换格式 |
| MP4 | MPEG-4 Part 14 | MPEG-4 第 14 部分视频容器 |
一、实验目标
想验证一件事:只给一段自然语言需求,AI 编程代理(Claude Code)能不能独立做出一个"机械上说得通"的 3D 运动机械动画------不是随便转几个圆柱体,而是活塞、连杆、曲轴、凸轮、气门、点火之间的相位关系都对得上。
给 Claude Code 的原始提示词(原文照录):
text
Build a single-file index.html: an interactive 3D cutaway of a running
V8 car engine in three.js. 8 pistons in a V on a rotating crankshaft,
two camshafts opening valves in the real firing order (1-8-4-3-6-5-7-2),
a spark flash in each chamber when it fires, a spinning flywheel.
Bright studio look: light warm gradient, soft shadows, chrome and steel
plus red valve covers and blue cutaway faces. Small clean labels.
Camera orbits 120 degrees over 16 s, engine always fully in frame.
RPM ramps 900 -> 6500 between t=0 and t=12, then holds.
这段需求里藏着几个硬约束:
- 真实点火顺序 1-8-4-3-6-5-7-2(通用汽车 GM(General Motors,通用汽车)小缸体 V8 的经典顺序),气门开闭和火花必须按它走;
- 镜头 16 秒环绕 120° ,且发动机始终完整在画面内;
- 转速 RPM(Revolutions Per Minute,每分钟转数) 在 0--12 秒从 900 升到 6500,然后保持;
- 明亮的棚拍风格:暖色渐变背景、柔和阴影、镀铬/钢材质、红色气门室盖、蓝色剖切面。
二、最终效果

正面视角:两侧气缸组都被沿气缸轴线剖开,能看到活塞、连杆、曲轴和顶置凸轮轴;正在做功的燃烧室呈橙色。

侧面视角:转速已稳定在 6500 RPM;标签自动切换到"剖面朝向镜头"的那一侧气缸组。

成品是一个约 760 行的单文件 index.html,只依赖从 CDN(Content Delivery Network,内容分发网络)加载的 three.js r160,没有任何构建步骤。右上角是转速表,左下角是点火顺序指示灯(哪个缸点火哪个亮),右下角是材质图例。
三、整体流程
整个实验在一个 Claude Code 会话里完成,人只做了三件事:贴需求、要求导出视频、要求发布本文。其余的设计推导、编码、自检、修复和导出都由代理完成。
#mermaid-svg-rbbnWQvxeVitUSoG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rbbnWQvxeVitUSoG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rbbnWQvxeVitUSoG .error-icon{fill:#552222;}#mermaid-svg-rbbnWQvxeVitUSoG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rbbnWQvxeVitUSoG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rbbnWQvxeVitUSoG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rbbnWQvxeVitUSoG .marker.cross{stroke:#333333;}#mermaid-svg-rbbnWQvxeVitUSoG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rbbnWQvxeVitUSoG p{margin:0;}#mermaid-svg-rbbnWQvxeVitUSoG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rbbnWQvxeVitUSoG .cluster-label text{fill:#333;}#mermaid-svg-rbbnWQvxeVitUSoG .cluster-label span{color:#333;}#mermaid-svg-rbbnWQvxeVitUSoG .cluster-label span p{background-color:transparent;}#mermaid-svg-rbbnWQvxeVitUSoG .label text,#mermaid-svg-rbbnWQvxeVitUSoG span{fill:#333;color:#333;}#mermaid-svg-rbbnWQvxeVitUSoG .node rect,#mermaid-svg-rbbnWQvxeVitUSoG .node circle,#mermaid-svg-rbbnWQvxeVitUSoG .node ellipse,#mermaid-svg-rbbnWQvxeVitUSoG .node polygon,#mermaid-svg-rbbnWQvxeVitUSoG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rbbnWQvxeVitUSoG .rough-node .label text,#mermaid-svg-rbbnWQvxeVitUSoG .node .label text,#mermaid-svg-rbbnWQvxeVitUSoG .image-shape .label,#mermaid-svg-rbbnWQvxeVitUSoG .icon-shape .label{text-anchor:middle;}#mermaid-svg-rbbnWQvxeVitUSoG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rbbnWQvxeVitUSoG .rough-node .label,#mermaid-svg-rbbnWQvxeVitUSoG .node .label,#mermaid-svg-rbbnWQvxeVitUSoG .image-shape .label,#mermaid-svg-rbbnWQvxeVitUSoG .icon-shape .label{text-align:center;}#mermaid-svg-rbbnWQvxeVitUSoG .node.clickable{cursor:pointer;}#mermaid-svg-rbbnWQvxeVitUSoG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rbbnWQvxeVitUSoG .arrowheadPath{fill:#333333;}#mermaid-svg-rbbnWQvxeVitUSoG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rbbnWQvxeVitUSoG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rbbnWQvxeVitUSoG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rbbnWQvxeVitUSoG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rbbnWQvxeVitUSoG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rbbnWQvxeVitUSoG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rbbnWQvxeVitUSoG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rbbnWQvxeVitUSoG .cluster text{fill:#333;}#mermaid-svg-rbbnWQvxeVitUSoG .cluster span{color:#333;}#mermaid-svg-rbbnWQvxeVitUSoG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rbbnWQvxeVitUSoG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rbbnWQvxeVitUSoG rect.text{fill:none;stroke-width:0;}#mermaid-svg-rbbnWQvxeVitUSoG .icon-shape,#mermaid-svg-rbbnWQvxeVitUSoG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rbbnWQvxeVitUSoG .icon-shape p,#mermaid-svg-rbbnWQvxeVitUSoG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rbbnWQvxeVitUSoG .icon-shape .label rect,#mermaid-svg-rbbnWQvxeVitUSoG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rbbnWQvxeVitUSoG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rbbnWQvxeVitUSoG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rbbnWQvxeVitUSoG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
提示词
8 行英文需求
机械设计推导
曲柄角/凸轮相位
编写单文件
index.html
无头 Chrome
多时间点截图
截图自检
发现问题?
定位并修复
dt/标签/遮挡
加入固定步长
捕获钩子
Playwright
逐帧截图 960 帧
ffmpeg 编码
MP4 + GIF
下面按"设计 → 实现 → 自检 → 导出"展开,重点讲几个决定动画"对不对"的技术点。
四、关键设计:让机械相位真正对上
4.1 从点火顺序反推曲柄销角度
这是整个项目最核心的一步。代理没有去"查一张 V8 曲轴图",而是从约束直接推导:
- 两列气缸组夹角 90°,左列(奇数缸 1/3/5/7)轴线方向 φ = +45°,右列(偶数缸)φ = −45°;
- 四冲程一个循环 720° 曲轴转角,8 个缸均匀点火,点火间隔 90°;按 1-8-4-3-6-5-7-2 排序,第 k 个点火的缸在曲轴转角 k×90° 时到达压缩上止点 TDC(Top Dead Center,上止点);
- 某缸到达上止点 ⇔ 它的曲柄销方向与本列气缸轴线重合,所以该缸曲柄销角度 α = φ − 点火角(mod 360°)。
代入 8 个缸算出来的结果:
| 缸号 | 气缸列 | 点火角 | 曲柄销角 α |
|---|---|---|---|
| 1 / 2 | 左 / 右 | 0° / 630° | 45° |
| 3 / 4 | 左 / 右 | 270° / 180° | 135° |
| 5 / 6 | 左 / 右 | 450° / 360° | 315° |
| 7 / 8 | 左 / 右 | 540° / 90° | 225° |
两个漂亮的结论自动浮现:每对相邻缸(1&2、3&4...)恰好共用一个曲柄销 ,而 4 个曲柄销角度 45°/135°/315°/225° 两两相差 90°------这正是真实 V8 的十字形(crossplane)曲轴。也就是说,只要点火顺序和夹角对,曲轴结构是"推"出来的,不是"画"出来的。
4.2 活塞位置:标准曲柄连杆公式
每个活塞的位置用经典曲柄-滑块公式计算,ψ 是曲柄销与本列气缸轴线的夹角,r 为曲柄半径,l 为连杆长度:
js
const psi = pin - cy.phi, rs = CRANK_R * Math.sin(psi);
const s = CRANK_R * Math.cos(psi) + Math.sqrt(ROD_L * ROD_L - rs * rs);
cy.piston.position.y = s; // 沿气缸轴线的位移
cy.rod.position.copy(P); // 连杆大头在曲柄销上
cy.rod.quaternion.setFromUnitVectors(UP, Dv.subVectors(Q, P).normalize());
连杆两端分别"钉"在曲柄销和活塞销上,所以连杆的摆动是算出来的,不会出现穿模或脱节。
4.3 凸轮与气门:同一条曲线驱动两件事
需求要求两根凸轮轴,所以代理按单顶置凸轮轴 SOHC(Single Overhead Camshaft,单顶置凸轮轴)布局,每列一根,转速为曲轴的一半。关键技巧是:气门升程函数和凸轮桃尖的几何轮廓用的是同一条余弦曲线------
- 排气门在循环角 255° 处升程最大,进气门在 465°,各自开启宽度 ±125° 曲轴转角(两者在上止点附近有一小段重叠,和真实发动机一致);
- 凸轮转角是曲轴的一半,所以凸轮轮廓的半宽是 62.5°,每个凸轮桃尖的安装角按
(点火角 + 峰值角) / 2 − 180°计算。
结果是:画面上桃尖转到正下方的那一刻,气门正好开到最大,挺柱始终贴着凸轮表面------这是"看起来对"的关键。
4.4 点火:用"跨越检测"而不是角度窗口
火花在压缩上止点前 10°,即 BTDC(Before Top Dead Center,上止点前)10° 触发。代理没有用"当前角度是否落在某个窗口内"来判断,因为高转速时一帧可能跨过整个窗口而漏掉。它改用累计计数:
js
const count = Math.floor((theta - cy.fire + 10) / 720);
if (count !== cy.count) { cy.count = count; cy.flash = 1; cy.spark = 1; }
cy.flash *= Math.exp(-dt / 0.13); // 按真实时间衰减,而非按曲轴角
只要这一帧跨过了点火角,就一定触发;闪光按真实时间指数衰减,保证在任何转速下都看得见。燃烧室里的橙色体积随活塞下行而拉长,模拟做功冲程。
4.5 一个必须做的取舍:1/25 慢放
这是代理主动提出、并在界面上明确标注的设计决定。6500 RPM 意味着曲轴每秒转 108 圈,在 60 fps(frames per second,每秒帧数)下每帧转过约 650° ------活塞会因为时间混叠(aliasing)看起来静止、倒转或乱跳。所以动画按 1/25 真实速度播放,而转速表显示的仍是真实 RPM 曲线。这比"偷偷把转速改低"更诚实:数值符合需求,画面又看得清。
五、关键实现:剖切、构图与标签
5.1 剖切面:按法线把三角面分到不同材质
"蓝色剖切面"是最容易做丑的部分。代理的做法是:每列缸体沿"包含所有气缸轴线的平面"剖开,保留外侧一半、去掉 V 形谷内侧一半 ;缸体截面画成一个带半圆缺口(缸孔)的二维形状,再沿气缸轴线拉伸(ExtrudeGeometry)。
拉伸后,用一个通用函数按三角面的位置和法线重新分组:
js
// 0 = 普通表面, 1 = 蓝色剖切面, 2 = 缸孔内壁(并替换为平滑法线)
function sectionize(geo, classify, smooth) { /* 遍历三角形 → 分桶 → addGroup */ }
mesh(sectionize(block, (c, n) => {
if (Math.abs(c.x) < 1e-3 && n.x * sigma < -0.9) return 1; // 位于剖切平面且朝外
if (Math.abs(n.y) < 0.2 && nearBoreWall(c)) return 2; // 缸孔内壁
return 0;
}), [M.alu, M.cut, M.liner], g);
同一套方法复用在缸盖(带气门腔缺口)、气门室盖(红色薄壳)和下曲轴箱上。因为两列缸体剖面分别朝左上、右上,镜头在环绕时始终至少有一列的内部是可见的。
5.2 "始终完整在画面内":包围球反算相机距离
js
const vf = camera.fov * DEG;
const hf = 2 * Math.atan(Math.tan(vf / 2) * camera.aspect); // 水平视角
camDist = (halfDiag / Math.sin(Math.min(vf, hf) / 2)) * 1.06; // 取较窄的视角
用包围盒半对角线作为保守半径,取垂直/水平视角中较小的一个反算距离;窗口缩放时重新计算。所以无论宽屏还是竖屏,任何环绕角度下发动机都不会出画。
5.3 标签:跟随"可见的那一侧"并自动避让
标签用 HTML 叠加层 + SVG(Scalable Vector Graphics,可缩放矢量图形)引线实现,每帧把三维锚点投影到屏幕。两个细节是截图自检后才加上的(见下一节):
- 双锚点:"活塞""凸轮轴"等标签在两列缸体上各有一个锚点,每帧根据镜头方向与两列剖切面法线的点积,选择剖面正对镜头的那一列;
- 防重叠:按纵坐标排序后逐个放置,与已放置标签重叠就向下推。
六、自检:代理如何发现并修掉自己的 bug
这次实验里最值得记录的是:代理不是写完就交付,而是用无头 Chrome 在不同时间点截图,然后"看图"找问题 。它为此在页面里加了一个 #t=秒数 的跳转参数,用来直接渲染某一时刻的画面。
无头 Chrome Claude Code 无头 Chrome Claude Code #mermaid-svg-51ABQ4FhkikJ6JqQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-51ABQ4FhkikJ6JqQ .error-icon{fill:#552222;}#mermaid-svg-51ABQ4FhkikJ6JqQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-51ABQ4FhkikJ6JqQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-51ABQ4FhkikJ6JqQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-51ABQ4FhkikJ6JqQ .marker.cross{stroke:#333333;}#mermaid-svg-51ABQ4FhkikJ6JqQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-51ABQ4FhkikJ6JqQ p{margin:0;}#mermaid-svg-51ABQ4FhkikJ6JqQ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-51ABQ4FhkikJ6JqQ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-51ABQ4FhkikJ6JqQ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-51ABQ4FhkikJ6JqQ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-51ABQ4FhkikJ6JqQ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-51ABQ4FhkikJ6JqQ .sequenceNumber{fill:white;}#mermaid-svg-51ABQ4FhkikJ6JqQ #sequencenumber{fill:#333;}#mermaid-svg-51ABQ4FhkikJ6JqQ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-51ABQ4FhkikJ6JqQ .messageText{fill:#333;stroke:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-51ABQ4FhkikJ6JqQ .labelText,#mermaid-svg-51ABQ4FhkikJ6JqQ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .loopText,#mermaid-svg-51ABQ4FhkikJ6JqQ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-51ABQ4FhkikJ6JqQ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-51ABQ4FhkikJ6JqQ .noteText,#mermaid-svg-51ABQ4FhkikJ6JqQ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-51ABQ4FhkikJ6JqQ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-51ABQ4FhkikJ6JqQ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-51ABQ4FhkikJ6JqQ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-51ABQ4FhkikJ6JqQ .actorPopupMenu{position:absolute;}#mermaid-svg-51ABQ4FhkikJ6JqQ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-51ABQ4FhkikJ6JqQ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-51ABQ4FhkikJ6JqQ .actor-man circle,#mermaid-svg-51ABQ4FhkikJ6JqQ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-51ABQ4FhkikJ6JqQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 打开 index.html 并跳到 t=8 截图PNG 截图看图:转速 20650?时间为负?定位:首帧 dt 为负修复后重新截图新截图看图:标签指向被遮挡的一侧加双锚点与避让
截图暴露的问题与修复:
| 截图中的现象 | 根因 | 修复 |
|---|---|---|
| 转速显示 20,650、时间 t = −10.4 s,所有燃烧室同时发亮 | 首帧 requestAnimationFrame 时间戳早于 performance.now(),dt 为负;负 dt 让时间倒流、闪光指数"增长" |
dt 下限钳制为 0 |
| "活塞""连杆"等标签指着红色气门室盖 | 锚点固定在一列缸体上,镜头转到另一侧后它被挡住 | 双锚点,按剖面朝向自动切换 |
| "Piston" 与 "Connecting rod" 两个标签叠在一起 | 标签按径向外推,相邻锚点会撞车 | 纵向排序避让 |
| 飞轮标签指向活塞区域 | 锚点在飞轮背面边缘 | 锚点改到镜头一侧的飞轮外缘 |
| 窄屏截图右侧被裁掉 | 并非 bug:Chrome 无头模式最小窗口宽度大于 390px | 改用 560px 宽度复查,确认构图正确 |
最后一条很有代表性:代理没有为了一个"看起来的问题"去改代码,而是先判断它是不是测试环境造成的假象。
七、导出视频:固定步长逐帧渲染
直接录屏会受机器性能影响而掉帧。代理的方案是让页面支持"外部驱动":
- 把主循环拆成
frame(now)(实时)和tick(dt)(推进 + 渲染一帧); - 打开
index.html#capture时不启动requestAnimationFrame,而是暴露window.__tick; - 用 Playwright 驱动系统 Chrome,每次调用
__tick(1/30)后截一张图,共 32 秒 × 30 fps = 960 帧; - 用 ffmpeg 合成。
bash
# MP4:H.264,30 秒
ffmpeg -framerate 30 -i frames/f%05d.png -frames:v 900 \
-c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart v8-engine.mp4
# GIF:降到 15 fps、800 宽,调色板两遍法
ffmpeg -framerate 30 -i frames/f%05d.png -frames:v 900 \
-vf "fps=15,scale=800:-1:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \
-loop 0 v8-engine.gif
#mermaid-svg-2GRcaYfjYD1jZJCr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2GRcaYfjYD1jZJCr .error-icon{fill:#552222;}#mermaid-svg-2GRcaYfjYD1jZJCr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2GRcaYfjYD1jZJCr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2GRcaYfjYD1jZJCr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2GRcaYfjYD1jZJCr .marker.cross{stroke:#333333;}#mermaid-svg-2GRcaYfjYD1jZJCr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2GRcaYfjYD1jZJCr p{margin:0;}#mermaid-svg-2GRcaYfjYD1jZJCr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr .cluster-label text{fill:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr .cluster-label span{color:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr .cluster-label span p{background-color:transparent;}#mermaid-svg-2GRcaYfjYD1jZJCr .label text,#mermaid-svg-2GRcaYfjYD1jZJCr span{fill:#333;color:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr .node rect,#mermaid-svg-2GRcaYfjYD1jZJCr .node circle,#mermaid-svg-2GRcaYfjYD1jZJCr .node ellipse,#mermaid-svg-2GRcaYfjYD1jZJCr .node polygon,#mermaid-svg-2GRcaYfjYD1jZJCr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2GRcaYfjYD1jZJCr .rough-node .label text,#mermaid-svg-2GRcaYfjYD1jZJCr .node .label text,#mermaid-svg-2GRcaYfjYD1jZJCr .image-shape .label,#mermaid-svg-2GRcaYfjYD1jZJCr .icon-shape .label{text-anchor:middle;}#mermaid-svg-2GRcaYfjYD1jZJCr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2GRcaYfjYD1jZJCr .rough-node .label,#mermaid-svg-2GRcaYfjYD1jZJCr .node .label,#mermaid-svg-2GRcaYfjYD1jZJCr .image-shape .label,#mermaid-svg-2GRcaYfjYD1jZJCr .icon-shape .label{text-align:center;}#mermaid-svg-2GRcaYfjYD1jZJCr .node.clickable{cursor:pointer;}#mermaid-svg-2GRcaYfjYD1jZJCr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2GRcaYfjYD1jZJCr .arrowheadPath{fill:#333333;}#mermaid-svg-2GRcaYfjYD1jZJCr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2GRcaYfjYD1jZJCr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2GRcaYfjYD1jZJCr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2GRcaYfjYD1jZJCr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2GRcaYfjYD1jZJCr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2GRcaYfjYD1jZJCr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2GRcaYfjYD1jZJCr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2GRcaYfjYD1jZJCr .cluster text{fill:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr .cluster span{color:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2GRcaYfjYD1jZJCr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2GRcaYfjYD1jZJCr rect.text{fill:none;stroke-width:0;}#mermaid-svg-2GRcaYfjYD1jZJCr .icon-shape,#mermaid-svg-2GRcaYfjYD1jZJCr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2GRcaYfjYD1jZJCr .icon-shape p,#mermaid-svg-2GRcaYfjYD1jZJCr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2GRcaYfjYD1jZJCr .icon-shape .label rect,#mermaid-svg-2GRcaYfjYD1jZJCr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2GRcaYfjYD1jZJCr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2GRcaYfjYD1jZJCr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2GRcaYfjYD1jZJCr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} index.html
捕获模式
Playwright
调用 __tick(1/30)
截图
f00000...f00959.png
ffmpeg
libx264
ffmpeg
调色板 GIF
v8-engine.mp4
7.8 MB
v8-engine.gif
16 MB
固定步长带来两个好处:结果可复现(同样的帧序号永远对应同样的画面),且与机器快慢无关------软件渲染(SwiftShader)下每帧约 1.5 秒,也不影响成片的流畅度。本文中的 GIF 是从中截取的 8 秒片段,压缩到 5 MB 以内以满足 CSDN 的图片上限。
八、实验结论
做得好的地方:
- 从约束推导,而不是凭印象画。十字形曲轴、共用曲柄销、凸轮安装角都是从"90° 夹角 + 点火顺序"算出来的,所以相位关系天然正确;
- 主动说明取舍。1/25 慢放、气门室盖剖开的方式等决定都在界面或说明里写明,而不是悄悄改需求;
- 闭环自检 。用截图验证而不是"我觉得没问题",抓到了负
dt、标签遮挡、标签重叠这类只有看画面才能发现的问题; - 工程化导出。固定步长捕获钩子让视频导出可复现,并且不影响页面的正常交互。
局限与可改进之处:
- 这是视觉上正确的教学动画,不是工程仿真:尺寸是比例化的,没有配气相位的精确数据,也没有进排气歧管;
- 正时皮带是静态几何,没有表现皮带的运动;
- 标签不做深度遮挡判断,被挡住的部件仍会显示标签;
- 导出用的是软件渲染(SwiftShader),960 帧需要较长等待,可以改用 GPU 渲染或降低分辨率提速。
一句话总结 :对于"有明确物理约束的 3D 运动机械"这类需求,AI 编程代理最大的价值不在于写 three.js 代码本身,而在于把自然语言约束转成可推导的数学关系,并用截图闭环验证结果。这套"推导 → 实现 → 截图自检 → 固定步长导出"的流程,可以直接复用到齿轮箱、凸轮机构、机械臂等其他机械动画上。