分享图
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;400;600;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root {
  --bg:#050a14;--grid:#0a1428;--pole:#1e3050;--bone:#00c8a0;--bone-hi:#00ffcc;
  --spring-idle:#1a5a3a;--spring-act:#00ff88;--sma-cold:#3a2820;--sma-hot:#ff6b2b;
  --canopy:#0a3555;--annot:#6090c0;--accent:#ffd700;--text:#c0d0e0;--dim:#3a5570;
  --card:rgba(8,16,30,0.85);--border:rgba(0,200,160,0.15);
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:'Rajdhani',sans-serif;
  overflow:hidden;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;
  user-select:none}
.title-bar{position:fixed;top:0;left:0;right:0;padding:18px 32px;z-index:10;
  background:linear-gradient(180deg,rgba(5,10,20,0.95),transparent);display:flex;align-items:baseline;gap:18px}
.title-bar h1{font-size:22px;font-weight:700;letter-spacing:2px;color:var(--bone-hi);text-transform:uppercase}
.title-bar p{font-size:13px;color:var(--dim);font-family:'Share Tech Mono',monospace;letter-spacing:1px}

.svg-wrap{flex:1;display:flex;align-items:center;justify-content:center;width:100%;padding:0 20px}
svg#mech{width:100%;max-width:1200px;height:auto;max-height:80vh}

/* 右侧参数面板 */
.param-panel{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:10;
  display:flex;flex-direction:column;gap:10px;width:180px}
.param-card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;
  backdrop-filter:blur(8px);transition:border-color .3s}
.param-card.active{border-color:var(--spring-act)}
.param-card.active-sma{border-color:var(--sma-hot)}
.param-label{font-size:10px;color:var(--dim);font-family:'Share Tech Mono',monospace;letter-spacing:1px;text-transform:uppercase}
.param-value{font-size:22px;font-weight:700;color:var(--text);margin-top:2px;font-family:'Share Tech Mono',monospace}
.param-unit{font-size:11px;color:var(--dim);margin-left:4px}

/* 左侧阶段指示器 */
.phase-timeline{position:fixed;left:28px;top:50%;transform:translateY(-50%);z-index:10;
  display:flex;flex-direction:column;gap:0;align-items:flex-start}
.phase-item{display:flex;align-items:center;gap:10px;padding:8px 0;opacity:0.35;transition:all .4s}
.phase-item.active{opacity:1}
.phase-dot{width:10px;height:10px;border-radius:50%;border:2px solid var(--dim);transition:all .4s;flex-shrink:0}
.phase-item.active .phase-dot{border-color:var(--bone-hi);background:var(--bone-hi);
  box-shadow:0 0 10px var(--bone-hi)}
.phase-item.done .phase-dot{border-color:var(--spring-act);background:var(--spring-act)}
.phase-item.active-sma .phase-dot{border-color:var(--sma-hot);background:var(--sma-hot);
  box-shadow:0 0 10px var(--sma-hot)}
.phase-line{width:2px;height:20px;background:var(--dim);margin-left:4px;opacity:0.3;transition:all .4s}
.phase-line.lit{background:var(--spring-act);opacity:0.7}
.phase-name{font-size:12px;font-family:'Share Tech Mono',monospace;color:var(--text);letter-spacing:0.5px;white-space:nowrap}

/* 底部控制栏 */
.controls{position:fixed;bottom:0;left:0;right:0;z-index:10;
  background:linear-gradient(0deg,rgba(5,10,20,0.95),transparent);padding:16px 32px 22px;
  display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap}
.btn{padding:8px 22px;border:1px solid var(--border);background:var(--card);color:var(--text);
  border-radius:6px;font-family:'Rajdhani',sans-serif;font-size:14px;font-weight:600;
  cursor:pointer;transition:all .25s;letter-spacing:1px;backdrop-filter:blur(6px)}
.btn:hover{border-color:var(--bone-hi);color:var(--bone-hi);box-shadow:0 0 12px rgba(0,255,204,0.15)}
.btn:active{transform:scale(0.96)}
.btn.primary{border-color:var(--bone);background:rgba(0,200,160,0.12);color:var(--bone-hi)}
.btn.primary:hover{background:rgba(0,200,160,0.22);box-shadow:0 0 16px rgba(0,255,204,0.2)}
.slider-wrap{display:flex;align-items:center;gap:10px}
.slider-wrap label{font-size:11px;color:var(--dim);font-family:'Share Tech Mono',monospace;white-space:nowrap}
input[type=range]{-webkit-appearance:none;width:180px;height:4px;background:var(--dim);
  border-radius:2px;outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;
  border-radius:50%;background:var(--bone-hi);border:2px solid var(--bg);cursor:pointer;
  box-shadow:0 0 6px var(--bone-hi)}
.speed-label{font-size:11px;color:var(--dim);font-family:'Share Tech Mono',monospace;min-width:28px;text-align:center}

