<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>气动柔性传动扑翼 · 最终理想解原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root {
--bg: #070b16;
--bg2: #0c1224;
--steel: #6a7d96;
--steel-lt: #98adc4;
--teal: #00e8be;
--teal-dim: #007a64;
--amber: #ff9a22;
--amber-lt: #ffc86a;
--green: #3dd870;
--red: #ff3850;
--text: #c0cee0;
--text-dim: #4e6080;
--card: #0f1728;
--border: #1a2640;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{
background:var(--bg);
color:var(--text);
font-family:'Rajdhani',sans-serif;
display:flex;flex-direction:column;
align-items:center;justify-content:center;
}
/* 顶部标题 */
#header{
position:fixed;top:0;left:0;right:0;z-index:10;
display:flex;align-items:center;justify-content:center;gap:18px;
padding:14px 28px;
background:linear-gradient(180deg,rgba(7,11,22,.95) 60%,transparent);
pointer-events:none;
}
#header h1{
font-size:22px;font-weight:700;letter-spacing:2px;
color:var(--amber);text-transform:uppercase;
}
#header .sep{width:1px;height:20px;background:var(--border)}
#header .sub{font-size:14px;color:var(--text-dim);font-weight:500;letter-spacing:1px}
/* SVG 容器 */
#svg-wrap{
width:100vw;height:100vh;
display:flex;align-items:center;justify-content:center;
}
#svg-wrap svg{width:100%;height:100%;display:block}
/* 控制面板 */
#panel{
position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:10;
display:flex;align-items:center;gap:28px;
padding:14px 32px;
background:rgba(12,18,36,.92);
border:1px solid var(--border);border-radius:14px;
backdrop-filter:blur(12px);
}
.ctrl{display:flex;flex-direction:column;gap:4px;align-items:flex-start}
.ctrl label{font-size:11px;color:var(--text-dim);letter-spacing:1px;font-weight:600;text-transform:uppercase}
.ctrl .row{display:flex;align-items:center;gap:8px}
.ctrl input[type=range]{
-webkit-appearance:none;width:130px;height:4px;
background:var(--border);border-radius:2px;outline:none;
}
.ctrl input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;
background:var(--amber);border-radius:50%;cursor:pointer;
box-shadow:0 0 8px rgba(255,154,34,.5);
}
.ctrl .val{
font-family:'Share Tech Mono',monospace;font-size:13px;
color:var(--amber-lt);min-width:65px;
}
#play-btn{
padding:8px 20px;border:1px solid var(--border);border-radius:8px;
background:transparent;color:var(--text);font-family:'Rajdhani',sans-serif;
font-size:14px;font-weight:600;cursor:pointer;letter-spacing:1px;
transition:all .2s;
}
#play-btn:hover{border-color:var(--amber);color:var(--amber)}
/* 相位指示器 */
#phase-badge{
position:fixed;top:62px;left:50%;transform:translateX(-50%);z-index:10;
padding:5px 18px;border-radius:20px;
font-size:13px;font-weight:600;letter-spacing:1.5px;
transition:all .3s;
}
#phase-badge.down{background:rgba(255,154,34,.15);color:var(--amber);border:1px solid rgba(255,154,34,.3)}
#phase-badge.up{background:rgba(61,216,112,.12);color:var(--green);border:1px solid rgba(61,216,112,.25)}
/* 响应式 */
@media(max-width:800px){
#panel{gap:14px;padding:10px 16px;flex-wrap:wrap;justify-content:center}
.ctrl input[type=range]{width:90px}
#header h1{font-size:16px}
}
</style>
</head>
<body>
<div id="header">
<h1>气动柔性传动扑翼</h1>
<div class="sep"></div>
<span class="sub">最终理想解 (IFR) 原理动画</span>
</div>
<div id="phase-badge" class="down">下扑 · 充气驱动</div>
<div id="svg-wrap">
<svg id="svg" viewBox="0 0 1400 800" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="panel">
<div class="ctrl">
<label>扑动频率</label>
<div class="row">
<input type="range" id="sl-freq" min="0.4" max="3" step="0.1" value="1.2">
<span class="val" id="v-freq">1.2 Hz</span>
</div>
</div>
<div class="ctrl">
<label>工作气压</label>
<div class="row">
<input type="range" id="sl-pres" min="0.20" max="0.50" step="0.01" value="0.35">
<span class="val" id="v-pres">0.35 MPa</span>
</div>
</div>
<button id="play-btn">暂停</button>
</div>
<script>
(function(){
/* ============ 常量与状态 ============ */
const NS='http://www.w3.org/2000/svg';
const CX=700,CY=370; // 机身中心
const LPX=632,LPY=350; // 左翼枢轴
const RPX=768,RPY=350; // 右翼枢轴
const WLEN=375; // 翼展半长
const DOWN_MAX=24,UP_MAX=-16; // 扑动角度范围(度)
const BELLOWS_START=80,BELLOWS_END=310; // 波纹管在翼展上的区间(相对枢轴)
const BELLOWS_FOLDS=8;
const SPRING_START=18,SPRING_END=72;
const SPRING_COILS=4;
let time=0,freq=1.2,pres=0.35,playing=true,lastTs=0;
let particles=[];
/* ============ 工具函数 ============ */
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
const easeIO=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const deg2rad=d=>d*Math.PI/180;
function rotPt(x,y,cx,cy,deg){
const r=deg2rad(deg),dx=x-cx,dy=y-cy;
return{x:cx+dx*Math.cos(r)-dy*Math.sin(r),y:cy+dx*Math.sin(r)+dy*Math.cos(r)};
}
function svgEl(tag,attrs,parent){
const el=document.createElementNS(NS,tag);
for(const[k,v]of Object.entries(attrs||{}))el.setAttribute(k,v);
if(parent)parent.appendChild(el);
return el;
}
/* ============ 构建 SVG ============ */
const svg=document.getElementById('svg');
// -- defs --
const defs=svgEl('defs',{},svg);
// 网格背景
const pat=svgEl('pattern',{id:'grid',width:40,height:40,patternUnits:'userSpaceOnUse'},defs);
svgEl('path',{d:'M 40 0 L 0 0 0 40',fill:'none',stroke:'#141e34','stroke-width':0.5},pat);
// 发光滤镜
function mkGlow(id,sd){
const f=svgEl('filter',{id,x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
svgEl('feGaussianBlur',{in:'SourceGraphic',stdDeviation:sd,result:'b'},f);
const m=svgEl('feMerge',{},f);
svgEl('feMergeNode',{in:'b'},m);
svgEl('feMergeNode',{in:'b'},m);
svgEl('feMergeNode',{in:'SourceGraphic'},m);
}
mkGlow('glow',3);mkGlow('glow-s',6);mkGlow('glow-xs',1.5);
// 渐变
const bgGrad=svgEl('radialGradient',{id:'bg-grad',cx:'50%',cy:'45%',r:'55%'},defs);
svgEl('stop',{offset:'0%','stop-color':'#0e1830'},bgGrad);
svgEl('stop',{offset:'100%','stop-color':'#070b16'},bgGrad);
const bodyGrad=svgEl('linearGradient',{id:'body-grad',x1:'0',y1:'0',x2:'0',y2:'1'},defs);
svgEl('stop',{offset:'0%','stop-color':'#1e2c48'},bodyGrad);
svgEl('stop',{offset:'100%','stop-color':'#0e1628'},bodyGrad);
const vesselGrad=svgEl('linearGradient',{id:'vessel-grad',x1:'0',y1:'0',x2:'1',y2:'1'},defs);
svgEl('stop',{offset:'0%','stop-color':'#ff9a22'},vesselGrad);
svgEl('stop',{offset:'100%','stop-color':'#cc6600'},vesselGrad);
const wingGradL=svgEl('linearGradient',{id:'wing-grad-l',x1:'1',y1:'0',x2:'0',y2:'0'},defs);
svgEl('stop',{offset:'0%','stop-color':'rgba(30,50,80,0.7)'},wingGradL);
svgEl('stop',{offset:'100%','stop-color':'rgba(15,25,45,0.3)'},wingGradL);
const wingGradR=svgEl('linearGradient',{id:'wing-grad-r',x1:'0',y1:'0',x2:'1',y2:'0'},defs);
svgEl('stop',{offset:'0%','stop-color':'rgba(30,50,80,0.7)'},wingGradR);
svgEl('stop',{offset:'100%','stop-color':'rgba(15,25,45,0.3)'},wingGradR);
// 箭头标记
const mArrow=svgEl('marker',{id:'arrow-teal',viewBox:'0 0 10 10',refX:'10',refY:'5',markerWidth:'6',markerHeight:'6',orient:'auto'},defs);
svgEl('path',{d:'M 0 0 L 10 5 L 0 10 z',fill:'#00e8be'},mArrow);
// -- 背景层 --
svgEl('rect',{width:1400,height:800,fill:'url(#bg-grad)'},svg);
svgEl('rect',{width:1400,height:800,fill:'url(#grid)',opacity:0.6},svg);
// -- 气流路径层 --
const gAirflow=svgEl('g',{id:'g-airflow'},svg);
// -- 左翼组 --
const gWingL=svgEl('g',{id:'g-wing-l'},svg);
// 翼面膜
svgEl('path',{id:'wing-l-mem',d:wingMemPath(-1),fill:'url(#wing-grad-l)',stroke:'#3a5478','stroke-width':1.2},gWingL);
// 翼肋(前缘)
svgEl('line',{id:'spar-l',x1:0,y1:0,x2:-WLEN,y2:0,stroke:'#4a6888','stroke-width':2.5,'stroke-linecap':'round'},gWingL);
// 波纹管
svgEl('path',{id:'bellows-l',fill:'none',stroke:'#ff9a22','stroke-width':2,'stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#glow-xs)'},gWingL);
// 鲍登线
svgEl('path',{id:'cable-l',fill:'none',stroke:'#00e8be','stroke-width':1.2,'stroke-dasharray':'6 4',opacity:0.7},gWingL);
// 弹簧
svgEl('path',{id:'spring-l',fill:'none',stroke:'#3dd870','stroke-width':1.8,'stroke-linecap':'round'},gWingL);
// -- 右翼组 --
const gWingR=svgEl('g',{id:'g-wing-r'},svg);
svgEl('path',{id:'wing-r-mem',d:wingMemPath(1),fill:'url(#wing-grad-r)',stroke:'#3a5478','stroke-width':1.2},gWingR);
svgEl('line',{id:'spar-r',x1:0,y1:0,x2:WLEN,y2:0,stroke:'#4a6888','stroke-width':2.5,'stroke-linecap':'round'},gWingR);
svgEl('path',{id:'bellows-r',fill:'none',stroke:'#ff9a22','stroke-width':2,'stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#glow-xs)'},gWingR);
svgEl('path',{id:'cable-r',fill:'none',stroke:'#00e8be','stroke-width':1.2,'stroke-dasharray':'6 4',opacity:0.7},gWingR);
svgEl('path',{id:'spring-r',fill:'none',stroke:'#3dd870','stroke-width':1.8,'stroke-linecap':'round'},gWingR);
// -- 机身层 --
const gBody=svgEl('g',{id:'g-body'},svg);
// 机身外框
svgEl('rect',{x:CX-68,y:CY-100,width:136,height:200,rx:22,fill:'url(#body-grad)',stroke:'#2a3c5c','stroke-width':1.5},gBody);
// 压力容器(高压气源)
svgEl('rect',{id:'vessel',x:CX-32,y:CY-70,width:64,height:44,rx:10,fill:'url(#vessel-grad)',opacity:0.85,filter:'url(#glow)'},gBody);
svgEl('text',{x:CX,y:CY-52,'text-anchor':'middle',fill:'#fff','font-size':'9','font-family':'Share Tech Mono','font-weight':'700'},gBody).textContent='HP AIR';
// 气泵活塞
const gPump=svgEl('g',{id:'g-pump'},gBody);
svgEl('rect',{x:CX-18,y:CY-16,width:36,height:28,rx:4,fill:'#1a2840',stroke:'#3a5478','stroke-width':1},gPump);
svgEl('rect',{id:'piston',x:CX-10,y:CY-10,width:20,height:10,rx:2,fill:'#5a7a9a'},gPump);
// 阀门
svgEl('circle',{id:'valve-l',cx:CX-42,cy:CY-14,r:7,fill:'#1a2840',stroke:'#3a5478','stroke-width':1.2},gBody);
svgEl('circle',{id:'valve-r',cx:CX+42,cy:CY-14,r:7,fill:'#1a2840',stroke:'#3a5478','stroke-width':1.2},gBody);
svgEl('text',{x:CX-42,y:CY-11,'text-anchor':'middle',fill:'#8098b8','font-size':'7','font-family':'Share Tech Mono'},gBody).textContent='V_L';
svgEl('text',{x:CX+42,y:CY-11,'text-anchor':'middle',fill:'#8098b8','font-size':'7','font-family':'Share Tech Mono'},gBody).textContent='V_R';
// 气路连接线(机身内)
svgEl('path',{d:`M${CX-10},${CY-48} L${CX-42},${CY-48} L${CX-42},${CY-21}`,fill:'none',stroke:'#3a5478','stroke-width':1.5,'stroke-dasharray':'3 2'},gBody);
svgEl('path',{d:`M${CX+10},${CY-48} L${CX+42},${CY-48} L${CX+42},${CY-21}`,fill:'none',stroke:'#3a5478','stroke-width':1.5,'stroke-dasharray':'3 2'},gBody);
// 重心标记
svgEl('circle',{cx:CX,cy:CY+40,r:5,fill:'none',stroke:'#ff9a22','stroke-width':1.5,filter:'url(#glow-xs)'},gBody);
svgEl('line',{x1:CX-8,y1:CY+40,x2:CX+8,y2:CY+40,stroke:'#ff9a22','stroke-width':1},gBody);
svgEl('line',{x1:CX,y1:CY+32,x2:CX,y2:CY+48,stroke:'#ff9a22','stroke-width':1},gBody);
svgEl('text',{x:CX,y:CY+62,'text-anchor':'middle',fill:'#ff9a22','font-size':'9','font-family':'Share Tech Mono','font-weight':'600'},gBody).textContent='CG';
// -- 标注层 --
const gLabels=svgEl('g',{id:'g-labels'},svg);
// 波纹管标注
addLabel(380,260,'气动波纹管','0.2–0.5 MPa 充气膨胀直接驱动翼面','#ff9a22');
addLabel(1020,260,'鲍登线牵引','内线连接翼尖,如木偶提线','#00e8be');
addLabel(CX,CY+130,'动力源集中于重心','翼面近乎零质量 → 转动惯量极低','#ff9a22');
// -- 惯性对比插图 --
const gInertia=svgEl('g',{transform:'translate(1180,680)'},svg);
svgEl('rect',{x:-90,y:-50,width:180,height:100,rx:10,fill:'rgba(12,18,36,0.85)',stroke:'#1a2640','stroke-width':1},gInertia);
svgEl('text',{x:0,y:-32,'text-anchor':'middle',fill:'#8098b8','font-size':'10','font-family':'Rajdhani','font-weight':'600',letterSpacing:'1'},gInertia).textContent='转动惯量对比';
// 传统设计
svgEl('circle',{cx:-35,cy:8,r:22,fill:'none',stroke:'#ff3850','stroke-width':1.5,'stroke-dasharray':'4 3',opacity:0.7},gInertia);
svgEl('text',{x:-35,y:12,'text-anchor':'middle',fill:'#ff3850','font-size':'8','font-family':'Share Tech Mono',opacity:0.8},gInertia).textContent='高';
svgEl('text',{x:-35,y:38,'text-anchor':'middle',fill:'#607090','font-size':'8','font-family':'Rajdhani'},gInertia).textContent='传统';
// 理想解
svgEl('circle',{id:'inertia-ideal',cx:35,cy:8,r:8,fill:'none',stroke:'#3dd870','stroke-width':1.8,filter:'url(#glow-xs)'},gInertia);
svgEl('text',{x:35,y:12,'text-anchor':'middle',fill:'#3dd870','font-size':'8','font-family':'Share Tech Mono',opacity:0.9},gInertia).textContent='低';
svgEl('text',{x:35,y:38,'text-anchor':'middle',fill:'#607090','font-size':'8','font-family':'Rajdhani'},gInertia).textContent='IFR';
// ============ 辅助绘图 ============
function wingMemPath(side){
// side: -1=左翼, 1=右翼
const s=side, L=WLEN;
const rx=s*L;
return`M0,-32 C${s*-80},-30 ${s*-220},-16 ${rx},-9 L${rx},9 C${s*-220},16 ${s*-80},30 0,32 Z`;
}
function bellowsPath(side,inflation){
// side=-1左,1右; inflation:0~1
const sx=side===-1?LPX:RPX, sy=side===-1?LPY:RPY;
const dir=side;
const start=dir*BELLOWS_START, end=dir*BELLOWS_END;
const span=Math.abs(end-start);
const pitch=span/BELLOWS_FOLDS;
const amp=lerp(8,24,inflation);
let d=`M${sx+start},${sy}`;
for(let i=0;i<BELLOWS_FOLDS;i++){
const x0=sx+start+dir*pitch*i;
const x1=x0+dir*pitch*0.25;
const x2=x0+dir*pitch*0.75;
const x3=x0+dir*pitch;
d+=` L${x1},${sy-amp} L${x2},${sy+amp} L${x3},${sy}`;
}
return d;
}
function springPath(side,compression){
// compression: 0(放松)~1(压缩)
const sx=side===-1?LPX:RPX, sy=side===-1?LPY:RPY;
const dir=side;
const start=dir*SPRING_START, end=dir*SPRING_END;
const actualEnd=dir===-1?
sx+start+lerp(Math.abs(end-start),Math.abs(end-start)*0.55,compression):
sx+start-lerp(Math.abs(end-start),Math.abs(end-start)*0.55,compression);
const amp=10;
const segs=SPRING_COILS*2;
const totalLen=Math.abs(actualEnd-(sx+start));
const segLen=totalLen/segs;
let d=`M${sx+start},${sy}`;
for(let i=1;i<=segs;i++){
const x=sx+start+dir*segLen*i;
const y=sy+(i%2===1?-amp:amp);
d+=` L${x},${y}`;
}
d+=` L${actualEnd},${sy}`;
return d;
}
function cablePath(side,tension){
// tension: 0(松)~1(紧)
const sx=side===-1?LPX:RPX, sy=side===-1?LPY:RPY;
const dir=side;
const tipX=sx+dir*WLEN;
const sag=lerp(18,2,tension);
const cx=sx+dir*WLEN*0.5;
const cy=sy+sag;
return`M${sx+dir*12},${sy} Q${cx},${cy} ${tipX-dir*15},${sy}`;
}
function addLabel(x,y,title,desc,color){
const g=svgEl('g',{},gLabels);
svgEl('line',{x1:x,y1:y+6,x2:x,y2:y+22,stroke:color,'stroke-width':0.8,opacity:0.5},g);
svgEl('text',{x:x,y,'text-anchor':'middle',fill:color,'font-size':'12','font-family':'Rajdhani','font-weight':'700',letterSpacing:'0.5'},g).textContent=title;
// 多行描述
const words=desc.split('');
let line='',ly=y+16;
const maxW=22;
for(const ch of words){
line+=ch;
if(line.length>=maxW){
svgEl('text',{x:x,y:ly,'text-anchor':'middle',fill:'#607898','font-size':'9','font-family':'Rajdhani'},g).textContent=line;
ly+=12;line='';
}
}
if(line)svgEl('text',{x:x,y:ly,'text-anchor':'middle',fill:'#607898','font-size':'9','font-family':'Rajdhani'},g).textContent=line;
}
/* ============ 气流粒子系统 ============ */
function initParticles(){
particles=[];
for(let s=-1;s<=1;s+=2){
for(let i=0;i<14;i++){
const p={side:s,prog:Math.random(),speed:0.3+Math.random()*0.25,el:null};
p.el=svgEl('circle',{r:2.5,fill:s===-1?'#00e8be':'#00e8be',opacity:0},gAirflow);
particles.push(p);
}
}
}
function getParticlePos(side,prog,angle){
// 路径: 压力容器 → 阀门 → 翼根 → 波纹管中心
const sx=side===-1?LPX:RPX, sy=side===-1?LPY:RPY;
const waypoints=[
{x:CX,y:CY-48}, // 压力容器出口
{x:side===-1?CX-42:CX+42,y:CY-14}, // 阀门
{x:sx,y:sy}, // 翼根枢轴
{x:sx+side*(BELLOWS_START+BELLOWS_END)/2,y:sy} // 波纹管中心
];
// 最后一个点随翼旋转
const last=waypoints[waypoints.length-1];
const rotated=rotPt(last.x,last.y,sx,sy,side===-1?-angle:angle);
waypoints[waypoints.length-1]=rotated;
// 枢轴点也旋转后面的路径
const pivot=waypoints[2];
// 插值
const n=waypoints.length-1;
const seg=clamp(prog*n,0,n-0.001);
const si=Math.floor(seg);
const t=seg-si;
const a=waypoints[si],b=waypoints[Math.min(si+1,n)];
return{x:lerp(a.x,b.x,t),y:lerp(a.y,b.y,t)};
}
/* ============ 动画主循环 ============ */
function loop(ts){
const dt=lastTs?(ts-lastTs)/1000:0;
lastTs=ts;
if(playing)time+=dt;
const cycle=(time*freq)%1;
// 非对称扑动: 下扑快(0~0.35), 上扑慢(0.35~1)
let angle,inflation,compression,tension;
if(cycle<0.35){
const t=easeIO(cycle/0.35);
angle=lerp(UP_MAX,DOWN_MAX,t);
inflation=t;
compression=lerp(0.2,1,t);
tension=lerp(0.2,1,t);
}else{
const t=easeIO((cycle-0.35)/0.65);
angle=lerp(DOWN_MAX,UP_MAX,t);
inflation=1-t;
compression=lerp(1,0.2,t);
tension=lerp(1,0.2,t);
}
// 翼面旋转
gWingL.setAttribute('transform',`translate(${LPX},${LPY}) rotate(${-angle}) translate(${-LPX},${-LPY})`);
gWingR.setAttribute('transform',`translate(${RPX},${RPY}) rotate(${angle}) translate(${-RPX},${-RPY})`);
// 波纹管
document.getElementById('bellows-l').setAttribute('d',bellowsPath(-1,inflation));
document.getElementById('bellows-r').setAttribute('d',bellowsPath(1,inflation));
// 弹簧
document.getElementById('spring-l').setAttribute('d',springPath(-1,compression));
document.getElementById('spring-r').setAttribute('d',springPath(1,compression));
// 鲍登线
document.getElementById('cable-l').setAttribute('d',cablePath(-1,tension));
document.getElementById('cable-r').setAttribute('d',cablePath(1,tension));
// 阀门状态
const valveL=document.getElementById('valve-l');
const valveR=document.getElementById('valve-r');
const valveOpen=cycle<0.35;
valveL.setAttribute('fill',valveOpen?'#00e8be':'#1a2840');
valveL.setAttribute('stroke',valveOpen?'#00e8be':'#3a5478');
valveR.setAttribute('fill',valveOpen?'#00e8be':'#1a2840');
valveR.setAttribute('stroke',valveOpen?'#00e8be':'#3a5478');
// 活塞
const pistonY=CY-10+Math.sin(cycle*Math.PI*2)*5;
document.getElementById('piston').setAttribute('y',pistonY);
// 压力容器脉动
const vScale=1+Math.sin(cycle*Math.PI*2)*0.03;
const vessel=document.getElementById('vessel');
vessel.setAttribute('transform',`translate(${CX},${CY-48}) scale(${vScale}) translate(${-CX},${-(CY-48)})`);
// 气流粒子
const pActive=valveOpen;
for(const p of particles){
if(playing)p.prog+=dt*p.speed*freq*1.2;
if(p.prog>1)p.prog-=1;
const pos=getParticlePos(p.side,p.prog,angle);
p.el.setAttribute('cx',pos.x);
p.el.setAttribute('cy',pos.y);
// 粒子在管路内可见,进入翼面后渐隐
const opacity=pActive?clamp(1-p.prog*0.6,0.15,0.9):0.05;
p.el.setAttribute('opacity',opacity);
const r=pActive?lerp(3,1.5,p.prog):1;
p.el.setAttribute('r',r);
}
// 相位指示
const badge=document.getElementById('phase-badge');
if(cycle<0.35){
badge.className='down';
badge.textContent='下扑 · 充气驱动';
}else{
badge.className='up';
badge.textContent='上扑 · 弹簧复位';
}
// 惯性圆环动画
const idealCircle=document.getElementById('inertia-ideal');
const iScale=1+Math.sin(time*3)*0.15;
idealCircle.setAttribute('r',8*iScale);
requestAnimationFrame(loop);
}
/* ============ 控制交互 ============ */
const slFreq=document.getElementById('sl-freq');
const slPres=document.getElementById('sl-pres');
const vFreq=document.getElementById('v-freq');
const vPres=document.getElementById('v-pres');
const playBtn=document.getElementById('play-btn');
slFreq.addEventListener('input',()=>{
freq=parseFloat(slFreq.value);
vFreq.textContent=freq.toFixed(1)+' Hz';
});
slPres.addEventListener('input',()=>{
pres=parseFloat(slPres.value);
vPres.textContent=pres.toFixed(2)+' MPa';
});
playBtn.addEventListener('click',()=>{
playing=!playing;
playBtn.textContent=playing?'暂停':'播放';
});
/* ============ 初始化 ============ */
initParticles();
// 初始化翼面路径(spring/bellows/cable 需要初始值)
document.getElementById('spring-l').setAttribute('d',springPath(-1,0.2));
document.getElementById('spring-r').setAttribute('d',springPath(1,0.2));
document.getElementById('cable-l').setAttribute('d',cablePath(-1,0.2));
document.getElementById('cable-r').setAttribute('d',cablePath(1,0.2));
document.getElementById('bellows-l').setAttribute('d',bellowsPath(-1,0));
document.getElementById('bellows-r').setAttribute('d',bellowsPath(1,0));
// 启动动画
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
实现说明:
核心原理可视化:动画直接呈现最终理想解的运行状态——机身核心的高压气源通过阀门阵列交替向左右翼内的气动波纹管充气,波纹管膨胀驱动翼面下扑,弹簧复位完成上扑。整个循环自动播放,无需手动触发。
关键创新点视觉引导:
- 琥珀色高亮:波纹管膨胀/收缩是破除"远端质量大→转动惯量高"矛盾的核心动作,以琥珀色发光路径突出
- 青色气流粒子:从压力容器经阀门流向波纹管的气流,以动态粒子可视化驱动时序
- 阀门开闭状态:阀门在充气相以青色点亮,复位相暗淡,明确展示"气阀左开→关闭→交替"的时序
- CG标记:机身重心处十字标记,强调"最重动力源集中于重心"的IFR核心思路
转动惯量对比插图:右下角小面板直观对比传统设计(大红虚线圆=高惯量)与IFR方案(小绿实线圆=低惯量),呼应"翼面近乎零质量"的矛盾消解。
交互控制:频率滑块(0.4–3 Hz)和气压滑块(0.20–0.50 MPa)允许实时调节关键参数,直接体验不同工况下的扑动动态;播放/暂停按钮控制动画运行。
非对称扑动:下扑相占周期35%(气动驱动,快速有力),上扑相占65%(弹簧复位,较慢),符合物理实际并增强视觉节奏感。
<!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 rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Teko:wght@300;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root{
--bg:#050910;--bg2:#0a1020;
--copper:#e8842a;--copper-lt:#ffba5c;--copper-glow:rgba(232,132,42,.45);
--teal:#00ddb8;--teal-dim:#007a64;--teal-glow:rgba(0,221,184,.4);
--green:#3cd868;--green-dim:#1a7a38;
--red:#ff3850;
--steel:#4a6888;--steel-lt:#7a9ab8;
--text:#b8c8e0;--text-dim:#3e5878;--text-bright:#e0ecff;
--card:rgba(10,16,32,.92);--border:#162040;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{
background:var(--bg);color:var(--text);
font-family:'Teko',system-ui,sans-serif;
display:flex;flex-direction:column;align-items:center;justify-content:center;
}
#header{
position:fixed;top:0;left:0;right:0;z-index:10;
display:flex;align-items:center;justify-content:center;gap:16px;
padding:12px 24px;
background:linear-gradient(180deg,rgba(5,9,16,.96) 55%,transparent);
pointer-events:none;
}
#header h1{
font-size:26px;font-weight:700;letter-spacing:3px;
color:var(--copper-lt);text-transform:uppercase;
}
#header .sep{width:1px;height:22px;background:var(--border)}
#header .sub{font-size:15px;color:var(--text-dim);font-weight:500;letter-spacing:1.5px}
#phase-badge{
position:fixed;top:60px;left:50%;transform:translateX(-50%);z-index:10;
padding:5px 20px;border-radius:20px;
font-size:14px;font-weight:500;letter-spacing:2px;
font-family:'Share Tech Mono',monospace;
transition:all .35s;
}
#phase-badge.left-down{background:rgba(232,132,42,.12);color:var(--copper-lt);border:1px solid rgba(232,132,42,.3)}
#phase-badge.right-down{background:rgba(0,221,184,.1);color:var(--teal);border:1px solid rgba(0,221,184,.25)}
#phase-badge.both-up{background:rgba(60,216,104,.08);color:var(--green);border:1px solid rgba(60,216,104,.2)}
#svg-wrap{width:100vw;height:100vh;display:flex;align-items:center;justify-content:center}
#svg-wrap svg{width:100%;height:100%;display:block}
#panel{
position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:10;
display:flex;align-items:center;gap:24px;
padding:12px 28px;
background:var(--card);border:1px solid var(--border);border-radius:14px;
backdrop-filter:blur(14px);
}
.ctrl{display:flex;flex-direction:column;gap:3px;align-items:flex-start}
.ctrl label{font-size:11px;color:var(--text-dim);letter-spacing:1.5px;font-weight:500;text-transform:uppercase;font-family:'Share Tech Mono',monospace}
.ctrl .row{display:flex;align-items:center;gap:8px}
.ctrl input[type=range]{
-webkit-appearance:none;width:120px;height:3px;
background:var(--border);border-radius:2px;outline:none;
}
.ctrl input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:14px;height:14px;
background:var(--copper);border-radius:50%;cursor:pointer;
box-shadow:0 0 8px var(--copper-glow);
}
.ctrl .val{font-family:'Share Tech Mono',monospace;font-size:12px;color:var(--copper-lt);min-width:68px}
#play-btn{
padding:7px 18px;border:1px solid var(--border);border-radius:8px;
background:transparent;color:var(--text);font-family:'Teko',sans-serif;
font-size:15px;font-weight:500;cursor:pointer;letter-spacing:1.5px;
transition:all .2s;
}
#play-btn:hover{border-color:var(--copper);color:var(--copper-lt)}
@media(max-width:760px){
#panel{gap:12px;padding:10px 14px;flex-wrap:wrap;justify-content:center}
.ctrl input[type=range]{width:80px}
#header h1{font-size:18px}
}
</style>
</head>
<body>
<div id="header">
<h1>气动柔性扑翼传动</h1>
<div class="sep"></div>
<span class="sub">最终理想解 IFR 原理动画</span>
</div>
<div id="phase-badge" class="left-down">左翼下扑 · 波纹管充气</div>
<div id="svg-wrap">
<svg id="svg" viewBox="0 0 1400 800" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="panel">
<div class="ctrl">
<label>扑动频率</label>
<div class="row">
<input type="range" id="sl-freq" min="0.4" max="3" step="0.1" value="1.2">
<span class="val" id="v-freq">1.2 Hz</span>
</div>
</div>
<div class="ctrl">
<label>工作气压</label>
<div class="row">
<input type="range" id="sl-pres" min="0.20" max="0.50" step="0.01" value="0.35">
<span class="val" id="v-pres">0.35 MPa</span>
</div>
</div>
<button id="play-btn">暂停</button>
</div>
<script>
(function(){
/* ===== 常量 ===== */
const NS='http://www.w3.org/2000/svg';
const CX=700,CY=400;
const LPX=630,LPY=375;
const RPX=770,RPY=375;
const WLEN=350;
const DOWN_MAX=24,UP_MAX=-15;
const B_START=75,B_END=305,B_FOLDS=9;
const SP_START=14,SP_END=68,SP_COILS=5;
/* ===== 状态 ===== */
let time=0,freq=1.2,pres=0.35,playing=true,lastTs=0;
let particles=[];
/* ===== 工具 ===== */
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
const easeIO=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
function el(tag,attrs,parent){
const e=document.createElementNS(NS,tag);
for(const[k,v]of Object.entries(attrs||{}))e.setAttribute(k,v);
if(parent)parent.appendChild(e);
return e;
}
/* ===== SVG 构建 ===== */
const svg=document.getElementById('svg');
/* -- defs -- */
const defs=el('defs',{},svg);
// 背景渐变
const bgG=el('radialGradient',{id:'bg-g',cx:'50%',cy:'42%',r:'58%'},defs);
el('stop',{offset:'0%','stop-color':'#0c1428'},bgG);
el('stop',{offset:'100%','stop-color':'#050910'},bgG);
// 网格
const pat=el('pattern',{id:'grid',width:40,height:40,patternUnits:'userSpaceOnUse'},defs);
el('path',{d:'M 40 0 L 0 0 0 40',fill:'none',stroke:'#0e1828','stroke-width':0.5},pat);
// 机身渐变
const bodyG=el('linearGradient',{id:'body-g',x1:'0',y1:'0',x2:'0',y2:'1'},defs);
el('stop',{offset:'0%','stop-color':'#182440'},bodyG);
el('stop',{offset:'100%','stop-color':'#0c1628'},bodyG);
// 容器渐变
const vesG=el('linearGradient',{id:'ves-g',x1:'0',y1:'0',x2:'1',y2:'1'},defs);
el('stop',{offset:'0%','stop-color':'#e8842a'},vesG);
el('stop',{offset:'100%','stop-color':'#b85a10'},vesG);
// 翼膜渐变(左)
const wgL=el('linearGradient',{id:'wg-l',x1:'1',y1:'0',x2:'0',y2:'0'},defs);
el('stop',{offset:'0%','stop-color':'rgba(22,40,68,0.6)'},wgL);
el('stop',{offset:'100%','stop-color':'rgba(12,22,40,0.2)'},wgL);
// 翼膜渐变(右)
const wgR=el('linearGradient',{id:'wg-r',x1:'0',y1:'0',x2:'1',y2:'0'},defs);
el('stop',{offset:'0%','stop-color':'rgba(22,40,68,0.6)'},wgR);
el('stop',{offset:'100%','stop-color':'rgba(12,22,40,0.2)'},wgR);
// 发光滤镜
function mkGlow(id,sd){
const f=el('filter',{id,x:'-60%',y:'-60%',width:'220%',height:'220%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:sd,result:'b'},f);
const m=el('feMerge',{},f);
el('feMergeNode',{in:'b'},m);el('feMergeNode',{in:'b'},m);el('feMergeNode',{in:'SourceGraphic'},m);
}
mkGlow('glow',4);mkGlow('glow-s',7);mkGlow('glow-xs',1.8);
// 箭头
const ma=el('marker',{id:'arr-t',viewBox:'0 0 10 10',refX:'10',refY:'5',markerWidth:'5',markerHeight:'5',orient:'auto'},defs);
el('path',{d:'M 0 1 L 10 5 L 0 9 z',fill:'#00ddb8'},ma);
const ma2=el('marker',{id:'arr-c',viewBox:'0 0 10 10',refX:'10',refY:'5',markerWidth:'5',markerHeight:'5',orient:'auto'},defs);
el('path',{d:'M 0 1 L 10 5 L 0 9 z',fill:'#e8842a'},ma2);
/* -- 背景层 -- */
el('rect',{width:1400,height:800,fill:'url(#bg-g)'},svg);
el('rect',{width:1400,height:800,fill:'url(#grid)',opacity:0.5},svg);
/* -- 气流粒子容器 -- */
const gAir=el('g',{id:'g-air'},svg);
/* -- 左翼组 -- */
const gWL=el('g',{id:'g-wl'},svg);
// 翼膜
el('path',{id:'wm-l',d:memPath(-1),fill:'url(#wg-l)',stroke:'#2a4060','stroke-width':1},gWL);
// 翼肋
for(let i=1;i<=4;i++){
const x=-75*i;
const h=lerp(28,6,Math.abs(x)/WLEN);
el('line',{x1:x,y1:-h,x2:x,y2:h+2,stroke:'#2a4060','stroke-width':0.7,opacity:0.5},gWL);
}
// 前缘主梁
el('line',{x1:0,y1:0,x2:-WLEN,y2:-4,stroke:'#4a6888','stroke-width':2.8,'stroke-linecap':'round'},gWL);
// 波纹管
el('path',{id:'bl-l',fill:'none',stroke:'#e8842a','stroke-width':2.2,'stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#glow-xs)'},gWL);
// 弹簧
el('path',{id:'sp-l',fill:'none',stroke:'#3cd868','stroke-width':1.8,'stroke-linecap':'round'},gWL);
/* -- 右翼组 -- */
const gWR=el('g',{id:'g-wr'},svg);
el('path',{id:'wm-r',d:memPath(1),fill:'url(#wg-r)',stroke:'#2a4060','stroke-width':1},gWR);
for(let i=1;i<=4;i++){
const x=75*i;
const h=lerp(28,6,Math.abs(x)/WLEN);
el('line',{x1:x,y1:-h,x2:x,y2:h+2,stroke:'#2a4060','stroke-width':0.7,opacity:0.5},gWR);
}
el('line',{x1:0,y1:0,x2:WLEN,y2:-4,stroke:'#4a6888','stroke-width':2.8,'stroke-linecap':'round'},gWR);
// 鲍登线外壳
el('path',{id:'bdo-r',fill:'none',stroke:'#1a3048','stroke-width':4,'stroke-linecap':'round',opacity:0.6},gWR);
// 鲍登线内线
el('path',{id:'bdi-r',fill:'none',stroke:'#00ddb8','stroke-width':1.4,'stroke-linecap':'round',filter:'url(#glow-xs)'},gWR);
// 弹簧
el('path',{id:'sp-r',fill:'none',stroke:'#3cd868','stroke-width':1.8,'stroke-linecap':'round'},gWR);
/* -- 机身组 -- */
const gB=el('g',{id:'g-body'},svg);
// 机身外壳
el('rect',{x:CX-72,y:CY-108,width:144,height:216,rx:22,fill:'url(#body-g)',stroke:'#22345a','stroke-width':1.5},gB);
// 切口线
el('line',{x1:CX-72,y1:CY-60,x2:CX+72,y2:CY-60,stroke:'#2a3c5c','stroke-width':0.6,'stroke-dasharray':'4 3'},gB);
el('text',{x:CX+60,y:CY-63,fill:'#2a3c5c','font-size':'7','font-family':'Share Tech Mono'},gB).textContent='CUT';
// 高压气源容器
el('rect',{id:'vessel',x:CX-30,y:CY-96,width:60,height:42,rx:8,fill:'url(#ves-g)',opacity:0.9,filter:'url(#glow)'},gB);
el('text',{x:CX,y:CY-78,'text-anchor':'middle',fill:'#fff','font-size':'8','font-family':'Share Tech Mono','font-weight':'700'},gB).textContent='HP AIR';
// 压力表
el('circle',{cx:CX,cy:CY-62,r:8,fill:'#0c1628',stroke:'#3a5478','stroke-width':0.8},gB);
el('line',{id:'needle',x1:CX,y1:CY-62,x2:CX,y2:CY-68,stroke:'#e8842a','stroke-width':1.2,'stroke-linecap':'round'},gB);
// 气泵
const gPump=el('g',{id:'g-pump'},gB);
el('rect',{x:CX-20,y:CY-42,width:40,height:30,rx:4,fill:'#0e1a2e',stroke:'#2a4060','stroke-width':1},gPump);
el('rect',{id:'piston',x:CX-12,y:CY-38,width:24,height:10,rx:2,fill:'#5a7a9a'},gPump);
// 管路(容器→阀门)
el('path',{d:`M${CX-14},${CY-54} L${CX-50},${CY-54} L${CX-50},${CY-30}`,fill:'none',stroke:'#2a4060','stroke-width':1.8,'stroke-dasharray':'4 2'},gB);
el('path',{d:`M${CX+14},${CY-54} L${CX+50},${CY-54} L${CX+50},${CY-30}`,fill:'none',stroke:'#2a4060','stroke-width':1.8,'stroke-dasharray':'4 2'},gB);
// 阀门
el('circle',{id:'vl-l',cx:CX-50,cy:CY-22,r:9,fill:'#0e1a2e',stroke:'#2a4060','stroke-width':1.2},gB);
el('text',{x:CX-50,y:CY-19,'text-anchor':'middle',fill:'#5a7a9a','font-size':'7','font-family':'Share Tech Mono'},gB).textContent='VL';
el('circle',{id:'vl-r',cx:CX+50,cy:CY-22,r:9,fill:'#0e1a2e',stroke:'#2a4060','stroke-width':1.2},gB);
el('text',{x:CX+50,y:CY-19,'text-anchor':'middle',fill:'#5a7a9a','font-size':'7','font-family':'Share Tech Mono'},gB).textContent='VR';
// 管路(阀门→翼根)
el('path',{d:`M${CX-50},${CY-13} L${CX-50},${CY+5} L${LPX+8},${LPY-CY+CY}`,fill:'none',stroke:'#2a4060','stroke-width':1.3,'stroke-dasharray':'3 2',opacity:0.7},gB);
el('path',{d:`M${CX+50},${CY-13} L${CX+50},${CY+5} L${RPX-8},${RPY-CY+CY}`,fill:'none',stroke:'#2a4060','stroke-width':1.3,'stroke-dasharray':'3 2',opacity:0.7},gB);
// 重心标记
const cgG=el('g',{id:'cg-mark'},gB);
el('circle',{cx:CX,cy:CY+52,r:6,fill:'none',stroke:'#e8842a','stroke-width':1.5,filter:'url(#glow-xs)'},cgG);
el('line',{x1:CX-10,y1:CY+52,x2:CX+10,y2:CY+52,stroke:'#e8842a','stroke-width':0.8},cgG);
el('line',{x1:CX,y1:CY+42,x2:CX,y2:CY+62,stroke:'#e8842a','stroke-width':0.8},cgG);
el('text',{x:CX,y:CY+74,'text-anchor':'middle',fill:'#e8842a','font-size':'10','font-family':'Share Tech Mono','font-weight':'700',letterSpacing:'2'},cgG).textContent='CG';
// 质量分布点
const gMass=el('g',{id:'g-mass'},gB);
for(let i=0;i<8;i++){
const a=(i/8)*Math.PI*2;
const r=12+Math.random()*8;
el('circle',{cx:CX+Math.cos(a)*r,cy:CY+52+Math.sin(a)*r*0.5,r:2.5,fill:'#e8842a',opacity:0.5},gMass);
}
/* -- 标注层 -- */
const gLab=el('g',{id:'g-labels'},svg);
// 左翼标注: 波纹管
addAnno(360,240,'气动波纹管驱动','充气膨胀 → 直接推顶翼面下扑','0.2–0.5 MPa','#e8842a');
// 右翼标注: 鲍登线
addAnno(1040,240,'鲍登线提线牵引','活塞拉线 → 如木偶提线下扑','线径 0.3mm','#00ddb8');
// 机身标注
addAnno(CX,CY+140,'动力源集中于重心','翼面近乎零质量 → 转动惯量极低','#e8842a');
// 弹簧标注
addAnno(540,480,'弹簧复位上扑','气动释放后弹簧回弹','#3cd868');
addAnno(860,480,'弹簧复位上扑','牵引释放后弹簧回弹','#3cd868');
/* -- 惯性对比插图 -- */
const gIns=el('g',{transform:'translate(1220,700)'},svg);
el('rect',{x:-100,y:-55,width:200,height:110,rx:10,fill:'rgba(8,14,28,0.9)',stroke:'#162040','stroke-width':1},gIns);
el('text',{x:0,y:-36,'text-anchor':'middle',fill:'#5a7a9a','font-size':'11','font-family':'Teko',fontWeight:'600',letterSpacing:'2'},gIns).textContent='转动惯量 I';
// 传统
el('circle',{cx:-38,cy:10,r:26,fill:'none',stroke:'#ff3850','stroke-width':1.5,'stroke-dasharray':'5 3',opacity:0.65},gIns);
el('text',{x:-38,y:14,'text-anchor':'middle',fill:'#ff3850','font-size':'9','font-family':'Share Tech Mono',opacity:0.8},gIns).textContent='HIGH';
el('text',{x:-38,y:42,'text-anchor':'middle',fill:'#3e5878','font-size':'9','font-family':'Teko'},gIns).textContent='传统连杆';
// IFR
el('circle',{id:'i-ideal',cx:38,cy:10,r:9,fill:'none',stroke:'#3cd868','stroke-width':2,filter:'url(#glow-xs)'},gIns);
el('text',{x:38,y:14,'text-anchor':'middle',fill:'#3cd868','font-size':'9','font-family':'Share Tech Mono',opacity:0.9},gIns).textContent='LOW';
el('text',{x:38,y:42,'text-anchor':'middle',fill:'#3e5878','font-size':'9','font-family':'Teko'},gIns).textContent='IFR 理想解';
/* -- 翼尖轨迹层 -- */
const gTrail=el('g',{id:'g-trail'},svg);
el('path',{id:'trail-l',fill:'none',stroke:'rgba(232,132,42,0.15)','stroke-width':1.5},gTrail);
el('path',{id:'trail-r',fill:'none',stroke:'rgba(0,221,184,0.15)','stroke-width':1.5},gTrail);
/* ===== 辅助绘图函数 ===== */
function memPath(s){
// s: -1=左, 1=右
const L=WLEN;
return`M0,-26 C${s*-55},-24 ${s*-180},-12 ${s*-L},-4 L${s*-L},6 C${s*-180},16 ${s*-55},28 0,30 Z`;
}
function bellowsD(inflation){
// 左翼波纹管 (负x方向)
const s=-1,start=s*B_START,end=s*B_END;
const folds=B_FOLDS,span=Math.abs(end-start);
const pitch=span/folds,amp=lerp(4,22,inflation);
let d=`M${start},0`;
for(let i=0;i<folds;i++){
const x0=start+s*pitch*i;
d+=` L${x0+s*pitch*0.25},${-amp} L${x0+s*pitch*0.75},${amp} L${x0+s*pitch},0`;
}
return d;
}
function springD(side,comp){
const s=side,start=s*SP_START;
const endFull=s*SP_END;
const endComp=s*lerp(Math.abs(SP_END),Math.abs(SP_END)*0.45,comp);
const end=side===-1?-endComp:endComp;
const total=Math.abs(end-start);
const segs=SP_COILS*2,segLen=total/segs,amp=9;
let d=`M${start},0`;
for(let i=1;i<=segs;i++){
d+=` L${start+s*segLen*i},${i%2===1?-amp:amp}`;
}
d+=` L${end},0`;
return d;
}
function bowdenOuterD(){
// 右翼鲍登线外壳 (正x方向), 相对枢轴原点
return`M8,0 L${WLEN-20},0`;
}
function bowdenInnerD(tension){
// 右翼鲍登线内线, sag随tension变化
const sag=lerp(14,1,tension);
return`M12,0 Q${WLEN/2},${sag} ${WLEN-24},0`;
}
function addAnno(x,y,title,desc,color){
const g=el('g',{},gLab);
// 连接点
el('circle',{cx:x,cy:y,r:2.5,fill:color,opacity:0.7},g);
// 标题
el('text',{x:x,y:y-10,'text-anchor':'middle',fill:color,'font-size':'13','font-family':'Teko','font-weight':'700',letterSpacing:'1'},g).textContent=title;
// 描述
const lines=splitLine(desc,20);
lines.forEach((ln,i)=>{
el('text',{x:x,y:y+6+i*13,'text-anchor':'middle',fill:'#4a6888','font-size':'10','font-family':'Teko'},g).textContent=ln;
});
}
function splitLine(str,max){
const res=[];let line='';
for(const ch of str){
line+=ch;
if(line.length>=max){res.push(line);line=''}
}
if(line)res.push(line);
return res;
}
/* ===== 翼尖轨迹 ===== */
let trailLPts=[],trailRPts=[];
const TRAIL_MAX=60;
function updateTrail(lAngle,rAngle){
const lTip={x:LPX-WLEN,y:LPY-4}; // 翼尖在局部坐标
const rTip={x:RPX+WLEN,y:RPY-4};
// 旋转翼尖
const lr=rotPt(lTip.x,lTip.y,LPX,LPY,-lAngle);
const rr=rotPt(rTip.x,rTip.y,RPX,RPY,rAngle);
trailLPts.push(lr);trailRPts.push(rr);
if(trailLPts.length>TRAIL_MAX)trailLPts.shift();
if(trailRPts.length>TRAIL_MAX)trailRPts.shift();
if(trailLPts.length>2){
document.getElementById('trail-l').setAttribute('d',ptsToD(trailLPts));
document.getElementById('trail-r').setAttribute('d',ptsToD(trailRPts));
}
}
function rotPt(x,y,cx,cy,deg){
const r=deg*Math.PI/180,dx=x-cx,dy=y-cy;
return{x:cx+dx*Math.cos(r)-dy*Math.sin(r),y:cy+dx*Math.sin(r)+dy*Math.cos(r)};
}
function ptsToD(pts){
let d=`M${pts[0].x.toFixed(1)},${pts[0].y.toFixed(1)}`;
for(let i=1;i<pts.length;i++)d+=` L${pts[i].x.toFixed(1)},${pts[i].y.toFixed(1)}`;
return d;
}
/* ===== 气流粒子系统 ===== */
function initParticles(){
particles=[];
for(let s=-1;s<=1;s+=2){
for(let i=0;i<16;i++){
const p={side:s,prog:Math.random(),speed:0.28+Math.random()*0.22,el:null};
p.el=el('circle',{r:2.2,fill:s===-1?'#e8842a':'#00ddb8',opacity:0},gAir);
particles.push(p);
}
}
}
function getParticleXY(side,prog){
// 路径: 容器出口 → 阀门 → 翼根
const wx=side===-1?LPX:RPX;
const wy=side===-1?LPY:RPY;
const vx=side===-1?CX-50:CX+50;
const pts=[
{x:CX+(side===-1?-12:12),y:CY-54}, // 容器出口
{x:vx,y:CY-22}, // 阀门
{x:vx,y:CY+5}, // 转折
{x:wx,y:wy} // 翼根
];
const n=pts.length-1;
const seg=clamp(prog*n,0,n-0.001);
const si=Math.floor(seg);
const t=seg-si;
return{x:lerp(pts[si].x,pts[Math.min(si+1,n)].x,t),y:lerp(pts[si].y,pts[Math.min(si+1,n)].y,t)};
}
/* ===== 翼面状态计算 ===== */
function wingState(phase){
// 下扑0~0.35(快), 上扑0.35~1.0(慢)
if(phase<0.35){
const t=easeIO(phase/0.35);
return{angle:lerp(UP_MAX,DOWN_MAX,t),inflation:t,comp:lerp(0.08,1,t),tension:lerp(0.08,1,t),isDown:true};
}else{
const t=easeIO((phase-0.35)/0.65);
return{angle:lerp(DOWN_MAX,UP_MAX,t),inflation:1-t,comp:lerp(1,0.08,t),tension:lerp(1,0.08,t),isDown:false};
}
}
/* ===== 主动画循环 ===== */
function loop(ts){
const dt=lastTs?(ts-lastTs)/1000:0;
lastTs=ts;
if(playing)time+=dt;
// 气压影响下扑幅度
const ampScale=0.7+(pres-0.2)/0.3*0.6; // 0.7~1.3
const downMax=DOWN_MAX*ampScale;
// 左右翼交替相位
const lPhase=(time*freq)%1;
const rPhase=(time*freq+0.5)%1;
// 临时修改DOWN_MAX
const savedDown=DOWN_MAX;
// 使用ampScale后的角度
function ws(ph){
if(ph<0.35){
const t=easeIO(ph/0.35);
return{angle:lerp(UP_MAX,downMax,t),inflation:t,comp:lerp(0.08,1,t),tension:lerp(0.08,1,t),isDown:true};
}else{
const t=easeIO((ph-0.35)/0.65);
return{angle:lerp(downMax,UP_MAX,t),inflation:1-t,comp:lerp(1,0.08,t),tension:lerp(1,0.08,t),isDown:false};
}
}
const ls=ws(lPhase);
const rs=ws(rPhase);
// 翼面旋转
gWL.setAttribute('transform',`translate(${LPX},${LPY}) rotate(${-ls.angle})`);
gWR.setAttribute('transform',`translate(${RPX},${RPY}) rotate(${rs.angle})`);
// 波纹管(左翼)
document.getElementById('bl-l').setAttribute('d',bellowsD(ls.inflation));
const blOp=lerp(0.4,1,ls.inflation);
document.getElementById('bl-l').setAttribute('opacity',blOp);
// 弹簧
document.getElementById('sp-l').setAttribute('d',springD(-1,ls.comp));
document.getElementById('sp-r').setAttribute('d',springD(1,rs.comp));
// 鲍登线(右翼)
document.getElementById('bdo-r').setAttribute('d',bowdenOuterD());
document.getElementById('bdi-r').setAttribute('d',bowdenInnerD(rs.tension));
const bdOp=lerp(0.35,1,rs.tension);
document.getElementById('bdi-r').setAttribute('opacity',bdOp);
// 阀门状态
const vlL=document.getElementById('vl-l');
const vlR=document.getElementById('vl-r');
if(ls.isDown){
vlL.setAttribute('fill','#e8842a');vlL.setAttribute('stroke','#e8842a');
}else{
vlL.setAttribute('fill','#0e1a2e');vlL.setAttribute('stroke','#2a4060');
}
if(rs.isDown){
vlR.setAttribute('fill','#00ddb8');vlR.setAttribute('stroke','#00ddb8');
}else{
vlR.setAttribute('fill','#0e1a2e');vlR.setAttribute('stroke','#2a4060');
}
// 活塞
const pY=CY-38+Math.sin(time*freq*Math.PI*2)*6;
document.getElementById('piston').setAttribute('y',pY);
// 压力表指针
const nAngle=lerp(-40,40,ls.isDown?ls.inflation:rs.inflation);
const nRad=nAngle*Math.PI/180;
const nx=CX+Math.sin(nRad)*7;
const ny=CY-62-Math.cos(nRad)*7;
document.getElementById('needle').setAttribute('x2',nx);
document.getElementById('needle').setAttribute('y2',ny);
// 容器脉动
const vSc=1+Math.sin(time*freq*Math.PI*2)*0.025;
document.getElementById('vessel').setAttribute('transform',`translate(${CX},${CY-75}) scale(${vSc}) translate(${-CX},${-(CY-75)})`);
// CG标记脉动
const cgSc=1+Math.sin(time*2.5)*0.08;
document.getElementById('cg-mark').setAttribute('transform',`translate(${CX},${CY+52}) scale(${cgSc}) translate(${-CX},${-(CY+52)})`);
// 气流粒子
for(const p of particles){
if(playing)p.prog+=dt*p.speed*freq*1.1;
if(p.prog>1)p.prog-=1;
const active=p.side===-1?ls.isDown:rs.isDown;
const pos=getParticleXY(p.side,p.prog);
p.el.setAttribute('cx',pos.x);
p.el.setAttribute('cy',pos.y);
const op=active?clamp(0.85-p.prog*0.5,0.1,0.85):0.04;
p.el.setAttribute('opacity',op);
p.el.setAttribute('r',active?lerp(2.8,1.2,p.prog):0.8);
}
// 翼尖轨迹
if(playing&&Math.floor(time*freq*30)%1===0){
updateTrail(ls.angle,rs.angle);
}
// 相位指示
const badge=document.getElementById('phase-badge');
if(ls.isDown&&!rs.isDown){
badge.className='left-down';badge.textContent='左翼下扑 · 波纹管充气';
}else if(!ls.isDown&&rs.isDown){
badge.className='right-down';badge.textContent='右翼下扑 · 鲍登线牵引';
}else{
badge.className='both-up';badge.textContent='弹簧复位 · 上扑回弹';
}
// 惯性圆动画
const iR=9+Math.sin(time*3)*1.5;
document.getElementById('i-ideal').setAttribute('r',Math.max(1,iR));
requestAnimationFrame(loop);
}
/* ===== 交互控制 ===== */
const slFreq=document.getElementById('sl-freq');
const slPres=document.getElementById('sl-pres');
const vFreq=document.getElementById('v-freq');
const vPres=document.getElementById('v-pres');
const playBtn=document.getElementById('play-btn');
slFreq.addEventListener('input',()=>{freq=parseFloat(slFreq.value);vFreq.textContent=freq.toFixed(1)+' Hz'});
slPres.addEventListener('input',()=>{pres=parseFloat(slPres.value);vPres.textContent=pres.toFixed(2)+' MPa'});
playBtn.addEventListener('click',()=>{playing=!playing;playBtn.textContent=playing?'暂停':'播放'});
/* ===== 初始化 ===== */
initParticles();
// 初始路径
document.getElementById('bl-l').setAttribute('d',bellowsD(0));
document.getElementById('sp-l').setAttribute('d',springD(-1,0.08));
document.getElementById('sp-r').setAttribute('d',springD(1,0.08));
document.getElementById('bdo-r').setAttribute('d',bowdenOuterD());
document.getElementById('bdi-r').setAttribute('d',bowdenInnerD(0.08));
// 启动
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
实现说明:
双驱动模式可视化:左翼展示气动波纹管驱动(琥珀色),右翼展示鲍登线牵引驱动(青色),两种方案在同一画面中对比呈现,清晰展示"充气膨胀推顶翼面"和"提线牵引如木偶"两种核心机理的差异与共性——都将驱动质量集中在机身。
交替扑动时序:左右翼相位偏移 0.5(180°),严格遵循方案描述的"气阀左开→左翼下扑→关闭→交替"时序。阀门以对应颜色点亮(琥珀/青),配合气流粒子从压力容器经阀门流向翼根,精确呈现驱动时序。
IFR 核心思想聚焦:
- CG 标记脉动:机身重心处的十字标记持续呼吸式脉动,强调"动力源集中于重心"
- 质量分布点:CG 周围的琥珀色小圆点象征质量集中
- 惯性对比插图:右下角直接对比传统连杆(大红虚线圆 = 高惯量)与 IFR 方案(小绿实线圆 = 低惯量)
- 翼面近乎透明:翼膜使用低透明度渐变,视觉传达"翼面几乎零质量"
视觉引导层次:波纹管膨胀时琥珀色亮度增强;鲍登线紧绷时青色发光加强;弹簧压缩/伸展的锯齿形态随扑动相变化;翼尖留下半透明运动轨迹,勾勒扑动弧线。
交互控制:频率滑块(0.4–3 Hz)和气压滑块(0.20–0.50 MPa)实时影响扑动速度和幅度,气压越高下扑角度越大,直观体验气压与驱动力关系。
<!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=Oxanium:wght@300;500;700&family=Fira+Code:wght@400;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0C0B10;--bg2:#151420;
--copper:#D4831A;--copper-lt:#FFB840;--copper-dim:#8B5510;
--blue:#3B9EFF;--blue-lt:#7AC0FF;
--mint:#4EED8A;--mint-dim:#2A8A50;
--steel:#6B7B8D;--steel-lt:#9AABBD;
--text:#E8DDD0;--text-dim:#5A6474;
--card:rgba(18,17,26,.92);--border:#252333;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{
background:var(--bg);color:var(--text);
font-family:'Oxanium',sans-serif;
display:flex;align-items:center;justify-content:center;
}
#wrap{width:100vw;height:100vh;position:relative}
#wrap svg{width:100%;height:100%;display:block}
/* 顶部标题栏 */
#topbar{
position:fixed;top:0;left:0;right:0;z-index:10;
display:flex;align-items:center;justify-content:center;gap:16px;
padding:16px 24px;
background:linear-gradient(180deg,rgba(12,11,16,.96) 55%,transparent);
pointer-events:none;
}
#topbar h1{
font-size:20px;font-weight:700;letter-spacing:3px;
color:var(--copper-lt);text-transform:uppercase;
}
#topbar .bar{width:1px;height:18px;background:var(--border)}
#topbar .sub{font-size:13px;color:var(--text-dim);font-weight:500;letter-spacing:1.5px}
/* 相位指示器 */
#phase{
position:fixed;top:60px;left:50%;transform:translateX(-50%);z-index:10;
padding:5px 22px;border-radius:20px;
font-size:12px;font-weight:600;letter-spacing:2px;
transition:all .35s;pointer-events:none;
}
#phase.drive{background:rgba(212,131,26,.14);color:var(--copper-lt);border:1px solid rgba(212,131,26,.3)}
#phase.return{background:rgba(78,237,138,.1);color:var(--mint);border:1px solid rgba(78,237,138,.22)}
/* 底部控制面板 */
#panel{
position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:10;
display:flex;align-items:center;gap:24px;
padding:12px 28px;
background:var(--card);border:1px solid var(--border);border-radius:12px;
backdrop-filter:blur(14px);
}
.ctrl{display:flex;flex-direction:column;gap:3px}
.ctrl label{font-size:10px;color:var(--text-dim);letter-spacing:1.5px;font-weight:600;text-transform:uppercase}
.ctrl .row{display:flex;align-items:center;gap:8px}
.ctrl input[type=range]{
-webkit-appearance:none;width:120px;height:3px;
background:var(--border);border-radius:2px;outline:none;cursor:pointer;
}
.ctrl input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:13px;height:13px;
background:var(--copper);border-radius:50%;cursor:pointer;
box-shadow:0 0 8px rgba(212,131,26,.45);
}
.ctrl .val{
font-family:'Fira Code',monospace;font-size:12px;
color:var(--copper-lt);min-width:72px;
}
#pbtn{
padding:7px 18px;border:1px solid var(--border);border-radius:7px;
background:transparent;color:var(--text);font-family:'Oxanium',sans-serif;
font-size:13px;font-weight:600;cursor:pointer;letter-spacing:1px;
transition:all .2s;
}
#pbtn:hover{border-color:var(--copper);color:var(--copper-lt)}
/* 响应式 */
@media(max-width:760px){
#panel{gap:12px;padding:10px 14px;flex-wrap:wrap;justify-content:center}
.ctrl input[type=range]{width:80px}
#topbar h1{font-size:15px}
}
</style>
</head>
<body>
<div id="topbar">
<h1>气动柔性传动扑翼</h1>
<div class="bar"></div>
<span class="sub">IFR · 最终理想解原理动画</span>
</div>
<div id="phase" class="drive">下扑 · 气压驱动</div>
<div id="wrap">
<svg id="svg" viewBox="0 0 1400 800" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="panel">
<div class="ctrl">
<label>扑动频率</label>
<div class="row">
<input type="range" id="sf" min="0.4" max="3" step="0.1" value="1.2">
<span class="val" id="vf">1.2 Hz</span>
</div>
</div>
<div class="ctrl">
<label>工作气压</label>
<div class="row">
<input type="range" id="sp" min="0.20" max="0.50" step="0.01" value="0.35">
<span class="val" id="vp">0.35 MPa</span>
</div>
</div>
<button id="pbtn">暂停</button>
</div>
<script>
(function(){
/* ========== 常量 ========== */
const NS='http://www.w3.org/2000/svg';
const CX=700,CY=395; // 机身中心
const LPX=630,LPY=365; // 左翼枢轴
const RPX=770,RPY=365; // 右翼枢轴
const WLEN=395; // 翼展半长
const DOWN=25,UP=-18; // 扑动角度范围
const BF_START=85,BF_END=315; // 波纹管区间
const BF_N=8; // 波纹管折叠数
const SP_START=22,SP_END=75; // 弹簧区间
const SP_COILS=5;
let time=0,freq=1.2,pres=0.35,playing=true,lt=0;
let parts=[];
/* ========== 工具 ========== */
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
const ease=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const deg2r=d=>d*Math.PI/180;
function rotPt(x,y,cx,cy,d){
const r=deg2r(d),dx=x-cx,dy=y-cy;
return{x:cx+dx*Math.cos(r)-dy*Math.sin(r),y:cy+dx*Math.sin(r)+dy*Math.cos(r)};
}
function el(tag,attrs,p){
const e=document.createElementNS(NS,tag);
for(const[k,v]of Object.entries(attrs||{}))e.setAttribute(k,v);
if(p)p.appendChild(e);
return e;
}
/* ========== SVG 构建 ========== */
const svg=document.getElementById('svg');
// -- defs --
const defs=el('defs',{},svg);
// 六角网格
function hexGrid(){
const sz=28,h=sz*Math.sqrt(3);
const pat=el('pattern',{id:'hx',width:sz*3,height:h*2,patternUnits:'userSpaceOnUse'},defs);
let d='';
for(let row=-1;row<3;row++){
for(let col=-1;col<4;col++){
const x=col*sz*1.5;
const y=row*h+(col%2?h/2:0);
d+=`M${x},${y} L${x+sz/2},${y-h/2} L${x+sz},${y} L${x+sz},${y+h} L${x+sz/2},${y+h*1.5} L${x},${y+h}Z `;
}
}
el('path',{d,fill:'none',stroke:'#1a1826','stroke-width':0.4},pat);
}
hexGrid();
// 发光滤镜
function mkGlow(id,sd){
const f=el('filter',{id,x:'-60%',y:'-60%',width:'220%',height:'220%'},defs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:sd,result:'b'},f);
const m=el('feMerge',{},f);
el('feMergeNode',{in:'b'},m);el('feMergeNode',{in:'b'},m);el('feMergeNode',{in:'SourceGraphic'},m);
}
mkGlow('gw',4);mkGlow('gs',7);mkGlow('gx',1.5);
// 渐变
const bgR=el('radialGradient',{id:'bgR',cx:'50%',cy:'42%',r:'58%'},defs);
el('stop',{offset:'0%','stop-color':'#13111c'},bgR);
el('stop',{offset:'100%','stop-color':'#0C0B10'},bgR);
const bodyG=el('linearGradient',{id:'bodyG',x1:'0',y1:'0',x2:'0',y2:'1'},defs);
el('stop',{offset:'0%','stop-color':'#1e2438'},bodyG);
el('stop',{offset:'100%','stop-color':'#0e1220'},bodyG);
const vesG=el('linearGradient',{id:'vesG',x1:'0',y1:'0',x2:'1',y2:'1'},defs);
el('stop',{offset:'0%','stop-color':'#e89520'},vesG);
el('stop',{offset:'100%','stop-color':'#a05c10'},vesG);
const wmL=el('linearGradient',{id:'wmL',x1:'1',y1:'0',x2:'0',y2:'0.3'},defs);
el('stop',{offset:'0%','stop-color':'rgba(35,50,75,0.65)'},wmL);
el('stop',{offset:'100%','stop-color':'rgba(18,24,40,0.2)'},wmL);
const wmR=el('linearGradient',{id:'wmR',x1:'0',y1:'0',x2:'1',y2:'0.3'},defs);
el('stop',{offset:'0%','stop-color':'rgba(35,50,75,0.65)'},wmR);
el('stop',{offset:'100%','stop-color':'rgba(18,24,40,0.2)'},wmR);
// -- 背景 --
el('rect',{width:1400,height:800,fill:'url(#bgR)'},svg);
el('rect',{width:1400,height:800,fill:'url(#hx)',opacity:0.5},svg);
// 环境光晕
el('circle',{cx:CX,cy:CY-20,r:320,fill:'radial-gradient()',opacity:0.06},svg);
const ambG=el('radialGradient',{id:'ambG',cx:'50%',cy:'50%',r:'50%'},defs);
el('stop',{offset:'0%','stop-color':'#D4831A'},ambG);
el('stop',{offset:'100%','stop-color':'transparent'},ambG);
el('circle',{cx:CX,cy:CY-20,r:320,fill:'url(#ambG)',opacity:0.04},svg);
// -- 气流粒子层 --
const gAir=el('g',{id:'g-air'},svg);
// -- 左翼组 --
const gWL=el('g',{id:'gWL'},svg);
el('path',{id:'wmL',d:wmPath(-1),fill:'url(#wmL)',stroke:'#3a5070','stroke-width':1},gWL);
el('line',{id:'sparL',x1:0,y1:0,x2:-WLEN,y2:0,stroke:'#4e6888','stroke-width':2.2,'stroke-linecap':'round'},gWL);
el('path',{id:'bfL',fill:'none',stroke:'#D4831A','stroke-width':2,'stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#gx)'},gWL);
el('path',{id:'cbL',fill:'none',stroke:'#3B9EFF','stroke-width':1.1,'stroke-dasharray':'5 4',opacity:0.7},gWL);
el('path',{id:'spL',fill:'none',stroke:'#4EED8A','stroke-width':1.6,'stroke-linecap':'round'},gWL);
// -- 右翼组 --
const gWR=el('g',{id:'gWR'},svg);
el('path',{id:'wmR',d:wmPath(1),fill:'url(#wmR)',stroke:'#3a5070','stroke-width':1},gWR);
el('line',{id:'sparR',x1:0,y1:0,x2:WLEN,y2:0,stroke:'#4e6888','stroke-width':2.2,'stroke-linecap':'round'},gWR);
el('path',{id:'bfR',fill:'none',stroke:'#D4831A','stroke-width':2,'stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#gx)'},gWR);
el('path',{id:'cbR',fill:'none',stroke:'#3B9EFF','stroke-width':1.1,'stroke-dasharray':'5 4',opacity:0.7},gWR);
el('path',{id:'spR',fill:'none',stroke:'#4EED8A','stroke-width':1.6,'stroke-linecap':'round'},gWR);
// -- 机身 --
const gBd=el('g',{id:'gBd'},svg);
// 机身外壳
el('path',{d:`M${CX-60},${CY-115} Q${CX-72},${CY} ${CX-60},${CY+115} Q${CX},${CY+130} ${CX+60},${CY+115} Q${CX+72},${CY} ${CX+60},${CY-115} Q${CX},${CY-130} ${CX-60},${CY-115}Z`,
fill:'url(#bodyG)',stroke:'#2a3c5a','stroke-width':1.3},gBd);
// 切面线(表示剖视)
el('line',{x1:CX-55,y1:CY-110,x2:CX+55,y2:CY-110,stroke:'#3a506a','stroke-width':0.6,'stroke-dasharray':'4 3',opacity:0.5},gBd);
// 压力容器
el('rect',{id:'ves',x:CX-30,y:CY-80,width:60,height:42,rx:9,fill:'url(#vesG)',opacity:0.88,filter:'url(#gw)'},gBd);
el('text',{x:CX,y:CY-62,'text-anchor':'middle',fill:'#fff','font-size':'8','font-family':'Fira Code','font-weight':'600'},gBd).textContent='HP AIR';
// 压力表
el('circle',{cx:CX,cy:CY-88,r:8,fill:'#0e1220',stroke:'#3a506a','stroke-width':0.8},gBd);
el('line',{id:'needle',x1:CX,y1:CY-88,x2:CX+5,y2:CY-93,stroke:'#D4831A','stroke-width':1,'stroke-linecap':'round'},gBd);
// 气泵活塞
el('rect',{x:CX-16,y:CY-28,width:32,height:24,rx:3,fill:'#162030',stroke:'#3a506a','stroke-width':0.8},gBd);
el('rect',{id:'piston',x:CX-9,y:CY-23,width:18,height:8,rx:2,fill:'#5a7a9a'},gBd);
// 阀门
el('circle',{id:'vL',cx:CX-44,cy:CY-25,r:6.5,fill:'#162030',stroke:'#3a506a','stroke-width':1},gBd);
el('circle',{id:'vR',cx:CX+44,cy:CY-25,r:6.5,fill:'#162030',stroke:'#3a506a','stroke-width':1},gBd);
el('text',{x:CX-44,y:CY-22,'text-anchor':'middle',fill:'#607898','font-size':'6','font-family':'Fira Code'},gBd).textContent='V_L';
el('text',{x:CX+44,y:CY-22,'text-anchor':'middle',fill:'#607898','font-size':'6','font-family':'Fira Code'},gBd).textContent='V_R';
// 机身内气路
el('path',{d:`M${CX-8},${CY-58} L${CX-44},${CY-58} L${CX-44},${CY-32}`,fill:'none',stroke:'#3a506a','stroke-width':1.3,'stroke-dasharray':'3 2'},gBd);
el('path',{d:`M${CX+8},${CY-58} L${CX+44},${CY-58} L${CX+44},${CY-32}`,fill:'none',stroke:'#3a506a','stroke-width':1.3,'stroke-dasharray':'3 2'},gBd);
// 机身到翼根气路
el('path',{d:`M${CX-44},${CY-25} L${CX-56},${CY-10} L${LPX+5},${LPY-18}`,fill:'none',stroke:'#3a506a','stroke-width':1.2,'stroke-dasharray':'4 3',opacity:0.6},gBd);
el('path',{d:`M${CX+44},${CY-25} L${CX+56},${CY-10} L${RPX-5},${RPY-18}`,fill:'none',stroke:'#3a506a','stroke-width':1.2,'stroke-dasharray':'4 3',opacity:0.6},gBd);
// CG 重心标记
el('circle',{cx:CX,cy:CY+55,r:6,fill:'none',stroke:'#D4831A','stroke-width':1.5,filter:'url(#gx)'},gBd);
el('line',{x1:CX-10,y1:CY+55,x2:CX+10,y2:CY+55,stroke:'#D4831A','stroke-width':0.8},gBd);
el('line',{x1:CX,y1:CY+45,x2:CX,y2:CY+65,stroke:'#D4831A','stroke-width':0.8},gBd);
el('text',{x:CX,y:CY+78,'text-anchor':'middle',fill:'#D4831A','font-size':'9','font-family':'Fira Code','font-weight':'600'},gBd).textContent='CG';
// -- 标注层 --
const gLab=el('g',{id:'gLab'},svg);
addLbl(340,228,'气动波纹管','充气膨胀直接驱动翼面下扑','#D4831A');
addLbl(1060,228,'鲍登线牵引','如木偶提线 · 内线连接翼尖','#3B9EFF');
addLbl(340,560,'弹簧复位','储能释放 · 驱动翼面上扑','#4EED8A');
addLbl(CX,CY+165,'动力源集中于重心','翼面近乎零质量 → 转动惯量极低','#D4831A');
// 翼尖质量标注
const gML=el('g',{id:'gML'},svg);
el('text',{id:'mlL',x:0,y:0,'text-anchor':'middle',fill:'#4EED8A','font-size':'11','font-family':'Fira Code','font-weight':'600',opacity:0.85},gML).textContent='≈0g';
const gMR=el('g',{id:'gMR'},svg);
el('text',{id:'mrR',x:0,y:0,'text-anchor':'middle',fill:'#4EED8A','font-size':'11','font-family':'Fira Code','font-weight':'600',opacity:0.85},gMR).textContent='≈0g';
// 惯性对比
const gIn=el('g',{transform:'translate(1200,690)'},svg);
el('rect',{x:-95,y:-48,width:190,height:96,rx:9,fill:'rgba(14,13,20,0.88)',stroke:'#252333','stroke-width':1},gIn);
el('text',{x:0,y:-30,'text-anchor':'middle',fill:'#7A8290','font-size':'10','font-family':'Oxanium','font-weight':'600',letterSpacing:'1.5'},gIn).textContent='转动惯量 I';
el('circle',{cx:-36,cy:6,r:20,fill:'none',stroke:'#ff3850','stroke-width':1.3,'stroke-dasharray':'4 3',opacity:0.6},gIn);
el('text',{x:-36,y:10,'text-anchor':'middle',fill:'#ff3850','font-size':'8','font-family':'Fira Code',opacity:0.7},gIn).textContent='高';
el('text',{x:-36,y:35,'text-anchor':'middle',fill:'#5A6474','font-size':'8','font-family':'Oxanium'},gIn).textContent='传统';
el('circle',{id:'iIdeal',cx:36,cy:6,r:7,fill:'none',stroke:'#4EED8A','stroke-width':1.6,filter:'url(#gx)'},gIn);
el('text',{x:36,y:10,'text-anchor':'middle',fill:'#4EED8A','font-size':'8','font-family':'Fira Code'},gIn).textContent='低';
el('text',{x:36,y:35,'text-anchor':'middle',fill:'#5A6474','font-size':'8','font-family':'Oxanium'},gIn).textContent='IFR';
// -- 扑动轨迹弧线(虚线) --
const gArc=el('g',{id:'gArc'},svg);
// 左翼扑动弧
el('path',{d:arcPath(LPX,LPY,-1),fill:'none',stroke:'#2a3c5a','stroke-width':0.8,'stroke-dasharray':'5 5',opacity:0.35},gArc);
// 右翼扑动弧
el('path',{d:arcPath(RPX,RPY,1),fill:'none',stroke:'#2a3c5a','stroke-width':0.8,'stroke-dasharray':'5 5',opacity:0.35},gArc);
// 力的方向箭头(动态)
const gForce=el('g',{id:'gForce'},svg);
el('path',{id:'fL',fill:'none',stroke:'#D4831A','stroke-width':1.5,'stroke-linecap':'round',opacity:0},gForce);
el('path',{id:'fR',fill:'none',stroke:'#D4831A','stroke-width':1.5,'stroke-linecap':'round',opacity:0},gForce);
/* ========== 路径生成 ========== */
function wmPath(s){
const L=WLEN;
return`M0,-30 C${s*-90},-28 ${s*-240},-14 ${s*-L},-7 L${s*-L},7 C${s*-240},14 ${s*-90},28 0,30 Z`;
}
function bfPath(side,inf){
const dir=side,start=dir*BF_START,end=dir*BF_END;
const span=Math.abs(end-start),pitch=span/BF_N;
const amp=lerp(7,22,inf);
let d=`M${start},0`;
for(let i=0;i<BF_N;i++){
const x0=start+dir*pitch*i;
d+=` L${x0+dir*pitch*0.25},${-amp} L${x0+dir*pitch*0.75},${amp} L${x0+dir*pitch},0`;
}
return d;
}
function spPath(side,comp){
const dir=side,start=dir*SP_START;
const fullLen=Math.abs(SP_END-SP_START);
const len=lerp(fullLen,fullLen*0.45,comp);
const end=start+dir*len;
const amp=9,segs=SP_COILS*2;
const segL=len/segs;
let d=`M${start},0`;
for(let i=1;i<=segs;i++){
const x=start+dir*segL*i;
const y=(i%2===1?-amp:amp);
d+=` L${x},${y}`;
}
d+=` L${end},0`;
return d;
}
function cbPath(side,ten){
const dir=side;
const sag=lerp(20,1.5,ten);
const cx=dir*WLEN*0.5;
return`M${dir*14},0 Q${cx},${sag} ${dir*(WLEN-18)},0`;
}
function arcPath(px,py,side){
// 画扑动范围弧线
const r=WLEN*0.92;
const a1=deg2r(UP-3),a2=deg2r(DOWN+3);
const x1=px+side*r*Math.cos(a1),y1=py+r*Math.sin(a1);
const x2=px+side*r*Math.cos(a2),y2=py+r*Math.sin(a2);
return`M${x1},${y1} A${r},${r} 0 0,1 ${x2},${y2}`;
}
function addLbl(x,y,title,desc,color){
const g=el('g',{},gLab);
el('circle',{cx:x,cy:y-5,r:2.5,fill:color,opacity:0.6},g);
el('line',{x1:x,y1:y-3,x2:x,y2:y+8,stroke:color,'stroke-width':0.6,opacity:0.4},g);
el('text',{x:x,y,'text-anchor':'middle',fill:color,'font-size':'11.5','font-family':'Oxanium','font-weight':'700',letterSpacing:'0.5'},g).textContent=title;
// 分行描述
const chars=desc.split('');
let line='',ly=y+14;
const maxC=18;
for(const ch of chars){
line+=ch;
if(line.length>=maxC){
el('text',{x:x,y:ly,'text-anchor':'middle',fill:'#5A6474','font-size':'8.5','font-family':'Oxanium'},g).textContent=line;
ly+=11;line='';
}
}
if(line)el('text',{x:x,y:ly,'text-anchor':'middle',fill:'#5A6474','font-size':'8.5','font-family':'Oxanium'},g).textContent=line;
}
/* ========== 气流粒子 ========== */
function initParts(){
parts=[];
for(let s=-1;s<=1;s+=2){
for(let i=0;i<16;i++){
const p={side:s,prog:Math.random(),spd:0.28+Math.random()*0.22,el:null};
p.el=el('circle',{r:2.2,fill:s===-1?'#3B9EFF':'#3B9EFF',opacity:0},gAir);
parts.push(p);
}
}
}
function partPos(side,prog,ang){
const px=side===-1?LPX:RPX,py=side===-1?LPY:RPY;
// 路径:压力容器→阀门→翼根→波纹管中心
const wps=[
{x:CX,y:CY-60},
{x:side===-1?CX-44:CX+44,y:CY-25},
{x:px,y:py},
{x:px+side*(BF_START+BF_END)/2,y:py}
];
// 最后两个点随翼旋转
for(let i=2;i<wps.length;i++){
wps[i]=rotPt(wps[i].x,wps[i].y,px,py,side===-1?-ang:ang);
}
const n=wps.length-1;
const seg=clamp(prog*n,0,n-0.001);
const si=Math.floor(seg),t=seg-si;
const a=wps[si],b=wps[Math.min(si+1,n)];
return{x:lerp(a.x,b.x,t),y:lerp(a.y,b.y,t)};
}
/* ========== 压力波纹 ========== */
let waves=[];
function initWaves(){
for(let i=0;i<3;i++){
const w={prog:0,el:null};
w.el=el('circle',{cx:CX,cy:CY-60,r:10,fill:'none',stroke:'#D4831A','stroke-width':0.8,opacity:0},gAir);
waves.push(w);
}
}
/* ========== 动画主循环 ========== */
function loop(ts){
const dt=lt?(ts-lt)/1000:0;
lt=ts;
if(playing)time+=dt;
const cyc=(time*freq)%1;
let ang,inf,comp,ten;
// 非对称:下扑0~0.35(快),上扑0.35~1(慢)
if(cyc<0.35){
const t=ease(cyc/0.35);
ang=lerp(UP,DOWN,t);
inf=t;comp=lerp(0.15,1,t);ten=lerp(0.15,1,t);
}else{
const t=ease((cyc-0.35)/0.65);
ang=lerp(DOWN,UP,t);
inf=1-t;comp=lerp(1,0.15,t);ten=lerp(1,0.15,t);
}
// 翼面旋转
gWL.setAttribute('transform',`translate(${LPX},${LPY}) rotate(${-ang})`);
gWR.setAttribute('transform',`translate(${RPX},${RPY}) rotate(${ang})`);
// 波纹管
document.getElementById('bfL').setAttribute('d',bfPath(-1,inf));
document.getElementById('bfR').setAttribute('d',bfPath(1,inf));
// 波纹管颜色亮度随充气变化
const bfOp=lerp(0.6,1,inf);
document.getElementById('bfL').setAttribute('opacity',bfOp);
document.getElementById('bfR').setAttribute('opacity',bfOp);
// 弹簧
document.getElementById('spL').setAttribute('d',spPath(-1,comp));
document.getElementById('spR').setAttribute('d',spPath(1,comp));
// 弹簧亮度:压缩储能时更亮
const spOp=lerp(0.4,0.95,comp);
document.getElementById('spL').setAttribute('opacity',spOp);
document.getElementById('spR').setAttribute('opacity',spOp);
// 鲍登线
document.getElementById('cbL').setAttribute('d',cbPath(-1,ten));
document.getElementById('cbR').setAttribute('d',cbPath(1,ten));
const cbOp=lerp(0.35,0.9,ten);
document.getElementById('cbL').setAttribute('opacity',cbOp);
document.getElementById('cbR').setAttribute('opacity',cbOp);
// 阀门
const vOn=cyc<0.35;
const vL=document.getElementById('vL'),vR=document.getElementById('vR');
vL.setAttribute('fill',vOn?'#3B9EFF':'#162030');
vL.setAttribute('stroke',vOn?'#3B9EFF':'#3a506a');
vR.setAttribute('fill',vOn?'#3B9EFF':'#162030');
vR.setAttribute('stroke',vOn?'#3B9EFF':'#3a506a');
// 活塞
const pY=CY-23+Math.sin(cyc*Math.PI*2)*4;
document.getElementById('piston').setAttribute('y',pY);
// 压力表指针
const nAngle=-60+pres/0.5*120; // -60° ~ 60°
const nx=CX+5*Math.cos(deg2r(nAngle-90));
const ny=CY-88+5*Math.sin(deg2r(nAngle-90));
document.getElementById('needle').setAttribute('x2',nx);
document.getElementById('needle').setAttribute('y2',ny);
// 压力容器脉动
const vSc=1+Math.sin(cyc*Math.PI*2)*0.025;
const ves=document.getElementById('ves');
ves.setAttribute('transform',`translate(${CX},${CY-59}) scale(${vSc}) translate(${-CX},${-(CY-59)})`);
// 气流粒子
for(const p of parts){
if(playing)p.prog+=dt*p.spd*freq*1.3;
if(p.prog>1)p.prog-=1;
const pos=partPos(p.side,p.prog,ang);
p.el.setAttribute('cx',pos.x);
p.el.setAttribute('cy',pos.y);
const op=vOn?clamp(1-p.prog*0.55,0.12,0.85):0.04;
p.el.setAttribute('opacity',op);
p.el.setAttribute('r',vOn?lerp(2.8,1.4,p.prog):0.8);
}
// 压力波纹
for(let i=0;i<waves.length;i++){
const w=waves[i];
if(playing)w.prog+=dt*0.6*freq;
if(w.prog>1)w.prog-=1;
const r=10+w.prog*45;
const op=vOn?clamp(0.35-w.prog*0.35,0,0.35):0;
w.el.setAttribute('r',r);
w.el.setAttribute('opacity',op);
}
// 翼尖质量标注位置
const lTip=rotPt(LPX-WLEN,LPY,LPX,LPY,-ang);
const rTip=rotPt(RPX+WLEN,RPY,RPX,RPY,ang);
gML.setAttribute('transform',`translate(${lTip.x},${lTip.y-18})`);
gMR.setAttribute('transform',`translate(${rTip.x},${rTip.y-18})`);
// 力方向箭头
if(vOn){
const lMid=rotPt(LPX-WLEN*0.5,LPY,LPX,LPY,-ang);
const rMid=rotPt(RPX+WLEN*0.5,RPY,RPX,RPY,ang);
// 下扑力箭头(向下)
const fLen=18*inf;
document.getElementById('fL').setAttribute('d',`M${lMid.x},${lMid.y} L${lMid.x},${lMid.y+fLen}`);
document.getElementById('fR').setAttribute('d',`M${rMid.x},${rMid.y} L${rMid.x},${rMid.y+fLen}`);
document.getElementById('fL').setAttribute('opacity',0.7*inf);
document.getElementById('fR').setAttribute('opacity',0.7*inf);
}else{
document.getElementById('fL').setAttribute('opacity',0);
document.getElementById('fR').setAttribute('opacity',0);
}
// 相位指示
const badge=document.getElementById('phase');
if(cyc<0.35){
badge.className='drive';
badge.textContent='下扑 · 气压驱动';
}else{
badge.className='return';
badge.textContent='上扑 · 弹簧复位';
}
// 惯性圆环呼吸
const iR=7+Math.sin(time*3.5)*1.2;
document.getElementById('iIdeal').setAttribute('r',iR);
requestAnimationFrame(loop);
}
/* ========== 控制交互 ========== */
const sf=document.getElementById('sf'),sp=document.getElementById('sp');
const vf=document.getElementById('vf'),vp=document.getElementById('vp');
const pb=document.getElementById('pbtn');
sf.addEventListener('input',()=>{freq=parseFloat(sf.value);vf.textContent=freq.toFixed(1)+' Hz'});
sp.addEventListener('input',()=>{pres=parseFloat(sp.value);vp.textContent=pres.toFixed(2)+' MPa'});
pb.addEventListener('click',()=>{playing=!playing;pb.textContent=playing?'暂停':'播放'});
/* ========== 初始化 ========== */
// 设置初始路径
document.getElementById('bfL').setAttribute('d',bfPath(-1,0));
document.getElementById('bfR').setAttribute('d',bfPath(1,0));
document.getElementById('spL').setAttribute('d',spPath(-1,0.15));
document.getElementById('spR').setAttribute('d',spPath(1,0.15));
document.getElementById('cbL').setAttribute('d',cbPath(-1,0.15));
document.getElementById('cbR').setAttribute('d',cbPath(1,0.15));
initParts();
initWaves();
// DOMContentLoaded 后自动启动动画
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
实现要点说明:
IFR 核心可视化:动画直接呈现最终理想解运行状态——所有重部件(高压气源、阀门阵列、活塞泵)集中于机身重心(CG 标记),翼面仅有波纹管/鲍登线/弹簧等极轻组件。翼尖
≈0g标注随翼运动,直观传达"翼面零质量→转动惯量极低"的矛盾消解。三色编码子系统:
- 铜橙色 — 气动波纹管充放气膨胀,是破除"远端质量大"矛盾的核心驱动
- 冰蓝色 — 鲍登线牵引,紧绷时高亮、松弛时暗淡,可视化"木偶提线"机理
- 薄荷绿 — 弹簧复位,压缩储能时增亮,释放时暗淡
动态视觉引导:
- 气流粒子从压力容器经阀门流向波纹管,阀门开启时粒子活跃、关闭时几乎消失
- 压力波纹从容器向外扩散,可视化气压传递
- 下扑力箭头在驱动相出现,方向随翼面旋转
- 扑动弧线虚线标示运动范围
交互控制:频率滑块(0.4–3.0 Hz)和气压滑块(0.20–0.50 MPa)实时调节,压力表指针随气压旋转;播放/暂停按钮控制动画运行。
非对称扑动节奏:下扑相占周期 35%(气动驱动,快速有力),上扑相占 65%(弹簧复位,较缓慢),符合物理实际并形成鲜明视觉节奏。
等待动画代码生成...
