分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>行星轮越障自平衡系统 — IFR原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#060a12;--surface:#0b1220;--border:#152030;
  --cyan:#00e5ff;--cyan-mid:#0097a7;--cyan-dim:#004d57;
  --amber:#ff9100;--amber-mid:#c56200;--amber-dim:#5c2d00;
  --green:#00e676;--green-dim:#005e30;
  --steel:#4a5c6d;--steel-lt:#7a909e;--steel-dk:#2a3844;
  --txt:#8a9baa;--txt-br:#cdd8e0;
}
body{
  background:var(--bg);color:var(--txt);
  font-family:'Share Tech Mono',monospace;
  min-height:100vh;display:flex;flex-direction:column;align-items:center;
  overflow-x:hidden;padding:16px 12px;
  background-image:
    radial-gradient(ellipse 800px 400px at 50% 60%,rgba(0,229,255,.03),transparent),
    radial-gradient(ellipse 600px 300px at 30% 70%,rgba(255,145,0,.02),transparent);
}
.container{width:100%;max-width:1280px;display:flex;flex-direction:column;align-items:center;gap:14px}
.title-bar{text-align:center;padding:6px 0}
.title-bar h1{
  font-family:'Rajdhani',sans-serif;font-weight:700;font-size:26px;
  color:var(--txt-br);letter-spacing:3px;
}
.title-bar .sub{font-size:12px;color:var(--cyan);margin-top:2px;letter-spacing:1.5px;opacity:.85}
.svg-wrap{
  width:100%;max-width:1200px;aspect-ratio:12/7;position:relative;
  border:1px solid var(--border);border-radius:10px;overflow:hidden;
  background:var(--surface);
  box-shadow:0 0 60px rgba(0,229,255,.04),inset 0 0 80px rgba(0,0,0,.3);
}
.svg-wrap svg{width:100%;height:100%;display:block}
.controls{
  display:flex;align-items:center;gap:20px;padding:10px 22px;
  background:var(--surface);border:1px solid var(--border);border-radius:8px;
  flex-wrap:wrap;justify-content:center;
}
.cg{display:flex;align-items:center;gap:6px}
.cg label{font-size:11px;color:var(--txt);white-space:nowrap}
.cg input[type=range]{width:130px;accent-color:var(--cyan);height:4px}
.cg .val{font-size:11px;color:var(--cyan);min-width:42px;text-align:right}
button{
  font-family:'Share Tech Mono',monospace;font-size:11px;
  padding:5px 14px;border:1px solid var(--cyan-dim);background:transparent;
  color:var(--cyan);border-radius:4px;cursor:pointer;transition:all .2s;
}
button:hover{background:rgba(0,229,255,.1);border-color:var(--cyan)}
button.active{background:rgba(0,229,255,.15);border-color:var(--cyan)}
.data-panel{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
.di{
  padding:7px 14px;background:var(--surface);border:1px solid var(--border);
  border-radius:6px;text-align:center;min-width:115px;
}
.di .lb{font-size:9px;color:var(--txt);margin-bottom:2px;letter-spacing:.5px}
.di .vl{font-size:17px;font-family:'Rajdhani',sans-serif;font-weight:700}
.di.ch .vl{color:var(--steel-lt)}.di.gm .vl{color:var(--amber)}
.di.cg2 .vl{color:var(--green)}.di.ph .vl{color:var(--cyan);font-size:13px}
.legend{
  display:flex;gap:16px;flex-wrap:wrap;justify-content:center;font-size:10px;
}
.legend span{display:flex;align-items:center;gap:5px}
.legend .dot{width:10px;height:10px;border-radius:50%;display:inline-block}
</style>
</head>
<body>
<div class="container">
  <div class="title-bar">
    <h1>行星轮越障 + 主动自平衡云台</h1>
    <div class="sub">IFR 最终理想解 · 姿态解耦原理动画</div>
  </div>
  <div class="svg-wrap" id="svgWrap"></div>
  <div class="controls">
    <div class="cg">
      <label>台阶高度</label>
      <input type="range" id="stepSlider" min="60" max="160" value="130">
      <span class="val" id="stepVal">130mm</span>
    </div>
    <div class="cg">
      <label>播放速度</label>
      <input type="range" id="speedSlider" min="25" max="200" value="100">
      <span class="val" id="speedVal">1.0x</span>
    </div>
    <button id="btnPlay">暂停</button>
    <button id="btnReset">重置</button>
  </div>
  <div class="data-panel">
    <div class="di ch"><div class="lb">底盘倾角</div><div class="vl" id="dChassis">0.0°</div></div>
    <div class="di gm"><div class="lb">云台补偿</div><div class="vl" id="dGimbal">0.0°</div></div>
    <div class="di cg2"><div class="lb">货物倾角</div><div class="vl" id="dCargo">0.0°</div></div>
    <div class="di ph"><div class="lb">当前阶段</div><div class="vl" id="dPhase">平地行驶</div></div>
  </div>
  <div class="legend">
    <span><span class="dot" style="background:var(--cyan)"></span>行星轮系 — 几何越障</span>
    <span><span class="dot" style="background:var(--amber)"></span>液压云台 — 姿态隔离</span>
    <span><span class="dot" style="background:var(--green)"></span>货物平台 — 始终水平</span>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  /* ====== 常量 ====== */
  const NS = 'http://www.w3.org/2000/svg';
  const GY = 565;          // 地面Y
  const SX = 600;          // 台阶边缘X
  let SH = 130;            // 台阶高度
  const AR = 68;           // 行星臂长
  const WR = 11;           // 小轮半径
  const CW = 250;          // 底盘宽
  const CH = 26;           // 底盘高
  const WO = 105;          // 轮毂偏移
  const GH = 42;           // 云台高度
  const PW = 190;          // 平台宽
  const PH = 12;           // 平台高
  const BXW = 130;         // 货箱宽
  const BXH = 58;          // 货箱高
  const DUR = 9000;        // 动画周期(ms)

  /* ====== 颜色 ====== */
  const C = {
    cyan:'#00e5ff', cyanM:'#0097a7', cyanD:'#004d57',
    amber:'#ff9100', amberM:'#c56200', amberD:'#5c2d00',
    green:'#00e676', greenD:'#005e30',
    steel:'#4a5c6d', steelL:'#7a909e', steelD:'#2a3844',
  };

  /* ====== SVG辅助 ====== */
  function el(tag, attrs, parent) {
    const e = document.createElementNS(NS, tag);
    if (attrs) Object.entries(attrs).forEach(([k,v]) => e.setAttribute(k,v));
    if (parent) parent.appendChild(e);
    return e;
  }
  function grp(parent, id) { return el('g', id ? {id} : null, parent); }

  /* ====== 创建SVG ====== */
  const svg = el('svg', {viewBox:'0 0 1200 700', xmlns:NS});
  document.getElementById('svgWrap').appendChild(svg);

  /* -- 定义 -- */
  const defs = el('defs', null, svg);

  // 发光滤镜
  function makeGlow(id, color, dev) {
    const f = el('filter', {id, x:'-50%',y:'-50%',width:'200%',height:'200%'}, defs);
    el('feGaussianBlur', {in:'SourceGraphic', stdDeviation:String(dev), result:'b'}, f);
    const fm = el('feMerge', null, f);
    el('feMergeNode', {in:'b'}, fm);
    el('feMergeNode', {in:'SourceGraphic'}, fm);
    return f;
  }
  makeGlow('gCyan', C.cyan, 5);
  makeGlow('gAmber', C.amber, 4);
  makeGlow('gGreen', C.green, 4);
  makeGlow('gWhite', '#ffffff', 3);

  // 地面渐变
  const gGround = el('linearGradient', {id:'gGround', x1:'0',y1:'0',x2:'0',y2:'1'}, defs);
  el('stop', {offset:'0%','stop-color':'#1a2a3a'}, gGround);
  el('stop', {offset:'100%','stop-color':'#0d1520'}, gGround);

  // 台阶渐变
  const gStep = el('linearGradient', {id:'gStep', x1:'0',y1:'0',x2:'0',y2:'1'}, defs);
  el('stop', {offset:'0%','stop-color':'#1e2d40'}, gStep);
  el('stop', {offset:'100%','stop-color':'#121e2c'}, gStep);

  // 底盘渐变
  const gChassis = el('linearGradient', {id:'gCh', x1:'0',y1:'0',x2:'0',y2:'1'}, defs);
  el('stop', {offset:'0%','stop-color':'#5a6e7e'}, gChassis);
  el('stop', {offset:'100%','stop-color':'#3a4c5a'}, gChassis);

  /* -- 静态场景 -- */
  // 网格
  const gridG = grp(svg, 'grid');
  for (let x = 0; x <= 1200; x += 50) {
    el('line', {x1:x,y1:0,x2:x,y2:700,stroke:'#0d1a28','stroke-width':'0.5'}, gridG);
  }
  for (let y = 0; y <= 700; y += 50) {
    el('line', {x1:0,y1:y,x2:1200,y2:y,stroke:'#0d1a28','stroke-width':'0.5'}, gridG);
  }

  // 地面
  const groundG = grp(svg, 'ground');
  el('rect', {x:0,y:GY,width:1200,height:135,fill:'url(#gGround)'}, groundG);
  el('line', {x1:0,y1:GY,x2:1200,y2:GY,stroke:'#2a3a4a','stroke-width':'1.5'}, groundG);

  // 台阶(动态更新)
  const stepG = grp(svg, 'step');
  let stepRect, stepEdgeLine, stepTopLine;
  function drawStep() {
    stepG.innerHTML = '';
    const topY = GY - SH;
    stepRect = el('rect', {x:SX,y:topY,width:600,height:SH,fill:'url(#gStep)'}, stepG);
    stepEdgeLine = el('line', {x1:SX,y1:GY,x2:SX,y2:topY,stroke:C.cyanD,'stroke-width':'2'}, stepG);
    stepTopLine = el('line', {x1:SX,y1:topY,x2:1200,y2:topY,stroke:'#2a3a4a','stroke-width':'1.5'}, stepG);
    // 台阶边缘高亮点
    el('circle', {cx:SX,cy:topY,r:3,fill:C.cyan,opacity:'0.7',filter:'url(#gCyan)'}, stepG);
  }
  drawStep();

  /* -- 车辆组件 -- */
  const vehG = grp(svg, 'vehicle');

  // 底盘
  const chassisG = grp(vehG, 'chassis');
  const chassisRect = el('rect', {x:-CW/2,y:-CH/2,width:CW,height:CH,rx:4,fill:'url(#gCh)',stroke:C.steelL,'stroke-width':'1'}, chassisG);
  // 底盘细节线条
  el('line', {x1:-CW/2+8,y1:0,x2:CW/2-8,y2:0,stroke:C.steelD,'stroke-width':'0.8','stroke-dasharray':'4,3'}, chassisG);
  // IMU标识
  const imuCircle = el('rect', {x:-12,y:-8,width:24,height:16,rx:2,fill:'none',stroke:C.amberM,'stroke-width':'1','stroke-dasharray':'2,2'}, chassisG);
  const imuText = el('text', {x:0,y:3,'text-anchor':'middle',fill:C.amberM,'font-size':'7','font-family':'Share Tech Mono'}, chassisG);
  imuText.textContent = 'IMU';

  // 前行星轮
  const frontWG = grp(chassisG, 'frontWheel');
  // 后行星轮
  const rearWG = grp(chassisG, 'rearWheel');

  function createPlanetaryWheel(parent) {
    const g = grp(parent);
    // 轨迹圆(虚线)
    el('circle', {cx:0,cy:0,r:AR,fill:'none',stroke:C.cyanD,'stroke-width':'0.8','stroke-dasharray':'3,5',opacity:'0.5'}, g);
    // 三角框架
    const frame = el('polygon', {points:'',fill:'none',stroke:C.cyanM,'stroke-width':'2',opacity:'0.6'}, g);
    // 三条臂
    const arms = [];
    for (let i = 0; i < 3; i++) {
      arms.push(el('line', {x1:0,y1:0,x2:0,y2:0,stroke:C.cyan,'stroke-width':'2.5',filter:'url(#gCyan)'}, g));
    }
    // 三个小轮
    const wheels = [];
    for (let i = 0; i < 3; i++) {
      wheels.push(el('circle', {cx:0,cy:0,r:WR,fill:C.steelD,stroke:C.cyanM,'stroke-width':'1.5'}, g));
      // 轮辐
      wheels.push(el('line', {x1:0,y1:0,x2:0,y2:0,stroke:C.cyanD,'stroke-width':'0.8'}, g));
      wheels.push(el('line', {x1:0,y1:0,x2:0,y2:0,stroke:C.cyanD,'stroke-width':'0.8'}, g));
    }
    // 轮毂
    el('circle', {cx:0,cy:0,r:5,fill:C.cyanD,stroke:C.cyan,'stroke-width':'1.5'}, g);
    return {g, frame, arms, wheels};
  }

  const fWheel = createPlanetaryWheel(frontWG);
  const rWheel = createPlanetaryWheel(rearWG);
  frontWG.setAttribute('transform', `translate(${WO},${CH/2+8})`);
  rearWG.setAttribute('transform', `translate(${-WO},${CH/2+8})`);

  // 云台
  const gimbalG = grp(vehG, 'gimbal');
  // 前液压缸
  const fPistonOuter = el('rect', {x:0,y:0,width:10,height:20,rx:2,fill:C.amberD,stroke:C.amberM,'stroke-width':'1'}, gimbalG);
  const fPistonInner = el('rect', {x:1.5,y:0,width:7,height:14,rx:1,fill:C.amber,opacity:'0.7'}, gimbalG);
  // 后液压缸
  const rPistonOuter = el('rect', {x:0,y:0,width:10,height:20,rx:2,fill:C.amberD,stroke:C.amberM,'stroke-width':'1'}, gimbalG);
  const rPistonInner = el('rect', {x:1.5,y:0,width:7,height:14,rx:1,fill:C.amber,opacity:'0.7'}, gimbalG);
  // 连接基座
  const gimbalBase = el('rect', {x:-55,y:0,width:110,height:6,rx:2,fill:C.amberD,stroke:C.amberM,'stroke-width':'0.8'}, gimbalG);

  // 货物平台
  const cargoG = grp(vehG, 'cargo');
  const platformRect = el('rect', {x:-PW/2,y:0,width:PW,height:PH,rx:2,fill:C.greenD,stroke:C.green,'stroke-width':'1.5',filter:'url(#gGreen)'}, cargoG);
  // 货箱
  const boxRect = el('rect', {x:-BXW/2,y:-BXH,width:BXW,height:BXH,rx:3,fill:'#1a3a2a',stroke:C.green,'stroke-width':'1',opacity:'0.9'}, cargoG);
  // 货箱内的"货物"标识
  const cargoLabel = el('text', {x:0,y:-BXH/2+4,'text-anchor':'middle',fill:C.green,'font-size':'11','font-family':'Rajdhani','font-weight':'600',opacity:'0.8'}, cargoG);
  cargoLabel.textContent = 'CARGO';
  // 水平指示器(气泡水平仪)
  const levelG = grp(cargoG);
  el('rect', {x:-20,y:-BXH-12,width:40,height:8,rx:4,fill:'#0a1a14',stroke:C.green,'stroke-width':'0.8'}, levelG);
  const bubble = el('circle', {cx:0,cy:-BXH-8,r:3,fill:C.green,filter:'url(#gGreen)'}, levelG);

  // 参考水平线
  const refLineG = grp(svg, 'refLine');
  const refLine = el('line', {x1:0,y1:0,x2:0,y2:0,stroke:C.green,'stroke-width':'0.8','stroke-dasharray':'6,4',opacity:'0'}, refLineG);

  // 接触点高亮
  const contactG = grp(svg, 'contact');
  const contactDot = el('circle', {cx:0,cy:0,r:0,fill:C.cyan,filter:'url(#gCyan)',opacity:'0'}, contactG);

  // 轨迹弧线
  const trailG = grp(svg, 'trail');
  const trailArc = el('path', {d:'',fill:'none',stroke:C.cyan,'stroke-width':'1.5','stroke-dasharray':'4,3',opacity:'0',filter:'url(#gCyan)'}, trailG);

  // 标注
  const annoG = grp(svg, 'annotations');
  const annos = [];
  function makeAnno(text, color) {
    const g = grp(annoG);
    el('rect', {x:-80,y:-12,width:160,height:24,rx:4,fill:'rgba(6,10,18,0.85)',stroke:color,'stroke-width':'1'}, g);
    const t = el('text', {x:0,y:4,'text-anchor':'middle',fill:color,'font-size':'11','font-family':'Rajdhani','font-weight':'600'}, g);
    t.textContent = text;
    g.style.opacity = '0';
    g.style.transition = 'opacity 0.4s';
    return g;
  }
  const annoWheel = makeAnno('行星轮系 · 几何越障', C.cyan);
  const annoGimbal = makeAnno('液压云台 · 姿态隔离', C.amber);
  const annoCargo = makeAnno('货物始终水平 · IFR达成', C.green);

  // 阶段文字
  const phaseG = grp(svg, 'phaseLabel');
  const phaseBg = el('rect', {x:20,y:650,width:200,height:28,rx:4,fill:'rgba(6,10,18,0.8)',stroke:C.cyanD,'stroke-width':'1'}, phaseG);
  const phaseText = el('text', {x:30,y:668,fill:C.cyan,'font-size':'12','font-family':'Share Tech Mono'}, phaseG);

  // 进度条
  const progG = grp(svg, 'progress');
  el('rect', {x:20,y:685,width:1160,height:4,rx:2,fill:'#0d1a28'}, progG);
  const progBar = el('rect', {x:20,y:685,width:0,height:4,rx:2,fill:C.cyan,opacity:'0.6'}, progG);

  /* ====== 关键帧 ====== */
  function genKeyframes(sh) {
    const maxA = Math.atan2(sh, 2*WO) * 180 / Math.PI;
    return [
      {t:0.00, x:100, yO:0,   ca:0,        fa:0,   ra:0,   ph:'平地行驶'},
      {t:0.18, x:350, yO:0,   ca:0,        fa:0,   ra:0,   ph:'平地行驶'},
      {t:0.25, x:430, yO:0,   ca:0,        fa:0,   ra:0,   ph:'撞击台阶'},
      {t:0.32, x:465, yO:sh*.22, ca:maxA*.35, fa:45,  ra:0,   ph:'前轮翻转'},
      {t:0.40, x:505, yO:sh*.50, ca:maxA*.65, fa:90,  ra:0,   ph:'前轮翻转'},
      {t:0.48, x:540, yO:sh*.75, ca:maxA*.85, fa:120, ra:0,   ph:'前轮就位'},
      {t:0.54, x:565, yO:sh*.88, ca:maxA*.70, fa:120, ra:25,  ph:'后轮撞击'},
      {t:0.62, x:590, yO:sh*.93, ca:maxA*.45, fa:120, ra:70,  ph:'后轮翻转'},
      {t:0.70, x:618, yO:sh*.97, ca:maxA*.18, fa:120, ra:110, ph:'后轮就位'},
      {t:0.80, x:660, yO:sh,    ca:0,        fa:120, ra:120, ph:'姿态回正'},
      {t:1.00, x:1000,yO:sh,    ca:0,        fa:120, ra:120, ph:'IFR达成'},
    ];
  }
  let KF = genKeyframes(SH);

  /* ====== 插值 ====== */
  function ease(t) { return t<.5?2*t*t:-1+(4-2*t)*t; }

  function lerpState(t) {
    t = Math.max(0, Math.min(1, t));
    let i = 0;
    for (let j = 0; j < KF.length-1; j++) {
      if (t >= KF[j].t && t <= KF[j+1].t) { i = j; break; }
    }
    if (t >= KF[KF.length-1].t) i = KF.length-2;
    const a = KF[i], b = KF[i+1];
    const lt = (t - a.t) / (b.t - a.t);
    const e = ease(lt);
    return {
      x: a.x + (b.x - a.x)*e,
      yO: a.yO + (b.yO - a.yO)*e,
      ca: a.ca + (b.ca - a.ca)*e,
      fa: a.fa + (b.fa - a.fa)*e,
      ra: a.ra + (b.ra - a.ra)*e,
      ph: lt < 0.5 ? a.ph : b.ph,
    };
  }

  /* ====== 绘制行星轮 ====== */
  function drawWheel(wObj, angle, hubX, hubY, wheelSpin) {
    const angles = [];
    for (let i = 0; i < 3; i++) {
      angles.push((-90 + i*120 + angle) * Math.PI / 180);
    }
    const pts = angles.map(a => ({
      x: hubX + AR * Math.cos(a),
      y: hubY + AR * Math.sin(a),
    }));

    // 更新臂
    for (let i = 0; i < 3; i++) {
      wObj.arms[i].setAttribute('x1', hubX);
      wObj.arms[i].setAttribute('y1', hubY);
      wObj.arms[i].setAttribute('x2', pts[i].x);
      wObj.arms[i].setAttribute('y2', pts[i].y);
    }

    // 更新三角框
    wObj.frame.setAttribute('points', pts.map(p=>`${p.x},${p.y}`).join(' '));

    // 更新小轮
    for (let i = 0; i < 3; i++) {
      const ci = i * 3;
      wObj.wheels[ci].setAttribute('cx', pts[i].x);
      wObj.wheels[ci].setAttribute('cy', pts[i].y);
      // 轮辐
      const sa = wheelSpin * Math.PI / 180;
      wObj.wheels[ci+1].setAttribute('x1', pts[i].x);
      wObj.wheels[ci+1].setAttribute('y1', pts[i].y);
      wObj.wheels[ci+1].setAttribute('x2', pts[i].x + WR*0.7*Math.cos(sa+i*2.1));
      wObj.wheels[ci+1].setAttribute('y2', pts[i].y + WR*0.7*Math.sin(sa+i*2.1));
      wObj.wheels[ci+2].setAttribute('x1', pts[i].x);
      wObj.wheels[ci+2].setAttribute('y1', pts[i].y);
      wObj.wheels[ci+2].setAttribute('x2', pts[i].x + WR*0.7*Math.cos(sa+i*2.1+Math.PI));
      wObj.wheels[ci+2].setAttribute('y2', pts[i].y + WR*0.7*Math.sin(sa+i*2.1+Math.PI));
    }
  }

  /* ====== 绘制云台 ====== */
  function drawGimbal(cx, cy, chassisAng, gimbalAng) {
    const rad = chassisAng * Math.PI / 180;
    // 底盘顶部中心(世界坐标)
    const baseX = cx + (-CH/2 - 2) * Math.sin(rad);
    const baseY = cy + (-CH/2 - 2) * (-Math.cos(rad));

    // 基座
    gimbalBase.setAttribute('x', baseX - 55);
    gimbalBase.setAttribute('y', baseY - 3);
    gimbalBase.setAttribute('transform', `rotate(${-chassisAng},${baseX},${baseY})`);

    // 前液压缸(从基座前端到平台前端)
    const fBaseX = baseX + 40 * Math.cos(rad);
    const fBaseY = baseY - 40 * Math.sin(rad);
    const fTopX = cx + 40;
    const fTopY = cy - CH/2 - GH;
    const fH = Math.sqrt((fTopX-fBaseX)**2 + (fTopY-fBaseY)**2);
    const fAng = Math.atan2(fTopY-fBaseY, fTopX-fBaseX) * 180/Math.PI;

    fPistonOuter.setAttribute('x', fBaseX - 5);
    fPistonOuter.setAttribute('y', fBaseY);
    fPistonOuter.setAttribute('height', Math.max(8, fH));
    fPistonOuter.setAttribute('transform', `rotate(${-fAng+90},${fBaseX},${fBaseY})`);

    const fIH = Math.max(4, fH * 0.65);
    fPistonInner.setAttribute('x', fBaseX - 3.5);
    fPistonInner.setAttribute('y', fBaseY);
    fPistonInner.setAttribute('height', fIH);
    fPistonInner.setAttribute('transform', `rotate(${-fAng+90},${fBaseX},${fBaseY})`);

    // 后液压缸
    const rBaseX = baseX - 40 * Math.cos(rad);
    const rBaseY = baseY + 40 * Math.sin(rad);
    const rTopX = cx - 40;
    const rTopY = cy - CH/2 - GH;
    const rH = Math.sqrt((rTopX-rBaseX)**2 + (rTopY-rBaseY)**2);
    const rAng = Math.atan2(rTopY-rBaseY, rTopX-rBaseX) * 180/Math.PI;

    rPistonOuter.setAttribute('x', rBaseX - 5);
    rPistonOuter.setAttribute('y', rBaseY);
    rPistonOuter.setAttribute('height', Math.max(8, rH));
    rPistonOuter.setAttribute('transform', `rotate(${-rAng+90},${rBaseX},${rBaseY})`);

    const rIH = Math.max(4, rH * 0.65);
    rPistonInner.setAttribute('x', rBaseX - 3.5);
    rPistonInner.setAttribute('y', rBaseY);
    rPistonInner.setAttribute('height', rIH);
    rPistonInner.setAttribute('transform', `rotate(${-rAng+90},${rBaseX},${rBaseY})`);
  }

  /* ====== 更新画面 ====== */
  function updateScene(t) {
    const s = lerpState(t);

    // 车辆世界坐标
    const vx = s.x;
    const vy = GY - AR - WR - 8 - CH/2 - s.yO;

    // 底盘
    chassisG.setAttribute('transform', `translate(${vx},${vy}) rotate(${-s.ca})`);

    // 轮毂世界位置
    const rad = -s.ca * Math.PI / 180;
    const fHubX = vx + WO * Math.cos(rad) - (CH/2+8) * Math.sin(rad);
    const fHubY = vy + WO * Math.sin(rad) + (CH/2+8) * Math.cos(rad);  // wait, this needs re-thinking

    // Actually, the wheel groups are children of chassisG, so they rotate with it.
    // I need to compute their world positions for the trail/contact effects.

    // 前轮毂(局部坐标在chassisG中为 (WO, CH/2+8))
    const fLocalX = WO, fLocalY = CH/2 + 8;
    const fWorldX = vx + fLocalX*Math.cos(rad) - fLocalY*Math.sin(rad);
    const fWorldY = vy + fLocalX*Math.sin(rad) + fLocalY*Math.cos(rad);

    const rLocalX = -WO, rLocalY = CH/2 + 8;
    const rWorldX = vx + rLocalX*Math.cos(rad) - rLocalY*Math.sin(rad);
    const rWorldY = vy + rLocalX*Math.sin(rad) + rLocalY*Math.cos(rad);

    // 轮子旋转(视觉自转)
    const dist = s.x - 100;
    const wheelSpin = dist / WR * (180/Math.PI) * 0.3;

    // 绘制行星轮
    drawWheel(fWheel, s.fa, 0, 0, wheelSpin);
    drawWheel(rWheel, s.ra, 0, 0, wheelSpin);

    // 云台和货物平台
    const platY = vy - CH/2 - GH;
    const platX = vx;

    drawGimbal(vx, vy, s.ca, -s.ca);

    // 货物平台(始终水平)
    cargoG.setAttribute('transform', `translate(${platX},${platY})`);
    // 气泡位置(始终居中,因为平台水平)
    bubble.setAttribute('cx', 0);

    // 水平参考线
    if (Math.abs(s.ca) > 1) {
      refLine.setAttribute('x1', vx - 160);
      refLine.setAttribute('y1', platY);
      refLine.setAttribute('x2', vx + 160);
      refLine.setAttribute('y2', platY);
      refLine.setAttribute('opacity', '0.4');
    } else {
      refLine.setAttribute('opacity', '0');
    }

    // 接触点高亮
    const isFlipping = (t > 0.25 && t < 0.50) || (t > 0.54 && t < 0.72);
    if (isFlipping) {
      contactDot.setAttribute('cx', SX);
      contactDot.setAttribute('cy', GY - SH);
      contactDot.setAttribute('r', '6');
      contactDot.setAttribute('opacity', String(0.6 + 0.4*Math.sin(Date.now()*0.008)));
    } else {
      contactDot.setAttribute('opacity', '0');
    }

    // 翻转轨迹弧
    if (t > 0.25 && t < 0.52) {
      const arcCx = fWorldX, arcCy = fWorldY;
      const a1 = (-90) * Math.PI/180;
      const a2 = (-90 + s.fa) * Math.PI/180;
      const x1 = arcCx + AR*Math.cos(a1), y1 = arcCy + AR*Math.sin(a1);
      const x2 = arcCx + AR*Math.cos(a2), y2 = arcCy + AR*Math.sin(a2);
      const largeArc = s.fa > 180 ? 1 : 0;
      trailArc.setAttribute('d', `M${x1},${y1} A${AR},${AR} 0 ${largeArc},1 ${x2},${y2}`);
      trailArc.setAttribute('opacity', '0.5');
    } else if (t > 0.54 && t < 0.74) {
      const arcCx = rWorldX, arcCy = rWorldY;
      const a1 = (-90) * Math.PI/180;
      const a2 = (-90 + s.ra) * Math.PI/180;
      const x1 = arcCx + AR*Math.cos(a1), y1 = arcCy + AR*Math.sin(a1);
      const x2 = arcCx + AR*Math.cos(a2), y2 = arcCy + AR*Math.sin(a2);
      trailArc.setAttribute('d', `M${x1},${y1} A${AR},${AR} 0 0,1 ${x2},${y2}`);
      trailArc.setAttribute('opacity', '0.5');
    } else {
      trailArc.setAttribute('opacity', '0');
    }

    // 标注
    const showWheelAnno = (t > 0.28 && t < 0.55);
    const showGimbalAnno = (t > 0.32 && t < 0.78);
    const showCargoAnno = (t > 0.80);

    annoWheel.style.opacity = showWheelAnno ? '1' : '0';
    annoWheel.setAttribute('transform', `translate(${vx + 30},${fWorldY - AR - 30})`);

    annoGimbal.style.opacity = showGimbalAnno ? '1' : '0';
    annoGimbal.setAttribute('transform', `translate(${vx - 10},${vy - CH/2 - GH/2 - 25})`);

    annoCargo.style.opacity = showCargoAnno ? '1' : '0';
    annoCargo.setAttribute('transform', `translate(${vx},${platY - BXH - 30})`);

    // 阶段文字
    phaseText.textContent = s.ph;
    progBar.setAttribute('width', t * 1160);

    // 数据面板
    document.getElementById('dChassis').textContent = s.ca.toFixed(1) + '°';
    document.getElementById('dGimbal').textContent = (-s.ca).toFixed(1) + '°';
    document.getElementById('dCargo').textContent = '0.0°';
    document.getElementById('dPhase').textContent = s.ph;
  }

  /* ====== 动画循环 ====== */
  let animT = 0;
  let speed = 1;
  let playing = true;
  let lastTS = 0;

  function loop(ts) {
    if (!lastTS) lastTS = ts;
    const dt = ts - lastTS;
    lastTS = ts;

    if (playing) {
      animT += (dt / DUR) * speed;
      if (animT > 1.12) animT = 0;
    }

    updateScene(Math.min(animT, 1.0));
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);

  /* ====== 交互控制 ====== */
  const stepSlider = document.getElementById('stepSlider');
  const speedSlider = document.getElementById('speedSlider');
  const btnPlay = document.getElementById('btnPlay');
  const btnReset = document.getElementById('btnReset');

  stepSlider.addEventListener('input', () => {
    SH = parseInt(stepSlider.value);
    document.getElementById('stepVal').textContent = SH + 'mm';
    KF = genKeyframes(SH);
    drawStep();
  });

  speedSlider.addEventListener('input', () => {
    speed = parseInt(speedSlider.value) / 100;
    document.getElementById('speedVal').textContent = speed.toFixed(1) + 'x';
  });

  btnPlay.addEventListener('click', () => {
    playing = !playing;
    btnPlay.textContent = playing ? '暂停' : '播放';
    btnPlay.classList.toggle('active', !playing);
  });

  btnReset.addEventListener('click', () => {
    animT = 0;
    lastTS = 0;
    playing = true;
    btnPlay.textContent = '暂停';
    btnPlay.classList.remove('active');
  });
});
</script>
</body>
</html>

