分享图
动画工坊
引擎就绪
<!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=Orbitron:wght@400;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#050a14;--accent:#00e5ff;--cold:#4fc3f7;--hot:#ff6d00;--peak:#ff1744;--text:#c8d6e5;--muted:#4a5a78;--card:rgba(10,18,36,0.85)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans SC',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden}
body::before{content:'';position:fixed;inset:0;background:linear-gradient(rgba(0,229,255,.02) 1px,transparent 1px),linear-gradient(90deg,rgba(0,229,255,.02) 1px,transparent 1px);background-size:50px 50px;pointer-events:none;z-index:0}
body::after{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 50% 40%,rgba(0,229,255,.04) 0%,transparent 60%);pointer-events:none;z-index:0}
.wrap{position:relative;z-index:1;width:96vw;max-width:1100px;display:flex;flex-direction:column;align-items:center;gap:12px}
.hd{text-align:center}
.hd h1{font-family:'Orbitron',monospace;font-size:clamp(.9rem,2vw,1.3rem);font-weight:700;letter-spacing:3px;color:var(--accent);text-shadow:0 0 24px rgba(0,229,255,.35)}
.hd p{font-size:.78rem;color:var(--muted);margin-top:2px;font-weight:300;letter-spacing:1px}
.svg-box{width:100%;max-width:1050px;aspect-ratio:10/7;border-radius:14px;overflow:hidden;border:1px solid rgba(0,229,255,.08);background:rgba(5,10,20,.6);box-shadow:0 0 60px rgba(0,229,255,.04)}
.svg-box svg{width:100%;height:100%;display:block}
.ctrl{display:flex;align-items:center;gap:16px;background:var(--card);border:1px solid rgba(0,229,255,.12);border-radius:10px;padding:10px 22px;backdrop-filter:blur(10px);flex-wrap:wrap;justify-content:center}
.ctrl label{font-size:.75rem;color:var(--muted);font-weight:500;white-space:nowrap}
.ctrl input[type=range]{-webkit-appearance:none;width:180px;height:5px;border-radius:3px;background:linear-gradient(90deg,var(--cold),#fff176,var(--hot),var(--peak));outline:none;cursor:pointer}
.ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 0 8px rgba(255,255,255,.5);cursor:pointer}
.tdisp{font-family:'Orbitron',monospace;font-size:.95rem;font-weight:700;min-width:52px;text-align:right}
.btn{background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.25);color:var(--accent);padding:5px 14px;border-radius:6px;font-family:'Noto Sans SC',sans-serif;font-size:.75rem;cursor:pointer;transition:all .2s;white-space:nowrap}
.btn:hover{background:rgba(0,229,255,.18)}
.btn.on{background:rgba(0,229,255,.22);border-color:var(--accent);box-shadow:0 0 10px rgba(0,229,255,.2)}
@media(prefers-reduced-motion:reduce){*{animation-duration:0s!important;transition-duration:0s!important}}
</style>
</head>
<body>
<div class="wrap">
  <div class="hd">
    <h1>SMA THERMAL GRIP OPENER</h1>
    <p>温控收缩式柔性环带开瓶器 — 最终理想解 (IFR) 原理演示</p>
  </div>
  <div class="svg-box">
    <svg id="svg" viewBox="0 0 1000 700" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
  <div class="ctrl">
    <label>手动温控</label>
    <input type="range" id="slider" min="20" max="55" value="20" step="0.5">
    <span class="tdisp" id="tdisp">20°C</span>
    <button class="btn on" id="btnAuto">自动演示</button>
    <button class="btn" id="btnReset">重置</button>
  </div>
