【前端+Canvas+航天】长征十号乙火箭回收全流程动画

长征十号乙火箭回收全流程动画

一、前言

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系统
遥测更新

流程说明:

  1. 用户交互层:用户点击"点火发射"按钮触发整个动画流程
  2. 核心状态机:按照7个阶段顺序执行,包含条件分支判断
  3. 二子级并行流程:分离后持续加速直至入轨标记
  4. 一子级返回流程:包含滑行、翻转、返回三级制动逻辑,最终进入捕获判定
  5. 渲染系统:相机、粒子、HUD三大子系统并行处理,最终汇聚到Canvas绘制
  6. 完成状态:网系收束完成后显示庆祝特效,动画结束

该流程图清晰地展示了系统各模块之间的数据流向和依赖关系,有助于理解动画的整体架构。

三、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)

  • 三级制动逻辑:
    1. Boost-back(远距离高空):dist>300, alt>200
    2. 再入制动(中高空减速):alt<200, alt>80
    3. 着陆制动(低空最终减速):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挂钩锁定(按顺序):

  1. 25%进度 → 挂钩1
  2. 40%进度 → 挂钩2
  3. 55%进度 → 挂钩3
  4. 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 操作指南

  1. 点击 "点火发射" 按钮开始动画
  2. 使用视角按钮切换观察模式:
    • 全程:自动跟踪火箭
    • 一子级:聚焦返回级
    • 二子级:聚焦入轨级
    • 领航者号:聚焦回收平台
    • 分屏:同时显示入轨和回收
  3. 点击 "重置" 按钮重新开始

8.3 HUD信息

  • 一子级遥测:高度、速度、垂直速度、燃料、状态
  • 任务阶段:任务时间、当前阶段、各部件状态

九、开发说明

9.1 扩展建议

  1. 增加代码行数:将每个类拆分为独立文件,使用ES Module或构建工具
  2. 增加物理精度:加入更精确的大气模型、风阻计算
  3. 增加视觉细节:3D火箭模型、更精细的粒子系统
  4. 增加音效:发射音效、分离音效、成功音效

9.2 已知问题

  • 一子级弹道计算基于简化物理模型,与实际轨迹有差异
  • 网系捕获判定使用矩形区域,实际为"井"字形网
  • 二子级入轨后简化为"已入轨"状态,未模拟真实轨道

9.3 调试技巧

  • 浏览器F12打开控制台可查看实时错误
  • 调整 G(重力)、AD(阻力)等常量可改变物理表现
  • 调整 s1.sep() 中的 vxvy 可改变返回轨迹

部分代码如何下:

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 核心技术》

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 基础到高级动画、从简化物理到真实火箭力学的多个层面,可为项目的进一步开发和优化提供技术指导。

相关推荐
CappuccinoRose2 小时前
CSS、Less、Sass(含 SCSS)、Stylus
前端·css·less·sass·stylus
不听话坏10 小时前
Ignition篇(下 一) 动态执行前的事情
开发语言·前端·javascript
likeyi0711 小时前
require 和 import的区别
开发语言·前端
pany11 小时前
做 AI 友好的开源 Vue3 模板 🌈
前端·vue.js·ai编程
小二·11 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·react.js
谙忆102413 小时前
前端图片直传对象存储:OSS/S3 预签名 URL、STS 临时凭证与回调校验
前端
CHNE_TAO_EMSM14 小时前
Android studio 打开文件时自动下载源码
前端·javascript·android studio
一孤程15 小时前
Airtest自动化测试第五篇:小程序与Web测试——跨平台自动化全覆盖
前端·自动化测试·小程序·自动化·测试·airtest
IT_陈寒15 小时前
SpringBoot自动配置不是你以为的那样的智能
前端·人工智能·后端