/* 响应式 */
@media(max-width:900px){
  .param-panel{right:8px;width:140px}
  .param-value{font-size:17px}
  .phase-timeline{left:10px}
  .phase-name{font-size:10px}
}
@media(max-width:600px){
  .param-panel,.phase-timeline{display:none}
  .title-bar h1{font-size:16px}
}
</style>
</head>
<body>

<div class="title-bar">
  <h1>IFR 无源两段自动折叠</h1>
  <p>利用现有资源破除矛盾的理想解</p>
</div>

<div class="svg-wrap">
<svg id="mech" viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- 网格 -->
    <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
      <path d="M40 0L0 0 0 40" fill="none" stroke="#0b1528" stroke-width="0.5"/>
    </pattern>
    <pattern id="gridFine" width="8" height="8" patternUnits="userSpaceOnUse">
      <path d="M8 0L0 0 0 8" fill="none" stroke="#08101e" stroke-width="0.3"/>
    </pattern>
    <!-- 发光滤镜 -->
    <filter id="glowGreen" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
      <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
    </filter>
    <filter id="glowOrange" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur in="SourceGraphic" stdDeviation="7" result="b"/>
      <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
    </filter>
    <filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="b"/>
      <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
    </filter>
    <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur in="SourceGraphic" stdDeviation="12"/>
    </filter>
    <!-- 箭头标记 -->
    <marker id="arrGreen" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M0,1 L9,5 L0,9Z" fill="#00ff88"/>
    </marker>
    <marker id="arrOrange" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M0,1 L9,5 L0,9Z" fill="#ff6b2b"/>
    </marker>
    <!-- SMA热辐射渐变 -->
    <radialGradient id="smaHeatGrad" cx="50%" cy="50%" r="50%">
      <stop offset="0%" stop-color="#ff6b2b" stop-opacity="0.4"/>
      <stop offset="100%" stop-color="#ff6b2b" stop-opacity="0"/>
    </radialGradient>
  </defs>

  <!-- 背景层 -->
  <rect width="1200" height="800" fill="url(#gridFine)"/>
  <rect width="1200" height="800" fill="url(#grid)"/>
  <rect width="1200" height="800" fill="rgba(5,10,20,0.5)"/>

  <!-- 聚光效果 -->
  <ellipse cx="420" cy="340" rx="280" ry="200" fill="rgba(0,200,160,0.015)"/>

  <!-- ===== 机构图层 ===== -->
  <g id="layerMech">
    <!-- 伞柄 -->
    <rect id="handle" x="336" y="600" width="28" height="48" rx="6" fill="#141e30" stroke="#1e3050" stroke-width="1.5"/>
    <rect x="340" y="608" width="20" height="6" rx="2" fill="#1e3050"/>

    <!-- 主杆 -->
    <line id="pole" x1="350" y1="600" x2="350" y2="220" stroke="#1a2d4a" stroke-width="6" stroke-linecap="round"/>
    <line x1="350" y1="600" x2="350" y2="220" stroke="#223a5a" stroke-width="3" stroke-linecap="round"/>

    <!-- 微动开关 -->
    <g id="microSwitch">
      <rect x="340" y="580" width="20" height="10" rx="2" fill="#1a1a2a" stroke="#3a3a5a" stroke-width="1"/>
      <rect id="switchBtn" x="344" y="582" width="5" height="6" rx="1" fill="#5a5a7a"/>
    </g>

    <!-- 滑套(Runner) -->
    <g id="runner">
      <rect x="340" y="260" width="20" height="16" rx="3" fill="#12203a" stroke="#2a4a6a" stroke-width="1.5"/>
      <line x1="343" y1="264" x2="357" y2="264" stroke="#3a6a8a" stroke-width="0.8"/>
      <line x1="343" y1="268" x2="357" y2="268" stroke="#3a6a8a" stroke-width="0.8"/>
    </g>

    <!-- 撑骨(Stretcher) -->
    <line id="stretcher" x1="350" y1="268" x2="420" y2="290" stroke="#1a3a5a" stroke-width="2.5" stroke-linecap="round"/>

    <!-- 伞面(Canopy) -->
    <path id="canopy" d="" fill="rgba(10,53,85,0.25)" stroke="rgba(0,200,160,0.12)" stroke-width="1"/>

    <!-- 骨节轨迹 -->
    <path id="traceMid" d="" fill="none" stroke="rgba(0,255,136,0.15)" stroke-width="1" stroke-dasharray="3,4"/>
    <path id="traceDist" d="" fill="none" stroke="rgba(255,107,43,0.12)" stroke-width="1" stroke-dasharray="3,4"/>

    <!-- 近端骨 -->
    <line id="boneProx" x1="350" y1="220" x2="510" y2="150" stroke="#00c8a0" stroke-width="5" stroke-linecap="round"/>
    <line id="boneProxHi" x1="350" y1="220" x2="510" y2="150" stroke="#00ffcc" stroke-width="2" stroke-linecap="round" opacity="0.3"/>

    <!-- 铰接点P-M (近端-中端) -->
    <circle id="hingePM" cx="510" cy="150" r="5" fill="#0a1a2a" stroke="#00c8a0" stroke-width="2"/>

    <!-- 双扭簧 -->
    <g id="torsionSpring"></g>
    <circle id="springGlow" cx="510" cy="150" r="20" fill="rgba(0,255,136,0)" filter="url(#softGlow)"/>

    <!-- 中端骨 -->
    <line id="boneMid" x1="510" y1="150" x2="640" y2="100" stroke="#00c8a0" stroke-width="4" stroke-linecap="round"/>
    <line id="boneMidHi" x1="510" y1="150" x2="640" y2="100" stroke="#00ffcc" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>

    <!-- 铰接点M-D (中端-远端) -->
    <circle id="hingeMD" cx="640" cy="100" r="4.5" fill="#0a1a2a" stroke="#00c8a0" stroke-width="1.8"/>

    <!-- SMA致动器 -->
    <g id="smaActuator"></g>
    <circle id="smaGlow" cx="640" cy="100" r="30" fill="url(#smaHeatGrad)" opacity="0"/>

    <!-- 远端骨 -->
    <line id="boneDist" x1="640" y1="100" x2="720" y2="60" stroke="#00c8a0" stroke-width="3" stroke-linecap="round"/>
    <line id="boneDistHi" x1="640" y1="100" x2="720" y2="60" stroke="#00ffcc" stroke-width="1" stroke-linecap="round" opacity="0.3"/>

    <!-- 远端骨尖端 -->
    <circle id="boneTip" cx="720" cy="60" r="2.5" fill="#00ffcc" opacity="0.6"/>
  </g>

  <!-- ===== 力箭头图层 ===== -->
  <g id="layerArrows">
    <line id="arrowSpring" x1="0" y1="0" x2="0" y2="0" stroke="#00ff88" stroke-width="2"
      marker-end="url(#arrGreen)" opacity="0" filter="url(#glowGreen)"/>
    <line id="arrowSMA" x1="0" y1="0" x2="0" y2="0" stroke="#ff6b2b" stroke-width="2"
      marker-end="url(#arrOrange)" opacity="0" filter="url(#glowOrange)"/>
  </g>

  <!-- ===== 标注图层 ===== -->
  <g id="layerAnnot" font-family="'Share Tech Mono',monospace" font-size="11" fill="#6090c0">
    <g id="annotSpring" opacity="0">
      <rect x="0" y="0" width="200" height="36" rx="4" fill="rgba(0,255,136,0.06)" stroke="rgba(0,255,136,0.2)" stroke-width="0.8"/>
      <text x="10" y="15" fill="#00ff88" font-size="10">双扭簧释能</text>
      <text x="10" y="29" fill="#6b8ab8" font-size="9.5">刚度 0.02 N·mm/deg → 第一阶段折叠</text>
    </g>
    <g id="annotSMA" opacity="0">
      <rect x="0" y="0" width="220" height="36" rx="4" fill="rgba(255,107,43,0.06)" stroke="rgba(255,107,43,0.2)" stroke-width="0.8"/>
      <text x="10" y="15" fill="#ff6b2b" font-size="10">SMA 形变致动</text>
      <text x="10" y="29" fill="#6b8ab8" font-size="9.5">45℃相变 · 0.8A · 1.2s → 第二阶段折叠</text>
    </g>
    <g id="annotUnlock" opacity="0">
      <rect x="0" y="0" width="160" height="24" rx="4" fill="rgba(0,200,160,0.06)" stroke="rgba(0,200,160,0.2)" stroke-width="0.8"/>
      <text x="10" y="16" fill="#00ffcc" font-size="10">锁定解除 · 滑套下行</text>
    </g>
  </g>
