分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<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=Syne:wght@400;600;700;800&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#060a12;--bg2:#0c1220;--fg:#d8e0ec;--muted:#556880;--accent:#f5a623;--cool:#06d6a0;--cyan:#06b6d4;--skin:rgba(180,195,215,0.12);--skin-stroke:rgba(180,195,215,0.28);--muscle-off:#1e2a3c;--muscle-off-s:#2e3e55;--friction:#8b5e3c}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Fira Code',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
.wrap{width:100%;max-width:1500px;padding:18px 16px 24px}
.hdr{text-align:center;margin-bottom:10px}
.hdr h1{font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(1.3rem,2.6vw,2rem);letter-spacing:-.02em;background:linear-gradient(120deg,var(--cool),var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hdr p{font-size:.78rem;color:var(--muted);margin-top:3px}
.svg-box{width:100%;display:flex;justify-content:center}
.svg-box svg{width:100%;max-width:1500px;height:auto;display:block}
.ctrls{display:flex;gap:28px;justify-content:center;align-items:center;padding:14px 22px;background:var(--bg2);border-radius:10px;border:1px solid rgba(255,255,255,.05);margin-top:12px;flex-wrap:wrap}
.cg{display:flex;align-items:center;gap:9px}
.cg label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap}
.cg input[type=range]{-webkit-appearance:none;width:130px;height:4px;background:rgba(255,255,255,.08);border-radius:2px;outline:none}
.cg input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;background:var(--cyan);border-radius:50%;cursor:pointer;box-shadow:0 0 7px rgba(6,182,212,.45)}
.cv{font-size:.78rem;color:var(--cyan);min-width:56px;text-align:right}
.leg{display:flex;gap:20px;justify-content:center;margin-top:10px;flex-wrap:wrap}
.li{display:flex;align-items:center;gap:7px;font-size:.68rem;color:var(--muted)}
.ls{width:11px;height:11px;border-radius:3px;flex-shrink:0}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
<body>
<div class="wrap">
  <header class="hdr">
    <h1>气动人工肌肉驱动 · 蛇形机器人</h1>
    <p>McKibben 驱动器 + 弹性硅胶骨架 — 连续蜿蜒波传播原理 (IFR 视角)</p>
  </header>
  <div class="svg-box">
    <svg id="svg" viewBox="0 0 1400 740" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
  <div class="ctrls">
    <div class="cg"><label>波速</label><input type="range" id="rSpd" min="0.2" max="3" step="0.1" value="1"><span class="cv" id="vSpd">1.0x</span></div>
    <div class="cg"><label>气压</label><input type="range" id="rPrs" min="0.1" max="0.6" step="0.02" value="0.4"><span class="cv" id="vPrs">0.40 MPa</span></div>
    <div class="cg"><label>振幅</label><input type="range" id="rAmp" min="0.08" max="0.55" step="0.01" value="0.32"><span class="cv" id="vAmp">0.32 rad</span></div>
  </div>
  <div class="leg">
    <div class="li"><div class="ls" style="background:#06d6a0"></div>弹性硅胶骨架</div>
    <div class="li"><div class="ls" style="background:#f5a623"></div>充气肌肉 (缩短)</div>
    <div class="li"><div class="ls" style="background:#1e2a3c"></div>放气肌肉 (伸展)</div>
    <div class="li"><div class="ls" style="background:rgba(180,195,215,.25);border:1px solid rgba(180,195,215,.45)"></div>气密蒙皮</div>
    <div class="li"><div class="ls" style="background:#8b5e3c"></div>单向摩擦棱</div>
  </div>
</div>
<script>
document.addEventListener('DOMContentLoaded',()=>{
/* ====== 常量 ====== */
const NS='http://www.w3.org/2000/svg';
const N=14, SL=34, SX=130, SY=270;
const BH=20, MO=13, MBL=25, MBW=7;
const PHASE=Math.PI/2.5;
let spd=1, prs=0.4, amp=0.32, t=0, lt=null;

/* ====== 辅助函数 ====== */
const el=(tag)=>document.createElementNS(NS,tag);
const sat=(e,o)=>{for(const k in o)e.setAttribute(k,o[k]);return e};
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));

