分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>二级升降换模车 · IFR原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet">
<style>
:root{
  --bg:#080d1a;--surface:#0f1628;--card:#151d33;--border:#1e2d4a;
  --text:#c8d6e5;--dim:#5a6e87;--amber:#f0a830;--cyan:#00d4ff;
  --red:#e74c3c;--green:#2ecc71;--purple:#9b59b6;
  --font-display:'Rajdhani',sans-serif;--font-mono:'Share Tech Mono',monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:var(--font-display);
  min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden}
.app{max-width:1440px;margin:0 auto;padding:16px 20px;display:flex;flex-direction:column;gap:12px;flex:1}
header{text-align:center;padding:8px 0}
header h1{font-size:clamp(1.4rem,3vw,2rem);font-weight:700;letter-spacing:2px;
  background:linear-gradient(135deg,var(--amber),#ffe0a0);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
header .sub{font-family:var(--font-mono);font-size:clamp(.7rem,1.5vw,.85rem);color:var(--dim);margin-top:2px;letter-spacing:1px}
.main{display:flex;gap:14px;flex:1;min-height:0}
.scene-wrap{flex:1;min-width:0;background:var(--surface);border:1px solid var(--border);
  border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center;position:relative}
.scene-wrap svg{width:100%;height:100%;display:block}
.sidebar{width:200px;display:flex;flex-direction:column;gap:10px;flex-shrink:0}
@media(max-width:860px){.sidebar{display:none}.main{flex-direction:column}}
.ind-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px 14px}
.ind-card .ind-label{font-size:.7rem;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;font-family:var(--font-mono)}
.ind-card .ind-bar{height:8px;background:#1a2238;border-radius:4px;overflow:hidden;position:relative}
.ind-card .ind-fill{height:100%;border-radius:4px;transition:width .4s ease,width .1s linear;position:absolute;left:0;top:0}
.ind-card .ind-val{font-family:var(--font-mono);font-size:.85rem;margin-top:5px;text-align:right;transition:color .3s}
.fill-risk{background:linear-gradient(90deg,var(--green),var(--amber),var(--red))}
.fill-temp{background:linear-gradient(90deg,#3498db,var(--amber),var(--red))}
.fill-force{background:linear-gradient(90deg,var(--dim),var(--cyan))}
.phase-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;text-align:center}
.phase-card .ph-num{font-size:2.2rem;font-weight:700;line-height:1;color:var(--amber)}
.phase-card .ph-name{font-size:.95rem;margin-top:4px;min-height:1.2em}
.phase-card .ph-desc{font-family:var(--font-mono);font-size:.65rem;color:var(--dim);margin-top:6px;line-height:1.4;min-height:2.8em}
.ctrls{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px 18px;
  display:flex;flex-wrap:wrap;align-items:center;gap:12px}
.phase-btns{display:flex;gap:6px;flex-wrap:wrap}
.phase-btns button{font-family:var(--font-display);font-size:.78rem;font-weight:500;
  padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--dim);
  cursor:pointer;transition:all .2s;white-space:nowrap}
.phase-btns button:hover{border-color:var(--amber);color:var(--text)}
.phase-btns button.active{background:var(--amber);color:#0a0f1e;border-color:var(--amber);font-weight:700}
.transport{display:flex;gap:6px}
.transport button{font-family:var(--font-display);font-size:.85rem;font-weight:600;
  padding:7px 16px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);
  cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px}
.transport button:hover{border-color:var(--cyan);color:var(--cyan)}
.transport button.playing{background:var(--cyan);color:#0a0f1e;border-color:var(--cyan)}
.sliders{display:flex;gap:16px;flex-wrap:wrap;margin-left:auto}
.sliders label{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.7rem;color:var(--dim)}
.sliders input[type=range]{width:100px;accent-color:var(--amber);cursor:pointer}
.glow-text{filter:drop-shadow(0 0 6px currentColor)}
@keyframes pulse{0%,100%{opacity:.6}50%{opacity:1}}
@keyframes dash-flow{to{stroke-dashoffset:-20}}
.field-line{animation:dash-flow .6s linear infinite}
</style>
</head>
<body>
<div class="app">
  <header>
    <h1>二级升降换模车系统</h1>
    <div class="sub">短行程粗调 &middot; SMA微调 &middot; 电磁锁紧 &mdash; IFR 原理演示</div>
  </header>
  <div class="main">
    <div class="scene-wrap">
      <svg id="scene" viewBox="0 0 1400 750" preserveAspectRatio="xMidYMid meet"></svg>
    </div>
    <aside class="sidebar">
      <div class="ind-card">
        <div class="ind-label">倾覆风险</div>
        <div class="ind-bar"><div class="ind-fill fill-risk" id="fill-risk" style="width:85%"></div></div>
        <div class="ind-val" id="val-risk" style="color:var(--red)">高</div>
      </div>
      <div class="ind-card">
        <div class="ind-label">SMA 温度</div>
        <div class="ind-bar"><div class="ind-fill fill-temp" id="fill-temp" style="width:5%"></div></div>
        <div class="ind-val" id="val-temp" style="color:var(--dim)">25°C</div>
      </div>
      <div class="ind-card">
        <div class="ind-label">电磁吸力</div>
        <div class="ind-bar"><div class="ind-fill fill-force" id="fill-force" style="width:0%"></div></div>
        <div class="ind-val" id="val-force" style="color:var(--dim)">0 kg</div>
      </div>
      <div class="phase-card">
        <div class="ph-num" id="ph-num">-</div>
        <div class="ph-name" id="ph-name">待命</div>
        <div class="ph-desc" id="ph-desc">点击阶段按钮或按"演示"开始</div>
      </div>
    </aside>
  </div>
  <div class="ctrls">
    <div class="phase-btns" id="phase-btns"></div>
    <div class="transport">
      <button id="btn-prev" title="上一阶段">&#9664;&#9664;</button>
      <button id="btn-play" title="自动演示">&#9654; 演示</button>
      <button id="btn-next" title="下一阶段">&#9654;&#124;</button>
      <button id="btn-reset" title="重置">&#8634; 重置</button>
    </div>
    <div class="sliders">
      <label>SMA温度 <input type="range" id="sma-slider" min="0" max="100" value="0"></label>
      <label>电磁吸力 <input type="range" id="em-slider" min="0" max="100" value="0"></label>
    </div>
  </div>
</div>

<script>
/* =============== 配置 =============== */
const C = {
  // 场景
  floorY: 660,
  // 机床
  machineLeft: 580, machineRight: 1020,
  machineTableTop: 248, machineTableH: 32,
  machineColW: 36, machineTopBarY: 95, machineTopBarH: 30,
  // 换模车
  cartStartX: 160, cartTargetX: 800,
  cartW: 380, cartFrameBot: 640, cartFrameTop: 390,
  wheelR: 18, wheelY: 648,
  // 剪叉
  scissorBotY: 385, scissorMinH: 8, scissorMaxH: 58,
  scissorArmSpan: 75,
  // 平台
  platW: 350, platH: 16,
  // SMA
  smaW: 28, smaMinH: 8, smaMaxH: 22,
  smaOffsetX: 130, // 距车中心偏移
  // 电磁吸盘
  emW: 56, emH: 10,
  // 模具
  moldW: 100, moldH: 70,
  moldTargetOffset: 180, // 模具推入距离
};

/* =============== 阶段定义 =============== */
const PHASES = [
  { name:'低位平移', desc:'保持低重心,平移至机床下方', dur:2200 },
  { name:'电动粗调升起', desc:'剪叉举升10-20cm接近目标', dur:2400 },
  { name:'SMA微调对位', desc:'通电加热SMA,微米级精准对齐', dur:2200 },
  { name:'电磁锁定', desc:'吸盘通电,与机床刚体连接', dur:1800 },
  { name:'稳定装卸模具', desc:'零晃动推入模具,安全紧固', dur:2600 },
  { name:'释放与退车', desc:'吸盘断电→SMA冷却→降下→退车', dur:3200 },
];

/* =============== 状态 =============== */
const S = {
  phase: -1, progress: 0, playing: false, autoAdv: true,
  cartX: C.cartStartX,
  scissorT: 0,   // 0~1
  smaT: 0,       // 0~1
  emT: 0,        // 0~1
  moldT: 0,      // 0~1
  riskT: 0.85,   // 0~1
  manualSMA: -1,  // -1=自动
  manualEM: -1,
};

/* =============== 缓动 =============== */
const ease = {
  inOut: t => t<.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2,
  out: t => 1-Math.pow(1-t,3),
  in: t => t*t*t,
  outBack: t => { const c=1.7; return 1+((t-1)**2)*((c+1)*(t-1)+c); },
};
const lerp = (a,b,t) => a+(b-a)*t;
const clamp = (v,lo,hi) => Math.max(lo,Math.min(hi,v));

/* =============== SVG 辅助 =============== */
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('scene');

function el(tag, attrs={}, parent=svg) {
  const e = document.createElementNS(NS, tag);
  for (const [k,v] of Object.entries(attrs)) e.setAttribute(k,v);
  parent.appendChild(e);
  return e;
}

function group(attrs={}, parent=svg) { return el('g', attrs, parent); }

/* =============== 构建 SVG 场景 =============== */
// -- defs --
const defs = el('defs');

// 辉光滤镜
function makeGlow(id, color, dev) {
  const f = el('filter', {id, x:'-50%', y:'-50%', width:'200%', height:'200%'}, defs);
  el('feGaussianBlur', {in:'SourceGraphic', stdDeviation:dev, result:'blur'}, f);
  el('feColorMatrix', {in:'blur', type:'matrix',
    values:`0 0 0 0 ${color[0]} 0 0 0 0 ${color[1]} 0 0 0 0 ${color[2]} 0 0 0 1 0`, result:'glow'}, f);
  const m = el('feMerge', {}, f);
  el('feMergeNode', {in:'glow'}, m);
  el('feMergeNode', {in:'SourceGraphic'}, m);
}
makeGlow('glow-amber', [0.94,0.66,0.19], 6);
makeGlow('glow-red', [0.91,0.30,0.24], 5);
makeGlow('glow-cyan', [0,0.83,1], 7);
makeGlow('glow-green', [0.18,0.8,0.44], 6);

// 钢材渐变
let g = el('linearGradient', {id:'grad-steel', x1:'0', y1:'0', x2:'0', y2:'1'}, defs);
el('stop', {offset:'0%', 'stop-color':'#3d4f65'}, g);
el('stop', {offset:'100%', 'stop-color':'#263040'}, g);

g = el('linearGradient', {id:'grad-machine', x1:'0', y1:'0', x2:'0', y2:'1'}, defs);
el('stop', {offset:'0%', 'stop-color':'#2a3a54'}, g);
el('stop', {offset:'100%', 'stop-color':'#1a2744'}, g);

g = el('linearGradient', {id:'grad-sma', x1:'0', y1:'0', x2:'0', y2:'1'}, defs);
el('stop', {offset:'0%', 'stop-color':'#5a6577', 'class':'sma-stop0'}, g);
el('stop', {offset:'100%', 'stop-color':'#4a5565', 'class':'sma-stop1'}, g);

g = el('linearGradient', {id:'grad-floor', x1:'0', y1:'0', x2:'0', y2:'1'}, defs);
el('stop', {offset:'0%', 'stop-color':'#111b2e'}, g);
el('stop', {offset:'100%', 'stop-color':'#0a0f1a'}, g);

// 网格图案
let pat = el('pattern', {id:'grid', width:40, height:40, patternUnits:'userSpaceOnUse'}, defs);
el('path', {d:'M 40 0 L 0 0 0 40', fill:'none', stroke:'#1a2540', 'stroke-width':0.5}, pat);

// -- 背景层 --
el('rect', {x:0, y:0, width:1400, height:750, fill:'url(#grid)', opacity:0.6});

// -- 地面 --
el('rect', {x:0, y:C.floorY, width:1400, height:90, fill:'url(#grad-floor)'});
el('line', {x1:0, y1:C.floorY, x2:1400, y2:C.floorY, stroke:'#2a3a54', 'stroke-width':2});

// -- 机床 --
const machineG = group({id:'machine'});
// 左立柱
el('rect', {x:C.machineLeft, y:C.machineTopBarY, width:C.machineColW,
  height:C.machineTableTop+C.machineTableH-C.machineTopBarY,
  fill:'url(#grad-machine)', rx:3}, machineG);
// 右立柱
el('rect', {x:C.machineRight-C.machineColW, y:C.machineTopBarY, width:C.machineColW,
  height:C.machineTableTop+C.machineTableH-C.machineTopBarY,
  fill:'url(#grad-machine)', rx:3}, machineG);
// 顶梁
el('rect', {x:C.machineLeft, y:C.machineTopBarY, width:C.machineRight-C.machineLeft,
  height:C.machineTopBarH, fill:'url(#grad-machine)', rx:3}, machineG);
// 工作台(加厚,重点标示)
el('rect', {x:C.machineLeft-10, y:C.machineTableTop, width:C.machineRight-C.machineLeft+20,
  height:C.machineTableH, fill:'#2e4058', rx:4, stroke:'#3d5570', 'stroke-width':1.5}, machineG);
// 工作台铁磁性标注线
el('line', {x1:C.machineLeft+20, y1:C.machineTableTop+C.machineTableH+6,
  x2:C.machineRight-20, y2:C.machineTableTop+C.machineTableH+6,
  stroke:'#4a6080', 'stroke-width':1, 'stroke-dasharray':'4 3'}, machineG);
// 机床标签
const mLabel = el('text', {x:(C.machineLeft+C.machineRight)/2, y:C.machineTopBarY+C.machineTopBarH/2+5,
  'text-anchor':'middle', fill:'#5a7a9a', 'font-family':'Rajdhani', 'font-size':'16', 'font-weight':'500',
  'letter-spacing':'3'}, machineG);
mLabel.textContent = '冲床 / 注塑机';

// 模具安装位(机床工作台上的凹槽)
el('rect', {x:(C.machineLeft+C.machineRight)/2-C.moldW/2-5, y:C.machineTableTop+4,
  width:C.moldW+10, height:C.machineTableH-8, fill:'#1a2744', rx:3}, machineG);

// -- 换模车组 --
const cartG = group({id:'cart'});

// 车架
const cartFrame = el('rect', {x:-C.cartW/2, y:C.cartFrameTop, width:C.cartW,
  height:C.cartFrameBot-C.cartFrameTop, fill:'url(#grad-steel)', rx:6, stroke:'#3d5570', 'stroke-width':1}, cartG);
// 车架加强筋
for (let i=-2; i<=2; i++) {
  el('line', {x1:i*70, y1:C.cartFrameTop+8, x2:i*70, y2:C.cartFrameBot-8,
    stroke:'#2a3a50', 'stroke-width':2}, cartG);
}
// 车架底板
el('rect', {x:-C.cartW/2+10, y:C.cartFrameBot-8, width:C.cartW-20, height:8, fill:'#3d4f65', rx:2}, cartG);

// 轮子
const wheelG = group({}, cartG);
const w1 = el('circle', {cx:-C.cartW/2+55, cy:C.wheelY, r:C.wheelR, fill:'#1a2540', stroke:'#3d5570', 'stroke-width':2}, wheelG);
const w2 = el('circle', {cx:C.cartW/2-55, cy:C.wheelY, r:C.wheelR, fill:'#1a2540', stroke:'#3d5570', 'stroke-width':2}, wheelG);
// 轮辐
function addSpokes(cx, cy, r, g) {
  for (let a=0; a<4; a++) {
    const rad = a*Math.PI/4;
    el('line', {x1:cx, y1:cy, x2:cx+r*0.7*Math.cos(rad), y2:cy+r*0.7*Math.sin(rad),
      stroke:'#2a3a50', 'stroke-width':2}, g);
  }
}
addSpokes(-C.cartW/2+55, C.wheelY, C.wheelR, wheelG);
addSpokes(C.cartW/2-55, C.wheelY, C.wheelR, wheelG);

// 剪叉机构
const scissorG = group({id:'scissor'}, cartG);
const scissorArm1 = el('line', {x1:0, y1:0, x2:0, y2:0,
  stroke:'#8a7a50', 'stroke-width':8, 'stroke-linecap':'round'}, scissorG);
const scissorArm2 = el('line', {x1:0, y1:0, x2:0, y2:0,
  stroke:'#8a7a50', 'stroke-width':8, 'stroke-linecap':'round'}, scissorG);
const scissorPivot = el('circle', {cx:0, cy:0, r:6, fill:'#a09060', stroke:'#6a5a30', 'stroke-width':1.5}, scissorG);
// 液压缸
const scissorCyl = el('rect', {x:0,y:0,width:12,height:30,fill:'#7a6a40',rx:3,stroke:'#5a4a20','stroke-width':1}, scissorG);

// 平台
const platformG = group({id:'platform'}, cartG);
const platRect = el('rect', {x:-C.platW/2, y:0, width:C.platW, height:C.platH,
  fill:'#3d5068', rx:3, stroke:'#4a6580', 'stroke-width':1}, platformG);
// 平台防滑纹
for (let i=-6; i<=6; i++) {
  el('line', {x1:i*24, y1:3, x2:i*24, y2:C.platH-3,
    stroke:'#4a6070', 'stroke-width':1, opacity:0.5}, platformG);
}

// SMA 致动器
const smaG = group({id:'sma-group'}, cartG);
const smaLeftG = group({}, smaG);
const smaRightG = group({}, smaG);
function buildSMA(parent, offsetX) {
  const body = el('rect', {x:offsetX-C.smaW/2, y:0, width:C.smaW, height:C.smaMinH,
    fill:'url(#grad-sma)', rx:3, stroke:'#5a6577', 'stroke-width':1.2}, parent);
  // SMA 线圈纹
  const coil = el('path', {d:'', fill:'none', stroke:'#7a8a9a', 'stroke-width':1.5, opacity:0.6}, parent);
  return {body, coil, offsetX};
}
const smaL = buildSMA(smaLeftG, -C.smaOffsetX);
const smaR = buildSMA(smaRightG, C.smaOffsetX);

// SMA 热粒子容器
const smaParticlesG = group({id:'sma-particles'}, cartG);

// 电磁吸盘
const emG = group({id:'em-group'}, cartG);
const emLeftG = group({}, emG);
const emRightG = group({}, emG);
function buildEM(parent, offsetX) {
  const body = el('rect', {x:offsetX-C.emW/2, y:0, width:C.emW, height:C.emH,
    fill:'#3d4f65', rx:2, stroke:'#4a6080', 'stroke-width':1}, parent);
  // 线圈纹
  const coilPath = el('path', {d:'', fill:'none', stroke:'#5a7a9a', 'stroke-width':1, opacity:0.5}, parent);
  return {body, coilPath, offsetX};
}
const emL = buildEM(emLeftG, -C.smaOffsetX);
const emR = buildEM(emRightG, C.smaOffsetX);

// 磁力线容器
const fieldLinesG = group({id:'field-lines'}, cartG);

// 模具
const moldG = group({id:'mold'}, cartG);
const moldRect = el('rect', {x:-C.moldW/2, y:-C.moldH, width:C.moldW, height:C.moldH,
  fill:'#6a4a8a', rx:4, stroke:'#8a6aaa', 'stroke-width':1.5}, moldG);
// 模具上的安装孔
el('circle', {cx:-20, cy:-C.moldH/2, r:5, fill:'#4a2a6a', stroke:'#6a4a8a', 'stroke-width':1}, moldG);
el('circle', {cx:20, cy:-C.moldH/2, r:5, fill:'#4a2a6a', stroke:'#6a4a8a', 'stroke-width':1}, moldG);
// 模具标签
const moldLabel = el('text', {x:0, y:-C.moldH/2+5, 'text-anchor':'middle',
  fill:'#b898d8', 'font-family':'Rajdhani', 'font-size':'14', 'font-weight':'600'}, moldG);
moldLabel.textContent = '模具';

// 刚体连接高亮
const rigidG = group({id:'rigid-highlight', opacity:0}, cartG);

// 标注层
const annotG = group({id:'annotations'});

/* =============== 粒子系统 =============== */
const particles = [];
function spawnParticle(x, y, color) {
  const p = {x, y, vx:(Math.random()-0.5)*0.8, vy:-Math.random()*1.5-0.5,
    life:1, decay:0.01+Math.random()*0.015, r:2+Math.random()*2, color};
  particles.push(p);
  const e = el('circle', {cx:x, cy:y, r:p.r, fill:color, opacity:0.8}, smaParticlesG);
  p.el = e;
}
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) { p.el.remove(); particles.splice(i,1); continue; }
    p.el.setAttribute('cx', p.x);
    p.el.setAttribute('cy', p.y);
    p.el.setAttribute('opacity', p.life*0.7);
    p.el.setAttribute('r', p.r*p.life);
  }
}

