会算题的烟花:点火前我算出最高点 214.3m、滞空 12.0s,积分实测误差 0.24%
**一键开通华为云码道 CodeArts 代码智能体: **https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgithd
作品介绍
「会算题的烟花」是一个纯前端网页:夜空里每放一发烟花,在点火之前,它先用抛体公式把这一发的最高点、滞空、落点算出来;炸完之后,再用数值积分把实际飞出来的轨迹测一遍,两个数摆在一起对答案。误差表常年挂在第一屏,最高点误差 0.2% 上下。全程零第三方依赖,29 项测试全绿,同一颗种子能逐帧重放。
说白了,它想解决一个我很烦的现象:满屏的「粒子烟花」其实是随机速度向量瞎崩,跟物理没半毛钱关系。我想做一发算得出来、对得上答案的烟花。
一、为什么做「会算题的烟花」,而不是又一个粒子特效
CodePen 上随便一搜「fireworks canvas」几百个,都挺好看。但扒开看,绝大多数是:给粒子一个随机方向、随机速度、加个重力往下掉、透明度衰减到消失。好看,但你问它「这发最高能飞多高」,它答不上来——因为根本没有物理,只有随机数。
我较劲的点就在这:能不能让每一发烟花都先报答案、再验证。点火前用 h = v0²·sin²θ / 2g 算出最高点,炸完用积分跑出实测值,误差摆出来给人看。这么一来,评委的判断题(好不好看)就被我换成了客观题(准不准)——这刀,是我整个项目最想切下去的地方。
二、先跑起来看

打开网页,第一屏不是夜空,是这张表:每发烟花一行,列出预测最高点 / 实测最高点 / 误差 / 预测滞空 / 实测滞空 / 误差。你会看到误差常年趴在 0.2%~0.26%(滞空 0.04%~0.13%)。表下面一行小字写明「同模型 drag=0 对拍 · 误差=积分误差」——这句很关键,第六节解释。

烟花本身也好看:国庆红和香槟金的粒子,炸开是一个个亮点扩散的环,带重力 + 空气阻力往下坠、拖着衰减的尾焰。能点画布发射、能自动连放、能一键清空。
三、提示词:把「可验证」钉死到函数级
我没跟码道说「做个烟花特效」,那种一句话需求出来的必然是随机粒子。我一开始就把需求钉到函数签名和判据:
predictor.js:只用闭式解算 h=v0²sin²θ/2g、T=2v0sinθ/g、R=v0²sin2θ/g。
verifier.js:用 stepShell 数值积分(半隐式欧拉 + 二次阻力)跑出实测 h/T/R。
两者必须独立实现,不得互相调用;误差表比的是「闭式 vs 积分」,不是公式对公式。
physics/render/app 分层,纯函数不 fetch、不读时钟、不用 Math.random,dt/seed 作参数。
这条「两者独立实现」是命门。后来三轮专家评审里,技术专家专门盯着它问:如果 predictor 和 verifier 共用同一套积分器,误差小是数学必然,那只能证明数值稳定,证明不了物理对。这就是差点让我翻车的自证循环,第七节细说。

四、架构:把「预言」和「验证」拆成两个独立模块
src/
predictor.js 闭式解析解预言器(只用公式,不碰积分)
verifier.js 数值积分实测器(用 stepShell 积分跑轨迹)
physics.js 抛体 + 爆炸:launchShell / stepShell / burst / stepParticle(纯函数)
rng.js 可注入的确定性随机(mulberry32,seed 作参数)
render.js 夜空 / 弹道 / 炸开粒子 / 解析叠加虚线
app.js 状态机 + 主循环 + 误差表 + HUD
test/ 29 项:解析对标 / 积分收敛 / 确定性重放 / 逐帧哈希漂移 / 安全红线
tools/ evidence.cjs(联网取证)/ serve.cjs(零依赖预览)
依赖单向:app → {render, predictor, verifier, physics} → rng。predictor 和 verifier 是两条独立的路通往同一个答案——一个走公式,一个走积分,它俩对得上,才叫真验证。
五、核心算法:半隐式欧拉 + 二次阻力
上升阶段用半隐式欧拉积分,空气阻力取二次模型 a = -k·v·|v|:
export function stepShell(shell, dt, drag) {
const speed = Math.hypot(shell.vx, shell.vy);
const ax = –drag * shell.vx * speed;
const ay = –shell.g – drag * shell.vy * speed;
const vx1 = shell.vx + ax * dt;
const vy1 = shell.vy + ay * dt;
return { x: shell.x + vx1 * dt, y: shell.y + vy1 * dt,
vx: vx1, vy: vy1, g: shell.g, t: shell.t + dt, alive: shell.alive };
}
预言器则完全不走这条路,直接套闭式解:
export function predict(v0, angleDeg, g = 9.8) {
const s = Math.sin(angleDeg * Math.PI / 180);
const h = (v0 * v0 * s * s) / (2 * g); // 最高点
const T = (2 * v0 * s) / g; // 滞空
const R = (v0 * v0 * Math.sin(2 * angleDeg * Math.PI / 180)) / g; // 射程
return { h, T, R };
}
两条独立实现,一个 dt=0.016 积分、一个套公式,v0=60、θ=75° 时最高点误差 0.27%,v0=50、θ=85° 时 0.079%。阻力终端速度 v_term=√(g/k)≈44m/s,数值上 dt≤0.05 时 k·v·dt≪1,稳定不发散。
六、护城河:可重放 + 第三方真值对拍
我用 AI 写代码的心病是「它当场跑对,是真对还是碰巧」。所以守了两条:
确定性重放:固定步长累加器 + 每发独立种子 mulberry32(seed ^ shellId),replay 只存 (seed, events, H) 靠确定性重建。取证脚本里 seed=12345 跑 300 帧两次,逐帧最大误差 = 0。
第三方真值对拍:误差表里 predictor 用闭式解、verifier 用积分,两个独立实现对同一发 (v0,θ) 算,误差就是真实的积分误差。取证输出长这样:
== 真·第三方对拍 (predictor 闭式 vs verifier 积分) ==
v0=60 θ=75° dt=0.016 h误差=0.270% T误差=0.031%
v0=50 θ=85° dt=0.004 h误差=0.079% T误差=0.012%
== 确定性重放 ==
seed=12345 frames=300 逐帧最大误差=0 重放一致 ✓
== SHA-256 指纹 ==
d2fd218fd05b0fc54a47a4cfe139d893c3f594f6016298c658afd677ae498952
每次取证都带一个 SHA-256 指纹,参数串一改指纹就变,数据可溯源。
七、真实的坑:三个差点让我翻车的瞬间
这项目不是一遍过的,坑都挺实在:
坑一:码道把仓库建成了私有。 第一轮建 fireworks-physics 时它默认设成了 private,我匿名 clone 直接 403。比赛硬性要求公开仓,我让它改回 public 才继续。这个坑不查会一直埋到投稿。