function muscleColor(inf){
  const r=Math.round(30+(245-30)*inf);
  const g=Math.round(42+(166-42)*inf);
  const b=Math.round(60+(35-60)*inf);
  return`rgb(${r},${g},${b})`;
}

/* 圆角矩形路径(局部坐标,再旋转平移) */
function rrect(cx,cy,hw,hh,rad,ang){
  const c=Math.cos(ang),s=Math.sin(ang);
  const rot=(lx,ly)=>({x:cx+lx*c-ly*s,y:cy+lx*s+ly*c});
  rad=Math.min(rad,hw,hh);
  const p=[
    rot(-hw+rad,-hh),rot(hw-rad,-hh),
    rot(hw,-hh+rad),rot(hw,hh-rad),
    rot(hw-rad,hh),rot(-hw+rad,hh),
    rot(-hw,hh-rad),rot(-hw,-hh+rad)
  ];
  const q=[
    rot(hw,-hh),rot(hw,hh),rot(-hw,hh),rot(-hw,-hh)
  ];
  let d=`M${p[0].x} ${p[0].y}`;
  d+=`L${p[1].x} ${p[1].y}Q${q[0].x} ${q[0].y} ${p[2].x} ${p[2].y}`;
  d+=`L${p[3].x} ${p[3].y}Q${q[1].x} ${q[1].y} ${p[4].x} ${p[4].y}`;
  d+=`L${p[5].x} ${p[5].y}Q${q[2].x} ${q[2].y} ${p[6].x} ${p[6].y}`;
  d+=`L${p[7].x} ${p[7].y}Q${q[3].x} ${q[3].y} ${p[0].x} ${p[0].y}Z`;
  return d;
}

/* ====== SVG 根节点 ====== */
const svg=document.getElementById('svg');

/* ====== Defs ====== */
const defs=el('defs');svg.appendChild(defs);

// 肌肉发光滤镜
const gf=el('filter');sat(gf,{id:'mGlow',x:'-60%',y:'-60%',width:'220%',height:'220%'});
const gb=el('feGaussianBlur');sat(gb,{stdDeviation:'5',result:'b'});gf.appendChild(gb);
const fm=el('feMerge');const n1=el('feMergeNode');n1.setAttribute('in','b');fm.appendChild(n1);
const n2=el('feMergeNode');n2.setAttribute('in','SourceGraphic');fm.appendChild(n2);gf.appendChild(fm);
defs.appendChild(gf);

// 骨架发光
const sf=el('filter');sat(sf,{id:'sGlow',x:'-40%',y:'-40%',width:'180%',height:'180%'});
const sb=el('feGaussianBlur');sat(sb,{stdDeviation:'4',result:'b'});sf.appendChild(sb);
const sm=el('feMerge');const s1=el('feMergeNode');s1.setAttribute('in','b');sm.appendChild(s1);
const s2=el('feMergeNode');s2.setAttribute('in','SourceGraphic');sm.appendChild(s2);sf.appendChild(sm);
defs.appendChild(sf);

// 小发光
const sgf=el('filter');sat(sgf,{id:'smGlow',x:'-50%',y:'-50%',width:'200%',height:'200%'});
const sgb=el('feGaussianBlur');sat(sgb,{stdDeviation:'2.5',result:'b'});sgf.appendChild(sgb);
const sgm=el('feMerge');const sg1=el('feMergeNode');sg1.setAttribute('in','b');sgm.appendChild(sg1);
const sg2=el('feMergeNode');sg2.setAttribute('in','SourceGraphic');sgm.appendChild(sg2);sgf.appendChild(sgm);
defs.appendChild(sgf);

/* ====== 背景网格 ====== */
const bgG=el('g');bgG.setAttribute('opacity','0.12');svg.appendChild(bgG);
for(let x=0;x<=1400;x+=40){const l=el('line');sat(l,{x1:x,y1:0,x2:x,y2:740,stroke:'#15243d','stroke-width':'0.5'});bgG.appendChild(l)}
for(let y=0;y<=740;y+=40){const l=el('line');sat(l,{x1:0,y1:y,x2:1400,y2:y,stroke:'#15243d','stroke-width':'0.5'});bgG.appendChild(l)}

