分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<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=Rajdhani:wght@300;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{
  background:#060A14;
  color:#B0C4D8;
  font-family:'Rajdhani',sans-serif;
  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:18px 16px 24px}
#header{text-align:center;margin-bottom:12px}
#header h1{
  font-weight:700;font-size:26px;color:#00E5FF;letter-spacing:3px;text-transform:uppercase;
}
#header .sub{
  font-family:'Share Tech Mono',monospace;font-size:13px;color:#3E6078;margin-top:2px;letter-spacing:1px;
}
#svg-wrap{
  width:100%;max-width:1360px;
  background:linear-gradient(145deg,#080D1A 0%,#0C1324 50%,#080D1A 100%);
  border:1px solid #152038;border-radius:10px;overflow:hidden;
  box-shadow:0 0 60px rgba(0,229,255,0.04),inset 0 0 80px rgba(0,0,0,0.3);
}
#svg-wrap svg{width:100%;height:auto;display:block}
#controls{
  width:100%;max-width:1360px;margin-top:14px;
  display:flex;align-items:center;flex-wrap:wrap;gap:18px;
  padding:14px 22px;background:#0B1120;border:1px solid #152038;border-radius:8px;
}
.cg{display:flex;align-items:center;gap:7px}
.cl{font-family:'Share Tech Mono',monospace;font-size:12px;color:#3E6078;white-space:nowrap}
.cv{font-family:'Share Tech Mono',monospace;font-size:12px;color:#00E5FF;min-width:56px}
input[type=range]{
  -webkit-appearance:none;height:3px;background:#152038;border-radius:2px;outline:none;width:110px;
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:13px;height:13px;background:#00E5FF;border-radius:50%;cursor:pointer;
  box-shadow:0 0 6px rgba(0,229,255,0.5);
}
#phase-box{
  margin-left:auto;display:flex;align-items:center;gap:8px;
}
#phase-dot{width:8px;height:8px;border-radius:50%;background:#FF9100;box-shadow:0 0 8px rgba(255,145,0,0.6);
  animation:pulse-dot 1.2s ease-in-out infinite}
@keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.4}}
#phase-text{font-family:'Share Tech Mono',monospace;font-size:13px;color:#FF9100}
.cbtn{
  font-family:'Rajdhani',sans-serif;font-weight:700;font-size:13px;
  padding:5px 14px;background:transparent;color:#00E5FF;
  border:1px solid rgba(0,229,255,0.35);border-radius:4px;cursor:pointer;
  transition:all .2s;letter-spacing:1px;
}
.cbtn:hover{background:rgba(0,229,255,0.08);border-color:#00E5FF}
</style>
</head>
<body>
<div id="app">
  <div id="header">
    <h1>负压吸附爬楼机器人</h1>
    <div class="sub">IFR 最终理想解原理演示 — 以负压场替代复杂机械结构</div>
  </div>
  <div id="svg-wrap">
    <svg id="scene" viewBox="0 0 1400 780" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
  <div id="controls">
    <div class="cg">
      <span class="cl">播放速度</span>
      <input type="range" id="sld-speed" min="0.3" max="3" step="0.1" value="1">
      <span class="cv" id="val-speed">1.0x</span>
    </div>
    <div class="cg">
      <span class="cl">负压强度</span>
      <input type="range" id="sld-pressure" min="500" max="3000" step="100" value="2000">
      <span class="cv" id="val-pressure">-2000Pa</span>
    </div>
    <button class="cbtn" id="btn-reset">重置动画</button>
    <div id="phase-box">
      <div id="phase-dot"></div>
      <span id="phase-text">初始化</span>
    </div>
  </div>
</div>

<script>
(function(){
/* ========== 常量与配置 ========== */
const NS='http://www.w3.org/2000/svg';
const STEP_TREAD=290, STEP_RISER=155, STAIR_X0=80, GROUND_Y=660;
const NUM_STEPS=4; // 可爬台阶数
const RW=165, RH=52, SKIRT_H=13, WHEEL_R=12;

/* 台阶数据 */
const steps=[];
for(let i=0;i<=NUM_STEPS;i++){
  steps.push({sy:GROUND_Y-i*STEP_RISER, lx:STAIR_X0+i*STEP_TREAD});
}

/* 机器人在台阶i上的位置(body中心底部) */
function robotPos(i){
  return {x:steps[i].lx+STEP_TREAD-RW/2-28, y:steps[i].sy};
}

/* 动画阶段 */
const PHASES=[
  {name:'负压建立',  dur:1800},
  {name:'机械足伸出',dur:2400},
  {name:'吸盘锁定',  dur:1200},
  {name:'收缩提拉',  dur:2800},
  {name:'就位稳定',  dur:1400},
];

/* 状态 */
let curStep=0, phase=0, pp=0, speed=1, pressureVal=2000;
let lastTS=0, running=true;
let fanAngle=0, particleTime=0;

/* ========== SVG 辅助 ========== */
function ce(tag,attrs,parent){
  const el=document.createElementNS(NS,tag);
  if(attrs) for(const[k,v]of Object.entries(attrs)) el.setAttribute(k,v);
  if(parent) parent.appendChild(el);
  return el;
}
function sa(el,attrs){for(const[k,v]of Object.entries(attrs))el.setAttribute(k,v)}

const svg=document.getElementById('scene');

/* ========== 定义滤镜与渐变 ========== */
const defs=ce('defs',null,svg);

/* 发光滤镜 */
const fGlow=ce('filter',{id:'glowCyan',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
ce('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'6',result:'b'},fGlow);
const fm1=ce('feMerge',null,fGlow);
ce('feMergeNode',{in:'b'},fm1);ce('feMergeNode',{in:'SourceGraphic'},fm1);

const fGlowO=ce('filter',{id:'glowOrange',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
ce('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'b'},fGlowO);
const fm2=ce('feMerge',null,fGlowO);
ce('feMergeNode',{in:'b'},fm2);ce('feMergeNode',{in:'SourceGraphic'},fm2);

const fGlowBig=ce('filter',{id:'glowBig',x:'-80%',y:'-80%',width:'260%',height:'260%'},defs);
ce('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'14',result:'b'},fGlowBig);
const fm3=ce('feMerge',null,fGlowBig);
ce('feMergeNode',{in:'b'},fm3);ce('feMergeNode',{in:'SourceGraphic'},fm3);

/* 压力区渐变 */
const gPressure=ce('linearGradient',{id:'gradPressure',x1:'0',y1:'0',x2:'0',y2:'1'},defs);
ce('stop',{offset:'0%','stop-color':'#00E5FF','stop-opacity':'0.35'},gPressure);
ce('stop',{offset:'100%','stop-color':'#00E5FF','stop-opacity':'0.08'},gPressure);

/* 台阶截面填充 */
const gStair=ce('linearGradient',{id:'gradStair',x1:'0',y1:'0',x2:'1',y2:'1'},defs);
ce('stop',{offset:'0%','stop-color':'#141E30'},gStair);
ce('stop',{offset:'100%','stop-color':'#0F1724'},gStair);

/* 机器人身体渐变 */
const gBody=ce('linearGradient',{id:'gradBody',x1:'0',y1:'0',x2:'0',y2:'1'},defs);
ce('stop',{offset:'0%','stop-color':'#4A5C72'},gBody);
ce('stop',{offset:'100%','stop-color':'#2E3D50'},gBody);

/* 吸盘渐变(激活) */
const gCupOn=ce('radialGradient',{id:'gradCupOn'},defs);
ce('stop',{offset:'0%','stop-color':'#00E5FF','stop-opacity':'0.9'},gCupOn);
ce('stop',{offset:'70%','stop-color':'#00B8D4','stop-opacity':'0.5'},gCupOn);
ce('stop',{offset:'100%','stop-color':'#00838F','stop-opacity':'0.2'},gCupOn);

/* 网格图案 */
const patGrid=ce('pattern',{id:'patGrid',width:40,height:40,patternUnits:'userSpaceOnUse'},defs);
ce('line',{x1:0,y1:0,x2:40,y2:0,stroke:'#0E1828','stroke-width':'0.5'},patGrid);
ce('line',{x1:0,y1:0,x2:0,y2:40,stroke:'#0E1828','stroke-width':'0.5'},patGrid);

/* 截面填充图案 */
const patHatch=ce('pattern',{id:'patHatch',width:8,height:8,patternUnits:'userSpaceOnUse',patternTransform:'rotate(45)'},defs);
ce('line',{x1:0,y1:0,x2:0,y2:8,stroke:'#1A2A40','stroke-width':'1.5'},patHatch);

/* ========== 背景层 ========== */
ce('rect',{x:0,y:0,width:1400,height:780,fill:'#060A14'},svg);
ce('rect',{x:0,y:0,width:1400,height:780,fill:'url(#patGrid)'},svg);

/* ========== 绘制楼梯 ========== */
const gStairs=ce('g',{id:'g-stairs'},svg);

/* 楼梯主体路径 */
let stairPath=`M${STAIR_X0},${GROUND_Y+120} L${STAIR_X0},${GROUND_Y}`;
for(let i=0;i<=NUM_STEPS;i++){
  const s=steps[i];
  stairPath+=` L${s.lx},${s.sy}`;
  if(i<NUM_STEPS) stairPath+=` L${s.lx+STEP_TREAD},${s.sy}`;
}
stairPath+=` L${steps[NUM_STEPS].lx+STEP_TREAD},${steps[NUM_STEPS].sy} L${steps[NUM_STEPS].lx+STEP_TREAD},${GROUND_Y+120} Z`;
ce('path',{d:stairPath,fill:'url(#gradStair)',stroke:'#1E2D44','stroke-width':'1.5'},gStairs);
ce('path',{d:stairPath,fill:'url(#patHatch)',opacity:'0.3'},gStairs);

/* 台阶表面高亮线 */
for(let i=0;i<=NUM_STEPS;i++){
  const s=steps[i];
  ce('line',{x1:s.lx,y1:s.sy,x2:s.lx+STEP_TREAD,y2:s.sy,stroke:'#2A3F5C','stroke-width':'2'},gStairs);
  /* 踢面线 */
  if(i>0){
    ce('line',{x1:s.lx,y1:s.sy,x2:s.lx,y2:steps[i-1].sy,stroke:'#2A3F5C','stroke-width':'1.5'},gStairs);
  }
}

/* 台阶标注 */
const gAnn=ce('g',{id:'g-ann','font-family':'Share Tech Mono, monospace','font-size':'11',fill:'#2E4A64'},svg);
/* 踏步深度标注 */
ce('line',{x1:steps[1].lx,y1:GROUND_Y+30,x2:steps[1].lx+STEP_TREAD,y2:GROUND_Y+30,stroke:'#1E3050','stroke-width':'1','stroke-dasharray':'3,3'},gAnn);
ce('text',{x:steps[1].lx+STEP_TREAD/2,y:GROUND_Y+44,'text-anchor':'middle'},gAnn).textContent='290mm';
/* 踢面高度标注 */
ce('line',{x1:STAIR_X0-20,y1:steps[0].sy,x2:STAIR_X0-20,y2:steps[1].sy,stroke:'#1E3050','stroke-width':'1','stroke-dasharray':'3,3'},gAnn);
ce('text',{x:STAIR_X0-24,y:(steps[0].sy+steps[1].sy)/2+4,'text-anchor':'end'},gAnn).textContent='155mm';

/* ========== 机器人组 ========== */
const gRobot=ce('g',{id:'g-robot'},svg);

/* 负压发光层(裙边下方) */
const gPressureGlow=ce('g',{id:'g-pressure-glow'},gRobot);

/* 身体 */
const gBody_=ce('g',{id:'g-body'},gRobot);
ce('rect',{x:-RW/2,y:-RH-SKIRT_H,width:RW,height:RH,rx:6,ry:6,fill:'url(#gradBody)',stroke:'#5A7090','stroke-width':'1.2'},gBody_);

/* 裙边 */
const skirtPts=[
  `${-RW/2-10},${-SKIRT_H}`,
  `${RW/2+10},${-SKIRT_H}`,
  `${RW/2+6},0`,
  /* 波浪底边 */
  `${RW/2+2},4 ${RW/2-8},2 ${RW/2-20},5 ${RW/2-40},3 ${RW/2-60},5 ${RW/2-80},3 ${RW/2-100},5 ${RW/2-120},3 ${RW/2-140},5 ${RW/2-155},4 ${RW/2-165},6`,
  `${-RW/2+155},6 ${-RW/2+165},4`,
  `${-RW/2+140},5 ${-RW/2+120},3 ${-RW/2+100},5 ${-RW/2+80},3 ${-RW/2+60},5 ${-RW/2+40},3 ${-RW/2+20},5 ${-RW/2+8},2 ${-RW/2-2},4`,
  `${-RW/2-6},0`,
].join(' ');
ce('path',{d:`M${skirtPts} Z`,fill:'#1E2D42',stroke:'#3A5070','stroke-width':'0.8',opacity:'0.9'},gBody_);

/* 车体细节线 */
ce('line',{x1:-RW/2+15,y1:-RH-SKIRT_H+8,x2:RW/2-15,y2:-RH-SKIRT_H+8,stroke:'#3A5070','stroke-width':'0.6'},gBody_);
/* 指示灯 */
ce('circle',{cx:-RW/2+20,cy:-RH-SKIRT_H+18,r:3,fill:'#00E5FF',opacity:'0.7',id:'led1'},gBody_);
ce('circle',{cx:-RW/2+30,cy:-RH-SKIRT_H+18,r:3,fill:'#FF9100',opacity:'0.5',id:'led2'},gBody_);

/* 麦克纳姆轮 */
function drawWheel(cx,cy,parent){
  const gw=ce('g',null,parent);
  ce('circle',{cx,cy,r:WHEEL_R,fill:'#1A2538',stroke:'#4A6080','stroke-width':'1.2'},gw);
  /* X花纹 */
  ce('line',{x1:cx-WHEEL_R+3,y1:cy,x2:cx+WHEEL_R-3,y2:cy,stroke:'#3A5070','stroke-width':'1'},gw);
  ce('line',{x1:cx,y1:cy-WHEEL_R+3,x2:cx,y2:cy+WHEEL_R-3,stroke:'#3A5070','stroke-width':'1'},gw);
  ce('line',{x1:cx-5,y1:cy-5,x2:cx+5,y2:cy+5,stroke:'#3A5070','stroke-width':'0.7'},gw);
  ce('line',{x1:cx+5,y1:cy-5,x2:cx-5,y2:cy+5,stroke:'#3A5070','stroke-width':'0.7'},gw);
  ce('circle',{cx,cy,r:2.5,fill:'#4A6080'},gw);
  return gw;
}
drawWheel(-62,6,gBody_);
drawWheel(62,6,gBody_);

/* 风扇 */
const gFan=ce('g',{id:'g-fan'},gBody_);
ce('circle',{cx:0,cy:-RH/2-SKIRT_H,r:16,fill:'#0D1422',stroke:'#2A3F5C','stroke-width':'1'},gFan);
for(let a=0;a<6;a++){
  const ang=a*60*Math.PI/180;
  const x1=3*Math.cos(ang),y1=3*Math.sin(ang)-RH/2-SKIRT_H;
  const x2=13*Math.cos(ang),y2=13*Math.sin(ang)-RH/2-SKIRT_H;
  ce('line',{x1,y1,x2,y2,stroke:'#3A6080','stroke-width':'2.5','stroke-linecap':'round'},gFan);
}
ce('circle',{cx:0,cy:-RH/2-SKIRT_H,r:3,fill:'#2A3F5C'},gFan);

/* ========== 机械足组 ========== */
const gLegs=ce('g',{id:'g-legs'},gRobot);

/* 两条腿:主腿(更粗)与副腿 */
function createLeg(offsetY,color){
  const g=ce('g',null,gLegs);
  /* 基座 */
  ce('rect',{x:RW/2-2,y:-RH-SKIRT_H+offsetY-5,width:14,height:10,rx:2,fill:'#2E3D50',stroke:'#4A6080','stroke-width':'0.8'},g);
  /* 伸缩段1(外筒) */
  const s1=ce('rect',{x:RW/2,y:-RH-SKIRT_H+offsetY-5,width:10,height:20,rx:1.5,fill:color,stroke:'#5A3820','stroke-width':'0.8'},g);
  s1.id='leg-s1-'+offsetY;
  /* 伸缩段2(内筒) */
  const s2=ce('rect',{x:RW/2+1.5,y:-RH-SKIRT_H+offsetY-25,width:7,height:20,rx:1,fill:color,stroke:'#5A3820','stroke-width':'0.6',opacity:'0.9'},g);
  s2.id='leg-s2-'+offsetY;
  /* 伸缩段3(最内) */
  const s3=ce('rect',{x:RW/2+2.5,y:-RH-SKIRT_H+offsetY-45,width:5,height:20,rx:1,fill:color,stroke:'#5A3820','stroke-width':'0.5',opacity:'0.8'},g);
  s3.id='leg-s3-'+offsetY;
  /* 吸盘 */
  const cup=ce('ellipse',{cx:RW/2+5,cy:-RH-SKIRT_H+offsetY-50,rx:9,ry:5,fill:'#3A2510',stroke:'#5A3820','stroke-width':'0.8'},g);
  cup.id='cup-'+offsetY;
  /* 吸盘发光 */
  const cupGlow=ce('ellipse',{cx:RW/2+5,cy:-RH-SKIRT_H+offsetY-50,rx:14,ry:9,fill:'none',stroke:'#00E5FF','stroke-width':'1.5',opacity:'0',filter:'url(#glowCyan)'},g);
  cupGlow.id='cup-glow-'+offsetY;
  return g;
}
createLeg(12,'#D46808');
createLeg(28,'#C05A06');

/* ========== 效果层 ========== */
const gFX=ce('g',{id:'g-fx'},svg);

/* 负压场发光体 */
const pressureZone=ce('rect',{x:-RW/2-6,y:0,width:RW+12,height:SKIRT_H,rx:3,fill:'url(#gradPressure)',opacity:'0',filter:'url(#glowBig)'},gFX);
pressureZone.id='fx-pressure';

/* 负压粒子 */
const particles=[];
for(let i=0;i<18;i++){
  const p=ce('circle',{r:1.5+Math.random(),fill:'#00E5FF',opacity:'0'},gFX);
  particles.push({el:p,phase:Math.random()*Math.PI*2,speed:0.5+Math.random()*1.5,rad:20+Math.random()*60});
}

/* 吸附力线(从裙边到中心) */
const suctionLines=[];
for(let i=0;i<5;i++){
  const l=ce('line',{stroke:'#00E5FF','stroke-width':'0.8',opacity:'0','stroke-dasharray':'4,6'},gFX);
  suctionLines.push(l);
}

/* 锁定闪光 */
const lockFlash=ce('circle',{r:0,fill:'none',stroke:'#00E5FF','stroke-width':'2',opacity:'0',filter:'url(#glowCyan)'},gFX);
lockFlash.id='fx-lock';

/* 拉力箭头 */
const pullArrow=ce('g',{opacity:'0'},gFX);
ce('line',{x1:0,y1:0,x2:0,y2:-40,stroke:'#FF9100','stroke-width':'2','stroke-dasharray':'5,4'},pullArrow);
ce('polygon',{points:'-5,-40 5,-40 0,-48',fill:'#FF9100'},pullArrow);

/* ========== 信息标注层 ========== */
const gInfo=ce('g',{id:'g-info','font-family':'Share Tech Mono, monospace'},svg);

/* 负压值标注 */
const infoP=ce('g',{opacity:'0'},gInfo);
ce('rect',{x:0,y:0,width:140,height:28,rx:4,fill:'rgba(0,229,255,0.08)',stroke:'rgba(0,229,255,0.25)','stroke-width':'0.8'},infoP);
const infoPText=ce('text',{x:70,y:18,'text-anchor':'middle',fill:'#00E5FF','font-size':'12'},infoP);
infoPText.textContent='负压: -2000Pa';
infoP.id='info-pressure';

/* 行程标注 */
const infoL=ce('g',{opacity:'0'},gInfo);
ce('rect',{x:0,y:0,width:140,height:28,rx:4,fill:'rgba(255,145,0,0.08)',stroke:'rgba(255,145,0,0.25)','stroke-width':'0.8'},infoL);
const infoLText=ce('text',{x:70,y:18,'text-anchor':'middle',fill:'#FF9100','font-size':'12'},infoL);
infoLText.textContent='伸缩: 0mm';
infoL.id='info-leg';

/* 阶段大字 */
const phaseLabel=ce('text',{x:700,y:52,'text-anchor':'middle',fill:'#1A2A40','font-family':'Rajdhani, sans-serif','font-size':'22','font-weight':'700','letter-spacing':'4'},gInfo);
phaseLabel.id='phase-label';

/* IFR 标注 */
const ifrLabel=ce('text',{x:700,y:770,'text-anchor':'middle',fill:'#152038','font-family':'Share Tech Mono, monospace','font-size':'11'},gInfo);
ifrLabel.textContent='IFR: 以负压场为"抓手",消除复杂仿生腿/履带 → 极简构型实现可靠爬楼';

/* ========== 缓动函数 ========== */
function easeInOut(t){return t<0.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}
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,mn,mx){return Math.max(mn,Math.min(mx,v))}

/* ========== 动画核心 ========== */
function resetAnim(){
  curStep=0;phase=0;pp=0;lastTS=0;fanAngle=0;
}

function stepPhase(){
  phase++;
  pp=0;
  if(phase>=PHASES.length){
    phase=0;
    curStep++;
    if(curStep>=NUM_STEPS) curStep=0;
  }
}

function update(dt){
  if(!running) return;
  pp+=dt*speed/PHASES[phase].dur;
  if(pp>=1){pp=1;stepPhase()}

  /* 风扇持续旋转 */
  fanAngle+=dt*0.3*speed;
  particleTime+=dt*0.001*speed;

  /* 更新界面指示 */
  document.getElementById('phase-text').textContent=PHASES[phase].name;
}

/* 获取当前机器人的世界坐标 */
function getRobotWorldPos(){
  const from=robotPos(curStep);
  if(phase<3) return from;
  const to=robotPos(Math.min(curStep+1,NUM_STEPS));
  const t=easeInOut(clamp(pp,0,1));
  /* 提拉运动:带轻微弧线 */
  const x=lerp(from.x,to.x,t);
  const y=lerp(from.y,to.y,t)-Math.sin(t*Math.PI)*18; /* 弧线偏移 */
  return {x,y};
}

/* 获取机械足参数 */
function getLegParams(){
  /* extension: 0=收回, 1=完全伸出 */
  let ext=0;
  if(phase===1) ext=easeOut(clamp(pp,0,1));
  else if(phase===2) ext=1;
  else if(phase===3) ext=1-easeIn(clamp(pp,0,1));
  return {ext};
}

/* 获取负压强度 0~1 */
function getPressureIntensity(){
  if(phase===0) return easeOut(clamp(pp,0,1));
  if(phase>=1&&phase<=3) return 1;
  if(phase===4) return 1-easeIn(clamp(pp,0,1))*0.15;
  return 0;
}

/* 吸盘锁定状态 */
function isCupLocked(){
  return phase===2||(phase===3&&pp<0.8);
}

/* ========== 渲染 ========== */
function render(){
  const pos=getRobotWorldPos();
  const legP=getLegParams();
  const pInt=getPressureIntensity();
  const cupOn=isCupLocked();

  /* 机器人组变换 */
  let rot=0;
  if(phase===3){
    const t=clamp(pp,0,1);
    rot=Math.sin(t*Math.PI)*12; /* 提拉时轻微仰头 */
  }
  sa(gRobot,{transform:`translate(${pos.x},${pos.y}) rotate(${rot})`});

  /* 风扇旋转 */
  sa(gFan,{transform:`rotate(${fanAngle*180/Math.PI},0,${-RH/2-SKIRT_H})`});

  /* 机械足伸缩 */
  const legOffsets=[12,28];
  legOffsets.forEach((oy,idx)=>{
    const ext=legP.ext;
    const s1h=20+ext*15;
    const s2y=-RH-SKIRT_H+oy-5- ext*55;
    const s2h=20+ext*20;
    const s3y=-RH-SKIRT_H+oy-5- ext*110;
    const s3h=20+ext*25;
    const cupY=-RH-SKIRT_H+oy-5- ext*130;
    const s1=document.getElementById('leg-s1-'+oy);
    const s2=document.getElementById('leg-s2-'+oy);
    const s3=document.getElementById('leg-s3-'+oy);
    const cup=document.getElementById('cup-'+oy);
    const cupG=document.getElementById('cup-glow-'+oy);

    if(s1) sa(s1,{height:s1h});
    if(s2) sa(s2,{y:s2y,height:s2h});
    if(s3) sa(s3,{y:s3y,height:s3h});
    if(cup) sa(cup,{cy:cupY, fill:cupOn?'url(#gradCupOn)':'#3A2510', stroke:cupOn?'#00E5FF':'#5A3820', 'stroke-width':cupOn?'1.5':'0.8'});
    if(cupG) sa(cupG,{cy:cupY, opacity:cupOn?'0.7':'0'});
  });

  /* 负压效果 */
  const pz=document.getElementById('fx-pressure');
  if(pz) sa(pz,{x:pos.x-RW/2-6, y:pos.y-2, opacity:pInt*0.8});

  /* 粒子 */
  particles.forEach((p,i)=>{
    if(pInt>0.1){
      const angle=p.phase+particleTime*p.speed;
      const cx=pos.x+Math.cos(angle)*p.rad*0.5;
      const cy=pos.y-4+Math.sin(angle)*3;
      const targetX=pos.x;
      const targetY=pos.y-RH/2-SKIRT_H;
      const t2=((particleTime*p.speed+i*0.3)%1);
      const px=lerp(cx,targetX,t2);
      const py=lerp(cy,targetY,t2);
      sa(p.el,{cx:px,cy:py,opacity:pInt*0.5*(1-t2)});
    }else{
      sa(p.el,{opacity:0});
    }
  });

  /* 吸力线 */
  suctionLines.forEach((l,i)=>{
    if(pInt>0.2){
      const spread=(i-2)*30;
      const bx=pos.x+spread;
      const by=pos.y-2;
      const tx=pos.x+spread*0.2;
      const ty=pos.y-RH/2-SKIRT_H;
      const dashOff=-particleTime*60;
      sa(l,{x1:bx,y1:by,x2:tx,y2:ty,opacity:pInt*0.35,'stroke-dashoffset':dashOff});
    }else{
      sa(l,{opacity:0});
    }
  });

  /* 锁定闪光 */
  const lockEl=document.getElementById('fx-lock');
  if(phase===2&&pp<0.5){
    const legExt=1;
    const cupWX=pos.x+RW/2+5;
    const cupWY=pos.y-RH-SKIRT_H+12-5-legExt*130+Math.sin(0)*0;
    const flashR=pp*40;
    const flashOp=1-pp*2;
    sa(lockEl,{cx:cupWX,cy:cupWY,r:flashR,opacity:Math.max(0,flashOp)});
  }else{
    sa(lockEl,{opacity:0});
  }

  /* 拉力箭头 */
  if(phase===3&&pp>0.1&&pp<0.9){
    const arrowX=pos.x+RW/2+5;
    const arrowY=pos.y-RH-SKIRT_H+12-5-1*130;
    sa(pullArrow,{transform:`translate(${arrowX},${arrowY+20})`,opacity:0.7+Math.sin(particleTime*8)*0.3});
  }else{
    sa(pullArrow,{opacity:0});
  }

  /* 信息标注 */
  const infoP_=document.getElementById('info-pressure');
  const infoL_=document.getElementById('info-leg');
  if(pInt>0.1){
    const ipx=pos.x-70;
    const ipy=pos.y-RH-SKIRT_H-48;
    sa(infoP_,{transform:`translate(${ipx},${ipy})`,opacity:1});
    infoPText.textContent=`负压: -${Math.round(pressureVal*pInt)}Pa`;
  }else{
    sa(infoP_,{opacity:0});
  }
  if(legP.ext>0.05){
    const ilx=pos.x+RW/2+24;
    const ily=pos.y-RH-SKIRT_H-20;
    sa(infoL_,{transform:`translate(${ilx},${ily})`,opacity:1});
    infoLText.textContent=`伸缩: ${Math.round(legP.ext*200)}mm`;
  }else{
    sa(infoL_,{opacity:0});
  }

  /* 阶段大字 */
  phaseLabel.textContent=PHASES[phase].name;

  /* LED */
  const led1=document.getElementById('led1');
  const led2=document.getElementById('led2');
  if(led1) sa(led1,{opacity:pInt>0.3?0.9:0.2});
  if(led2) sa(led2,{opacity:phase===3?0.9:0.3});
}

/* ========== 动画循环 ========== */
function loop(ts){
  if(!lastTS) lastTS=ts;
  const dt=Math.min(ts-lastTS,50);
  lastTS=ts;
  update(dt);
  render();
  requestAnimationFrame(loop);
}

/* ========== 交互绑定 ========== */
document.getElementById('sld-speed').addEventListener('input',function(){
  speed=parseFloat(this.value);
  document.getElementById('val-speed').textContent=speed.toFixed(1)+'x';
});
document.getElementById('sld-pressure').addEventListener('input',function(){
  pressureVal=parseInt(this.value);
  document.getElementById('val-pressure').textContent='-'+pressureVal+'Pa';
});
document.getElementById('btn-reset').addEventListener('click',function(){
  resetAnim();
});

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

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

实现说明

本动画以 TRIZ"最终理想解"思想为核心,聚焦展示负压吸附爬楼方案如何以极简构型替代复杂仿生腿/履带

动画设计要点

  1. 五阶段循环:负压建立 → 机械足伸出 → 吸盘锁定 → 收缩提拉 → 就位稳定,自动循环爬升 4 级台阶后重置,全程无需手动触发。

  2. IFR 视觉引导

    • 负压场用大面积青色辉光 + 流向风机的粒子群 + 虚线吸力线三层叠加呈现,让"无形负压"变为可见力场;
    • 吸盘锁定瞬间释放扩散闪光环,突出"抓取"这一破矛盾关键动作;
    • 提拉阶段用橙色脉冲箭头标注拉力方向,体现"负压作抓手、伸缩足作肌肉"的协作。
  3. 交互控制:速度滑块(0.3x3x)和负压强度滑块(5003000Pa)让用户实时体验参数对吸附效果的影响。

  4. 技术标注:动态跟随机器人的负压值和伸缩行程数据框,实时反映当前状态;底部 IFR 原理文字提示理想解逻辑。

  5. 视觉风格:深空蓝图底色 + 网格 + 截面填充图案,营造工程原理图氛围;青色(负压)与橙色(机械)双色系清晰区分两种力的作用域。

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