长征十号乙火箭回收全流程动画
一、前言
1.1 项目背景
长征十号乙(CZ-10B)是中国航天科技集团研制的新型重复使用液体运载火箭,采用5米直径两级串联构型。2026年7月10日,长征十号乙在海南商业航天发射场成功首飞,实现了全球首次运载火箭海上网系回收。
本动画项目旨在通过HTML5 Canvas技术,完整模拟长征十号乙的发射与回收全过程,包括7个精确阶段:发射→上升→分离→入轨→返回→捕获→收束。
视频动画如下:
长征十号乙 2026-07-11 232545

1.2 真实火箭参数
| 参数 | 值 |
|---|---|
| 构型 | 5米直径两级串联 |
| 全长 | 约63米 |
| 起飞推力 | 约890吨 |
| 起飞重量 | 约760吨 |
| LEO运力 | 16吨(复用状态) |
| 一子级发动机 | 7台液发100(液氧煤油) |
| 二子级发动机 | 液氧甲烷 |
| 回收方式 | 海上网系回收 |
| 回收平台 | 领航者号(长144m,宽50m,2.5万吨) |
1.3 技术栈
- 渲染引擎:HTML5 Canvas 2D
- 编程语言:JavaScript(ES6+)
- 文件结构:单HTML文件(内联所有CSS和JavaScript)
- 运行环境:任何现代浏览器,直接打开即可运行
二、系统架构
2.1 总体架构
┌─────────────────────────────────────────────────────────┐
│ HTML 文件结构 │
├─────────────────────────────────────────────────────────┤
│ CSS 样式 → 深色主题HUD,Canvas样式,控制按钮 │
├─────────────────────────────────────────────────────────┤
│ JavaScript 代码模块: │
│ ├── 纹理系统 → 预渲染粒子纹理(火焰/烟雾/火花) │
│ ├── 粒子系统 → 粒子生命周期管理 │
│ ├── 领航者号类 → 回收平台绘制与网系逻辑 │
│ ├── 一子级类 → 返回级物理与状态机 │
│ ├── 二子级类 → 入轨级物理与状态机 │
│ ├── 相机系统 → 多视角跟踪 │
│ ├── 逻辑更新 → 7阶段核心逻辑 │
│ ├── HUD系统 → 遥测面板与任务状态 │
│ ├── 绘制系统 → 背景/轨迹/特效绘制 │
│ ├── 分屏系统 → 双视角同步渲染 │
│ └── 主循环 → requestAnimationFrame驱动 │
└─────────────────────────────────────────────────────────┘
2.2 核心类结构
PS (粒子系统)
├── p[] - 粒子数组
├── add() - 添加粒子
├── burst() - 爆发粒子
├── update() - 更新物理
└── draw() - 渲染粒子
RS (领航者号回收平台)
├── netActive - 网系激活状态
├── netP - 网系展开进度
├── captured - 捕获状态
├── hk[] - 4挂钩状态
├── activate() - 激活网系
├── update() - 更新逻辑
├── draw() - 绘制平台
└── drawNet() - 绘制绳网
S1 (一子级/返回级)
├── 状态机: ATTACHED→ASCENT→CORE_ASCENT→FLIP→RETURNING→CAPTURED
├── update() - 物理更新(FLIP/RETURNING)
├── separate() - 分离翻转
├── explode() - 爆炸
└── draw() - 绘制火箭
S2 (二子级/入轨级)
├── 状态机: ATTACHED→ASCENT→ORBITING→LANDED
├── update() - 物理更新
├── separate() - 分离入轨
└── draw() - 绘制火箭
2.3 系统流程图
以下是长征十号乙火箭回收动画的完整系统流程图,展示了从用户交互到最终渲染的完整数据流:
#mermaid-svg-o4xwSyAvEEmHqTsR{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-o4xwSyAvEEmHqTsR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o4xwSyAvEEmHqTsR .error-icon{fill:#552222;}#mermaid-svg-o4xwSyAvEEmHqTsR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o4xwSyAvEEmHqTsR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o4xwSyAvEEmHqTsR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o4xwSyAvEEmHqTsR .marker.cross{stroke:#333333;}#mermaid-svg-o4xwSyAvEEmHqTsR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o4xwSyAvEEmHqTsR p{margin:0;}#mermaid-svg-o4xwSyAvEEmHqTsR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR .cluster-label text{fill:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR .cluster-label span{color:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR .cluster-label span p{background-color:transparent;}#mermaid-svg-o4xwSyAvEEmHqTsR .label text,#mermaid-svg-o4xwSyAvEEmHqTsR span{fill:#333;color:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR .node rect,#mermaid-svg-o4xwSyAvEEmHqTsR .node circle,#mermaid-svg-o4xwSyAvEEmHqTsR .node ellipse,#mermaid-svg-o4xwSyAvEEmHqTsR .node polygon,#mermaid-svg-o4xwSyAvEEmHqTsR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-o4xwSyAvEEmHqTsR .rough-node .label text,#mermaid-svg-o4xwSyAvEEmHqTsR .node .label text,#mermaid-svg-o4xwSyAvEEmHqTsR .image-shape .label,#mermaid-svg-o4xwSyAvEEmHqTsR .icon-shape .label{text-anchor:middle;}#mermaid-svg-o4xwSyAvEEmHqTsR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-o4xwSyAvEEmHqTsR .rough-node .label,#mermaid-svg-o4xwSyAvEEmHqTsR .node .label,#mermaid-svg-o4xwSyAvEEmHqTsR .image-shape .label,#mermaid-svg-o4xwSyAvEEmHqTsR .icon-shape .label{text-align:center;}#mermaid-svg-o4xwSyAvEEmHqTsR .node.clickable{cursor:pointer;}#mermaid-svg-o4xwSyAvEEmHqTsR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-o4xwSyAvEEmHqTsR .arrowheadPath{fill:#333333;}#mermaid-svg-o4xwSyAvEEmHqTsR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-o4xwSyAvEEmHqTsR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-o4xwSyAvEEmHqTsR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-o4xwSyAvEEmHqTsR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-o4xwSyAvEEmHqTsR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-o4xwSyAvEEmHqTsR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-o4xwSyAvEEmHqTsR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-o4xwSyAvEEmHqTsR .cluster text{fill:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR .cluster span{color:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR 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-o4xwSyAvEEmHqTsR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-o4xwSyAvEEmHqTsR rect.text{fill:none;stroke-width:0;}#mermaid-svg-o4xwSyAvEEmHqTsR .icon-shape,#mermaid-svg-o4xwSyAvEEmHqTsR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-o4xwSyAvEEmHqTsR .icon-shape p,#mermaid-svg-o4xwSyAvEEmHqTsR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-o4xwSyAvEEmHqTsR .icon-shape .label rect,#mermaid-svg-o4xwSyAvEEmHqTsR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-o4xwSyAvEEmHqTsR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-o4xwSyAvEEmHqTsR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-o4xwSyAvEEmHqTsR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一子级流程
是
否
是
是
否
二子级流程
是
否
持续加速
y < -600?
标记'已入轨'
用户控制面板
用户点击'点火发射'
状态机初始化
阶段1: 发射动画
阶段2: 上升动画
高度 > 600px?
阶段3: 分离动画
滑行阶段
到达远地点?
翻转阶段
返回阶段
进入捕获范围?
阶段6: 网系捕获
阶段7: 网系收束
庆祝特效
动画完成
相机系统
视角跟踪
分屏渲染
Canvas绘制
粒子系统
特效生成
HUD系统
遥测更新
流程说明:
- 用户交互层:用户点击"点火发射"按钮触发整个动画流程
- 核心状态机:按照7个阶段顺序执行,包含条件分支判断
- 二子级并行流程:分离后持续加速直至入轨标记
- 一子级返回流程:包含滑行、翻转、返回三级制动逻辑,最终进入捕获判定
- 渲染系统:相机、粒子、HUD三大子系统并行处理,最终汇聚到Canvas绘制
- 完成状态:网系收束完成后显示庆祝特效,动画结束
该流程图清晰地展示了系统各模块之间的数据流向和依赖关系,有助于理解动画的整体架构。
三、7阶段实现细节
3.1 阶段1:发射动画
触发条件:点击"点火发射"按钮
实现逻辑:
javascript
function launch() {
// 设置状态
s1.state = 'ASCENT';
s2.state = 'ASCENT';
// 粒子特效
ps.burst('sm', ...); // 烟雾60个
ps.burst('s', ...); // 尾迹100个
ps.burst('f', ...); // 火焰40个
}
视觉效果:
- 烟雾粒子(灰色,向上飘散)
- 尾迹粒子(蓝色,持续产生)
- 火焰粒子(橙色,向下喷射)
- 火箭抖动(随机偏移)
3.2 阶段2:上升动画
物理模型:
javascript
// 总推力 / 总质量 = 加速度
ay = G - (totalThrust / totalMass)
// ay = 0.12 - 8.5/33 = -0.138(向上加速)
重力转弯:
- 当高度 > 60px时启动重力转弯
- 水平速度每帧增加0.008
- 火箭角度根据速度方向计算
视觉效果:
- 分层绘制(一子级 + 二子级堆叠)
- 中国航天标识(红色文字)
- 尾焰粒子持续喷射
- 弹道轨迹青色实线记录
3.3 阶段3:分离动画
触发条件:高度 > 600px
物理过程:
javascript
if (s1.alt > 600) {
s1.state = 'CORE_ASCENT'; // 一子级继续滑行
s2.state = 'ORBITING'; // 二子级入轨
s2.sep(); // 二子级发动机点火
sepDone = true;
}
关键设计 :一子级不立即翻转,而是继续惯性滑行至远地点
视觉效果:
- 橙色分离火花40个
- 白色闪光20个
- HUD显示"分离返回"
3.4 阶段4:二子级入轨
物理模型:
javascript
// 二子级持续加速
vy -= 0.06; // 向上加速
vx += 0.005; // 水平加速
飞行轨迹:
- 二子级保持分离时的速度(约-12.7)
- 持续加速飞向太空
- 当 y < -600 时标记为"已入轨"
HUD显示 :二子级入轨飞行中 → 已入轨
3.5 阶段5:一子级返回
滑行阶段(CORE_ASCENT):
javascript
// 仅重力作用,无推力
s1.vy += G; // vy += 0.12(逐渐减速)
// 到达远地点后触发翻转
if (s1.vy >= 0 && s1.alt > 200) {
s1.sep(); // 开始翻转
}
翻转阶段(FLIP):
- 持续80帧(约1.3秒)
- 从0°旋转至153°(朝下)
- 栅格舵在15帧后展开
- 小推力辅助调姿
返回阶段(RETURNING):
- 三级制动逻辑:
- Boost-back(远距离高空):dist>300, alt>200
- 再入制动(中高空减速):alt<200, alt>80
- 着陆制动(低空最终减速):alt<80
- 推力矢量引导(指向平台方向)
- 尾焰从橙色渐变为蓝色(低空)
3.6 阶段6:网系捕获
捕获判定:
javascript
// 火箭底部进入绳网范围
rb > ny - 50 && rb < by + 100 // 纵向
dx < 200 // 横向
this.vy > 0 // 下降状态
捕获瞬间特效:
- 白色闪光40个
- 环形波80个
- 烟雾50个
- 火花40个
绳网结构:
- 两根桁架柱(带横档纹路)
- 顶部横梁和斜撑
- 纵网线8条(弧形)
- 横网线10条(弧形)
3.7 阶段7:网系收束
4挂钩锁定(按顺序):
- 25%进度 → 挂钩1
- 40%进度 → 挂钩2
- 55%进度 → 挂钩3
- 70%进度 → 挂钩4
网系收束动画:
- 8条环绕线逐渐收紧
- 4条径向线收缩
- 绳网高度从140px降至20px
庆祝特效:
- 环形波持续产生
- 彩色纸屑(HSL随机颜色)
- 绿色成功横幅
四、物理参数详解
4.1 基本物理常量
| 常量 | 值 | 说明 |
|---|---|---|
| W | 1400 | 画布宽度(px) |
| H | 800 | 画布高度(px) |
| GY | 700 | 地面Y坐标 |
| G | 0.12 | 重力加速度 |
| AD | 0.0006 | 空气阻力系数 |
4.2 火箭参数
| 参数 | 一子级(S1) | 二子级(S2) |
|---|---|---|
| 质量 | 25 | 8 |
| 燃料 | 200 | 80 |
| 最大推力 | 6.0 | 2.5 |
| 燃料消耗率 | 0.15/帧 | 0.18/帧 |
| 宽度 | 18px | 12px |
| 高度 | 80px | 45px |
4.3 弹道计算
上升阶段(ASCENT):
- 加速度:ay = 0.12 - 8.5/33 = -0.138
- 到达高度600px所需时间:约93帧
- 分离时垂直速度:vy ≈ -12.7
滑行阶段(CORE_ASCENT):
- 仅重力作用:vy += 0.12/帧
- 到达远地点时间:约106帧
- 远地点高度:约1272px
翻转阶段(FLIP):
- 持续时间:80帧
- 水平速度:vx = 1.5
4.4 网系参数
| 参数 | 值 |
|---|---|
| 展开速度 | 0.018/帧 |
| 最大高度 | 140px |
| 桁架高度 | 120px |
| 挂钩锁定阈值 | 25%, 40%, 55%, 70% |
| 收束速度 | 0.012/帧 |
| 捕获横向范围 | dx < 200 |
| 捕获纵向范围 | ny-50 至 by+100 |
五、视角系统
5.1 五种视角模式
| 模式 | 按钮 | 跟踪目标 | 缩放 | 适用场景 |
|---|---|---|---|---|
| FREE | 全程 | 自动跟踪 | 0.5 | 全局观察 |
| STAGE1 | 一子级 | 一子级 | 0.85 | 观察返回过程 |
| STAGE2 | 二子级 | 二子级 | 0.95 | 观察入轨 |
| SHIP | 领航者号 | 回收平台 | 0.6 | 观察网系捕获 |
| SPLIT | 分屏 | 双视角 | 0.5/0.6 | 同时观察入轨和回收 |
5.2 分屏模式
┌─────────────────┬─────────────────┐
│ 二子级入轨追踪 │ 一子级网系回收 │
│ (青色轨迹线) │ (橙色轨迹线) │
│ │ │
│ 相机: camL │ 相机: camR │
│ 缩放: 0.5 │ 缩放: 0.6 │
│ 跟踪: s2 │ 跟踪: s1/ship │
├─────────────────┴─────────────────┤
│ 分隔线(青色) │
└───────────────────────────────────┘
六、视觉特效系统
6.1 粒子类型
| 类型代码 | 名称 | 纹理 | 用途 |
|---|---|---|---|
| f | 火焰 | 橙色径向渐变 | 尾焰 |
| fb | 蓝色火焰 | 蓝色径向渐变 | 低空制动尾焰 |
| e | 爆炸 | 黄色径向渐变 | 爆炸 |
| sp | 火花 | 白色径向渐变 | 分离/捕获 |
| sm | 烟雾 | 灰色径向渐变 | 发射烟雾 |
| s | 尾迹 | 蓝色圆形 | 飞行轨迹 |
| n | 环形 | 青色描边圆 | 雷达波 |
| sep | 分离火花 | 橙色圆 | 分离特效 |
| c | 纸屑 | HSL彩色方块 | 庆祝特效 |
| bf | 闪光 | 白色大圆 | 捕获闪光 |
6.2 轨迹绘制
上升段: 青色实线 (rgba(0,200,255,0.25), 线宽2)
返回段: 橙色虚线 (rgba(255,150,50,0.3), 线宽2.5)
分离点: 橙色发光圆点 (shadowBlur=8)
6.3 背景系统
- 星空:200个闪烁星星(随机位置,闪烁动画)
- 海洋:深蓝色渐变 + 4层波浪动画
- 发射台:基座 + 塔架 + 导流槽
七、代码结构
7.1 文件结构(单文件)
recovery-recoket.html (250行)
├── 行1-21: CSS样式
├── 行22-45: HTML结构
├── 行46-53: 文档注释
├── 行54-55: 画布初始化
├── 行56-65: 纹理系统
├── 行66-81: 粒子系统类
├── 行82-118: 领航者号类
├── 行119-123: 相机系统
├── 行124-153: 一子级类
├── 行154-171: 二子级类
├── 行172-175: 全局变量
├── 行176-177: 初始化/发射
├── 行178-185: 核心逻辑
├── 行186-192: HUD更新
├── 行193-196: 绘制堆叠
├── 行197-200: HUD绘制
├── 行201-221: 分屏渲染
├── 行222-226: 背景绘制
├── 行227-245: 主循环
└── 行246-247: 启动
7.2 状态机
一子级(S1)状态机:
ATTACHED → ASCENT → CORE_ASCENT → FLIP → RETURNING → CAPTURED
↓
EXPLODED
二子级(S2)状态机:
ATTACHED → ASCENT → ORBITING → LANDED
八、使用说明
8.1 运行方式
直接使用浏览器打开 recovery-recoket.html 文件(无需任何构建工具或服务器)。
8.2 操作指南
- 点击 "点火发射" 按钮开始动画
- 使用视角按钮切换观察模式:
- 全程:自动跟踪火箭
- 一子级:聚焦返回级
- 二子级:聚焦入轨级
- 领航者号:聚焦回收平台
- 分屏:同时显示入轨和回收
- 点击 "重置" 按钮重新开始
8.3 HUD信息
- 一子级遥测:高度、速度、垂直速度、燃料、状态
- 任务阶段:任务时间、当前阶段、各部件状态
九、开发说明
9.1 扩展建议
- 增加代码行数:将每个类拆分为独立文件,使用ES Module或构建工具
- 增加物理精度:加入更精确的大气模型、风阻计算
- 增加视觉细节:3D火箭模型、更精细的粒子系统
- 增加音效:发射音效、分离音效、成功音效
9.2 已知问题
- 一子级弹道计算基于简化物理模型,与实际轨迹有差异
- 网系捕获判定使用矩形区域,实际为"井"字形网
- 二子级入轨后简化为"已入轨"状态,未模拟真实轨道
9.3 调试技巧
- 浏览器F12打开控制台可查看实时错误
- 调整
G(重力)、AD(阻力)等常量可改变物理表现 - 调整
s1.sep()中的vx和vy可改变返回轨迹
部分代码如何下:
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>长征十号乙 - 一子级网系回收全流程</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#000;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;font-family:monospace;overflow:hidden;color:#0ff}
#canvas{border:1px solid rgba(0,255,255,.3);box-shadow:0 0 30px rgba(0,255,255,.1);background:#000}
.hud{position:absolute;background:rgba(0,10,20,.9);border:1px solid rgba(0,255,255,.4);padding:12px;font-size:11px;backdrop-filter:blur(5px);pointer-events:none;border-radius:4px}
#telemetry{top:10px;left:10px;width:260px}#mission{top:10px;right:10px;text-align:right;min-width:240px}
.controls{position:absolute;bottom:20px;display:flex;gap:15px;z-index:100}
button{padding:10px 24px;font-size:13px;border:1px solid #0ff;background:rgba(0,255,255,.1);color:#0ff;cursor:pointer;font-family:inherit;transition:all .2s;border-radius:3px}
button:hover{background:rgba(0,255,255,.3);box-shadow:0 0 15px rgba(0,255,255,.4)}
button:disabled{opacity:.3;cursor:not-allowed}
h3{color:#fff;margin-bottom:8px;font-size:13px;border-bottom:1px solid rgba(0,255,255,.2);padding-bottom:5px;letter-spacing:2px}
.row{display:flex;justify-content:space-between;margin:4px 0}
.warn{color:red;text-shadow:0 0 5px red}.ok{color:#0f0;text-shadow:0 0 5px #0f0}.caution{color:#ff0;text-shadow:0 0 5px #ff0}
.view-btns{position:absolute;bottom:70px;display:flex;gap:10px}
</style>
</head>
<body>
<canvas id="canvas" width="1400" height="800"></canvas>
<div class="hud" id="telemetry"><h3>一子级遥测</h3>
<div class="row"><span>高度:</span><span id="alt">0.00 km</span></div>
<div class="row"><span>速度:</span><span id="vel">0 m/s</span></div>
<div class="row"><span>垂直速度:</span><span id="vvel">0 m/s</span></div>
<div class="row"><span>剩余推进剂:</span><span id="fuel">100%</span></div>
<div class="row"><span>状态:</span><span id="stat" class="ok">待命</span></div></div>
<div class="hud" id="mission"><h3>任务阶段</h3>
<div class="row"><span>任务时间:</span><span id="t">T-0</span></div>
<div class="row"><span>当前阶段:</span><span id="phase">系统自检</span></div>
<div class="row"><span>一子级:</span><span id="boost_stat" class="ok">锚定</span></div>
<div class="row"><span>二子级:</span><span id="upper_stat" class="ok">锚定</span></div>
<div class="row"><span>领航者号:</span><span id="ship_stat" class="ok">待命</span></div></div>
<div class="view-btns">
<button onclick="setView(event,'FREE')" class="active">全程</button>
<button onclick="setView(event,'STAGE1')">一子级</button>
<button onclick="setView(event,'STAGE2')">二子级</button>
<button onclick="setView(event,'SHIP')">领航者号</button>
<button onclick="setView(event,'SPLIT')">分屏</button></div>
<div class="controls">
<button onclick="launch()" id="btn_launch">点火发射</button>
<button onclick="reset()">重置</button></div>
<script>
// ============================================================
// 长征十号乙 - 一子级网系回收全流程动画
// 真实构型:5米直径两级串联
// 一子级:液氧煤油 7台发动机 | 二子级:液氧甲烷
// 回收方式:一子级网系回收(领航者号海上平台)
// 7阶段:发射→上升→分离→二子级入轨→一子级返回→网系捕获→收束
// ============================================================
const c=document.getElementById('canvas'),ctx=c.getContext('2d'),W=1400,H=800,GY=700,G=.12,AD=.0006;
c.width=W;c.height=H;
// 纹理
const tx={};
function mT(w,h,fn){let t=document.createElement('canvas');t.width=w;t.height=h;fn(t.getContext('2d'),w,h);return t}
function rg(g,s){g.addColorStop(0,s[0]);for(let i=1;i<s.length;i++)g.addColorStop(i/(s.length-1),s[i])}
tx.flame=mT(64,64,(c,w,h)=>{let g=c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);rg(g,['rgba(255,255,255,1)','rgba(255,200,50,0.8)','rgba(255,100,0,0.4)','rgba(255,50,0,0)']);c.fillStyle=g;c.fillRect(0,0,w,h)});
tx.flameBlue=mT(64,64,(c,w,h)=>{let g=c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);rg(g,['rgba(200,255,255,1)','rgba(50,150,255,0.8)','rgba(0,50,255,0.3)','rgba(0,0,255,0)']);c.fillStyle=g;c.fillRect(0,0,w,h)});
tx.glow=mT(128,128,(c,w,h)=>{let g=c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);rg(g,['rgba(0,255,255,0.6)','rgba(0,255,255,0)']);c.fillStyle=g;c.fillRect(0,0,w,h)});
tx.expl=mT(128,128,(c,w,h)=>{let g=c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);rg(g,['rgba(255,255,200,1)','rgba(255,100,0,0.8)','rgba(100,0,0,0)']);c.fillStyle=g;c.fillRect(0,0,w,h)});
tx.spark=mT(8,8,(c,w,h)=>{let g=c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);rg(g,['rgba(255,255,200,1)','rgba(255,200,50,0.5)','rgba(255,100,0,0)']);c.fillStyle=g;c.fillRect(0,0,w,h)});
tx.smoke=mT(48,48,(c,w,h)=>{let g=c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);rg(g,['rgba(200,200,200,0.4)','rgba(150,150,150,0.2)','rgba(100,100,100,0)']);c.fillStyle=g;c.fillRect(0,0,w,h)});
// 粒子系统
class PS{
constructor(){this.p=[]}
add(t,x,y,o){o=o||{};this.p.push({t,x,y,vx:o.vx||0,vy:o.vy||0,life:o.life||50,max:o.life||50,size:o.size||5,drag:o.drag||.95,g:o.g||0,hue:o.hue||0})}
burst(t,x,y,n,o){o=o||{};for(let i=0;i<n;i++){let a=Math.random()*Math.PI*2,s=(o.spd||3)*(.5+Math.random()*.5);this.add(t,x+(Math.random()-.5)*(o.spread||10),y+(Math.random()-.5)*(o.spread||10),{vx:Math.cos(a)*s+(o.dvx||0),vy:Math.sin(a)*s+(o.dvy||0),size:(o.size||5)*(.5+Math.random()*.5),life:(o.life||30)*(.5+Math.random()*.5),drag:o.drag||.96,g:o.g||.05,hue:o.hue||0})}}
update(){this.p=this.p.filter(p=>{p.x+=p.vx;p.y+=p.vy;p.vy+=p.g;p.vx*=p.drag;p.vy*=p.drag;p.life--;return p.life>0})}
draw(){for(const p of this.p){const a=p.life/p.max,s=p.size*(.3+a*.7);ctx.save();ctx.globalAlpha=a;
if(p.t==='f')ctx.drawImage(tx.flame,p.x-s,p.y-s,s*2,s*2);else if(p.t==='fb')ctx.drawImage(tx.flameBlue,p.x-s,p.y-s,s*2,s*2);
else if(p.t==='e')ctx.drawImage(tx.expl,p.x-s,p.y-s,s*2,s*2);else if(p.t==='sp')ctx.drawImage(tx.spark,p.x-s,p.y-s,s*2,s*2);
else if(p.t==='sm')ctx.drawImage(tx.smoke,p.x-s,p.y-s,s*2,s*2);
else if(p.t==='s'){ctx.fillStyle='rgba(200,220,255,'+(a*.4)+')';ctx.beginPath();ctx.arc(p.x,p.y,s*(2-a),0,Math.PI*2);ctx.fill()}
else if(p.t==='n'){ctx.strokeStyle='rgba(0,255,255,'+a+')';ctx.lineWidth=2;ctx.beginPath();ctx.arc(p.x,p.y,s,0,Math.PI*2);ctx.stroke()}
else if(p.t==='sep'){ctx.fillStyle='rgba(255,200,50,'+a+')';ctx.beginPath();ctx.arc(p.x,p.y,s,0,Math.PI*2);ctx.fill()}
else if(p.t==='c'){ctx.fillStyle='hsl('+(p.hue||0)+',100%,'+(50+50*a)+'%)';ctx.fillRect(p.x-s/2,p.y-s/2,s,s)}
else if(p.t==='bf'){ctx.fillStyle='rgba(255,255,200,'+(a*.8)+')';ctx.beginPath();ctx.arc(p.x,p.y,s*2,0,Math.PI*2);ctx.fill()}
ctx.restore()}}}
const ps=new PS();
// 领航者号回收平台
class RS{
constructor(x,y){this.x=x;this.y=y;this.w=240;this.h=28;this.netActive=false;this.netP=0;this.captured=false;this.capA=0;this.secP=0;this.rf=0;this.sr=null;this.nh=0;this.nth=0;this.hk=[false,false,false,false];this.oa=0}
activate(){this.netActive=true;this.nh=0;this.nth=0}
update(){this.oa+=.008;
if(this.netActive&&!this.captured){this.netP=Math.min(1,this.netP+.018);this.nh=this.netP*140;this.nth=this.netP*120;this.rf+=.05;
if(this.sr&&this.sr.state==='RETURNING'&&Math.hypot(this.sr.x-this.x,this.sr.y-this.y)<400&&Math.random()<.15)ps.add('n',this.x+(Math.random()-.5)*120,this.y-this.nh*.5-Math.random()*60,{size:4+Math.random()*8,life:15+Math.random()*10})}
if(this.captured){if(this.capA<1){this.capA+=.025;if(this.capA>.25&&!this.hk[0])this.hk[0]=true;if(this.capA>.40&&!this.hk[1])this.hk[1]=true;if(this.capA>.55&&!this.hk[2])this.hk[2]=true;if(this.capA>.70&&!this.hk[3])this.hk[3]=true}
if(this.capA>=1&&this.secP<1){this.secP+=.012;this.nh=Math.max(20,this.nh-.8)}}}
draw(){ctx.save();ctx.translate(this.x,this.y);
let gr=ctx.createLinearGradient(0,-this.h/2,0,this.h/2);gr.addColorStop(0,'#3a4a6a');gr.addColorStop(.3,'#2a3a5a');gr.addColorStop(.7,'#1a2a4a');gr.addColorStop(1,'#0a1a3a');
ctx.fillStyle=gr;ctx.beginPath();ctx.roundRect(-this.w/2,-this.h/2,this.w,this.h,4);ctx.fill();ctx.strokeStyle='#4a6a8a';ctx.lineWidth=2;ctx.stroke();
ctx.fillStyle='#2a3a5a';for(let i=-110;i<110;i+=18)ctx.fillRect(i,-this.h/2+3,12,this.h-6);
ctx.fillStyle='#0ff';ctx.font='bold 10px Arial';ctx.textAlign='center';ctx.fillText('领航者号',0,-this.h/2-10);ctx.fillStyle='#8af';ctx.font='7px Arial';ctx.fillText('LHZ-001',0,this.h/2+14);
ctx.fillStyle='#1a2a3a';[-100,-50,50,100].forEach(ox=>{ctx.fillRect(ox-5,this.h/2,10,35);ctx.fillStyle='#2a3a5a';ctx.fillRect(ox-8,this.h/2+10,3,20);ctx.fillRect(ox+5,this.h/2+10,3,20)});
[-110,-55,55,110].forEach(ox=>{ctx.fillStyle=this.captured?'#0f0':(this.netActive?'hsl('+(180+Math.sin(this.rf)*25)+',100%,50%)':'#ff0');ctx.shadowBlur=12;ctx.shadowColor=ctx.fillStyle;ctx.beginPath();ctx.arc(ox,-this.h/2-5,3.5,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0});
if(this.netActive){ctx.fillStyle='rgba(0,255,255,0.2)';ctx.font='6px Arial';ctx.fillText('DP2 动力定位',0,this.h/2+28)}
if(this.netActive||this.captured){let tw=60,th=this.nth,nh=this.nh,na=this.captured?0.85:(.3+this.netP*.5);
ctx.strokeStyle='#556';ctx.lineWidth=3;ctx.lineCap='round';
[-tw/2,tw/2].forEach(ox=>{ctx.beginPath();ctx.moveTo(ox,0);ctx.lineTo(ox,-th);ctx.stroke();for(let j=0;j<th;j+=20){ctx.strokeStyle='#445';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(ox-8,-j);ctx.lineTo(ox+8,-j);ctx.stroke()}ctx.strokeStyle='#556';ctx.lineWidth=3});
ctx.strokeStyle='#667';ctx.lineWidth=4;ctx.beginPath();ctx.moveTo(-tw/2,-th);ctx.lineTo(tw/2,-th);ctx.stroke();
ctx.strokeStyle='#445';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(-tw/2,0);ctx.lineTo(tw/2,-th*.6);ctx.stroke();ctx.beginPath();ctx.moveTo(tw/2,0);ctx.lineTo(-tw/2,-th*.6);ctx.stroke();
ctx.save();let nl=-tw/2-6,nr=tw/2+6,nt=-th,nb=-th+nh;
ctx.strokeStyle='rgba(0,255,255,'+na+')';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(nl,nt);ctx.quadraticCurveTo(0,nt-20,nr,nt);ctx.stroke();ctx.beginPath();ctx.moveTo(nl,nb);ctx.quadraticCurveTo(0,nb+20,nr,nb);ctx.stroke();
for(let i=1;i<8;i++){let t=i/8,x=nl+(nr-nl)*t;ctx.strokeStyle='rgba(0,255,255,'+(na*.6)+')';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(x,nt);ctx.quadraticCurveTo(x+Math.sin(t*Math.PI)*7.5,(nt+nb)/2,x,nb);ctx.stroke()}
for(let i=1;i<10;i++){let t=i/10,y=nt+(nb-nt)*t;ctx.strokeStyle='rgba(0,255,255,'+(na*.5)+')';ctx.lineWidth=.8;ctx.beginPath();ctx.moveTo(nl,y);ctx.quadraticCurveTo(0,y+Math.sin(t*Math.PI)*8,nr,y);ctx.stroke()}
ctx.restore();
if(!this.captured&&this.netP>.3){ctx.shadowBlur=20*this.netP;ctx.shadowColor='#0ff';ctx.strokeStyle='rgba(0,255,255,'+(this.netP*.2)+')';ctx.lineWidth=1.5;let pr=50*this.netP+Math.sin(this.rf*3)*15;ctx.beginPath();ctx.arc(0,-th+nh*.5,pr,0,Math.PI*2);ctx.stroke();ctx.shadowBlur=0;
ctx.strokeStyle='rgba(0,255,255,'+(this.netP*.1)+')';ctx.lineWidth=1;for(let i=0;i<4;i++){let a=this.oa+i*Math.PI/2;ctx.beginPath();ctx.moveTo(0,-th+nh*.5);ctx.lineTo(Math.cos(a)*120,-th+nh*.5+Math.sin(a)*120);ctx.stroke()}}
if(this.captured){[-20,-7,7,20].forEach((ox,i)=>{let l=this.hk[i];ctx.save();ctx.translate(ox,-th-5);ctx.fillStyle=l?'#0f0':'#888';ctx.shadowBlur=l?10:0;ctx.shadowColor='#0f0';ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();if(l){ctx.strokeStyle='rgba(0,255,100,0.6)';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(0,3);ctx.lineTo(0,15+this.secP*20);ctx.stroke()}ctx.shadowBlur=0;ctx.restore()})}
if(this.captured&&this.secP>0){let sp=this.secP,cx=0,cy=-th+nh*.5;ctx.strokeStyle='rgba(0,255,100,'+(sp*.7)+')';ctx.lineWidth=2+sp*3;
for(let i=0;i<8;i++){let a=(i/8)*Math.PI*2+sp*Math.PI*2;ctx.beginPath();ctx.arc(cx,cy,15+(1-sp)*45,a,a+.35);ctx.stroke()}
if(sp<1){ctx.strokeStyle='rgba(0,255,200,'+(sp*.5)+')';ctx.lineWidth=1.5;for(let i=0;i<4;i++){let a=(i/4)*Math.PI*2+sp*Math.PI;ctx.beginPath();ctx.moveTo(cx+Math.cos(a)*(12+(1-sp)*35),cy+Math.sin(a)*(12+(1-sp)*35));ctx.lineTo(cx+Math.cos(a+.5)*(4+(1-sp)*18),cy+Math.sin(a+.5)*(4+(1-sp)*18));ctx.stroke()}}
if(sp>=1){ctx.fillStyle='#0f0';ctx.shadowBlur=30;ctx.shadowColor='#0f0';ctx.font='bold 20px Arial';ctx.textAlign='center';ctx.fillText('网系收束完毕',0,-th-60);ctx.shadowBlur=0}}}
if(this.captured){if(Math.random()<.2)ps.add('n',this.x+(Math.random()-.5)*180,this.y-120-Math.random()*80,{size:6+Math.random()*14,life:25+Math.random()*20});if(this.secP>=.3&&Math.random()<.12)ps.add('c',this.x+(Math.random()-.5)*250,this.y-60-Math.random()*120,{vx:(Math.random()-.5)*3,vy:-Math.random()*4-1,size:7+Math.random()*8,life:50+Math.random()*30,drag:.97,g:.02,hue:Math.random()*360})}
ctx.restore()}
}
// 相机
const cam={x:0,y:0,z:1,mode:'FREE'};
function uC(t){let tx,ty,tz;if(cam.mode==='FREE'){tx=Math.max(W/2,t.x+50);ty=Math.min(t.y,GY-250);tz=.5}else if(cam.mode==='SHIP'){tx=t.x;ty=t.y-160;tz=.6}else if(cam.mode==='STAGE1'){tx=t.x;ty=t.y-80;tz=.85}else{tx=t.x;ty=t.y-80;tz=.95}
cam.x+=(tx-cam.x)*.05;cam.y+=(ty-cam.y)*.05;cam.z+=(tz-cam.z)*.05}
function setView(evt,m){cam.mode=m;document.querySelectorAll('.view-btns button').forEach(b=>b.classList.remove('active'));evt.target.classList.add('active')}
// 一子级(返回级)
class S1{
constructor(x,y){this.x=x;this.y=y;this.vx=0;this.vy=0;this.angle=0;this.angV=0;this.state='ATTACHED';this.shake=0;this.mass=25;this.fuel=200;this.maxT=6;this.rate=.15;this.thrust=0;this.gf=false;this.gfA=0;this.tx=x;this.ty=GY-10;this.st=0;this.fp=0;this.bb=false;this.rb=false;this.lb=false;this.bc=0;this.hk=[false,false,false,false]}
get alt(){return GY-this.y}
get spd(){return Math.hypot(this.vx,this.vy)}
update(){if(this.state==='ATTACHED'||this.state==='LANDED'||this.state==='EXPLODED'||this.state==='CAPTURED'||this.state==='ASCENT'||this.state==='CORE_ASCENT')return;let fx=0,fy=this.mass*G;
if(this.state==='FLIP'){this.st++;this.fp=Math.min(1,this.st/80);let ta=Math.PI*.85,ad=ta-this.angle;while(ad>Math.PI)ad-=Math.PI*2;while(ad<-Math.PI)ad+=Math.PI*2;this.angV+=ad*.06;this.angle+=this.angV;this.angV*=.6;if(this.st>15){this.gf=true;this.gfA=Math.min(1,this.gfA+.04)}if(this.fp>=1&&Math.abs(ad)<.15){this.state='RETURNING';this.bb=false;this.tx=window.ship.x;this.ty=window.ship.y-5}if(this.fuel>0&&this.st>10){this.thrust=.2*(1-this.fp);let t=this.maxT*this.thrust;fx+=t*Math.sin(this.angle);fy-=t*Math.cos(this.angle);this.fuel-=this.rate*this.thrust*.3;let ex=this.x-Math.sin(this.angle)*35,ey=this.y+Math.cos(this.angle)*35;ps.add('fb',ex,ey,{vx:-Math.sin(this.angle)*(1+Math.random()),vy:Math.cos(this.angle)*(1+Math.random()),size:3+Math.random()*5,life:6+Math.random()*4})}this.shake=.3}
if(this.state==='RETURNING'){let dx=this.tx-this.x,dy=this.ty-this.y,alt=this.alt,dist=Math.hypot(dx,dy);if(this.gf)this.gfA=Math.min(1,this.gfA+.02);let na=Math.atan2(dx,-dy),bw=Math.max(0,Math.min(1,1-alt/350));
let bbw=(!this.bb&&dist>300&&alt>200)?Math.min(1,(dist-200)/300):0;if(bbw>.3&&this.st>80)this.bb=true;
let rw=(!this.rb&&alt<200&&alt>80&&dist<350)?Math.min(1,1-alt/200):0;if(alt<85)this.rb=true;
let lw=(alt<80&&dist<200)?Math.min(1,1-alt/80):0;if(lw>0)this.lb=true;
this.thrust=Math.max(.05,Math.min(1,Math.max(bbw*.6,rw,lw*.8,bw*.3)));let da=na*(1-bw*.7)+(bw>.5?.15:0),ad=da-this.angle;while(ad>Math.PI)ad-=Math.PI*2;while(ad<-Math.PI)ad+=Math.PI*2;this.angV+=ad*.07;this.angle+=this.angV;this.angV*=.5;this.shake=this.thrust*.5;
if(this.thrust>.01&&this.fuel>0){let t=this.maxT*this.thrust;fx+=t*Math.sin(this.angle);fy-=t*Math.cos(this.angle);this.fuel-=this.rate*this.thrust;let ex=this.x-Math.sin(this.angle)*38,ey=this.y+Math.cos(this.angle)*38,ib=alt<100||bw>.5;ps.add(ib?'fb':'f',ex,ey,{vx:-Math.sin(this.angle)*(2+this.thrust*2.5)+(Math.random()-.5)*1.5,vy:Math.cos(this.angle)*(2+this.thrust*2.5)+(Math.random()-.5)*1.5,size:5+Math.random()*12,life:10+Math.random()*8});if(alt<40&&Math.random()<.3)ps.add('s',this.x+(Math.random()-.5)*30,this.y+50+Math.random()*20,{vx:(Math.random()-.5)*2,vy:Math.random()*2,size:8+Math.random()*10,life:15+Math.random()*10})}
if(this.thrust>.3){if(bbw>.3&&this.bc===0)this.bc=1;if(rw>.3&&this.bc===1)this.bc=2;if(lw>.3&&this.bc===2)this.bc=3}}
let v=this.spd;if(v>0){let d=AD*v*v;fx-=d*(this.vx/v);fy-=d*(this.vy/v)}this.vx+=fx/this.mass;this.vy+=fy/this.mass;this.x+=this.vx;this.y+=this.vy;
if(this.state==='RETURNING'||this.state==='FLIP'){let s=window.ship;if(s&&s.netActive&&s.netP>=.5){let ny=s.y-s.nth,by=s.y-s.nth+s.nh,dx=Math.abs(this.x-s.x),rb=this.y+45;if(dx<200&&rb>ny-50&&rb<by+100&&this.vy>0){this.state='CAPTURED';this.vx=0;this.vy=0;this.thrust=0;s.captured=true;s.sr=this;this.hk=[true,true,true,true];ps.burst('bf',this.x,this.y,40,{size:12,life:25,spread:40,spd:4,drag:.9,g:-.03});ps.burst('n',this.x,this.y-30,80,{size:10,life:40,spread:60,dvx:0,dvy:-2,g:-.02});ps.burst('sm',this.x,this.y+10,50,{size:20,life:45,spread:50,spd:2,drag:.9,g:-.02});ps.burst('sp',this.x,this.y-20,40,{size:4,life:25,spread:35,spd:6,drag:.88,g:-.01})}}}
if(this.y>=GY-10&&this.vy>0){if(this.state==='CAPTURED'){this.y=GY-10}else{this.state='EXPLODED';this.expl()}}}
sep(){this.state='FLIP';this.st=0;this.fp=0;this.gf=false;this.gfA=0;this.vx=1.5;this.angle=Math.PI*.15}
expl(){ps.burst('e',this.x,this.y,40,{size:12,life:30,spread:20,spd:6,drag:.95,g:.02});ps.burst('sp',this.x,this.y,30,{size:4,life:20,spread:15,spd:8,drag:.92,g:.05})}
draw(){if(this.state==='ATTACHED'||this.state==='EXPLODED')return;ctx.save();let sx=this.x+(Math.random()-.5)*this.shake,sy=this.y+(Math.random()-.5)*this.shake;ctx.translate(sx,sy);ctx.rotate(this.angle);
if(this.thrust>0){ctx.globalAlpha=this.thrust*.35;ctx.drawImage(tx.glow,-50,-70,100,100);ctx.globalAlpha=1}let w=18,h=80,bh=h+10;
let g=ctx.createLinearGradient(-w,0,w,0);g.addColorStop(0,'#444');g.addColorStop(.3,'#ccc');g.addColorStop(.5,'#fff');g.addColorStop(.7,'#ccc');g.addColorStop(1,'#444');
ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(0,-bh);ctx.bezierCurveTo(3,-bh+5,w,-bh+18,w,-bh+25);ctx.lineTo(w,h);ctx.lineTo(-w,h);ctx.lineTo(-w,-bh+25);ctx.bezierCurveTo(-w,-bh+18,-3,-bh+5,0,-bh);ctx.fill();ctx.strokeStyle='rgba(255,255,255,0.1)';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='#d00';ctx.fillRect(-w+3,-bh+15,w*2-6,8);ctx.fillStyle='#000';ctx.font='bold 7px Arial';ctx.textAlign='center';ctx.fillText('一子级',0,-5);ctx.fillStyle='#d00';ctx.font='bold 5px Arial';ctx.fillText('中国航天',0,-bh+35);
ctx.fillStyle='#333';let nw=w*.6;ctx.beginPath();ctx.moveTo(-nw,h);ctx.lineTo(-nw-3,h+5);ctx.lineTo(nw+3,h+5);ctx.lineTo(nw,h);ctx.fill();
if(this.gf){let ga=this.gfA*.35;ctx.fillStyle='#555';ctx.strokeStyle='#888';ctx.lineWidth=1;[-1,1].forEach(s=>{ctx.save();ctx.translate(s*(w+6),0);ctx.rotate(s*ga);ctx.fillRect(-4,-12,8,24);ctx.strokeRect(-4,-12,8,24);for(let j=-10;j<12;j+=4){ctx.beginPath();ctx.moveTo(-4,j);ctx.lineTo(4,j);ctx.stroke()}for(let j=-3;j<=3;j+=3){ctx.beginPath();ctx.moveTo(j,-12);ctx.lineTo(j,12);ctx.stroke()}ctx.restore()});ctx.save();ctx.translate(0,20);[-1,1].forEach(s=>{ctx.save();ctx.translate(s*(w+6),0);ctx.rotate(s*ga*.8);ctx.fillStyle='#555';ctx.strokeStyle='#888';ctx.lineWidth=1;ctx.fillRect(-3,-8,6,16);ctx.strokeRect(-3,-8,6,16);for(let j=-6;j<8;j+=4){ctx.beginPath();ctx.moveTo(-3,j);ctx.lineTo(3,j);ctx.stroke()}ctx.restore()});ctx.restore()}
if(this.state==='CAPTURED'){ctx.fillStyle='#0f0';ctx.shadowBlur=8;ctx.shadowColor='#0f0';[-12,-4,4,12].forEach(ox=>{ctx.beginPath();ctx.arc(ox,h+12,2.5,0,Math.PI*2);ctx.fill()});ctx.shadowBlur=0}
if(this.state==='RETURNING'||this.state==='FLIP'){ctx.fillStyle='rgba(255,255,0,0.3)';ctx.font='6px Arial';ctx.fillText('返回',0,h+22)}
ctx.restore()}
}
// 二子级
class S2{
constructor(x,y){this.x=x;this.y=y;this.vx=0;this.vy=0;this.angle=0;this.angV=0;this.state='ATTACHED';this.shake=0;this.mass=8;this.fuel=80;this.maxT=2.5;this.rate=.18;this.thrust=0;this.ot=0;this.fd=false}
get alt(){return GY-this.y}
get spd(){return Math.hypot(this.vx,this.vy)}
update(){if(this.state==='ATTACHED'||this.state==='LANDED')return;
if(this.state==='ORBITING'){this.ot++;this.thrust=.6;if(this.fuel>0){this.vy-=.06;this.vx+=.005;this.fuel-=this.rate*.4}this.y+=this.vy;this.x+=this.vx;this.angle=Math.atan2(this.vx,-this.vy)*.3;
if(this.ot>80&&!this.fd){this.fd=true;ps.burst('sep',this.x,this.y-20,15,{size:3,life:15,spread:10,spd:3,drag:.95,g:.01})}
if(Math.random()<.4)ps.add('fb',this.x+(Math.random()-.5)*4,this.y+30+Math.random()*5,{vx:(Math.random()-.5)*.8,vy:Math.random()+.5,size:4+Math.random()*5,life:8+Math.random()*5,drag:.94,g:.02});this.shake=.1;if(this.y<-600)this.state='LANDED'}}
sep(){this.state='ORBITING';this.ot=0;this.fd=false;this.angle=.1;this.thrust=.5}
draw(){if(this.state==='ATTACHED'||this.state==='LANDED')return;ctx.save();let sx=this.x+(Math.random()-.5)*this.shake,sy=this.y+(Math.random()-.5)*this.shake;ctx.translate(sx,sy);ctx.rotate(this.angle);
if(this.thrust>0){ctx.globalAlpha=this.thrust*.25;ctx.drawImage(tx.glow,-40,-50,80,80);ctx.globalAlpha=1}let w=12,h=45,bh=h+10;
let g=ctx.createLinearGradient(-w,0,w,0);g.addColorStop(0,'#666');g.addColorStop(.3,'#ddd');g.addColorStop(.5,'#fff');g.addColorStop(.7,'#ddd');g.addColorStop(1,'#666');
ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(0,-bh);ctx.bezierCurveTo(3,-bh+5,w,-bh+12,w,-bh+18);ctx.lineTo(w,h);ctx.lineTo(-w,h);ctx.lineTo(-w,-bh+18);ctx.bezierCurveTo(-w,-bh+12,-3,-bh+5,0,-bh);ctx.fill();ctx.strokeStyle='rgba(255,255,255,0.15)';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='#d00';ctx.fillRect(-w+3,-bh+12,w*2-6,5);ctx.fillStyle='#000';ctx.font='bold 6px Arial';ctx.textAlign='center';ctx.fillText('二子级',0,-5);ctx.fillStyle='#444';let nw=w*.6;ctx.beginPath();ctx.moveTo(-nw,h);ctx.lineTo(-nw-3,h+5);ctx.lineTo(nw+3,h+5);ctx.lineTo(nw,h);ctx.fill();
if(this.state==='ORBITING'&&this.ot>30){ctx.fillStyle='rgba(0,255,255,0.5)';ctx.font='5px Arial';ctx.fillText('入轨飞行中',0,-bh-25)}
ctx.restore()}
}
// 全局变量
let s1,s2,ship,time=0,running=false,sepDone=false,traj=[];
window.ship=null;
function init(){ship=new RS(W/2+300,GY);s1=new S1(W/2,GY-10);s2=new S2(W/2,GY-10);window.ship=ship;s1.tx=ship.x;s1.ty=ship.y-5;sepDone=false;cam.x=W/2;cam.y=GY-200;cam.z=.6;document.getElementById('btn_launch').disabled=false;traj=[]}
function launch(){if(s1.state!=='ATTACHED')return;s1.state='ASCENT';s2.state='ASCENT';running=true;document.getElementById('btn_launch').disabled=true;ps.burst('sm',s1.x,GY,60,{size:30,life:60,spread:80,spd:5,drag:.92,g:-.04,dvx:0,dvy:-3});ps.burst('s',s1.x,GY,100,{size:25,life:80,spread:60,spd:8,drag:.95,g:-.02,dvx:0,dvy:-4});ps.burst('f',s1.x,GY+20,40,{size:15,life:20,spread:20,spd:3,drag:.9,g:-.01,dvx:0,dvy:-2})}
function reset(){running=false;time=0;ps.p=[];traj=[];init()}
function uL(){if(!running)return;time+=.5;
if(s1.state==='ASCENT'){let tm=s1.mass+s2.mass,tt=s1.maxT+s2.maxT,ay=G-(tt/tm);s1.vy+=ay;s1.y+=s1.vy;if(s1.alt>60){s1.vx+=.008;s1.angle=Math.atan2(s1.vx,-s1.vy)*.35}s1.x+=s1.vx;s2.x=s1.x;s2.y=s1.y-85;s2.vx=s1.vx;s2.vy=s1.vy;s2.angle=s1.angle;s1.fuel-=s1.rate;s2.fuel-=s2.rate*.5;s1.shake=.3;if(Math.random()<.6)ps.add('f',s1.x+(Math.random()-.5)*8,s1.y+55+Math.random()*10,{vx:(Math.random()-.5)*1.5,vy:Math.random()*2+1,size:6+Math.random()*8,life:10+Math.random()*6,drag:.92,g:.02});if(s1.alt>600){s1.state='CORE_ASCENT';s2.state='ORBITING';s2.sep();sepDone=true;ps.burst('sep',s1.x,s1.y-40,40,{size:5,life:25,spread:15,spd:4,drag:.95,g:.01});ps.burst('sp',s1.x,s1.y-40,20,{size:3,life:15,spread:10,spd:6,drag:.9,g:.02});document.getElementById('boost_stat').textContent='分离返回';document.getElementById('boost_stat').className='caution'}}
else if(s1.state==='CORE_ASCENT'){s1.vy+=G;s1.y+=s1.vy;s1.vx+=.005;s1.x+=s1.vx;s1.angle=Math.atan2(s1.vx,-s1.vy)*.35;s1.shake=.1;if(Math.random()<.3)ps.add('s',s1.x+(Math.random()-.5)*6,s1.y+(Math.random()-.5)*6,{vx:(Math.random()-.5)*.3,vy:(Math.random()-.5)*.3,size:2+Math.random()*3,life:15+Math.random()*10,drag:.98,g:0});if(s1.vy>=0&&s1.alt>200){s1.sep()}}
if(s2.state==='ORBITING'&&s2.ot>10){document.getElementById('upper_stat').textContent='已入轨';document.getElementById('upper_stat').className='ok'}
if(s1.state==='RETURNING'&&!ship.netActive&&Math.hypot(s1.x-ship.x,s1.y-ship.y)<600){ship.activate();ship.sr=s1;document.getElementById('ship_stat').textContent='网系展开中';document.getElementById('ship_stat').className='caution'}
if(ship.netActive&&!ship.captured&&ship.netP>=1)document.getElementById('ship_stat').textContent='36m绳网已就绪';
s1.update();s2.update();ship.update();if(running&&s1.state!=='ATTACHED'){traj.push({x:s1.x,y:s1.y});if(traj.length>300)traj.shift()}}
function uH(){let t=s1;if(cam.mode==='STAGE1')t=s1;else if(cam.mode==='SHIP')t=ship;
if(t instanceof RS){document.getElementById('alt').textContent='0.00 km';document.getElementById('vel').textContent='0 m/s';document.getElementById('vvel').textContent='0 m/s';document.getElementById('fuel').textContent='100%';document.getElementById('stat').textContent=t.captured?'已网系捕获':'待命';document.getElementById('stat').className=t.captured?'ok':'caution'}
else{document.getElementById('alt').textContent=(t.alt/100).toFixed(2)+' km';document.getElementById('vel').textContent=(t.spd*10).toFixed(1)+' m/s';document.getElementById('vvel').textContent=(t.vy*10).toFixed(1)+' m/s';document.getElementById('fuel').textContent=Math.max(0,t.fuel).toFixed(0)+'%';let s=document.getElementById('stat');if(t.state==='EXPLODED'){s.textContent='失败';s.className='warn'}else if(t.state==='LANDED'||t.state==='CAPTURED'){s.textContent='回收成功';s.className='ok'}else if(t.state==='ASCENT'||t.state==='CORE_ASCENT'){s.textContent='上升中';s.className='caution'}else if(t.state==='RETURNING'){s.textContent='返回中';s.className='caution'}else{s.textContent='待命';s.className='caution'}}
document.getElementById('t').textContent='T+'+time.toFixed(0)+'s';let ph='全箭上升';if(s1.state==='CORE_ASCENT')ph='一子级工作 二子级分离';else if(s1.state==='RETURNING'&&!ship.captured)ph='一子级返回 二子级入轨中';else if(ship.captured&&ship.secP<1)ph='网系收束中...';else if(ship.captured&&ship.secP>=1)ph='一子级网系回收成功!';document.getElementById('phase').textContent=ph;
let bs=document.getElementById('boost_stat');if(s1.state==='CAPTURED'){bs.textContent='网系捕获';bs.className='ok'}else if(s1.state==='RETURNING'){bs.textContent='返回中';bs.className='caution'}else{bs.textContent='工作中';bs.className='caution'}
let us=document.getElementById('upper_stat');if(s2.state==='LANDED'){us.textContent='已入轨';us.className='ok'}else if(s2.state==='ORBITING'){us.textContent='入轨飞行中';us.className='caution'}else{us.textContent='锚定';us.className='ok'}
let pse=document.getElementById('ship_stat');if(ship.captured&&ship.secP>=1){pse.textContent='收束完毕';pse.className='ok'}else if(ship.captured){pse.textContent='收束中';pse.className='ok'}else if(ship.netActive&&ship.netP>=1){pse.textContent='36m绳网已展开';pse.className='ok'}else if(ship.netActive){pse.textContent='网系展开中';pse.className='caution'}}
function drawStack(){ctx.save();ctx.translate(s1.x,s1.y);ctx.rotate(s1.angle);let sk=s1.shake||0,sx=(Math.random()-.5)*sk,sy=(Math.random()-.5)*sk;ctx.save();ctx.translate(sx,sy);
let cg=ctx.createLinearGradient(-16,0,16,0);cg.addColorStop(0,'#444');cg.addColorStop(.3,'#bbb');cg.addColorStop(.5,'#fff');cg.addColorStop(.7,'#bbb');cg.addColorStop(1,'#444');ctx.fillStyle=cg;ctx.beginPath();ctx.moveTo(0,-90);ctx.bezierCurveTo(4,-80,16,-70,16,-50);ctx.lineTo(16,60);ctx.lineTo(-16,60);ctx.lineTo(-16,-50);ctx.bezierCurveTo(-16,-70,-4,-80,0,-90);ctx.fill();ctx.fillStyle='#d00';ctx.fillRect(-10,-75,20,12);ctx.fillStyle='#000';ctx.font='bold 9px Arial';ctx.textAlign='center';ctx.fillText('一子级',0,-25);ctx.fillStyle='#d00';ctx.font='bold 6px Arial';ctx.fillText('中国航天',0,-40);ctx.fillStyle='#333';ctx.fillRect(-18,55,36,6);
ctx.save();ctx.translate(sx,sy-80);let ug=ctx.createLinearGradient(-10,0,10,0);ug.addColorStop(0,'#666');ug.addColorStop(.5,'#ddd');ug.addColorStop(1,'#666');ctx.fillStyle=ug;ctx.beginPath();ctx.moveTo(0,-40);ctx.bezierCurveTo(2,-35,10,-28,10,-20);ctx.lineTo(10,15);ctx.lineTo(-10,15);ctx.lineTo(-10,-20);ctx.bezierCurveTo(-10,-28,-2,-35,0,-40);ctx.fill();ctx.fillStyle='#000';ctx.font='bold 6px Arial';ctx.textAlign='center';ctx.fillText('二子级',0,-5);ctx.restore();
ctx.globalAlpha=.85;ctx.drawImage(tx.flame,-14,60,28,35+Math.random()*15);ctx.globalAlpha=1;ctx.drawImage(tx.glow,-50,30,100,100);ctx.restore();ctx.restore()}
function drawHUD(){ctx.save();ctx.font='9px Arial';ctx.textAlign='left';ctx.fillStyle='rgba(0,255,255,0.5)';
if(s1.state==='RETURNING'){let a=s1.alt,d=Math.hypot(s1.x-ship.x,s1.y-ship.y);ctx.fillText('一子级距平台: '+(d/10).toFixed(0)+'m',10,GY-40);ctx.fillText('一子级高度: '+(a/10).toFixed(0)+'m',10,GY-28);ctx.fillText('制动阶段: '+(['','动力减速','气动减速','着陆减速'][s1.bc]||'无'),10,GY-16)}
if(s2.state==='ORBITING'){ctx.fillStyle='rgba(0,200,255,0.6)';ctx.fillText('二子级入轨飞行中',W/2-60,100)}
ctx.restore()}
// 分屏渲染
var camL={x:0,y:0,z:.5},camR={x:0,y:0,z:.6};
function drawSplitScreen(){
const hw=W/2;
// 左半屏:二子级入轨
ctx.save();ctx.beginPath();ctx.rect(0,0,hw,H);ctx.clip();ctx.translate(hw/2,H/2);
camL.x+=(Math.max(s2.x,-100)-camL.x)*.05;camL.y+=(Math.min(s2.y,GY-250)-camL.y)*.05;camL.z+=(.5-camL.z)*.05;
ctx.scale(camL.z,camL.z);ctx.translate(-camL.x,-camL.y);
drawBG();ship.draw();if(s2.state!=='LANDED')s2.draw();
if(traj.length>2){ctx.save();ctx.beginPath();for(var i=0;i<traj.length;i++){if(i===0)ctx.moveTo(traj[i].x,traj[i].y);else ctx.lineTo(traj[i].x,traj[i].y)}ctx.strokeStyle='rgba(0,200,255,0.15)';ctx.lineWidth=1;ctx.setLineDash([6,4]);ctx.stroke();ctx.setLineDash([]);ctx.restore()}
ctx.restore();ctx.save();ctx.fillStyle='rgba(0,200,255,0.8)';ctx.font='bold 14px Arial';ctx.textAlign='center';ctx.fillText('二子级入轨追踪',hw/2,30);ctx.restore();
// 右半屏:一子级回收
ctx.save();ctx.beginPath();ctx.rect(hw,0,hw,H);ctx.clip();ctx.translate(hw+hw/2,H/2);
var tx=s1.x,ty=s1.y-80;if(ship.captured||s1.state==='CAPTURED'){tx=ship.x;ty=ship.y-160}
camR.x+=(tx-camR.x)*.05;camR.y+=(ty-camR.y)*.05;camR.z+=(.6-camR.z)*.05;
ctx.scale(camR.z,camR.z);ctx.translate(-camR.x,-camR.y);
drawBG();ship.draw();if(s1.state!=='EXPLODED'&&s1.state!=='LANDED'&&s1.state!=='CAPTURED')s1.draw();
if(traj.length>2){ctx.save();ctx.beginPath();for(var i=0;i<traj.length;i++){if(i===0)ctx.moveTo(traj[i].x,traj[i].y);else ctx.lineTo(traj[i].x,traj[i].y)}ctx.strokeStyle='rgba(255,200,0,0.2)';ctx.lineWidth=1.5;ctx.setLineDash([6,4]);ctx.stroke();ctx.setLineDash([]);ctx.restore()}
ps.draw();ctx.restore();ctx.save();ctx.fillStyle='rgba(255,200,0,0.8)';ctx.font='bold 14px Arial';ctx.textAlign='center';ctx.fillText('一子级网系回收',hw+hw/2,30);ctx.restore();
// 分隔线
ctx.save();ctx.strokeStyle='rgba(0,255,255,0.3)';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(hw,0);ctx.lineTo(hw,H);ctx.stroke();ctx.restore()}
function drawBG(){
var bg=ctx.createLinearGradient(0,GY-800,0,GY);bg.addColorStop(0,'#000005');bg.addColorStop(.5,'#050a15');bg.addColorStop(1,'#0a1530');ctx.fillStyle=bg;ctx.fillRect(-2000,-1000,4000,2000);
ctx.fillStyle='#fff';for(var i=0;i<200;i++){var x=(i*137)%2000-1000,y=(i*97)%600;ctx.globalAlpha=.2+Math.sin(time*.1+i*.7)*.3;ctx.beginPath();ctx.arc(x,y,(i%3)*.5+.5,0,Math.PI*2);ctx.fill()}ctx.globalAlpha=1;
var og=ctx.createLinearGradient(0,GY-30,0,GY+100);og.addColorStop(0,'#0a1a3a');og.addColorStop(1,'#050f20');ctx.fillStyle=og;ctx.fillRect(-2000,GY-20,4000,500);
ctx.strokeStyle='rgba(100,150,200,0.25)';ctx.lineWidth=1;for(var i=0;i<4;i++){ctx.beginPath();for(var x=-2000;x<2000;x+=20){var y=GY-12+i*8+Math.sin(x*.018+time*.12+i*1.5)*3;if(x===-2000)ctx.moveTo(x,y);else ctx.lineTo(x,y)}ctx.stroke()}}
function loop(){requestAnimationFrame(loop);ctx.clearRect(0,0,W,H);if(running){uL();ps.update()}
if(cam.mode==='SPLIT'){drawSplitScreen();uH();return}
var t;if(cam.mode==='SHIP')t=ship;else if(cam.mode==='STAGE1')t=s1;else if(cam.mode==='STAGE2')t=s2;else if(cam.mode==='FREE'){var ty=Math.min(s1.y,s2.y),mx=Math.max(s1.x,s2.x,ship.x);t=(s2.state==='LANDED')?{x:mx,y:Math.min(s1.y,s2.y)}:{x:mx,y:ty}}else t=s1;uC(t);
ctx.save();ctx.translate(W/2,H/2);ctx.scale(cam.z,cam.z);ctx.translate(-cam.x,-cam.y);
if(traj.length>2){ctx.save();ctx.beginPath();for(var i=0;i<traj.length;i++){if(i===0)ctx.moveTo(traj[i].x,traj[i].y);else ctx.lineTo(traj[i].x,traj[i].y)}ctx.strokeStyle='rgba(0,200,255,0.2)';ctx.lineWidth=1.5;ctx.setLineDash([6,4]);ctx.stroke();ctx.setLineDash([]);ctx.restore()}
// 高级轨迹绘制:上升段青色实线,返回段橙色实线
if(traj.length>4&&sepDone){var sepIdx=Math.floor(traj.length*.35);
ctx.save();ctx.beginPath();for(var i=0;i<sepIdx;i++){if(i===0)ctx.moveTo(traj[i].x,traj[i].y);else ctx.lineTo(traj[i].x,traj[i].y)}ctx.strokeStyle='rgba(0,200,255,0.25)';ctx.lineWidth=2;ctx.stroke();ctx.restore();
ctx.save();ctx.beginPath();for(var i=sepIdx;i<traj.length;i++){if(i===sepIdx)ctx.moveTo(traj[i].x,traj[i].y);else ctx.lineTo(traj[i].x,traj[i].y)}ctx.strokeStyle='rgba(255,150,50,0.3)';ctx.lineWidth=2.5;ctx.setLineDash([8,6]);ctx.stroke();ctx.setLineDash([]);ctx.restore();
// 分离点标记
if(sepDone){ctx.save();ctx.fillStyle='#f80';ctx.shadowBlur=8;ctx.shadowColor='#f80';ctx.beginPath();ctx.arc(traj[sepIdx].x,traj[sepIdx].y,4,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.restore()}}
if(running&&(s1.state==='ASCENT'||s1.state==='CORE_ASCENT')&&Math.random()<.3)ps.add('s',s1.x+(Math.random()-.5)*8,s1.y+(Math.random()-.5)*8,{vx:(Math.random()-.5)*.3,vy:(Math.random()-.5)*.3,size:2+Math.random()*3,life:20+Math.random()*15,drag:.98,g:0});
drawBG();ship.draw();
if(s1.state==='ASCENT')drawStack();else{if(s1.state!=='EXPLODED'&&s1.state!=='LANDED'&&s1.state!=='CAPTURED')s1.draw();if(s2.state!=='LANDED')s2.draw()}
ps.draw();drawHUD();
if(s1.state==='RETURNING'&&!ship.captured){ctx.save();ctx.fillStyle='#ff0';ctx.shadowBlur=20;ctx.shadowColor='#ff0';ctx.font='bold 18px Arial';ctx.textAlign='center';var l=(ship.netActive&&ship.netP>=1)?'一子级接近网系':'一子级返回中';ctx.fillText(l,s1.x,Math.min(s1.y-70,-50));ctx.shadowBlur=0;ctx.restore()}
if(ship.captured&&ship.secP>=1){ctx.save();ctx.fillStyle='#0f0';ctx.shadowBlur=40;ctx.shadowColor='#0f0';ctx.font='bold 28px Arial';ctx.textAlign='center';ctx.fillText('长征十号乙一子级网系回收圆满成功',ship.x,-200);ctx.shadowBlur=0;ctx.restore()}
if(sepDone&&s1.state==='FLIP'&&s1.st<20){ctx.save();ctx.fillStyle='#f80';ctx.shadowBlur=15;ctx.shadowColor='#f80';ctx.font='bold 16px Arial';ctx.textAlign='center';ctx.fillText('分离完成',s1.x,s1.y-120);ctx.shadowBlur=0;ctx.restore()}
ctx.restore();uH()}
init();
loop();
</script>
</body>
</html>
代码仓库:https://download.csdn.net/download/LIU_CAN/93107758


十、参考资料
以下是与本动画项目技术栈(HTML5 Canvas、火箭物理模拟)相关的权威或实用参考资料:
1. MDN Canvas API 文档
- 链接 :MDN Canvas Tutorial
- 说明:Mozilla Developer Network 提供的 Canvas API 完整教程,涵盖基础绘图、动画、图像处理等核心概念,是学习 HTML5 Canvas 的权威参考资料。
2. 《HTML5 Canvas 核心技术》
- 作者:David Geary
- 链接 :HTML5 Canvas: Native Interactivity and Animation for the Web
- 说明:O'Reilly 出版的经典 Canvas 书籍,深入讲解 Canvas 2D 绘图、动画、游戏开发等高级技术,适合希望深入掌握 Canvas 的开发者。
3. NASA 火箭飞行力学基础
- 链接 :NASA Rocket Flight Mechanics
- 说明:NASA 教育网站提供的火箭飞行力学基础知识,包括推力、重力、加速度等物理概念,为火箭动画的物理模拟提供理论依据。
4. GitHub - matter.js 2D 物理引擎
- 链接 :matter.js GitHub Repository
- 说明:轻量级 2D 物理引擎,可用于更复杂的物理模拟。虽然本项目使用简化物理模型,但 matter.js 展示了如何实现更真实的刚体物理、碰撞检测等。
5. 《游戏编程模式》- 游戏循环章节
- 作者:Robert Nystrom
- 链接 :Game Programming Patterns - Game Loop
- 说明 :详细讲解游戏循环(Game Loop)设计模式,对于理解
requestAnimationFrame驱动的动画更新逻辑、时间步长处理等有重要帮助。
6. W3C Canvas 规范
- 链接 :W3C Canvas 2D Context Specification
- 说明:Canvas 2D 上下文的官方规范文档,包含所有 API 的详细定义和行为说明,是开发 Canvas 应用时的权威参考。
这些参考资料涵盖了从 Canvas 基础到高级动画、从简化物理到真实火箭力学的多个层面,可为项目的进一步开发和优化提供技术指导。