/* =============== 磁力线 =============== */
const fieldLineEls = [];
function buildFieldLines() {
  const positions = [-C.smaOffsetX, C.smaOffsetX];
  positions.forEach(ox => {
    for (let i=-2; i<=2; i++) {
      const path = el('path', {d:'', fill:'none', stroke:'#00d4ff', 'stroke-width':1.5,
        'stroke-dasharray':'5 5', opacity:0, class:'field-line'}, fieldLinesG);
      fieldLineEls.push({path, ox, offset:i*8});
    }
  });
}
buildFieldLines();

/* =============== 刚体高亮 =============== */
function buildRigidHighlight() {
  // 左侧连接弧
  el('path', {d:`M${-C.smaOffsetX-C.emW/2-5},${-2} L${-C.smaOffsetX-C.emW/2-5},${-C.machineTableTop+C.machineTableH+2+280}`,
    fill:'none', stroke:'#2ecc71', 'stroke-width':2.5, 'stroke-dasharray':'8 4', opacity:0.8}, rigidG);
  // 右侧连接弧
  el('path', {d:`M${C.smaOffsetX+C.emW/2+5},${-2} L${C.smaOffsetX+C.emW/2+5},${-C.machineTableTop+C.machineTableH+2+280}`,
    fill:'none', stroke:'#2ecc71', 'stroke-width':2.5, 'stroke-dasharray':'8 4', opacity:0.8}, rigidG);
  // 标签
  const t = el('text', {x:0, y:-C.machineTableTop+C.machineTableH+280+20,
    'text-anchor':'middle', fill:'#2ecc71', 'font-family':'Rajdhani',
    'font-size':'16', 'font-weight':'700', 'letter-spacing':'2'}, rigidG);
  t.textContent = '超系统刚体连接';
}
buildRigidHighlight();

