独立渲染引擎就绪引擎就绪
<!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分
等待动画代码生成...