这个动画实现了以下核心内容:

IFR 原理展示

  • 直接呈现"最终理想解"状态:行星轮系在底层暴力越障,自平衡云台在上层隔离姿态,货物始终水平——无需前后对比,一目了然。
  • 突出"资源利用":台阶边缘本身成为行星支架翻转的支点(接触点高亮闪烁),IMU 陀螺仪直接利用底盘倾斜信号驱动云台反向补偿,系统复杂度极低。

视觉引导

  • 青色(Cyan):行星轮系的臂、框架、轨迹弧线——聚焦"几何越障"创新点
  • 琥珀色(Amber):液压缸活塞——聚焦"姿态隔离"创新点
  • 绿色(Green):货物平台及气泡水平仪——聚焦"始终水平"的理想结果
  • 翻转时出现虚线轨迹弧、接触点脉冲发光、参考水平线对比

交互控制

  • 台阶高度滑块:调整 60–160mm,关键帧自动重算,直观体验"臂长决定越障上限"
  • 播放速度滑块:0.25x–2.0x,慢放观察翻转细节
  • 暂停/重置按钮,以及实时数据面板(底盘倾角 / 云台补偿角 / 货物倾角 / 当前阶段)

自动播放 & 重开即播

  • 页面加载后 DOMContentLoaded 自动启动 requestAnimationFrame 循环,无需任何手动触发;刷新或 iframe 重载时同样自动从头播放。
积分规则:第一轮对话扣减8分,后续每轮扣6分