/* =============== 标注 =============== */
let currentAnnotEls = [];
function clearAnnotations() {
  currentAnnotEls.forEach(e => e.remove());
  currentAnnotEls = [];
}
function addAnnot(x, y, text, color='#c8d6e5', size=13, anchor='middle') {
  const t = el('text', {x, y, 'text-anchor':anchor, fill:color,
    'font-family':'Share Tech Mono', 'font-size':size}, annotG);
  t.textContent = text;
  currentAnnotEls.push(t);
  return t;
}
function addMeasureLine(x1, y1, x2, y2, label, color='#f0a830') {
  const g2 = group({}, annotG);
  el('line', {x1, y1, x2, y2, stroke:color, 'stroke-width':1.5, 'stroke-dasharray':'3 2'}, g2);
  // 端点标记
  el('line', {x1:x1-4, y1:y1, x2:x1+4, y2:y1, stroke:color, 'stroke-width':1.5}, g2);
  el('line', {x1:x2-4, y1:y2, x2:x2+4, y2:y2, stroke:color, 'stroke-width':1.5}, g2);
  const mx = (x1+x2)/2, my = (y1+y2)/2;
  const t = el('text', {x:mx+(x1===x2?12:0), y:my+(y1===y2?-6:0), 'text-anchor':'middle', fill:color,
    'font-family':'Share Tech Mono', 'font-size':'11'}, g2);
  t.textContent = label;
  currentAnnotEls.push(g2);
}