/* ====== 蛇体元素组 ====== */
const skinP=el('path');svg.appendChild(skinP);
const skinO=el('path');svg.appendChild(skinO);
const mG=el('g');svg.appendChild(mG);
const spP=el('path');svg.appendChild(spP);
const frG=el('g');svg.appendChild(frG);
const afG=el('g');svg.appendChild(afG);
const headG=el('g');svg.appendChild(headG);

/* 肌肉元素 */
const LM=[],RM=[];
for(let i=0;i<N;i++){
  const lp=el('path');mG.appendChild(lp);LM.push(lp);
  const rp=el('path');mG.appendChild(rp);RM.push(rp);
}

/* 摩擦棱 */
const FR=[];
for(let i=0;i<N*2;i++){const p=el('polygon');frG.appendChild(p);FR.push(p)}

/* 气流箭头 */
const AF=[];
for(let i=0;i<N;i++){const p=el('path');afG.appendChild(p);AF.push(p)}

/* 蛇头装饰 */
const headL=el('circle');headG.appendChild(headL);
const headR=el('circle');headG.appendChild(headR);

/* ====== 截面图 ====== */
const csG=el('g');svg.appendChild(csG);
const csX=1200,csY=210;

// 背景
sat(el('rect'),{x:csX-125,y:csY-135,width:250,height:310,rx:8,fill:'rgba(10,16,28,.85)',stroke:'rgba(255,255,255,.06)'}).forEach?v=>{}:0;
const csBg=el('rect');sat(csBg,{x:csX-125,y:csY-135,width:250,height:310,rx:8,fill:'rgba(10,16,28,.85)',stroke:'rgba(255,255,255,.06)','stroke-width':'1'});csG.appendChild(csBg);
// 标题
const csT=el('text');sat(csT,{x:csX,y:csY-110,'text-anchor':'middle',fill:'#7888a0','font-size':'11','font-family':'Fira Code,monospace'});csT.textContent='截面示意 (第8节段)';csG.appendChild(csT);
// 蒙皮圆
const csSkin=el('circle');sat(csSkin,{cx:csX,cy:csY,r:78,fill:'rgba(180,195,215,.04)',stroke:'rgba(180,195,215,.22)','stroke-width':'2','stroke-dasharray':'6 4'});csG.appendChild(csSkin);
// 骨架圆
const csSp=el('circle');sat(csSp,{cx:csX,cy:csY,r:11,fill:'rgba(6,214,160,.2)',stroke:'#06d6a0','stroke-width':'2'});csG.appendChild(csSp);
// 左肌肉
const csLM=[];for(let j=0;j<2;j++){
  const a=Math.PI+(j-0.5)*0.55;
  const mx=csX+48*Math.cos(a),my=csY+48*Math.sin(a);
  const e=el('ellipse');sat(e,{cx:mx,cy:my,rx:15,ry:9,fill:'rgba(30,42,60,.5)',stroke:'#2e3e55','stroke-width':'1.5',transform:`rotate(${a*180/Math.PI},${mx},${my})`});csG.appendChild(e);csLM.push(e);
}
// 右肌肉
const csRM=[];for(let j=0;j<2;j++){
  const a=(j-0.5)*0.55;
  const mx=csX+48*Math.cos(a),my=csY+48*Math.sin(a);
  const e=el('ellipse');sat(e,{cx:mx,cy:my,rx:15,ry:9,fill:'rgba(30,42,60,.5)',stroke:'#2e3e55','stroke-width':'1.5',transform:`rotate(${a*180/Math.PI},${mx},${my})`});csG.appendChild(e);csRM.push(e);
}
// 摩擦棱
const csFR=el('polygon');sat(csFR,{points:`${csX-14},${csY+73} ${csX},${csY+88} ${csX+14},${csY+73}`,fill:'#8b5e3c',opacity:'0.7'});csG.appendChild(csFR);
// 标签
const csLabels=[
  {t:'硅胶骨架',x:csX,y:csY+3,a:'middle',s:8,c:'#06d6a0'},
  {t:'左肌肉',x:csX-72,y:csY+2,a:'end',s:9,c:'#f5a623'},
  {t:'右肌肉',x:csX+72,y:csY+2,a:'start',s:9,c:'#556880'},
  {t:'蒙皮',x:csX+88,y:csY-20,a:'start',s:9,c:'#7888a0'},
  {t:'摩擦棱',x:csX,y:csY+102,a:'middle',s:9,c:'#8b5e3c'}
];
csLabels.forEach(l=>{const tx=el('text');sat(tx,{x:l.x,y:l.y,'text-anchor':l.a,fill:l.c,'font-size':l.s,'font-family':'Fira Code,monospace'});tx.textContent=l.t;csG.appendChild(tx)});
// 连线
sat(el('line'),{x1:csX+84,y1:csY-20,x2:csX+74,y2:csY-12,stroke:'#7888a0','stroke-width':'0.7'});csG.appendChild(el('line'));
const csLine=el('line');sat(csLine,{x1:csX+84,y1:csY-20,x2:csX+74,y2:csY-12,stroke:'#7888a0','stroke-width':'0.7'});csG.appendChild(csLine);
// 动态状态文本
const csState=el('text');sat(csState,{x:csX,y:csY+145,'text-anchor':'middle',fill:'#f5a623','font-size':'10','font-family':'Fira Code,monospace','font-weight':'500'});csState.textContent='—';csG.appendChild(csState);