</svg>
</div>

<!-- 左侧阶段指示器 -->
<div class="phase-timeline">
  <div class="phase-item active" data-phase="0"><div class="phase-dot"></div><span class="phase-name">展开锁定</span></div>
  <div class="phase-line" data-line="0"></div>
  <div class="phase-item" data-phase="1"><div class="phase-dot"></div><span class="phase-name">解除锁定</span></div>
  <div class="phase-line" data-line="1"></div>
  <div class="phase-item" data-phase="2"><div class="phase-dot"></div><span class="phase-name">扭簧一次折叠</span></div>
  <div class="phase-line" data-line="2"></div>
  <div class="phase-item" data-phase="3"><div class="phase-dot"></div><span class="phase-name">SMA二次折叠</span></div>
  <div class="phase-line" data-line="3"></div>
  <div class="phase-item" data-phase="4"><div class="phase-dot"></div><span class="phase-name">收拢完成</span></div>
</div>

<!-- 右侧参数面板 -->
<div class="param-panel">
  <div class="param-card" id="cardSpring">
    <div class="param-label">扭簧角位移</div>
    <div><span class="param-value" id="valSpring">10</span><span class="param-unit">deg</span></div>
  </div>
  <div class="param-card" id="cardSMA">
    <div class="param-label">SMA 温度</div>
    <div><span class="param-value" id="valSMA">25</span><span class="param-unit">℃</span></div>
  </div>
  <div class="param-card">
    <div class="param-label">致动电流</div>
    <div><span class="param-value" id="valCurrent">0.0</span><span class="param-unit">A</span></div>
  </div>
  <div class="param-card">
    <div class="param-label">中端骨折叠角</div>
    <div><span class="param-value" id="valMidAng">170</span><span class="param-unit">deg</span></div>
  </div>
  <div class="param-card">
    <div class="param-label">远端骨折叠角</div>
    <div><span class="param-value" id="valDistAng">180</span><span class="param-unit">deg</span></div>
  </div>