/* =============== 阶段按钮 =============== */
const btnsDiv = document.getElementById('phase-btns');
PHASES.forEach((p, i) => {
  const btn = document.createElement('button');
  btn.textContent = `${i+1}. ${p.name}`;
  btn.dataset.phase = i;
  btn.addEventListener('click', () => jumpToPhase(i));
  btnsDiv.appendChild(btn);
});

/* =============== 渲染 =============== */
function render() {
  const cx = S.cartX;
  const scissorH = C.scissorMinH + (C.scissorMaxH - C.scissorMinH) * S.scissorT;
  const smaH = C.smaMinH + (C.smaMaxH - C.smaMinH) * S.smaT;
  const emActive = S.emT > 0.3;

  // 车组平移
  cartG.setAttribute('transform', `translate(${cx}, 0)`);

  // 剪叉臂
  const sTop = C.scissorBotY - scissorH;
  const armSpan = C.scissorArmSpan;
  scissorArm1.setAttribute('x1', cx - armSpan);
  scissorArm1.setAttribute('y1', C.scissorBotY);
  scissorArm1.setAttribute('x2', cx + armSpan);
  scissorArm1.setAttribute('y2', sTop);
  scissorArm2.setAttribute('x1', cx + armSpan);
  scissorArm2.setAttribute('y1', C.scissorBotY);
  scissorArm2.setAttribute('x2', cx - armSpan);
  scissorArm2.setAttribute('y2', sTop);
  scissorPivot.setAttribute('cx', cx);
  scissorPivot.setAttribute('cy', (C.scissorBotY + sTop) / 2);

  // 剪叉颜色(激活时金色)
  const scissorColor = S.scissorT > 0.01 ? `rgb(${Math.round(138+77*S.scissorT)},${Math.round(122+50*S.scissorT)},${Math.round(80-20*S.scissorT)})` : '#8a7a50';
  scissorArm1.setAttribute('stroke', scissorColor);
  scissorArm2.setAttribute('stroke', scissorColor);
  scissorG.setAttribute('filter', S.scissorT > 0.5 ? 'url(#glow-amber)' : '');

  // 液压缸位置
  const cylX = cx + 20;
  const cylY = (C.scissorBotY + sTop) / 2 - 15;
  scissorCyl.setAttribute('x', cylX);
  scissorCyl.setAttribute('y', cylY);
  scissorCyl.setAttribute('height', Math.max(10, scissorH * 0.5));

  // 平台位置
  const platY = sTop - C.platH;
  platRect.setAttribute('y', platY);

  // SMA 位置
  const smaBaseY = platY;
  updateSMAElement(smaL, smaBaseY, smaH);
  updateSMAElement(smaR, smaBaseY, smaH);

  // SMA 颜色
  const smaHeat = S.smaT;
  const smaR_c = Math.round(90 + 141 * smaHeat);
  const smaG_c = Math.round(101 - 55 * smaHeat);
  const smaB_c = Math.round(119 - 83 * smaHeat);
  const smaColor = `rgb(${smaR_c},${smaG_c},${smaB_c})`;
  [smaL, smaR].forEach(s => {
    s.body.setAttribute('fill', smaColor);
    s.body.setAttribute('stroke', smaHeat > 0.3 ? `rgba(231,76,60,${smaHeat})` : '#5a6577');
  });
  smaG.setAttribute('filter', smaHeat > 0.3 ? 'url(#glow-red)' : '');

  // SMA 粒子
  if (smaHeat > 0.2 && Math.random() < smaHeat * 0.3) {
    const side = Math.random() > 0.5 ? 1 : -1;
    spawnParticle(cx + side * C.smaOffsetX + (Math.random()-0.5)*C.smaW,
      smaBaseY - smaH * Math.random(),
      smaHeat > 0.6 ? '#f39c12' : '#e74c3c');
  }

  // 电磁吸盘位置
  const emBaseY = smaBaseY - smaH;
  updateEMElement(emL, emBaseY);
  updateEMElement(emR, emBaseY);

  // 电磁吸盘颜色
  const emColor = emActive ? `rgba(0,212,255,${0.4+0.6*S.emT})` : '#3d4f65';
  const emStroke = emActive ? '#00d4ff' : '#4a6080';
  [emL, emR].forEach(e => {
    e.body.setAttribute('fill', emColor);
    e.body.setAttribute('stroke', emStroke);
  });
  emG.setAttribute('filter', emActive ? 'url(#glow-cyan)' : '');

  // 磁力线
  const emTopY = emBaseY - C.emH;
  const tableBotY = C.machineTableTop + C.machineTableH;
  fieldLineEls.forEach(fl => {
    if (emActive && Math.abs(cx + fl.ox - (C.machineLeft+C.machineRight)/2) < 250) {
      const fx = cx + fl.ox + fl.offset;
      const fy1 = emTopY;
      const fy2 = tableBotY;
      fl.path.setAttribute('d', `M${fx},${fy1} C${fx+fl.offset*0.5},${(fy1+fy2)/2} ${fx-fl.offset*0.5},${(fy1+fy2)/2} ${fx},${fy2}`);
      fl.path.setAttribute('opacity', 0.3 + 0.5 * S.emT);
    } else {
      fl.path.setAttribute('opacity', 0);
    }
  });

  // 刚体高亮
  const rigidOpacity = S.emT > 0.5 ? clamp((S.emT - 0.5) * 2, 0, 1) : 0;
  rigidG.setAttribute('opacity', rigidOpacity);
  if (rigidOpacity > 0) {
    rigidG.setAttribute('filter', 'url(#glow-green)');
  }

  // 模具位置
  const moldBaseY = platY - 2;
  const moldOff = S.moldT * C.moldTargetOffset;
  moldG.setAttribute('transform', `translate(${cx + moldOff}, ${moldBaseY})`);

  // 标注更新
  clearAnnotations();
  if (S.phase >= 0) {
    addAnnot(cx, C.cartFrameBot + 28, `换模车`, '#7a8a9a', 12);
  }
  if (S.scissorT > 0.05) {
    const mX = cx + C.scissorArmSpan + 30;
    addMeasureLine(mX, C.scissorBotY, mX, sTop,
      `${Math.round(10 + 10 * S.scissorT)}cm`, '#f0a830');
  }
  if (S.smaT > 0.1) {
    addAnnot(cx - C.smaOffsetX - C.smaW/2 - 8, smaBaseY - smaH/2 + 4,
      `±5mm`, '#e74c3c', 11, 'end');
  }
  if (emActive) {
    addAnnot(cx + C.smaOffsetX + C.emW/2 + 10, emBaseY - C.emH/2 + 4,
      `>200kg×4`, '#00d4ff', 11, 'start');
  }
  if (S.moldT > 0.1 && S.moldT < 0.9) {
    addAnnot(cx + moldOff, moldBaseY - C.moldH - 12,
      `零晃动推入`, '#2ecc71', 13);
  }

  // 侧栏指示器
  document.getElementById('fill-risk').style.width = (S.riskT * 100) + '%';
  const riskLabel = S.riskT > 0.6 ? '高' : S.riskT > 0.3 ? '中' : '极低';
  const riskColor = S.riskT > 0.6 ? 'var(--red)' : S.riskT > 0.3 ? 'var(--amber)' : 'var(--green)';
  document.getElementById('val-risk').textContent = riskLabel;
  document.getElementById('val-risk').style.color = riskColor;

  const tempVal = Math.round(25 + S.smaT * 75);
  document.getElementById('fill-temp').style.width = (S.smaT * 100) + '%';
  document.getElementById('val-temp').textContent = tempVal + '°C';
  document.getElementById('val-temp').style.color = S.smaT > 0.5 ? 'var(--red)' : S.smaT > 0.2 ? 'var(--amber)' : 'var(--dim)';

  const forceVal = Math.round(S.emT * 800);
  document.getElementById('fill-force').style.width = (S.emT * 100) + '%';
  document.getElementById('val-force').textContent = forceVal + ' kg';
  document.getElementById('val-force').style.color = S.emT > 0.3 ? 'var(--cyan)' : 'var(--dim)';

  document.getElementById('ph-num').textContent = S.phase >= 0 ? (S.phase + 1) : '-';
  document.getElementById('ph-name').textContent = S.phase >= 0 ? PHASES[S.phase].name : '待命';
  document.getElementById('ph-desc').textContent = S.phase >= 0 ? PHASES[S.phase].desc : '点击阶段按钮或按"演示"开始';

  // 阶段按钮高亮
  document.querySelectorAll('.phase-btns button').forEach(b => {
    b.classList.toggle('active', parseInt(b.dataset.phase) === S.phase);
  });
}