/* ====== 相位指示器 ====== */
const phG=el('g');svg.appendChild(phG);
const phY=510;
const phLLabel=el('text');sat(phLLabel,{x:80,y:phY-14,'text-anchor':'middle',fill:'#7888a0','font-size':'10','font-family':'Fira Code,monospace'});phLLabel.textContent='左 侧 肌 肉 激 活 相 位';phG.appendChild(phLLabel);
const phRLabel=el('text');sat(phRLabel,{x:80,y:phY+46,'text-anchor':'middle',fill:'#7888a0','font-size':'10','font-family':'Fira Code,monospace'});phRLabel.textContent='右 侧 肌 肉 激 活 相 位';phG.appendChild(phRLabel);
const phLC=[],phRC=[];
for(let i=0;i<N;i++){
  const cx=170+i*48;
  const lc=el('circle');sat(lc,{cx,cy:phY,r:10,fill:'#1e2a3c',stroke:'#2e3e55','stroke-width':'1'});phG.appendChild(lc);phLC.push(lc);
  const rc=el('circle');sat(rc,{cx,cy:phY+32,r:10,fill:'#1e2a3c',stroke:'#2e3e55','stroke-width':'1'});phG.appendChild(rc);phRC.push(rc);
  const sn=el('text');sat(sn,{x:cx,y:phY+4,'text-anchor':'middle',fill:'#556880','font-size':'7','font-family':'Fira Code,monospace'});sn.textContent=i+1;phG.appendChild(sn);
  const sn2=el('text');sat(sn2,{x:cx,y:phY+36,'text-anchor':'middle',fill:'#556880','font-size':'7','font-family':'Fira Code,monospace'});sn2.textContent=i+1;phG.appendChild(sn2);
}
// 传播方向箭头
const phArr=el('text');sat(phArr,{x:880,y:phY+18,'text-anchor':'start',fill:'#06b6d4','font-size':'12','font-family':'Fira Code,monospace'});phArr.textContent='→ 行波方向';phG.appendChild(phArr);

/* ====== IFR 注释 ====== */
const anG=el('g');svg.appendChild(anG);
const ifrNotes=[
  {t:'弹性骨架 = 结构支撑 + 回复力',x:120,y:600,c:'#06d6a0',sub:'(资源自服务 — IFR)'},
  {t:'气动肌肉消除刚性关节 → 连续平滑弯曲',x:120,y:636,c:'#f5a623',sub:'(矛盾根源消除)'},
  {t:'腹部摩擦棱复用体表 → 无额外推进机构',x:120,y:672,c:'#8b5e3c',sub:'(现有资源利用)'},
  {t:'单气源 + 阀列 → 全身蜿蜒',x:120,y:708,c:'#06b6d4',sub:'(极简系统复杂度)'}
];
ifrNotes.forEach(n=>{
  const tx=el('text');sat(tx,{x:n.x,y:n.y,fill:n.c,'font-size':'12','font-family':'Fira Code,monospace','font-weight':'500'});tx.textContent=n.t;anG.appendChild(tx);
  const st=el('text');sat(st,{x:n.x,y:n.y+15,fill:'#556880','font-size':'9','font-family':'Fira Code,monospace'});st.textContent=n.sub;anG.appendChild(st);
});

