独立渲染引擎就绪引擎就绪
<!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>
实现说明:
核心结构:SVG 以车辆组
#vehicle为容器,内部分离出行星轮架#wheelCarrier、底盘#chassis、云台弹簧#gimbal、平台#platformGroup和货物#cargoGroup五大子系统,各自拥有独立的旋转原点,互不耦合。翻转机理:行星轮架初始旋转 90°(一轮触地),翻越台阶时旋转至 210°(+120°),下一小轮成为新的支撑点——这正是"跨步"的视觉表达。同时车辆整体沿弧线抬升,模拟轮毂绕台阶棱角翻转的真实轨迹。
云台补偿:使用代理对象
tilt驱动底盘倾角,平台组始终施加反向等量旋转(rotation: -tilt.v),确保货物平台绝对水平。弹簧作动器在补偿激活时高亮发光,并弹出"⚡ 云台实时补偿"标注,引导用户关注核心创新点。交互设计:台阶高度滑块可调节 H 值——当 H ≤ 臂长(90)时动画展示成功越障;当 H > 90 时动画展示碰撞弹回、剧烈晃动的失效场景,直观呈现"臂长决定最大越障高度"的边界条件。播放速度滑块可加速/减速观察细节。
时序编排:全部动作组织在单一
gsap.timeline()中,通过标签(roll→hit→flip→land→depart)精确控制先后顺序与同步关系,底盘倾角与平台补偿通过onUpdate回调逐帧同步,零漂移。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