function updateSMAElement(s, baseY, h) {
  s.body.setAttribute('x', s.offsetX - C.smaW/2);
  s.body.setAttribute('y', baseY - h);
  s.body.setAttribute('height', h);
  // 线圈纹
  const segments = 5;
  let d = `M${s.offsetX - C.smaW/2 + 3},${baseY - h + 3}`;
  for (let i = 1; i <= segments; i++) {
    const yy = baseY - h + 3 + (h - 6) * i / segments;
    const xOff = (i % 2 === 0) ? -C.smaW/2 + 3 : C.smaW/2 - 3;
    d += ` L${s.offsetX + xOff},${yy}`;
  }
  s.coil.setAttribute('d', d);
}

function updateEMElement(e, baseY) {
  e.body.setAttribute('x', e.offsetX - C.emW/2);
  e.body.setAttribute('y', baseY - C.emH);
  // 线圈纹
  const y = baseY - C.emH + C.emH/2;
  let d = `M${e.offsetX - C.emW/2 + 4},${y}`;
  for (let i = 0; i < 6; i++) {
    const xx = e.offsetX - C.emW/2 + 4 + (C.emW - 8) * (i + 0.5) / 6;
    d += ` L${xx},${y + (i%2===0 ? -2 : 2)}`;
  }
  e.coilPath.setAttribute('d', d);
}