</div>

<!-- 底部控制栏 -->
<div class="controls">
  <button class="btn" id="btnOpen" onclick="startOpen()">◀ 开伞</button>
  <button class="btn primary" id="btnAuto" onclick="toggleAuto()">自动演示</button>
  <button class="btn" id="btnClose" onclick="startClose()">收伞 ▶</button>
  <div class="slider-wrap">
    <label>进度</label>
    <input type="range" id="sliderProgress" min="0" max="1000" value="0" oninput="onSliderInput(this)">
  </div>
  <div class="slider-wrap">
    <label>速度</label>
    <input type="range" id="sliderSpeed" min="20" max="300" value="100">
    <span class="speed-label" id="speedLabel">1.0x</span>
  </div>
</div>

<script>
/* ===== 常量 ===== */
const DEG = Math.PI / 180;
const NOTCH = {x:350, y:220};          // 铰接点(伞顶)
const POLE_BOT = 600;                   // 主杆底端y
const L = {p:160, m:130, d:95};         // 骨节长度
const OPEN_A  = {p:-28, mRel:-8, dRel:0};  // 展开角度
const FOLD_A  = {mRel:140, dRel:130};      // 折叠终态相对角度

/* ===== 动画状态 ===== */
let progress  = 0;    // 0=展开 1=收拢
let target    = 0;    // 目标progress
let autoPlay  = false;
let autoDir   = 1;    // 1=收拢 -1=展开
let manualMode= false;
let traceMid  = [];   // 中端骨末端轨迹
let traceDist = [];   // 远端骨末端轨迹

/* ===== 缓动函数 ===== */
function easeInOutCubic(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2}
function easeOutQuad(t){return 1-(1-t)*(1-t)}

/* ===== 角度与位置计算 ===== */
function getAngles(p){
  // p: 0~1 总进度
  let mRel = OPEN_A.mRel, dRel = OPEN_A.dRel, runnerOff = 0, smaHeat = 0, springAct = 0;

  if(p <= 0.08){
    // 滑套下行,解锁
    runnerOff = (p/0.08)*50;
  } else if(p <= 0.45){
    // 第一阶段:扭簧折叠
    const t = (p-0.08)/0.37;
    const e = easeInOutCubic(t);
    mRel = OPEN_A.mRel + (FOLD_A.mRel - OPEN_A.mRel)*e;
    runnerOff = 50;
    springAct = 1 - e*0.6;
  } else if(p <= 0.55){
    // SMA加热
    const t = (p-0.45)/0.10;
    mRel = FOLD_A.mRel;
    runnerOff = 50;
    smaHeat = easeOutQuad(t);
    springAct = 0.4;
  } else if(p <= 0.90){
    // 第二阶段:SMA折叠
    const t = (p-0.55)/0.35;
    const e = easeInOutCubic(t);
    mRel = FOLD_A.mRel;
    dRel = OPEN_A.dRel + (FOLD_A.dRel - OPEN_A.dRel)*e;
    runnerOff = 50;
    smaHeat = 1 - e*0.3;
    springAct = 0.4*(1-e);
  } else {
    // 收拢完成
    const t = (p-0.90)/0.10;
    mRel = FOLD_A.mRel;
    dRel = FOLD_A.dRel;
    runnerOff = 50 + t*10;
    smaHeat = 0.7*(1-t);
  }
  return {pAngle:OPEN_A.p, mRel, dRel, runnerOff, smaHeat, springAct};
}

function computePos(a){
  const p1 = {x:NOTCH.x, y:NOTCH.y};
  const p2 = {x:p1.x + L.p*Math.cos(a.pAngle*DEG), y:p1.y + L.p*Math.sin(a.pAngle*DEG)};
  const mAbs = a.pAngle + a.mRel;
  const p3 = {x:p2.x + L.m*Math.cos(mAbs*DEG), y:p2.y + L.m*Math.sin(mAbs*DEG)};
  const dAbs = mAbs + a.dRel;
  const p4 = {x:p3.x + L.d*Math.cos(dAbs*DEG), y:p3.y + L.d*Math.sin(dAbs*DEG)};
  return {p1,p2,p3,p4, mAbs, dAbs};
}

