交互式仿真学习工作台
用单文件 HTML(零依赖、可离线、手机电脑通用)构建 PhET 风格的学科仿真台。区别于"静态演示动画",本技能的核心主张是:让使用者能自己搭建模型,而不是只能拖两个滑块看预设动画。
何时使用
- 用户要做物理/化学/数学/生物的交互式仿真、可视化实验台、教学演示页
- 明确提到 PhET 风格,或"模型条件可以随便搭建""能自己扩展加入"
- 要求电路能自由加元件、连电线、力学能自由加滑轮/物体、光学能自由放物体
- 需要给一个已存在的大型单文件 HTML 做全量交互验证(哪些按钮是坏的、哪些动画没动)
交付物标准(不可协商)
- 单文件 HTML,CSS/JS/图标全部内联,零外部框架/CDN/字体/图表库
- 图表与图形一律手写内联 SVG;图标用内联 SVG path,不用 emoji
- 数据存
localStorage(统一前缀wb_<标识>_),首屏提供导出 JSON / 导入恢复,清空二次确认 - 移动端适配:窄屏单列、按钮 ≥44px、输入框字号 ≥16px、
type="number"唤起数字键盘 - 首次打开必须有预置示例数据,严禁空白页
- 每个仿真模块必须能被验证脚本驱动(注册进全局
sims数组)
工作流
阶段 1 · 骨架
复制 assets/core-runtime.js 整段内联,改掉 PREFIX。它提供:
$/$all/el/clamp/toast/arrow 工具 → 全局 sims 注册表 + rAF 循环(只驱动当前 tab)→ Tab 切换 → localStorage 存取 → 导出/导入。
阶段 2 · 逐模块实现
每个仿真写成一个 IIFE,末尾 sims.push({tab:"kin", step:step}) 注册。模块模板与命名规范见 references/architecture.md。
阶段 3 · 高自由度改造(本技能的核心价值)
把"滑块调参"升级为"自己搭模型"。两种范式,按模块性质选:
A. 画布编辑器型(电路、力学装配这类有拓扑关系的)
直接用 assets/circuit-editor.js(makeEditor(cfg),已修全部已知 bug)。要点是「六件套」:
| # | 要件 | 说明 |
|---|---|---|
| 1 | 单一状态 st |
{comps:[], wires:[], tool, pending, drag, sel} 全部状态收拢到一个对象 |
| 2 | 工具栏 setTool() |
select / wire / del / add:<type> 模式切换 |
| 3 | 画布指针事件 | pointerdown/move/up,坐标用 getScreenCTM().inverse() 映射 |
| 4 | sync() 单一闭环 |
任何结构变更都只调 sync():solve→draw→renderList→onUpdate |
| 5 | DIY 列表 | 每个元件一行,参数可改、可删 |
| 6 | 预设 + 清空 | 示例电路一键搭好,另给清空 |
B. 实体数组型(滑轮组、平面镜成像这类无拓扑、靠几何重算的)
不需要完整编辑器,只需:可增删的实体数组 + 每个实体参数独立可调 + 几何自动重算重绘。
例:滑轮组 movs:[{g:10},...] 支持 +/- 动滑轮(上限 3),自动生成绕绳路径并重算 F=(G+ΣG动+f)/n;平面镜 objs:[{d,h},...] 支持任意加/拖/删蜡烛,自动做镜像对称与反射光路。
阶段 4 · 验证(不要靠"我觉得没问题")
node assets/validate-harness.js 你的页面.html
六道关:语法解析 → 外链扫描 → id 一致性 → 初始化(数 sims) → 逐仿真驱动 6 帧 → 动画活性递归快照。
按钮触发型模块另写 E2E 点击测试。完整方法论与排障流程见 references/verification.md。
踩坑铁律(全部来自真实事故,违反必出问题)
-
跨结构引用一律存 id 字符串,绝不存对象引用。
真实事故:addWire()把元件对象存进wire.a.c,而solve()/draw()期望的是 id 字符串(c.id===w.a.c)→ 点击连出的线永远接不上节点,电路恒报"未闭合或含浮空节点"。预设电路因为直接写{c:B.id}反而正常,导致 bug 长期被掩盖。
推论:只要出现"预设能用、手动搭建不能用",第一个就查 id/对象混用。 -
结构变更必须走
sync()单一闭环。
真实事故:addComp/removeComp/addWire只调了draw(); renderList();,漏了solve()+onUpdate→ 加完元件读数不刷新,用户以为没生效。 -
编辑器初始化若自带预设,二次搭建前必须先
clear()。
真实事故:makeEditor末尾自动presetSeries(),测试里再addComp3 次 → 变成 6 个元件、7 根线,拓扑全乱,误判为"重复添加 bug"。 -
rAF 只驱动当前 tab 的仿真。 十几个仿真同时跑必卡;用
sims[i].tab===currentTab过滤,并判document.hidden。 -
SVG 画布坐标必须用
createSVGPoint()+getScreenCTM().inverse(),不要用offsetX/clientX直接当画布坐标——viewBox 缩放下必然错位。 -
自由放置必须钳制边界,且动态量要按当前布局算上限。
真实事故:平面镜物距不限,像跑出画布。解法:dmax()按镜面位置实时算clamp(min(mx-25, W-mx)/SCALE, 20, 200),改镜面位置时反向钳制已有物体。 -
动画活性检测必须递归进
<g>。 只比较顶层 children 会把"其实在动"的画布误判为静止(元素都嵌在分组里)。 -
测试里的按钮 id / 滑块默认值要从 HTML 正则实测解析,不要凭记忆猜。
真实事故:测试写ge('cvAuto'),实际是cvDemo,白排查半天。 -
DOM 桩的元素级
querySelector绝不能返回 null。
动态列表行普遍写row.querySelector('.v').oninput = fn,返回 null 会崩掉整个初始化,且报错点极具误导性。 -
本机删临时文件:
rm/ PowerShellRemove-Item/cmd del都会被 safe-delete 拦截。
可靠做法是在沙箱内跑 Node:
bash node -e 'const fs=require("fs");["_a.js","_b.js"].forEach(f=>{try{fs.unlinkSync("绝对路径/"+f)}catch(e){}})'
资产
| 文件 | 用途 |
|---|---|
assets/core-runtime.js |
公共运行时:工具函数 / rAF 循环 / Tab / 存储 / 导入导出 / 电路工具栏绑定 |
assets/circuit-editor.js |
自由搭建电路编辑器引擎 makeEditor(),含节点电压法求解器,已修全部已知 bug |
assets/validate-harness.js |
Node 零依赖验证桩,node validate-harness.js 页面.html |
references/architecture.md |
单文件多仿真架构、模块模板、命名规范、高自由度改造清单 |
references/verification.md |
验证方法论、E2E 写法、典型故障→根因对照表 |