/* =============== 阶段逻辑 =============== */
function updatePhase(dt) {
  if (S.phase < 0 || !S.playing) return;
  const ph = PHASES[S.phase];
  S.progress += dt / ph.dur;
  if (S.progress >= 1) {
    S.progress = 1;
    applyPhaseState(S.phase, 1);
    // 自动前进
    if (S.autoAdv && S.phase < PHASES.length - 1) {
      setTimeout(() => {
        if (S.playing) jumpToPhase(S.phase + 1);
      }, 400);
    } else {
      S.playing = false;
      updatePlayBtn();
    }
    return;
  }
  applyPhaseState(S.phase, S.progress);
}

function applyPhaseState(phase, p) {
  // 根据阶段和进度更新状态
  switch(phase) {
    case 0: // 低位平移
      S.cartX = lerp(C.cartStartX, C.cartTargetX, ease.inOut(p));
      S.scissorT = 0; S.smaT = 0; S.emT = 0; S.moldT = 0;
      S.riskT = lerp(0.85, 0.7, p); // 低重心,风险不高
      break;
    case 1: // 电动粗调升起
      S.cartX = C.cartTargetX;
      S.scissorT = ease.out(p);
      S.smaT = 0; S.emT = 0; S.moldT = 0;
      S.riskT = lerp(0.7, 0.55, p); // 升起中风险略增
      break;
    case 2: // SMA微调
      S.cartX = C.cartTargetX;
      S.scissorT = 1;
      S.smaT = ease.inOut(p);
      S.emT = 0; S.moldT = 0;
      S.riskT = lerp(0.55, 0.45, p);
      break;
    case 3: // 电磁锁定
      S.cartX = C.cartTargetX;
      S.scissorT = 1; S.smaT = 1;
      S.emT = ease.in(clamp(p * 1.5, 0, 1)); // 快速锁定
      S.moldT = 0;
      S.riskT = lerp(0.45, 0.05, ease.out(p)); // 风险骤降!
      break;
    case 4: // 模具推入
      S.cartX = C.cartTargetX;
      S.scissorT = 1; S.smaT = 1; S.emT = 1;
      S.moldT = ease.inOut(p);
      S.riskT = 0.05; // 刚体连接,极低风险
      break;
    case 5: // 释放退车
      S.cartX = C.cartTargetX;
      if (p < 0.25) {
        // 释放吸盘
        const p1 = p / 0.25;
        S.emT = 1 - ease.in(p1);
        S.scissorT = 1; S.smaT = 1; S.moldT = 1;
        S.riskT = lerp(0.05, 0.4, p1);
      } else if (p < 0.5) {
        // SMA冷却
        const p2 = (p - 0.25) / 0.25;
        S.emT = 0;
        S.smaT = 1 - ease.out(p2);
        S.scissorT = 1; S.moldT = 1;
        S.riskT = lerp(0.4, 0.55, p2);
      } else if (p < 0.75) {
        // 降下
        const p3 = (p - 0.5) / 0.25;
        S.emT = 0; S.smaT = 0;
        S.scissorT = 1 - ease.inOut(p3);
        S.moldT = 1;
        S.riskT = lerp(0.55, 0.7, p3);
      } else {
        // 退车
        const p4 = (p - 0.75) / 0.25;
        S.emT = 0; S.smaT = 0; S.scissorT = 0;
        S.moldT = 1;
        S.cartX = lerp(C.cartTargetX, C.cartStartX, ease.inOut(p4));
        S.riskT = lerp(0.7, 0.85, p4);
      }
      break;
  }
}