坑二:误差表第一版显示 54%。 首屏误差表刚接上时,我截图一看——预测 272m、实测 124m、误差 54%。原因是 predictor 用 drag=0 闭式解,现场烟花却带 drag=0.005 阻力飞,两个模型不一样,比出来的是「模型差」不是「积分误差」。这直接把「误差<1%」的卖点打脸了。修法:误差表里 predictor 和 verifier 都用 drag=0 同模型对拍,误差立刻回到 0.2% 量级,HUD 也标清「对拍模型 drag=0 / 现场阻力 k=0.005」。

坑三:自证循环。 三轮专家评审里技术专家点破:如果 predictor 和 verifier 共用同一套积分器,误差小是数学必然,只证明数值稳定、不证明物理对。我把它拆成两条独立实现(公式 vs 积分),再加「dt 减半、误差约减半」的一阶收敛断言,才把这个洞堵上。
八、测试:29 项全绿
解析对标、积分收敛、确定性重放、逐帧哈希漂移、坏数据 fuzz、安全红线,29 项,我每一轮都 clone 到本地独立 node –test 复核,不是码道说绿就绿。
九、提效数据
| 物理层 + predictor/verifier + 29 测试 | 两三天 | 约 2.5 小时(六轮) |
| 夜空渲染 + 炸开粒子 + 解析叠加 | 一天 | 一轮 |
| 状态机 + 页面 + 取证脚本 | 一天 | 两轮 |
全程我逐字敲的代码不到一成,剩下都是码道写、我 clone 复核。码道每轮底下会自己贴运行数据(耗时、输入/输出 token、子任务数),报账挺清楚。
分轮产出大致是这样,一轮一步、一步能跑:





十、五维自检
-
架构:predictor/verifier/physics/render/app 分层,两条独立路径对拍。
-
代码:29 项测试全绿 + 逐帧重放误差 0 + SHA-256 指纹可溯源。
-
安全:fuzz dt/energy、NaN 拒绝、clamp、v_term 合理、不联网不采集。
-
UI:第一屏误差表 + 夜空 + 叠加解析虚线,深色克制、国庆红金点缀。
-
内容:真实截图 + 真实代码 + 真跑出来的误差数字 + 诚实写三个坑。
十一、本地怎么跑
git clone https://atomgit.com/azhiqiu/fireworks-physics.git
cd fireworks-physics
node –test # 29 项全绿
node tools/evidence.cjs # 第三方对拍 + 重放 + 指纹
npm run serve # 打开 http://localhost:8081
打开后建议亲手试三下:点画布放一发看误差表实时填一行;开「对比解析解」看仿真轨迹和理论抛物线叠在一起;把 seed 写进 URL 刷新,看同一发烟花逐帧一模一样重放。
十二、写在最后
这个项目最大的收获不是烟花,是被自己的误差表教育了两次:一次是 54% 的模型口径 bug,一次是差点写出自证循环。AI 把代码写得又快又像模像样,但「这个误差到底是不是真的」这种问题,它不会主动替你想到——你得盯着。它负责快,我负责对。


数据与致谢:重力模型取标准 g=9.8、无风质点假设;开发工具 华为云码道 CodeArts 代码智能体(AtomGit)。仓库 MIT 开源,地址就是上面 clone 命令里那个。
你做过这种「让程序自己算答案、再自己验答案」的东西吗?评论区聊聊——你是更在意它好看,还是更在意它「敢把误差摊在桌面上」?
网硕互联帮助中心







评论前必须登录!
注册