// 行波传播标注箭头(动态更新)
const waveArrG=el('g');svg.appendChild(waveArrG);
const waveArr=el('path');waveArrG.appendChild(waveArr);
const waveArrT=el('text');sat(waveArrT,{x:0,y:0,fill:'#06b6d4','font-size':'11','font-family':'Fira Code,monospace','text-anchor':'middle'});waveArrT.textContent='行波 →';waveArrG.appendChild(waveArrT);

/* ====== 控件绑定 ====== */
document.getElementById('rSpd').addEventListener('input',e=>{spd=+e.target.value;document.getElementById('vSpd').textContent=spd.toFixed(1)+'x'});
document.getElementById('rPrs').addEventListener('input',e=>{prs=+e.target.value;document.getElementById('vPrs').textContent=prs.toFixed(2)+' MPa'});
document.getElementById('rAmp').addEventListener('input',e=>{amp=+e.target.value;document.getElementById('vAmp').textContent=amp.toFixed(2)+' rad'});

/* ====== 计算蛇体关节 ====== */
function computeSnake(t){
  const joints=[];
  let x=SX,y=SY,h=0;
  const wk=2*Math.PI/7.5, omega=spd*2.6;
  joints.push({x,y,h});
  for(let i=0;i<N;i++){
    const bend=amp*Math.sin(wk*i+PHASE-omega*t);
    h+=bend;
    x+=SL*Math.cos(h);
    y+=SL*Math.sin(h);
    joints.push({x,y,h,bend});
  }
  return joints;
}

