<!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=Chakra+Petch:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#060b16;--panel:#0c1221;--border:#162040;
--text:#c5d3e3;--dim:#4a5e78;--accent:#00e0c0;
--sma-hot:#ff6028;--sma-warm:#c84820;--sma-cold:#14586a;
--scale:#00d4a8;--force-lat:#ff3060;--force-fwd:#00ccff;
--metal1:#5e7290;--metal2:#2e3e58;--metal3:#1a2844;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans SC',sans-serif;min-height:100vh;overflow-x:hidden}
header{text-align:center;padding:24px 20px 8px;position:relative;z-index:2}
header h1{font-family:'Chakra Petch',sans-serif;font-size:clamp(22px,3.2vw,38px);font-weight:700;letter-spacing:2px;
background:linear-gradient(90deg,var(--accent),#00ccff,var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
header p{font-size:clamp(12px,1.4vw,16px);color:var(--dim);margin-top:4px;font-weight:300;letter-spacing:.5px}
.main-wrap{display:flex;gap:12px;padding:12px 16px;max-width:1600px;margin:0 auto;align-items:stretch;flex-wrap:wrap}
.anim-box{flex:1 1 680px;min-height:420px;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;position:relative}
.anim-box svg{width:100%;height:100%;display:block}
.side-col{flex:0 0 360px;display:flex;flex-direction:column;gap:12px}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;flex:1;display:flex;flex-direction:column}
.panel h3{font-family:'Chakra Petch',sans-serif;font-size:13px;font-weight:600;color:var(--accent);padding:10px 14px 4px;letter-spacing:1.5px;text-transform:uppercase}
.panel svg{flex:1;width:100%;display:block}
.controls{max-width:1600px;margin:8px auto 20px;padding:0 16px;display:flex;flex-wrap:wrap;gap:10px 20px;align-items:center;justify-content:center}
.ctrl-group{display:flex;align-items:center;gap:6px}
.ctrl-group label{font-size:12px;color:var(--dim);white-space:nowrap;min-width:72px}
.ctrl-group input[type=range]{-webkit-appearance:none;appearance:none;width:120px;height:4px;border-radius:2px;background:var(--border);outline:none}
.ctrl-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;border:2px solid var(--bg)}
.ctrl-val{font-family:'Chakra Petch',sans-serif;font-size:12px;color:var(--accent);min-width:40px;text-align:right}
.btn{font-family:'Chakra Petch',sans-serif;font-size:12px;font-weight:600;padding:6px 16px;border-radius:6px;border:1px solid var(--border);background:var(--panel);color:var(--text);cursor:pointer;letter-spacing:.5px;transition:all .2s}
.btn:hover{border-color:var(--accent);color:var(--accent)}
.btn.active{background:var(--accent);color:var(--bg);border-color:var(--accent)}
.legend{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;padding:4px 16px 0}
.legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--dim)}
.legend-dot{width:10px;height:10px;border-radius:2px}
@media(max-width:900px){.side-col{flex:0 0 100%}.anim-box{min-height:340px}}
</style>
</head>
<body>
<header>
<h1>自涌现蜿蜒推进 · IFR 原理演示</h1>
<p>SMA相变收缩 + 方向性摩擦鳞片 → 无主动控制下的持续蜿蜒前进</p>
</header>
<div class="main-wrap">
<div class="anim-box">
<svg id="mainSvg" viewBox="0 0 1000 480" preserveAspectRatio="xMidYMid meet"></svg>
</div>
<div class="side-col">
<div class="panel">
<h3>截面结构 · 弯曲机理</h3>
<svg id="crossSvg" viewBox="0 0 360 290" preserveAspectRatio="xMidYMid meet"></svg>
</div>
<div class="panel">
<h3>SMA 激活行波</h3>
<svg id="waveSvg" viewBox="0 0 360 200" preserveAspectRatio="xMidYMid meet"></svg>
</div>
</div>
</div>
<div class="controls">
<div class="ctrl-group">
<label>蜿蜒频率</label>
<input type="range" id="freqSlider" min="0.2" max="2" step="0.05" value="0.8">
<span class="ctrl-val" id="freqVal">0.80 Hz</span>
</div>
<div class="ctrl-group">
<label>弯曲幅度</label>
<input type="range" id="ampSlider" min="0.1" max="0.55" step="0.01" value="0.35">
<span class="ctrl-val" id="ampVal">0.35 rad</span>
</div>
<button class="btn active" id="btnPlay">播放中</button>
<button class="btn" id="btnForce">力矢量</button>
<button class="btn" id="btnSMA">SMA高亮</button>
<button class="btn" id="btnTrail">轨迹</button>
</div>
<div class="legend">
<span class="legend-item"><span class="legend-dot" style="background:var(--sma-hot)"></span>SMA受热收缩</span>
<span class="legend-item"><span class="legend-dot" style="background:var(--sma-cold)"></span>SMA马氏体(软)</span>
<span class="legend-item"><span class="legend-dot" style="background:var(--scale)"></span>方向性鳞片</span>
<span class="legend-item"><span class="legend-dot" style="background:var(--force-lat)"></span>侧向地面反力</span>
<span class="legend-item"><span class="legend-dot" style="background:var(--force-fwd)"></span>前进推力</span>
</div>
<script>
/* ============ 配置 ============ */
const NS='http://www.w3.org/2000/svg';
const CFG={
N:14, segLen:30, segW:18, waveLen:6.5,
freq:0.8, amp:0.35,
showForce:true, showSMA:true, showTrail:true, playing:true
};
let time=0, lastTs=0, trail=[];
/* ============ 工具函数 ============ */
function el(tag,attrs,parent){
const e=document.createElementNS(NS,tag);
for(const k in attrs) e.setAttribute(k,attrs[k]);
if(parent) parent.appendChild(e);
return e;
}
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}
function deg(r){return r*180/Math.PI}
function smaColor(t){// t:0=cold,1=hot
const r=Math.round(lerp(20,255,t)),g=Math.round(lerp(88,96,t)),b=Math.round(lerp(106,40,t));
return `rgb(${r},${g},${b})`
}
/* ============ 主动画 SVG ============ */
const mainSvg=document.getElementById('mainSvg');
// defs
const defs=el('defs',{},mainSvg);
// 金属渐变
const mg=el('linearGradient',{id:'metalG',x1:'0',y1:'0',x2:'0',y2:'1'},defs);
el('stop',{offset:'0%','stop-color':'#6a7e98'},mg);
el('stop',{offset:'35%','stop-color':'#3e526e'},mg);
el('stop',{offset:'65%','stop-color':'#2a3c58'},mg);
el('stop',{offset:'100%','stop-color':'#1a2a44'},mg);
// 发光滤镜
const gf=el('filter',{id:'glowF',x:'-40%',y:'-40%',width:'180%',height:'180%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'b'},gf);
const gm=el('feMerge',{},gf);el('feMergeNode',{in:'b'},gm);el('feMergeNode',{in:'SourceGraphic'},gm);
// 力箭头标记
function arrowMarker(id,color){
const m=el('marker',{id:id,viewBox:'0 0 10 10',refX:'9',refY:'5',markerWidth:'6',markerHeight:'6',orient:'auto-start-reverse'},defs);
el('path',{d:'M 0 1 L 10 5 L 0 9 Z',fill:color},m);
}
arrowMarker('arrLat','#ff3060');arrowMarker('arrFwd','#00ccff');
// 图层
const gridLayer=el('g',{id:'gridL',opacity:'0.08'},mainSvg);
const groundLayer=el('g',{id:'groundL'},mainSvg);
const trailLayer=el('g',{id:'trailL'},mainSvg);
const snakeLayer=el('g',{id:'snakeL'},mainSvg);
const forceLayer=el('g',{id:'forceL'},mainSvg);
const labelLayer=el('g',{id:'labelL'},mainSvg);
// 绘制网格
for(let x=0;x<=1000;x+=40) el('line',{x1:x,y1:0,x2:x,y2:480,stroke:'#2a4060','stroke-width':'0.5'},gridLayer);
for(let y=0;y<=480;y+=40) el('line',{x1:0,y1:y,x2:1000,y2:y,stroke:'#2a4060','stroke-width':'0.5'},gridLayer);
// 地面标记(会随时间滚动)
const groundDots=[];
for(let i=0;i<60;i++){
const d=el('circle',{r:'1.5',fill:'#1a3050',opacity:'0.5'},groundLayer);
groundDots.push({el:d,baseX:Math.random()*1200-100,baseY:Math.random()*480});
}
// 蛇身段元素
const segEls=[];
for(let i=0;i<CFG.N;i++){
const g=el('g',{},snakeLayer);
// 身体矩形
const body=el('rect',{x:'0',y:`${-CFG.segW/2}`,width:`${CFG.segLen}`,height:`${CFG.segW}`,rx:'4',fill:'url(#metalG)',stroke:'#3a5070','stroke-width':'0.8'},g);
// 左SMA条
const lSMA=el('rect',{x:'2',y:`${-CFG.segW/2-4}`,width:`${CFG.segLen-4}`,height:'3.5',rx:'1.5',fill:CFG.smaCold||'#14586a'},g);
// 右SMA条
const rSMA=el('rect',{x:'2',y:`${CFG.segW/2+0.5}`,width:`${CFG.segLen-4}`,height:'3.5',rx:'1.5',fill:'#14586a'},g);
// 鳞片标记(腹部,3个小三角)
const scales=el('g',{opacity:'0.7'},g);
for(let s=0;s<3;s++){
const sx=6+s*8;
el('path',{d:`M${sx},0 L${sx-2.5},${3} L${sx+2.5},${3} Z`,fill:'#00d4a8',opacity:'0.6'},scales);
}
// 关节圆
const joint=el('circle',{cx:'0',cy:'0',r:'3.5',fill:'#4a5e78',stroke:'#6a7e98','stroke-width':'0.8'},g);
// 头部标记
const headMark=(i===CFG.N-1)?el('circle',{cx:`${CFG.segLen}`,cy:'0',r:'5',fill:'none',stroke:'#00e0c0','stroke-width':'1.5',opacity:'0.6'},g):null;
segEls.push({g,body,lSMA,rSMA,scales,joint,headMark,
leftAct:0,rightAct:0});
}
// 蛇头眼睛
const headIdx=CFG.N-1;
const eyeL=el('circle',{cx:'0',cy:'-4',r:'2',fill:'#00e0c0'},segEls[headIdx].g);
const eyeR=el('circle',{cx:'0',cy:'4',r:'2',fill:'#00e0c0'},segEls[headIdx].g);
segEls[headIdx].eyeL=eyeL;segEls[headIdx].eyeR=eyeR;
// 力矢量元素池
const forceEls=[];
for(let i=0;i<CFG.N;i++){
const lg=el('g',{opacity:'0'},forceLayer);
// 侧向力线
const latLine=el('line',{x1:'0',y1:'0',x2:'0',y2:'0',stroke:'#ff3060','stroke-width':'2','marker-end':'url(#arrLat)'},lg);
// 前进推力线
const fwdLine=el('line',{x1:'0',y1:'0',x2:'0',y2:'0',stroke:'#00ccff','stroke-width':'2','marker-end':'url(#arrFwd)'},lg);
forceEls.push({g:lg,latLine,fwdLine});
}
// 轨迹路径
const trailPath=el('path',{fill:'none',stroke:'#00e0c0','stroke-width':'1.5',opacity:'0.3','stroke-dasharray':'4 3'},trailLayer);
// 标注
const infoText=el('text',{x:'20',y:'470',fill:'#4a5e78','font-size':'11','font-family':'Chakra Petch, sans-serif'},labelLayer);
const velText=el('text',{x:'980',y:'470',fill:'#00e0c0','font-size':'12','font-family':'Chakra Petch, sans-serif','text-anchor':'end'},labelLayer);
/* ============ 截面 SVG ============ */
const crossSvg=document.getElementById('crossSvg');
const cDefs=el('defs',{},crossSvg);
const cg1=el('linearGradient',{id:'cMetalG',x1:'0',y1:'0',x2:'0',y2:'1'},cDefs);
el('stop',{offset:'0%','stop-color':'#5e7290'},cg1);el('stop',{offset:'100%','stop-color':'#2a3c58'},cg1);
// 背景
el('rect',{width:'360',height:'290',fill:'#080e1c'},crossSvg);
// 标注线区
const csStatic=el('g',{},crossSvg);
const csAnim=el('g',{},crossSvg);
// 骨架截面(方形)
const cSk=el('rect',{x:'150',y:'95',width:'60',height:'60',rx:'3',fill:'url(#cMetalG)',stroke:'#4a6080','stroke-width':'1.2'},csStatic);
// 球铰中心
el('circle',{cx:'180',cy:'125',r:'8',fill:'#3a4e6a',stroke:'#6a7e98','stroke-width':'1'},csStatic);
el('circle',{cx:'180',cy:'125',r:'3',fill:'#8a9eb8'},csStatic);
// 左SMA网(波纹状)
const csLPath=el('path',{fill:'none',stroke:'#14586a','stroke-width':'3','stroke-linecap':'round'},csAnim);
// 右SMA网
const csRPath=el('path',{fill:'none',stroke:'#14586a','stroke-width':'3','stroke-linecap':'round'},csAnim);
// 鳞片(底部)
const csScales=el('g',{},csStatic);
for(let i=0;i<5;i++){
const sx=145+i*14;
el('path',{d:`M${sx},160 L${sx+5},168 L${sx+10},160`,fill:'none',stroke:'#00d4a8','stroke-width':'1.5','stroke-linecap':'round'},csScales);
}
// 鳞片方向标注
el('line',{x1:'130',y1:'175',x2:'230',y2:'175',stroke:'#00d4a8','stroke-width':'0.5','stroke-dasharray':'3 2',opacity:'0.4'},csStatic);
el('text',{x:'180',y:'188',fill:'#00d4a8','font-size':'9','text-anchor':'middle','font-family':'Noto Sans SC'},csStatic).textContent='腹部鳞片 (后掠30°)';
// 标注文字
const csLabels=[
{x:60,y:60,text:'SMA丝网(左)',fill:'#14586a'},
{x:260,y:60,text:'SMA丝网(右)',fill:'#14586a'},
{x:180,y:82,text:'方形骨架',fill:'#6a7e98'},
{x:180,y:136,text:'球铰链',fill:'#8a9eb8'},
];
csLabels.forEach(l=>{
el('text',{x:l.x,y:l.y,fill:l.fill,'font-size':'10','text-anchor':'middle','font-family':'Noto Sans SC'},csStatic).textContent=l.text;
});
// 弯曲方向指示
const csBendArrow=el('path',{fill:'none',stroke:'#ff6028','stroke-width':'1.5','stroke-dasharray':'4 2',opacity:'0'},csAnim);
const csBendText=el('text',{x:'180',y:'250',fill:'#ff6028','font-size':'10','text-anchor':'middle','font-family':'Noto Sans SC',opacity:'0'},csAnim);
csBendText.textContent='SMA收缩 → 侧向弯曲';
// 收缩指示
const csContractL=el('rect',{x:'100',y:'105',width:'46',height:'40',rx:'4',fill:'none',stroke:'#ff6028','stroke-width':'1','stroke-dasharray':'3 2',opacity:'0'},csAnim);
const csContractR=el('rect',{x:'214',y:'105',width:'46',height:'40',rx:'4',fill:'none',stroke:'#ff6028','stroke-width':'1','stroke-dasharray':'3 2',opacity:'0'},csAnim);
/* ============ 波形 SVG ============ */
const waveSvg=document.getElementById('waveSvg');
el('rect',{width:'360',height:'200',fill:'#080e1c'},waveSvg);
// 坐标轴
const wAxis=el('g',{},waveSvg);
el('line',{x1:'40',y1:'100',x2:'340',y2:'100',stroke:'#1a2a44','stroke-width':'1'},wAxis);
el('line',{x1:'40',y1:'30',x2:'40',y2:'170',stroke:'#1a2a44','stroke-width':'1'},wAxis);
el('text',{x:'190',y:'195',fill:'#4a5e78','font-size':'9','text-anchor':'middle','font-family':'Chakra Petch'},wAxis).textContent='节段序号 →';
el('text',{x:'18',y:'105',fill:'#4a5e78','font-size':'9','text-anchor':'middle','font-family':'Chakra Petch'},wAxis).textContent='激活';
el('text',{x:'190',y:'22',fill:'#4a5e78','font-size':'9','text-anchor':'middle','font-family':'Noto Sans SC'},wAxis).textContent='SMA激活行波';
// 刻度
for(let i=0;i<CFG.N;i++){
const x=50+i*20;
el('line',{x1:x,y1:'97',x2:x,y2:'103',stroke:'#2a3a54','stroke-width':'0.5'},wAxis);
if(i%3===0) el('text',{x:x,y:'115',fill:'#3a506a','font-size':'8','text-anchor':'middle','font-family':'Chakra Petch'},wAxis).textContent=i;
}
// 左SMA激活曲线
const wLeftPath=el('path',{fill:'none',stroke:'#ff6028','stroke-width':'2',opacity:'0.85'},waveSvg);
// 右SMA激活曲线
const wRightPath=el('path',{fill:'none',stroke:'#00aacc','stroke-width':'2',opacity:'0.85'},waveSvg);
// 当前时间指示线
const wTimeLine=el('line',{x1:'40',y1:'30',x2:'40',y2:'170',stroke:'#00e0c0','stroke-width':'1',opacity:'0.5','stroke-dasharray':'3 2'},waveSvg);
// 图例
el('rect',{x:'240',y:'32',width:'10',height:'3',fill:'#ff6028'},waveSvg);
el('text',{x:'254',y:'37',fill:'#ff6028','font-size':'8','font-family':'Noto Sans SC'},waveSvg).textContent='左侧SMA';
el('rect',{x:'240',y:'44',width:'10',height:'3',fill:'#00aacc'},waveSvg);
el('text',{x:'254',y:'49',fill:'#00aacc','font-size':'8','font-family':'Noto Sans SC'},waveSvg).textContent='右侧SMA';
/* ============ 蛇身运动学 ============ */
const segs=[];
for(let i=0;i<CFG.N;i++) segs.push({x:0,y:0,angle:0,dAngle:0,lAct:0,rAct:0});
function updateSnake(t){
const omega=2*Math.PI*CFG.freq;
let x=0,y=0,angle=0;
for(let i=0;i<CFG.N;i++){
// 行波:从头部向尾部传播(+ωt使波向-i方向传播)
const dA=CFG.amp*Math.sin(2*Math.PI*i/CFG.waveLen+omega*t);
angle+=dA;
segs[i].x=x;segs[i].y=y;segs[i].angle=angle;segs[i].dAngle=dA;
const norm=dA/CFG.amp;
segs[i].lAct=clamp(norm,0,1);
segs[i].rAct=clamp(-norm,0,1);
x+=CFG.segLen*Math.cos(angle);
y+=CFG.segLen*Math.sin(angle);
}
// 居中偏移
const mid=segs[Math.floor(CFG.N/2)];
const ox=500-mid.x,oy=240-mid.y;
for(let i=0;i<CFG.N;i++){segs[i].x+=ox;segs[i].y+=oy}
}
/* ============ 渲染主动画 ============ */
let groundScroll=0;
function renderMain(t){
updateSnake(t);
// 地面滚动
const speed=CFG.freq*CFG.amp*60;
groundScroll+=speed*0.016;
groundDots.forEach(d=>{
const gx=((d.baseX-groundScroll)%1200+1200)%1200-100;
d.el.setAttribute('cx',gx);
d.el.setAttribute('cy',d.baseY);
});
// 蛇身段
for(let i=0;i<CFG.N;i++){
const s=segs[i],e=segEls[i];
e.g.setAttribute('transform',`translate(${s.x},${s.y}) rotate(${deg(s.angle)})`);
// SMA颜色
if(CFG.showSMA){
e.lSMA.setAttribute('fill',smaColor(s.lAct));
e.rSMA.setAttribute('fill',smaColor(s.rAct));
e.lSMA.setAttribute('filter',s.lAct>0.3?'url(#glowF)':'none');
e.rSMA.setAttribute('filter',s.rAct>0.3?'url(#glowF)':'none');
e.lSMA.setAttribute('opacity',s.lAct>0.05?'1':'0.4');
e.rSMA.setAttribute('opacity',s.rAct>0.05?'1':'0.4');
}else{
e.lSMA.setAttribute('fill','#14586a');e.rSMA.setAttribute('fill','#14586a');
e.lSMA.setAttribute('filter','none');e.rSMA.setAttribute('filter','none');
e.lSMA.setAttribute('opacity','0.4');e.rSMA.setAttribute('opacity','0.4');
}
// 鳞片闪烁(有侧向力时)
const latForce=Math.abs(s.dAngle);
e.scales.setAttribute('opacity',latForce>0.1?clamp(latForce*2,0.4,1):0.3);
}
// 头部眼睛位置修正
const hE=segEls[headIdx];
hE.eyeL.setAttribute('cx',CFG.segLen-2);hE.eyeL.setAttribute('cy',-4);
hE.eyeR.setAttribute('cx',CFG.segLen-2);hE.eyeR.setAttribute('cy',4);
// 力矢量
for(let i=0;i<CFG.N;i++){
const s=segs[i],f=forceEls[i];
if(CFG.showForce && Math.abs(s.dAngle)>0.08){
const intensity=clamp(Math.abs(s.dAngle)*3,0,1);
f.g.setAttribute('opacity',intensity*0.85);
const mx=s.x+CFG.segLen*0.5*Math.cos(s.angle);
const my=s.y+CFG.segLen*0.5*Math.sin(s.angle);
// 侧向力(垂直于身体方向,指向弯曲内侧)
const perpAngle=s.angle+(s.dAngle>0?-Math.PI/2:Math.PI/2);
const latLen=25*intensity;
f.latLine.setAttribute('x1',mx);f.latLine.setAttribute('y1',my);
f.latLine.setAttribute('x2',mx+latLen*Math.cos(perpAngle));
f.latLine.setAttribute('y2',my+latLen*Math.sin(perpAngle));
// 前进推力(沿身体方向前方)
const fwdLen=20*intensity;
const fwdAngle=s.angle;
f.fwdLine.setAttribute('x1',mx);f.fwdLine.setAttribute('y1',my);
f.fwdLine.setAttribute('x2',mx+fwdLen*Math.cos(fwdAngle));
f.fwdLine.setAttribute('y2',my+fwdLen*Math.sin(fwdAngle));
}else{
f.g.setAttribute('opacity','0');
}
}
// 轨迹
if(CFG.showTrail){
const hp=segs[headIdx];
const hx=hp.x+CFG.segLen*Math.cos(hp.angle);
const hy=hp.y+CFG.segLen*Math.sin(hp.angle);
trail.push({x:hx,y:hy});
if(trail.length>300) trail.shift();
if(trail.length>2){
let d=`M${trail[0].x.toFixed(1)},${trail[0].y.toFixed(1)}`;
for(let i=1;i<trail.length;i++) d+=` L${trail[i].x.toFixed(1)},${trail[i].y.toFixed(1)}`;
trailPath.setAttribute('d',d);
}
trailLayer.setAttribute('opacity','1');
}else{
trailLayer.setAttribute('opacity','0');
}
// 速度显示
const vel=(CFG.freq*CFG.amp*0.8).toFixed(2);
velText.textContent=`v ≈ ${vel} m/s`;
// 信息文字
infoText.textContent=`ω=${CFG.freq.toFixed(1)}Hz A=${CFG.amp.toFixed(2)}rad λ=${CFG.waveLen.toFixed(1)}节段`;
}
/* ============ 渲染截面 ============ */
let crossSegIdx=7;
function renderCross(t){
if(crossSegIdx>=CFG.N) crossSegIdx=CFG.N-1;
const s=segs[crossSegIdx];
const lA=s.lAct,rA=s.rAct;
// 波纹SMA路径
const ampL=lerp(12,4,lA),ampR=lerp(12,4,rA);
let dL='M',dR='M';
for(let j=0;j<=8;j++){
const yy=85+j*10;
const xl=120+((j%2===0)?ampL:-ampL);
const xr=240+((j%2===0)?-ampR:ampR);
dL+=(j===0?'':' L')+` ${xl},${yy}`;
dR+=(j===0?'':' L')+` ${xr},${yy}`;
}
csLPath.setAttribute('d',dL);
csRPath.setAttribute('d',dR);
csLPath.setAttribute('stroke',smaColor(lA));
csRPath.setAttribute('stroke',smaColor(rA));
csLPath.setAttribute('stroke-width',lA>0.3?'4':'3');
csRPath.setAttribute('stroke-width',rA>0.3?'4':'3');
csLPath.setAttribute('filter',lA>0.3?'url(#glowF)':'none');
csRPath.setAttribute('filter',rA>0.3?'url(#glowF)':'none');
// 收缩指示框
const anyActive=lA>0.2||rA>0.2;
csContractL.setAttribute('opacity',lA>0.2?clamp(lA,0.2,0.9):0);
csContractR.setAttribute('opacity',rA>0.2?clamp(rA,0.2,0.9):0);
// 弯曲方向
csBendArrow.setAttribute('opacity',anyActive?'0.8':'0');
csBendText.setAttribute('opacity',anyActive?'0.8':'0');
if(anyActive){
const bendDir=lA>rA?-1:1;
const cx=180,cy=220,rx=30;
const startA=bendDir>0?0:Math.PI;
const endA=bendDir>0?Math.PI:2*Math.PI;
csBendArrow.setAttribute('d',`M${cx+rx*Math.cos(startA)},${cy+15*Math.sin(startA)} A${rx},15,0,0,${bendDir>0?1:0},${cx+rx*Math.cos(endA)},${cy+15*Math.sin(endA)}`);
}
// 标注颜色随激活变化
const lLabel=csStatic.querySelector('text[x="60"]');
const rLabel=csStatic.querySelector('text[x="260"]');
if(lLabel) lLabel.setAttribute('fill',smaColor(lA));
if(rLabel) rLabel.setAttribute('fill',smaColor(rA));
}
/* ============ 渲染波形图 ============ */
function renderWave(t){
const omega=2*Math.PI*CFG.freq;
let dL='M',dR='M';
for(let i=0;i<CFG.N;i++){
const x=50+i*20;
const phase=2*Math.PI*i/CFG.waveLen+omega*t;
const val=Math.sin(phase);
const yL=100-val*55;
const yR=100+val*55;
dL+=(i===0?'':' L')+`${x},${yL.toFixed(1)}`;
dR+=(i===0?'':' L')+`${x},${yR.toFixed(1)}`;
}
wLeftPath.setAttribute('d',dL);
wRightPath.setAttribute('d',dR);
// 时间指示线(随相位移动)
const phasePos=((omega*t)%(2*Math.PI))/(2*Math.PI);
const tx=50+phasePos*CFG.N*20;
wTimeLine.setAttribute('x1',clamp(tx,40,340));
wTimeLine.setAttribute('x2',clamp(tx,40,340));
}
/* ============ 动画主循环 ============ */
function animate(ts){
if(!lastTs) lastTs=ts;
const dt=Math.min((ts-lastTs)/1000,0.05);
lastTs=ts;
if(CFG.playing) time+=dt;
renderMain(time);
renderCross(time);
renderWave(time);
requestAnimationFrame(animate);
}
/* ============ 控件绑定 ============ */
const freqSlider=document.getElementById('freqSlider');
const ampSlider=document.getElementById('ampSlider');
const freqVal=document.getElementById('freqVal');
const ampVal=document.getElementById('ampVal');
const btnPlay=document.getElementById('btnPlay');
const btnForce=document.getElementById('btnForce');
const btnSMA=document.getElementById('btnSMA');
const btnTrail=document.getElementById('btnTrail');
freqSlider.addEventListener('input',()=>{
CFG.freq=parseFloat(freqSlider.value);
freqVal.textContent=CFG.freq.toFixed(2)+' Hz';
// 频率高于2Hz时提示散热限制
if(CFG.freq>1.8) freqVal.style.color='#ff6028';
else freqVal.style.color='';
});
ampSlider.addEventListener('input',()=>{
CFG.amp=parseFloat(ampSlider.value);
ampVal.textContent=CFG.amp.toFixed(2)+' rad';
});
btnPlay.addEventListener('click',()=>{
CFG.playing=!CFG.playing;
btnPlay.textContent=CFG.playing?'播放中':'已暂停';
btnPlay.classList.toggle('active',CFG.playing);
});
btnForce.addEventListener('click',()=>{
CFG.showForce=!CFG.showForce;
btnForce.classList.toggle('active',CFG.showForce);
});
btnSMA.addEventListener('click',()=>{
CFG.showSMA=!CFG.showSMA;
btnSMA.classList.toggle('active',CFG.showSMA);
});
btnTrail.addEventListener('click',()=>{
CFG.showTrail=!CFG.showTrail;
btnTrail.classList.toggle('active',CFG.showTrail);
if(!CFG.showTrail) trail.length=0;
});
// 初始状态
btnForce.classList.add('active');
btnSMA.classList.add('active');
btnTrail.classList.add('active');
/* ============ 启动 ============ */
requestAnimationFrame(animate);
</script>
</body>
</html>
实现说明:
这个高保真动画从三个维度完整诠释了"自涌现蜿蜒推进"的 IFR 原理:
1. 主视图(俯视蜿蜒动画)
- 14 节串联骨架通过行波模型
δθ = A·sin(2πi/λ + ωt)实时解算姿态,波从头部向尾部传播 - 每段两侧的 SMA 条带根据弯曲方向实时变色(冷态深青 → 热态橙红 + 发光),直观展示"一侧收缩、对侧柔软"的矛盾破除机制
- 腹部鳞片在有侧向力时亮度增强,揭示方向性摩擦的"免费资源"利用
- 力矢量箭头(珊瑚红=侧向地面反力,青蓝=前进推力)清晰展示蜿蜒运动如何通过鳞片将侧向力完全转化为前进推力
2. 截面结构面板
- 方形骨架 + 球铰链 + 两侧波纹状 SMA 丝网 + 底部鳞片
- 波纹振幅随激活收缩而收紧,颜色同步变热,直观呈现马氏体→奥氏体相变收缩机理
- 弯曲方向弧形指示与收缩框线联动
3. 行波激活面板
- 左/右 SMA 激活强度的正弦行波实时绘制
- 时间指示线随相位推进,与主动画严格同步
交互控件:频率滑块(超过 1.8Hz 变红提示散热极限)、幅度滑块、播放/暂停、力矢量/SMA 高亮/轨迹三个独立开关,让用户亲手调控关键变量,深度体验 IFR 的动态涌现过程。
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