/* ===== SVG 元素引用 ===== */
const $ = id => document.getElementById(id);
const el = {
  pole:$('pole'), handle:$('handle'),
  runner:$('runner'), stretcher:$('stretcher'),
  boneP:$('boneProx'), bonePHi:$('boneProxHi'),
  boneM:$('boneMid'), boneMHi:$('boneMidHi'),
  boneD:$('boneDist'), boneDHi:$('boneDistHi'),
  hingePM:$('hingePM'), hingeMD:$('hingeMD'),
  boneTip:$('boneTip'), canopy:$('canopy'),
  springGlow:$('springGlow'), smaGlow:$('smaGlow'),
  arrowSpring:$('arrowSpring'), arrowSMA:$('arrowSMA'),
  annotSpring:$('annotSpring'), annotSMA:$('annotSMA'), annotUnlock:$('annotUnlock'),
  traceMid:$('traceMid'), traceDist:$('traceDist'),
  microSwitch:$('microSwitch'), switchBtn:$('switchBtn'),
  torsionSpring:$('torsionSpring'), smaActuator:$('smaActuator'),
};

/* ===== 绘制函数 ===== */
function drawScene(a, pos){
  // 主杆
  // (静态,不更新)

  // 滑套位置
  const runnerY = 260 + a.runnerOff;
  el.runner.setAttribute('transform', `translate(0,${a.runnerOff})`);

  // 微动开关触发
  const swActive = a.runnerOff > 30;
  el.switchBtn.setAttribute('fill', swActive ? '#ff6b2b' : '#5a5a7a');
  el.switchBtn.setAttribute('x', swActive ? '349' : '344');

  // 撑骨 - 从滑套到近端骨40%处
  const strutEnd = {
    x: pos.p1.x + 0.4*(pos.p2.x - pos.p1.x),
    y: pos.p1.y + 0.4*(pos.p2.y - pos.p1.y)
  };
  el.stretcher.setAttribute('x1', 350);
  el.stretcher.setAttribute('y1', runnerY + 8);
  el.stretcher.setAttribute('x2', strutEnd.x);
  el.stretcher.setAttribute('y2', strutEnd.y);

  // 近端骨
  setLine(el.boneP, pos.p1, pos.p2);
  setLine(el.bonePHi, pos.p1, pos.p2);

  // 中端骨
  setLine(el.boneM, pos.p2, pos.p3);
  setLine(el.boneMHi, pos.p2, pos.p3);

  // 远端骨
  setLine(el.boneD, pos.p3, pos.p4);
  setLine(el.boneDHi, pos.p3, pos.p4);

  // 铰接点
  setCircle(el.hingePM, pos.p2, 5);
  setCircle(el.hingeMD, pos.p3, 4.5);
  setCircle(el.boneTip, pos.p4, 2.5);

  // 骨节高亮
  const midFold = Math.abs(a.mRel - OPEN_A.mRel) / (FOLD_A.mRel - OPEN_A.mRel);
  const distFold = Math.abs(a.dRel - OPEN_A.dRel) / (FOLD_A.dRel - OPEN_A.dRel);
  el.boneMHi.setAttribute('opacity', 0.2 + midFold*0.5);
  el.boneDHi.setAttribute('opacity', 0.2 + distFold*0.5);

  // 扭簧发光
  const sg = a.springAct;
  el.springGlow.setAttribute('cx', pos.p2.x);
  el.springGlow.setAttribute('cy', pos.p2.y);
  el.springGlow.setAttribute('r', 18 + sg*12);
  el.springGlow.setAttribute('fill', `rgba(0,255,136,${sg*0.25})`);

  // SMA发光
  const sh = a.smaHeat;
  el.smaGlow.setAttribute('cx', pos.p3.x);
  el.smaGlow.setAttribute('cy', pos.p3.y);
  el.smaGlow.setAttribute('r', 25 + sh*25);
  el.smaGlow.setAttribute('opacity', sh*0.7);

  // 绘制扭簧
  drawTorsionSpring(pos.p2, a.pAngle, a.pAngle + a.mRel, sg);

  // 绘制SMA
  drawSMA(pos.p3, pos.p2, pos.p4, a.pAngle + a.mRel, sh);

  // 伞面
  drawCanopy(pos);

  // 力箭头
  drawForceArrows(a, pos);

  // 标注
  drawAnnotations(a, pos);

  // 铰接点颜色
  el.hingePM.setAttribute('stroke', sg > 0.3 ? '#00ff88' : '#00c8a0');
  el.hingeMD.setAttribute('stroke', sh > 0.3 ? '#ff6b2b' : '#00c8a0');

  // 轨迹
  drawTraces(pos);
}

function setLine(el, a, b){
  el.setAttribute('x1',a.x); el.setAttribute('y1',a.y);
  el.setAttribute('x2',b.x); el.setAttribute('y2',b.y);
}
function setCircle(el, c, r){
  el.setAttribute('cx',c.x); el.setAttribute('cy',c.y);
  if(r) el.setAttribute('r',r);
}

