<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>行星式翻转轮组 · 主动液压自平衡云台</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#060a12;--fg:#d0dae6;--muted:#3a4a5c;--accent:#ff9100;--cyan:#00e5ff;--green:#00e676;--red:#ff1744;--card:#0b1320;--border:#152030}
body{background:var(--bg);color:var(--fg);font-family:'Share Tech Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px 10px}
.wrap{max-width:1440px;width:100%}
header{text-align:center;margin-bottom:18px}
header h1{font-family:'Orbitron',sans-serif;font-weight:900;font-size:clamp(20px,3vw,34px);letter-spacing:4px;background:linear-gradient(90deg,var(--cyan),var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent;line-height:1.3}
header p{font-size:clamp(12px,1.5vw,16px);color:var(--muted);margin-top:4px;letter-spacing:2px}
.svg-box{width:100%;aspect-ratio:2/1;min-height:340px;max-height:700px;background:var(--card);border:1px solid var(--border);border-radius:12px;overflow:hidden;position:relative}
.svg-box svg{width:100%;height:100%;display:block}
.ctrl{display:flex;flex-wrap:wrap;gap:16px 28px;margin-top:18px;padding:16px 24px;background:var(--card);border:1px solid var(--border);border-radius:10px;align-items:center;justify-content:center}
.ctrl-group{display:flex;flex-direction:column;gap:4px;min-width:160px}
.ctrl-group label{font-size:11px;color:var(--muted);letter-spacing:1px;text-transform:uppercase}
.ctrl-group input[type=range]{-webkit-appearance:none;width:100%;height:6px;background:var(--border);border-radius:3px;outline:none}
.ctrl-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--cyan);cursor:pointer;box-shadow:0 0 8px var(--cyan)}
.ctrl-group .val{font-size:13px;color:var(--cyan);font-weight:bold}
.btn-row{display:flex;gap:10px;align-items:center}
.btn{padding:8px 20px;border:1px solid var(--border);background:transparent;color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:13px;border-radius:6px;cursor:pointer;transition:all .2s;letter-spacing:1px}
.btn:hover{border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 12px rgba(0,229,255,.15)}
.btn.active{background:rgba(0,229,255,.1);border-color:var(--cyan);color:var(--cyan)}
.dashboard{display:flex;flex-wrap:wrap;gap:12px;margin-top:14px;justify-content:center}
.dash-item{padding:10px 18px;background:var(--card);border:1px solid var(--border);border-radius:8px;text-align:center;min-width:130px}
.dash-item .dl{font-size:10px;color:var(--muted);letter-spacing:1px;text-transform:uppercase;margin-bottom:4px}
.dash-item .dv{font-size:18px;font-weight:bold;font-family:'Orbitron',sans-serif}
.dv.cyan{color:var(--cyan)}.dv.orange{color:var(--accent)}.dv.green{color:var(--green)}.dv.red{color:var(--red)}
.ifr-note{margin-top:16px;padding:14px 22px;background:linear-gradient(135deg,rgba(0,230,118,.04),rgba(0,229,255,.04));border:1px solid rgba(0,230,118,.15);border-radius:8px;font-size:13px;line-height:1.7;color:var(--muted);max-width:900px;text-align:center}
.ifr-note strong{color:var(--green)}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>PLANETARY FLIP-WHEEL & ACTIVE GIMBAL</h1>
<p>行星式翻转轮组 · 主动液压自平衡云台 — 越障与平衡解耦</p>
</header>
<div class="svg-box">
<svg id="svg" viewBox="0 0 1400 700" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div class="ctrl">
<div class="ctrl-group">
<label>台阶高度</label>
<input type="range" id="rngStep" min="5" max="28" step="1" value="20">
<span class="val" id="valStep">20 cm</span>
</div>
<div class="ctrl-group">
<label>动画速度</label>
<input type="range" id="rngSpeed" min="3" max="20" step="1" value="8">
<span class="val" id="valSpeed">1.0x</span>
</div>
<div class="btn-row">
<button class="btn active" id="btnPlay">播放</button>
<button class="btn" id="btnReset">重置</button>
</div>
</div>
<div class="dashboard">
<div class="dash-item"><div class="dl">当前阶段</div><div class="dv cyan" id="dPhase">平地行驶</div></div>
<div class="dash-item"><div class="dl">底盘倾角</div><div class="dv orange" id="dTilt">0.0°</div></div>
<div class="dash-item"><div class="dl">云台补偿角</div><div class="dv green" id="dGimbal">0.0°</div></div>
<div class="dash-item"><div class="dl">前臂旋转</div><div class="dv cyan" id="dFront">0°</div></div>
<div class="dash-item"><div class="dl">后臂旋转</div><div class="dv cyan" id="dRear">0°</div></div>
</div>
<div class="ifr-note">
<strong>最终理想解 (IFR)</strong>:将"移动越障"与"保持平衡"两个矛盾功能彻底解耦——底层用刚性翻转暴力克服高差,上层用主动液压云台吸收冲击并保持姿态,货物全程维持绝对水平。
</div>
</div>
<script>
/* ===== 配置 ===== */
const NS='http://www.w3.org/2000/svg';
const C={
W:1400,H:700,
groundY:530,stepX:690,
armR:80,wheelR:14,
pivotSpan:240,
chassisW:270,chassisH:28,
gimbalH:50,
platW:250,platH:16,
cargoW:170,cargoH:62,
};
const CM_PER_PX=25/C.armR; // 25cm = armR px
/* ===== 状态 ===== */
let S={progress:0,playing:true,speed:1,stepCm:20};
let lastTime=0;
/* ===== 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 setA(e,attrs){Object.entries(attrs).forEach(([k,v])=>e.setAttribute(k,v))}
function lerp(a,b,t){return a+(b-a)*t}
function smoothstep(t){return t*t*(3-2*t)}
function deg(rad){return rad*180/Math.PI}
function rad(d){return d*Math.PI/180}
/* ===== 获取SVG ===== */
const svg=document.getElementById('svg');
/* ===== 创建滤镜和渐变 ===== */
const defs=el('defs',null,svg);
// 辉光滤镜
['cyanGlow','orangeGlow','greenGlow'].forEach(id=>{
const c={cyanGlow:'0 1 1 0 0',orangeGlow:'0 0 0 0 0 0 0.57 0 0 0 0 0.33 0 0 0 0 0 0 1 0',greenGlow:'0 0 0 0 0 0 0.9 0 0 0 0 0.27 0 0 0 0 0 0 1 0'}[id];
const f=el('filter',{id,x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'5',result:'b'},f);
el('feColorMatrix',{in:'b',type:'matrix',values:c,result:'cb'},f);
const m=el('feMerge',null,f);
el('feMergeNode',{in:'cb'},m);el('feMergeNode',{in:'SourceGraphic'},m);
});
// 网格图案
const pat=el('pattern',{id:'grid',width:40,height:40,patternUnits:'userSpaceOnUse'},defs);
el('path',{d:'M 40 0 L 0 0 0 40',fill:'none',stroke:'rgba(0,229,255,0.04)',"stroke-width":0.5},pat);
/* ===== 创建静态背景 ===== */
el('rect',{width:C.W,height:C.H,fill:'#060a12'},svg);
el('rect',{width:C.W,height:C.H,fill:'url(#grid)'},svg);
/* ===== 创建动态元素组 ===== */
const gGround=el('g',{id:'gGround'},svg);
const gTrailF=el('g',{id:'gTrailF'},svg);
const gTrailR=el('g',{id:'gTrailR'},svg);
const gCart=el('g',{id:'gCart'},svg);
const gAnnot=el('g',{id:'gAnnot'},svg);
/* ===== 地面与台阶 ===== */
const groundL=el('path',{fill:'#0b1520',stroke:'#1a3050',"stroke-width":1.5},gGround);
const groundR=el('path',{fill:'#0b1520',stroke:'#1a3050',"stroke-width":1.5},gGround);
const stepFace=el('line',{stroke:'#00e5ff',"stroke-width":2,"stroke-dasharray":"6,4",opacity:0.6},gGround);
const stepTop=el('line',{stroke:'#00e5ff',"stroke-width":1.5,opacity:0.4},gGround);
// 台阶高度标注
const stepDimLine=el('line',{stroke:'#ff9100',"stroke-width":1,"stroke-dasharray":"4,3",opacity:0.7},gGround);
const stepDimT1=el('text',{fill:'#ff9100','font-size':12,'font-family':'Share Tech Mono','text-anchor':'middle'},gGround);
const stepDimT2=el('text',{fill:'#ff9100','font-size':12,'font-family':'Share Tech Mono','text-anchor':'middle'},gGround);
/* ===== 翻转轨迹弧线 ===== */
const trailArcF=el('path',{fill:'none',stroke:'rgba(0,229,255,0.15)',"stroke-width":2,"stroke-dasharray":"4,4"},gTrailF);
const trailArcR=el('path',{fill:'none',stroke:'rgba(0,229,255,0.15)',"stroke-width":2,"stroke-dasharray":"4,4"},gTrailR);
/* ===== 底盘组 ===== */
const gChassis=el('g',null,gCart);
const chassisRect=el('rect',{rx:4,ry:4,fill:'rgba(0,188,212,0.1)',stroke:'#00bcd4',"stroke-width":2,filter:'url(#cyanGlow)'},gChassis);
// 前臂组
const gArmF=el('g',null,gChassis);
const armLineF1=el('line',{stroke:'#00e5ff',"stroke-width":2.5},gArmF);
const armLineF2=el('line',{stroke:'#00e5ff',"stroke-width":2.5},gArmF);
const armLineF3=el('line',{stroke:'#00e5ff',"stroke-width":2.5},gArmF);
const armPivotF=el('circle',{r:5,fill:'#0b1520',stroke:'#00e5ff',"stroke-width":2},gArmF);
const wheelF=[];for(let i=0;i<3;i++){const g=el('g',null,gArmF);el('circle',{r:C.wheelR,fill:'rgba(0,229,255,0.05)',stroke:'#90a4ae',"stroke-width":1.8},g);for(let s=0;s<4;s++){el('line',{stroke:'#607080',"stroke-width":1.2,opacity:0.6},g)}wheelF.push(g)}
// 后臂组
const gArmR=el('g',null,gChassis);
const armLineR1=el('line',{stroke:'#00e5ff',"stroke-width":2.5},gArmR);
const armLineR2=el('line',{stroke:'#00e5ff',"stroke-width":2.5},gArmR);
const armLineR3=el('line',{stroke:'#00e5ff',"stroke-width":2.5},gArmR);
const armPivotR=el('circle',{r:5,fill:'#0b1520',stroke:'#00e5ff',"stroke-width":2},gArmR);
const wheelR=[];for(let i=0;i<3;i++){const g=el('g',null,gArmR);el('circle',{r:C.wheelR,fill:'rgba(0,229,255,0.05)',stroke:'#90a4ae',"stroke-width":1.8},g);for(let s=0;s<4;s++){el('line',{stroke:'#607080',"stroke-width":1.2,opacity:0.6},g)}wheelR.push(g)}
/* ===== 云台液压缸 ===== */
const gGimbal=el('g',null,gCart);
const gimCylL=el('rect',{rx:3,fill:'rgba(255,145,0,0.15)',stroke:'#ff9100',"stroke-width":1.8,filter:'url(#orangeGlow)'},gGimbal);
const gimPistL=el('rect',{rx:2,fill:'#ff9100',opacity:0.8},gGimbal);
const gimCylR=el('rect',{rx:3,fill:'rgba(255,145,0,0.15)',stroke:'#ff9100',"stroke-width":1.8,filter:'url(#orangeGlow)'},gGimbal);
const gimPistR=el('rect',{rx:2,fill:'#ff9100',opacity:0.8},gGimbal);
// 陀螺仪图标
const gyroG=el('g',null,gGimbal);
const gyroOuter=el('circle',{r:10,fill:'none',stroke:'#ff9100',"stroke-width":1.2,opacity:0.7},gyroG);
const gyroInner=el('ellipse',{rx:10,ry:4,fill:'none',stroke:'#ff9100',"stroke-width":1,opacity:0.5},gyroG);
const gyroDot=el('circle',{r:2,fill:'#ff9100',opacity:0.9},gyroG);
/* ===== 平台组 ===== */
const gPlatform=el('g',null,gCart);
const platRect=el('rect',{rx:3,fill:'rgba(0,230,118,0.1)',stroke:'#00e676',"stroke-width":2,filter:'url(#greenGlow)'},gPlatform);
// 水平仪
const spiritBg=el('rect',{rx:4,fill:'rgba(0,230,118,0.08)',stroke:'#00e676',"stroke-width":1,opacity:0.7},gPlatform);
const spiritBubble=el('circle',{r:5,fill:'#00e676',opacity:0.9,filter:'url(#greenGlow)'},gPlatform);
const spiritMark1=el('line',{stroke:'#00e676',"stroke-width":1,opacity:0.4},gPlatform);
const spiritMark2=el('line',{stroke:'#00e676',"stroke-width":1,opacity:0.4},gPlatform);
/* ===== 货物 ===== */
const cargoRect=el('rect',{rx:4,fill:'rgba(224,232,240,0.06)',stroke:'#78909c',"stroke-width":1.5},gPlatform);
const cargoLabel=el('text',{fill:'#90a4ae','font-size':13,'font-family':'Share Tech Mono','text-anchor':'middle',dominantBaseline:'middle'},gPlatform);
/* ===== 水平参考线 ===== */
const refLine=el('line',{stroke:'#00e676',"stroke-width":1,"stroke-dasharray":"8,6",opacity:0.3},gCart);
/* ===== 标注文字 ===== */
const lblArm=el('text',{fill:'#00e5ff','font-size':11,'font-family':'Share Tech Mono','text-anchor':'middle',opacity:0.8},gAnnot);
const lblGimbal=el('text',{fill:'#ff9100','font-size':11,'font-family':'Share Tech Mono','text-anchor':'middle',opacity:0.8},gAnnot);
const lblPlat=el('text',{fill:'#00e676','font-size':11,'font-family':'Share Tech Mono','text-anchor':'middle',opacity:0.8},gAnnot);
const lblPhase=el('text',{fill:'#e0e8f0','font-size':15,'font-family':'Orbitron','text-anchor':'start','font-weight':700,opacity:0.9},gAnnot);
const lblWarn=el('text',{fill:'#ff1744','font-size':16,'font-family':'Orbitron','text-anchor':'middle','font-weight':700},gAnnot);
// 臂长标注线
const armDimLine=el('line',{stroke:'#00e5ff',"stroke-width":0.8,"stroke-dasharray":"3,3",opacity:0.5},gAnnot);
const armDimTxt=el('text',{fill:'#00e5ff','font-size':10,'font-family':'Share Tech Mono','text-anchor':'middle',opacity:0.7},gAnnot);
// 倾角弧线
const tiltArc=el('path',{fill:'none',stroke:'#ff9100',"stroke-width":1.5,opacity:0.6},gAnnot);
const tiltTxt=el('text',{fill:'#ff9100','font-size':11,'font-family':'Share Tech Mono','text-anchor':'start',opacity:0.8},gAnnot);
/* ===== 关键帧计算 ===== */
function stepPx(){return S.stepCm/CM_PER_PX}
function basePY(){return C.groundY-C.wheelR-C.armR*0.5}
function stepPY(){return C.groundY-stepPx()-C.wheelR-C.armR*0.5}
function getKeyframes(){
const sh=stepPx(),bY=basePY(),sY=stepPY();
const rise=bY-sY;
return[
{t:0.00,cx:300, fpY:bY, rpY:bY, faA:0, raA:0},
{t:0.12,cx:470, fpY:bY, rpY:bY, faA:0, raA:0},
{t:0.18,cx:520, fpY:bY-rise*0.05,rpY:bY, faA:8, raA:0},
{t:0.26,cx:570, fpY:bY-rise*0.25,rpY:bY, faA:35, raA:0},
{t:0.34,cx:620, fpY:bY-rise*0.55,rpY:bY, faA:65, raA:0},
{t:0.40,cx:655, fpY:bY-rise*0.85,rpY:bY, faA:95, raA:0},
{t:0.46,cx:690, fpY:sY, rpY:bY, faA:120,raA:0},
{t:0.54,cx:750, fpY:sY, rpY:bY, faA:120,raA:0},
{t:0.60,cx:790, fpY:sY, rpY:bY-rise*0.05,faA:120,raA:8},
{t:0.68,cx:840, fpY:sY, rpY:bY-rise*0.25,faA:120,raA:35},
{t:0.76,cx:890, fpY:sY, rpY:bY-rise*0.55,faA:120,raA:65},
{t:0.82,cx:925, fpY:sY, rpY:bY-rise*0.85,faA:120,raA:95},
{t:0.88,cx:960, fpY:sY, rpY:sY, faA:120,raA:120},
{t:1.00,cx:1180, fpY:sY, rpY:sY, faA:120,raA:120},
];
}
function interpKF(kfs,t){
let i=0;
while(i<kfs.length-1&&kfs[i+1].t<=t)i++;
if(i>=kfs.length-1)return{...kfs[kfs.length-1]};
const a=kfs[i],b=kfs[i+1];
const lt=(t-a.t)/Math.max(0.001,b.t-a.t);
const s=smoothstep(Math.max(0,Math.min(1,lt)));
return{cx:lerp(a.cx,b.cx,s),fpY:lerp(a.fpY,b.fpY,s),rpY:lerp(a.rpY,b.rpY,s),faA:lerp(a.faA,b.faA,s),raA:lerp(a.raA,b.raA,s)};
}
/* ===== 轮子位置计算 ===== */
function wheelPos(px,py,armAngle){
const ws=[];
for(let i=0;i<3;i++){
const a=rad(armAngle-90+i*120);
ws.push({x:px+C.armR*Math.cos(a),y:py+C.armR*Math.sin(a)});
}
return ws;
}
/* ===== 更新地面 ===== */
function updateGround(){
const sh=stepPx(),sx=C.stepX,gy=C.groundY;
setA(groundL,{d:`M0,${gy} L${sx},${gy} L${sx},${gy+200} L0,${gy+200} Z`});
setA(groundR,{d:`M${sx},${gy-sh} L${C.W},${gy-sh} L${C.W},${gy+200} L${sx},${gy+200} Z`});
setA(stepFace,{x1:sx,y1:gy-sh,x2:sx,y2:gy});
setA(stepTop,{x1:sx,y1:gy-sh,x2:sx+120,y2:gy-sh});
setA(stepDimLine,{x1:sx+30,y1:gy,x2:sx+30,y2:gy-sh});
setA(stepDimT1,{x:sx+30,y:gy+16});stepDimT1.textContent=`H=${S.stepCm}cm`;
setA(stepDimT2,{x:sx+30,y:gy-sh-8});stepDimT2.textContent='▲';
// 越限警告
if(S.stepCm>25){
setA(lblWarn,{x:sx,y:gy-sh-30,opacity:1});lblWarn.textContent='超过臂长极限!';
}else{setA(lblWarn,{opacity:0})}
}
/* ===== 主渲染 ===== */
function render(){
const kfs=getKeyframes();
const st=interpKF(kfs,S.progress);
const sh=stepPx();
const halfSpan=C.pivotSpan/2;
const fpx=st.cx+halfSpan, rpx=st.cx-halfSpan;
const fpy=st.fpY, rpy=st.rpY;
// 底盘倾角
const tiltRad=Math.atan2(fpy-rpy,fpx-rpx);
const tiltDeg=deg(tiltRad);
const chassisMidX=st.cx;
const chassisMidY=(fpy+rpy)/2-C.chassisH/2;
// 平台(保持水平)
const platY=chassisMidY-C.chassisH/2-C.gimbalH-C.platH/2;
// 货物
const cargoY=platY-C.platH/2-C.cargoH/2;
// --- 底盘 ---
const cosT=Math.cos(tiltRad),sinT=Math.sin(tiltRad);
function rotRect(cx,cy,w,h,angle){
const c=Math.cos(angle),s=Math.sin(angle);
const dx=w/2,dy=h/2;
return[
{x:cx+dx*c-dy*s,y:cy+dx*s+dy*c},
{x:cx-dx*c-dy*s,y:cy-dx*s+dy*c},
{x:cx-dx*c+dy*s,y:cy-dx*s-dy*c},
{x:cx+dx*c+dy*s,y:cy+dx*s-dy*c},
];
}
const cr=rotRect(chassisMidX,chassisMidY,C.chassisW,C.chassisH,tiltRad);
setA(chassisRect,{x:Math.min(cr[0].x,cr[1].x,cr[2].x,cr[3].x),y:Math.min(cr[0].y,cr[1].y,cr[2].y,cr[3].y),width:C.chassisW,height:C.chassisH,transform:`rotate(${tiltDeg},${chassisMidX},${chassisMidY})`});
// --- 前臂 ---
const fw=wheelPos(fpx,fpy,st.faA);
setA(armPivotF,{cx:fpx,cy:fpy});
[armLineF1,armLineF2,armLineF3].forEach((l,i)=>{setA(l,{x1:fpx,y1:fpy,x2:fw[i].x,y2:fw[i].y})});
const wheelSpin=st.cx/C.wheelR*0.8;
fw.forEach((w,i)=>{
const g=wheelF[i];
setA(g,{transform:`translate(${w.x},${w.y})`});
g.querySelector('circle').setAttribute('r',C.wheelR);
const lines=g.querySelectorAll('line');
for(let s=0;s<4;s++){
const a=wheelSpin+rad(s*90);
setA(lines[s],{x1:0,y1:0,x2:C.wheelR*0.7*Math.cos(a),y2:C.wheelR*0.7*Math.sin(a)});
}
// 接地高亮
const onGround=w.y>=C.groundY-C.wheelR-3||(w.x>C.stepX&&w.y>=C.groundY-sh-C.wheelR-3);
g.querySelector('circle').setAttribute('stroke',onGround?'#e0e8f0':'#607080');
g.querySelector('circle').setAttribute('stroke-width',onGround?2.2:1.5);
});
// --- 后臂 ---
const rw=wheelPos(rpx,rpy,st.raA);
setA(armPivotR,{cx:rpx,cy:rpy});
[armLineR1,armLineR2,armLineR3].forEach((l,i)=>{setA(l,{x1:rpx,y1:rpy,x2:rw[i].x,y2:rw[i].y})});
rw.forEach((w,i)=>{
const g=wheelR[i];
setA(g,{transform:`translate(${w.x},${w.y})`});
g.querySelector('circle').setAttribute('r',C.wheelR);
const lines=g.querySelectorAll('line');
for(let s=0;s<4;s++){
const a=wheelSpin+rad(s*90);
setA(lines[s],{x1:0,y1:0,x2:C.wheelR*0.7*Math.cos(a),y2:C.wheelR*0.7*Math.sin(a)});
}
const onGround=w.y>=C.groundY-C.wheelR-3||(w.x>C.stepX&&w.y>=C.groundY-sh-C.wheelR-3);
g.querySelector('circle').setAttribute('stroke',onGround?'#e0e8f0':'#607080');
g.querySelector('circle').setAttribute('stroke-width',onGround?2.2:1.5);
});
// --- 翻转轨迹弧线 ---
if(st.faA>5&&st.faA<115){
const a1=rad(-90),a2=rad(30);
const largeArc=st.faA>60?1:0;
setA(trailArcF,{d:`M${fpx+C.armR*Math.cos(a1)},${fpy+C.armR*Math.sin(a1)} A${C.armR},${C.armR} 0 ${largeArc} 1 ${fpx+C.armR*Math.cos(a2)},${fpy+C.armR*Math.sin(a2)}`,opacity:0.3});
}else{setA(trailArcF,{opacity:0})}
if(st.raA>5&&st.raA<115){
const a1=rad(-90),a2=rad(30);
const largeArc=st.raA>60?1:0;
setA(trailArcR,{d:`M${rpx+C.armR*Math.cos(a1)},${rpy+C.armR*Math.sin(a1)} A${C.armR},${C.armR} 0 ${largeArc} 1 ${rpx+C.armR*Math.cos(a2)},${rpy+C.armR*Math.sin(a2)}`,opacity:0.3});
}else{setA(trailArcR,{opacity:0})}
// --- 云台液压缸 ---
const chTopL=rotRect(chassisMidX,chassisMidY,C.chassisW,C.chassisH,tiltRad)[1];// 左上角
const chTopR=rotRect(chassisMidX,chassisMidY,C.chassisW,C.chassisH,tiltRad)[0];// 右上角
const platBL={x:st.cx-C.platW/2,y:platY+C.platH/2};
const platBR={x:st.cx+C.platW/2,y:platY+C.platH/2};
// 左液压缸
drawCylinder(gimCylL,gimPistL,chTopL,platBL);
// 右液压缸
drawCylinder(gimCylR,gimPistR,chTopR,platBR);
// 陀螺仪
const gyroX=st.cx,gyroY=chassisMidY-C.chassisH/2-8;
setA(gyroG,{transform:`translate(${gyroX},${gyroY})`});
const gSpin=S.progress*400;
setA(gyroInner,{transform:`rotate(${gSpin})`});
// 信号线(陀螺仪到液压缸)
// --- 平台 ---
setA(platRect,{x:st.cx-C.platW/2,y:platY-C.platH/2,width:C.platW,height:C.platH});
// 水平仪
const spX=st.cx,spY=platY-C.platH/2-12;
setA(spiritBg,{x:spX-22,y:spY-6,width:44,height:12,rx:6});
setA(spiritBubble,{cx:spX,cy:spY});// 永远居中=水平
setA(spiritMark1,{x1:spX-3,y1:spY-5,x2:spX-3,y2:spY+5});
setA(spiritMark2,{x1:spX+3,y1:spY-5,x2:spX+3,y2:spY+5});
// --- 货物 ---
setA(cargoRect,{x:st.cx-C.cargoW/2,y:cargoY-C.cargoH/2,width:C.cargoW,height:C.cargoH});
cargoLabel.textContent='CARGO';
setA(cargoLabel,{x:st.cx,y:cargoY});
// --- 水平参考线 ---
setA(refLine,{x1:st.cx-200,y1:platY,x2:st.cx+200,y2:platY});
// --- 标注 ---
const aMidF=fw[0];// 顶部轮(翻转中的关键轮)
setA(lblArm,{x:fpx+55,y:fpy-30});lblArm.textContent='行星轮组';
setA(lblGimbal,{x:st.cx,y:chassisMidY-C.chassisH/2-C.gimbalH/2-4});lblGimbal.textContent='液压云台';
setA(lblPlat,{x:st.cx+C.platW/2+10,y:platY});lblPlat.textContent='载货平台';
// 阶段标签
let phase='平地行驶',phaseColor='#00e5ff';
if(S.progress>0.12&&S.progress<0.46){phase='前轮翻转跨级';phaseColor='#ff9100'}
else if(S.progress>=0.46&&S.progress<0.54){phase='云台补偿保持水平';phaseColor='#00e676'}
else if(S.progress>=0.54&&S.progress<0.88){phase='后轮翻转跨级';phaseColor='#ff9100'}
else if(S.progress>=0.88){phase='完成跨越';phaseColor='#00e676'}
setA(lblPhase,{x:50,y:40,fill:phaseColor});lblPhase.textContent=phase;
// 臂长标注
if(st.faA<5){
setA(armDimLine,{x1:fpx,y1:fpy,x2:fpx,y2:fpy-C.armR,opacity:0.5});
setA(armDimTxt,{x:fpx+18,y:fpy-C.armR/2,opacity:0.7});armDimTxt.textContent='L=25cm';
}else{setA(armDimLine,{opacity:0});setA(armDimTxt,{opacity:0})}
// 倾角弧
if(Math.abs(tiltDeg)>0.5){
const arcR=50;
const aStart=-Math.PI/2;
const aEnd=-Math.PI/2+tiltRad;
const x1=chassisMidX+arcR*Math.cos(aStart),y1=chassisMidY+arcR*Math.sin(aStart);
const x2=chassisMidX+arcR*Math.cos(aEnd),y2=chassisMidY+arcR*Math.sin(aEnd);
const large=tiltDeg>90?1:0;
setA(tiltArc,{d:`M${x1},${y1} A${arcR},${arcR} 0 ${large} 1 ${x2},${y2}`,opacity:0.6});
setA(tiltTxt,{x:chassisMidX+arcR+8,y:chassisMidY-10,opacity:0.8});tiltTxt.textContent=tiltDeg.toFixed(1)+'°';
}else{setA(tiltArc,{opacity:0});setA(tiltTxt,{opacity:0})}
// --- 仪表盘 ---
document.getElementById('dPhase').textContent=phase;
document.getElementById('dPhase').style.color=phaseColor;
document.getElementById('dTilt').textContent=tiltDeg.toFixed(1)+'°';
document.getElementById('dTilt').style.color=Math.abs(tiltDeg)>25?'#ff1744':'#ff9100';
document.getElementById('dGimbal').textContent=(-tiltDeg).toFixed(1)+'°';
document.getElementById('dFront').textContent=Math.round(st.faA)+'°';
document.getElementById('dRear').textContent=Math.round(st.raA)+'°';
}
/* ===== 绘制液压缸 ===== */
function drawCylinder(cylEl,pistEl,bottom,top){
const dx=top.x-bottom.x,dy=top.y-bottom.y;
const len=Math.sqrt(dx*dx+dy*dy);
const angle=Math.atan2(dy,dx)*180/Math.PI;
const cylW=12,cylLen=Math.max(10,len*0.55);
const pistW=6,pistLen=Math.max(5,len*0.5);
setA(cylEl,{x:bottom.x-cylLen/2*Math.cos(rad(angle)),y:bottom.y-cylLen/2*Math.sin(rad(angle))-cylW/2,width:cylLen,height:cylW,transform:`rotate(${angle},${bottom.x},${bottom.y})`});
setA(pistEl,{x:top.x-pistLen/2*Math.cos(rad(angle)),y:top.y-pistLen/2*Math.sin(rad(angle))-pistW/2,width:pistLen,height:pistW,transform:`rotate(${angle},${top.x},${top.y})`});
}
/* ===== 控制面板 ===== */
const rngStep=document.getElementById('rngStep');
const rngSpeed=document.getElementById('rngSpeed');
const btnPlay=document.getElementById('btnPlay');
const btnReset=document.getElementById('btnReset');
rngStep.addEventListener('input',()=>{
S.stepCm=+rngStep.value;
document.getElementById('valStep').textContent=S.stepCm+' cm';
updateGround();
});
rngSpeed.addEventListener('input',()=>{
S.speed=+rngSpeed.value/8;
document.getElementById('valSpeed').textContent=S.speed.toFixed(1)+'x';
});
btnPlay.addEventListener('click',()=>{
S.playing=!S.playing;
btnPlay.textContent=S.playing?'暂停':'播放';
btnPlay.classList.toggle('active',S.playing);
});
btnReset.addEventListener('click',()=>{
S.progress=0;S.playing=true;
btnPlay.textContent='暂停';btnPlay.classList.add('active');
});
/* ===== 动画循环 ===== */
function animate(time){
if(lastTime===0)lastTime=time;
const dt=(time-lastTime)/1000;
lastTime=time;
if(S.playing){
S.progress+=dt*S.speed/12;// 12秒一个完整循环
if(S.progress>1.05)S.progress=0;
if(S.progress>1)S.progress=1;// 在终点短暂停留
}
render();
requestAnimationFrame(animate);
}
/* ===== 初始化 ===== */
updateGround();
requestAnimationFrame(animate);
</script>
</body>
</html>
这段代码实现了一个完整的行星式翻转轮组与主动液压自平衡云台的高保真 SVG 原理动画,以下是关键设计与实现要点:
核心动画逻辑
- 关键帧插值系统:将整个越障过程分解为 14 个关键帧(平地→前臂翻转→过渡→后臂翻转→驶离),使用 smoothstep 平滑插值,确保运动轨迹自然
- 行星轮组:三小轮以 120° 均布于旋转支架,平地时下方两轮着地;遇台阶时支架绕中心轴翻转 120°,顶部小轮翻越成为新支撑点
- 底盘倾角:由前后枢轴点高度差实时计算
atan2(Δy, Δx),液压云台以相反角度补偿,使载货平台始终保持绝对水平
IFR 可视化引导
- 绿色水平参考线 + 水平仪气泡:始终居中的气泡直观测地传达"平台绝对水平"这一理想状态
- 橙色辉光液压缸:在翻转瞬间高亮伸缩动作,引导注意力聚焦于"平衡解耦"的核心创新
- 倾角弧线标注:底盘倾斜时实时显示角度值,与云台补偿角形成对比
- 轨迹弧线:翻转过程中绘制轮心运动轨迹,直观展示"跨步"机理
交互控制
- 台阶高度滑块(5-28cm):超过 25cm 臂长极限时显示红色越限警告,体现适用边界
- 速度滑块:0.4x ~ 2.5x 无级调速
- 实时仪表盘:显示当前阶段、底盘倾角、云台补偿角、前后臂旋转角
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
