分享图
动画工坊
引擎就绪
<!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分