独立渲染引擎就绪引擎就绪
<!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;900&family=Noto+Sans+SC:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#050910}
body{display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'Noto Sans SC',sans-serif;color:#b8c8dc}
#wrap{width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;position:relative}
#main-svg{width:100%;height:100%;max-width:1500px;max-height:900px}
#controls{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);display:flex;gap:28px;align-items:center;background:rgba(8,14,24,0.88);border:1px solid rgba(255,120,40,0.18);border-radius:14px;padding:12px 32px;backdrop-filter:blur(12px);z-index:10}
.cg{display:flex;align-items:center;gap:8px}
.cl{font-size:12.5px;color:#7888a8;font-weight:400;white-space:nowrap;letter-spacing:.3px}
.cv{font-family:'Rajdhani',sans-serif;font-size:14px;color:#ff8c42;font-weight:700;min-width:36px;text-align:right}
input[type=range]{-webkit-appearance:none;appearance:none;width:110px;height:4px;background:linear-gradient(90deg,#14203a,#ff8c42);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:15px;height:15px;border-radius:50%;background:#ff8c42;cursor:pointer;border:2px solid #0a1020;box-shadow:0 0 6px rgba(255,140,66,0.5)}
.btn{background:rgba(255,120,40,0.12);border:1px solid rgba(255,120,40,0.35);color:#ff8c42;border-radius:8px;padding:6px 18px;cursor:pointer;font-family:'Rajdhani',sans-serif;font-size:14px;font-weight:700;transition:all .2s;letter-spacing:.5px}
.btn:hover{background:rgba(255,120,40,0.28);border-color:rgba(255,120,40,0.7)}
</style>
</head>
<body>
<div id="wrap">
<svg id="main-svg" viewBox="0 0 1440 840" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#070c16"/><stop offset="100%" stop-color="#0a1020"/>
</linearGradient>
<linearGradient id="bodyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a5a6a"/><stop offset="40%" stop-color="#0e3848"/><stop offset="100%" stop-color="#082838"/>
</linearGradient>
<linearGradient id="bodyStroke" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3aa0b8"/><stop offset="100%" stop-color="#1a6070"/>
</linearGradient>
<linearGradient id="groundGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#111828"/><stop offset="100%" stop-color="#080e18"/>
</linearGradient>
<radialGradient id="bulgeGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#00e0ff" stop-opacity="0.55"/><stop offset="70%" stop-color="#00e0ff" stop-opacity="0.08"/><stop offset="100%" stop-color="#00e0ff" stop-opacity="0"/>
</radialGradient>
<radialGradient id="cableGlowR" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff8c42" stop-opacity="0.5"/><stop offset="100%" stop-color="#ff8c42" stop-opacity="0"/>
</radialGradient>
<radialGradient id="motorGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ff8c42" stop-opacity="0.25"/><stop offset="100%" stop-color="#ff8c42" stop-opacity="0"/>
</radialGradient>
<filter id="glow4" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow8" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow12" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="12" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<clipPath id="csClip"><rect x="1110" y="30" width="300" height="280" rx="12"/></clipPath>
<clipPath id="scaleClip"><rect x="1110" y="530" width="300" height="250" rx="12"/></clipPath>
</defs>
<!-- 背景 -->
<rect width="1440" height="840" fill="url(#bgGrad)"/>
<!-- 网格 -->
<g id="grid" opacity="0.035"></g>
<!-- 地面 -->
<rect x="0" y="540" width="1440" height="300" fill="url(#groundGrad)"/>
<line x1="0" y1="540" x2="1440" y2="540" stroke="#2a3a5a" stroke-width="1.2" opacity="0.6"/>
<!-- 地面纹理 -->
<g id="groundTex" opacity="0.08"></g>
<!-- 动态层 -->
<g id="groundArrows"></g>
<g id="pressureMarks"></g>
<g id="bodyGroup"></g>
<g id="scalesGroup"></g>
<g id="bulgeGroup"></g>
<g id="cableGroup"></g>
<g id="motorGroup"></g>
<g id="headGroup"></g>
<g id="waveArrows"></g>
<!-- 截面图面板 -->
<rect x="1110" y="30" width="300" height="280" rx="12" fill="rgba(8,14,26,0.85)" stroke="rgba(255,120,40,0.15)" stroke-width="1"/>
<g id="crossSection"></g>
<!-- 鳞片细节面板 -->
<rect x="1110" y="530" width="300" height="250" rx="12" fill="rgba(8,14,26,0.85)" stroke="rgba(60,180,100,0.15)" stroke-width="1"/>
<g id="scaleDetail"></g>
<!-- 标题 -->
<text x="40" y="52" fill="#e0e8f4" font-family="Rajdhani,sans-serif" font-size="28" font-weight="900" letter-spacing="1.5">SINGLE-MOTOR TRAVELING WAVE</text>
<text x="40" y="78" fill="#7888a8" font-family="Noto Sans SC,sans-serif" font-size="14" font-weight="300">单电机偏心缆绳行波驱动 · 柔性蛇理想解原理</text>
<!-- IFR标注 -->
<text x="40" y="108" fill="#ff8c42" font-family="Rajdhani,sans-serif" font-size="13" font-weight="700" letter-spacing="1" opacity="0.7">IFR: 一旋转 → 全身行波 → 自行前进</text>
<!-- 图例 -->
<g transform="translate(40,750)">
<rect x="0" y="0" width="12" height="12" rx="2" fill="#ff8c42"/><text x="18" y="11" fill="#8898b8" font-size="11">偏心钢缆</text>
<rect x="110" y="0" width="12" height="12" rx="2" fill="#1a5a6a"/><text x="128" y="11" fill="#8898b8" font-size="11">柔性主干</text>
<rect x="220" y="0" width="12" height="12" rx="2" fill="#3aaa5a"/><text x="238" y="11" fill="#8898b8" font-size="11">鳞片(抓地)</text>
<rect x="340" y="0" width="12" height="12" rx="2" fill="#2a3a3a"/><text x="358" y="11" fill="#8898b8" font-size="11">鳞片(回缩)</text>
<circle cx="460" cy="6" r="6" fill="url(#bulgeGlow)"/><text x="472" y="11" fill="#8898b8" font-size="11">凸起波峰</text>
</g>
<!-- 方向标注 -->
<g id="dirLabel"></g>
</svg>
<div id="controls">
<div class="cg"><span class="cl">电机转速</span><input type="range" id="spdR" min="0.2" max="3" step="0.1" value="1"><span class="cv" id="spdV">1.0x</span></div>
<div class="cg"><span class="cl">偏心距</span><input type="range" id="offR" min="0.3" max="2" step="0.1" value="1"><span class="cv" id="offV">1.0</span></div>
<div class="cg"><span class="cl">波幅</span><input type="range" id="ampR" min="0.3" max="2" step="0.1" value="1"><span class="cv" id="ampV">1.0</span></div>
<button class="btn" id="ppBtn">暂停</button>
</div>
</div>
<script>
(function(){
const NS='http://www.w3.org/2000/svg';
const svg=document.getElementById('main-svg');
function ce(tag,attrs){const e=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs||{}))e.setAttribute(k,v);return e}
function sat(el,attrs){for(const[k,v]of Object.entries(attrs||{}))el.setAttribute(k,v)}
/* ── 配置 ── */
let motorSpd=1,cableOff=1,ampScale=1,running=true,time=0,prevTs=0;
const S={
sx:170,ex:1060,cy:460,bw:30,amp:40,waveN:1.5,
nSeg:18,nPts:140,cableOff:7,scaleH:9,scaleAngle:30
};
const CS={cx:1260,cy:175,or:65,cr:7,orbit:20};
const SD={cx:1260,cy:660};
/* ── 静态元素 ── */
(function initStatic(){
const g=document.getElementById('grid');
for(let x=0;x<=1440;x+=60)g.appendChild(ce('line',{x1:x,y1:0,x2:x,y2:840,stroke:'#3a5a8a','stroke-width':'0.5'}));
for(let y=0;y<=840;y+=60)g.appendChild(ce('line',{x1:0,y1:y,x2:1440,y2:y,stroke:'#3a5a8a','stroke-width':'0.5'}));
const gt=document.getElementById('groundTex');
for(let x=0;x<1440;x+=12)gt.appendChild(ce('line',{x1:x,y1:542,x2:x+6,y2:548,stroke:'#2a3a5a','stroke-width':'0.6'}));
})();
/* ── 动态元素引用 ── */
let bodyPath,bodyHL,cableGlowP,cableP;
const segLines=[],scales=[],bulges=[],waveArrs=[],gArrows=[],pMarks=[];
(function initDynamic(){
const bg=document.getElementById('bodyGroup');
// 蛇身阴影
bg.appendChild(ce('ellipse',{id:'bodyShadow',cx:600,cy:548,rx:420,ry:10,fill:'rgba(0,0,0,0.25)',filter:'url(#glow12)'}));
// 蛇身主体
bodyPath=ce('path',{d:'M0 0',fill:'url(#bodyGrad)',stroke:'url(#bodyStroke)','stroke-width':'1.5'});
bg.appendChild(bodyPath);
// 身体高光
bodyHL=ce('path',{d:'M0 0',fill:'none',stroke:'rgba(120,220,240,0.18)','stroke-width':'3','stroke-linecap':'round'});
bg.appendChild(bodyHL);
// 节间线
const slG=ce('g',{opacity:'0.5'});
for(let i=0;i<=S.nSeg;i++){const l=ce('line',{x1:0,y1:0,x2:0,y2:0,stroke:'rgba(50,150,170,0.5)','stroke-width':'0.8','stroke-dasharray':'4,3'});slG.appendChild(l);segLines.push(l)}
bg.appendChild(slG);
// 缆绳发光
const cg=document.getElementById('cableGroup');
cableGlowP=ce('path',{d:'M0 0',fill:'none',stroke:'#ff8c42','stroke-width':'8',opacity:'0.2',filter:'url(#glow8)'});
cg.appendChild(cableGlowP);
cableP=ce('path',{d:'M0 0',fill:'none',stroke:'#ff8c42','stroke-width':'2.5','stroke-linecap':'round',filter:'url(#glow4)'});
cg.appendChild(cableP);
// 凸起高亮
const blG=document.getElementById('bulgeGroup');
for(let i=0;i<6;i++){const c=ce('circle',{cx:0,cy:0,r:18,fill:'url(#bulgeGlow)',opacity:'0'});blG.appendChild(c);bulges.push(c)}
// 鳞片
const scG=document.getElementById('scalesGroup');
for(let i=0;i<=S.nSeg;i++){
const sc=ce('path',{d:'M0 0',fill:'#2a3a2a',stroke:'#3a6a4a','stroke-width':'0.6'});
scG.appendChild(sc);scales.push(sc);
}
// 电机
const mG=document.getElementById('motorGroup');
mG.appendChild(ce('circle',{cx:S.sx-10,cy:S.cy,r:28,fill:'url(#motorGlow)'}));
mG.appendChild(ce('circle',{cx:S.sx-10,cy:S.cy,r:22,fill:'#1a2a3a',stroke:'#3a5a7a','stroke-width':'1.5'}));
mG.appendChild(ce('circle',{cx:S.sx-10,cy:S.cy,r:6,fill:'#2a4a5a',stroke:'#ff8c42','stroke-width':'1.5'}));
for(let i=0;i<4;i++){const a=i*Math.PI/2;mG.appendChild(ce('line',{x1:S.sx-10+Math.cos(a)*8,y1:S.cy+Math.sin(a)*8,x2:S.sx-10+Math.cos(a)*18,y2:S.cy+Math.sin(a)*18,stroke:'#ff8c42','stroke-width':'1.5',opacity:'0.6',class:'spoke'}))}
mG.appendChild(ce('text',{x:S.sx-10,y:S.cy+42,fill:'#8898b8','font-size':'10','text-anchor':'middle','font-family':'Noto Sans SC'})).textContent='单电机';
// 蛇头
const hG=document.getElementById('headGroup');
hG.appendChild(ce('ellipse',{cx:S.ex+18,cy:S.cy,rx:20,ry:14,fill:'#0e3848',stroke:'#3aa0b8','stroke-width':'1.2'}));
hG.appendChild(ce('circle',{id:'eyeL',cx:S.ex+26,cy:S.cy-5,r:3,fill:'#00e0ff',opacity:'0.8'}));
hG.appendChild(ce('circle',{id:'eyeR',cx:S.ex+26,cy:S.cy+5,r:3,fill:'#00e0ff',opacity:'0.8'}));
// 波传播箭头
const waG=document.getElementById('waveArrows');
for(let i=0;i<5;i++){const a=ce('path',{d:'M0 0 L-8 -5 L-8 5 Z',fill:'#00e0ff',opacity:'0'});waG.appendChild(a);waveArrs.push(a)}
// 地面运动箭头
const gaG=document.getElementById('groundArrows');
for(let i=0;i<10;i++){const a=ce('path',{d:'M0 0 L-7 -4 L-7 4 Z',fill:'#2a5a3a',opacity:'0.3'});gaG.appendChild(a);gArrows.push(a)}
// 地面压力标记
const pmG=document.getElementById('pressureMarks');
for(let i=0;i<8;i++){const m=ce('ellipse',{cx:0,cy:544,rx:14,ry:3,fill:'rgba(0,224,255,0.08)',opacity:'0'});pmG.appendChild(m);pMarks.push(m)}
// 方向标注
const dL=document.getElementById('dirLabel');
dL.appendChild(ce('line',{x1:S.ex+50,y1:S.cy,x2:S.ex+100,y2:S.cy,stroke:'#00e0ff','stroke-width':'1.5','marker-end':'url(#arrowCyan)'}));
dL.appendChild(ce('text',{x:S.ex+105,y:S.cy+4,fill:'#00e0ff','font-size':'12','font-family':'Rajdhani','font-weight':'700'})).textContent='前进方向';
// 波传播方向
dL.appendChild(ce('line',{x1:S.sx-30,y1:S.cy-60,x2:S.sx+40,y2:S.cy-60,stroke:'#ff8c42','stroke-width':'1.2','stroke-dasharray':'4,3'}));
dL.appendChild(ce('text',{x:S.sx+46,y:S.cy-56,fill:'#ff8c42','font-size':'11','font-family':'Noto Sans SC'})).textContent='行波传播 →';
initCrossSection();
initScaleDetail();
})();
/* ── 截面图 ── */
let csCableDot,csForceLine,csPressure,csAngleLine;
function initCrossSection(){
const g=document.getElementById('crossSection');
// 标题
g.appendChild(ce('text',{x:1260,y:58,fill:'#c8d4e8','font-size':'14','font-weight':'700','text-anchor':'middle','font-family':'Noto Sans SC'})).textContent='横截面视图';
g.appendChild(ce('text',{x:1260,y:76,fill:'#6878a0','font-size':'10','text-anchor':'middle','font-family':'Noto Sans SC'})).textContent='偏心钢缆旋转 → 凸起位移';
// 主干截面
g.appendChild(ce('circle',{cx:CS.cx,cy:CS.cy,r:CS.or,fill:'none',stroke:'#1a5a6a','stroke-width':'2.5'}));
g.appendChild(ce('circle',{cx:CS.cx,cy:CS.cy,r:CS.or,fill:'rgba(10,50,70,0.4)'}));
// 轨道
g.appendChild(ce('circle',{cx:CS.cx,cy:CS.cy,r:CS.orbit,fill:'none',stroke:'rgba(255,140,66,0.2)','stroke-width':'1','stroke-dasharray':'4,4'}));
// 中心标记
g.appendChild(ce('line',{x1:CS.cx-5,y1:CS.cy,x2:CS.cx+5,y2:CS.cy,stroke:'#3a6a8a','stroke-width':'0.8'}));
g.appendChild(ce('line',{x1:CS.cx,y1:CS.cy-5,x2:CS.cx,y2:CS.cy+5,stroke:'#3a6a8a','stroke-width':'0.8'}));
// 压力区
csPressure=ce('path',{d:'M0 0',fill:'rgba(0,224,255,0.12)',stroke:'none'});
g.appendChild(csPressure);
// 缆绳点
csCableDot=ce('circle',{cx:CS.cx,cy:CS.cy-CS.orbit,r:CS.cr,fill:'#ff8c42',filter:'url(#glow4)'});
g.appendChild(csCableDot);
// 力线
csForceLine=ce('line',{x1:0,y1:0,x2:0,y2:0,stroke:'#00e0ff','stroke-width':'2','stroke-dasharray':'3,2',opacity:'0.7'});
g.appendChild(csForceLine);
// 偏心距标注线
csAngleLine=ce('line',{x1:CS.cx,y1:CS.cy,x2:0,y2:0,stroke:'rgba(255,140,66,0.4)','stroke-width':'1','stroke-dasharray':'2,2'});
g.appendChild(csAngleLine);
// 标注
g.appendChild(ce('text',{x:CS.cx-55,y:CS.cy+CS.or+20,fill:'#5a7a9a','font-size':'10','font-family':'Noto Sans SC'})).textContent='柔性主干';
g.appendChild(ce('text',{x:CS.cx+25,y:CS.cy-CS.orbit-8,fill:'#ff8c42','font-size':'10','font-weight':'600','font-family':'Noto Sans SC'})).textContent='偏心钢缆';
// 旋转箭头
const arR=CS.or+14;
const a1=Math.PI*0.15,a2=Math.PI*0.45;
g.appendChild(ce('path',{
d:`M ${CS.cx+arR*Math.cos(a1)} ${CS.cy-arR*Math.sin(a1)} A ${arR} ${arR} 0 0 0 ${CS.cx+arR*Math.cos(a2)} ${CS.cy-arR*Math.sin(a2)}`,
fill:'none',stroke:'#ff8c42','stroke-width':'1.5',opacity:'0.5'
}));
// 箭头尖
const ax=CS.cx+arR*Math.cos(a2),ay=CS.cy-arR*Math.sin(a2);
g.appendChild(ce('path',{d:`M ${ax} ${ay} L ${ax+6} ${ay-4} L ${ax+2} ${ay+4} Z`,fill:'#ff8c42',opacity:'0.5'}));
g.appendChild(ce('text',{x:CS.cx+arR+10,y:CS.cy-10,fill:'#ff8c42','font-size':'10','font-family':'Rajdhani','font-weight':'600',opacity:'0.6'})).textContent='ω';
}
/* ── 鳞片细节 ── */
function initScaleDetail(){
const g=document.getElementById('scaleDetail');
g.appendChild(ce('text',{x:1260,y:558,fill:'#c8d4e8','font-size':'14','font-weight':'700','text-anchor':'middle','font-family':'Noto Sans SC'})).textContent='腹鳞单向摩擦原理';
g.appendChild(ce('text',{x:1260,y:576,fill:'#6878a0','font-size':'10','text-anchor':'middle','font-family':'Noto Sans SC'})).textContent='倾斜30° · 类百叶窗结构';
// 地面
g.appendChild(ce('line',{x1:1140,y1:710,x2:1380,y2:710,stroke:'#2a3a5a','stroke-width':'1.5'}));
g.appendChild(ce('text',{x:1150,y:728,fill:'#3a5a7a','font-size':'9','font-family':'Noto Sans SC'})).textContent='地面';
// 抓地状态
g.appendChild(ce('text',{x:1170,y:605,fill:'#3aaa5a','font-size':'11','font-weight':'600','font-family':'Noto Sans SC'})).textContent='抓地状态';
// 鳞片
g.appendChild(ce('path',{d:`M 1170 700 L 1170 680 L 1196 695 L 1196 700 Z`,fill:'#3aaa5a',stroke:'#4acc6a','stroke-width':'0.8'}));
// 力箭头 - 向后推时鳞片卡住
g.appendChild(ce('line',{x1:1180,y1:670,x2:1180,y2:695,stroke:'#ff4444','stroke-width':'1.5',opacity:'0.7'}));
g.appendChild(ce('path',{d:'M 1175 678 L 1180 668 L 1185 678 Z',fill:'#ff4444',opacity:'0.7'}));
g.appendChild(ce('text',{x:1160,y:665,fill:'#ff6666','font-size':'9','font-family':'Noto Sans SC'})).textContent='反向力';
g.appendChild(ce('text',{x:1168,y:720,fill:'#3aaa5a','font-size':'9','font-family':'Noto Sans SC'})).textContent='鳞片卡住';
// 滑动状态
g.appendChild(ce('text',{x:1310,y:605,fill:'#5a7a6a','font-size':'11','font-weight':'600','font-family':'Noto Sans SC'})).textContent='回缩状态';
g.appendChild(ce('path',{d:`M 1310 700 L 1310 690 L 1336 700 L 1336 700 Z`,fill:'#2a3a2a',stroke:'#3a5a3a','stroke-width':'0.8'}));
// 力箭头 - 顺滑滑过
g.appendChild(ce('line',{x1:1320,y1:680,x2:1320,y2:695,stroke:'#4a8aff','stroke-width':'1.5',opacity:'0.5'}));
g.appendChild(ce('path',{d:'M 1315 688 L 1320 678 L 1325 688 Z',fill:'#4a8aff',opacity:'0.5'}));
g.appendChild(ce('text',{x:1300,y:665,fill:'#6a9aff','font-size':'9','font-family':'Noto Sans SC'})).textContent='正向力');
g.appendChild(ce('text',{x:1305,y:720,fill:'#5a7a6a','font-size':'9','font-family':'Noto Sans SC'})).textContent='鳞片顺滑滑过');
// 30°标注
g.appendChild(ce('path',{d:'M 1170 700 L 1190 700',stroke:'#8898b8','stroke-width':'0.8','stroke-dasharray':'2,2'}));
g.appendChild(ce('path',{d:'M 1170 700 A 20 20 0 0 0 1180 694',fill:'none',stroke:'#8898b8','stroke-width':'0.8'}));
g.appendChild(ce('text',{x:1183,y:698,fill:'#8898b8','font-size':'9','font-family':'Rajdhani'})).textContent='30°';
// 动态鳞片动画
const animSc=ce('path',{id:'animScale',d:`M 1240 700 L 1240 685 L 1266 698 L 1266 700 Z`,fill:'#3aaa5a',stroke:'#4acc6a','stroke-width':'0.8'});
g.appendChild(animSc);
g.appendChild(ce('text',{x:1240,y:660,fill:'#8898b8','font-size':'9','text-anchor':'middle','font-family':'Noto Sans SC'})).textContent='动态示意';
}
/* ── 核心计算 ── */
function bodyY(frac,t){
const phase=frac*S.waveN*2*Math.PI;
return S.cy+S.amp*ampScale*Math.sin(phase-t*3);
}
function bodyDyDx(frac,t){
const L=S.ex-S.sx;
const k=S.waveN*2*Math.PI/L;
const phase=frac*S.waveN*2*Math.PI;
return S.amp*ampScale*k*Math.cos(phase-t*3);
}
function cableY(frac,t){
const phase=frac*S.waveN*2*Math.PI;
return S.cy+(S.amp*ampScale+S.cableOff*cableOff)*Math.sin(phase-t*3+0.3);
}
/* ── 更新循环 ── */
function update(dt){
const L=S.ex-S.sx;
const pts=[];
// 计算中心线点
for(let i=0;i<=S.nPts;i++){
const f=i/S.nPts;
const x=S.sx+f*L;
const y=bodyY(f,time);
const dy=bodyDyDx(f,time);
const nLen=Math.sqrt(dy*dy+1);
const nx=-dy/nLen, ny=1/nLen;
const hw=S.bw/2;
pts.push({x,y,nx,ny,ux:x+nx*hw,uy:y+ny*hw,lx:x-nx*hw,ly:y-ny*hw,f});
}
// 蛇身路径
let d=`M ${pts[0].ux.toFixed(1)} ${pts[0].uy.toFixed(1)}`;
for(let i=1;i<pts.length;i++) d+=` L ${pts[i].ux.toFixed(1)} ${pts[i].uy.toFixed(1)}`;
// 蛇头圆弧
const last=pts[pts.length-1];
d+=` A ${S.bw/2} ${S.bw/2} 0 0 1 ${last.lx.toFixed(1)} ${last.ly.toFixed(1)}`;
for(let i=pts.length-1;i>=0;i--) d+=` L ${pts[i].lx.toFixed(1)} ${pts[i].ly.toFixed(1)}`;
// 蛇尾圆弧
d+=` A ${S.bw/2} ${S.bw/2} 0 0 1 ${pts[0].ux.toFixed(1)} ${pts[0].uy.toFixed(1)}`;
d+=' Z';
sat(bodyPath,{d});
// 高光
let hd=`M ${pts[0].ux.toFixed(1)} ${(pts[0].uy+2).toFixed(1)}`;
for(let i=1;i<pts.length;i++) hd+=` L ${pts[i].ux.toFixed(1)} ${(pts[i].uy+2).toFixed(1)}`;
sat(bodyHL,{d:hd});
// 阴影
const shadowY=S.cy+80+S.amp*ampScale*0.5;
sat(document.getElementById('bodyShadow'),{cy:shadowY,rx:L*0.45,ry:8});
// 节间线
for(let i=0;i<=S.nSeg;i++){
const f=i/S.nSeg;
const idx=Math.round(f*S.nPts);
const p=pts[Math.min(idx,pts.length-1)];
sat(segLines[i],{x1:p.ux,y1:p.uy,x2:p.lx,y2:p.ly});
}
// 缆绳
let cd='M';
for(let i=0;i<=S.nPts;i++){
const f=i/S.nPts;
const x=S.sx+f*L;
const y=cableY(f,time);
cd+=(i===0?'':' ')+`${x.toFixed(1)} ${y.toFixed(1)}`;
}
sat(cableP,{d:cd});
sat(cableGlowP,{d:cd});
// 凸起高亮
let bIdx=0;
for(let i=0;i<pts.length;i+=Math.floor(pts.length/6)){
if(bIdx>=bulges.length)break;
const f=pts[i].f;
const phase=f*S.waveN*2*Math.PI-time*3;
const sinV=Math.sin(phase);
if(sinV>0.5){
const x=pts[i].x;
const y=bodyY(f,time)-S.bw/2*0.7;
sat(bulges[bIdx],{cx:x,cy:y,r:14+sinV*8,opacity:sinV*0.6});
}else{
sat(bulges[bIdx],{opacity:'0'});
}
bIdx++;
}
for(;bIdx<bulges.length;bIdx++)sat(bulges[bIdx],{opacity:'0'});
// 鳞片
for(let i=0;i<=S.nSeg;i++){
const f=i/S.nSeg;
const idx=Math.round(f*S.nPts);
const p=pts[Math.min(idx,pts.length-1)];
const phase=f*S.waveN*2*Math.PI-time*3;
const sinV=Math.sin(phase);
const cosV=Math.cos(phase);
// 鳞片在身体下方
const bx=p.lx;
const by=p.ly;
// 鳞片角度:抓地时更竖直,回缩时更平
const grip=sinV>0.1; // 身体向下压时抓地
const baseAngle=grip?-60:-30;
const rad=baseAngle*Math.PI/180;
const sH=S.scaleH;
const tx=bx+Math.cos(rad)*sH;
const ty=by+Math.sin(rad)*sH;
const d=`M ${bx.toFixed(1)} ${by.toFixed(1)} L ${tx.toFixed(1)} ${ty.toFixed(1)} L ${(tx+4).toFixed(1)} ${(ty-1).toFixed(1)} L ${(bx+4).toFixed(1)} ${(by-1).toFixed(1)} Z`;
const fill=grip?'#3aaa5a':'#2a3a2a';
const stroke=grip?'#4acc6a':'#3a5a3a';
sat(scales[i],{d,fill,stroke});
}
// 电机旋转
const spokes=svg.querySelectorAll('.spoke');
const mAngle=time*3*180/Math.PI;
spokes.forEach((s,idx)=>{
const a=(idx*Math.PI/2)+time*3;
const cx=S.sx-10,cy=S.cy;
sat(s,{x1:cx+Math.cos(a)*8,y1:cy+Math.sin(a)*8,x2:cx+Math.cos(a)*18,y2:cy+Math.sin(a)*18});
});
// 蛇头跟随
const headPt=pts[pts.length-1];
const headG=document.getElementById('headGroup');
const dy1=bodyDyDx(1,time);
const headAngle=Math.atan(dy1)*180/Math.PI;
sat(headG,{transform:`translate(${headPt.x},${headPt.y}) rotate(${headAngle}) translate(${-headPt.x},${-headPt.y})`});
// 眼睛微闪
const eyeOp=0.5+0.3*Math.sin(time*2);
sat(document.getElementById('eyeL'),{opacity:eyeOp});
sat(document.getElementById('eyeR'),{opacity:eyeOp});
// 波传播箭头
for(let i=0;i<waveArrs.length;i++){
const f=(i+0.5)/waveArrs.length;
const phase=f*S.waveN*2*Math.PI-time*3;
const sinV=Math.sin(phase);
const x=S.sx+f*L;
const y=bodyY(f,time)-S.bw/2-18;
const op=Math.max(0,sinV*0.5+0.2);
sat(waveArrs[i],{transform:`translate(${x},${y})`,opacity:op.toFixed(2)});
}
// 地面箭头
for(let i=0;i<gArrows.length;i++){
const baseX=220+i*85;
const offX=(time*40*motorSpd)%85;
const x=baseX+offX;
const op=0.15+0.15*Math.sin(time*2+i);
sat(gArrows[i],{transform:`translate(${x},555)`,opacity:op.toFixed(2)});
}
// 地面压力标记
for(let i=0;i<pMarks.length;i++){
const f=(i+0.5)/pMarks.length;
const phase=f*S.waveN*2*Math.PI-time*3;
const sinV=Math.sin(phase);
const x=S.sx+f*(S.ex-S.sx);
// 身体最低点时压力最大
const press=Math.max(0,-sinV);
sat(pMarks[i],{cx:x,opacity:(press*0.4).toFixed(2),rx:10+press*8});
}
// 截面图更新
const csAngle=-time*3;
const cX=CS.cx+CS.orbit*Math.cos(csAngle);
const cY=CS.cy+CS.orbit*Math.sin(csAngle);
sat(csCableDot,{cx:cX,cy:cY});
// 力线 - 从缆绳指向主干壁
const fAngle=csAngle;
const fEndX=CS.cx+CS.or*Math.cos(fAngle);
const fEndY=CS.cy+CS.or*Math.sin(fAngle);
sat(csForceLine,{x1:cX,y1:cY,x2:fEndX,y2:fEndY});
// 压力区弧
const arcSpan=0.4;
const a1=fAngle-arcSpan,a2=fAngle+arcSpan;
const largeArc=arcSpan*2>Math.PI?1:0;
const pD=`M ${CS.cx+CS.or*Math.cos(a1)} ${CS.cy+CS.or*Math.sin(a1)} A ${CS.or} ${CS.or} 0 ${largeArc} 1 ${CS.cx+CS.or*Math.cos(a2)} ${CS.cy+CS.or*Math.sin(a2)} L ${CS.cx+CS.or*0.6*Math.cos(a2)} ${CS.cy+CS.or*0.6*Math.sin(a2)} A ${CS.or*0.6} ${CS.or*0.6} 0 ${largeArc} 0 ${CS.cx+CS.or*0.6*Math.cos(a1)} ${CS.cy+CS.or*0.6*Math.sin(a1)} Z`;
sat(csPressure,{d:pD});
// 偏心距线
sat(csAngleLine,{x2:cX,y2:cY});
// 鳞片动态
const animSc=document.getElementById('animScale');
if(animSc){
const scPhase=Math.sin(time*3);
const scAngle2=scPhase>0.1?-60:-30;
const scRad=scAngle2*Math.PI/180;
const sH2=15;
const bx2=1253,by2=700;
const tx2=bx2+Math.cos(scRad)*sH2;
const ty2=by2+Math.sin(scRad)*sH2;
const grip2=scPhase>0.1;
sat(animSc,{
d:`M ${bx2} ${by2} L ${tx2} ${ty2} L ${tx2+5} ${ty2-1} L ${bx2+5} ${by2-1} Z`,
fill:grip2?'#3aaa5a':'#2a3a2a',
stroke:grip2?'#4acc6a':'#3a5a3a'
});
}
}
/* ── 动画循环 ── */
function loop(ts){
if(!prevTs)prevTs=ts;
const dt=Math.min((ts-prevTs)/1000,0.05);
prevTs=ts;
if(running)time+=dt*motorSpd;
update(dt);
requestAnimationFrame(loop);
}
/* ── 控件 ── */
document.getElementById('spdR').addEventListener('input',function(){
motorSpd=parseFloat(this.value);
document.getElementById('spdV').textContent=motorSpd.toFixed(1)+'x';
});
document.getElementById('offR').addEventListener('input',function(){
cableOff=parseFloat(this.value);
document.getElementById('offV').textContent=cableOff.toFixed(1);
});
document.getElementById('ampR').addEventListener('input',function(){
ampScale=parseFloat(this.value);
document.getElementById('ampV').textContent=ampScale.toFixed(1);
});
document.getElementById('ppBtn').addEventListener('click',function(){
running=!running;
this.textContent=running?'暂停':'播放';
});
/* ── 启动 ── */
document.addEventListener('DOMContentLoaded',function(){requestAnimationFrame(loop)});
// 备用:如果 DOMContentLoaded 已经触发
if(document.readyState!=='loading')requestAnimationFrame(loop);
})();
</script>
</body>
</html>
这个动画实现了以下核心内容:
视觉呈现
- 蛇身行波:柔性主干以正弦波形式从尾部向头部传播,体现"单旋转→全身行波"的核心 IFR 理念
- 偏心钢缆:橙色发光线条在蛇身内部随行波偏移,清晰展示凸起驱动弯曲的机理
- 凸起高亮:青色光晕标记波峰位置,引导视线关注关键创新点
- 腹部鳞片:绿色(抓地)/暗色(回缩)动态切换,展示单向摩擦原理
两个辅助面板
- 横截面视图(右上):展示偏心钢缆在柔性主干内旋转的截面,力线和压力区实时跟随旋转角度变化
- 腹鳞原理图(右下):对比抓地/回缩两种状态,动态鳞片随行波同步开合
交互控制
- 电机转速滑块:调节行波传播速度
- 偏心距滑块:调节缆绳偏心量,直观感受对波幅的影响
- 波幅滑块:直接控制弯曲波振幅
- 暂停/播放按钮
自动播放:页面加载后动画即刻启动,重新打开或刷新自动从头播放,无需任何手动触发。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