/* ====== 主动画循环 ====== */
function animate(ts){
  if(!lt)lt=ts;
  const dt=Math.min((ts-lt)/1000,0.05);
  lt=ts; t+=dt;

  const J=computeSnake(t);
  const heads=J.map(j=>j.h);

  /* ---- 蒙皮包络 ---- */
  const up=[],lo=[];
  for(let i=0;i<J.length;i++){
    const segI=Math.min(i,N-1);
    const cur=(J[Math.min(i+1,J.length-1)].bend||0);
    const norm=cur/amp;
    const lInf=clamp(-norm,0,1), rInf=clamp(norm,0,1);
    const uw=BH+lInf*5, lw=BH+rInf*5;
    const nx=-Math.sin(heads[i]),ny=Math.cos(heads[i]);
    up.push({x:J[i].x+nx*uw,y:J[i].y+ny*uw});
    lo.push({x:J[i].x-nx*lw,y:J[i].y-ny*lw});
  }
  let sd=`M${up[0].x} ${up[0].y}`;
  for(let i=1;i<up.length;i++)sd+=` L${up[i].x} ${up[i].y}`;
  for(let i=lo.length-1;i>=0;i--)sd+=` L${lo[i].x} ${lo[i].y}`;
  sd+='Z';
  sat(skinP,{d:sd,fill:'rgba(180,195,215,0.05)',stroke:'none'});
  sat(skinO,{d:sd,fill:'none',stroke:'rgba(180,195,215,0.22)','stroke-width':'1.5'});

  /* ---- 骨架线 ---- */
  let spD=`M${J[0].x} ${J[0].y}`;
  for(let i=1;i<J.length;i++)spD+=` L${J[i].x} ${J[i].y}`;
  sat(spP,{d:spD,fill:'none',stroke:'#06d6a0','stroke-width':'2.8','stroke-linecap':'round',filter:'url(#sGlow)'});

  /* ---- 肌肉 ---- */
  const lInfs=[],rInfs=[];
  for(let i=0;i<N;i++){
    const j0=J[i],j1=J[i+1];
    const mx=(j0.x+j1.x)/2,my=(j0.y+j1.y)/2;
    const hd=j1.h;
    const cur=j1.bend||0;
    const norm=cur/amp;
    const lI=clamp(-norm,0,1), rI=clamp(norm,0,1);
    lInfs.push(lI);rInfs.push(rI);

    const nx=-Math.sin(hd),ny=Math.cos(hd);
    const pulse=0.92+0.08*Math.sin(t*9+i*0.7);

    // 左肌肉
    const lLen=MBL*(1-lI*0.3),lWid=MBW*(1+lI*0.9)*pulse;
    const lcx=mx+nx*MO,lcy=my+ny*MO;
    sat(LM[i],{d:rrect(lcx,lcy,lLen/2,lWid/2,2.5,hd),fill:muscleColor(lI),opacity:0.45+lI*0.55,filter:lI>0.25?'url(#mGlow)':'none',stroke:lI>0.3?'rgba(251,191,36,.4)':'#2e3e55','stroke-width':lI>0.3?'1.2':'0.7'});

    // 右肌肉
    const rLen=MBL*(1-rI*0.3),rWid=MBW*(1+rI*0.9)*pulse;
    const rcx=mx-nx*MO,rcy=my-ny*MO;
    sat(RM[i],{d:rrect(rcx,rcy,rLen/2,rWid/2,2.5,hd),fill:muscleColor(rI),opacity:0.45+rI*0.55,filter:rI>0.25?'url(#mGlow)':'none',stroke:rI>0.3?'rgba(251,191,36,.4)':'#2e3e55','stroke-width':rI>0.3?'1.2':'0.7'});

    /* ---- 气流指示 ---- */
    const maxI=Math.max(lI,rI);
    if(maxI>0.25){
      const side=lI>rI?1:-1;
      const acx=mx+nx*MO*side*2.2,acy=my+ny*MO*side*2.2;
      const dx=Math.cos(hd),dy=Math.sin(hd);
      const al=10*maxI;
      sat(AF[i],{d:`M${acx+dx*al} ${acy+dy*al}L${acx-dx*al} ${acy-dy*al}`,stroke:`rgba(6,182,212,${maxI*0.55})`,'stroke-width':'1.5','stroke-dasharray':'3 2.5','marker-end':'none',opacity:maxI});
    }else{
      sat(AF[i],{opacity:0});
    }

    /* ---- 摩擦棱 ---- */
    for(let k=0;k<2;k++){
      const ft=0.25+k*0.5;
      const px=lerp(j0.x,j1.x,ft),py=lerp(j0.y,j1.y,ft);
      const bnx=-Math.sin(hd),bny=Math.cos(hd);
      const fx=px-bnx*BH*0.75,fy=py-bny*BH*0.75;
      const bkx=-Math.cos(hd),bky=-Math.sin(hd);
      const sz=3.5;
      const p1x=fx+bnx*sz,p1y=fy+bny*sz;
      const p2x=fx-bnx*sz,p2y=fy-bny*sz;
      const p3x=fx+bkx*sz*1.6,p3y=fy+bky*sz*1.6;
      sat(FR[i*2+k],{points:`${p1x},${p1y} ${p2x},${p2y} ${p3x},${p3y}`,fill:'#8b5e3c',opacity:'0.55'});
    }
  }

  /* ---- 蛇头 ---- */
  const hJ=J[0],hh=heads[0];
  const hnx=-Math.sin(hh),hny=Math.cos(hh);
  sat(headL,{cx:hJ.x+hnx*6-2*Math.cos(hh),cy:hJ.y+hny*6-2*Math.sin(hh),r:2.5,fill:'#06d6a0',filter:'url(#smGlow)'});
  sat(headR,{cx:hJ.x-hnx*6-2*Math.cos(hh),cy:hJ.y-hny*6-2*Math.sin(hh),r:2.5,fill:'#06d6a0',filter:'url(#smGlow)'});

  /* ---- 截面动态更新 ---- */
  const csSeg=7;
  const csL=lInfs[csSeg]||0, csR=rInfs[csSeg]||0;
  for(let j=0;j<2;j++){
    const aL=Math.PI+(j-0.5)*0.55;
    const aR=(j-0.5)*0.55;
    const lmx=csX+48*Math.cos(aL),lmy=csY+48*Math.sin(aL);
    const rmx=csX+48*Math.cos(aR),rmy=csY+48*Math.sin(aR);
    const lRx=15*(1-csL*0.15),lRy=9*(1+csL*0.6);
    const rRx=15*(1-csR*0.15),rRy=9*(1+csR*0.6);
    sat(csLM[j],{cx:lmx,cy:lmy,rx:lRx,ry:lRy,fill:muscleColor(csL),stroke:csL>0.3?'#fbbf24':'#2e3e55','stroke-width':csL>0.3?'1.8':'1.2',filter:csL>0.3?'url(#smGlow)':'none'});
    sat(csRM[j],{cx:rmx,cy:rmy,rx:rRx,ry:rRy,fill:muscleColor(csR),stroke:csR>0.3?'#fbbf24':'#2e3e55','stroke-width':csR>0.3?'1.8':'1.2',filter:csR>0.3?'url(#smGlow)':'none'});
  }
  // 截面状态文字
  if(csL>csR&&csL>0.2)csState.textContent=`左侧充气 ${Math.round(csL*100)}% · 右侧放气`;
  else if(csR>csL&&csR>0.2)csState.textContent=`右侧充气 ${Math.round(csR*100)}% · 左侧放气`;
  else csState.textContent='中立位';

  /* ---- 相位指示器 ---- */
  for(let i=0;i<N;i++){
    const lI=lInfs[i],rI=rInfs[i];
    sat(phLC[i],{fill:muscleColor(lI),stroke:lI>0.3?'#fbbf24':'#2e3e55','stroke-width':lI>0.3?'1.5':'0.8',filter:lI>0.35?'url(#smGlow)':'none',r:10+lI*3});
    sat(phRC[i],{fill:muscleColor(rI),stroke:rI>0.3?'#fbbf24':'#2e3e55','stroke-width':rI>0.3?'1.5':'0.8',filter:rI>0.35?'url(#smGlow)':'none',r:10+rI*3});
  }

  /* ---- 行波传播方向标注 ---- */
  // 在蛇体下方画一个动态箭头
  const midJ=J[Math.floor(N/2)];
  const arrX=midJ.x,arrY=midJ.y+BH+45;
  const arrPhase=Math.sin(t*spd*3)*15;
  sat(waveArr,{d:`M${arrX-40} ${arrY+arrPhase*0.3} L${arrX+40} ${arrY-arrPhase*0.3}`,stroke:'#06b6d4','stroke-width':'1.5',fill:'none',opacity:'0.6'});
  sat(waveArrT,{x:arrX,y:arrY+18,fill:'#06b6d4',opacity:'0.7'});

  requestAnimationFrame(animate);
}

