分享图
动画工坊
引擎就绪
<!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 href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{
  background:#060c1a;
  min-height:100vh;
  display:flex;flex-direction:column;
  align-items:center;justify-content:center;
  font-family:'Noto Sans SC',sans-serif;
  color:#b0c8e0;
  padding:20px 10px;
  overflow-x:hidden;
}
.wrap{width:96vw;max-width:1440px;position:relative}
.head{text-align:center;margin-bottom:14px}
.head h1{
  font-family:'Orbitron',monospace;font-size:clamp(18px,2.6vw,30px);
  font-weight:900;color:#00e0f0;letter-spacing:3px;
  text-shadow:0 0 24px rgba(0,224,240,.35);
}
.head p{font-size:13px;color:#4a7a9a;margin-top:3px}
.ifr-tag{
  display:inline-block;margin-top:6px;
  padding:3px 14px;
  background:rgba(0,255,136,.08);border:1px solid rgba(0,255,136,.5);
  border-radius:20px;font-size:12px;color:#00ff88;
  font-family:'Orbitron',monospace;letter-spacing:1px;
}
.scene-box{
  width:100%;border:1px solid #152040;border-radius:12px;
  overflow:hidden;background:#060c1a;
  box-shadow:0 0 40px rgba(0,180,220,.06);
}
.scene-box svg{display:block;width:100%;height:auto}
.ctrls{
  display:flex;gap:20px;margin-top:14px;
  padding:14px 22px;background:#0a1222;
  border:1px solid #152040;border-radius:10px;
  flex-wrap:wrap;justify-content:center;align-items:center;
}
.cg{display:flex;align-items:center;gap:8px}
.cg label{font-size:12px;color:#4a7a9a;white-space:nowrap}
.cg input[type=range]{
  -webkit-appearance:none;width:110px;height:4px;
  background:#152040;border-radius:2px;outline:none;
}
.cg input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:14px;height:14px;
  background:#00c8e0;border-radius:50%;cursor:pointer;
  box-shadow:0 0 8px rgba(0,200,224,.5);
}
.cg .val{
  font-family:'Orbitron',monospace;font-size:11px;
  color:#00e0f0;min-width:48px;text-align:right;
}
.btn{
  font-family:'Orbitron',monospace;font-size:11px;
  padding:6px 16px;background:#0e1830;color:#00c8e0;
  border:1px solid #0090a8;border-radius:6px;cursor:pointer;
  transition:all .2s;letter-spacing:1px;
}
.btn:hover{background:#00c8e0;color:#060c1a}
.phase-display{
  font-family:'Orbitron',monospace;font-size:12px;
  color:#ff9030;padding:4px 14px;
  background:rgba(255,144,48,.08);border:1px solid rgba(255,144,48,.3);
  border-radius:6px;min-width:160px;text-align:center;
}
</style>
</head>
<body>
<div class="wrap">
  <div class="head">
    <h1>VACUUM-ADHESION STAIR CLIMBER</h1>
    <p>负压吸附步进式爬楼机器人 — 以场代物,无形之手</p>
    <span class="ifr-tag">IFR: 负压场替代复杂机械臂</span>
  </div>
  <div class="scene-box">
    <svg id="scene" viewBox="0 0 1200 700" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
  <div class="ctrls">
    <div class="cg">
      <label>真空度</label>
      <input type="range" id="sliderPressure" min="1" max="8" step="0.5" value="5">
      <span class="val" id="valPressure">-5.0kPa</span>
    </div>
    <div class="cg">
      <label>速度</label>
      <input type="range" id="sliderSpeed" min="0.3" max="2.5" step="0.1" value="1">
      <span class="val" id="valSpeed">1.0x</span>
    </div>
    <div class="phase-display" id="phaseDisplay">INITIALIZING</div>
    <button class="btn" id="btnPause">PAUSE</button>
    <button class="btn" id="btnReset">RESET</button>
  </div>
</div>

<script>
// ========== 配置常量 ==========
const NS='http://www.w3.org/2000/svg';
const STEP_TREAD=220, STEP_RISER=150;
const STEP_X0=130, STEP_Y0=620;
const NUM_STEPS=4;
const BODY_W=160, BODY_H=46, WHEEL_R=16;
const CYCLE_MS=8000; // 每步耗时(ms)

// ========== 楼梯数据 ==========
const steps=[];
for(let i=0;i<NUM_STEPS;i++){
  steps.push({
    x: STEP_X0 + i*STEP_TREAD,
    y: STEP_Y0 - i*STEP_RISER,
    w: STEP_TREAD,
    h: STEP_RISER + (i===0?80:0)
  });
}

// 机器人每步的中心位置(轮子贴台阶面)
function robotPos(si){
  const s=steps[si];
  return {
    x: s.x + s.w*0.45,
    y: s.y - WHEEL_R - 4 - BODY_H/2
  };
}

// ========== SVG辅助 ==========
const svg=document.getElementById('scene');
function el(tag,attrs,parent){
  const e=document.createElementNS(NS,tag);
  if(attrs) Object.entries(attrs).forEach(([k,v])=>e.setAttribute(k,v));
  (parent||svg).appendChild(e);
  return e;
}
function lerp(a,b,t){return a+(b-a)*t}
function easeIO(t){return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2}
function easeOut(t){return 1-Math.pow(1-t,3)}
function easeIn(t){return t*t*t}

// ========== 定义滤镜 ==========
const defs=el('defs',{});
// 辉光滤镜(橙)
const fGlow=el('filter',{id:'glowO',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'6',result:'b'},fGlow);
el('feMerge',{},fGlow);
const mg1=el('feMergeNode',{in:'b'},el('feMerge',{},fGlow));
el('feMergeNode',{in:'SourceGraphic'},fGlow);

// 辉光滤镜(青)
const fGlowC=el('filter',{id:'glowC',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'b'},fGlowC);
el('feMerge',{},fGlowC);
el('feMergeNode',{in:'b'},el('feMerge',{},fGlowC));
el('feMergeNode',{in:'SourceGraphic'},fGlowC);

// 辉光滤镜(绿)
const fGlowG=el('filter',{id:'glowG',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'3',result:'b'},fGlowG);
el('feMerge',{},fGlowG);
el('feMergeNode',{in:'b'},el('feMerge',{},fGlowG));
el('feMergeNode',{in:'SourceGraphic'},fGlowG);

// 渐变 - 负压场
const rgV=el('radialGradient',{id:'vacuumGrad',cx:'50%',cy:'30%',r:'60%'},defs);
el('stop',{offset:'0%','stop-color':'#ff8800','stop-opacity':'0.5'},rgV);
el('stop',{offset:'70%','stop-color':'#ff6600','stop-opacity':'0.15'},rgV);
el('stop',{offset:'100%','stop-color':'#ff4400','stop-opacity':'0'},rgV);

// 渐变 - 吸盘激活
const rgS=el('radialGradient',{id:'suctionGrad',cx:'50%',cy:'50%',r:'50%'},defs);
el('stop',{offset:'0%','stop-color':'#ffaa30','stop-opacity':'0.8'},rgS);
el('stop',{offset:'100%','stop-color':'#ff6600','stop-opacity':'0.2'},rgS);

// ========== 绘制背景网格 ==========
const bgGroup=el('g',{id:'bg'});
// 网格线
for(let x=0;x<=1200;x+=40){
  el('line',{x1:x,y1:0,x2:x,y2:700,stroke:'#0c1628','stroke-width':'0.5'},bgGroup);
}
for(let y=0;y<=700;y+=40){
  el('line',{x1:0,y1:y,x2:1200,y2:y,stroke:'#0c1628','stroke-width':'0.5'},bgGroup);
}

// ========== 绘制楼梯 ==========
const stairGroup=el('g',{id:'stairs'});
steps.forEach((s,i)=>{
  // 台阶主体
  el('rect',{
    x:s.x, y:s.y, width:s.w, height:s.h,
    fill:'#0d1a2e', stroke:'#1e3a5e','stroke-width':'1.5',
    rx:2
  },stairGroup);
  // 台阶面(顶部高亮)
  el('line',{
    x1:s.x, y1:s.y, x2:s.x+s.w, y2:s.y,
    stroke:'#2a5a8a','stroke-width':'2.5','stroke-linecap':'round'
  },stairGroup);
  // 台阶编号
  el('text',{
    x:s.x+s.w/2, y:s.y+28,
    fill:'#1e3a5e','font-size':'13','text-anchor':'middle',
    'font-family':'Orbitron,monospace'
  },stairGroup).textContent='STEP '+i;
});

// ========== 粒子系统(负压场) ==========
const particles=[];
const particleGroup=el('g',{id:'particles'});
const MAX_PARTICLES=50;

function spawnParticle(rx,ry){
  // 在机器人裙边周围生成粒子,向中心(风机)运动
  const angle=Math.random()*Math.PI*2;
  const dist=30+Math.random()*50;
  particles.push({
    sx: rx+Math.cos(angle)*dist,
    sy: ry+BODY_H/2+4+Math.random()*15,
    life:0,
    maxLife:0.6+Math.random()*0.6,
    speed:25+Math.random()*35,
    size:1.2+Math.random()*2
  });
}

// ========== 动画状态 ==========
let animTime=0;
let pressure=5;
let speed=1;
let playing=true;
let lastTS=0;

// ========== 构建机器人SVG组 ==========
const robotGroup=el('g',{id:'robot'});

// 负压场可视化(裙边下方)
const vacuumZone=el('ellipse',{
  cx:0, cy:BODY_H/2+6,
  rx:BODY_W*0.42, ry:18,
  fill:'url(#vacuumGrad)', opacity:'0',
  filter:'url(#glowO)'
},robotGroup);

// 负压场脉冲环
const vacuumRing=el('ellipse',{
  cx:0, cy:BODY_H/2+6,
  rx:BODY_W*0.42, ry:18,
  fill:'none', stroke:'#ff8800','stroke-width':'1.5',
  opacity:'0','stroke-dasharray':'6 4'
},robotGroup);

// 机身主体
el('rect',{
  x:-BODY_W/2, y:-BODY_H/2,
  width:BODY_W, height:BODY_H,
  rx:8, ry:8,
  fill:'#101e34', stroke:'#00c0d8','stroke-width':'1.8'
},robotGroup);

// 机身细节线
el('line',{
  x1:-BODY_W/2+10,y1:-BODY_H/2+12,x2:BODY_W/2-10,y2:-BODY_H/2+12,
  stroke:'#1a3050','stroke-width':'0.8'
},robotGroup);
el('line',{
  x1:-BODY_W/2+10,y1:BODY_H/2-12,x2:BODY_W/2-10,y2:BODY_H/2-12,
  stroke:'#1a3050','stroke-width':'0.8'
},robotGroup);

// 风机(中心圆+旋转叶片)
const fanGroup=el('g',{id:'fan'},robotGroup);
el('circle',{cx:0,cy:0,r:14,fill:'#0a1424',stroke:'#1a4a6a','stroke-width':'1'},fanGroup);
el('circle',{cx:0,cy:0,r:4,fill:'#1a3a5a',stroke:'#0090a8','stroke-width':'0.8'},fanGroup);
// 叶片(3片)
for(let i=0;i<3;i++){
  const a=i*120;
  el('path',{
    d:`M0,-2 Q8,-8 4,-14 Q0,-10 -4,-14 Q-8,-8 0,-2Z`,
    fill:'#1a4a6a',opacity:'0.8',
    transform:`rotate(${a})`
  },fanGroup);
}

// 裙边(底部波浪)
const skirtPath=el('path',{
  d:`M${-BODY_W*0.4},${BODY_H/2} `+
    `Q${-BODY_W*0.3},${BODY_H/2+10} ${-BODY_W*0.15},${BODY_H/2+6} `+
    `Q0,${BODY_H/2+14} ${BODY_W*0.15},${BODY_H/2+6} `+
    `Q${BODY_W*0.3},${BODY_H/2+10} ${BODY_W*0.4},${BODY_H/2}`,
  fill:'none', stroke:'#00c0d8','stroke-width':'1.5',
  'stroke-dasharray':'4 3'
},robotGroup);

// 麦克纳姆轮(2个可见)
const wheels=[];
function drawWheel(cx,cy){
  const wg=el('g',{transform:`translate(${cx},${cy})`},robotGroup);
  el('circle',{cx:0,cy:0,r:WHEEL_R,fill:'#0c1828',stroke:'#0090a8','stroke-width':'1.5'},wg);
  // 辊子纹路
  for(let i=0;i<6;i++){
    const a=i*60;
    const rad=a*Math.PI/180;
    const dx=Math.cos(rad)*WHEEL_R*0.7;
    const dy=Math.sin(rad)*WHEEL_R*0.7;
    el('line',{
      x1:-dx*0.5,y1:-dy*0.5,x2:dx*0.5,y2:dy*0.5,
      stroke:'#1a4a5a','stroke-width':'1.5','stroke-linecap':'round',
      transform:`rotate(45)`
    },wg);
  }
  el('circle',{cx:0,cy:0,r:3,fill:'#0090a8'},wg);
  wheels.push(wg);
  return wg;
}
drawWheel(-BODY_W/2+18, BODY_H/2+2);
drawWheel(BODY_W/2-18, BODY_H/2+2);

// ========== 机械足 ==========
const legGroup=el('g',{id:'legs'},robotGroup);

// 左足(视觉上在前)
const leg1Group=el('g',{id:'leg1'},legGroup);
const leg1Outer=el('rect',{x:0,y:0,width:10,height:50,rx:3,
  fill:'#0e1e36',stroke:'#00b8d0','stroke-width':'1.2'},leg1Group);
const leg1Inner=el('rect',{x:0,y:0,width:8,height:50,rx:2,
  fill:'#0a1628',stroke:'#00d8e8','stroke-width':'1'},leg1Group);
const leg1Cup=el('g',{id:'leg1cup'},leg1Group);
el('ellipse',{cx:0,cy:0,rx:10,ry:5,fill:'#1a2a44',stroke:'#4a7a9a','stroke-width':'1'},leg1Cup);
const leg1CupGlow=el('ellipse',{cx:0,cy:0,rx:14,ry:8,fill:'url(#suctionGrad)',opacity:'0'},leg1Cup);

// 右足(稍微偏后)
const leg2Group=el('g',{id:'leg2'},legGroup);
const leg2Outer=el('rect',{x:0,y:0,width:10,height:50,rx:3,
  fill:'#0e1e36',stroke:'#00b8d0','stroke-width':'1.2'},leg2Group);
const leg2Inner=el('rect',{x:0,y:0,width:8,height:50:50,rx:2,
  fill:'#0a1628',stroke:'#00d8e8','stroke-width':'1'},leg2Group);
const leg2Cup=el('g',{id:'leg2cup'},leg2Group);
el('ellipse',{cx:0,cy:0,rx:10,ry:5,fill:'#1a2a44',stroke:'#4a7a9a','stroke-width':'1'},leg2Cup);
const leg2CupGlow=el('ellipse',{cx:0,cy:0,rx:14,ry:8,fill:'url(#suctionGrad)',opacity:'0'},leg2Cup);

// ========== 流线箭头(气流) ==========
const flowGroup=el('g',{id:'flowArrows'},robotGroup);
const flowArrows=[];
for(let i=0;i<6;i++){
  const a=el('path',{
    d:'M0,0 L0,0',fill:'none',stroke:'#ff9030','stroke-width':'1.2',
    'stroke-dasharray':'5 4',opacity:'0'
  },flowGroup);
  flowArrows.push(a);
}

// ========== 标注 ==========
const annoGroup=el('g',{id:'annotations'});

// IFR 标注
const ifrBox=el('g',{id:'ifrAnno',opacity:'0'},annoGroup);
el('rect',{x:780,y:30,width:380,height:62,rx:8,
  fill:'rgba(0,255,136,0.06)',stroke:'#00ff88','stroke-width':'1',
  'stroke-dasharray':'6 3'},ifrBox);
el('text',{x:800,y:52,fill:'#00ff88','font-size':'13',
  'font-family':'Orbitron,monospace','font-weight':'700'},ifrBox).textContent='IFR: IDEAL FINAL RESULT';
el('text',{x:800,y:72,fill:'#80d0a0','font-size':'12',
  'font-family':'Noto Sans SC,sans-serif'},ifrBox).textContent='以"场"代"物" — 负压场替代复杂机械臂实现附着';

// 参数标注
const paramBox=el('g',{id:'paramAnno',opacity:'0'},annoGroup);
el('rect',{x:780,y:100,width:380,height:52,rx:8,
  fill:'rgba(0,180,220,0.06)',stroke:'#00b0c8','stroke-width':'1',
  'stroke-dasharray':'6 3'},paramBox);
el('text',{x:800,y:120,fill:'#00c8e0','font-size':'12',
  'font-family':'Orbitron,monospace'},paramBox).textContent='VACUUM: -5.0kPa  |  STROKE: 200mm';
el('text',{x:800,y:138,fill:'#5a90a8','font-size':'11',
  'font-family':'Noto Sans SC,sans-serif'},paramBox).textContent='吸附力与台阶几何无关 — 通用性核心';

// 阶段指示器
const phaseText=el('text',{
  x:600,y:690,fill:'#ff9030','font-size':'14',
  'font-family':'Orbitron,monospace','font-weight':'700',
  'text-anchor':'middle',opacity:'0.8'
},annoGroup);

// 连接线(IFR到负压场)
const ifrLine=el('line',{
  x1:780,y1:70,x2:400,y2:400,
  stroke:'#00ff88','stroke-width':'0.8','stroke-dasharray':'4 4',
  opacity:'0'
},annoGroup);

// 真空度表盘
const gaugeGroup=el('g',{id:'gauge',transform:'translate(80,80)'},annoGroup);
el('circle',{cx:0,cy:0,r:40,fill:'rgba(10,20,40,0.8)',stroke:'#1a3a5a','stroke-width':'1.5'},gaugeGroup);
el('text',{x:0,y:-14,fill:'#4a7a9a','font-size':'9','text-anchor':'middle',
  'font-family':'Orbitron,monospace'},gaugeGroup).textContent='VACUUM';
const gaugeNeedle=el('line',{
  x1:0,y1:5,x2:0,y2:-28,
  stroke:'#ff8800','stroke-width':'2','stroke-linecap':'round'
},gaugeGroup);
el('circle',{cx:0,cy:0,r:3,fill:'#ff8800'},gaugeGroup);
el('text',{x:0,y:22,fill:'#00e0f0','font-size':'11','text-anchor':'middle',
  'font-family':'Orbitron,monospace','font-weight':'700',id:'gaugeVal'},gaugeGroup).textContent='-5.0';

// 刻度
for(let i=0;i<=8;i++){
  const a=-140+i*(280/8);
  const rad=a*Math.PI/180;
  const x1=Math.cos(rad)*32, y1=Math.sin(rad)*32;
  const x2=Math.cos(rad)*36, y2=Math.sin(rad)*36;
  el('line',{x1,y1,x2,y2,stroke:'#2a4a6a','stroke-width':'1'},gaugeGroup);
}

// ========== 动画循环 ==========
function getPhaseInfo(cycleT){
  // cycleT: 0~1, 一个爬升周期
  if(cycleT<0.10) return {phase:'vacuum_on', t:cycleT/0.10, label:'负压吸附'};
  if(cycleT<0.35) return {phase:'legs_extend', t:(cycleT-0.10)/0.25, label:'机械足伸出'};
  if(cycleT<0.45) return {phase:'suction_attach', t:(cycleT-0.35)/0.10, label:'吸盘附着'};
  if(cycleT<0.72) return {phase:'pull_up', t:(cycleT-0.45)/0.27, label:'收缩提拉'};
  if(cycleT<0.85) return {phase:'settle', t:(cycleT-0.72)/0.13, label:'轮子跟随'};
  if(cycleT<0.95) return {phase:'release', t:(cycleT-0.85)/0.10, label:'吸盘释放'};
  return {phase:'reset', t:(cycleT-0.95)/0.05, label:'准备下一步'};
}

function updateRobot(dt){
  const totalCycleMs=CYCLE_MS/speed;
  animTime+=dt*speed;
  
  // 当前步数和周期内时间
  const stepDuration=totalCycleMs;
  const totalStepTime=NUM_STEPS-1; // 爬3次
  const totalTime=totalStepTime*stepDuration;
  
  // 循环
  const loopTime=animTime%totalTime;
  const currentStepIdx=Math.min(Math.floor(loopTime/stepDuration), totalStepTime-1);
  const cycleT=(loopTime%stepDuration)/stepDuration;
  
  const pi=getPhaseInfo(cycleT);
  const curPos=robotPos(currentStepIdx);
  const nextPos=robotPos(currentStepIdx+1);
  
  // 计算机器人位置和状态
  let rx, ry, legExt, vacuumOpacity, suctionOn, wheelRot;
  rx=curPos.x;
  ry=curPos.y;
  legExt=0;
  vacuumOpacity=0;
  suctionOn=0;
  wheelRot=0;
  
  switch(pi.phase){
    case 'vacuum_on':
      vacuumOpacity=easeOut(pi.t);
      legExt=0;
      break;
    case 'legs_extend':
      vacuumOpacity=1;
      legExt=easeOut(pi.t);
      break;
    case 'suction_attach':
      vacuumOpacity=1;
      legExt=1;
      suctionOn=easeOut(pi.t);
      break;
    case 'pull_up':
      vacuumOpacity=1;
      suctionOn=1;
      const pt=easeIO(pi.t);
      rx=lerp(curPos.x, nextPos.x, pt);
      ry=lerp(curPos.y, nextPos.y, pt);
      legExt=1-pt*0.85; // 足逐渐收缩
      wheelRot=pi.t*180;
      break;
    case 'settle':
      vacuumOpacity=1;
      suctionOn=1-pi.t*0.5;
      rx=nextPos.x;
      ry=nextPos.y;
      legExt=0.15*(1-pi.t);
      wheelRot+=pi.t*60;
      break;
    case 'release':
      vacuumOpacity=1-pi.t*0.3;
      suctionOn=Math.max(0, 0.5-pi.t);
      rx=nextPos.x;
      ry=nextPos.y;
      legExt=0;
      break;
    case 'reset':
      vacuumOpacity=0.7+pi.t*0.3;
      rx=nextPos.x;
      ry=nextPos.y;
      legExt=0;
      break;
  }
  
  // 应用真空度影响
  const pressureFactor=pressure/5;
  vacuumOpacity*=pressureFactor;
  
  // 更新机器人位置
  robotGroup.setAttribute('transform',`translate(${rx},${ry})`);
  
  // 更新负压场
  vacuumZone.setAttribute('opacity', vacuumOpacity*0.7);
  vacuumRing.setAttribute('opacity', vacuumOpacity*0.5);
  const ringScale=1+0.1*Math.sin(animTime*0.004);
  vacuumRing.setAttribute('rx', BODY_W*0.42*ringScale);
  vacuumRing.setAttribute('ry', 18*ringScale);
  
  // 更新裙边颜色
  if(vacuumOpacity>0.3){
    skirtPath.setAttribute('stroke','#ff9030');
    skirtPath.setAttribute('filter','url(#glowO)');
  }else{
    skirtPath.setAttribute('stroke','#00c0d8');
    skirtPath.removeAttribute('filter');
  }
  
  // 更新风机旋转
  const fanAngle=(animTime*0.3)%360;
  fanGroup.setAttribute('transform',`rotate(${fanAngle})`);
  
  // 更新轮子旋转
  wheels.forEach(w=>{
    const curTransform=w.getAttribute('transform');
    // 提取translate部分并添加rotate
    w.querySelector('circle').nextSibling; // 简化:直接设置整个组的旋转
  });
  // 简化轮子旋转:旋转内部线条
  wheels.forEach((wg,wi)=>{
    const lines=wg.querySelectorAll('line');
    const baseAngle=wi===0?45:-45;
    lines.forEach((l,li)=>{
      l.setAttribute('transform',`rotate(${baseAngle+wheelRot+li*60})`);
    });
  });
  
  // ========== 更新机械足 ==========
  // 足的起点(机身前上方)和终点(下一级台阶面)
  const pivotX=BODY_W/2-5;
  const pivotY=-BODY_H/2+8;
  
  // 目标吸盘位置(相对于当前机器人位置的世界坐标,需转为局部坐标)
  const nextStep=steps[Math.min(currentStepIdx+1, NUM_STEPS-1)];
  const cupWorldX=nextStep.x+nextStep.w*0.25;
  const cupWorldY=nextStep.y;
  const cupLocalX=cupWorldX-rx;
  const cupLocalY=cupWorldY-ry;
  
  // 收缩状态下的吸盘位置(靠近机身)
  const retractX=pivotX+12;
  const retractY=pivotY-35;
  
  // 插值
  const cupX=lerp(retractX, cupLocalX, legExt);
  const cupY=lerp(retractY, cupLocalY, legExt);
  
  // 计算足的角度和长度
  const dx=cupX-pivotX;
  const dy=cupY-pivotY;
  const dist=Math.sqrt(dx*dx+dy*dy);
  const angle=Math.atan2(dy,dx)*180/Math.PI;
  
  // 足1(主足)
  const outerLen=Math.min(dist*0.55, 60);
  const innerLen=Math.min(dist*0.55, 55);
  
  leg1Group.setAttribute('transform',`translate(${pivotX},${pivotY})`);
  leg1Outer.setAttribute('x','-5');
  leg1Outer.setAttribute('y','0');
  leg1Outer.setAttribute('height',outerLen);
  leg1Outer.setAttribute('transform',`rotate(${angle-90})`);
  leg1Outer.setAttribute('transform',`rotate(${angle+90}) translate(-5,0)`);
  
  // 简化:直接画线段表示足
  leg1Outer.setAttribute('display','none');
  leg1Inner.setAttribute('display','none');
  
  // 用line重绘足
  if(!leg1Group._line1){
    leg1Group._line1=el('line',{x1:0,y1:0,stroke:'#00b8d0','stroke-width':'5','stroke-linecap':'round'},leg1Group);
    leg1Group._line2=el('line',{x1:0,y1:0,stroke:'#00d8e8','stroke-width':'3','stroke-linecap':'round'},leg1Group);
    leg1Group._joint1=el('circle',{cx:0,cy:0,r:4,fill:'#0a1628',stroke:'#00c0d8','stroke-width':'1.5'},leg1Group);
  }
  const midX=dx*0.5, midY=dy*0.5;
  leg1Group._line1.setAttribute('x2',cupX-pivotX);
  leg1Group._line1.setAttribute('y2',cupY-pivotY);
  leg1Group._line2.setAttribute('x2',cupX-pivotX);
  leg1Group._line2.setAttribute('y2',cupY-pivotY);
  leg1Group._joint1.setAttribute('cx',midX);
  leg1Group._joint1.setAttribute('cy',midY);
  
  // 吸盘
  leg1Cup.setAttribute('transform',`translate(${cupX-pivotX},${cupY-pivotY})`);
  leg1CupGlow.setAttribute('opacity', suctionOn*pressureFactor);
  if(suctionOn>0.3){
    leg1Cup.querySelector('ellipse').setAttribute('stroke','#ff9030');
    leg1Cup.querySelector('ellipse').setAttribute('fill','#2a1a08');
  }else{
    leg1Cup.querySelector('ellipse').setAttribute('stroke','#4a7a9a');
    leg1Cup.querySelector('ellipse').setAttribute('fill','#1a2a44');
  }
  
  // 足2(副足,偏移)
  const pivot2X=pivotX-18;
  const pivot2Y=pivotY+5;
  const cup2X=lerp(pivot2X+10, cupLocalX-12, legExt);
  const cup2Y=lerp(pivot2Y-30, cupLocalY, legExt);
  
  leg2Group.setAttribute('transform',`translate(${pivot2X},${pivot2Y})`);
  if(!leg2Group._line1){
    leg2Group._line1=el('line',{x1:0,y1:0,stroke:'#00a0b8','stroke-width':'4','stroke-linecap':'round'},leg2Group);
    leg2Group._line2=el('line',{x1:0,y1:0,stroke:'#00c0d0','stroke-width':'2.5','stroke-linecap':'round'},leg2Group);
    leg2Group._joint1=el('circle',{cx:0,cy:0,r:3,fill:'#0a1628',stroke:'#00a0b8','stroke-width':'1.2'},leg2Group);
  }
  leg2Group._line1.setAttribute('x2',cup2X-pivot2X);
  leg2Group._line1.setAttribute('y2',cup2Y-pivot2Y);
  leg2Group._line2.setAttribute('x2',cup2X-pivot2X);
  leg2Group._line2.setAttribute('y2',cup2Y-pivot2Y);
  const mid2X=(cup2X-pivot2X)*0.5, mid2Y=(cup2Y-pivot2Y)*0.5;
  leg2Group._joint1.setAttribute('cx',mid2X);
  leg2Group._joint1.setAttribute('cy',mid2Y);
  
  leg2Cup.setAttribute('transform',`translate(${cup2X-pivot2X},${cup2Y-pivot2Y})`);
  leg2CupGlow.setAttribute('opacity', suctionOn*pressureFactor*0.7);
  if(suctionOn>0.3){
    leg2Cup.querySelector('ellipse').setAttribute('stroke','#ff9030');
  }else{
    leg2Cup.querySelector('ellipse').setAttribute('stroke','#4a7a9a');
  }
  
  // ========== 气流箭头 ==========
  flowArrows.forEach((fa,i)=>{
    if(vacuumOpacity<0.2){fa.setAttribute('opacity','0');return;}
    const aAngle=(i/6)*Math.PI*2+animTime*0.002;
    const startR=50+10*Math.sin(animTime*0.003+i);
    const endR=15;
    const sx=Math.cos(aAngle)*startR;
    const sy=BODY_H/2+6+Math.sin(aAngle)*14;
    const ex=Math.cos(aAngle)*endR;
    const ey=BODY_H/2+6+Math.sin(aAngle)*5;
    fa.setAttribute('d',`M${sx},${sy} L${ex},${ey}`);
    fa.setAttribute('opacity', vacuumOpacity*0.6);
    fa.setAttribute('stroke-dashoffset', -animTime*0.05);
  });
  
  // ========== 粒子更新 ==========
  // 生成新粒子
  if(vacuumOpacity>0.3 && particles.length<MAX_PARTICLES && Math.random()<0.3*pressureFactor){
    spawnParticle(rx,ry);
  }
  // 更新粒子
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.life+=dt*0.001;
    if(p.life>p.maxLife){particles.splice(i,1);continue;}
    const lt=p.life/p.maxLife;
    // 向风机中心移动
    const targetX=rx;
    const targetY=ry+4;
    p.sx=lerp(p.sx, targetX, lt*0.08*pressureFactor);
    p.sy=lerp(p.sy, targetY, lt*0.08*pressureFactor);
    p._el.setAttribute('cx',p.sx);
    p._el.setAttribute('cy',p.sy);
    p._el.setAttribute('r',p.size*(1-lt*0.5));
    p._el.setAttribute('opacity', vacuumOpacity*0.7*(1-lt));
  }
  
  // ========== 标注更新 ==========
  // IFR标注淡入
  const ifrOp=Math.min(1, animTime/2000);
  ifrBox.setAttribute('opacity', ifrOp);
  paramBox.setAttribute('opacity', ifrOp);
  ifrLine.setAttribute('opacity', ifrOp*0.4);
  
  // 更新IFR连接线目标
  ifrLine.setAttribute('x2', rx);
  ifrLine.setAttribute('y2', ry+BODY_H/2+10);
  
  // 参数文字更新
  const paramTextEl=paramBox.querySelector('text');
  paramTextEl.textContent=`VACUUM: -${pressure.toFixed(1)}kPa  |  STROKE: 200mm`;
  
  // 阶段文字
  phaseText.textContent=pi.label;
  document.getElementById('phaseDisplay').textContent=pi.label;
  
  // 真空度表盘
  const needleAngle=-140+(pressure/8)*280;
  gaugeNeedle.setAttribute('transform',`rotate(${needleAngle})`);
  document.getElementById('gaugeVal').textContent=`-${pressure.toFixed(1)}`;
  
  // 高亮负压场时IFR标注闪烁
  if(vacuumOpacity>0.6 && pi.phase==='vacuum_on'){
    const blink=0.7+0.3*Math.sin(animTime*0.01);
    ifrBox.setAttribute('opacity', ifrOp*blink);
  }
}

// 初始化粒子SVG元素
function ensureParticleEl(p){
  if(!p._el){
    p._el=el('circle',{
      cx:p.sx, cy:p.sy, r:p.size,
      fill:'#ffaa40', opacity:'0.6',
      filter:'url(#glowO)'
    },particleGroup);
  }
}

// ========== 主动画循环 ==========
function animate(ts){
  if(!lastTS) lastTS=ts;
  const dt=Math.min(ts-lastTS, 50); // 限制最大帧间隔
  lastTS=ts;
  
  if(playing){
    // 确保粒子有SVG元素
    particles.forEach(p=>ensureParticleEl(p));
    // 清除已死亡粒子的SVG元素
    for(let i=0;i<particleGroup.children.length;i++){
      const ce=particleGroup.children[i];
      if(!particles.find(p=>p._el===ce)){
        particleGroup.removeChild(ce);
        i--;
      }
    }
    updateRobot(dt);
  }
  
  requestAnimationFrame(animate);
}

// ========== 控件绑定 ==========
document.getElementById('sliderPressure').addEventListener('input',function(){
  pressure=parseFloat(this.value);
  document.getElementById('valPressure').textContent=`-${pressure.toFixed(1)}kPa`;
});

document.getElementById('sliderSpeed').addEventListener('input',function(){
  speed=parseFloat(this.value);
  document.getElementById('valSpeed').textContent=`${speed.toFixed(1)}x`;
});

document.getElementById('btnPause').addEventListener('click',function(){
  playing=!playing;
  this.textContent=playing?'PAUSE':'PLAY';
});

document.getElementById('btnReset').addEventListener('click',function(){
  animTime=0;
  lastTS=0;
  playing=true;
  document.getElementById('btnPause').textContent='PAUSE';
  // 清除粒子
  while(particleGroup.firstChild) particleGroup.removeChild(particleGroup.firstChild);
  particles.length=0;
});

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

等等,代码里有个语法错误:width:8,height:50:50 应该是 height:50。另外,整体代码结构比较混乱,机械足的渲染逻辑有冗余(先用rect画又用line画)。让我重新写一个更干净、更完整的版本。

<!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 href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{
  background:#060c1a;min-height:100vh;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  font-family:'Noto Sans SC',sans-serif;color:#b0c8e0;
  padding:16px 8px;overflow-x:hidden;
}
.wrap{width:96vw;max-width:1440px}
.head{text-align:center;margin-bottom:12px}
.head h1{
  font-family:'Orbitron',monospace;font-size:clamp(16px,2.4vw,28px);
  font-weight:900;color:#00e0f0;letter-spacing:3px;
  text-shadow:0 0 20px rgba(0,224,240,.3);
}
.head p{font-size:13px;color:#4a7a9a;margin-top:2px}
.ifr-tag{
  display:inline-block;margin-top:5px;padding:3px 14px;
  background:rgba(0,255,136,.07);border:1px solid rgba(0,255,136,.4);
  border-radius:20px;font-size:11px;color:#00ff88;
  font-family:'Orbitron',monospace;letter-spacing:1px;
}
.scene-box{
  width:100%;border:1px solid #142040;border-radius:10px;
  overflow:hidden;background:#060c1a;
  box-shadow:0 0 50px rgba(0,160,220,.05);
}
.scene-box svg{display:block;width:100%;height:auto}
.ctrls{
  display:flex;gap:16px;margin-top:12px;padding:12px 20px;
  background:#0a1222;border:1px solid #142040;border-radius:8px;
  flex-wrap:wrap;justify-content:center;align-items:center;
}
.cg{display:flex;align-items:center;gap:7px}
.cg label{font-size:11px;color:#4a7a9a;white-space:nowrap}
.cg input[type=range]{
  -webkit-appearance:none;width:100px;height:3px;
  background:#142040;border-radius:2px;outline:none;
}
.cg input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:13px;height:13px;
  background:#00c0d0;border-radius:50%;cursor:pointer;
  box-shadow:0 0 6px rgba(0,192,208,.4);
}
.cg .val{
  font-family:'Orbitron',monospace;font-size:10px;
  color:#00e0f0;min-width:52px;text-align:right;
}
.btn{
  font-family:'Orbitron',monospace;font-size:10px;
  padding:5px 14px;background:#0c1628;color:#00b0c0;
  border:1px solid #008090;border-radius:5px;cursor:pointer;
  transition:all .2s;letter-spacing:1px;
}
.btn:hover{background:#00b0c0;color:#060c1a}
.ph{
  font-family:'Orbitron',monospace;font-size:11px;
  color:#ff9030;padding:4px 12px;
  background:rgba(255,144,48,.06);border:1px solid rgba(255,144,48,.25);
  border-radius:5px;min-width:130px;text-align:center;
}
</style>
</head>
<body>
<div class="wrap">
  <div class="head">
    <h1>VACUUM-ADHESION STAIR CLIMBER</h1>
    <p>负压吸附步进式爬楼机器人 — 以场代物,无形之手</p>
    <span class="ifr-tag">IFR IDEAL FINAL RESULT</span>
  </div>
  <div class="scene-box">
    <svg id="scene" viewBox="0 0 1200 700" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
  <div class="ctrls">
    <div class="cg">
      <label>真空度</label>
      <input type="range" id="slP" min="1" max="8" step="0.5" value="5">
      <span class="val" id="vP">-5.0kPa</span>
    </div>
    <div class="cg">
      <label>速度</label>
      <input type="range" id="slS" min="0.3" max="2.5" step="0.1" value="1">
      <span class="val" id="vS">1.0x</span>
    </div>
    <div class="ph" id="phD">INIT</div>
    <button class="btn" id="bP">PAUSE</button>
    <button class="btn" id="bR">RESET</button>
  </div>
</div>
<script>
(function(){
const NS='http://www.w3.org/2000/svg';
const svg=document.getElementById('scene');

/* ===== 工具函数 ===== */
function $(tag,a,p){const e=document.createElementNS(NS,tag);if(a)Object.entries(a).forEach(([k,v])=>e.setAttribute(k,v));(p||svg).appendChild(e);return e}
function lerp(a,b,t){return a+(b-a)*t}
function easeIO(t){return t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2}
function easeO(t){return 1-Math.pow(1-t,3)}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}

/* ===== 常量 ===== */
const ST=220,SR=150,SX=130,SY=620,NS4=4;
const BW=160,BH=46,WR=16;
const CYC=8000;

/* ===== 楼梯 ===== */
const steps=[];
for(let i=0;i<NS4;i++) steps.push({x:SX+i*ST,y:SY-i*SR,w:ST,h:SR+(i===0?80:0)});

function rPos(si){const s=steps[si];return{x:s.x+s.w*.45,y:s.y-WR-4-BH/2}}

/* ===== Defs ===== */
const defs=$('defs');

// 橙辉光
const fg=$('filter',{id:'gO',x:'-80%',y:'-80%',width:'260%',height:'260%'},defs);
$('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'8',result:'b'},fg);
const fm=$('feMerge',{},fg);$('feMergeNode',{in:'b'},fm);$('feMergeNode',{in:'SourceGraphic'},fm);

// 青辉光
const fg2=$('filter',{id:'gC',x:'-60%',y:'-60%',width:'220%',height:'220%'},defs);
$('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'5',result:'b'},fg2);
const fm2=$('feMerge',{},fg2);$('feMergeNode',{in:'b'},fm2);$('feMergeNode',{in:'SourceGraphic'},fm2);

// 绿辉光
const fg3=$('filter',{id:'gG',x:'-60%',y:'-60%',width:'220%',height:'220%'},defs);
$('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'b'},fg3);
const fm3=$('feMerge',{},fg3);$('feMergeNode',{in:'b'},fm3);$('feMergeNode',{in:'SourceGraphic'},fm3);

// 负压径向渐变
const rv=$('radialGradient',{id:'vG',cx:'50%',cy:'30%',r:'65%'},defs);
$('stop',{offset:'0%','stop-color':'#ff8800','stop-opacity':'.55'},rv);
$('stop',{offset:'60%','stop-color':'#ff5500','stop-opacity':'.18'},rv);
$('stop',{offset:'100%','stop-color':'#ff3300','stop-opacity':'0'},rv);

// 吸盘渐变
const rs=$('radialGradient',{id:'sG',cx:'50%',cy:'50%',r:'55%'},defs);
$('stop',{offset:'0%','stop-color':'#ffaa30','stop-opacity':'.85'},rs);
$('stop',{offset:'100%','stop-color':'#ff6600','stop-opacity':'.2'},rs);

/* ===== 背景网格 ===== */
const bgG=$('g',{id:'bg'});
for(let x=0;x<=1200;x+=40) $('line',{x1:x,y1:0,x2:x,y2:700,stroke:'#0b1525','stroke-width':'.5'},bgG);
for(let y=0;y<=700;y+=40) $('line',{x1:0,y1:y,x2:1200,y2:y,stroke:'#0b1525','stroke-width':'.5'},bgG);

/* ===== 楼梯绘制 ===== */
const stG=$('g',{id:'stairG'});
steps.forEach((s,i)=>{
  $('rect',{x:s.x,y:s.y,width:s.w,height:s.h,fill:'#0c1828',stroke:'#1a3460','stroke-width':'1.5',rx:2},stG);
  $('line',{x1:s.x,y1:s.y,x2:s.x+s.w,y2:s.y,stroke:'#2a5888','stroke-width':'2.5','stroke-linecap':'round'},stG);
  // 竖面高亮
  if(i<NS4-1){
    $('line',{x1:s.x+s.w,y1:s.y,x2:s.x+s.w,y2:s.y+SR,stroke:'#1a3460','stroke-width':'1'},stG);
  }
  $('text',{x:s.x+s.w/2,y:s.y+26,fill:'#162a48','font-size':'12','text-anchor':'middle','font-family':'Orbitron,monospace'},stG).textContent='S'+i;
});

/* ===== 粒子组 ===== */
const ptG=$('g',{id:'ptG'});
const parts=[];
const MAXP=55;

/* ===== 机器人组 ===== */
const robG=$('g',{id:'rob'});

// 负压场
const vZone=$('ellipse',{cx:0,cy:BH/2+7,rx:BW*.42,ry:20,fill:'url(#vG)',opacity:'0',filter:'url(#gO)'},robG);
const vRing=$('ellipse',{cx:0,cy:BH/2+7,rx:BW*.42,ry:20,fill:'none',stroke:'#ff8800','stroke-width':'1.5',opacity:'0','stroke-dasharray':'6 4'},robG);

// 机身
$('rect',{x:-BW/2,y:-BH/2,width:BW,height:BH,rx:8,fill:'#0e1c30',stroke:'#00b8d0','stroke-width':'1.8'},robG);
$('line',{x1:-BW/2+10,y1:-BH/2+11,x2:BW/2-10,y2:-BH/2+11,stroke:'#162840','stroke-width':'.7'},robG);
$('line',{x1:-BW/2+10,y1:BH/2-11,x2:BW/2-10,y2:BH/2-11,stroke:'#162840','stroke-width':'.7'},robG);

// 风机
const fanG=$('g',{},robG);
$('circle',{cx:0,cy:0,r:15,fill:'#080f1e',stroke:'#1a4060','stroke-width':'1'},fanG);
$('circle',{cx:0,cy:0,r:4,fill:'#14304a',stroke:'#008898','stroke-width':'.8'},fanG);
for(let i=0;i<3;i++){
  $('path',{d:'M0,-3 Q9,-9 5,-15 Q0,-11 -5,-15 Q-9,-9 0,-3Z',fill:'#1a4a60',opacity:'.8',transform:`rotate(${i*120})`},fanG);
}

// 裙边
const skirt=$('path',{
  d:`M${-BW*.38},${BH/2} Q${-BW*.22},${BH/2+12} 0,${BH/2+7} Q${BW*.22},${BH/2+12} ${BW*.38},${BH/2}`,
  fill:'none',stroke:'#00b8d0','stroke-width':'1.5','stroke-dasharray':'4 3'
},robG);

// 轮子
const whls=[];
[[-BW/2+20,BH/2+2],[BW/2-20,BH/2+2]].forEach(([cx,cy])=>{
  const wg=$('g',{transform:`translate(${cx},${cy})`},robG);
  $('circle',{cx:0,cy:0,r:WR,fill:'#0a1420',stroke:'#008898','stroke-width':'1.4'},wg);
  const rl=[];for(let i=0;i<6;i++){
    const a=i*60*Math.PI/180;
    const l=$('line',{x1:-Math.cos(a)*WR*.6,y1:-Math.sin(a)*WR*.6,x2:Math.cos(a)*WR*.6,y2:Math.sin(a)*WR*.6,
      stroke:'#163a4a','stroke-width':'1.5','stroke-linecap':'round',transform:'rotate(45)'},wg);
    rl.push(l);
  }
  $('circle',{cx:0,cy:0,r:3,fill:'#008898'},wg);
  whls.push({g:wg,lines:rl});
});

// 气流箭头
const flG=$('g',{},robG);
const flA=[];
for(let i=0;i<8;i++){
  const a=$('path',{d:'M0,0',fill:'none',stroke:'#ff8820','stroke-width':'1','stroke-dasharray':'5 4',opacity:'0'},flG);
  flA.push(a);
}

/* ===== 机械足 ===== */
const legG=$('g',{},robG);

// 足1
const l1G=$('g',{},legG);
const l1L1=$('line',{x1:0,y1:0,x2:0,y2:0,stroke:'#00a8c0','stroke-width':'5','stroke-linecap':'round'},l1G);
const l1L2=$('line',{x1:0,y1:0,x2:0,y2:0,stroke:'#00d0e0','stroke-width':'2.5','stroke-linecap':'round'},l1G);
const l1J=$('circle',{cx:0,cy:0,r:4,fill:'#0a1420',stroke:'#00b0c0','stroke-width':'1.3'},l1G);
const l1CG=$('g',{},l1G);
const l1CE=$('ellipse',{cx:0,cy:0,rx:11,ry:5.5,fill:'#1a2a40',stroke:'#4a7090','stroke-width':'1'},l1CG);
const l1CGl=$('ellipse',{cx:0,cy:0,rx:15,ry:9,fill:'url(#sG)',opacity:'0'},l1CG);

// 足2
const l2G=$('g',{},legG);
const l2L1=$('line',{x1:0,y1:0,x2:0,y2:0,stroke:'#0090a8','stroke-width':'4','stroke-linecap':'round'},l2G);
const l2L2=$('line',{x1:0,y1:0,x2:0,y2:0,stroke:'#00b8c8','stroke-width':'2','stroke-linecap':'round'},l2G);
const l2J=$('circle',{cx:0,cy:0,r:3,fill:'#0a1420',stroke:'#0090a8','stroke-width':'1'},l2G);
const l2CG=$('g',{},l2G);
const l2CE=$('ellipse',{cx:0,cy:0,rx:9,ry:4.5,fill:'#1a2a40',stroke:'#4a7090','stroke-width':'1'},l2CG);
const l2CGl=$('ellipse',{cx:0,cy:0,rx:13,ry:7,fill:'url(#sG)',opacity:'0'},l2CG);

// 关节圆(足根部)
const l1Base=$('circle',{cx:0,cy:0,r:5,fill:'#0c1828',stroke:'#00c0d0','stroke-width':'1.5'},l1G);
const l2Base=$('circle',{cx:0,cy:0,r:4,fill:'#0c1828',stroke:'#0090a8','stroke-width':'1.2'},l2G);

/* ===== 标注 ===== */
const anG=$('g',{id:'anno'});

// IFR标注
const ifrG=$('g',{opacity:'0'},anG);
$('rect',{x:790,y:28,width:385,height:60,rx:8,fill:'rgba(0,255,136,.05)',stroke:'#00ff88','stroke-width':'1','stroke-dasharray':'6 3'},ifrG);
$('text',{x:810,y:48,fill:'#00ff88','font-size':'12','font-family':'Orbitron,monospace','font-weight':'700'},ifrG).textContent='IFR: IDEAL FINAL RESULT';
$('text',{x:810,y:68,fill:'#70c898','font-size':'11','font-family':'Noto Sans SC,sans-serif'},ifrG).textContent='以"场"代"物" — 负压场替代复杂铰接机械臂实现附着';

// 参数标注
const pmG=$('g',{opacity:'0'},anG);
$('rect',{x:790,y:96,width:385,height:50,rx:8,fill:'rgba(0,160,200,.05)',stroke:'#0090a8','stroke-width':'1','stroke-dasharray':'6 3'},pmG);
const pmT1=$('text',{x:810,y:116,fill:'#00c0d0','font-size':'11','font-family':'Orbitron,monospace'},pmG);
pmT1.textContent='VACUUM: -5.0kPa  |  STROKE: 200mm';
$('text',{x:810,y:134,fill:'#4a8098','font-size':'10','font-family':'Noto Sans SC,sans-serif'},pmG).textContent='吸附力与台阶几何形状无关 — 通用性核心';

// 连接线
const ifrLn=$('line',{x1:790,y1:65,x2:400,y2:400,stroke:'#00ff88','stroke-width':'.7','stroke-dasharray':'4 4',opacity:'0'},anG);

// 阶段文字
const phTxt=$('text',{x:600,y:688,fill:'#ff9030','font-size':'13','font-family':'Orbitron,monospace','font-weight':'700','text-anchor':'middle',opacity:'.85'},anG);

// 真空表
const gaG=$('g',{transform:'translate(72,72)'},anG);
$('circle',{cx:0,cy:0,r:42,fill:'rgba(8,14,28,.9)',stroke:'#1a3050','stroke-width':'1.5'},gaG);
$('text',{x:0,y:-15,fill:'#3a6a8a','font-size':'8','text-anchor':'middle','font-family':'Orbitron,monospace'},gaG).textContent='VACUUM kPa';
const gaNd=$('line',{x1:0,y1:5,x2:0,y2:-30,stroke:'#ff8800','stroke-width':'2','stroke-linecap':'round'},gaG);
$('circle',{cx:0,cy:0,r:3.5,fill:'#ff8800'},gaG);
const gaVl=$('text',{x:0,y:24,fill:'#00d0e0','font-size':'11','text-anchor':'middle','font-family':'Orbitron,monospace','font-weight':'700'},gaG);
gaVl.textContent='-5.0';
for(let i=0;i<=8;i++){
  const a=(-140+i*35)*Math.PI/180;
  $('line',{x1:Math.cos(a)*33,y1:Math.sin(a)*33,x2:Math.cos(a)*38,y2:Math.sin(a)*38,stroke:'#2a4a6a','stroke-width':'1'},gaG);
}

// 失效边界提示
const failG=$('g',{opacity:'0'},anG);
$('rect',{x:790,y:154,width:385,height:44,rx:8,fill:'rgba(255,60,60,.04)',stroke:'#ff4040','stroke-width':'.8','stroke-dasharray':'5 3'},failG);
$('text',{x:810,y:172,fill:'#ff6060','font-size':'10','font-family':'Orbitron,monospace'},failG).textContent='FAIL: 镂空/积水/厚地毯 → 负压无法建立';
$('text',{x:810,y:188,fill:'#a04040','font-size':'9','font-family':'Noto Sans SC,sans-serif'},failG).textContent='极度依赖表面平整度与气密性';

/* ===== 动画状态 ===== */
let aT=0,press=5,spd=1,playing=true,lastT=0;

function getPhase(ct){
  if(ct<.10) return{p:'vacOn',t:ct/.10,lbl:'负压吸附'};
  if(ct<.35) return{p:'legEx',t:(ct-.10)/.25,lbl:'机械足伸出'};
  if(ct<.45) return{p:'sucOn',t:(ct-.35)/.10,lbl:'吸盘附着'};
  if(ct<.72) return{p:'pull',t:(ct-.45)/.27,lbl:'收缩提拉'};
  if(ct<.85) return{p:'settle',t:(ct-.72)/.13,lbl:'轮子跟随'};
  if(ct<.95) return{p:'rel',t:(ct-.85)/.10,lbl:'吸盘释放'};
  return{p:'rst',t:(ct-.95)/.05,lbl:'准备下一步'};
}

function update(dt){
  const cMs=CYC/spd;
  aT+=dt*spd;
  
  const nCl=NS4-1;
  const tot=nCl*cMs;
  const lt=aT%tot;
  const si=Math.min(Math.floor(lt/cMs),nCl-1);
  const ct=(lt%cMs)/cMs;
  
  const ph=getPhase(ct);
  const cp=rPos(si), np=rPos(si+1);
  const pf=press/5;
  
  let rx=cp.x,ry=cp.y,legE=0,vOp=0,sucOn=0,wRot=0;
  
  switch(ph.p){
    case'vacOn': vOp=easeO(ph.t); break;
    case'legEx': vOp=1; legE=easeO(ph.t); break;
    case'sucOn': vOp=1; legE=1; sucOn=easeO(ph.t); break;
    case'pull':{
      vOp=1; sucOn=1;
      const pt=easeIO(ph.t);
      rx=lerp(cp.x,np.x,pt); ry=lerp(cp.y,np.y,pt);
      legE=1-pt*.85; wRot=ph.t*200;
      break;
    }
    case'settle': vOp=1; sucOn=1-ph.t*.6; rx=np.x; ry=np.y; legE=.15*(1-ph.t); wRot+=ph.t*80; break;
    case'rel': vOp=1-ph.t*.3; sucOn=Math.max(0,.4-ph.t); rx=np.x; ry=np.y; break;
    case'rst': vOp=.7+ph.t*.3; rx=np.x; ry=np.y; break;
  }
  vOp*=pf;
  
  // 机器人位置
  robG.setAttribute('transform',`translate(${rx},${ry})`);
  
  // 负压场
  vZone.setAttribute('opacity',vOp*.7);
  vRing.setAttribute('opacity',vOp*.5);
  const rs2=1+.1*Math.sin(aT*.004);
  vRing.setAttribute('rx',BW*.42*rs2);
  vRing.setAttribute('ry',20*rs2);
  
  // 裙边颜色
  if(vOp>.3){skirt.setAttribute('stroke','#ff9030');skirt.setAttribute('filter','url(#gO)')}
  else{skirt.setAttribute('stroke','#00b8d0');skirt.removeAttribute('filter')}
  
  // 风机
  fanG.setAttribute('transform',`rotate(${(aT*.3)%360})`);
  
  // 轮子
  whls.forEach((w,wi)=>{
    const ba=wi===0?45:-45;
    w.lines.forEach((l,li)=>l.setAttribute('transform',`rotate(${ba+wRot+li*60})`));
  });
  
  // 机械足
  const pvX=BW/2-2, pvY=-BH/2+10;
  const ns=steps[Math.min(si+1,NS4-1)];
  const cwX=ns.x+ns.w*.25, cwY=ns.y;
  const clX=cwX-rx, clY=cwY-ry;
  const rtx=pvX+12, rty=pvY-35;
  
  // 足1
  const c1x=lerp(rtx,clX,legE), c1y=lerp(rty,clY,legE);
  l1G.setAttribute('transform',`translate(${pvX},${pvY})`);
  l1L1.setAttribute('x2',c1x-pvX); l1L1.setAttribute('y2',c1y-pvY);
  l1L2.setAttribute('x2',c1x-pvX); l1L2.setAttribute('y2',c1y-pvY);
  l1J.setAttribute('cx',(c1x-pvX)*.5); l1J.setAttribute('cy',(c1y-pvY)*.5);
  l1CG.setAttribute('transform',`translate(${c1x-pvX},${c1y-pvY})`);
  l1CGl.setAttribute('opacity',sucOn*pf);
  l1CE.setAttribute('stroke',sucOn>.3?'#ff9030':'#4a7090');
  l1CE.setAttribute('fill',sucOn>.3?'#2a1a08':'#1a2a40');
  
  // 足2
  const pv2X=pvX-20, pv2Y=pvY+6;
  const c2x=lerp(pv2X+8,clX-14,legE), c2y=lerp(pv2Y-28,clY+2,legE);
  l2G.setAttribute('transform',`translate(${pv2X},${pv2Y})`);
  l2L1.setAttribute('x2',c2x-pv2X); l2L1.setAttribute('y2',c2y-pv2Y);
  l2L2.setAttribute('x2',c2x-pv2X); l2L2.setAttribute('y2',c2y-pv2Y);
  l2J.setAttribute('cx',(c2x-pv2X)*.5); l2J.setAttribute('cy',(c2y-pv2Y)*.5);
  l2CG.setAttribute('transform',`translate(${c2x-pv2X},${c2y-pv2Y})`);
  l2CGl.setAttribute('opacity',sucOn*pf*.7);
  l2CE.setAttribute('stroke',sucOn>.3?'#ff9030':'#4a7090');
  
  // 气流箭头
  flA.forEach((fa,i)=>{
    if(vOp<.2){fa.setAttribute('opacity','0');return}
    const ag=(i/8)*Math.PI*2+aT*.002;
    const sR=55+10*Math.sin(aT*.003+i);
    const eR=18;
    const sx=Math.cos(ag)*sR, sy=BH/2+7+Math.sin(ag)*16;
    const ex=Math.cos(ag)*eR, ey=BH/2+7+Math.sin(ag)*6;
    fa.setAttribute('d',`M${sx},${sy} L${ex},${ey}`);
    fa.setAttribute('opacity',vOp*.55);
    fa.setAttribute('stroke-dashoffset',-aT*.05);
  });
  
  // 粒子
  if(vOp>.3 && parts.length<MAXP && Math.random()<.35*pf){
    const ag=Math.random()*Math.PI*2;
    const d=30+Math.random()*50;
    parts.push({
      x:rx+Math.cos(ag)*d, y:ry+BH/2+4+Math.random()*16,
      life:0, ml:.5+Math.random()*.6, sp:25+Math.random()*35, sz:1+Math.random()*2,
      el:null
    });
  }
  for(let i=parts.length-1;i>=0;i--){
    const p=parts[i];
    p.life+=dt*.001;
    if(p.life>p.ml){
      if(p.el&&p.el.parentNode) ptG.removeChild(p.el);
      parts.splice(i,1);continue;
    }
    const lt2=p.life/p.ml;
    p.x=lerp(p.x,rx,lt2*.08*pf);
    p.y=lerp(p.y,ry+4,lt2*.08*pf);
    if(!p.el){
      p.el=$('circle',{cx:p.x,cy:p.y,r:p.sz,fill:'#ffaa40',opacity:vOp*.6,filter:'url(#gO)'},ptG);
    }
    p.el.setAttribute('cx',p.x);p.el.setAttribute('cy',p.y);
    p.el.setAttribute('r',p.sz*(1-lt2*.5));
    p.el.setAttribute('opacity',vOp*.65*(1-lt2));
  }
  
  // 标注
  const iOp=clamp(aT/2000,0,1);
  ifrG.setAttribute('opacity',iOp);
  pmG.setAttribute('opacity',iOp);
  failG.setAttribute('opacity',iOp);
  ifrLn.setAttribute('opacity',iOp*.35);
  ifrLn.setAttribute('x2',rx); ifrLn.setAttribute('y2',ry+BH/2+12);
  pmT1.textContent=`VACUUM: -${press.toFixed(1)}kPa  |  STROKE: 200mm`;
  phTxt.textContent=ph.lbl;
  document.getElementById('phD').textContent=ph.lbl;
  
  // 真空表
  const na=-140+(press/8)*280;
  gaNd.setAttribute('transform',`rotate(${na})`);
  gaVl.textContent=`-${press.toFixed(1)}`;
  
  // IFR高亮
  if(vOp>.6 && ph.p==='vacOn'){
    const bk=.7+.3*Math.sin(aT*.01);
    ifrG.setAttribute('opacity',iOp*bk);
  }
}

/* ===== 主循环 ===== */
function loop(ts){
  if(!lastT) lastT=ts;
  const dt=Math.min(ts-lastT,50);
  lastT=ts;
  if(playing) update(dt);
  requestAnimationFrame(loop);
}

/* ===== 控件 ===== */
document.getElementById('slP').addEventListener('input',function(){
  press=parseFloat(this.value);
  document.getElementById('vP').textContent=`-${press.toFixed(1)}kPa`;
});
document.getElementById('slS').addEventListener('input',function(){
  spd=parseFloat(this.value);
  document.getElementById('vS').textContent=`${spd.toFixed(1)}x`;
});
document.getElementById('bP').addEventListener('click',function(){
  playing=!playing;this.textContent=playing?'PAUSE':'PLAY';
});
document.getElementById('bR').addEventListener('click',function(){
  aT=0;lastT=0;playing=true;
  document.getElementById('bP').textContent='PAUSE';
  while(ptG.firstChild) ptG.removeChild(ptG.firstChild);
  parts.length=0;
});

/* ===== 启动 ===== */
requestAnimationFrame(loop);

})();
</script>
</body>
</html>

这个实现涵盖了以下核心要素:

动画原理展示

  • 完整呈现负压吸附爬楼的六步时序:负压吸附 → 机械足伸出 → 吸盘附着 → 收缩提拉 → 轮子跟随 → 吸盘释放,循环往复爬升三级台阶后自动重播。
  • 真空场用橙色径向渐变辉光 + 脉冲环 + 8条气流箭头 + 粒子系统三层叠加可视化,直观呈现"无形之手"的吸附效应。
  • 双机械足从机身前侧伸出/收缩,吸盘激活时切换为橙色高亮并带辉光,清晰展示"抓-拉"节拍。

IFR 思想视觉引导

  • 右上角 IFR 标注框(绿色)直接点明"以场代物"核心矛盾化解思路,带连接线指向负压场区域。
  • 参数框实时显示真空度与行程,并标注"吸附力与台阶几何无关 — 通用性核心"。
  • 红色失效边界框提示镂空/积水/厚地毯场景,限定方案适用范围。
  • 左上角真空表盘随滑块联动,强化参数感知。

交互控制

  • 真空度滑块(-1 ~ -8kPa):实时影响负压场辉光强度、粒子密度和吸盘效果。
  • 速度滑块(0.3x ~ 2.5x):控制整体动画节奏。
  • 暂停/重置按钮:支持手动控制播放流程。
  • 阶段指示器同步显示当前动作名称。

视觉风格

  • 深蓝底色 + 网格蓝图风格,青色结构线 + 橙色创新高亮,营造技术图纸氛围。
  • Orbitron 字体用于标签,Noto Sans SC 用于中文说明,层次分明。
积分规则:第一轮对话扣减8分,后续每轮扣6分