独立渲染引擎就绪引擎就绪
<!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:#080c14;color:#d0d8e8;font-family:'Segoe UI',system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:16px 12px}
.header{text-align:center;margin-bottom:12px}
.header h1{font-size:1.25rem;font-weight:600;background:linear-gradient(90deg,#6eaff5,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:.5px}
.header p{font-size:.78rem;color:#6b7a94;margin-top:4px}
.container{width:100%;max-width:1140px;background:#0e1524;border-radius:14px;padding:12px;box-shadow:0 6px 40px rgba(0,0,0,.55);border:1px solid #1a2540}
svg{width:100%;height:auto;display:block;border-radius:8px}
.controls{margin-top:14px;display:flex;gap:18px;align-items:center;flex-wrap:wrap;justify-content:center}
.controls label{font-size:.8rem;color:#7b8ba3;display:flex;align-items:center;gap:6px}
.controls input[type=range]{width:130px;accent-color:#4a90e2}
.controls button{padding:6px 18px;background:linear-gradient(135deg,#2a6bc5,#4a90e2);border:none;border-radius:7px;color:#fff;cursor:pointer;font-size:.82rem;font-weight:500;transition:background .2s}
.controls button:hover{background:linear-gradient(135deg,#1a5bb5,#3a80d2)}
.legend{margin-top:10px;display:flex;gap:18px;font-size:.75rem;color:#5a6a82;justify-content:center;flex-wrap:wrap}
.legend span{display:flex;align-items:center;gap:5px}
.legend .dot{width:10px;height:10px;border-radius:50%;display:inline-block;flex-shrink:0}
</style>
</head>
<body>
<div class="header">
<h1>柔性铰接底盘 · 自适应台阶攀爬原理</h1>
<p>多段万向铰接 + 宽体弹性履带 → 地形自适应蠕动攀爬</p>
</div>
<div class="container">
<svg id="scene" viewBox="0 0 1300 640" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gGround" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3a3228"/><stop offset="100%" stop-color="#221e18"/>
</linearGradient>
<linearGradient id="gStep" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#4a4238"/><stop offset="100%" stop-color="#2e2a22"/>
</linearGradient>
<linearGradient id="gBody" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3a80d8"/><stop offset="100%" stop-color="#1e5aa0"/>
</linearGradient>
<radialGradient id="gHub"><stop offset="0%" stop-color="#606060"/><stop offset="100%" stop-color="#2a2a2a"/></radialGradient>
<filter id="glowJoint"><feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="glowSoft"><feGaussianBlur stdDeviation="2.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<marker id="arrowUp" markerWidth="8" markerHeight="8" refX="4" refY="8" orient="auto"><path d="M1,8 L4,1 L7,8" fill="none" stroke="#ff8844" stroke-width="1.5"/></marker>
<marker id="arrowRight" markerWidth="8" markerHeight="8" refX="0" refY="4" orient="auto"><path d="M0,1 L7,4 L0,7" fill="none" stroke="#44bb88" stroke-width="1.5"/></marker>
</defs>
<!-- 背景 -->
<rect width="1300" height="640" fill="#0a0f1a"/>
<!-- 网格 -->
<g opacity=".06" stroke="#4488ff" stroke-width=".5">
<line x1="0" y1="130" x2="1300" y2="130"/><line x1="0" y1="260" x2="1300" y2="260"/>
<line x1="0" y1="390" x2="1300" y2="390"/><line x1="0" y1="520" x2="1300" y2="520"/>
<line x1="260" y1="0" x2="260" y2="640"/><line x1="520" y1="0" x2="520" y2="640"/>
<line x1="780" y1="0" x2="780" y2="640"/><line x1="1040" y1="0" x2="1040" y2="640"/>
</g>
<!-- 地面 -->
<rect id="ground" x="0" y="510" width="690" height="130" fill="url(#gGround)"/>
<line x1="0" y1="510" x2="690" y2="510" stroke="#5a5040" stroke-width="1.5"/>
<!-- 地面纹理 -->
<g opacity=".15" stroke="#6a5a40" stroke-width=".8">
<line x1="30" y1="530" x2="90" y2="530"/><line x1="150" y1="545" x2="240" y2="545"/>
<line x1="320" y1="535" x2="400" y2="535"/><line x1="480" y1="550" x2="560" y2="550"/>
<line x1="600" y1="530" x2="670" y2="530"/>
</g>
<!-- 台阶 -->
<rect id="stepBody" x="690" y="380" width="610" height="260" fill="url(#gStep)"/>
<line id="stepTopLine" x1="690" y1="380" x2="1300" y2="380" stroke="#7a6f5f" stroke-width="1.5"/>
<rect id="stepFace" x="686" y="380" width="7" height="130" fill="#5a5040" rx="1"/>
<line id="stepEdge" x1="690" y1="380" x2="690" y2="510" stroke="#9a8f7f" stroke-width="1.2"/>
<!-- 台阶纹理 -->
<g opacity=".12" stroke="#6a5a40" stroke-width=".8">
<line x1="750" y1="400" x2="850" y2="400"/><line x1="920" y1="420" x2="1050" y2="420"/>
<line x1="780" y1="460" x2="900" y2="460"/><line x1="1000" y1="440" x2="1150" y2="440"/>
</g>
<!-- 台阶高度标注 -->
<g id="stepAnnot" opacity=".55">
<line x1="720" y1="380" x2="720" y2="510" stroke="#ff8844" stroke-width="1" stroke-dasharray="4,3"/>
<line x1="715" y1="380" x2="725" y2="380" stroke="#ff8844" stroke-width="1"/>
<line x1="715" y1="510" x2="725" y2="510" stroke="#ff8844" stroke-width="1"/>
<text id="stepLabel" x="728" y="450" fill="#ff8844" font-size="11" font-family="monospace">h=130</text>
</g>
<!-- ====== 舱段 A(后段) ====== -->
<g id="segA">
<!-- 履带底色 -->
<path d="M20,-26 L108,-26 Q134,-26 134,0 Q134,26 108,26 L20,26 Q-6,26 -6,0 Q-6,-26 20,-26Z" fill="#1e1e1e" opacity=".7"/>
<!-- 履带齿纹 -->
<path class="track-tread" d="M20,-26 L108,-26 Q134,-26 134,0 Q134,26 108,26 L20,26 Q-6,26 -6,0 Q-6,-26 20,-26Z" fill="none" stroke="#5a5a5a" stroke-width="3" stroke-dasharray="9,6" stroke-linecap="round"/>
<!-- 车体 -->
<rect x="8" y="-56" width="122" height="30" rx="6" fill="url(#gBody)" stroke="#5aa0f0" stroke-width=".8"/>
<!-- 车体细节 -->
<rect x="18" y="-50" width="20" height="8" rx="2" fill="#4a90d8" opacity=".4"/>
<rect x="48" y="-50" width="30" height="8" rx="2" fill="#4a90d8" opacity=".3"/>
<rect x="90" y="-50" width="30" height="8" rx="2" fill="#4a90d8" opacity=".4"/>
<line x1="8" y1="-41" x2="130" y2="-41" stroke="#5aa0f0" stroke-width=".3" opacity=".5"/>
<!-- 驱动标识 -->
<text x="64" y="-33" fill="#8ac4ff" font-size="7" text-anchor="middle" font-family="monospace" opacity=".7">DRIVE-A</text>
<!-- 轮子 -->
<g class="wheel" transform="translate(28,0)">
<circle r="21" fill="#2a2a2a" stroke="#4a4a4a" stroke-width="2"/>
<g class="spoke"><line x1="-14" y1="0" x2="14" y2="0" stroke="#4a4a4a" stroke-width="1.5"/><line x1="0" y1="-14" x2="0" y2="14" stroke="#4a4a4a" stroke-width="1.5"/><line x1="-10" y1="-10" x2="10" y2="10" stroke="#4a4a4a" stroke-width="1"/><line x1="10" y1="-10" x2="-10" y2="10" stroke="#4a4a4a" stroke-width="1"/></g>
<circle r="6" fill="url(#gHub)"/><circle r="2" fill="#555"/>
</g>
<g class="wheel" transform="translate(108,0)">
<circle r="21" fill="#2a2a2a" stroke="#4a4a4a" stroke-width="2"/>
<g class="spoke"><line x1="-14" y1="0" x2="14" y2="0" stroke="#4a4a4a" stroke-width="1.5"/><line x1="0" y1="-14" x2="0" y2="14" stroke="#4a4a4a" stroke-width="1.5"/><line x1="-10" y1="-10" x2="10" y2="10" stroke="#4a4a4a" stroke-width="1"/><line x1="10" y1="-10" x2="-10" y2="10" stroke="#4a4a4a" stroke-width="1"/></g>
<circle r="6" fill="url(#gHub)"/><circle r="2" fill="#555"/>
</g>
</g>
<!-- 关节 A→B 视觉 -->
<g id="jointAB-vis">
<rect x="-4" y="-22" width="8" height="22" rx="2" fill="#3a3a3a" stroke="#555" stroke-width=".5"/>
<circle cx="0" cy="0" r="7" fill="#e89520" stroke="#ffb844" stroke-width="1.5" filter="url(#glowSoft)"/>
<circle cx="0" cy="0" r="3" fill="#ffb844"/>
</g>
<!-- ====== 舱段 B(中段) ====== -->
<g id="segB">
<path d="M20,-26 L108,-26 Q134,-26 134,0 Q134,26 108,26 L20,26 Q-6,26 -6,0 Q-6,-26 20,-26Z" fill="#1e1e1e" opacity=".7"/>
<path class="track-tread" d="M20,-26 L108,-26 Q134,-26 134,0 Q134,26 108,26 L20,26 Q-6,26 -6,0 Q-6,-26 20,-26Z" fill="none" stroke="#5a5a5a" stroke-width="3" stroke-dasharray="9,6" stroke-linecap="round"/>
<rect x="8" y="-56" width="122" height="30" rx="6" fill="url(#gBody)" stroke="#5aa0f0" stroke-width=".8"/>
<rect x="18" y="-50" width="20" height="8" rx="2" fill="#4a90d8" opacity=".4"/>
<rect x="48" y="-50" width="30" height="8" rx="2" fill="#4a90d8" opacity=".3"/>
<rect x="90" y="-50" width="30" height="8" rx="2" fill="#4a90d8" opacity=".4"/>
<line x1="8" y1="-41" x2="130" y2="-41" stroke="#5aa0f0" stroke-width=".3" opacity=".5"/>
<text x="64" y="-33" fill="#8ac4ff" font-size="7" text-anchor="middle" font-family="monospace" opacity=".7">DRIVE-B</text>
<g class="wheel" transform="translate(28,0)">
<circle r="21" fill="#2a2a2a" stroke="#4a4a4a" stroke-width="2"/>
<g class="spoke"><line x1="-14" y1="0" x2="14" y2="0" stroke="#4a4a4a" stroke-width="1.5"/><line x1="0" y1="-14" x2="0" y2="14" stroke="#4a4a4a" stroke-width="1.5"/><line x1="-10" y1="-10" x2="10" y2="10" stroke="#4a4a4a" stroke-width="1"/><line x1="10" y1="-10" x2="-10" y2="10" stroke="#4a4a4a" stroke-width="1"/></g>
<circle r="6" fill="url(#gHub)"/><circle r="2" fill="#555"/>
</g>
<g class="wheel" transform="translate(108,0)">
<circle r="21" fill="#2a2a2a" stroke="#4a4a4a" stroke-width="2"/>
<g class="spoke"><line x1="-14" y1="0" x2="14" y2="0" stroke="#4a4a4a" stroke-width="1.5"/><line x1="0" y1="-14" x2="0" y2="14" stroke="#4a4a4a" stroke-width="1.5"/><line x1="-10" y1="-10" x2="10" y2="10" stroke="#4a4a4a" stroke-width="1"/><line x1="10" y1="-10" x2="-10" y2="10" stroke="#4a4a4a" stroke-width="1"/></g>
<circle r="6" fill="url(#gHub)"/><circle r="2" fill="#555"/>
</g>
</g>
<!-- 关节 B→C 视觉 -->
<g id="jointBC-vis">
<rect x="-4" y="-22" width="8" height="22" rx="2" fill="#3a3a3a" stroke="#555" stroke-width=".5"/>
<circle cx="0" cy="0" r="7" fill="#e89520" stroke="#ffb844" stroke-width="1.5" filter="url(#glowSoft)"/>
<circle cx="0" cy="0" r="3" fill="#ffb844"/>
</g>
<!-- ====== 舱段 C(前段) ====== -->
<g id="segC">
<path d="M20,-26 L108,-26 Q134,-26 134,0 Q134,26 108,26 L20,26 Q-6,26 -6,0 Q-6,-26 20,-26Z" fill="#1e1e1e" opacity=".7"/>
<path class="track-tread" d="M20,-26 L108,-26 Q134,-26 134,0 Q134,26 108,26 L20,26 Q-6,26 -6,0 Q-6,-26 20,-26Z" fill="none" stroke="#5a5a5a" stroke-width="3" stroke-dasharray="9,6" stroke-linecap="round"/>
<rect x="8" y="-56" width="122" height="30" rx="6" fill="url(#gBody)" stroke="#5aa0f0" stroke-width=".8"/>
<rect x="18" y="-50" width="20" height="8" rx="2" fill="#4a90d8" opacity=".4"/>
<rect x="48" y="-50" width="30" height="8" rx="2" fill="#4a90d8" opacity=".3"/>
<rect x="90" y="-50" width="30" height="8" rx="2" fill="#4a90d8" opacity=".4"/>
<line x1="8" y1="-41" x2="130" y2="-41" stroke="#5aa0f0" stroke-width=".3" opacity=".5"/>
<text x="64" y="-33" fill="#8ac4ff" font-size="7" text-anchor="middle" font-family="monospace" opacity=".7">DRIVE-C</text>
<g class="wheel" transform="translate(28,0)">
<circle r="21" fill="#2a2a2a" stroke="#4a4a4a" stroke-width="2"/>
<g class="spoke"><line x1="-14" y1="0" x2="14" y2="0" stroke="#4a4a4a" stroke-width="1.5"/><line x1="0" y1="-14" x2="0" y2="14" stroke="#4a4a4a" stroke-width="1.5"/><line x1="-10" y1="-10" x2="10" y2="10" stroke="#4a4a4a" stroke-width="1"/><line x1="10" y1="-10" x2="-10" y2="10" stroke="#4a4a4a" stroke-width="1"/></g>
<circle r="6" fill="url(#gHub)"/><circle r="2" fill="#555"/>
</g>
<g class="wheel" transform="translate(108,0)">
<circle r="21" fill="#2a2a2a" stroke="#4a4a4a" stroke-width="2"/>
<g class="spoke"><line x1="-14" y1="0" x2="14" y2="0" stroke="#4a4a4a" stroke-width="1.5"/><line x1="0" y1="-14" x2="0" y2="14" stroke="#4a4a4a" stroke-width="1.5"/><line x1="-10" y1="-10" x2="10" y2="10" stroke="#4a4a4a" stroke-width="1"/><line x1="10" y1="-10" x2="-10" y2="10" stroke="#4a4a4a" stroke-width="1"/></g>
<circle r="6" fill="url(#gHub)"/><circle r="2" fill="#555"/>
</g>
<!-- 前端指示 -->
<polygon points="134,-8 146,0 134,8" fill="#5aa0f0" opacity=".6"/>
</g>
<!-- 阻力/受力箭头 (动态显示) -->
<g id="forceArrows" opacity="0">
<line id="forceUp" x1="0" y1="0" x2="0" y2="-50" stroke="#ff8844" stroke-width="2" marker-end="url(#arrowUp)"/>
<line id="forceRight" x1="0" y1="0" x2="45" y2="0" stroke="#44bb88" stroke-width="2" marker-end="url(#arrowRight)"/>
<text id="forceLabelUp" x="8" y="-35" fill="#ff8844" font-size="9" font-family="monospace">F↑</text>
<text id="forceLabelRt" x="30" y="-8" fill="#44bb88" font-size="9" font-family="monospace">F→</text>
</g>
<!-- 弯折角标注 (动态) -->
<g id="angleAnnotBC" opacity="0">
<path id="angleArcBC" d="" fill="none" stroke="#ffcc44" stroke-width="1.2"/>
<text id="angleTextBC" x="0" y="0" fill="#ffcc44" font-size="10" font-family="monospace"></text>
</g>
<g id="angleAnnotAB" opacity="0">
<path id="angleArcAB" d="" fill="none" stroke="#ffcc44" stroke-width="1.2"/>
<text id="angleTextAB" x="0" y="0" fill="#ffcc44" font-size="10" font-family="monospace"></text>
</g>
<!-- 阶段文字 -->
<rect x="30" y="560" width="420" height="30" rx="6" fill="#0e1524" stroke="#1a2a44" stroke-width=".8" opacity=".85"/>
<text id="phaseText" x="42" y="580" fill="#8cb4f5" font-size="12" font-family="'Segoe UI',sans-serif"></text>
<!-- 参数面板 -->
<g transform="translate(920,30)" opacity=".6">
<rect x="0" y="0" width="240" height="95" rx="8" fill="#0e1524" stroke="#1a2a44" stroke-width=".8"/>
<text x="12" y="18" fill="#6eaff5" font-size="10" font-weight="600">核心参数</text>
<text x="12" y="36" fill="#7b8ba3" font-size="9">舱段间最大弯折角:≥45°</text>
<text x="12" y="52" fill="#7b8ba3" font-size="9">履带横向延展率:≥15%</text>
<text x="12" y="68" fill="#7b8ba3" font-size="9">独立轮毂电机:3×2 驱动</text>
<text x="12" y="84" fill="#5a6a82" font-size="8">弹性履带随形变自动贴合</text>
</g>
</svg>
</div>
<div class="controls">
<button id="replayBtn">▶ 重新播放</button>
<label>动画速度 <input type="range" id="speedSlider" min="0.3" max="2.5" step="0.1" value="1"><span id="speedVal">1.0x</span></label>
<label>台阶高度 <input type="range" id="stepSlider" min="60" max="180" step="5" value="130"><span id="stepVal">130</span></label>
</div>
<div class="legend">
<span><span class="dot" style="background:#2a6bc5"></span>独立驱动舱段</span>
<span><span class="dot" style="background:#e89520"></span>万向铰接关节</span>
<span><span class="dot" style="background:#5a5a5a"></span>宽体弹性履带</span>
<span><span class="dot" style="background:#ff8844"></span>台阶阻力 / 驱动力</span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
/* ====== 常量与元素引用 ====== */
const SEG_LEN = 130; // 舱段长度(枢轴到枢轴)
const GROUND_Y = 510; // 地面 y 坐标
const INITIAL_X = 70; // 初始 x 位置
const segA = document.getElementById('segA');
const segB = document.getElementById('segB');
const segC = document.getElementById('segC');
const jointABVis = document.getElementById('jointAB-vis');
const jointBCVis = document.getElementById('jointBC-vis');
const phaseText = document.getElementById('phaseText');
const forceArrows= document.getElementById('forceArrows');
const angleAnnotBC = document.getElementById('angleAnnotBC');
const angleAnnotAB = document.getElementById('angleAnnotAB');
/* ====== 动画代理状态 ====== */
const proxy = { baseX: INITIAL_X, baseY: GROUND_Y, angleA: 0, angleB: 0, angleC: 0 };
/* ====== 渲染函数:根据代理状态更新所有舱段位置 ====== */
function render() {
const { baseX, baseY, angleA, angleB, angleC } = proxy;
const rad = Math.PI / 180;
const absA = angleA;
const absB = angleA + angleB;
const absC = angleA + angleB + angleC;
// 舱段 A
segA.setAttribute('transform', `translate(${baseX},${baseY}) rotate(${absA})`);
// 舱段 B 枢轴(A 的前端)
const bX = baseX + SEG_LEN * Math.cos(absA * rad);
const bY = baseY + SEG_LEN * Math.sin(absA * rad);
segB.setAttribute('transform', `translate(${bX},${bY}) rotate(${absB})`);
jointABVis.setAttribute('transform', `translate(${bX},${bY})`);
// 舱段 C 枢轴(B 的前端)
const cX = bX + SEG_LEN * Math.cos(absB * rad);
const cY = bY + SEG_LEN * Math.sin(absB * rad);
segC.setAttribute('transform', `translate(${cX},${cY}) rotate(${absC})`);
jointBCVis.setAttribute('transform', `translate(${cX},${cY})`);
// 更新受力箭头位置(在 C 前轮处)
const fX = cX + 108 * Math.cos(absC * rad);
const fY = cY + 108 * Math.sin(absC * rad);
forceArrows.setAttribute('transform', `translate(${fX},${fY}) rotate(${absC})`);
// 弯折角标注 - BC 关节
if (Math.abs(angleC) > 2) {
angleAnnotBC.setAttribute('opacity', '0.8');
angleAnnotBC.setAttribute('transform', `translate(${cX},${cY})`);
const arcR = 35;
const startA = absB * rad;
const endA = absC * rad;
const sx = arcR * Math.cos(startA), sy = arcR * Math.sin(startA);
const ex = arcR * Math.cos(endA), ey = arcR * Math.sin(endA);
const largeArc = Math.abs(angleC) > 180 ? 1 : 0;
const sweep = angleC < 0 ? 0 : 1;
document.getElementById('angleArcBC').setAttribute('d',
`M${sx},${sy} A${arcR},${arcR} 0 ${largeArc},${sweep} ${ex},${ey}`);
const midA = (absB + absC) / 2 * rad;
document.getElementById('angleTextBC').setAttribute('x', (arcR + 12) * Math.cos(midA));
document.getElementById('angleTextBC').setAttribute('y', (arcR + 12) * Math.sin(midA) + 4);
document.getElementById('angleTextBC').textContent = Math.abs(angleC).toFixed(0) + '°';
} else {
angleAnnotBC.setAttribute('opacity', '0');
}
// 弯折角标注 - AB 关节
if (Math.abs(angleB) > 2) {
angleAnnotAB.setAttribute('opacity', '0.8');
angleAnnotAB.setAttribute('transform', `translate(${bX},${bY})`);
const arcR = 35;
const startA = absA * rad;
const endA = absB * rad;
const sx = arcR * Math.cos(startA), sy = arcR * Math.sin(startA);
const ex = arcR * Math.cos(endA), ey = arcR * Math.sin(endA);
const largeArc = Math.abs(angleB) > 180 ? 1 : 0;
const sweep = angleB < 0 ? 0 : 1;
document.getElementById('angleArcAB').setAttribute('d',
`M${sx},${sy} A${arcR},${arcR} 0 ${largeArc},${sweep} ${ex},${ey}`);
const midA = (absA + absB) / 2 * rad;
document.getElementById('angleTextAB').setAttribute('x', (arcR + 12) * Math.cos(midA));
document.getElementById('angleTextAB').setAttribute('y', (arcR + 12) * Math.sin(midA) + 4);
document.getElementById('angleTextAB').textContent = Math.abs(angleB).toFixed(0) + '°';
} else {
angleAnnotAB.setAttribute('opacity', '0');
}
}
/* ====== 更新台阶视觉 ====== */
function updateStepVisual(h) {
const topY = GROUND_Y - h;
document.getElementById('stepBody').setAttribute('y', topY);
document.getElementById('stepBody').setAttribute('height', h + 130);
document.getElementById('stepTopLine').setAttribute('y1', topY);
document.getElementById('stepTopLine').setAttribute('y2', topY);
document.getElementById('stepFace').setAttribute('y', topY);
document.getElementById('stepFace').setAttribute('height', h);
document.getElementById('stepEdge').setAttribute('y1', topY);
document.getElementById('stepEdge').setAttribute('y2', GROUND_Y);
document.getElementById('stepLabel').textContent = 'h=' + h;
document.getElementById('stepVal').textContent = h;
}
/* ====== 构建时间轴 ====== */
function buildTimeline(stepH) {
if (window._mainTL) window._mainTL.kill();
const stepY = GROUND_Y - stepH;
// 弯折角与台阶高度正相关,但不超过 75°
const bendAngle = -Math.min(75, 30 + stepH * 0.28);
// 重置代理
proxy.baseX = INITIAL_X; proxy.baseY = GROUND_Y;
proxy.angleA = 0; proxy.angleB = 0; proxy.angleC = 0;
render();
updateStepVisual(stepH);
const tl = gsap.timeline({
repeat: -1,
repeatDelay: 2.5,
onUpdate: render,
defaults: { ease: 'power2.inOut' }
});
/* --- 确保每次循环从初始状态开始 --- */
tl.set(proxy, { baseX: INITIAL_X, baseY: GROUND_Y, angleA: 0, angleB: 0, angleC: 0 }, 0);
tl.set(forceArrows, { opacity: 0 }, 0);
tl.set(angleAnnotBC, { opacity: 0 }, 0);
tl.set(angleAnnotAB, { opacity: 0 }, 0);
/* ── 履带持续滚动 ── */
tl.to('.track-tread', { strokeDashoffset: -600, duration: 16, ease: 'none' }, 0);
/* ── 轮毂持续旋转 ── */
tl.to('.spoke', { rotation: 1080, duration: 16, ease: 'none', svgOrigin: '0 0' }, 0);
/* ── Phase 1:接近台阶(0‑2.8s)── */
tl.addLabel('approach', 0);
tl.to(proxy, { baseX: 260, duration: 2.8, ease: 'power1.inOut' }, 'approach');
tl.to(phaseText, { duration: 0.01, text: '▶ 接近台阶 — 各舱段平直行进' }, 0);
/* ── Phase 2:前舱段 C 折叠上仰(2.8‑4.5s)── */
tl.addLabel('bendC', 2.8);
tl.to(proxy, { angleC: bendAngle, duration: 1.7 }, 'bendC');
// 关节 BC 高亮
tl.to('#jointBC-vis circle', { attr: { r: 10 }, fill: '#ffdd44', stroke: '#ffe877', strokeWidth: 2, duration: 0.3 }, 'bendC');
tl.to('#jointBC-vis', { filter: 'url(#glowJoint)', duration: 0.3 }, 'bendC');
// 受力箭头显示
tl.to(forceArrows, { opacity: 0.85, duration: 0.4 }, 'bendC+=0.2');
tl.to(phaseText, { duration: 0.01, text: '▶ 前舱段受阻折叠上仰 — 铰接关节自适应弯曲' }, 'bendC');
/* ── Phase 3:履带贴合攀爬(3.5‑6s)── */
tl.addLabel('climb1', 3.5);
tl.to(proxy, { baseX: 380, baseY: GROUND_Y - stepH * 0.4, duration: 2.5, ease: 'power1.inOut' }, 'climb1');
tl.to(phaseText, { duration: 0.01, text: '▶ 履带贴合台阶边缘 — 持续卷动提供抓地力' }, 'climb1');
/* ── Phase 4:前舱段 C 回正(5‑6.3s)── */
tl.addLabel('straightC', 5);
tl.to(proxy, { angleC: 0, duration: 1.3, ease: 'power2.out' }, 'straightC');
tl.to('#jointBC-vis circle', { attr: { r: 7 }, fill: '#e89520', stroke: '#ffb844', strokeWidth: 1.5, duration: 0.4 }, 'straightC+=0.5');
tl.to('#jointBC-vis', { filter: 'url(#glowSoft)', duration: 0.4 }, 'straightC+=0.5');
tl.to(forceArrows, { opacity: 0, duration: 0.3 }, 'straightC');
/* ── Phase 5:中舱段 B 折叠跟进(5.8‑7.5s)── */
tl.addLabel('bendB', 5.8);
tl.to(proxy, { angleB: bendAngle, duration: 1.7 }, 'bendB');
tl.to('#jointAB-vis circle', { attr: { r: 10 }, fill: '#ffdd44', stroke: '#ffe877', strokeWidth: 2, duration: 0.3 }, 'bendB');
tl.to('#jointAB-vis', { filter: 'url(#glowJoint)', duration: 0.3 }, 'bendB');
tl.to(forceArrows, { opacity: 0.7, duration: 0.3 }, 'bendB+=0.3');
tl.to(phaseText, { duration: 0.01, text: '▶ 中舱段折叠跟进 — 万向节串联传递弯折' }, 'bendB');
/* ── Phase 6:继续攀爬(6.5‑8.5s)── */
tl.addLabel('climb2', 6.5);
tl.to(proxy, { baseX: 490, baseY: GROUND_Y - stepH * 0.85, duration: 2, ease: 'power1.inOut' }, 'climb2');
/* ── Phase 7:中舱段 B 回正(7.8‑9s)── */
tl.addLabel('straightB', 7.8);
tl.to(proxy, { angleB: 0, duration: 1.2, ease: 'power2.out' }, 'straightB');
tl.to('#jointAB-vis circle', { attr: { r: 7 }, fill: '#e89520', stroke: '#ffb844', strokeWidth: 1.5, duration: 0.4 }, 'straightB+=0.5');
tl.to('#jointAB-vis', { filter: 'url(#glowSoft)', duration: 0.4 }, 'straightB+=0.5');
tl.to(forceArrows, { opacity: 0, duration: 0.3 }, 'straightB');
/* ── Phase 8:后舱段抬升登顶(8.8‑10.3s)── */
tl.addLabel('onTop', 8.8);
tl.to(proxy, { baseX: 560, baseY: stepY, angleA: 0, duration: 1.5, ease: 'power2.out' }, 'onTop');
tl.to(phaseText, { duration: 0.01, text: '▶ 全车登顶 — 底盘受自重与弹簧拉力恢复平直' }, 'onTop');
/* ── Phase 9:继续前进(10.3‑12.5s)── */
tl.addLabel('depart', 10.3);
tl.to(proxy, { baseX: 850, duration: 2.2, ease: 'power1.inOut' }, 'depart');
tl.to(phaseText, { duration: 0.01, text: '▶ 跨越台阶 — 柔性底盘重塑完成,继续前进' }, 'depart');
window._mainTL = tl;
return tl;
}
/* ====== 初始化 ====== */
let currentStepH = 130;
buildTimeline(currentStepH);
/* ====== 控件交互 ====== */
document.getElementById('replayBtn').addEventListener('click', () => {
buildTimeline(currentStepH);
});
document.getElementById('speedSlider').addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
document.getElementById('speedVal').textContent = v.toFixed(1) + 'x';
if (window._mainTL) window._mainTL.timeScale(v);
});
document.getElementById('stepSlider').addEventListener('input', (e) => {
currentStepH = parseInt(e.target.value);
document.getElementById('stepVal').textContent = currentStepH;
buildTimeline(currentStepH);
// 同步速度
const spd = parseFloat(document.getElementById('speedSlider').value);
if (window._mainTL) window._mainTL.timeScale(spd);
});
</script>
</body>
</html>
实现说明
核心架构:采用 JavaScript 代理对象 + setAttribute 直接操控 SVG transform 的方式,精确计算每个舱段枢轴的全局坐标与绝对旋转角,避免了嵌套 SVG group 中 transformOrigin 的不确定性问题。
动画编排:全部动作组织在一个 GSAP 主时间轴中,使用 addLabel 划分 9 个阶段——接近→前舱折叠→攀爬→前舱回正→中舱折叠→继续攀爬→中舱回正→登顶→驶离。各阶段通过位置参数精确控制错峰与同步关系。
视觉引导:
- 铰接关节在弯折时放大发光(
filter: url(#glowJoint)),平直时恢复常态 - 受力箭头在前舱接触台阶时出现,直观展示阻力如何驱动折叠
- 弯折角实时标注(弧线 + 角度数值),凸显"≥45°"核心参数
- 履带齿纹
stroke-dashoffset持续滚动 + 轮毂辐条旋转,表达驱动力传递
交互控制:台阶高度滑块实时重建动画(弯折角随高度自适应增大),速度滑块通过 timeScale 无缝变速,重播按钮从初始状态重新开始。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
