独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMA人工肌肉伞 · IFR原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;600;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root{--bg:#060a14;--fg:#c8d6e5;--muted:#4a5a70;--accent-hot:#ff5722;--accent-cold:#00c9a7;--card:#0c1220;--border:#1a2640}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'IBM Plex Mono',monospace;background:var(--bg);color:var(--fg);min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 30% 20%,rgba(0,201,167,.04) 0%,transparent 60%),radial-gradient(ellipse at 70% 80%,rgba(255,87,34,.04) 0%,transparent 60%);pointer-events:none}
.tf{font-family:'Chakra Petch',sans-serif}
.svg-wrap{width:100%;max-width:720px;flex:1;display:flex;align-items:center;justify-content:center;padding:0 12px}
.svg-wrap svg{width:100%;height:auto;max-height:78vh}
.panel{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:14px 22px;display:flex;align-items:center;gap:16px;flex-wrap:wrap;justify-content:center;max-width:680px;width:calc(100% - 24px);margin:8px 12px 18px}
.btn{padding:9px 22px;border:1px solid var(--border);border-radius:9px;background:transparent;color:var(--fg);font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:14px;cursor:pointer;transition:all .3s;user-select:none}
.btn:focus-visible{outline:2px solid var(--accent-hot);outline-offset:2px}
.btn-open:hover,.btn-open.act{background:rgba(255,87,34,.12);border-color:var(--accent-hot);color:var(--accent-hot);box-shadow:0 0 18px rgba(255,87,34,.18)}
.btn-close:hover,.btn-close.act{background:rgba(0,201,167,.12);border-color:var(--accent-cold);color:var(--accent-cold);box-shadow:0 0 18px rgba(0,201,167,.18)}
.tbar{width:110px;height:5px;background:var(--border);border-radius:3px;overflow:hidden}
.tfill{height:100%;border-radius:3px;transition:width .25s,background .25s}
.stxt{font-size:12px;color:var(--muted);min-width:130px;text-align:center}
@media(max-width:600px){.panel{gap:10px;padding:10px 14px}.btn{padding:7px 14px;font-size:13px}}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
<body>
<header class="w-full text-center pt-5 pb-1 relative z-10">
<h1 class="tf text-xl md:text-2xl font-bold tracking-widest" style="color:var(--fg)">SMA 人工肌肉伞</h1>
<p class="text-xs mt-1" style="color:var(--muted)">形状记忆合金驱动 · 弹性伞面回缩 · 零铰链理想解</p>
</header>
<main class="svg-wrap relative z-10">
<svg id="usvg" viewBox="0 0 1000 1080" xmlns="http://www.w3.org/2000/svg"></svg>
</main>
<footer class="relative z-10">
<div class="panel">
<button class="btn btn-open" id="bOpen" aria-label="开伞"><i class="fas fa-bolt mr-1"></i>开伞</button>
<button class="btn btn-close" id="bClose" aria-label="收伞"><i class="fas fa-wind mr-1"></i>收伞</button>
<div class="flex-1 flex flex-col items-center gap-1 min-w-[140px]">
<div class="flex items-center gap-2 w-full max-w-[180px]">
<span class="text-[10px]" style="color:var(--accent-cold)">25°C</span>
<div class="tbar flex-1"><div class="tfill" id="tfill"></div></div>
<span class="text-[10px]" style="color:var(--accent-hot)">65°C</span>
</div>
<div class="stxt" id="stxt">收纳状态</div>
</div>
<button class="btn" id="bAuto" style="font-size:12px" aria-label="自动播放"><i class="fas fa-play mr-1" id="autoIcon"></i>自动</button>
</div>
</footer>
<script>
(function(){
/* ====== 常量与配置 ====== */
const NS='http://www.w3.org/2000/svg';
const CX=500,STOP=95,SBOT=850,HBOT=940;
const SL_CLO=740,SL_OPE=290;
const DOME_P0={x:70,y:370},DOME_P1={x:CX,y:20},DOME_P2={x:930,y:370};
const RIB_T=[0.11,0.26,0.42,0.58,0.74,0.89];
const WAVE_AMP=14,CLOSED_R=10;
/* ====== 工具函数 ====== */
function lerp(a,b,t){return a+(b-a)*t}
function lerpPt(a,b,t){return{x:lerp(a.x,b.x,t),y:lerp(a.y,b.y,t)}}
function qBez(t,p0,p1,p2){const m=1-t;return{x:m*m*p0.x+2*m*t*p1.x+t*t*p2.x,y:m*m*p0.y+2*m*t*p1.y+t*t*p2.y}}
function easeOut(t){return 1-Math.pow(1-t,3)}
function easeIO(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2}
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
function el(tag,attrs){const e=document.createElementNS(NS,tag);for(const k in attrs)e.setAttribute(k,attrs[k]);return e}
/* ====== 状态 ====== */
let progress=0,autoPlay=true,phaseIdx=0,phaseT=0;
const PHASES=[
{name:'closed_pause',dur:1400},
{name:'opening',dur:3200},
{name:'open_pause',dur:2600},
{name:'closing',dur:3200},
];
let particles=[];
let lastTs=0;
/* ====== SVG 元素引用 ====== */
const S={};
const svg=document.getElementById('usvg');
/* ====== 计算伞骨端点 ====== */
function openPts(){return RIB_T.map(t=>qBez(t,DOME_P0,DOME_P1,DOME_P2))}
function closedPts(){return RIB_T.map((_,i)=>{const a=i/RIB_T.length*Math.PI*2;return{x:CX+Math.cos(a)*CLOSED_R,y:STOP+35+Math.sin(a)*CLOSED_R*.5}})}
function ribPts(p){const o=openPts(),c=closedPts();return o.map((op,i)=>lerpPt(c[i],op,easeOut(p)))}
/* ====== 伞面路径 ====== */
function canopyPath(p){
const hw=lerp(10,430,easeOut(p));
const rise=lerp(20,290,easeOut(p));
const by=lerp(STOP+55,390,easeOut(p));
const ay=by-rise;
const lx=CX-hw,rx=CX+hw;
/* 用多个贝塞尔段组成平滑穹顶 */
const d=`M ${lx},${by} C ${lx},${ay+rise*.25} ${CX-hw*.35},${ay} ${CX},${ay} C ${CX+hw*.35},${ay} ${rx},${ay+rise*.25} ${rx},${by} Z`;
return d;
}
/* ====== SMA丝路径(含波动) ====== */
function ribPath(sx,sy,ex,ey,p,idx){
const dx=ex-sx,dy=ey-sy;
const len=Math.max(1,Math.sqrt(dx*dx+dy*dy));
const nx=-dy/len,ny=dx/len;
const amp=WAVE_AMP*(1-p*p);
const N=20;
let d='';
for(let i=0;i<=N;i++){
const t=i/N;
const bx=sx+dx*t,by=sy+dy*t;
const wave=Math.sin(t*Math.PI*3+idx*1.1)*amp*Math.sin(t*Math.PI);
const px=(bx+nx*wave).toFixed(1),py=(by+ny*wave).toFixed(1);
d+=(i===0?'M':'L')+px+','+py;
}
return d;
}
/* ====== 颜色插值 ====== */
function smaColor(p){
const r=Math.round(lerp(0,255,p));
const g=Math.round(lerp(201,87,p));
const b=Math.round(lerp(167,34,p));
return`rgb(${r},${g},${b})`
}
/* ====== 构建 SVG ====== */
function build(){
svg.innerHTML='';
/* -- defs -- */
const defs=el('defs');
/* 热发光滤镜 */
const fHot=el('filter',{id:'gHot',x:'-60%',y:'-60%',width:'220%',height:'220%'});
fHot.appendChild(el('feGaussianBlur',{stdDeviation:'7',result:'b'}));
const fm1=el('feMerge');fm1.appendChild(el('feMergeNode',{in:'b'}));fm1.appendChild(el('feMergeNode',{in:'SourceGraphic'}));
fHot.appendChild(fm1);defs.appendChild(fHot);
/* 冷微光滤镜 */
const fCold=el('filter',{id:'gCold',x:'-40%',y:'-40%',width:'180%',height:'180%'});
fCold.appendChild(el('feGaussianBlur',{stdDeviation:'3',result:'b'}));
const fm2=el('feMerge');fm2.appendChild(el('feMergeNode',{in:'b'}));fm2.appendChild(el('feMergeNode',{in:'SourceGraphic'}));
fCold.appendChild(fm2);defs.appendChild(fCold);
/* 伞面渐变 */
const cg=el('linearGradient',{id:'cGrad',x1:'0',y1:'0',x2:'0',y2:'1'});
cg.appendChild(el('stop',{offset:'0%','stop-color':'#1e4a6e','stop-opacity':'0.55'}));
cg.appendChild(el('stop',{offset:'70%','stop-color':'#0e2a48','stop-opacity':'0.35'}));
cg.appendChild(el('stop',{offset:'100%','stop-color':'#0a1e38','stop-opacity':'0.2'}));
defs.appendChild(cg);
/* 伞面边缘高光渐变 */
const eg=el('linearGradient',{id:'eGrad',x1:'0',y1:'0',x2:'0',y2:'1'});
eg.appendChild(el('stop',{offset:'0%','stop-color':'#4a90c2','stop-opacity':'0.6'}));
eg.appendChild(el('stop',{offset:'100%','stop-color':'#1a4a72','stop-opacity':'0.15'}));
defs.appendChild(eg);
/* 伞轴金属渐变 */
const sg=el('linearGradient',{id:'sGrad',x1:'0',y1:'0',x2:'1',y2:'0'});
sg.appendChild(el('stop',{offset:'0%','stop-color':'#3a5068'}));
sg.appendChild(el('stop',{offset:'40%','stop-color':'#6a8aa8'}));
sg.appendChild(el('stop',{offset:'100%','stop-color':'#3a5068'}));
defs.appendChild(sg);
svg.appendChild(defs);
/* -- 背景网格 -- */
const gg=el('g',{opacity:'0.06'});
for(let x=0;x<=1000;x+=50)gg.appendChild(el('line',{x1:x,y1:0,x2:x,y2:1080,stroke:'#4a7a9a','stroke-width':'.5'}));
for(let y=0;y<=1080;y+=50)gg.appendChild(el('line',{x1:0,y1:y,x2:1000,y2:y,stroke:'#4a7a9a','stroke-width':'.5'}));
svg.appendChild(gg);
/* -- 伞面填充 -- */
S.canopy=el('path',{fill:'url(#cGrad)',stroke:'url(#eGrad)','stroke-width':'2'});
svg.appendChild(S.canopy);
/* -- 导管(半透明管套)-- */
S.conduits=[];
for(let i=0;i<6;i++){
const c=el('path',{fill:'none',stroke:'rgba(100,160,200,0.12)','stroke-width':'7','stroke-linecap':'round'});
S.conduits.push(c);svg.appendChild(c);
}
/* -- 伞轴 -- */
S.shaft=el('line',{x1:CX,y1:STOP,x2:CX,y2:SBOT,stroke:'url(#sGrad)','stroke-width':'7','stroke-linecap':'round'});
svg.appendChild(S.shaft);
/* -- SMA丝 -- */
S.ribs=[];
for(let i=0;i<6;i++){
const r=el('path',{fill:'none',stroke:'#00c9a7','stroke-width':'2.8','stroke-linecap':'round'});
S.ribs.push(r);svg.appendChild(r);
}
/* -- 滑块 -- */
S.slider=el('rect',{x:CX-20,y:SL_CLO-12,width:40,height:24,rx:6,fill:'#1e3050',stroke:'#4a7a9a','stroke-width':'1.5'});
svg.appendChild(S.slider);
/* 滑块指示线 */
S.slLine=el('line',{x1:CX-10,y1:SL_CLO,x2:CX+10,y2:SL_CLO,stroke:'#6a9aba','stroke-width':'1.5'});
svg.appendChild(S.slLine);
/* -- 手柄 -- */
S.handle=el('rect',{x:CX-26,y:SBOT,width:52,height:90,rx:12,fill:'#0e1a2e',stroke:'#2a4a6a','stroke-width':'1.5'});
svg.appendChild(S.handle);
/* 电路板小图标 */
S.circBrd=el('rect',{x:CX-12,y:SBOT+12,width:24,height:16,rx:3,fill:'none',stroke:'#3a6a8a','stroke-width':'1'});
svg.appendChild(S.circBrd);
/* LED */
S.led=el('circle',{cx:CX,cy:SBOT+20,r:3.5,fill:'#222'});
svg.appendChild(S.led);
/* 电池 */
S.batBody=el('rect',{x:CX-9,y:SBOT+40,width:18,height:28,rx:3,fill:'none',stroke:'#3a6a8a','stroke-width':'1'});
svg.appendChild(S.batBody);
S.batCap=el('rect',{x:CX-4,y:SBOT+37,width:8,height:4,rx:1.5,fill:'#3a6a8a'});
svg.appendChild(S.batCap);
S.batFill=el('rect',{x:CX-6,y:SBOT+52,width:12,height:13,rx:2,fill:'#00c9a7'});
svg.appendChild(S.batFill);
/* -- 按钮标记(手柄上的开/收按钮)-- */
S.btnMark1=el('circle',{cx:CX-10,cy:SBOT+75,r:4,fill:'none',stroke:'#3a6a8a','stroke-width':'1'});
svg.appendChild(S.btnMark1);
S.btnMark2=el('circle',{cx:CX+10,cy:SBOT+75,r:4,fill:'none',stroke:'#3a6a8a','stroke-width':'1'});
svg.appendChild(S.btnMark2);
/* -- 粒子层 -- */
S.pGroup=el('g');
svg.appendChild(S.pGroup);
/* -- 标注 -- */
S.labels=el('g',{'font-family':"'IBM Plex Mono',monospace",'font-size':'13',fill:'#7a9ab8'});
svg.appendChild(S.labels);
/* -- 相态微观示意(小面板)-- */
const px=70,py=700,pw=160,ph=100;
S.microPanel=el('g');
S.microPanel.appendChild(el('rect',{x:px,y:py,width:pw,height:ph,rx:8,fill:'rgba(10,18,32,0.85)',stroke:'#1e3050','stroke-width':'1'}));
S.microPanel.appendChild(el('text',{x:px+pw/2,y:py+16,'text-anchor':'middle','font-size':'10',fill:'#5a8aaa','font-family':"'IBM Plex Mono',monospace"})).textContent='SMA 微观相态';
S.martLine=el('path',{fill:'none',stroke:'#00c9a7','stroke-width':'2','stroke-linecap':'round'});
S.austLine=el('path',{fill:'none',stroke:'#ff5722','stroke-width':'2','stroke-linecap':'round',opacity:'0'});
S.microPanel.appendChild(S.martLine);
S.microPanel.appendChild(S.austLine);
S.martLabel=el('text',{x:px+pw/2,y:py+ph-8,'text-anchor':'middle','font-size':'9',fill:'#5a8aaa','font-family':"'IBM Plex Mono',monospace"});
S.martLabel.textContent='马氏体(松弛)';
S.microPanel.appendChild(S.martLabel);
S.austLabel=el('text',{x:px+pw/2,y:py+ph-8,'text-anchor':'middle','font-size':'9',fill:'#ff8a65','font-family':"'IBM Plex Mono',monospace",opacity:'0'});
S.austLabel.textContent='奥氏体(收缩)';
S.microPanel.appendChild(S.austLabel);
svg.appendChild(S.microPanel);
updateMicro(0);
}
/* ====== 更新微观相态面板 ====== */
function updateMicro(p){
const px=70,py=720,pw=160;
/* 马氏体:锯齿形 */
let md='M '+(px+15)+','+(py+45);
for(let i=0;i<8;i++){
const x=px+15+(i+1)*(pw-30)/8;
const y=py+45+((i%2===0)?-12:12)*(1-p);
md+=' L '+x.toFixed(1)+','+y.toFixed(1);
}
S.martLine.setAttribute('d',md);
S.martLine.setAttribute('opacity',(1-p).toFixed(2));
/* 奥氏体:直线 */
const ad='M '+(px+15)+','+(py+45)+' L '+(px+pw-15)+','+(py+45);
S.austLine.setAttribute('d',ad);
S.austLine.setAttribute('opacity',p.toFixed(2));
S.martLabel.setAttribute('opacity',(1-p).toFixed(2));
S.austLabel.setAttribute('opacity',p.toFixed(2));
}
/* ====== 更新渲染 ====== */
function render(){
const p=progress;
const sly=lerp(SL_CLO,SL_OPE,easeOut(p));
const pts=ribPts(p);
/* 伞面 */
S.canopy.setAttribute('d',canopyPath(p));
/* 伞骨与导管 */
const col=smaColor(p);
const glow=p>.25;
for(let i=0;i<6;i++){
const pt=pts[i];
const d=ribPath(CX,sly,pt.x,pt.y,p,i);
S.ribs[i].setAttribute('d',d);
S.ribs[i].setAttribute('stroke',col);
S.ribs[i].setAttribute('filter',glow?'url(#gHot)':'url(#gCold)');
S.ribs[i].setAttribute('stroke-width',lerp(2.2,3.2,p).toFixed(1));
/* 导管(直线) */
S.conduits[i].setAttribute('d',`M ${CX},${sly} L ${pt.x},${pt.y}`);
}
/* 滑块 */
S.slider.setAttribute('y',sly-12);
S.slLine.setAttribute('y1',sly);
S.slLine.setAttribute('y2',sly);
/* LED */
const ledCol=p>.1?(`rgba(255,${Math.round(lerp(200,60,p))},${Math.round(lerp(100,20,p))},${lerp(.4,1,p).toFixed(2)})`):'#222';
S.led.setAttribute('fill',ledCol);
if(p>.2)S.led.setAttribute('filter','url(#gHot)');
else S.led.removeAttribute('filter');
/* 电池指示 */
const batH=lerp(3,13,Math.max(0,1-p*.3));
S.batFill.setAttribute('y',SBOT+52+13-batH);
S.batFill.setAttribute('height',batH.toFixed(1));
S.batFill.setAttribute('fill',p>.5?'#ff8a65':'#00c9a7');
/* 微观面板 */
updateMicro(p);
/* 标注 */
updateLabels(p,sly,pts);
/* 温度条与状态 */
const temp=Math.round(lerp(25,65,p));
const tfill=document.getElementById('tfill');
tfill.style.width=(p*100).toFixed(0)+'%';
tfill.style.background=p>.5?'var(--accent-hot)':'var(--accent-cold)';
const stxt=document.getElementById('stxt');
if(p<.05)stxt.textContent='收纳状态 · 25°C';
else if(p<.95)stxt.textContent='SMA相变收缩中 · '+temp+'°C';
else stxt.textContent='形态锁止 · 65°C';
if(phaseIdx===3&&p>.05&&p<.95)stxt.textContent='弹性回缩中 · '+temp+'°C';
}
/* ====== 标注更新 ====== */
function updateLabels(p,sly,pts){
S.labels.innerHTML='';
function addLabel(x,y,text,anchor,col,fs){
const t=el('text',{x,y,'text-anchor':anchor||'start','font-size':fs||12,fill:col||'#5a8aaa','font-family':"'IBM Plex Mono',monospace"});
t.textContent=text;S.labels.appendChild(t);
}
function addLine(x1,y1,x2,y2,col){
S.labels.appendChild(el('line',{x1,y1,x2,y2,stroke:col||'#2a4a6a','stroke-width':'1','stroke-dasharray':'4,3'}));
}
/* 伞面标注 */
if(p>.3){
const op=.3+Math.min(p-.3,.7)*1.1;
addLabel(CX,lerp(460,430,easeOut(p)),'弹性伞面','middle',`rgba(74,144,194,${op.toFixed(2)})`,13);
}
/* SMA丝标注 */
if(p>.15){
const op=Math.min((p-.15)*2,1);
const lx=pts[0].x-20,ly=lerp(sly,pts[0].y,.4);
addLabel(lx-75,ly,'SMA记忆合金丝','end',`rgba(${Math.round(lerp(0,255,p))},${Math.round(lerp(201,87,p))},${Math.round(lerp(167,34,p))},${op.toFixed(2)})`,12);
addLine(lx-70,ly,lx,ly,`rgba(90,140,180,${(op*.5).toFixed(2)})`);
}
/* 滑块标注 */
if(p>.1){
const op=Math.min(p*2,1);
addLabel(CX+50,sly+4,'中心滑块','start',`rgba(106,154,186,${op.toFixed(2)})`,11);
addLine(CX+22,sly,CX+48,sly,`rgba(90,140,180,${(op*.4).toFixed(2)})`);
}
/* 手柄标注 */
addLabel(CX+50,SBOT+25,'微型电池 & 控制电路','start','rgba(90,140,180,0.6)',10);
addLine(CX+28,SBOT+22,CX+48,SBOT+22,'rgba(90,140,180,0.3)');
/* 温度/状态标注 */
if(p>.6){
const temp=Math.round(lerp(25,65,p));
addLabel(CX+55,SBOT+65,temp+'°C · 丝径0.6mm','start',`rgba(255,138,101,${((p-.6)*2.5).toFixed(2)})`,11);
}
/* 动作提示 */
if(phaseIdx===1&&p>.1&&p<.9){
addLabel(CX,520,'通电 → SMA相变收缩 → 伞骨展开','middle','rgba(255,138,101,0.7)',14);
}else if(phaseIdx===3&&p>.1&&p<.9){
addLabel(CX,520,'断电 → SMA降温软化 → 弹性回缩','middle','rgba(0,201,167,0.7)',14);
}
}
/* ====== 粒子系统 ====== */
function spawnParticles(){
if(progress<.2||phaseIdx!==1)return;
const pts=ribPts(progress);
/* 沿伞骨随机位置生成 */
for(let i=0;i<2;i++){
const ri=Math.floor(Math.random()*6);
const pt=pts[ri];
const sly=lerp(SL_CLO,SL_OPE,easeOut(progress));
const t=Math.random()*.6+.2;
const px=lerp(CX,pt.x,t)+(Math.random()-.5)*8;
const py=lerp(sly,pt.y,t)+(Math.random()-.5)*8;
particles.push({x:px,y:py,vx:(Math.random()-.5)*.6,vy:-Math.random()*1.5-.5,life:1,decay:.015+Math.random()*.01,r:Math.random()*2+1.5});
}
}
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.x+=p.vx;p.y+=p.vy;p.life-=p.decay;
if(p.life<=0){particles.splice(i,1);continue;}
}
/* 渲染粒子 */
S.pGroup.innerHTML='';
for(const p of particles){
const c=el('circle',{cx:p.x.toFixed(1),cy:p.y.toFixed(1),r:p.r.toFixed(1),
fill:`rgba(255,${Math.round(100+p.life*100)},${Math.round(30+p.life*50)},${(p.life*.7).toFixed(2)})`});
if(p.life>.5)c.setAttribute('filter','url(#gHot)');
S.pGroup.appendChild(c);
}
}
/* ====== 自动播放状态机 ====== */
function tickPhase(dt){
if(!autoPlay)return;
phaseT+=dt;
const ph=PHASES[phaseIdx];
if(phaseT>=ph.dur){
phaseT=0;
phaseIdx=(phaseIdx+1)%PHASES.length;
}
const ph2=PHASES[phaseIdx];
const t=clamp(phaseT/ph2.dur,0,1);
switch(ph2.name){
case'closed_pause':progress=0;break;
case'opening':progress=easeOut(t);break;
case'open_pause':progress=1;break;
case'closing':progress=1-easeIO(t);break;
}
}
/* ====== 手动控制 ====== */
let manualTarget=-1;
document.getElementById('bOpen').addEventListener('click',()=>{
autoPlay=false;manualTarget=1;updateAutoBtn();
});
document.getElementById('bClose').addEventListener('click',()=>{
autoPlay=false;manualTarget=0;updateAutoBtn();
});
document.getElementById('bAuto').addEventListener('click',()=>{
autoPlay=!autoPlay;manualTarget=-1;updateAutoBtn();
});
function updateAutoBtn(){
const ic=document.getElementById('autoIcon');
ic.className=autoPlay?'fas fa-pause mr-1':'fas fa-play mr-1';
document.getElementById('bOpen').classList.toggle('act',manualTarget===1);
document.getElementById('bClose').classList.toggle('act',manualTarget===0);
}
function tickManual(dt){
if(autoPlay||manualTarget<0)return;
const speed=1.2;
if(manualTarget===1){
progress=clamp(progress+dt/1800*speed,0,1);
if(progress>=1)manualTarget=-1;
}else{
progress=clamp(progress-dt/1800*speed,0,1);
if(progress<=0)manualTarget=-1;
}
/* 同步phaseIdx以便标注正确 */
if(progress>=1){phaseIdx=2;phaseT=0;}
else if(progress<=0){phaseIdx=0;phaseT=0;}
else if(manualTarget===1){phaseIdx=1;phaseT=progress/1*PHASES[1].dur;}
else{phaseIdx=3;phaseT=(1-progress)/1*PHASES[3].dur;}
}
/* ====== 主动画循环 ====== */
function animate(ts){
const dt=lastTs?ts-lastTs:16;
lastTs=ts;
tickPhase(dt);
tickManual(dt);
spawnParticles();
updateParticles(dt);
render();
requestAnimationFrame(animate);
}
/* ====== 初始化 ====== */
build();
updateAutoBtn();
requestAnimationFrame(animate);
/* 键盘辅助 */
document.addEventListener('keydown',e=>{
if(e.key==='o'||e.key==='O'){document.getElementById('bOpen').click();}
if(e.key==='c'||e.key==='C'){document.getElementById('bClose').click();}
if(e.key===' '){e.preventDefault();document.getElementById('bAuto').click();}
});
})();
</script>
</body>
</html>
实现说明
本动画围绕 TRIZ 最终理想解(IFR) 思想设计,直接展示 SMA 人工肌肉伞的理想工作状态,无需与旧方案对比即可理解核心创新:
视觉引导策略
- SMA 丝颜色渐变:从冷态青绿(#00c9a7)到热态橙红(#ff5722),配合发光滤镜,直观呈现"通电相变→收缩"的核心机理
- 丝形波动→绷直:冷态时 SMA 丝呈正弦波动(松弛马氏体),升温后逐渐绷直(收缩奥氏体),物理状态变化一目了然
- 微观相态面板:左下角实时展示马氏体锯齿形→奥氏体直线的晶格变化,将不可见的相变可视化
资源利用展示
- 伞面弹性回缩力作为"免费资源"实现自动收伞——断电后伞面自身弹性将骨架拉回,无需额外驱动
- 彻底消除铰链摩擦死点——SMA 丝既是结构又是驱动,实现"自执行"理想解
交互设计
- 开伞/收伞按钮:手动控制动画进程,可停留在任意中间状态观察
- 自动循环:页面加载即自动播放完整的开→停→收→停循环
- 键盘支持:O 键开伞、C 键收伞、空格切换自动模式
- 底部温度条与状态文字实时反馈当前相变进度
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