/* =============== 控制 =============== */
function jumpToPhase(phase) {
  S.phase = clamp(phase, 0, PHASES.length - 1);
  S.progress = 0;
  S.playing = true;
  // 先设置前面阶段完成的状态
  for (let i = 0; i < S.phase; i++) applyPhaseState(i, 1);
  updatePlayBtn();
}

function resetAll() {
  S.phase = -1; S.progress = 0; S.playing = false;
  S.cartX = C.cartStartX; S.scissorT = 0; S.smaT = 0;
  S.emT = 0; S.moldT = 0; S.riskT = 0.85;
  particles.forEach(p => p.el.remove());
  particles.length = 0;
  document.getElementById('sma-slider').value = 0;
  document.getElementById('em-slider').value = 0;
  updatePlayBtn();
}

function updatePlayBtn() {
  const btn = document.getElementById('btn-play');
  btn.textContent = S.playing ? '⏸ 暂停' : '▶ 演示';
  btn.classList.toggle('playing', S.playing);
}

document.getElementById('btn-play').addEventListener('click', () => {
  if (S.phase < 0) jumpToPhase(0);
  else { S.playing = !S.playing; updatePlayBtn(); }
});
document.getElementById('btn-prev').addEventListener('click', () => {
  if (S.phase > 0) jumpToPhase(S.phase - 1);
});
document.getElementById('btn-next').addEventListener('click', () => {
  if (S.phase < PHASES.length - 1) jumpToPhase(S.phase + 1);
});
document.getElementById('btn-reset').addEventListener('click', resetAll);