</div>
<script>
(() => {
  /* ====== 常量 ====== */
  const NS = 'http://www.w3.org/2000/svg';
  const CX = 480, CY = 310;           // 瓶盖中心
  const RING_REST_Y = 295;             // 环带就位Y
  const RING_START_Y = 110;            // 环带初始Y
  const RING_W = 126, RING_H = 36;     // 环带尺寸
  const NECK_W = 76, CAP_W = 106;      // 瓶颈/瓶盖宽度
  const CRIMP_N = 16, CRIMP_D = 10;    // 褶皱数与深度
  const CONTRACT = 0.15;               // 收缩比
  const CYCLE = 12000;                 // 循环时长ms
  const T_MIN = 20, T_MAX = 55, T_PHASE = 40;

  /* ====== 关键帧 ====== */
  const KF = [
    {t:0.00, ry:RING_START_Y, temp:20, con:0, lift:0, crD:0},
    {t:0.11, ry:RING_REST_Y,  temp:20, con:0, lift:0, crD:0},
    {t:0.14, ry:RING_REST_Y+3,temp:20, con:0, lift:0, crD:0},
    {t:0.17, ry:RING_REST_Y,  temp:20, con:0, lift:0, crD:0},
    {t:0.22, ry:RING_REST_Y,  temp:24, con:0, lift:0, crD:0},
    {t:0.42, ry:RING_REST_Y,  temp:52, con:1, lift:0, crD:.7},
    {t:0.52, ry:RING_REST_Y,  temp:52, con:1, lift:1, crD:1},
    {t:0.62, ry:RING_REST_Y,  temp:52, con:1, lift:1, crD:1},
    {t:0.78, ry:RING_REST_Y,  temp:20, con:0, lift:.3,crD:0},
    {t:0.86, ry:RING_REST_Y,  temp:20, con:0, lift:0, crD:0},
    {t:0.94, ry:RING_START_Y, temp:20, con:0, lift:0, crD:0},
    {t:1.00, ry:RING_START_Y, temp:20, con:0, lift:0, crD:0}
  ];

  /* ====== 工具函数 ====== */
  const lerp = (a,b,t) => a+(b-a)*t;
  const clamp = (v,lo,hi) => Math.max(lo,Math.min(hi,v));
  const lerpC = (c1,c2,t) => c1.map((v,i)=>Math.round(lerp(v,c2[i],t)));
  const rgb = c => `rgb(${c[0]},${c[1]},${c[2]})`;
  const rgba = (c,a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;

  function tempColor(t){
    const n = clamp((t-T_MIN)/(T_MAX-T_MIN),0,1);
    const C0=[79,195,247],C1=[255,241,118],C2=[255,109,0],C3=[255,23,68];
    if(n<.4) return lerpC(C0,C1,n/.4);
    if(n<.7) return lerpC(C1,C2,(n-.4)/.3);
    return lerpC(C2,C3,(n-.7)/.3);
  }

  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;
  }

  function interpKF(p){
    let i=0;
    for(;i<KF.length-1;i++) if(p<KF[i+1].t) break;
    const a=KF[i],b=KF[Math.min(i+1,KF.length-1)];
    const span=b.t-a.t;
    const f=span>0?clamp((p-a.t)/span,0,1):0;
    const ease=f<.5?2*f*f:1-Math.pow(-2*f+2,2)/2; // easeInOutQuad
    return{
      ry:lerp(a.ry,b.ry,ease),
      temp:lerp(a.temp,b.temp,ease),
      con:lerp(a.con,b.con,ease),
      lift:lerp(a.lift,b.lift,ease),
      crD:lerp(a.crD,b.crD,ease)
    };
  }

  /* ====== SVG 场景构建 ====== */
  const svg = document.getElementById('svg');

  // -- defs --
  const defs = el('defs',null,svg);

  // 背景径向渐变
  const bgG = el('radialGradient',{id:'bgG',cx:'50%',cy:'42%',r:'55%'},defs);
  el('stop',{offset:'0%','stop-color':'#0c1628'},bgG);
  el('stop',{offset:'100%','stop-color':'#050a14'},bgG);

  // 瓶身渐变
  const btlG = el('linearGradient',{id:'btlG',x1:'0',y1:'0',x2:'1',y2:'0'},defs);
  el('stop',{offset:'0%','stop-color':'#12261e'},btlG);
  el('stop',{offset:'35%','stop-color':'#1e4038'},btlG);
  el('stop',{offset:'50%','stop-color':'#265a4a'},btlG);
  el('stop',{offset:'65%','stop-color':'#1e4038'},btlG);
  el('stop',{offset:'100%','stop-color':'#12261e'},btlG);

  // 瓶盖渐变
  const capG = el('linearGradient',{id:'capG',x1:'0',y1:'0',x2:'1',y2:'0'},defs);
  el('stop',{offset:'0%','stop-color':'#6b4e14'},capG);
  el('stop',{offset:'30%','stop-color':'#b8922a'},capG);
  el('stop',{offset:'50%','stop-color':'#d4aa3c'},capG);
  el('stop',{offset:'70%','stop-color':'#b8922a'},capG);
  el('stop',{offset:'100%','stop-color':'#6b4e14'},capG);

  // 发光滤镜
  const glowF = el('filter',{id:'glow',x:'-50%',y:'-50%',width:'200%',height:'200%'},defs);
  el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'6',result:'b'},glowF);
  const gm = el('feMerge',null,glowF);
  el('feMergeNode',{in:'b'},gm);
  el('feMergeNode',{in:'SourceGraphic'},gm);

  const glowF2 = el('filter',{id:'glow2',x:'-80%',y:'-80%',width:'260%',height:'260%'},defs);
  el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'12',result:'b'},glowF2);
  const gm2 = el('feMerge',null,glowF2);
  el('feMergeNode',{in:'b'},gm2);
  el('feMergeNode',{in:'SourceGraphic'},gm2);

  // 箭头标记
  const arrowM = el('marker',{id:'arrM',markerWidth:'8',markerHeight:'6',refX:'8',refY:'3',orient:'auto',fill:'#ffd600'},defs);
  el('polygon',{points:'0 0, 8 3, 0 6'},arrowM);

  // -- 背景矩形 --
  el('rect',{x:0,y:0,width:1000,height:700,fill:'url(#bgG)'},svg);

  // -- 左侧温度计 --
  const thermoG = el('g',{id:'thermo'},svg);
  el('rect',{x:62,y:120,width:18,height:340,rx:9,fill:'#0a1428',stroke:'#1a3050','stroke-width':1.5},thermoG);
  const thermoFill = el('rect',{x:64,y:290,width:14,height:168,rx:7,fill:'var(--cold)'},thermoG);
  el('text',{x:71,y:112,'text-anchor':'middle',fill:'#4a5a78','font-size':'11','font-family':'Orbitron,monospace','font-weight':'700'},thermoG).textContent='TEMP';
  // 温度刻度
  [20,30,40,50].forEach(t=>{
    const y = 458 - (t-20)/35*336;
    el('line',{x1:82,y1:y,x2:92,y2:y,stroke:'#2a3a5a','stroke-width':1},thermoG);
    el('text',{x:96,y:y+4,fill:'#4a5a78','font-size':'10','font-family':'Orbitron,monospace'},thermoG).textContent=t+'°';
  });
  // 相变标记
  const phaseY = 458-(T_PHASE-20)/35*336;
  el('line',{x1:54,y1:phaseY,x2:82,y2:phaseY,stroke:'#ff6d00','stroke-width':1.5,'stroke-dasharray':'3,2'},thermoG);
  el('text',{x:52,y:phaseY+3,'text-anchor':'end',fill:'#ff6d00','font-size':'9','font-family':'Noto Sans SC,sans-serif'},thermoG).textContent='相变40°C';

  const thermoVal = el('text',{x:71,y:485,'text-anchor':'middle',fill:'var(--accent)','font-size':'14','font-family':'Orbitron,monospace','font-weight':'700'},thermoG);

  // -- 右侧信息面板 --
  const infoG = el('g',{id:'info',transform:'translate(740,0)'},svg);

  // 相态指示
  el('text',{x:0,y:108,fill:'#4a5a78','font-size':'12','font-family':'Orbitron,monospace','font-weight':'700','letter-spacing':'2'},infoG).textContent='SMA PHASE';
  const phaseBox = el('rect',{x:-4,y:118,width:210,height:52,rx:8,fill:'rgba(0,229,255,.06)',stroke:'rgba(0,229,255,.15)','stroke-width':1},infoG);
  const phaseText = el('text',{x:101,y:142,'text-anchor':'middle',fill:'var(--cold)','font-size':'16','font-family':'Orbitron,monospace','font-weight':'900'},infoG);
  phaseText.textContent='MARTENSITE';
  const phaseSub = el('text',{x:101,y:160,'text-anchor':'middle',fill:'#6b7a99','font-size':'11','font-family':'Noto Sans SC,sans-serif'},infoG);
  phaseSub.textContent='马氏体 · 柔性态';

  // 晶格可视化
  el('text',{x:0,y:204,fill:'#4a5a78','font-size':'12','font-family':'Orbitron,monospace','font-weight':'700','letter-spacing':'2'},infoG).textContent='LATTICE';
  const latG = el('g',{transform:'translate(0,214)'},infoG);
  el('rect',{x:-4,y:0,width:210,height:90,rx:8,fill:'rgba(0,229,255,.04)',stroke:'rgba(0,229,255,.1)','stroke-width':1},latG);
  // 马氏体(左侧)
  const martG = el('g',{transform:'translate(20,12)'},latG);
  for(let r=0;r<4;r++)for(let c=0;c<4;c++){
    const bx=c*18, by=r*18;
    const shift=(r%2)?5:0;
    el('line',{x1:bx+shift,y1:by,x2:bx+18+shift,y2:by,stroke:'#4fc3f7','stroke-width':1.2,opacity:.7},martG);
    el('line',{x1:bx+shift,y1:by,x2:bx+shift,y2:by+18,stroke:'#4fc3f7','stroke-width':1.2,opacity:.7},martG);
  }
  el('text',{x:38,y:85,'text-anchor':'middle',fill:'#4a5a78','font-size':'9','font-family':'Noto Sans SC,sans-serif'},martG).textContent='马氏体';
  // 箭头
  const latArrow = el('text',{x:100,y:48,'text-anchor':'middle',fill:'#ff6d00','font-size':'18','font-family':'Orbitron,monospace'},latG);
  latArrow.textContent='→';
  // 奥氏体(右侧)
  const austG = el('g',{transform:'translate(118,12)'},latG);
  for(let r=0;r<4;r++)for(let c=0;c<4;c++){
    const bx=c*16, by=r*16;
    el('line',{x1:bx,y1:by,x2:bx+16,y2:by,stroke:'#ff6d00','stroke-width':1.2,opacity:.7},austG);
    el('line',{x1:bx,y1:by,x2:bx,y2:by+16,stroke:'#ff6d00','stroke-width':1.2,opacity:.7},austG);
  }
  el('text',{x:32,y:85,'text-anchor':'middle',fill:'#4a5a78','font-size':'9','font-family':'Noto Sans SC,sans-serif'},austG).textContent='奥氏体';

  // 晶格高亮框
  const latHL = el('rect',{x:14,y:8,width:78,height:78,rx:6,fill:'none',stroke:'var(--cold)','stroke-width':2},latG);

  // 资源流图
  el('text',{x:0,y:335,fill:'#4a5a78','font-size':'12','font-family':'Orbitron,monospace','font-weight':'700','letter-spacing':'2'},infoG).textContent='RESOURCE FLOW';
  const resG = el('g',{transform:'translate(0,345)'},infoG);
  el('rect',{x:-4,y:0,width:210,height:115,rx:8,fill:'rgba(0,229,255,.04)',stroke:'rgba(0,229,255,.1)','stroke-width':1},resG);
  const resLabels = [
    {y:22,text:'体温 / 电热',sub:'(免费资源)',c:'#4fc3f7'},
    {y:54,text:'相变收缩力',sub:'(自动执行)',c:'#ff6d00'},
    {y:86,text:'夹持 + 破防',sub:'(合二为一)',c:'#ffd600'}
  ];
  resLabels.forEach((l,i)=>{
    el('text',{x:30,y:l.y,fill:l.c,'font-size':'13','font-family':'Noto Sans SC,sans-serif','font-weight':'700'},resG).textContent=l.text;
    el('text',{x:168,y:l.y,fill:'#4a5a78','font-size':'9','font-family':'Noto Sans SC,sans-serif'},resG).textContent=l.sub;
    if(i<2) el('text',{x:95,y:l.y+18,'text-anchor':'middle',fill:'#2a3a5a','font-size':'14'},resG).textContent='↓';
  });

  // 关键参数
  el('text',{x:0,y:490,fill:'#4a5a78','font-size':'12','font-family':'Orbitron,monospace','font-weight':'700','letter-spacing':'2'},infoG).textContent='PARAMS';
  const paramG = el('g',{transform:'translate(0,500)'},infoG);
  el('rect',{x:-4,y:0,width:210,height:80,rx:8,fill:'rgba(0,229,255,.04)',stroke:'rgba(0,229,255,.1)','stroke-width':1},paramG);
  el('text',{x:10,y:22,fill:'#8a9ab8','font-size':'11','font-family':'Noto Sans SC,sans-serif'},paramG).textContent='相变温度: 40°C';
  el('text',{x:10,y:42,fill:'#8a9ab8','font-size':'11','font-family':'Noto Sans SC,sans-serif'},paramG).textContent='收缩比: 15%';
  el('text',{x:10,y:62,fill:'#8a9ab8','font-size':'11','font-family':'Noto Sans SC,sans-serif'},paramG).textContent='驱动方式: 体温 / 微型电热';

  // -- 瓶身组 --
  const bottleG = el('g',{id:'bottle'},svg);

  // 瓶身路径
  const btlPath = d=>`M ${CX-38} 318 L ${CX-38} 400 C ${CX-38} 450,${CX-85} 500,${CX-90} 530 L ${CX-90} 700 L ${CX+90} 700 L ${CX+90} 530 C ${CX+85} 500,${CX+38} 450,${CX+38} 400 L ${CX+38} 318 Z`;
  el('path',{d:btlPath(),fill:'url(#btlG)',stroke:'#0a1a12','stroke-width':1.5},bottleG);

  // 瓶口唇缘
  el('path',{d:`M ${CX-44} 318 L ${CX-44} 306 Q ${CX-44} 300 ${CX-38} 300 L ${CX+38} 300 Q ${CX+44} 300 ${CX+44} 306 L ${CX+44} 318 Z`,fill:'url(#btlG)',stroke:'#0a1a12','stroke-width':1},bottleG);

  // 瓶身高光
  el('rect',{x:CX-24,y:340,width:8,height:180,rx:4,fill:'rgba(255,255,255,.04)'},bottleG);
  el('rect',{x:CX+16,y:360,width:4,height:140,rx:2,fill:'rgba(255,255,255,.025)'},bottleG);

  // 液面线
  el('rect',{x:CX-82,y:540,width:164,height:4,rx:2,fill:'rgba(180,140,60,.15)'},bottleG);
  el('text',{x:CX,y:558,'text-anchor':'middle',fill:'rgba(180,140,60,.25)','font-size':'10','font-family':'Orbitron,monospace'},bottleG).textContent='BEER';

  // -- 瓶盖组 (会随lift上移) --
  const capGroup = el('g',{id:'capGrp'},svg);

  function capCrimpsD(deform){
    const hw = CAP_W/2;
    const top = 268, bot = 300;
    const d = CRIMP_D * (1 - deform * 0.6); // 褶皱变形
    let path = `M ${CX-hw} ${top} L ${CX-hw} ${bot} `;
    const tw = CAP_W / CRIMP_N;
    for(let i=0;i<CRIMP_N;i++){
      const bx = CX - hw + i * tw;
      path += `L ${bx+tw*.3} ${bot+d} L ${bx+tw*.7} ${bot+d*.4} L ${bx+tw} ${bot+d} `;
    }
    path += `L ${CX+hw} ${bot} L ${CX+hw} ${top} Z`;
    return path;
  }

  const capBody = el('path',{d:capCrimpsD(0),fill:'url(#capG)',stroke:'#4a3010','stroke-width':1},capGroup);
  // 盖顶高光
  el('rect',{x:CX-CAP_W/2+8,y:270,width:CAP_W-16,height:8,rx:4,fill:'rgba(255,255,255,.12)'},capGroup);

  // -- SMA 环带组 --
  const ringGroup = el('g',{id:'ringGrp'},svg);
  // 外发光层
  const ringGlow = el('rect',{x:CX-RING_W/2-6,y:RING_START_Y-6,width:RING_W+12,height:RING_H+12,rx:14,fill:'rgba(79,195,247,.15)',filter:'url(#glow2)'},ringGroup);
  // 主体
  const ringBody = el('rect',{x:CX-RING_W/2,y:RING_START_Y,width:RING_W,height:RING_H,rx:10,fill:'#4fc3f7',stroke:'#2196f3','stroke-width':2},ringGroup);
  // 编织纹理 (对角线)
  const ringTexG = el('g',{id:'ringTex','clip-path':'url(#ringClip)'},ringGroup);
  const ringClip = el('clipPath',{id:'ringClip'},defs);
  const ringClipRect = el('rect',{x:CX-RING_W/2,y:RING_START_Y,width:RING_W,height:RING_H,rx:10},ringClip);

  for(let i=-10;i<20;i++){
    const ox = CX-RING_W/2+i*10;
    el('line',{x1:ox,y1:RING_START_Y,x2:ox+RING_H,y2:RING_START_Y+RING_H,stroke:'rgba(255,255,255,.1)','stroke-width':1},ringTexG);
    el('line',{x1:ox+RING_H,y1:RING_START_Y,x2:ox,y2:RING_START_Y+RING_H,stroke:'rgba(0,0,0,.1)','stroke-width':1},ringTexG);
  }

  // 摩擦齿 (内侧小三角)
  const teethG = el('g',{id:'teeth'},ringGroup);
  for(let i=0;i<8;i++){
    const tx = CX - RING_W/2 + 10 + i*(RING_W-20)/7;
    el('polygon',{points:`${tx},${RING_START_Y+RING_H} ${tx+4},${RING_START_Y+RING_H-7} ${tx+8},${RING_START_Y+RING_H}`,fill:'rgba(255,255,255,.15)',stroke:'none'},teethG);
  }

  // 加热指示 (小矩形-微型电热片)
  const heatIndG = el('g',{id:'heatInd'},ringGroup);
  el('rect',{x:CX-8,y:RING_START_Y+4,width:16,height:6,rx:2,fill:'rgba(255,255,255,.08)',stroke:'rgba(255,200,100,.3)','stroke-width':.5},heatIndG);
  const heatDot = el('circle',{cx:CX,cy:RING_START_Y+7,r:2,fill:'#4fc3f7'},heatIndG);

  // -- 力箭头组 --
  const arrowG = el('g',{id:'arrows',opacity:0},svg);
  const arrowPositions = [
    {x:CX-RING_W/2-30,y:RING_REST_Y+RING_H/2,dx:28,dy:0},
    {x:CX+RING_W/2+30,y:RING_REST_Y+RING_H/2,dx:-28,dy:0},
    {x:CX-20,y:RING_REST_Y-22,dx:12,dy:16},
    {x:CX+20,y:RING_REST_Y-22,dx:-12,dy:16},
    {x:CX-20,y:RING_REST_Y+RING_H+22,dx:12,dy:-16},
    {x:CX+20,y:RING_REST_Y+RING_H+22,dx:-12,dy:-16}
  ];
  arrowPositions.forEach(a=>{
    el('line',{x1:a.x,y1:a.y,x2:a.x+a.dx,y2:a.y+a.dy,stroke:'#ffd600','stroke-width':2.5,'marker-end':'url(#arrM)'},arrowG);
  });

  // 旋转力矩箭头
  const rotArrowG = el('g',{id:'rotArrow',opacity:0},svg);
  el('path',{d:`M ${CX+60} ${RING_REST_Y+RING_H+40} A 55 55 0 0 0 ${CX-50} ${RING_REST_Y+RING_H+40}`,fill:'none',stroke:'#00e5ff','stroke-width':2,'marker-end':'url(#arrM)'},rotArrowG);
  const rotLabel = el('text',{x:CX,y:RING_REST_Y+RING_H+58,'text-anchor':'middle',fill:'#00e5ff','font-size':'11','font-family':'Noto Sans SC,sans-serif'},rotArrowG);
  rotLabel.textContent='微转即可开盖';

  // -- 粒子组 --
  const partG = el('g',{id:'particles'},svg);

  // -- IFR 标签 --
  const ifrG = el('g',{id:'ifr',opacity:0},svg);
  const ifrBg = el('rect',{x:CX-155,y:170,width:310,height:60,rx:10,fill:'rgba(0,229,255,.08)',stroke:'rgba(0,229,255,.3)','stroke-width':1.5},ifrG);
  el('text',{x:CX,y:195,'text-anchor':'middle',fill:'#00e5ff','font-size':'14','font-family':'Orbitron,monospace','font-weight':'900'},ifrG).textContent='IDEAL FINAL RESULT';
  el('text',{x:CX,y:218,'text-anchor':'middle',fill:'#8aeaff','font-size':'12','font-family':'Noto Sans SC,sans-serif'},ifrG).textContent='环带自行收缩 → 夹持与破防合一 → 仅微力开盖';

  // -- 步骤指示 --
  const stepG = el('g',{id:'steps',transform:'translate(0,0)'},svg);
  const steps = ['套入','加热','收缩','开盖','冷却','脱开'];
  const stepCx = steps.map((_,i)=>200+i*120);
  const stepY = 645;
  // 连接线
  for(let i=0;i<steps.length-1;i++){
    el('line',{x1:stepCx[i]+18,y1:stepY,x2:stepCx[i+1]-18,y2:stepY,stroke:'#1a2a4a','stroke-width':2},stepG);
  }
  const stepCircles = steps.map((s,i)=>{
    const c = el('circle',{cx:stepCx[i],cy:stepY,r:16,fill:'#0a1428',stroke:'#1a3050','stroke-width':2},stepG);
    el('text',{x:stepCx[i],y:stepY+4,'text-anchor':'middle',fill:'#4a5a78','font-size':'10','font-family':'Orbitron,monospace','font-weight':'700'},stepG).textContent=i+1;
    el('text',{x:stepCx[i],y:stepY+30,'text-anchor':'middle',fill:'#4a5a78','font-size':'11','font-family':'Noto Sans SC,sans-serif'},stepG).textContent=s;
    return c;
  });

  // -- 热浪线组 --
  const heatWaveG = el('g',{id:'heatWaves',opacity:0},svg);

  /* ====== 粒子系统 ====== */
  let particles = [];
  function spawnParticle(temp){
    const col = tempColor(temp);
    const side = Math.random()>.5?1:-1;
    particles.push({
      x: CX + side*(RING_W/2*Math.random()),
      y: RING_REST_Y + Math.random()*RING_H,
      vx: (Math.random()-.5)*.8,
      vy: -Math.random()*1.8 - .6,
      life: 1,
      decay: .012 + Math.random()*.015,
      size: 2 + Math.random()*3,
      color: col
    });
  }

  function updateParticles(dt){
    particles.forEach(p=>{
      p.x += p.vx;
      p.y += p.vy;
      p.life -= p.decay;
      p.vy -= .02; // 微弱上升加速
    });
    particles = particles.filter(p=>p.life>0);
  }

  function renderParticles(){
    while(partG.firstChild) partG.removeChild(partG.firstChild);
    particles.forEach(p=>{
      el('circle',{cx:p.x,cy:p.y,r:p.size,fill:rgba(p.color,p.life*.6),filter:'url(#glow)'},partG);
    });
  }

  /* ====== 热浪线 ====== */
  let wavePhase = 0;
  function renderHeatWaves(temp, opacity){
    while(heatWaveG.firstChild) heatWaveG.removeChild(heatWaveG.firstChild);
    heatWaveG.setAttribute('opacity', opacity);
    if(opacity < .05) return;
    const n = Math.floor(3 + (temp-30)/10);
    for(let i=0;i<n;i++){
      const side = i%2?1:-1;
      const bx = CX + side*(RING_W/2+15+i*8);
      const by = RING_REST_Y + 5 + i*5;
      const amp = 4+i*1.5;
      let d = `M ${bx} ${by}`;
      for(let s=0;s<3;s++){
        const ny = by + (s+1)*8;
        const nx = bx + Math.sin(wavePhase+s*1.2+i)*amp*side;
        d += ` Q ${nx+amp*side} ${ny-4} ${nx} ${ny}`;
      }
      el('path',{d:d,fill:'none',stroke:rgba(tempColor(temp),.3),'stroke-width':1.5,'stroke-linecap':'round'},heatWaveG);
    }
  }

  /* ====== 动画状态 ====== */
  let progress = 0;
  let autoPlay = true;
  let manualTemp = 20;
  let lastTS = 0;
  let currentState = {ry:RING_START_Y,temp:20,con:0,lift:0,crD:0};

  function getStepIndex(p){
    if(p<.11) return 0;
    if(p<.22) return 1;
    if(p<.42) return 2;
    if(p<.62) return 3;
    if(p<.86) return 4;
    return 5;
  }

  /* ====== 渲染 ====== */
  function render(st){
    const {ry,temp,con,lift,crD} = st;
    const tNorm = clamp((temp-T_MIN)/(T_MAX-T_MIN),0,1);
    const col = tempColor(temp);

    // -- 环带位置与尺寸 --
    const rw = RING_W * (1 - con * CONTRACT);
    const rh = RING_H;
    ringGroup.setAttribute('transform',`translate(0,${ry-RING_START_Y})`);
    ringBody.setAttribute('x', CX-rw/2);
    ringBody.setAttribute('width', rw);
    ringBody.setAttribute('fill', rgb(col));
    ringBody.setAttribute('stroke', rgb(lerpC(col,[255,255,255],.3)));

    ringGlow.setAttribute('x', CX-rw/2-8);
    ringGlow.setAttribute('y', ry-8-RING_START_Y+RING_START_Y);
    ringGlow.setAttribute('width', rw+16);
    ringGlow.setAttribute('height', rh+16);
    ringGlow.setAttribute('fill', rgba(col,.18+con*.15));

    // 裁剪区域更新
    ringClipRect.setAttribute('x', CX-rw/2);
    ringClipRect.setAttribute('y', RING_START_Y);
    ringClipRect.setAttribute('width', rw);
    ringClipRect.setAttribute('height', rh);

    // 编织纹理更新
    while(ringTexG.firstChild) ringTexG.removeChild(ringTexG.firstChild);
    for(let i=-12;i<22;i++){
      const ox = CX-rw/2+i*9;
      el('line',{x1:ox,y1:RING_START_Y,x2:ox+rh,y2:RING_START_Y+rh,stroke:'rgba(255,255,255,.08)','stroke-width':1},ringTexG);
      el('line',{x1:ox+rh,y1:RING_START_Y,x2:ox,y2:RING_START_Y+rh,stroke:'rgba(0,0,0,.06)','stroke-width':1},ringTexG);
    }

    // 摩擦齿更新
    while(teethG.firstChild) teethG.removeChild(teethG.firstChild);
    const teethN = Math.max(4,Math.floor(rw/16));
    for(let i=0;i<teethN;i++){
      const tx = CX-rw/2+8+i*(rw-16)/(teethN-1);
      el('polygon',{points:`${tx},${RING_START_Y+rh} ${tx+3},${RING_START_Y+rh-6} ${tx+6},${RING_START_Y+rh}`,fill:'rgba(255,255,255,.12)'},teethG);
    }

    // 加热指示点
    heatDot.setAttribute('cx', CX);
    heatDot.setAttribute('cy', RING_START_Y+7);
    heatDot.setAttribute('fill', tNorm>.05?rgb(col):'#4fc3f7');

    // -- 瓶盖 --
    capBody.setAttribute('d', capCrimpsD(crD));
    const capLiftY = -lift * 55;
    capGroup.setAttribute('transform',`translate(0,${capLiftY})`);

    // -- 温度计 --
    const fillH = tNorm * 336;
    thermoFill.setAttribute('y', 458-fillH);
    thermoFill.setAttribute('height', fillH+2);
    thermoFill.setAttribute('fill', rgb(col));
    thermoVal.textContent = Math.round(temp)+'°C';
    thermoVal.setAttribute('fill', rgb(col));

    // -- 力箭头 --
    const arrowOp = con>.1?clamp((con-.1)/.5,0,1):0;
    arrowG.setAttribute('opacity', arrowOp);
    // 更新箭头位置跟随环带
    const apNew = [
      {x:CX-rw/2-30,y:ry+RING_H/2,dx:26,dy:0},
      {x:CX+rw/2+30,y:ry+RING_H/2,dx:-26,dy:0},
      {x:CX-18,y:ry-22,dx:10,dy:16},
      {x:CX+18,y:ry-22,dx:-10,dy:16},
      {x:CX-18,y:ry+RING_H+22,dx:10,dy:-16},
      {x:CX+18,y:ry+RING_H+22,dx:-10,dy:-16}
    ];
    const aLines = arrowG.querySelectorAll('line');
    apNew.forEach((a,i)=>{
      if(aLines[i]){
        aLines[i].setAttribute('x1',a.x);
        aLines[i].setAttribute('y1',a.y);
        aLines[i].setAttribute('x2',a.x+a.dx);
        aLines[i].setAttribute('y2',a.y+a.dy);
      }
    });

    // 旋转箭头
    const rotOp = lift>.3?clamp((lift-.3)/.4,0,1):0;
    rotArrowG.setAttribute('opacity', rotOp);

    // -- 热浪 --
    renderHeatWaves(temp, tNorm>.1?clamp(tNorm-.1,0,.8):0);

    // -- 相态标签 --
    if(temp<T_PHASE-3){
      phaseText.textContent='MARTENSITE';
      phaseText.setAttribute('fill','#4fc3f7');
      phaseSub.textContent='马氏体 · 柔性态';
      latHL.setAttribute('x',14);latHL.setAttribute('width',78);
      latHL.setAttribute('stroke','#4fc3f7');
    }else if(temp<T_PHASE+5){
      phaseText.textContent='TRANSITION';
      phaseText.setAttribute('fill','#fff176');
      phaseSub.textContent='相变进行中...';
      latHL.setAttribute('x',50);latHL.setAttribute('width',100);
      latHL.setAttribute('stroke','#fff176');
    }else{
      phaseText.textContent='AUSTENITE';
      phaseText.setAttribute('fill','#ff6d00');
      phaseSub.textContent='奥氏体 · 收缩态';
      latHL.setAttribute('x',118);latHL.setAttribute('width',78);
      latHL.setAttribute('stroke','#ff6d00');
    }

    // -- IFR 标签 --
    const ifrOp = lift>.6?clamp((lift-.6)/.3,0,1):0;
    ifrG.setAttribute('opacity', ifrOp);

    // -- 步骤高亮 --
    const si = getStepIndex(progress);
    stepCircles.forEach((c,i)=>{
      if(i===si){
        c.setAttribute('fill','rgba(0,229,255,.15)');
        c.setAttribute('stroke','#00e5ff');
        c.setAttribute('stroke-width',2.5);
      }else{
        c.setAttribute('fill','#0a1428');
        c.setAttribute('stroke','#1a3050');
        c.setAttribute('stroke-width',2);
      }
    });
    const stepTexts = stepG.querySelectorAll('text');
    stepTexts.forEach((t,i)=>{
      if(i>=steps.length){ // step label texts (every other)
      }
    });

    // -- 温度显示 --
    document.getElementById('tdisp').textContent = Math.round(temp)+'°C';
    document.getElementById('tdisp').style.color = rgb(col);
  }

  /* ====== 主循环 ====== */
  function mainLoop(ts){
    if(!lastTS) lastTS = ts;
    const dt = Math.min(ts-lastTS, 50);
    lastTS = ts;
    wavePhase += dt*.004;

    if(autoPlay){
      progress = (progress + dt/CYCLE) % 1;
      currentState = interpKF(progress);
      document.getElementById('slider').value = currentState.temp;
    }else{
      currentState.temp = manualTemp;
      currentState.ry = RING_REST_Y;
      const tN = clamp((manualTemp-T_MIN)/(T_MAX-T_MIN),0,1);
      currentState.con = tN>.3?clamp((tN-.3)/.4,0,1):0;
      currentState.lift = tN>.6?clamp((tN-.6)/.3,0,1):0;
      currentState.crD = currentState.con*.8;
      // 根据手动温度推断进度(仅用于步骤显示)
      if(manualTemp<25) progress=0.11;
      else if(manualTemp<T_PHASE) progress=0.22;
      else if(manualTemp<48) progress=0.35;
      else progress=0.52;
    }

    // 生成粒子
    if(currentState.temp > 28 && Math.random() < (currentState.temp-25)/30){
      spawnParticle(currentState.temp);
    }
    updateParticles(dt);
    renderParticles();

    render(currentState);
    requestAnimationFrame(mainLoop);
  }

  /* ====== 控件 ====== */
  const slider = document.getElementById('slider');
  const btnAuto = document.getElementById('btnAuto');
  const btnReset = document.getElementById('btnReset');

  slider.addEventListener('input', ()=>{
    manualTemp = parseFloat(slider.value);
    if(autoPlay){
      autoPlay = false;
      btnAuto.classList.remove('on');
      btnAuto.textContent='手动模式';
    }
  });

  btnAuto.addEventListener('click', ()=>{
    autoPlay = !autoPlay;
    btnAuto.classList.toggle('on', autoPlay);
    btnAuto.textContent = autoPlay?'自动演示':'手动模式';
    if(autoPlay) progress = 0;
  });

  btnReset.addEventListener('click', ()=>{
    progress = 0;
    autoPlay = true;
    btnAuto.classList.add('on');
    btnAuto.textContent='自动演示';
    slider.value = 20;
    particles = [];
  });

  /* ====== 启动 ====== */
  requestAnimationFrame(mainLoop);
})();
</script>
</body>
</html>