/* 扭簧绘制 - 在铰接点处画螺旋簧 */
function drawTorsionSpring(center, ang1, ang2, activation){
  const g = el.torsionSpring;
  let html = '';
  const r1 = 10, r2 = 14;
  const arm1Ang = ang1 * DEG;
  const arm2Ang = ang2 * DEG;
  const armLen = 18;
  // 臂1 - 沿近端骨方向
  const a1x = center.x + armLen*Math.cos(arm1Ang);
  const a1y = center.y + armLen*Math.sin(arm1Ang);
  // 臂2 - 沿中端骨方向
  const a2x = center.x + armLen*Math.cos(arm2Ang);
  const a2y = center.y + armLen*Math.sin(arm2Ang);

  // 弹簧螺旋
  const startA = ang1 + 180;
  const endA = ang2;
  const turns = 3;
  const steps = turns * 24;
  let d = `M${a1x},${a1y} L${center.x + r1*Math.cos(arm1Ang)},${center.y + r1*Math.sin(arm1Ang)}`;
  for(let i=0;i<=steps;i++){
    const t = i/steps;
    const angle = (startA + t*(endA - startA + turns*360))*DEG;
    const r = r1 + (r2-r1)*t;
    const x = center.x + r*Math.cos(angle);
    const y = center.y + r*Math.sin(angle);
    d += ` L${x},${y}`;
  }
  d += ` L${a2x},${a2y}`;

  const col = activation > 0.3 ? '#00ff88' : '#1a5a3a';
  const w = activation > 0.3 ? 2 : 1.2;
  const filt = activation > 0.5 ? ' filter="url(#glowGreen)"' : '';
  html += `<path d="${d}" fill="none" stroke="${col}" stroke-width="${w}" stroke-linecap="round"${filt}/>`;
  g.innerHTML = html;
}

/* SMA致动器绘制 */
function drawSMA(hinge, midStart, distEnd, midAbsAngle, heat){
  const g = el.smaActuator;
  let html = '';
  const ang = midAbsAngle * DEG;
  const perpAng = ang + Math.PI/2;
  const w = 14, h = 3;
  // SMA矩形位于铰接点旁
  const cx = hinge.x + 8*Math.cos(perpAng);
  const cy = hinge.y + 8*Math.sin(perpAng);
  const col = heat > 0.3 ? `rgb(${Math.round(60+195*heat)},${Math.round(40+67*heat)},${Math.round(30-10*heat)})` : '#3a2820';
  const filt = heat > 0.5 ? ' filter="url(#glowOrange)"' : '';

  // 画旋转的矩形
  const deg = midAbsAngle;
  html += `<rect x="${cx-w/2}" y="${cy-h/2}" width="${w}" height="${h}" rx="1" fill="${col}" stroke="${heat>0.3?'#ff6b2b':'#5a3a2a'}" stroke-width="0.8" transform="rotate(${deg},${cx},${cy})"${filt}/>`;

  // 加热时画弯曲方向指示
  if(heat > 0.4){
    const bendAng = (midAbsAngle + 90) * DEG;
    const arrowLen = 16 * heat;
    const ax = hinge.x + arrowLen*Math.cos(bendAng);
    const ay = hinge.y + arrowLen*Math.sin(bendAng);
    html += `<line x1="${hinge.x}" y1="${hinge.y}" x2="${ax}" y2="${ay}" stroke="#ff6b2b" stroke-width="1.2" stroke-dasharray="3,2" opacity="${heat*0.6}"/>`;
  }
  g.innerHTML = html;
}

/* 伞面绘制 */
function drawCanopy(pos){
  // 从远端尖端到伞顶的弧线
  const cp1x = (pos.p1.x + pos.p4.x)/2 + 30;
  const cp1y = Math.min(pos.p1.y, pos.p4.y) - 40;
  const d = `M${pos.p1.x},${pos.p1.y} Q${cp1x},${cp1y} ${pos.p4.x},${pos.p4.y}`;
  el.canopy.setAttribute('d', d);
}

/* 力箭头绘制 */
function drawForceArrows(a, pos){
  // 扭簧力箭头 - 在中端骨方向上
  const sf = a.springAct;
  if(sf > 0.2){
    const mAbs = a.pAngle + a.mRel;
    const perpAng = (mAbs + 90) * DEG;
    const len = 30 * sf;
    const sx = pos.p2.x + 10*Math.cos(perpAng);
    const sy = pos.p2.y + 10*Math.sin(perpAng);
    // 指向折叠方向
    const foldAng = (a.pAngle + FOLD_A.mRel) * DEG;
    const ex = sx + len*Math.cos(foldAng);
    const ey = sy + len*Math.sin(foldAng);
    el.arrowSpring.setAttribute('x1',sx);
    el.arrowSpring.setAttribute('y1',sy);
    el.arrowSpring.setAttribute('x2',ex);
    el.arrowSpring.setAttribute('y2',ey);
    el.arrowSpring.setAttribute('opacity', sf*0.8);
  } else {
    el.arrowSpring.setAttribute('opacity', 0);
  }

  // SMA力箭头
  const sh = a.smaHeat;
  if(sh > 0.3){
    const mAbs = a.pAngle + a.mRel;
    const perpAng = (mAbs + 90) * DEG;
    const len = 25 * sh;
    const sx = pos.p3.x + 8*Math.cos(perpAng);
    const sy = pos.p3.y + 8*Math.sin(perpAng);
    const foldAng = (mAbs + FOLD_A.dRel) * DEG;
    const ex = sx + len*Math.cos(foldAng);
    const ey = sy + len*Math.sin(foldAng);
    el.arrowSMA.setAttribute('x1',sx);
    el.arrowSMA.setAttribute('y1',sy);
    el.arrowSMA.setAttribute('x2',ex);
    el.arrowSMA.setAttribute('y2',ey);
    el.arrowSMA.setAttribute('opacity', sh*0.7);
  } else {
    el.arrowSMA.setAttribute('opacity', 0);
  }
}

