独立渲染引擎就绪引擎就绪
<!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分
等待动画代码生成...
