分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh">
<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:#0d1117;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;font-family:'Segoe UI',system-ui,sans-serif;color:#c9d1d9;overflow:hidden}
#wrap{width:96vw;max-width:1440px;display:flex;flex-direction:column;align-items:center}
h1{font-size:1.15rem;margin-bottom:10px;color:#58a6ff;font-weight:400;letter-spacing:3px}
svg{width:100%;height:auto;border-radius:14px;background:linear-gradient(170deg,#161b22 0%,#0d1117 100%);box-shadow:0 6px 40px rgba(0,0,0,.5)}
.ctrl{margin-top:14px;display:flex;gap:20px;align-items:center;flex-wrap:wrap;justify-content:center}
.cg{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.04);padding:7px 14px;border-radius:8px;border:1px solid rgba(255,255,255,.06)}
.cg label{font-size:12px;color:#8b949e}
.cg .vd{font-size:12px;color:#58a6ff;font-weight:600;min-width:50px}
input[type=range]{width:140px;accent-color:#58a6ff;height:4px}
button{background:#238636;color:#fff;border:none;padding:7px 18px;border-radius:8px;cursor:pointer;font-size:12px;transition:background .2s}
button:hover{background:#2ea043}
.warn{color:#f85149;font-size:12px;font-weight:600;margin-top:6px;min-height:18px}
</style>
</head>
<body>
<div id="wrap">
<h1>行星轮越障 + 主动云台自稳定 原理演示</h1>
<svg id="scene" viewBox="0 0 1400 800">
<defs>
  <pattern id="gd" width="24" height="24" patternUnits="userSpaceOnUse">
    <rect width="24" height="24" fill="#21262d"/><circle cx="12" cy="12" r=".8" fill="#30363d"/>
  </pattern>
  <filter id="gl"><feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <filter id="gl2"><feGaussianBlur stdDeviation="10" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <linearGradient id="chassisGrad" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="#1f6feb"/><stop offset="100%" stop-color="#1158c7"/>
  </linearGradient>
  <linearGradient id="platGrad" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="#238636"/><stop offset="100%" stop-color="#196c2e"/>
  </linearGradient>
  <marker id="arrR" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
    <path d="M0,0 L8,3 L0,6" fill="#f0883e" opacity=".7"/>
  </marker>
</defs>

<!-- 背景网格 -->
<g opacity=".06"><line x1="0" y1="200" x2="1400" y2="200" stroke="#8b949e" stroke-dasharray="2,8"/>
<line x1="0" y1="400" x2="1400" y2="400" stroke="#8b949e" stroke-dasharray="2,8"/>
<line x1="0" y1="600" x2="1400" y2="600" stroke="#8b949e" stroke-dasharray="2,8"/></g>

<!-- 地面与台阶 -->
<g id="env">
  <rect x="0" y="650" width="720" height="150" fill="url(#gd)"/>
  <line x1="0" y1="650" x2="720" y2="650" stroke="#30363d" stroke-width="2"/>
  <rect id="stepBody" x="720" y="575" width="680" height="75" fill="#282e36" stroke="#30363d" stroke-width="1"/>
  <rect x="720" y="575" width="680" height="75" fill="url(#gd)" opacity=".4"/>
  <line id="stepTopLine" x1="720" y1="575" x2="1400" y2="575" stroke="#58a6ff" stroke-width="1.5" stroke-dasharray="10,6" opacity=".35"/>
  <line id="stepFace" x1="720" y1="575" x2="720" y2="650" stroke="#58a6ff" stroke-width="2.5" opacity=".6"/>
  <!-- 台阶高度标注 -->
  <g id="stepAnnot" opacity=".55">
    <line id="annL" x1="698" y1="575" x2="698" y2="650" stroke="#e3b341" stroke-width="1" stroke-dasharray="3,3"/>
    <line x1="693" y1="575" x2="703" y2="575" stroke="#e3b341" stroke-width="1"/>
    <line x1="693" y1="650" x2="703" y2="650" stroke="#e3b341" stroke-width="1"/>
    <text id="annH" x="688" y="618" fill="#e3b341" font-size="11" text-anchor="end" transform="rotate(-90,688,618)">H = 75</text>
  </g>
</g>

<!-- 轮毂轨迹弧线 -->
<path id="hubArc" d="M640,560 Q730,455 790,485" stroke="#f0883e" stroke-width="2" fill="none" stroke-dasharray="8,5" opacity="0"/>

<!-- ========= 车辆总成 ========= -->
<g id="vehicle">
  <!-- 行星轮架 -->
  <g id="wheelCarrier">
    <line class="arm" x1="0" y1="0" x2="90" y2="0" stroke="#8b949e" stroke-width="5" stroke-linecap="round"/>
    <line class="arm" x1="0" y1="0" x2="-45" y2="77.9" stroke="#8b949e" stroke-width="5" stroke-linecap="round"/>
    <line class="arm" x1="0" y1="0" x2="-45" y2="-77.9" stroke="#8b949e" stroke-width="5" stroke-linecap="round"/>
    <!-- 小轮 A (0° 方向) -->
    <g class="sw" data-ox="90" data-oy="0">
      <circle cx="90" cy="0" r="16" fill="#30363d" stroke="#8b949e" stroke-width="2"/>
      <line x1="82" y1="0" x2="98" y2="0" stroke="#484f58" stroke-width="1.5"/>
      <line x1="90" y1="-8" x2="90" y2="8" stroke="#484f58" stroke-width="1.5"/>
    </g>
    <!-- 小轮 B (120° 方向) -->
    <g class="sw" data-ox="-45" data-oy="77.9">
      <circle cx="-45" cy="77.9" r="16" fill="#30363d" stroke="#8b949e" stroke-width="2"/>
      <line x1="-53" y1="77.9" x2="-37" y2="77.9" stroke="#484f58" stroke-width="1.5"/>
      <line x1="-45" y1="69.9" x2="-45" y2="85.9" stroke="#484f58" stroke-width="1.5"/>
    </g>
    <!-- 小轮 C (240° 方向) -->
    <g class="sw" data-ox="-45" data-oy="-77.9">
      <circle cx="-45" cy="-77.9" r="16" fill="#30363d" stroke="#8b949e" stroke-width="2"/>
      <line x1="-53" y1="-77.9" x2="-37" y2="-77.9" stroke="#484f58" stroke-width="1.5"/>
      <line x1="-45" y1="-85.9" x2="-45" y2="-69.9" stroke="#484f58" stroke-width="1.5"/>
    </g>
    <!-- 轮毂 -->
    <circle r="9" fill="#f0883e" stroke="#ffb366" stroke-width="2"/>
  </g>

  <!-- 底盘 -->
  <g id="chassis">
    <rect x="-100" y="-82" width="200" height="52" rx="8" fill="url(#chassisGrad)" stroke="#58a6ff" stroke-width="1.5" opacity=".92"/>
    <line x1="-85" y1="-56" x2="85" y2="-56" stroke="#58a6ff" stroke-width=".8" opacity=".25"/>
    <!-- 云台下铰支座 -->
    <circle cx="-68" cy="-82" r="5" fill="#e3b341" stroke="#f5d67b" stroke-width="1.5"/>
    <circle cx="68" cy="-82" r="5" fill="#e3b341" stroke="#f5d67b" stroke-width="1.5"/>
    <!-- 底盘标签 -->
    <text x="0" y="-52" text-anchor="middle" fill="#c9d1d9" font-size="9" opacity=".5">底 盘</text>
  </g>

  <!-- 云台弹簧/作动器 (属于车辆坐标系, 不随底盘或平台旋转) -->
  <g id="gimbal">
    <!-- 左弹簧 -->
    <path id="springL" d="M-68,-82 L-74,-92 L-62,-102 L-74,-112 L-62,-122 L-68,-132" stroke="#e3b341" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity=".35"/>
    <!-- 右弹簧 -->
    <path id="springR" d="M68,-82 L74,-92 L62,-102 L74,-112 L62,-122 L68,-132" stroke="#e3b341" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity=".35"/>
  </g>

  <!-- 载货平台 (保持水平) -->
  <g id="platformGroup">
    <rect x="-110" y="-148" width="220" height="16" rx="4" fill="url(#platGrad)" stroke="#3fb950" stroke-width="1.5"/>
    <!-- 水平仪 -->
    <g id="levelInd" transform="translate(0,-140)">
      <rect x="-22" y="-4" width="44" height="8" rx="4" fill="none" stroke="#3fb950" stroke-width="1" opacity=".7"/>
      <circle id="bubble" cx="0" cy="0" r="3.5" fill="#3fb950" opacity=".8"/>
    </g>
  </g>

  <!-- 货物 -->
  <g id="cargoGroup">
    <rect x="-55" y="-200" width="110" height="52" rx="6" fill="#da3633" stroke="#f85149" stroke-width="1.5" opacity=".9"/>
    <text x="0" y="-180" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">易碎货物</text>
    <text x="0" y="-164" text-anchor="middle" fill="#ffcdd2" font-size="14">⚠</text>
  </g>

  <!-- 臂长标注 -->
  <g id="armAnnot" opacity="0">
    <line x1="0" y1="0" x2="90" y2="0" stroke="#f0883e" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrR)"/>
    <text x="45" y="14" text-anchor="middle" fill="#f0883e" font-size="10">R (臂长)</text>
  </g>
</g>

<!-- 翻转方向箭头 (动画时显示) -->
<g id="flipArrow" opacity="0">
  <path d="M-30,-110 A80,80 0 0,1 50,-95" stroke="#f0883e" stroke-width="2.5" fill="none" marker-end="url(#arrR)" filter="url(#gl)"/>
  <text x="20" y="-120" fill="#f0883e" font-size="11" font-weight="600">翻转跨级</text>
</g>

<!-- 云台补偿标注 (动画时显示) -->
<g id="compensateLabel" opacity="0">
  <rect x="-70" y="-168" width="140" height="22" rx="6" fill="rgba(227,179,65,.15)" stroke="#e3b341" stroke-width="1"/>
  <text x="0" y="-153" text-anchor="middle" fill="#e3b341" font-size="10" font-weight="600">⚡ 云台实时补偿</text>
</g>

<!-- 信息面板 -->
<g id="infoPanel" transform="translate(28,24)">
  <rect width="260" height="130" rx="10" fill="rgba(13,17,23,.8)" stroke="rgba(48,54,61,.6)" stroke-width="1"/>
  <text x="14" y="22" fill="#58a6ff" font-size="13" font-weight="700">参数状态</text>
  <text id="pArm" x="14" y="44" fill="#8b949e" font-size="11">行星轮臂长 R: 90 (150mm)</text>
  <text id="pStep" x="14" y="62" fill="#8b949e" font-size="11">台阶高度 H: 75</text>
  <text id="pTilt" x="14" y="80" fill="#8b949e" font-size="11">底盘倾角: 0.0°</text>
  <text id="pPlat" x="14" y="98" fill="#8b949e" font-size="11">平台偏转: 0.0°</text>
  <text id="pStatus" x="14" y="120" fill="#3fb950" font-size="11" font-weight="600">状态: 平地行驶</text>
</g>
</svg>

<div class="ctrl">
  <div class="cg">
    <label>台阶高度 H:</label>
    <input type="range" id="sliderH" min="30" max="140" value="75"/>
    <span class="vd" id="valH">75</span>
  </div>
  <div class="cg">
    <label>播放速度:</label>
    <input type="range" id="sliderSpd" min="3" max="20" value="10"/>
    <span class="vd" id="valSpd">1.0x</span>
  </div>
  <button id="btnReplay">🔄 重播</button>
</div>
<div class="warn" id="warnMsg"></div>
</div>

<script>
/* ===== 常量 ===== */
const GROUND_Y = 650, STEP_X = 720, ARM = 90, HUB_GROUND = GROUND_Y - ARM;
const DEG = Math.PI / 180;

/* ===== DOM ===== */
const $ = id => document.getElementById(id);
const sliderH = $('sliderH'), sliderSpd = $('sliderSpd');
const valH = $('valH'), valSpd = $('valSpd'), warnMsg = $('warnMsg');
let mainTL = null;

/* ===== 更新台阶视觉 ===== */
function updateStepVisual(h) {
  const topY = GROUND_Y - h;
  gsap.set('#stepBody', { attr: { y: topY, height: h } });
  gsap.set('#stepTopLine', { attr: { y1: topY, y2: topY } });
  gsap.set('#stepFace', { attr: { y1: topY } });
  gsap.set('#annL', { attr: { y1: topY } });
  gsap.set('#annH', { attr: { transform: `rotate(-90,688,${(topY + GROUND_Y) / 2})` }, text: `H = ${h}` });
  $('pStep').textContent = `台阶高度 H: ${h}`;
}

/* ===== 构建时间轴 ===== */
function buildTimeline() {
  if (mainTL) { mainTL.kill(); }
  gsap.set('#hubArc', { opacity: 0 });
  gsap.set('#flipArrow', { opacity: 0 });
  gsap.set('#compensateLabel', { opacity: 0 });
  gsap.set('#armAnnot', { opacity: 0 });

  const h = parseInt(sliderH.value);
  const topY = GROUND_Y - h;
  const hubStep = topY - ARM;
  const canCross = h <= ARM;
  const spd = parseInt(sliderSpd.value) / 10;  // 0.3 ~ 2.0

  updateStepVisual(h);
  warnMsg.textContent = canCross ? '' : `⚠ 台阶高度(${h}) > 臂长(${ARM}),越障失效!`;

  /* 代理对象用于同步底盘与平台旋转 */
  const tilt = { v: 0 };
  function applyTilt() {
    const a = tilt.v;
    gsap.set('#chassis', { rotation: a, transformOrigin: '0px 0px' });
    gsap.set('#platformGroup', { rotation: -a, transformOrigin: '0px -140px' });
    gsap.set('#cargoGroup', { rotation: -a, transformOrigin: '0px -174px' });
    /* 弹簧高亮 */
    const act = Math.min(Math.abs(a) / 18, 1);
    gsap.set('#springL', { opacity: .35 + act * .65, stroke: act > .2 ? '#f5d67b' : '#e3b341' });
    gsap.set('#springR', { opacity: .35 + act * .65, stroke: act > .2 ? '#f5d67b' : '#e3b341' });
    if (act > .2) { gsap.set('#gimbal', { filter: 'url(#gl)' }); } else { gsap.set('#gimbal', { filter: 'none' }); }
    /* 信息面板 */
    $('pTilt').textContent = `底盘倾角: ${a.toFixed(1)}°`;
    $('pPlat').textContent = `平台偏转: ${(-a).toFixed(1)}°`;
  }

  const tl = gsap.timeline({ defaults: { ease: 'power2.inOut' } });

  /* ---- 重置 ---- */
  gsap.set('#vehicle', { x: 120, y: HUB_GROUND });
  gsap.set('#wheelCarrier', { rotation: 90, transformOrigin: '0px 0px' });
  tilt.v = 0; applyTilt();
  $('pStatus').textContent = '状态: 平地行驶';
  $('pStatus').setAttribute('fill', '#3fb950');

  if (canCross) {
    /* ====== 成功越障 ====== */

    /* Phase 1 — 平地行驶 2.5s */
    tl.addLabel('roll')
      .to('#vehicle', { x: 600, duration: 2.5 / spd, ease: 'none' })
      .to('#armAnnot', { opacity: .6, duration: .6 }, 'roll+=0.3')
      .to('#armAnnot', { opacity: 0, duration: .4 }, 'roll+=1.8');

    /* Phase 2 — 撞击台阶 0.35s */
    tl.addLabel('hit')
      .to('#vehicle', { x: 635, duration: 0.35 / spd, ease: 'power2.in' })
      .to('#stepFace', { attr: { 'stroke-width': 5, opacity: 1 }, duration: 0.15 / spd }, 'hit')
      .to('#stepFace', { attr: { 'stroke-width': 2.5, opacity: .6 }, duration: 0.2 / spd });

    /* Phase 3 — 行星架翻转跨级 2.2s */
    tl.addLabel('flip')
      /* 车辆沿弧线上移 */
      .to('#vehicle', {
        keyframes: [
          { x: 720, y: Math.max(hubStep - 30, HUB_GROUND - h - 30), duration: 1.1 / spd, ease: 'power2.in' },
          { x: 790, y: hubStep, duration: 1.1 / spd, ease: 'power2.out' }
        ]
      })
      /* 轮架旋转 120° */
      .to('#wheelCarrier', { rotation: 210, duration: 2.2 / spd, transformOrigin: '0px 0px' }, 'flip')
      /* 底盘倾角: 先增后减 */
      .to(tilt, {
        v: 20, duration: 1.0 / spd, ease: 'power2.in',
        onUpdate: applyTilt
      }, 'flip')
      .to(tilt, {
        v: 0, duration: 1.2 / spd, ease: 'power2.out',
        onUpdate: applyTilt
      })
      /* 轨迹弧线淡入 */
      .to('#hubArc', { opacity: .5, duration: .6 / spd }, 'flip')
      .to('#hubArc', { opacity: 0, duration: .5 / spd }, 'flip+=1.6')
      /* 翻转箭头 */
      .to('#flipArrow', { opacity: .8, duration: .4 / spd }, 'flip+=0.1')
      .to('#flipArrow', { opacity: 0, duration: .3 / spd }, 'flip+=1.4')
      /* 云台补偿标注 */
      .to('#compensateLabel', { opacity: 1, duration: .4 / spd }, 'flip+=0.3')
      .to('#compensateLabel', { opacity: 0, duration: .3 / spd }, 'flip+=1.7')
      /* 状态文字 */
      .call(() => { $('pStatus').textContent = '状态: 翻转跨级'; $('pStatus').setAttribute('fill', '#f0883e'); }, null, 'flip')
      .call(() => { $('pStatus').textContent = '状态: 云台补偿中'; $('pStatus').setAttribute('fill', '#e3b341'); }, null, 'flip+=0.5');

    /* Phase 4 — 着陆 0.4s */
    tl.addLabel('land')
      .to('#vehicle', { x: 810, duration: 0.4 / spd, ease: 'power2.out' })
      .call(() => { $('pStatus').textContent = '状态: 完成越障'; $('pStatus').setAttribute('fill', '#3fb950'); });

    /* Phase 5 — 继续行驶 2.5s */
    tl.addLabel('depart')
      .to('#vehicle', { x: 1200, duration: 2.5 / spd, ease: 'none' });

    /* 暂停后重播 */
    tl.to({}, { duration: 1.5 / spd })
      .call(() => buildTimeline());

  } else {
    /* ====== 越障失败 ====== */
    tl.addLabel('roll')
      .to('#vehicle', { x: 600, duration: 2.5 / spd, ease: 'none' });

    tl.addLabel('hit')
      .to('#vehicle', { x: 640, duration: 0.3 / spd, ease: 'power2.in' })
      .to('#stepFace', { attr: { 'stroke-width': 5, opacity: 1 }, duration: 0.15 / spd }, 'hit');

    /* 碰撞弹回 */
    tl.addLabel('bump')
      .to('#vehicle', { x: 620, y: HUB_GROUND - 15, duration: 0.4 / spd, ease: 'power2.out' })
      .to('#wheelCarrier', { rotation: 110, duration: 0.4 / spd, transformOrigin: '0px 0px' }, 'bump')
      .to(tilt, { v: 12, duration: 0.3 / spd, ease: 'power2.out', onUpdate: applyTilt }, 'bump');

    /* 剧烈晃动 */
    tl.to('#vehicle', { x: 615, y: HUB_GROUND, duration: 0.5 / spd, ease: 'elastic.out(1,.5)' })
      .to('#wheelCarrier', { rotation: 85, duration: 0.6 / spd, ease: 'elastic.out(1,.5)', transformOrigin: '0px 0px' }, '-=0.5')
      .to(tilt, { v: -8, duration: 0.3 / spd, ease: 'power2.inOut', onUpdate: applyTilt })
      .to(tilt, { v: 5, duration: 0.3 / spd, ease: 'power2.inOut', onUpdate: applyTilt })
      .to(tilt, { v: -3, duration: 0.3 / spd, ease: 'power2.inOut', onUpdate: applyTilt })
      .to(tilt, { v: 0, duration: 0.4 / spd, ease: 'power2.out', onUpdate: applyTilt });

    tl.call(() => { $('pStatus').textContent = '状态: ❌ 越障失败 — 剧烈晃动'; $('pStatus').setAttribute('fill', '#f85149'); });

    tl.to({}, { duration: 2 / spd })
      .call(() => buildTimeline());
  }

  mainTL = tl;
  return tl;
}

/* ===== 事件绑定 ===== */
sliderH.addEventListener('input', () => {
  valH.textContent = sliderH.value;
  updateStepVisual(parseInt(sliderH.value));
  const can = parseInt(sliderH.value) <= ARM;
  warnMsg.textContent = can ? '' : `⚠ 台阶高度(${sliderH.value}) > 臂长(${ARM}),越障失效!`;
});
sliderH.addEventListener('change', () => buildTimeline());

sliderSpd.addEventListener('input', () => {
  valSpd.textContent = (parseInt(sliderSpd.value) / 10).toFixed(1) + 'x';
});
sliderSpd.addEventListener('change', () => buildTimeline());

$('btnReplay').addEventListener('click', () => buildTimeline());

/* ===== 页面加载后自动播放 ===== */
window.addEventListener('load', () => buildTimeline());
</script>
</body>
</html>

实现说明:

  1. 核心结构:SVG 以车辆组 #vehicle 为容器,内部分离出行星轮架 #wheelCarrier、底盘 #chassis、云台弹簧 #gimbal、平台 #platformGroup 和货物 #cargoGroup 五大子系统,各自拥有独立的旋转原点,互不耦合。

  2. 翻转机理:行星轮架初始旋转 90°(一轮触地),翻越台阶时旋转至 210°(+120°),下一小轮成为新的支撑点——这正是"跨步"的视觉表达。同时车辆整体沿弧线抬升,模拟轮毂绕台阶棱角翻转的真实轨迹。

  3. 云台补偿:使用代理对象 tilt 驱动底盘倾角,平台组始终施加反向等量旋转(rotation: -tilt.v),确保货物平台绝对水平。弹簧作动器在补偿激活时高亮发光,并弹出"⚡ 云台实时补偿"标注,引导用户关注核心创新点。

  4. 交互设计:台阶高度滑块可调节 H 值——当 H ≤ 臂长(90)时动画展示成功越障;当 H > 90 时动画展示碰撞弹回、剧烈晃动的失效场景,直观呈现"臂长决定最大越障高度"的边界条件。播放速度滑块可加速/减速观察细节。

  5. 时序编排:全部动作组织在单一 gsap.timeline() 中,通过标签(rollhitfliplanddepart)精确控制先后顺序与同步关系,底盘倾角与平台补偿通过 onUpdate 回调逐帧同步,零漂移。

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