独立渲染引擎就绪引擎就绪
<!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秒):
- 套入 — SMA环带从上方下降并套入瓶盖,带有轻微弹跳反馈
- 加热 — 温度从20°C升至52°C,环带颜色从冷蓝渐变为橙红,热浪线与粒子效果同步出现
- 收缩 — 环带宽度缩减15%,褶皱变形可视化,6条径向力箭头亮起
- 开盖 — 瓶盖在收缩力下轻松脱离瓶口,旋转力矩箭头与IFR标签同步出现,突出"夹持与破防合一"的理想解
- 冷却 — 温度回落,环带松弛变色,粒子消散
- 脱开 — 环带上升复位,准备下一循环
IFR视觉引导:
- 左侧温度计实时映射温度与相变点(40°C标注)
- 右侧面板展示SMA晶格转变(马氏体↔奥氏体)、资源流图(体温→相变力→夹持破防)和关键参数
- 力箭头与旋转箭头突出核心创新——径向收缩力替代机械杠杆
- IFR标签在开盖瞬间浮现,点明"自行收缩→合二为一→微力开盖"
交互性: 滑块可手动控制温度(20–55°C),实时驱动环带收缩、颜色、盖体位移等全部可视化参数;可随时切换回自动演示模式。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