/* ====== 启动 ====== */
requestAnimationFrame(animate);
});
</script>
</body>
</html>

实现说明

本动画聚焦 IFR(最终理想解)视角,直接展示气动人工肌肉蛇形机器人的连续蜿蜒波传播原理,核心设计思路如下:

  1. 蛇体建模:14 个关节段,每段弯曲角按正弦行波 A·sin(k·i + φ - ω·t) 计算,通过逐段积分关节角得到蛇体中心线坐标,再沿法线方向偏移生成蒙皮包络、肌肉、摩擦棱等。

  2. 肌肉视觉状态:左侧肌肉充气→骨架左弯,右侧充气→右弯。充气肌肉以琥珀色 (#f5a623) 高亮 + 发光滤镜,同时变短变宽(McKibben 特征),放气肌肉为暗灰色细长形态,形成鲜明的视觉对比。

  3. 截面图联动:右侧截面示意图实时同步第 8 段的左右肌肉充放气状态,椭圆尺寸和颜色动态变化,配合状态文字("左侧充气 87% · 右侧放气"),直观揭示内部机理。

  4. 相位指示器:底部双排圆点分别对应左右肌肉,颜色和大小随激活程度脉动,一目了然地呈现行波沿体轴的传播规律。

  5. IFR 注释:四条关键注释突出理想解特征——弹性骨架双功能(结构+回复力)、消除刚性关节、摩擦棱复用体表、单气源驱动全身。

  6. 交互控制:三个滑块实时调节波速、气压(影响显示数值)、振幅,用户可深入体验参数对蜿蜒波形的影响。

积分规则:第一轮对话扣减8分,后续每轮扣6分