分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>行星轮系越障 + 主动云台补偿 原理动画</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0c0c1a;display:flex;flex-direction:column;align-items:center;min-height:100vh;font-family:'Segoe UI',system-ui,sans-serif;color:#d0d8e0;padding:16px}
.hd{font-size:20px;font-weight:700;color:#4a9eff;margin-bottom:4px;text-align:center}
.sub{font-size:12px;color:#6a7a8a;margin-bottom:14px;text-align:center;max-width:720px;line-height:1.6}
.wrap{width:100%;max-width:1200px;position:relative}
svg{width:100%;height:auto;display:block;border-radius:10px;background:#10101f;box-shadow:0 0 40px rgba(74,158,255,.08)}
.leg{display:flex;gap:16px;margin-top:10px;flex-wrap:wrap;justify-content:center}
.leg i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:4px;vertical-align:middle}
.leg span{font-size:11px;color:#7a8a9a}
.ctrl{margin-top:12px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;justify-content:center}
.ctrl button{padding:6px 16px;border:1px solid #4a9eff;background:transparent;color:#4a9eff;border-radius:5px;cursor:pointer;font-size:12px;transition:.2s}
.ctrl button:hover{background:#4a9eff;color:#0c0c1a}
.ctrl .sg{display:flex;align-items:center;gap:5px;font-size:11px;color:#7a8a9a}
.ctrl input[type=range]{width:90px}
</style>
</head>
<body>
<div class="hd">行星轮系越障 + 主动云台补偿 · 原理动画</div>
<div class="sub">底层行星轮系刚性翻转克服台阶高差,上层主动液压云台实时反向补偿保持载货平台水平——越障与平衡解耦</div>
<div class="wrap">
<svg id="svg" viewBox="0 0 1400 650" xmlns="http://www.w3.org/2000/svg">
<defs>
  <linearGradient id="gG" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#354555"/><stop offset="100%" stop-color="#222e38"/></linearGradient>
  <radialGradient id="wG" cx="38%" cy="38%"><stop offset="0%" stop-color="#8899aa"/><stop offset="100%" stop-color="#3a4550"/></radialGradient>
  <filter id="glo" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="5" result="b"/><feFlood flood-color="#e17055" flood-opacity=".7" result="c"/><feComposite in="c" in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <filter id="gloG" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="4" result="b"/><feFlood flood-color="#00b894" flood-opacity=".45" result="c"/><feComposite in="c" in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse"><path d="M50 0L0 0 0 50" fill="none" stroke="#181830" stroke-width=".5"/></pattern>
</defs>

<!-- background grid -->
<rect width="1400" height="650" fill="url(#grid)"/>

<!-- ===== GROUND & STEP ===== -->
<g id="env">
  <rect x="0" y="500" width="730" height="150" fill="url(#gG)"/>
  <rect x="730" y="415" width="670" height="235" fill="url(#gG)"/>
  <rect x="730" y="415" width="5" height="85" fill="#5a6a7a" rx="1"/>
  <line x1="0" y1="500" x2="730" y2="500" stroke="#5a7a8a" stroke-width="2"/>
  <line x1="735" y1="415" x2="1400" y2="415" stroke="#5a7a8a" stroke-width="2"/>
  <!-- step height annotation -->
  <g opacity=".5">
    <line x1="710" y1="500" x2="710" y2="415" stroke="#4a9eff" stroke-width="1" stroke-dasharray="4,3"/>
    <line x1="705" y1="500" x2="715" y2="500" stroke="#4a9eff" stroke-width="1"/>
    <line x1="705" y1="415" x2="715" y2="415" stroke="#4a9eff" stroke-width="1"/>
    <text x="706" y="462" fill="#4a9eff" font-size="10" text-anchor="end">h=85</text>
  </g>
  <!-- surface texture -->
  <line x1="100" y1="505" x2="650" y2="505" stroke="#3a4a55" stroke-width=".5" opacity=".5"/>
  <line x1="100" y1="512" x2="600" y2="512" stroke="#3a4a55" stroke-width=".5" opacity=".4"/>
  <line x1="800" y1="420" x2="1300" y2="420" stroke="#3a4a55" stroke-width=".5" opacity=".5"/>
</g>

<!-- Trajectory arc (visible during climb) -->
<path id="trajArc" d="M627,440 Q720,370 775,355" fill="none" stroke="rgba(74,158,255,.25)" stroke-width="1.5" stroke-dasharray="6,4" opacity="0"/>

<!-- ===== VEHICLE ===== -->
<g id="vehicle">

  <!-- ── chassis_rot: rotates around (0,0)=arm center ── -->
  <g id="chassis_rot">

    <!-- chassis body -->
    <rect x="-110" y="-44" width="220" height="40" rx="6" fill="#4a6572" stroke="#6a8a97" stroke-width="1.5"/>
    <line x1="-90" y1="-32" x2="90" y2="-32" stroke="#5a7a87" stroke-width=".8"/>
    <line x1="-90" y1="-22" x2="90" y2="-22" stroke="#5a7a87" stroke-width=".5" stroke-dasharray="4,4"/>
    <text x="0" y="-10" text-anchor="middle" fill="#8aaabb" font-size="9" font-weight="600">底盘</text>

    <!-- ── arm_rot: planetary arm, rotates around (0,0) ── -->
    <g id="arm_rot">
      <!-- arm spokes -->
      <line x1="0" y1="0" x2="0" y2="-90" stroke="#7f8c8d" stroke-width="5" stroke-linecap="round"/>
      <line x1="0" y1="0" x2="78" y2="45" stroke="#7f8c8d" stroke-width="5" stroke-linecap="round"/>
      <line x1="0" y1="0" x2="-78" y2="45" stroke="#7f8c8d" stroke-width="5" stroke-linecap="round"/>
      <!-- center hub -->
      <circle cx="0" cy="0" r="10" fill="#5a6a7a" stroke="#95a5a6" stroke-width="2"/>
      <circle cx="0" cy="0" r="4" fill="#95a5a6"/>

      <!-- Wheel 0 — initially at TOP -->
      <g transform="translate(0,-90)"><g id="w0s">
        <circle cx="0" cy="0" r="15" fill="url(#wG)" stroke="#aabbcc" stroke-width="1.5"/>
        <line x1="-10" y1="0" x2="10" y2="0" stroke="#aabbcc" stroke-width="1"/>
        <line x1="0" y1="-10" x2="0" y2="10" stroke="#aabbcc" stroke-width="1"/>
        <circle cx="0" cy="0" r="3" fill="#aabbcc"/>
      </g></g>

      <!-- Wheel 1 — initially BOTTOM-RIGHT (forward) -->
      <g transform="translate(78,45)"><g id="w1s">
        <circle cx="0" cy="0" r="15" fill="url(#wG)" stroke="#aabbcc" stroke-width="1.5"/>
        <line x1="-10" y1="0" x2="10" y2="0" stroke="#aabbcc" stroke-width="1"/>
        <line x1="0" y1="-10" x2="0" y2="10" stroke="#aabbcc" stroke-width="1"/>
        <circle cx="0" cy="0" r="3" fill="#aabbcc"/>
      </g></g>

      <!-- Wheel 2 — initially BOTTOM-LEFT (rear) -->
      <g transform="translate(-78,45)"><g id="w2s">
        <circle cx="0" cy="0" r="15" fill="url(#wG)" stroke="#aabbcc" stroke-width="1.5"/>
        <line x1="-10" y1="0" x2="10" y2="0" stroke="#aabbcc" stroke-width="1"/>
        <line x1="0" y1="-10" x2="0" y2="10" stroke="#aabbcc" stroke-width="1"/>
        <circle cx="0" cy="0" r="3" fill="#aabbcc"/>
      </g></g>

      <!-- arm length annotation line -->
      <g id="armAnno" opacity="0">
        <line x1="0" y1="0" x2="0" y2="-90" stroke="#4a9eff" stroke-width="1" stroke-dasharray="3,2"/>
        <text x="10" y="-50" fill="#4a9eff" font-size="9">R=90</text>
      </g>
    </g><!-- /arm_rot -->

    <!-- gimbal housing on chassis top -->
    <g id="gimHousing">
      <rect id="gimRect" x="-26" y="-62" width="52" height="18" rx="4" fill="#e17055" stroke="#fab1a0" stroke-width="1.5"/>
      <circle id="gimDot" cx="0" cy="-53" r="3.5" fill="#fab1a0"/>
      <rect x="-20" y="-70" width="5" height="10" rx="2" fill="#c0392b" opacity=".7"/>
      <rect x="15" y="-70" width="5" height="10" rx="2" fill="#c0392b" opacity=".7"/>
    </g>

    <!-- ghost platform (no compensation — tilts WITH chassis) -->
    <g transform="translate(0,-55)">
      <g id="ghost" opacity="0">
        <rect x="-90" y="-18" width="180" height="10" rx="3" fill="none" stroke="#ff6b6b" stroke-width="1.2" stroke-dasharray="5,3"/>
        <rect x="-50" y="-72" width="100" height="54" rx="4" fill="none" stroke="#ff6b6b" stroke-width="1" stroke-dasharray="5,3"/>
        <text x="0" y="-40" text-anchor="middle" fill="#ff6b6b" font-size="8">无补偿状态</text>
      </g>
    </g>

  </g><!-- /chassis_rot -->

  <!-- ── platform_rot: counter-rotates at gimbal pivot (0,-55) ── -->
  <g transform="translate(0,-55)">
    <g id="platform_rot">
      <!-- platform -->
      <rect id="platRect" x="-90" y="-18" width="180" height="10" rx="3" fill="#00b894" stroke="#55efc4" stroke-width="1.5" filter="url(#gloG)"/>
      <!-- cargo -->
      <g id="cargo">
        <rect x="-50" y="-72" width="100" height="54" rx="5" fill="#fdcb6e" stroke="#ffeaa7" stroke-width="1.5"/>
        <text x="0" y="-48" text-anchor="middle" fill="#2d3436" font-size="12" font-weight="700">货物</text>
        <text x="0" y="-34" text-anchor="middle" fill="#d63031" font-size="9">⚠ 易碎品</text>
      </g>
      <!-- level reference line -->
      <line id="lvlLine" x1="-115" y1="-13" x2="115" y2="-13" stroke="#55efc4" stroke-width=".7" stroke-dasharray="3,3" opacity=".4"/>
    </g>
  </g>

  <!-- gimbal activation badge (moves with vehicle) -->
  <g id="gimBadge" opacity="0">
    <rect x="55" y="-120" width="88" height="22" rx="11" fill="rgba(225,112,85,.15)" stroke="#e17055" stroke-width="1"/>
    <text x="99" y="-105" text-anchor="middle" fill="#e17055" font-size="10" font-weight="600">⚡ 云台补偿</text>
  </g>

</g><!-- /vehicle -->

<!-- phase banner -->
<g id="phaseBanner">
  <rect x="510" y="16" width="380" height="34" rx="17" fill="rgba(74,158,255,.08)" stroke="rgba(74,158,255,.25)" stroke-width="1"/>
  <text id="phaseTxt" x="700" y="39" text-anchor="middle" fill="#4a9eff" font-size="13" font-weight="500">平地滚动行驶</text>
</g>

</svg>

<div class="leg">
  <span><i style="background:#4a6572"></i>底盘</span>
  <span><i style="background:#7f8c8d"></i>行星轮系支架</span>
  <span><i style="background:#e17055"></i>主动云台</span>
  <span><i style="background:#00b894"></i>载货平台</span>
  <span><i style="background:#fdcb6e"></i>货物</span>
  <span><i style="background:#ff6b6b"></i>无补偿对照</span>
</div>

<div class="ctrl">
  <button id="btnReplay">🔄 重新播放</button>
  <button id="btnPause">⏸ 暂停</button>
  <div class="sg"><label>速度:</label><input type="range" id="spdR" min="0.3" max="2.5" step="0.1" value="1"><span id="spdV">1.0x</span></div>
</div>
</div>

<script>
(function(){
/* ───── element refs ───── */
const vehicle    = document.getElementById('vehicle');
const chassisRot = document.getElementById('chassis_rot');
const armRot     = document.getElementById('arm_rot');
const platRot    = document.getElementById('platform_rot');
const w0s = document.getElementById('w0s');
const w1s = document.getElementById('w1s');
const w2s = document.getElementById('w2s');
const phaseTxt   = document.getElementById('phaseTxt');
const ghost      = document.getElementById('ghost');
const gimBadge   = document.getElementById('gimBadge');
const gimRect    = document.getElementById('gimRect');
const trajArc    = document.getElementById('trajArc');
const armAnno    = document.getElementById('armAnno');

/* ───── constants ───── */
const GND  = 500;          // lower ground y
const UGND = 415;          // upper ground y
const STEP = 85;           // step height
const SX   = 730;          // step face x
const R    = 90;           // arm radius
const WR   = 15;           // wheel radius
const INIT_Y = GND  - 45 - WR;   // 440  arm-center on lower ground
const FIN_Y  = UGND - 45 - WR;   // 355  arm-center on upper ground

/* ───── animation state ───── */
const S = {
  vx: 260, vy: INIT_Y,   // vehicle position (arm center)
  cr: 0,                   // chassis rotation
  ar: 0,                   // arm rotation
  pr: 0,                   // platform rotation
  w0: 0, w1: 0, w2: 0     // wheel spin angles
};

/* apply all transforms every tick */
function render(){
  vehicle.setAttribute('transform','translate('+S.vx+','+S.vy+')');
  chassisRot.setAttribute('transform','rotate('+S.cr+',0,0)');
  armRot.setAttribute('transform','rotate('+S.ar+',0,0)');
  platRot.setAttribute('transform','rotate('+S.pr+',0,0)');
  w0s.setAttribute('transform','rotate('+S.w0+')');
  w1s.setAttribute('transform','rotate('+S.w1+')');
  w2s.setAttribute('transform','rotate('+S.w2+')');
}
gsap.ticker.add(render);

/* helper: set phase text */
function phase(t){ phaseTxt.textContent = t; }

/* ───── master timeline ───── */
const tl = gsap.timeline({
  repeat: -1,
  repeatDelay: 1.8,
  defaults: { ease:'power1.inOut' }
});

/* reset state at timeline start (important for repeats) */
tl.to(S, { vx:260, vy:INIT_Y, cr:0, ar:0, pr:0, w0:0, w1:0, w2:0, duration:0 }, 0);

/* ── Phase 1 : 平地滚动行驶 (0 → 3s) ── */
tl.addLabel('p1', 0)
  .to(S, { vx: 620, duration: 3, ease:'none' }, 'p1')
  .to(S, { w0:700, w1:700, w2:700, duration:3, ease:'none' }, 'p1')
  .call(phase, ['平地滚动行驶'], 'p1');

/* ── Phase 2 : 撞击台阶立面 (3 → 3.6s) ── */
tl.addLabel('p2', 3)
  .to(S, { vx:640, duration:0.5, ease:'power2.out' }, 'p2')
  .to(S, { cr:5, duration:0.35, ease:'power2.out' }, 'p2')
  .to(S, { w0:'+=60', w1:'+=60', w2:'+=60', duration:0.5, ease:'power2.out' }, 'p2')
  .call(phase, ['撞击台阶立面'], 'p2');

/* ── Phase 3+4 : 行星支架翻转 + 云台补偿 (3.6 → 6.2s) ── */
tl.addLabel('p3', 3.6)

  /* arm flips 120° CW */
  .to(S, { ar:120, duration:2.2, ease:'power2.inOut' }, 'p3')

  /* vehicle rises & advances */
  .to(S, { vx:775, vy:FIN_Y, duration:2.2, ease:'power2.inOut' }, 'p3')

  /* wheels keep spinning */
  .to(S, { w0:'+=300', w1:'+=300', w2:'+=300', duration:2.2, ease:'none' }, 'p3')

  /* chassis tilts forward then settles */
  .to(S, { cr:22, duration:0.9, ease:'power2.in' }, 'p3')
  .to(S, { cr:0,  duration:1.3, ease:'power3.out' }, 'p3+=0.9')

  /* show trajectory arc */
  .to(trajArc, { opacity:1, duration:0.3 }, 'p3')
  .to(trajArc, { opacity:0, duration:0.4 }, 'p3+=1.8')

  /* show arm length annotation */
  .to(armAnno, { opacity:0.7, duration:0.3 }, 'p3+=0.1')
  .to(armAnno, { opacity:0, duration:0.3 }, 'p3+=1.6')

  /* ghost platform fades in (shows what happens WITHOUT gimbal) */
  .to(ghost, { opacity:0.55, duration:0.3 }, 'p3+=0.3')
  .to(ghost, { opacity:0, duration:0.4 }, 'p3+=1.8')

  .call(phase, ['行星支架翻转跨级'], 'p3');

/* gimbal compensation — slightly delayed to show the catch-up */
tl.addLabel('p4', 3.85)
  /* brief moment: platform still at 0 while chassis already tilting → problem visible */
  .to(S, { pr:0, duration:0.15 }, 'p4')
  /* then fast compensation */
  .to(S, { pr:-22, duration:0.35, ease:'power3.out' }, 'p4+=0.15')
  /* track chassis back to level */
  .to(S, { pr:0, duration:1.3, ease:'power3.out' }, 'p3+=0.9')

  /* gimbal visual activation */
  .to(gimBadge, { opacity:1, duration:0.2 }, 'p4+=0.15')
  .to(gimBadge, { opacity:0, duration:0.4 }, 'p3+=1.8')
  .to(gimRect, { attr:{ stroke:'#ff7675', 'stroke-width':3 }, duration:0.15 }, 'p4+=0.15')
  .to(gimRect, { attr:{ stroke:'#fab1a0', 'stroke-width':1.5 }, duration:0.5 }, 'p3+=1.4')
  .to(gimRect, { filter:'url(#glo)', duration:0.15 }, 'p4+=0.15')
  .to(gimRect, { filter:'none', duration:0.5 }, 'p3+=1.4')

  .call(phase, ['云台补偿 → 平台保持水平'], 'p4+=0.15');

/* ── Phase 5 : 完成跨级 · 平稳行驶 (6.2 → 9s) ── */
tl.addLabel('p5', 6.2)
  .to(S, { vx:1120, duration:2.8, ease:'none' }, 'p5')
  .to(S, { w0:'+=650', w1:'+=650', w2:'+=650', duration:2.8, ease:'none' }, 'p5')
  .call(phase, ['完成跨级 · 平稳行驶'], 'p5');

/* ───── controls ───── */
document.getElementById('btnReplay').addEventListener('click', function(){ tl.restart(); });
document.getElementById('btnPause').addEventListener('click', function(){
  if(tl.isActive()){ tl.pause(); this.textContent='▶ 继续'; }
  else{ tl.play();  this.textContent='⏸ 暂停'; }
});
const spdR=document.getElementById('spdR'), spdV=document.getElementById('spdV');
spdR.addEventListener('input',function(){ tl.timeScale(this.value); spdV.textContent=parseFloat(this.value).toFixed(1)+'x'; });

/* auto-play on load */
tl.play();
})();
</script>
</body>
</html>

实现说明:

  1. 层级结构严格遵循 animation_planvehiclechassis_rot(含底盘体、行星轮系、云台壳体、幽灵对照平台) → arm_rot(含支架三杆、中心轮毂、三个小轮),platform_rot 独立于 chassis_rot 作为云台上层载荷层——底盘倾斜时平台可独立反向旋转。

  2. 旋转锚点精确chassis_rotarm_rot 均以 SVG rotate(angle, 0, 0) 方式绕臂系几何中心(即底盘铰接轴心)旋转;platform_rot 绕云台连接点 (0, -55) 旋转——全部通过 gsap.ticker 每帧手动写 transform 属性,规避了 svgOrigin 在移动元素上的坐标漂移问题。

  3. 时间轴五阶段编排

    • 平地滚动:匀速平移 + 轮子自转
    • 撞击台阶:减速 + 底盘微前倾 5°
    • 支架翻转:arm 顺时针 120°,车辆抬升 85px 越过台阶,底盘前倾至 22°
    • 云台补偿:延迟 ~0.15s 后快速反旋 -22°(模拟 <20ms 响应),使平台恢复水平;同时显示红色幽灵对照平台展示"无补偿"倾斜效果
    • 完成行驶:底盘归零、云台归零,继续平地滚动
  4. 视觉引导:红色虚线幽灵平台与绿色实线稳定平台形成鲜明对比;云台激活时边框变红并添加发光滤镜;右上角出现"⚡ 云台补偿"徽标;爬阶时显示臂长标注和轨迹弧线。

  5. 交互控制:重新播放、暂停/继续、速度滑块(0.3x–2.5x),动画加载后自动播放、循环重播。

积分规则:第一轮对话扣减8分,后续每轮扣6分