分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh">
<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=Orbitron:wght@400;700;900&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root {
  --bg-deep:#060a14;--bg-surface:#0c1222;--grid:rgba(0,180,255,0.04);
  --steel:#7b9cc0;--steel-hi:#a8c8e8;--cyan:#00e5ff;--cyan-g:rgba(0,229,255,0.25);
  --amber:#ff9100;--amber-g:rgba(255,145,0,0.25);--red:#ff3d00;--label:#7a9abb;
  --canopy:rgba(60,130,210,0.13);--canopy-s:rgba(90,160,240,0.28);
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg-deep);color:#b8cce0;font-family:'IBM Plex Mono',monospace;
  min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
header{text-align:center;padding:28px 20px 10px;width:100%}
header h1{font-family:'Orbitron',sans-serif;font-weight:900;font-size:clamp(18px,3vw,28px);
  letter-spacing:3px;color:#d0e4f8;text-transform:uppercase}
header p{font-size:12px;color:var(--label);margin-top:6px;letter-spacing:1px;max-width:700px;margin-inline:auto}
.svg-wrap{flex:1;display:flex;justify-content:center;align-items:center;width:100%;
  padding:8px 12px;min-height:420px}
svg#main{width:100%;max-width:1100px;height:auto;max-height:68vh;border-radius:12px;
  background:linear-gradient(170deg,#080e1e 0%,#0a1020 50%,#060c18 100%);
  box-shadow:0 0 60px rgba(0,229,255,0.04),inset 0 0 80px rgba(0,0,0,0.5)}
.controls{width:100%;max-width:1100px;padding:12px 16px 24px;display:flex;flex-wrap:wrap;
  gap:10px;align-items:center;justify-content:center}
.btn{font-family:'Orbitron',sans-serif;font-size:12px;font-weight:700;letter-spacing:1px;
  border:1.5px solid;border-radius:8px;padding:10px 22px;cursor:pointer;
  transition:all .25s;position:relative;overflow:hidden;outline:none}
.btn::after{content:'';position:absolute;inset:0;background:currentColor;opacity:0;transition:opacity .2s}
.btn:hover::after{opacity:0.08}
.btn:active{transform:scale(0.96)}
.btn-open{color:#00e5ff;border-color:#00e5ff;background:rgba(0,229,255,0.06)}
.btn-wind{color:#ff9100;border-color:#ff9100;background:rgba(255,145,0,0.06)}
.btn-pulse{color:#e0e8f0;border-color:#5a7a9a;background:rgba(90,122,154,0.08)}
.btn-emergency{color:#ff3d00;border-color:#ff3d00;background:rgba(255,61,0,0.06);
  font-size:10px;padding:8px 16px}
.btn:disabled{opacity:0.3;cursor:not-allowed;transform:none}
.slider-group{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--label)}
.slider-group input[type=range]{width:120px;accent-color:var(--amber)}
.status-bar{width:100%;max-width:1100px;display:flex;flex-wrap:wrap;gap:16px;
  justify-content:center;padding:0 16px 16px;font-size:11px;color:var(--label)}
.status-item{display:flex;align-items:center;gap:6px}
.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.dot-cyan{background:var(--cyan);box-shadow:0 0 6px var(--cyan)}
.dot-amber{background:var(--amber);box-shadow:0 0 6px var(--amber)}
.dot-red{background:var(--red);box-shadow:0 0 6px var(--red)}
.dot-off{background:#2a3a4a;box-shadow:none}
.val{color:#d0e4f8;font-weight:500}
@keyframes dashFlow{to{stroke-dashoffset:-20}}
@keyframes pulseGlow{0%,100%{opacity:.5}50%{opacity:1}}
@media(max-width:640px){
  .controls{gap:6px}.btn{padding:8px 14px;font-size:10px}
  .slider-group input[type=range]{width:80px}
}
</style>
</head>
<body>

<header>
  <h1>Magnetic-Lock Folding Rib</h1>
  <p>基于 TRIZ 最终理想解:利用伞面残余张力与扭簧势能实现瞬间解锁坍缩,磁力锁定抵抗风力,脉冲消磁即刻折叠</p>
</header>

<div class="svg-wrap">
  <svg id="main" viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <!-- 网格图案 -->
      <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
        <path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(0,160,255,0.05)" stroke-width="0.5"/>
      </pattern>
      <pattern id="gridFine" width="10" height="10" patternUnits="userSpaceOnUse">
        <path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(0,160,255,0.02)" stroke-width="0.3"/>
      </pattern>
      <!-- 青色辉光 -->
      <filter id="glowCyan" x="-80%" y="-80%" width="260%" height="260%">
        <feGaussianBlur in="SourceGraphic" stdDeviation="8" result="b"/>
        <feFlood flood-color="#00e5ff" flood-opacity="0.7" result="c"/>
        <feComposite in="c" in2="b" operator="in" result="g"/>
        <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <!-- 琥珀辉光 -->
      <filter id="glowAmber" x="-80%" y="-80%" width="260%" height="260%">
        <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/>
        <feFlood flood-color="#ff9100" flood-opacity="0.7" result="c"/>
        <feComposite in="c" in2="b" operator="in" result="g"/>
        <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <!-- 脉冲辉光 -->
      <filter id="glowPulse" x="-100%" y="-100%" width="300%" height="300%">
        <feGaussianBlur in="SourceGraphic" stdDeviation="14" result="b"/>
        <feFlood flood-color="#ffffff" flood-opacity="0.9" result="c"/>
        <feComposite in="c" in2="b" operator="in" result="g"/>
        <feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <!-- 轻辉光 -->
      <filter id="glowSoft" x="-50%" y="-50%" width="200%" height="200%">
        <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
        <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <!-- 轴渐变 -->
      <linearGradient id="shaftGrad" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#5a7a9a"/><stop offset="100%" stop-color="#3a5a7a"/>
      </linearGradient>
      <!-- 磁铁渐变 -->
      <linearGradient id="magGrad" x1="0" y1="0" x2="1" y2="0">
        <stop offset="0%" stop-color="#e03030"/><stop offset="50%" stop-color="#c02020"/>
        <stop offset="50%" stop-color="#2060e0"/><stop offset="100%" stop-color="#3070f0"/>
      </linearGradient>
    </defs>

    <!-- 背景网格 -->
    <rect width="100%" height="100%" fill="url(#gridFine)"/>
    <rect width="100%" height="100%" fill="url(#grid)"/>

    <!-- 所有动态元素将由JS创建 -->
  </svg>
</div>

<div class="controls">
  <button class="btn btn-open" id="btnOpen"><i class="fa-solid fa-lock"></i>&ensp;展开锁定</button>
  <button class="btn btn-wind" id="btnWind"><i class="fa-solid fa-wind"></i>&ensp;抗风测试</button>
  <div class="slider-group" id="windSlider" style="opacity:0.3;pointer-events:none">
    <span>风力</span>
    <input type="range" id="windRange" min="0" max="100" value="50">
    <span id="windVal">50%</span>
  </div>
  <button class="btn btn-pulse" id="btnPulse"><i class="fa-solid fa-bolt"></i>&ensp;脉冲收伞</button>
  <button class="btn btn-emergency" id="btnEmergency"><i class="fa-solid fa-triangle-exclamation"></i>&ensp;应急解锁</button>
</div>

<div class="status-bar">
  <div class="status-item"><div class="dot dot-cyan" id="dotMag"></div>磁力锁定 <span class="val" id="valMag">0 N</span></div>
  <div class="status-item"><div class="dot dot-amber" id="dotSpring"></div>扭簧势能 <span class="val" id="valSpring">0%</span></div>
  <div class="status-item"><div class="dot dot-off" id="dotPulse"></div>脉冲电流 <span class="val" id="valPulse">OFF</span></div>
  <div class="status-item"><div class="dot dot-off" id="dotWind"></div>风力 <span class="val" id="valWind">0 N</span></div>
  <div class="status-item">状态: <span class="val" id="valState">待机</span></div>
</div>

<script>
/* ===================== 常量与配置 ===================== */
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('main');

/* 伞骨参数 */
const HUB = { x: 520, y: 320 };
const INNER_LEN = 155, OUTER_LEN = 135;
const RIB_ANGLES = [-58, -29, 0, 29, 58]; // 度,从竖直方向偏转
const FOLD_DIR = [1, 1, -1, -1, -1]; // 左侧顺时针折,右侧逆时针折
const MAX_FOLD = 155; // 最大折叠角度

/* 临界参数 */
const MAG_FORCE_MAX = 12;    // N
const PULSE_CURRENT = 3;     // A
const SPRING_TORQUE = 0.2;   // N·m

/* ===================== 状态变量 ===================== */
let state = 'folded';
let foldAngle = MAX_FOLD;     // 0=展开, MAX_FOLD=折叠
let magStrength = 0;          // 0-1
let springEnergy = 0;         // 0-1
let pulseProgress = -1;       // -1=无, 0~1=脉冲行进
let windIntensity = 0;        // 0-1
let windOsc = 0;
let t = 0;
let windParticles = [];
let lastTime = 0;

/* ===================== 工具函数 ===================== */
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 damp(cur, tar, spd, dt) {
  return cur + (tar - cur) * (1 - Math.exp(-spd * dt));
}
function ribHinge(i) {
  const r = RIB_ANGLES[i] * Math.PI / 180;
  return { x: HUB.x + INNER_LEN * Math.sin(r), y: HUB.y - INNER_LEN * Math.cos(r) };
}
function ribTip(i, fa) {
  const h = ribHinge(i);
  const eff = (RIB_ANGLES[i] + fa * FOLD_DIR[i]) * Math.PI / 180;
  return { x: h.x + OUTER_LEN * Math.sin(eff), y: h.y - OUTER_LEN * Math.cos(eff) };
}

/* ===================== 构建 SVG 元素 ===================== */
const gRoot = el('g', { id: 'mechanism' }, svg);

// --- 轴 ---
const gShaft = el('g', {}, gRoot);
el('line', { x1: HUB.x, y1: HUB.y, x2: HUB.x, y2: 660, stroke: 'url(#shaftGrad)',
  'stroke-width': 6, 'stroke-linecap': 'round' }, gShaft);
// 轴内导线
el('line', { x1: HUB.x, y1: HUB.y + 10, x2: HUB.x, y2: 650, stroke: '#1a3a5a',
  'stroke-width': 2, 'stroke-dasharray': '6 4', opacity: 0.5 }, gShaft);

// --- 手柄 ---
const gHandle = el('g', {}, gRoot);
el('rect', { x: HUB.x - 18, y: 648, width: 36, height: 60, rx: 10, fill: '#1a2a3a',
  stroke: '#3a5a7a', 'stroke-width': 1.5 }, gHandle);
// 电路板指示
const circuitLed = el('circle', { cx: HUB.x, cy: 670, r: 3, fill: '#1a3a2a', stroke: '#2a5a3a',
  'stroke-width': 0.5 }, gHandle);
el('text', { x: HUB.x, y: 690, fill: '#3a6a5a', 'font-size': 6, 'text-anchor': 'middle',
  'font-family': 'IBM Plex Mono' }, gHandle).textContent = 'CTRL';

// --- 伞毂 ---
el('circle', { cx: HUB.x, cy: HUB.y, r: 10, fill: '#1a2a3a', stroke: '#5a7a9a', 'stroke-width': 2 }, gRoot);

// --- 伞骨组 ---
const ribGroups = [];
const hingeGlows = [];
const magRings = [];
const outerGroups = [];

for (let i = 0; i < RIB_ANGLES.length; i++) {
  const g = el('g', {}, gRoot);
  const h = ribHinge(i);
  const tip0 = ribTip(i, 0);

  // 内段伞骨
  el('line', { x1: HUB.x, y1: HUB.y, x2: h.x, y2: h.y, stroke: '#5a7a9a',
    'stroke-width': 4, 'stroke-linecap': 'round' }, g);

  // 磁场环(在铰链周围)
  const ring = el('ellipse', { cx: h.x, cy: h.y, rx: 18, ry: 12, fill: 'none',
    stroke: '#00e5ff', 'stroke-width': 1.2, 'stroke-dasharray': '4 4', opacity: 0 }, g);
  magRings.push(ring);

  // 铰链辉光
  const glow = el('circle', { cx: h.x, cy: h.y, r: 7, fill: '#00e5ff', opacity: 0,
    filter: 'url(#glowCyan)' }, g);
  hingeGlows.push(glow);

  // 铰链点
  el('circle', { cx: h.x, cy: h.y, r: 5, fill: '#0c1a2a', stroke: '#00e5ff',
    'stroke-width': 1.5, opacity: 0.8 }, g);

  // 扭簧标记(小弧线)
  const springArc = el('path', { d: springPath(h.x, h.y, RIB_ANGLES[i]),
    fill: 'none', stroke: '#ff9100', 'stroke-width': 1.5, opacity: 0 }, g);

  // 外段伞骨(可旋转组)
  const og = el('g', {}, g);
  el('line', { x1: h.x, y1: h.y, x2: tip0.x, y2: tip0.y, stroke: '#5a7a9a',
    'stroke-width': 3.5, 'stroke-linecap': 'round' }, og);
  // 骨尖端小圆
  el('circle', { cx: tip0.x, cy: tip0.y, r: 2.5, fill: '#7a9aba' }, og);
  outerGroups.push(og);

  ribGroups.push({ g, springArc, hinge: h });
}

function springPath(cx, cy, angle) {
  // 绘制小型扭簧螺旋
  const pts = [];
  for (let a = 0; a < 4 * Math.PI; a += 0.3) {
    const r = 8 + a * 1.2;
    const px = cx + r * Math.cos(a + angle * Math.PI / 180);
    const py = cy - r * Math.sin(a + angle * Math.PI / 180);
    pts.push(`${a === 0 ? 'M' : 'L'}${px.toFixed(1)},${py.toFixed(1)}`);
  }
  return pts.join(' ');
}

// --- 伞面 ---
const canopy = el('path', { fill: 'rgba(50,120,200,0.10)', stroke: 'rgba(80,150,240,0.22)',
  'stroke-width': 1.5, d: '' }, gRoot);
// 伞面放在伞骨下面 - 调整顺序
gRoot.insertBefore(canopy, ribGroups[0]?.g);

// --- 脉冲效果 ---
const pulseDot = el('circle', { cx: HUB.x, cy: 660, r: 8, fill: '#ffffff',
  filter: 'url(#glowPulse)', opacity: 0 }, gRoot);
const pulseLine = el('line', { x1: HUB.x, y1: 660, x2: HUB.x, y2: 660,
  stroke: '#00e5ff', 'stroke-width': 3, opacity: 0, filter: 'url(#glowCyan)' }, gRoot);

// --- 风粒子容器 ---
const gWind = el('g', {}, gRoot);
for (let i = 0; i < 40; i++) {
  const p = el('circle', { cx: 0, cy: 0, r: Math.random() * 2 + 0.5,
    fill: 'rgba(120,180,240,0.3)', opacity: 0 }, gWind);
  windParticles.push({ el: p, x: 0, y: 0, vx: 0, vy: 0, life: 0, active: false });
}

// --- 铰链细节放大图 ---
const insetX = 830, insetY = 440, insetW = 340, insetH = 280;
const gInset = el('g', {}, gRoot);

// 背景框
el('rect', { x: insetX, y: insetY, width: insetW, height: insetH, rx: 12,
  fill: 'rgba(6,10,20,0.92)', stroke: 'rgba(0,229,255,0.2)', 'stroke-width': 1 }, gInset);
el('text', { x: insetX + 14, y: insetY + 20, fill: '#5a8aaa', 'font-size': 10,
  'font-family': 'Orbitron', 'font-weight': 700, 'letter-spacing': 1.5 }, gInset).textContent = 'HINGE DETAIL';

// 细节内部元素
const detailCx = insetX + insetW / 2, detailCy = insetY + 140;

// 内段端
el('rect', { x: detailCx - 100, y: detailCy - 8, width: 70, height: 16, rx: 3,
  fill: '#2a3a4a', stroke: '#5a7a9a', 'stroke-width': 1 }, gInset);
el('text', { x: detailCx - 65, y: detailCy + 24, fill: '#5a7a9a', 'font-size': 8,
  'text-anchor': 'middle', 'font-family': 'IBM Plex Mono' }, gInset).textContent = '内段伞骨';

// 外段端
const outerDetailRect = el('rect', { x: detailCx + 30, y: detailCy - 8, width: 70, height: 16, rx: 3,
  fill: '#2a3a4a', stroke: '#5a7a9a', 'stroke-width': 1 }, gInset);

el('text', { x: detailCx + 65, y: detailCy + 24, fill: '#5a7a9a', 'font-size': 8,
  'text-anchor': 'middle', 'font-family': 'IBM Plex Mono' }, gInset).textContent = '外段伞骨';

// 永磁体(红蓝)
el('rect', { x: detailCx - 28, y: detailCy - 14, width: 12, height: 28, rx: 2,
  fill: 'url(#magGrad)' }, gInset);
el('text', { x: detailCx - 22, y: detailCy + 42, fill: '#8a6a6a', 'font-size': 7,
  'text-anchor': 'middle', 'font-family': 'IBM Plex Mono' }, gInset).textContent = '永磁体';

// 软磁铁芯
el('rect', { x: detailCx + 16, y: detailCy - 12, width: 10, height: 24, rx: 2,
  fill: '#4a5a6a', stroke: '#6a7a8a', 'stroke-width': 0.5 }, gInset);
el('text', { x: detailCx + 21, y: detailCy + 42, fill: '#6a7a8a', 'font-size': 7,
  'text-anchor': 'middle', 'font-family': 'IBM Plex Mono' }, gInset).textContent = '软磁芯';

// 线圈
for (let j = 0; j < 5; j++) {
  el('ellipse', { cx: detailCx + 21, cy: detailCy - 8 + j * 4, rx: 10, ry: 2.5,
    fill: 'none', stroke: '#aa8844', 'stroke-width': 0.8, opacity: 0.6 }, gInset);
}
el('text', { x: detailCx + 45, y: detailCy - 6, fill: '#8a7a5a', 'font-size': 7,
  'font-family': 'IBM Plex Mono' }, gInset).textContent = '线圈';

// 扭簧示意
const detailSpring = el('path', {
  d: `M ${detailCx - 5} ${detailCy - 20} Q ${detailCx} ${detailCy - 35} ${detailCx + 10} ${detailCy - 25}
      Q ${detailCx + 20} ${detailCy - 15} ${detailCx + 10} ${detailCy - 5}
      Q ${detailCx} ${detailCy + 5} ${detailCx + 5} ${detailCy + 15}`,
  fill: 'none', stroke: '#ff9100', 'stroke-width': 1.8, opacity: 0.7 }, gInset);
el('text', { x: detailCx + 35, y: detailCy - 22, fill: '#aa7a3a', 'font-size': 7,
  'font-family': 'IBM Plex Mono' }, gInset).textContent = '扭簧 0.2N·m';

// 铰链轴心
el('circle', { cx: detailCx, cy: detailCy, r: 4, fill: '#0a1a2a', stroke: '#4a6a8a',
  'stroke-width': 1.5 }, gInset);

// 磁力线(细节图中的动画线)
const detailMagLines = [];
for (let j = 0; j < 3; j++) {
  const ml = el('path', {
    d: `M ${detailCx - 26} ${detailCy - 8 + j * 8} Q ${detailCx} ${detailCy - 18 + j * 4} ${detailCx + 16} ${detailCy - 6 + j * 6}`,
    fill: 'none', stroke: '#00e5ff', 'stroke-width': 1, 'stroke-dasharray': '3 3',
    opacity: 0 }, gInset);
  detailMagLines.push(ml);
}

// 脉冲方向箭头
const detailPulseArrow = el('path', {
  d: `M ${detailCx + 21} ${detailCy + 30} L ${detailCx + 21} ${detailCy + 18}`,
  fill: 'none', stroke: '#ffffff', 'stroke-width': 2, opacity: 0,
  'marker-end': '' }, gInset);
const arrowHead = el('polygon', { points: `${detailCx+17},${detailCy+20} ${detailCx+21},${detailCy+12} ${detailCx+25},${detailCy+20}`,
  fill: '#ffffff', opacity: 0 }, gInset);

// 细节图状态标签
const detailStateLabel = el('text', { x: detailCx, y: insetY + insetH - 18, fill: '#00e5ff',
  'font-size': 10, 'text-anchor': 'middle', 'font-family': 'Orbitron', 'font-weight': 700,
  opacity: 0.8 }, gInset);
detailStateLabel.textContent = 'MAGNETIC LOCK ENGAGED';

// 连接线(从主图铰链到细节图)
const connectorLine = el('line', { x1: 0, y1: 0, x2: insetX, y2: insetY + 20,
  stroke: 'rgba(0,229,255,0.15)', 'stroke-width': 1, 'stroke-dasharray': '4 4' }, gRoot);

// --- 示波器面板 ---
const oscX = 30, oscY = 500, oscW = 220, oscH = 120;
const gOsc = el('g', {}, gRoot);
el('rect', { x: oscX, y: oscY, width: oscW, height: oscH, rx: 8,
  fill: 'rgba(6,10,20,0.9)', stroke: 'rgba(0,229,255,0.15)', 'stroke-width': 1 }, gOsc);
el('text', { x: oscX + 10, y: oscY + 16, fill: '#4a7a9a', 'font-size': 9,
  'font-family': 'Orbitron', 'font-weight': 700 }, gOsc).textContent = 'PULSE WAVEFORM';
// 中线
el('line', { x1: oscX + 10, y1: oscY + oscH / 2, x2: oscX + oscW - 10, y2: oscY + oscH / 2,
  stroke: 'rgba(0,229,255,0.1)', 'stroke-width': 0.5 }, gOsc);
// 刻度
for (let j = 0; j < 5; j++) {
  el('line', { x1: oscX + 10 + j * (oscW - 20) / 4, y1: oscY + oscH / 2 - 3,
    x2: oscX + 10 + j * (oscW - 20) / 4, y2: oscY + oscH / 2 + 3,
    stroke: 'rgba(0,229,255,0.15)', 'stroke-width': 0.5 }, gOsc);
}
const oscPath = el('polyline', { fill: 'none', stroke: '#00e5ff', 'stroke-width': 1.5,
  points: '', filter: 'url(#glowSoft)' }, gOsc);
const oscLabelA = el('text', { x: oscX + oscW - 10, y: oscY + oscH - 10, fill: '#3a6a8a',
  'font-size': 8, 'text-anchor': 'end', 'font-family': 'IBM Plex Mono' }, gOsc);
oscLabelA.textContent = '3A / 0.1s';

// --- 参数标签 ---
const labels = [
  { x: HUB.x - 80, y: HUB.y + 60, text: '永磁吸合力 12N', color: '#00bbcc' },
  { x: HUB.x + 20, y: HUB.y + 60, text: '扭簧扭矩 0.2N·m', color: '#cc7700' },
  { x: HUB.x - 40, y: 640, text: '瞬态脉冲 3A / 0.1s', color: '#8899aa' },
];
labels.forEach(l => {
  el('text', { x: l.x, y: l.y, fill: l.color, 'font-size': 9,
    'font-family': 'IBM Plex Mono', opacity: 0.6 }, gRoot).textContent = l.text;
});

/* ===================== 动画逻辑 ===================== */
function setState(s) {
  state = s;
  if (s === 'opening') {
    springEnergy = 0.1;
  } else if (s === 'wind') {
    windIntensity = parseInt(document.getElementById('windRange').value) / 100;
    document.getElementById('windSlider').style.opacity = '1';
    document.getElementById('windSlider').style.pointerEvents = 'auto';
  }
  if (s !== 'wind') {
    windIntensity = 0;
    document.getElementById('windSlider').style.opacity = '0.3';
    document.getElementById('windSlider').style.pointerEvents = 'none';
  }
  updateButtons();
}

function updateButtons() {
  const bo = document.getElementById('btnOpen');
  const bw = document.getElementById('btnWind');
  const bp = document.getElementById('btnPulse');
  const be = document.getElementById('btnEmergency');
  bo.disabled = !['folded','open'].includes(state);
  bw.disabled = !['open','wind'].includes(state);
  bp.disabled = !['open','wind'].includes(state);
  be.disabled = !['open','wind'].includes(state);
}

function update(dt) {
  t += dt;

  switch (state) {
    case 'opening':
      foldAngle = damp(foldAngle, 0, 5, dt);
      if (foldAngle < 0.5) {
        foldAngle = 0;
        state = 'locking';
      }
      break;
    case 'locking':
      magStrength = damp(magStrength, 1, 6, dt);
      springEnergy = damp(springEnergy, 0.85, 4, dt);
      if (magStrength > 0.97) { magStrength = 1; state = 'open'; }
      break;
    case 'open':
      magStrength = damp(magStrength, 1, 8, dt);
      springEnergy = damp(springEnergy, 0.85, 6, dt);
      windOsc *= 0.95;
      break;
    case 'wind':
      windOsc = Math.sin(t * 4) * windIntensity * 12 + Math.sin(t * 7.3) * windIntensity * 5;
      magStrength = damp(magStrength, 1, 8, dt);
      springEnergy = damp(springEnergy, 0.85, 6, dt);
      break;
    case 'pulse_firing':
      pulseProgress += dt * 2.8;
      if (pulseProgress >= 1) { pulseProgress = -1; state = 'demagnetizing'; }
      break;
    case 'demagnetizing':
      magStrength = damp(magStrength, 0, 12, dt);
      if (magStrength < 0.03) {
        magStrength = 0;
        state = 'spring_release';
      }
      break;
    case 'spring_release':
      springEnergy = damp(springEnergy, 0, 8, dt);
      foldAngle = damp(foldAngle, MAX_FOLD, 3, dt);
      if (foldAngle > MAX_FOLD - 3) { state = 'folding'; }
      break;
    case 'folding':
      foldAngle = damp(foldAngle, MAX_FOLD, 3, dt);
      springEnergy = damp(springEnergy, 0, 6, dt);
      if (foldAngle > MAX_FOLD - 0.5) {
        foldAngle = MAX_FOLD;
        springEnergy = 0;
        state = 'folded';
      }
      break;
    case 'emergency':
      magStrength = damp(magStrength, 0, 6, dt);
      if (magStrength < 0.05) {
        magStrength = 0;
        state = 'spring_release';
      }
      break;
  }
}

function render() {
  const fa = foldAngle + windOsc;

  // 更新外段伞骨旋转
  for (let i = 0; i < RIB_ANGLES.length; i++) {
    const h = ribHinge(i);
    const rot = fa * FOLD_DIR[i];
    outerGroups[i].setAttribute('transform', `rotate(${rot.toFixed(2)}, ${h.x.toFixed(1)}, ${h.y.toFixed(1)})`);

    // 更新铰链辉光
    hingeGlows[i].setAttribute('opacity', (magStrength * 0.7).toFixed(3));

    // 更新磁场环
    const ringScale = 1 + magStrength * 0.3 * Math.sin(t * 3 + i);
    magRings[i].setAttribute('opacity', (magStrength * 0.5).toFixed(3));
    magRings[i].setAttribute('rx', (18 * ringScale).toFixed(1));
    magRings[i].setAttribute('ry', (12 * ringScale).toFixed(1));
    magRings[i].setAttribute('stroke-dashoffset', ((t * 20 + i * 10) % 20).toFixed(1));

    // 扭簧可见性
    ribGroups[i].springArc.setAttribute('opacity', (springEnergy * 0.8).toFixed(3));
  }

  // 更新伞面
  const tips = RIB_ANGLES.map((_, i) => ribTip(i, fa));
  let canopyD = `M ${tips[0].x.toFixed(1)} ${tips[0].y.toFixed(1)}`;
  for (let i = 1; i < tips.length; i++) {
    const prev = tips[i - 1], cur = tips[i];
    const cpx = (prev.x + cur.x) / 2;
    const cpy = Math.min(prev.y, cur.y) - 25 - (1 - fa / MAX_FOLD) * 15;
    canopyD += ` Q ${cpx.toFixed(1)} ${cpy.toFixed(1)} ${cur.x.toFixed(1)} ${cur.y.toFixed(1)}`;
  }
  // 闭合回伞毂
  canopyD += ` L ${HUB.x} ${HUB.y} Z`;
  canopy.setAttribute('d', canopyD);

  // 连接线
  const mainHinge = ribHinge(0);
  connectorLine.setAttribute('x1', mainHinge.x.toFixed(1));
  connectorLine.setAttribute('y1', mainHinge.y.toFixed(1));

  // 脉冲效果
  if (pulseProgress >= 0 && pulseProgress <= 1) {
    const py = 660 - pulseProgress * (660 - HUB.y);
    pulseDot.setAttribute('cy', py.toFixed(1));
    pulseDot.setAttribute('opacity', '0.9');
    pulseLine.setAttribute('y1', '660');
    pulseLine.setAttribute('y2', py.toFixed(1));
    pulseLine.setAttribute('opacity', '0.5');
  } else {
    pulseDot.setAttribute('opacity', '0');
    pulseLine.setAttribute('opacity', '0');
  }

  // 电路LED
  if (pulseProgress >= 0) {
    circuitLed.setAttribute('fill', '#00ff88');
  } else if (magStrength > 0.5) {
    circuitLed.setAttribute('fill', `rgba(0,${Math.floor(180 + magStrength * 75)},${Math.floor(100 + magStrength * 50)},0.8)`);
  } else {
    circuitLed.setAttribute('fill', '#1a3a2a');
  }

  // 风粒子
  updateWindParticles();

  // 细节图更新
  renderDetail();

  // 示波器
  renderOscilloscope();

  // 状态栏
  renderStatusBar();
}

function updateWindParticles() {
  const active = state === 'wind' && windIntensity > 0.1;
  for (const p of windParticles) {
    if (!p.active && active && Math.random() < 0.15) {
      p.active = true;
      p.x = 1050 + Math.random() * 100;
      p.y = 80 + Math.random() * 300;
      p.vx = -(3 + Math.random() * 4) * windIntensity;
      p.vy = (Math.random() - 0.5) * 1.5;
      p.life = 1;
    }
    if (p.active) {
      p.x += p.vx;
      p.y += p.vy;
      p.life -= 0.015;
      if (p.life <= 0 || p.x < 150) {
        p.active = false;
        p.el.setAttribute('opacity', '0');
      } else {
        p.el.setAttribute('cx', p.x.toFixed(1));
        p.el.setAttribute('cy', p.y.toFixed(1));
        p.el.setAttribute('opacity', (p.life * 0.4 * windIntensity).toFixed(3));
      }
    }
  }
}

function renderDetail() {
  // 磁力线
  const mlOpacity = magStrength * 0.7;
  detailMagLines.forEach((ml, j) => {
    ml.setAttribute('opacity', mlOpacity.toFixed(3));
    ml.setAttribute('stroke-dashoffset', ((t * 15 + j * 5) % 20).toFixed(1));
  });

  // 脉冲箭头
  const showPulse = pulseProgress >= 0;
  arrowHead.setAttribute('opacity', showPulse ? '0.9' : '0');
  detailPulseArrow.setAttribute('opacity', showPulse ? '0.8' : '0');

  // 外段端位置变化
  const foldOffset = foldAngle > 10 ? 8 : 0;
  outerDetailRect.setAttribute('y', detailCy - 8 + foldOffset);
  outerDetailRect.setAttribute('transform', foldAngle > 10 ?
    `rotate(${Math.min(foldAngle * 0.3, 35)}, ${detailCx + 30}, ${detailCy})` : '');

  // 状态标签
  if (magStrength > 0.8) {
    detailStateLabel.textContent = 'MAGNETIC LOCK ENGAGED';
    detailStateLabel.setAttribute('fill', '#00e5ff');
  } else if (magStrength > 0.1) {
    detailStateLabel.textContent = 'DEMAGNETIZING...';
    detailStateLabel.setAttribute('fill', '#ff9100');
  } else if (foldAngle > 10) {
    detailStateLabel.textContent = 'SPRING RELEASE / FOLDING';
    detailStateLabel.setAttribute('fill', '#ff9100');
  } else {
    detailStateLabel.textContent = 'STANDBY';
    detailStateLabel.setAttribute('fill', '#5a7a9a');
  }
}

let oscData = new Array(100).fill(0);
function renderOscilloscope() {
  // 生成波形数据
  let val = 0;
  if (pulseProgress >= 0) {
    // 尖锐脉冲:快速上升,指数衰减
    const pt = pulseProgress;
    val = 3 * Math.exp(-pt * 12) * Math.sin(pt * 60);
    if (pt < 0.15) val = 3;
  } else if (state === 'open' || state === 'wind') {
    val = 0.05 * Math.sin(t * 10); // 微弱基线噪声
  }
  oscData.push(val);
  if (oscData.length > 100) oscData.shift();

  const x0 = oscX + 10, y0 = oscY + 15;
  const w = oscW - 20, h = oscH - 30;
  const mid = y0 + h / 2;
  const pts = oscData.map((v, i) => {
    const px = x0 + (i / 100) * w;
    const py = mid - (v / 4) * (h / 2);
    return `${px.toFixed(1)},${py.toFixed(1)}`;
  }).join(' ');
  oscPath.setAttribute('points', pts);
}

function renderStatusBar() {
  const mf = (magStrength * MAG_FORCE_MAX).toFixed(1);
  const se = (springEnergy * 100).toFixed(0);
  const isPulse = pulseProgress >= 0;
  const wf = (windIntensity * 30).toFixed(1);

  document.getElementById('valMag').textContent = `${mf} N`;
  document.getElementById('valSpring').textContent = `${se}%`;
  document.getElementById('valPulse').textContent = isPulse ? `${PULSE_CURRENT}A` : 'OFF';
  document.getElementById('valWind').textContent = `${wf} N`;

  const dotMag = document.getElementById('dotMag');
  const dotSpring = document.getElementById('dotSpring');
  const dotPulse = document.getElementById('dotPulse');
  const dotWind = document.getElementById('dotWind');

  dotMag.className = `dot ${magStrength > 0.5 ? 'dot-cyan' : 'dot-off'}`;
  dotSpring.className = `dot ${springEnergy > 0.3 ? 'dot-amber' : 'dot-off'}`;
  dotPulse.className = `dot ${isPulse ? 'dot-red' : 'dot-off'}`;
  dotWind.className = `dot ${windIntensity > 0.1 ? 'dot-amber' : 'dot-off'}`;

  const stateNames = {
    folded: '已折叠', opening: '展开中...', locking: '磁力锁定中...',
    open: '展开锁定', wind: '抗风测试', pulse_firing: '脉冲发射!',
    demagnetizing: '消磁中...', spring_release: '扭簧释放!',
    folding: '折叠中...', emergency: '应急解锁!'
  };
  document.getElementById('valState').textContent = stateNames[state] || state;
}

/* ===================== 主循环 ===================== */
function loop(ts) {
  const dt = Math.min((ts - lastTime) / 1000, 0.05);
  lastTime = ts;
  update(dt);
  render();
  requestAnimationFrame(loop);
}
lastTime = performance.now();
requestAnimationFrame(loop);

/* ===================== 事件处理 ===================== */
document.getElementById('btnOpen').addEventListener('click', () => {
  if (state === 'folded' || state === 'open') setState('opening');
});
document.getElementById('btnWind').addEventListener('click', () => {
  if (state === 'open' || state === 'wind') setState('wind');
});
document.getElementById('btnPulse').addEventListener('click', () => {
  if (state === 'open' || state === 'wind') {
    pulseProgress = 0;
    setState('pulse_firing');
  }
});
document.getElementById('btnEmergency').addEventListener('click', () => {
  if (state === 'open' || state === 'wind') {
    setState('emergency');
  }
});
document.getElementById('windRange').addEventListener('input', (e) => {
  windIntensity = parseInt(e.target.value) / 100;
  document.getElementById('windVal').textContent = e.target.value + '%';
});

updateButtons();
</script>
</body>
</html>

实现说明:

这个单文件 HTML 实现了一个完整的磁力锁定折叠伞骨 IFR 原理动画,核心技术要点如下:

  1. 双段伞骨 + 铰链机制:5 根伞骨各分为内外两段,中间铰链处可视化磁力锁定(青色辉光环)与扭簧势能(琥珀色螺旋线),折叠时外段绕铰链旋转 155°,实现"极细棒状坍缩"。

  2. 动画状态机:7 个状态完整覆盖整个机理链——展开 → 磁力锁定 → 抗风(弹性振荡)→ 脉冲消磁 → 扭簧释放 → 折叠,使用指数阻尼插值(damp)保证过渡平滑。

  3. 视觉引导设计

    • 磁力锁定:青色辉光 + 脉冲虚线环 + 强度呼吸动画,直观表达"磁力有弹性边界"
    • 脉冲消磁:白色光点沿伞柱上行 + 示波器面板实时显示 3A 尖峰波形
    • 扭簧释放:琥珀色辉光闪现 + 外段快速翻折
    • 抗风测试:风粒子流 + 伞骨微幅振荡,展示磁力锁定的弹性吸能特性
  4. 铰链细节放大图(右下角):展示永磁体(红蓝双色)、软磁铁芯、线圈绕组、扭簧的结构关系与磁力线动画,并通过连接线指向主图对应铰链。

  5. 交互控制:展开锁定 / 抗风测试(含风力滑块) / 脉冲收伞 / 应急机械解锁四个按钮,均与动画状态机联动;底部状态栏实时显示磁力、势能、电流、风力数值。

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