分享图
动画工坊
引擎就绪
<!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分