// SMA 手动滑块
document.getElementById('sma-slider').addEventListener('input', e => {
  const v = parseInt(e.target.value) / 100;
  S.smaT = v;
  S.manualSMA = v;
});
// EM 手动滑块
document.getElementById('em-slider').addEventListener('input', e => {
  const v = parseInt(e.target.value) / 100;
  S.emT = v;
  S.manualEM = v;
});

// 键盘控制
document.addEventListener('keydown', e => {
  if (e.code === 'Space') { e.preventDefault(); document.getElementById('btn-play').click(); }
  if (e.code === 'ArrowLeft') document.getElementById('btn-prev').click();
  if (e.code === 'ArrowRight') document.getElementById('btn-next').click();
  if (e.code === 'KeyR') resetAll();
});

/* =============== 主循环 =============== */
let lastTime = 0;
function loop(time) {
  const dt = lastTime ? time - lastTime : 16;
  lastTime = time;
  updatePhase(dt);
  updateParticles(dt);
  render();
  requestAnimationFrame(loop);
}

// 初始渲染
render();
requestAnimationFrame(loop);
</script>
</body>
</html>

实现说明

这个动画完整呈现了二级升降换模车从"低位平移"到"释放退车"的 6 阶段工作循环,核心围绕 TRIZ 的最终理想解 (IFR) 思想进行视觉设计:

视觉引导策略

  1. 颜色编码:三大子系统各有专属色——剪叉机构为琥珀金、SMA 致动器为热力红、电磁吸盘为电光青,激活时自动叠加辉光滤镜,一眼可辨。
  2. 动态标注:每个阶段自动弹出关键参数(粗调行程 10-20cm、微调 ±5mm、吸力 >200kg×4),在动作发生处就地显示。
  3. 刚体连接高亮:当电磁吸盘锁定时,绿色虚线边框包裹换模车与机床工作台,配合"超系统刚体连接"标签,直观表达 IFR 中"利用机床工作台自身铁磁性"这一资源复用。

交互设计

  • 阶段按钮:可直接跳转到任意阶段观察
  • 手动滑块:SMA 温度和电磁吸力可手动调节,体验微调精度与锁定力
  • 键盘快捷键:空格播放/暂停,左右箭头切换阶段,R 重置
  • 侧栏仪表:实时显示倾覆风险、SMA 温度、电磁吸力三项关键指标

IFR 体现

  • 倾覆风险仪表在电磁锁定阶段从"高"骤降至"极低",视觉化地展示了矛盾消除
  • 模具推入时标注"零晃动推入",强调刚体连接带来的理想结果
  • 动画全程不展示"旧方案对比",只展示理想解的运作——这正是 IFR 的核心理念
积分规则:第一轮对话扣减6分,后续每轮扣4分