独立渲染引擎就绪引擎就绪
<!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=Chakra+Petch:wght@400;600;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#060b16;--surface:#0c1525;--teal:#00c9a7;--teal-dim:#0a6b5a;--teal-light:#4aedc4;--amber:#ffa726;--amber-glow:#ff8f00;--red:#ff5252;--text:#c8d8e8;--text-dim:#5a7088;--stair-fill:#0e1a2e;--stair-edge:#1a3a5a}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
#app{width:100%;max-width:1440px;display:flex;flex-direction:column;align-items:center;padding:16px 12px}
header{text-align:center;margin-bottom:12px}
header h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:clamp(18px,3vw,28px);color:var(--teal);letter-spacing:2px}
header p{font-size:12px;color:var(--text-dim);letter-spacing:1px;margin-top:2px}
#svg-wrap{width:100%;max-width:1360px;aspect-ratio:16/9;border:1px solid var(--stair-edge);border-radius:8px;overflow:hidden;background:var(--bg);position:relative}
#scene{width:100%;height:100%;display:block}
#controls{display:flex;gap:20px;margin-top:14px;align-items:center;flex-wrap:wrap;justify-content:center}
.cg{display:flex;align-items:center;gap:6px}
.cg label{font-size:11px;color:var(--text-dim)}
.cg input[type=range]{-webkit-appearance:none;width:110px;height:4px;background:var(--stair-edge);border-radius:2px;outline:none}
.cg input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--teal);cursor:pointer}
.cg .v{font-size:11px;color:var(--teal);min-width:28px;text-align:center}
button{background:transparent;border:1px solid var(--teal-dim);color:var(--teal);padding:5px 14px;border-radius:4px;font-family:'IBM Plex Mono',monospace;font-size:11px;cursor:pointer;transition:.2s}
button:hover{background:var(--teal-dim);border-color:var(--teal)}
#legend{display:flex;gap:16px;margin-top:10px;flex-wrap:wrap;justify-content:center}
.li{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--text-dim)}
.ls{width:10px;height:10px;border-radius:2px}
#phase-box{position:absolute;top:12px;left:12px;background:rgba(6,11,22,.85);border:1px solid var(--stair-edge);border-radius:6px;padding:8px 14px;pointer-events:none}
#phase-box .ph{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:14px;color:var(--amber)}
#phase-box .pd{font-size:10px;color:var(--text-dim);margin-top:2px}
#ifr-box{position:absolute;bottom:12px;right:12px;background:rgba(6,11,22,.85);border:1px solid var(--teal-dim);border-radius:6px;padding:8px 14px;max-width:320px;pointer-events:none}
#ifr-box .it{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:12px;color:var(--teal);margin-bottom:3px}
#ifr-box .id{font-size:10px;color:var(--text-dim);line-height:1.5}
@media(max-width:768px){header h1{font-size:16px}#controls{gap:10px}#ifr-box{max-width:200px}}
</style>
</head>
<body>
<div id="app">
<header>
<h1>柔性铰接底盘 · 被动自适应攀爬</h1>
<p>多段万向节串联 + 柔性履带 → 不规则台阶自适应蠕动攀爬</p>
</header>
<div id="svg-wrap">
<svg id="scene" xmlns="http://www.w3.org/2000/svg"></svg>
<div id="phase-box"><div class="ph" id="phText">平地行驶</div><div class="pd" id="phDesc">各舱段保持直线</div></div>
<div id="ifr-box"><div class="it">IFR 最终理想解</div><div class="id">底盘形态随地形被动重塑,无需额外感知与控制,仅靠机械约束即可自适应不规则几何</div></div>
</div>
<div id="controls">
<div class="cg"><label>攀爬速度</label><input type="range" id="spdR" min="0.2" max="2.5" step="0.1" value="1"><span class="v" id="spdV">1.0x</span></div>
<div class="cg"><label>舱段数量</label><input type="range" id="segR" min="4" max="7" step="1" value="6"><span class="v" id="segV">6</span></div>
<button id="rstBtn">重置动画</button>
</div>
<div id="legend">
<div class="li"><div class="ls" style="background:#00c9a7"></div>舱段主体</div>
<div class="li"><div class="ls" style="background:#ffa726"></div>万向节(被动折叠)</div>
<div class="li"><div class="ls" style="background:#4aedc4"></div>柔性履带</div>
<div class="li"><div class="ls" style="background:#ff5252"></div>折叠角度指示</div>
</div>
</div>
<script>
(function(){
'use strict';
const NS='http://www.w3.org/2000/svg';
const svg=document.getElementById('scene');
/* ====== 常量 ====== */
const WH=20; // 轮子半径
const SL=78; // 舱段直线距离
const THW=24; // 履带半宽
const BHW=15; // 舱体半宽
const TSPACE=10; // 履带齿间距
const TDEPTH=5; // 防滑齿深度
/* ====== 颜色 ====== */
const C={teal:'#00c9a7',tealD:'#0a6b5a',tealL:'#4aedc4',amber:'#ffa726',amberG:'#ff8f00',red:'#ff5252',txt:'#c8d8e8',txtD:'#5a7088',sFill:'#0e1a2e',sEdge:'#1a3a5a'};
/* ====== 楼梯轮廓 ====== */
const SP=[
{x:-500,y:580},{x:380,y:580},
{x:380,y:518},{x:475,y:518}, // 阶1: 62升 95深
{x:475,y:448},{x:545,y:448}, // 阶2: 70升 70深(最陡)
{x:545,y:390},{x:645,y:390}, // 阶3: 58升 100深
{x:645,y:335},{x:770,y:335}, // 阶4: 55升 125深 → 顶部平台
{x:2500,y:335}
];
/* 预计算累积弧长 */
const SD=[0];
for(let i=1;i<SP.length;i++){const dx=SP[i].x-SP[i-1].x,dy=SP[i].y-SP[i-1].y;SD.push(SD[i-1]+Math.sqrt(dx*dx+dy*dy))}
const TPD=SD[SD.length-1];
function gp(d){
d=Math.max(0,Math.min(d,TPD));
for(let i=1;i<SP.length;i++){
if(d<=SD[i]){
const sl=SD[i]-SD[i-1],t=sl>0?(d-SD[i-1])/sl:0;
const a=SP[i-1],b=SP[i];
return{x:a.x+(b.x-a.x)*t,y:a.y+(b.y-a.y)*t,a:Math.atan2(b.y-a.y,b.x-a.x),pd:d};
}
}
const l=SP[SP.length-1];return{x:l.x,y:l.y,a:0,pd:TPD};
}
/* 直线距离找轮廓点 */
function fp(pt,fromD,td){
let ed=fromD+td;
for(let it=0;it<20;it++){
const p=gp(ed);
const dx=p.x-pt.x,dy=p.y-pt.y,ad=Math.sqrt(dx*dx+dy*dy);
if(Math.abs(ad-td)<0.3)break;
const r=ad>0?td/ad:1;
ed=fromD+(ed-fromD)*r;
}
return gp(ed);
}
/* ====== SVG 辅助 ====== */
function ce(t,a){const e=document.createElementNS(NS,t);if(a)for(const[k,v]of Object.entries(a))e.setAttribute(k,v);return e}
/* ====== 状态 ====== */
let spd=1,nSeg=6,bDist=0,tDist=0;
let camX=200,camY=460;
let lastT=null;
/* ====== 构建 SVG 静态结构 ====== */
const defs=ce('defs');svg.appendChild(defs);
/* 光晕滤镜 */
function mkGlow(id,sd){const f=ce('filter',{id,x:'-50%',y:'-50%',width:'200%',height:'200%'});f.appendChild(ce('feGaussianBlur',{stdDeviation:String(sd),result:'b'}));const m=ce('feMerge');m.appendChild(ce('feMergeNode',{in:'b'}));m.appendChild(ce('feMergeNode',{in:'SourceGraphic'}));f.appendChild(m);return f}
defs.appendChild(mkGlow('glow',4));
defs.appendChild(mkGlow('sglow',8));
/* 网格点阵 */
const gp2=ce('pattern',{id:'dot',width:40,height:40,patternUnits:'userSpaceOnUse'});
gp2.appendChild(ce('circle',{cx:1,cy:1,r:.6,fill:'#141e32'}));defs.appendChild(gp2);
/* 渐变 - 楼梯填充 */
const sg=ce('linearGradient',{id:'stG',x1:'0',y1:'0',x2:'0',y2:'1'});
sg.appendChild(ce('stop',{offset:'0%','stop-color':'#14233a'}));
sg.appendChild(ce('stop',{offset:'100%','stop-color':'#0a1422'}));defs.appendChild(sg);
/* 场景组 */
const scG=ce('g');svg.appendChild(scG);
/* 背景 */
scG.appendChild(ce('rect',{x:-1500,y:-600,width:5000,height:2400,fill:'url(#dot)'}));
/* 楼梯组 */
const stG=ce('g');scG.appendChild(stG);
function drawStairs(){
stG.innerHTML='';
/* 实体填充 */
let pts='';
for(const p of SP)pts+=p.x+','+p.y+' ';
pts+=SP[SP.length-1].x+','+SP[0].y+' '+SP[0].x+','+SP[0].y;
stG.appendChild(ce('polygon',{points:pts,fill:'url(#stG)'}));
/* 每段边线 */
for(let i=0;i<SP.length-1;i++){
const a=SP[i],b=SP[i+1];
const isR=Math.abs(a.x-b.x)<2;
const isH=Math.abs(a.y-b.y)<2;
if(isR){
/* 竖线-虚线 */
stG.appendChild(ce('line',{x1:a.x,y1:a.y,x2:b.x,y2:b.y,stroke:'#1e4a6a','stroke-width':1,'stroke-dasharray':'5,4'}));
} else if(isH && a.y<580){
/* 水平踏面-实线高亮 */
stG.appendChild(ce('line',{x1:a.x,y1:a.y,x2:b.x,y2:b.y,stroke:C.tealD,'stroke-width':2}));
}
}
/* 台阶序号 */
const stepTreads=[
{x:427,y:508,num:1,h:62,d:95},
{x:510,y:438,num:2,h:70,d:70},
{x:595,y:380,num:3,h:58,d:100},
{x:707,y:325,num:4,h:55,d:125}
];
for(const s of stepTreads){
const t=ce('text',{x:s.x,y:s.y,fill:'#2a4a6a','font-size':'11','font-family':'IBM Plex Mono,monospace','text-anchor':'middle','dominant-baseline':'central',opacity:.6});
t.textContent='#'+s.num;t.appendChild(ce('title'));t.querySelector('title').textContent=`阶${s.num}: 升${s.h} 深${s.d}`;
stG.appendChild(t);
}
}
drawStairs();
/* 机器人组 */
const rG=ce('g');scG.appendChild(rG);
const trkP=ce('path',{fill:'#082e26',stroke:C.tealD,'stroke-width':1,opacity:.85});rG.appendChild(trkP);
const trkS=ce('path',{fill:'none',stroke:C.tealL,'stroke-width':2.5,opacity:.5});rG.appendChild(trkS);
const tdG=ce('g');rG.appendChild(tdG);
const sg2=ce('g');rG.appendChild(sg2);
const wG=ce('g');rG.appendChild(wG);
const jG=ce('g');rG.appendChild(jG);
const aG=ce('g');rG.appendChild(aG);
const anG=ce('g');rG.appendChild(anG);
/* ====== 渲染 ====== */
function render(ws,sa,jb){
const n=ws.length;
/* -- 履带轮廓 -- */
const op=[];const AS=10;
for(let i=0;i<n;i++){
let ang;
if(i===0)ang=sa[0];else if(i===n-1)ang=sa[sa.length-1];else ang=(sa[i-1]+sa[i])/2;
op.push({x:ws[i].x+THW*Math.sin(ang),y:ws[i].y-THW*Math.cos(ang)});
}
/* 前轮弧 */
const fa=sa[sa.length-1],fc=ws[n-1];
for(let s=1;s<=AS;s++){const t=s/AS,a2=(fa-Math.PI/2)+t*Math.PI;op.push({x:fc.x+THW*Math.cos(a2),y:fc.y+THW*Math.sin(a2)})}
/* 底边(地面侧)从前到后 */
for(let i=n-1;i>=0;i--){
let ang;
if(i===0)ang=sa[0];else if(i===n-1)ang=sa[sa.length-1];else ang=(sa[i-1]+sa[i])/2;
op.push({x:ws[i].x-THW*Math.sin(ang),y:ws[i].y+THW*Math.cos(ang)});
}
/* 后轮弧 */
const ra=sa[0],rc=ws[0];
for(let s=1;s<=AS;s++){const t=s/AS,a2=(ra+Math.PI/2)+t*Math.PI;op.push({x:rc.x+THW*Math.cos(a2),y:rc.y+THW*Math.sin(a2)})}
let td=`M${op[0].x} ${op[0].y}`;
for(let i=1;i<op.length;i++)td+=` L${op[i].x} ${op[i].y}`;
td+='Z';trkP.setAttribute('d',td);
/* -- 履带地面侧线 + 防滑齿动画 -- */
let sd2='';
for(let i=n-1;i>=0;i--){
let ang;if(i===0)ang=sa[0];else if(i===n-1)ang=sa[sa.length-1];else ang=(sa[i-1]+sa[i])/2;
const px=ws[i].x-THW*Math.sin(ang),py=ws[i].y+THW*Math.cos(ang);
sd2+=(i===n-1?'M':'L')+px+' '+py+' ';
}
trkS.setAttribute('d',sd2);
trkS.setAttribute('stroke-dasharray',TSPACE+' '+(TSPACE));
trkS.setAttribute('stroke-dashoffset',String(tDist%TSPACE));
/* -- 防滑齿刻痕(地面侧外围) -- */
tdG.innerHTML='';
for(let i=0;i<sa.length;i++){
const p0=ws[i],p1=ws[i+1];
const dx=p1.x-p0.x,dy=p1.y-p0.y;
const sl=Math.sqrt(dx*dx+dy*dy);
const ang=sa[i];
const nx=-Math.sin(ang),ny=Math.cos(ang);
const dirX=Math.cos(ang),dirY=Math.sin(ang);
const cnt=Math.floor(sl/TSPACE);
const off=((tDist%TSPACE)+TSPACE)%TSPACE;
for(let t=0;t<cnt+1;t++){
const d2=off+t*TSPACE;
if(d2>sl||d2<0)continue;
const fr=d2/sl;
const cx=p0.x+dx*fr,cy=p0.y+dy*fr;
const hw=4;
const x1=cx+hw*dirX+(THW+TDEPTH)*nx,y1=cy+hw*dirY+(THW+TDEPTH)*ny;
const x2=cx-hw*dirX+(THW+TDEPTH)*nx,y2=cy-hw*dirY+(THW+TDEPTH)*ny;
tdG.appendChild(ce('line',{x1,y1,x2,y2,stroke:C.tealL,'stroke-width':1.2,opacity:.35}));
}
}
/* -- 舱段主体 -- */
sg2.innerHTML='';
for(let i=0;i<sa.length;i++){
const p0=ws[i],p1=ws[i+1];
const cx2=(p0.x+p1.x)/2,cy2=(p0.y+p1.y)/2;
const dx=p1.x-p0.x,dy=p1.y-p0.y;
const sl=Math.sqrt(dx*dx+dy*dy);
const ang=sa[i]*180/Math.PI;
const mg=10;
sg2.appendChild(ce('rect',{x:cx2-sl/2+mg,y:cy2-BHW,width:sl-mg*2,height:BHW*2,rx:4,fill:C.teal,opacity:.25,transform:`rotate(${ang},${cx2},${cy2})`}));
sg2.appendChild(ce('rect',{x:cx2-sl/2+mg,y:cy2-BHW,width:sl-mg*2,height:BHW*2,rx:4,fill:'none',stroke:C.teal,'stroke-width':1,opacity:.55,transform:`rotate(${ang},${cx2},${cy2})`}));
/* 电机标识小圆 */
const emx=cx2,emy=cy2;
sg2.appendChild(ce('circle',{cx:emx,cy:emy,r:3,fill:C.tealD,transform:`rotate(${ang},${cx2},${cy2})`}));
}
/* -- 轮子 -- */
wG.innerHTML='';
for(let i=0;i<n;i++){
wG.appendChild(ce('circle',{cx:ws[i].x,cy:ws[i].y,r:WH,fill:'#070e18',stroke:C.tealD,'stroke-width':1.5}));
wG.appendChild(ce('circle',{cx:ws[i].x,cy:ws[i].y,r:4,fill:C.tealD}));
/* 轮辐动画偏移 */
const spokeOff=(tDist*0.8)%20;
for(let s=0;s<4;s++){
const sa2=s*Math.PI/2+spokeOff*0.05;
wG.appendChild(ce('line',{x1:ws[i].x+6*Math.cos(sa2),y1:ws[i].y+6*Math.sin(sa2),x2:ws[i].x+(WH-3)*Math.cos(sa2),y2:ws[i].y+(WH-3)*Math.sin(sa2),stroke:'#0f2a3a','stroke-width':1}));
}
}
/* -- 万向节 -- */
jG.innerHTML='';
for(let i=1;i<n-1;i++){
const ba=jb[i-1],ab=Math.abs(ba),isA=ab>0.08;
const r=isA?8:5;
const col=isA?C.amber:C.amberG;
const fi=isA?'url(#sglow)':'none';
const op2=isA?1:.35;
jG.appendChild(ce('circle',{cx:ws[i].x,cy:ws[i].y,r,fill:col,opacity:op2,filter:fi}));
if(isA){
/* 外圈脉动 */
const pulse=Math.sin(Date.now()*0.006)*0.3+0.5;
jG.appendChild(ce('circle',{cx:ws[i].x,cy:ws[i].y,r:r+6,fill:'none',stroke:C.amber,'stroke-width':1,opacity:pulse*0.4}));
}
}
/* -- 折叠角度指示弧 -- */
aG.innerHTML='';
for(let i=0;i<jb.length;i++){
const ba=jb[i],ab=Math.abs(ba);
if(ab<0.1)continue;
const idx=i+1;
const a1=sa[i],a2=sa[i+1];
const cx2=ws[idx].x,cy2=ws[idx].y,ir=32;
const x1=cx2+ir*Math.cos(a1),y1=cy2+ir*Math.sin(a1);
const x2=cx2+ir*Math.cos(a2),y2=cy2+ir*Math.sin(a2);
const la=ab>Math.PI?1:0;
const sw=ba<0?1:0;
const op2=Math.min(1,ab/0.4);
aG.appendChild(ce('path',{d:`M${x1} ${y1} A${ir} ${ir} 0 ${la} ${sw} ${x2} ${y2}`,fill:'none',stroke:C.red,'stroke-width':2,opacity:op2.toFixed(2)}));
const deg=Math.round(ab*180/Math.PI);
const ma=(a1+a2)/2,tr=ir+16;
const tx=cx2+tr*Math.cos(ma),ty=cy2+tr*Math.sin(ma);
const at=ce('text',{x:tx,y:ty,fill:C.red,'font-size':'11','font-family':'IBM Plex Mono,monospace','text-anchor':'middle','dominant-baseline':'central',opacity:op2.toFixed(2)});
at.textContent=deg+'°';aG.appendChild(at);
/* 45°极限标记 */
if(ab>0.6){
const lt=ce('text',{x:tx,y:ty+13,fill:C.red,'font-size':'9','font-family':'IBM Plex Mono,monospace','text-anchor':'middle',opacity:(op2*0.6).toFixed(2)});
lt.textContent='≤45°';aG.appendChild(lt);
}
}
/* -- 标注 -- */
anG.innerHTML='';
/* 找最大弯折关节 */
let mxI=-1,mxV=0;
for(let i=0;i<jb.length;i++){if(Math.abs(jb[i])>mxV){mxV=Math.abs(jb[i]);mxI=i}}
if(mxI>=0&&mxV>0.15){
const jw=ws[mxI+1];
const ly=jw.y-60;
anG.appendChild(ce('line',{x1:jw.x,y1:jw.y-12,x2:jw.x,y2:ly+14,stroke:C.amber,'stroke-width':1,'stroke-dasharray':'3,3',opacity:.6}));
const t1=ce('text',{x:jw.x,y:ly,fill:C.amber,'font-size':'13','font-family':'Chakra Petch,sans-serif','font-weight':600,'text-anchor':'middle'});
t1.textContent='被动折叠';anG.appendChild(t1);
const t2=ce('text',{x:jw.x,y:ly+15,fill:C.txtD,'font-size':'9','font-family':'IBM Plex Mono,monospace','text-anchor':'middle'});
t2.textContent='受阻反力 → 万向节弯折';anG.appendChild(t2);
}
/* 柔性履带标注 */
const mi=Math.floor(nSeg/2);
const mw=ws[mi],mang=sa[mi]||0;
const tlx=mw.x+(THW+18)*(-Math.sin(mang)),tly=mw.y+(THW+18)*(Math.cos(mang));
const tl=ce('text',{x:tlx,y:tly,fill:C.tealL,'font-size':'10','font-family':'IBM Plex Mono,monospace','text-anchor':'middle',opacity:.55});
tl.textContent='柔性履带';anG.appendChild(tl);
/* 独立电机标注 */
const ei=Math.floor(nSeg/2)-1;
if(ei>=0&&ei<sa.length){
const ew=ws[ei],ew2=ws[ei+1];
const ecx=(ew.x+ew2.x)/2,ecy=(ew.y+ew2.y)/2;
const eang=sa[ei];
const emx2=ecx+(BHW+14)*Math.sin(eang),emy2=ecy-(BHW+14)*Math.cos(eang);
const et=ce('text',{x:emx2,y:emy2,fill:C.teal,'font-size':'9','font-family':'IBM Plex Mono,monospace','text-anchor':'middle',opacity:.5});
et.textContent='独立电机';anG.appendChild(et);
}
/* 接触力箭头 */
for(let i=0;i<n;i++){
const w=ws[i];
/* 检测是否在竖直面(台阶立面)附近 */
for(let si=0;si<SP.length-1;si++){
const a2=SP[si],b=SP[si+1];
if(Math.abs(a2.x-b.x)<2&&a2.y>b.y){
/* 竖直段 */
const ry=b.y+((a2.y-b.y)*(w.y-b.y))/(a2.y-b.y);
if(Math.abs(w.x-a2.x)<THW+5&&w.y>=b.y-5&&w.y<=a2.y+5){
/* 轮子在此立面附近,画接触力 */
const fx=a2.x-5,fy=w.y;
anG.appendChild(ce('line',{x1:fx+18,y1:fy,x2:fx,y2:fy,stroke:C.amberG,'stroke-width':1.5,opacity:.4}));
/* 箭头头 */
anG.appendChild(ce('polygon',{points:`${fx},${fy} ${fx+5},${fy-3} ${fx+5},${fy+3}`,fill:C.amberG,opacity:.4}));
}
}
}
}
}
/* ====== 相机 ====== */
function cam(ws,dt){
const n=ws.length;
const tx=(ws[0].x+ws[n-1].x)/2;
const ty=(ws[0].y+ws[n-1].y)/2;
const sm=1-Math.exp(-3*dt);
camX+=(tx-camX)*sm;
camY+=(ty-camY)*sm;
const vw=1200,vh=675;
svg.setAttribute('viewBox',`${camX-vw/2+50} ${camY-vh/2+70} ${vw} ${vh}`);
}
/* ====== 阶段指示 ====== */
function phase(jb){
const mx=Math.max(...jb.map(Math.abs));
const phT=document.getElementById('phText');
const phD=document.getElementById('phDesc');
if(mx<0.08){phT.textContent='平地行驶';phD.textContent='各舱段保持直线,履带匀速卷动';phT.style.color=C.teal}
else if(mx>0.6){phT.textContent='临界折叠';phD.textContent='关节接近 45° 极限,铰接受力最大';phT.style.color=C.red}
else{phT.textContent='被动折叠攀爬';phD.textContent='前端受阻 → 万向节被动弯折 → 贴合台阶轮廓';phT.style.color=C.amber}
}
/* ====== 主循环 ====== */
function loop(ts){
if(lastT===null)lastT=ts;
const dt=Math.min((ts-lastT)/1000,0.05);
lastT=ts;
const mv=85*spd;
bDist+=mv*dt;
tDist+=mv*dt;
const maxD=TPD-(nSeg+1)*SL-80;
if(bDist>maxD){bDist=-200;tDist=0}
/* 计算轮位 */
const ws=[];
let pp=gp(bDist);ws.push(pp);
for(let i=1;i<=nSeg;i++){
const np=fp(pp,i===1?bDist:ws[i-1].pd,SL);
ws.push(np);pp=np;
}
/* 舱段角度 */
const sa=[];
for(let i=0;i<nSeg;i++){const dx=ws[i+1].x-ws[i].x,dy=ws[i+1].y-ws[i].y;sa.push(Math.atan2(dy,dx))}
/* 关节弯折角 */
const jb=[];
for(let i=1;i<nSeg;i++){let b=sa[i]-sa[i-1];while(b>Math.PI)b-=2*Math.PI;while(b<-Math.PI)b+=2*Math.PI;jb.push(b)}
render(ws,sa,jb);
cam(ws,dt);
phase(jb);
requestAnimationFrame(loop);
}
/* ====== 控件 ====== */
document.getElementById('spdR').addEventListener('input',function(){spd=parseFloat(this.value);document.getElementById('spdV').textContent=spd.toFixed(1)+'x'});
document.getElementById('segR').addEventListener('input',function(){nSeg=parseInt(this.value);document.getElementById('segV').textContent=nSeg});
document.getElementById('rstBtn').addEventListener('click',function(){bDist=-200;tDist=0;lastT=null;camX=200;camY=460});
/* ====== 启动 ====== */
bDist=-200;
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
这个动画实现了以下核心内容:
原理展示
- 6 段(可调 4-7 段)铰接舱段沿不规则台阶轮廓被动弯折,完整还原"毛毛虫式蠕动攀爬"过程
- 万向节在接触台阶立面时自动高亮为琥珀色并带脉动光晕,标注"被动折叠"及受阻反力原理
- 红色弧线实时指示每个关节的折叠角度,接近 45° 极限时显示"≤45°"警示
IFR 理想解聚焦
- 无对比组,直接展示柔性底盘在不规则台阶上的自适应运作——底盘形态随地形被动重塑,无需额外感知与控制
- 利用现有资源(重力恢复平直、台阶反力驱动折叠、履带摩擦提供前进力),极低系统复杂度解决矛盾
- 右下角 IFR 原则框持续提示核心思想
交互与动态
- 速度滑块控制攀爬速率,舱段数量滑块改变构型(4-7 段),重置按钮回退动画
- 履带防滑齿随行驶距离实时滚动,轮辐同步旋转
- 相机带阻尼平滑跟随机器人,自动适配攀爬全过程
- 左上角阶段指示器实时切换"平地行驶 / 被动折叠攀爬 / 临界折叠"三种状态
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