/* 标注绘制 */
function drawAnnotations(a, pos){
  // 解锁标注
  const showUnlock = progress > 0.02 && progress < 0.18;
  el.annotUnlock.setAttribute('opacity', showUnlock ? 0.9 : 0);
  if(showUnlock){
    const bx = 370, by = 280 + a.runnerOff;
    el.annotUnlock.querySelector('rect').setAttribute('x', bx);
    el.annotUnlock.querySelector('rect').setAttribute('y', by);
    const texts = el.annotUnlock.querySelectorAll('text');
    texts[0].setAttribute('x', bx+10);
    texts[0].setAttribute('y', by+16);
  }

  // 扭簧标注
  const showSpring = a.springAct > 0.3;
  el.annotSpring.setAttribute('opacity', showSpring ? Math.min(1, a.springAct*1.5) : 0);
  if(showSpring){
    const bx = pos.p2.x + 20, by = pos.p2.y - 50;
    el.annotSpring.querySelector('rect').setAttribute('x', bx);
    el.annotSpring.querySelector('rect').setAttribute('y', by);
    const texts = el.annotSpring.querySelectorAll('text');
    texts[0].setAttribute('x', bx+10);
    texts[0].setAttribute('y', by+15);
    texts[1].setAttribute('x', bx+10);
    texts[1].setAttribute('y', by+29);
  }

  // SMA标注
  const showSMA = a.smaHeat > 0.2;
  el.annotSMA.setAttribute('opacity', showSMA ? Math.min(1, a.smaHeat*1.5) : 0);
  if(showSMA){
    const bx = pos.p3.x + 15, by = pos.p3.y - 50;
    el.annotSMA.querySelector('rect').setAttribute('x', bx);
    el.annotSMA.querySelector('rect').setAttribute('y', by);
    const texts = el.annotSMA.querySelectorAll('text');
    texts[0].setAttribute('x', bx+10);
    texts[0].setAttribute('y', by+15);
    texts[1].setAttribute('x', bx+10);
    texts[1].setAttribute('y', by+29);
  }
}

/* 轨迹绘制 */
function drawTraces(pos){
  // 采样轨迹点
  traceMid.push({x:pos.p3.x, y:pos.p3.y});
  traceDist.push({x:pos.p4.x, y:pos.p4.y});
  const maxPts = 120;
  if(traceMid.length > maxPts) traceMid.shift();
  if(traceDist.length > maxPts) traceDist.shift();

  if(traceMid.length > 2){
    let d = `M${traceMid[0].x},${traceMid[0].y}`;
    for(let i=1;i<traceMid.length;i++) d += ` L${traceMid[i].x},${traceMid[i].y}`;
    el.traceMid.setAttribute('d', d);
  }
  if(traceDist.length > 2){
    let d = `M${traceDist[0].x},${traceDist[0].y}`;
    for(let i=1;i<traceDist.length;i++) d += ` L${traceDist[i].x},${traceDist[i].y}`;
    el.traceDist.setAttribute('d', d);
  }
}

/* ===== 参数面板更新 ===== */
function updateParams(a){
  // 扭簧角位移(相对170°自由态的偏移)
  const springDisp = Math.abs(a.mRel - OPEN_A.mRel);
  $('valSpring').textContent = springDisp.toFixed(0);

  // SMA温度
  const smaTemp = 25 + a.smaHeat * 55;
  $('valSMA').textContent = smaTemp.toFixed(0);

  // 电流
  const current = a.smaHeat > 0.2 ? 0.8 : 0;
  $('valCurrent').textContent = current.toFixed(1);

  // 中端骨折叠角(两骨间角度)
  const midAng = 180 - a.mRel + OPEN_A.mRel;
  $('valMidAng').textContent = Math.max(0, midAng).toFixed(0);

  // 远端骨折叠角
  const distAng = 180 - a.dRel;
  $('valDistAng').textContent = Math.max(0, distAng).toFixed(0);

  // 高亮卡片
  const cardSpring = $('cardSpring');
  const cardSMA = $('cardSMA');
  cardSpring.className = 'param-card' + (a.springAct > 0.3 ? ' active' : '');
  cardSMA.className = 'param-card' + (a.smaHeat > 0.3 ? ' active-sma' : '');
}