这个动画实现了温控收缩式柔性环带开瓶器的完整工作原理演示,核心设计要点如下:

动画流程(自动循环12秒):

  1. 套入 — SMA环带从上方下降并套入瓶盖,带有轻微弹跳反馈
  2. 加热 — 温度从20°C升至52°C,环带颜色从冷蓝渐变为橙红,热浪线与粒子效果同步出现
  3. 收缩 — 环带宽度缩减15%,褶皱变形可视化,6条径向力箭头亮起
  4. 开盖 — 瓶盖在收缩力下轻松脱离瓶口,旋转力矩箭头与IFR标签同步出现,突出"夹持与破防合一"的理想解
  5. 冷却 — 温度回落,环带松弛变色,粒子消散
  6. 脱开 — 环带上升复位,准备下一循环

IFR视觉引导:

  • 左侧温度计实时映射温度与相变点(40°C标注)
  • 右侧面板展示SMA晶格转变(马氏体↔奥氏体)、资源流图(体温→相变力→夹持破防)和关键参数
  • 力箭头与旋转箭头突出核心创新——径向收缩力替代机械杠杆
  • IFR标签在开盖瞬间浮现,点明"自行收缩→合二为一→微力开盖"

交互性: 滑块可手动控制温度(20–55°C),实时驱动环带收缩、颜色、盖体位移等全部可视化参数;可随时切换回自动演示模式。

积分规则:第一轮对话扣减8分,后续每轮扣6分