/* ===== 阶段指示器更新 ===== */
function updatePhases(){
  const phases = document.querySelectorAll('.phase-item');
  const lines = document.querySelectorAll('.phase-line');
  let activePhase = 0;
  if(progress < 0.05) activePhase = 0;
  else if(progress < 0.15) activePhase = 1;
  else if(progress < 0.50) activePhase = 2;
  else if(progress < 0.90) activePhase = 3;
  else activePhase = 4;

  // 判断是否SMA阶段
  const isSMA = progress >= 0.45 && progress < 0.90;

  phases.forEach((el, i) => {
    el.className = 'phase-item';
    if(i < activePhase) el.classList.add('done');
    if(i === activePhase){
      el.classList.add('active');
      if(isSMA && i === 3) el.classList.add('active-sma');
    }
  });
  lines.forEach((el, i) => {
    el.className = 'phase-line' + (i < activePhase ? ' lit' : '');
  });
}

/* ===== 动画主循环 ===== */
let lastTime = 0;
function animate(time){
  if(!lastTime) lastTime = time;
  const dt = (time - lastTime) / 1000;
  lastTime = time;

  // 速度
  const speed = $('sliderSpeed').value / 100;
  $('speedLabel').textContent = speed.toFixed(1) + 'x';

  if(autoPlay && !manualMode){
    const dir = autoDir;
    const step = dt * 0.25 * speed * dir;
    progress += step;
    if(progress >= 1){ progress = 1; autoDir = -1; }
    if(progress <= 0){ progress = 0; autoDir = 1; }
    $('sliderProgress').value = Math.round(progress * 1000);
  } else if(!manualMode){
    // 缓动到目标
    const diff = target - progress;
    progress += diff * Math.min(1, dt * 4 * speed);
    if(Math.abs(diff) < 0.001) progress = target;
    $('sliderProgress').value = Math.round(progress * 1000);
  }

  // 计算
  const a = getAngles(progress);
  const pos = computePos(a);

  // 绘制
  drawScene(a, pos);
  updateParams(a);
  updatePhases();

  requestAnimationFrame(animate);
}

/* ===== 交互 ===== */
function startClose(){
  autoPlay = false;
  manualMode = false;
  target = 1;
  updateBtnState();
}
function startOpen(){
  autoPlay = false;
  manualMode = false;
  target = 0;
  updateBtnState();
}
function toggleAuto(){
  autoPlay = !autoPlay;
  manualMode = false;
  if(autoPlay && progress >= 0.99) autoDir = -1;
  if(autoPlay && progress <= 0.01) autoDir = 1;
  updateBtnState();
}
function onSliderInput(slider){
  manualMode = true;
  autoPlay = false;
  progress = slider.value / 1000;
  target = progress;
  // 清空轨迹(避免跳跃连线)
  traceMid = [];
  traceDist = [];
  updateBtnState();
}
function updateBtnState(){
  $('btnAuto').textContent = autoPlay ? '暂停演示' : '自动演示';
  $('btnAuto').classList.toggle('primary', !autoPlay);
}

/* ===== 启动 ===== */
// 初始轨迹采样(展开态)
const initA = getAngles(0);
const initPos = computePos(initA);
for(let i=0;i<3;i++){
  traceMid.push({x:initPos.p3.x, y:initPos.p3.y});
  traceDist.push({x:initPos.p4.x, y:initPos.p4.y});
}
requestAnimationFrame(animate);

/* 键盘快捷键 */
document.addEventListener('keydown', e => {
  if(e.key === ' ' || e.code === 'Space'){ e.preventDefault(); toggleAuto(); }
  if(e.key === 'ArrowRight'){ startClose(); }
  if(e.key === 'ArrowLeft'){ startOpen(); }
});
</script>
</body>
</html>

这个实现包含以下核心要素:

视觉设计

  • 深蓝黑色技术蓝图风格背景,搭配精密网格
  • 骨节采用青绿色(#00c8a0)主体 + 高亮描边双层绘制,折叠时亮度递增
  • 双扭簧以螺旋线绘制,激活时发出绿色辉光(SVG filter glow)
  • SMA 致动器以矩形色块呈现,加热时从暗棕渐变至橙红并辐射热光晕
  • 骨端运动轨迹以虚线残影实时描绘,中端骨绿色、远端骨橙色

动画机理

  • 进度变量 progress(0→1)驱动全部状态,通过分段映射实现:
    • 0~8%:滑套下行、锁定解除
    • 8~45%:扭簧释能、中端骨自动下折(第一阶段)
    • 45~55%:微动开关触发、SMA 通电升温
    • 55~90%:SMA 相变致动、远端骨二次折叠(第二阶段)
    • 90~100%:收拢就位、SMA 冷却
  • 三次缓动函数使折叠运动符合物理加速-减速特征

交互控制

  • 「开伞 / 收伞」按钮带缓动过渡
  • 「自动演示」循环播放开合全过程
  • 进度滑块支持手动逐帧拖拽
  • 速度滑块调节 0.2x ~ 3.0x
  • 键盘快捷键:空格=演示切换,←→=开收伞

信息层

  • 左侧五阶段时间线指示器,实时高亮当前阶段(扭簧阶段绿色、SMA 阶段橙色)
  • 右侧参数面板实时显示扭簧角位移、SMA 温度、致动电流、两段折叠角
  • 机构上浮动标注框在关键瞬间出现,说明机理参数
积分规则:第一轮对话扣减6分,后续每轮扣4分