分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>离心涡流气动供弹原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#060a12;--fg:#b8c8d8;--muted:#4a6078;
  --accent:#00d4ff;--accent2:#ff9500;--accent3:#00ffa3;
  --card:#0b1220;--border:#162535;
}
html,body{height:100%;overflow:hidden}
body{
  background:var(--bg);color:var(--fg);
  font-family:'Noto Sans SC',sans-serif;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  position:relative;
}
body::before{
  content:'';position:fixed;inset:0;
  background:
    radial-gradient(ellipse 60% 50% at 30% 45%,rgba(0,212,255,.04),transparent),
    radial-gradient(ellipse 50% 40% at 75% 55%,rgba(255,149,0,.03),transparent);
  pointer-events:none;z-index:0;
}
#app{
  position:relative;z-index:1;
  display:flex;flex-direction:column;align-items:center;
  width:100%;max-width:1200px;height:100vh;
  padding:12px 16px 10px;
}
header{
  text-align:center;margin-bottom:6px;flex-shrink:0;
}
header h1{
  font-family:'Rajdhani',sans-serif;font-weight:700;
  font-size:clamp(22px,3.2vw,36px);letter-spacing:3px;
  color:#e0ecf4;
  text-shadow:0 0 20px rgba(0,212,255,.25);
}
header p{
  font-size:clamp(11px,1.4vw,14px);color:var(--muted);
  font-weight:300;letter-spacing:1px;margin-top:2px;
}
header p span{color:var(--accent);font-weight:600}
#svg-wrap{
  flex:1;width:100%;display:flex;align-items:center;justify-content:center;
  min-height:0;
}
#main-svg{
  width:100%;height:100%;max-height:calc(100vh - 140px);
  display:block;
}
#bottom-bar{
  flex-shrink:0;width:100%;
  display:flex;align-items:center;justify-content:center;
  gap:clamp(12px,2.5vw,32px);padding:8px 0 4px;
  flex-wrap:wrap;
}
.phase-group{display:flex;gap:8px}
.phase-btn{
  padding:6px 16px;border-radius:6px;
  font-family:'Rajdhani',sans-serif;font-weight:600;font-size:13px;
  letter-spacing:1px;
  background:var(--card);border:1px solid var(--border);color:var(--muted);
  transition:all .35s ease;cursor:default;white-space:nowrap;
}
.phase-btn.active{
  background:rgba(0,212,255,.12);border-color:var(--accent);
  color:var(--accent);box-shadow:0 0 12px rgba(0,212,255,.15);
}
.phase-btn .num{
  display:inline-block;width:18px;height:18px;line-height:18px;
  text-align:center;border-radius:50%;font-size:11px;
  background:rgba(0,212,255,.15);color:var(--accent);margin-right:5px;
}
.ctrl-group{
  display:flex;align-items:center;gap:8px;
  font-size:12px;color:var(--muted);
}
.ctrl-group label{font-family:'Rajdhani',sans-serif;font-weight:600;letter-spacing:1px}
.ctrl-group input[type=range]{
  -webkit-appearance:none;appearance:none;
  width:90px;height:4px;background:var(--border);border-radius:2px;outline:none;
}
.ctrl-group input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:14px;height:14px;
  background:var(--accent);border-radius:50%;cursor:pointer;
  box-shadow:0 0 6px rgba(0,212,255,.4);
}
.ctrl-group .val{
  font-family:'Rajdhani',sans-serif;color:var(--accent);
  min-width:32px;text-align:right;font-weight:600;
}
@media(max-width:640px){
  #app{padding:8px 8px 6px}
  .phase-btn{padding:4px 10px;font-size:11px}
}
</style>
</head>
<body>
<div id="app">
  <header>
    <h1>CENTRIFUGAL VORTEX FEEDING</h1>
    <p>TRIZ 最终理想解 — 以<span>气动场</span>替代机械接触,消除卡弹矛盾</p>
  </header>
  <div id="svg-wrap">
    <svg id="main-svg" viewBox="0 0 1060 620" preserveAspectRatio="xMidYMid meet">
      <defs>
        <!-- 发光滤镜 -->
        <filter id="glow-cyan" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="glow-amber" 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="glow-soft" x="-80%" y="-80%" width="260%" height="260%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="14"/>
        </filter>
        <filter id="glow-wide" x="-100%" y="-100%" width="300%" height="300%">
          <feGaussianBlur in="SourceGraphic" stdDeviation="28"/>
        </filter>
        <!-- 涡流罩渐变 -->
        <radialGradient id="npGlow" cx="50%" cy="50%" r="50%">
          <stop offset="0%" stop-color="#00d4ff" stop-opacity="0.35"/>
          <stop offset="60%" stop-color="#00d4ff" stop-opacity="0.08"/>
          <stop offset="100%" stop-color="#00d4ff" stop-opacity="0"/>
        </radialGradient>
        <!-- 枪管渐变 -->
        <linearGradient id="barrelGrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#1a2e44"/>
          <stop offset="50%" stop-color="#243c56"/>
          <stop offset="100%" stop-color="#1a2e44"/>
        </linearGradient>
        <!-- 叶轮渐变 -->
        <linearGradient id="bladeGrad" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stop-color="#1e3650"/>
          <stop offset="100%" stop-color="#2a4e6e"/>
        </linearGradient>
      </defs>

      <!-- 背景网格 -->
      <g opacity="0.18">
        <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
          <path d="M40 0 L0 0 0 40" fill="none" stroke="#1a3050" stroke-width="0.5"/>
        </pattern>
        <rect width="1060" height="620" fill="url(#grid)"/>
      </g>

      <!-- === 侧面剖视小图 === -->
      <g id="side-view" transform="translate(25,430)">
        <rect x="0" y="0" width="210" height="170" rx="6" fill="#0a1220" stroke="#162535" stroke-width="1"/>
        <text x="105" y="16" text-anchor="middle" fill="#4a6078" font-size="9" font-family="Rajdhani" font-weight="600" letter-spacing="1">SIDE CROSS-SECTION</text>
        <!-- 弹夹 -->
        <rect x="90" y="24" width="30" height="62" rx="3" fill="none" stroke="#2a4a68" stroke-width="1.5"/>
        <text x="75" y="50" text-anchor="end" fill="#4a6078" font-size="8" font-family="Noto Sans SC">弹夹</text>
        <!-- 子弹(侧视) -->
        <ellipse id="side-bullet" cx="105" cy="52" rx="6" ry="7" fill="#ff9500" opacity="0"/>
        <!-- 涡流罩 -->
        <rect x="60" y="86" width="100" height="40" rx="5" fill="none" stroke="#2a5a7a" stroke-width="1.5" stroke-dasharray="4,2"/>
        <text x="48" y="108" text-anchor="end" fill="#4a6078" font-size="8" font-family="Noto Sans SC">涡流罩</text>
        <!-- 叶轮(侧视截面) -->
        <rect id="side-impeller" x="62" y="90" width="6" height="32" rx="2" fill="#2a5070"/>
        <!-- 负压区 -->
        <ellipse cx="110" cy="106" rx="22" ry="12" fill="url(#npGlow)" opacity="0.5" id="side-np"/>
        <!-- 枪管 -->
        <rect x="160" y="98" width="42" height="16" rx="2" fill="url(#barrelGrad)" stroke="#2a4a68" stroke-width="1"/>
        <text x="181" y="93" text-anchor="middle" fill="#4a6078" font-size="8" font-family="Noto Sans SC">枪管</text>
        <!-- 气流箭头 -->
        <path d="M80,100 Q70,106 80,112" fill="none" stroke="#00d4ff" stroke-width="1" opacity="0.5" id="side-air1"/>
        <path d="M100,96 L110,96" fill="none" stroke="#00d4ff" stroke-width="1" opacity="0.4" marker-end="none"/>
        <path d="M140,100 L155,100" fill="none" stroke="#00d4ff" stroke-width="1.2" opacity="0.6"/>
        <polygon points="155,97 162,100 155,103" fill="#00d4ff" opacity="0.6"/>
        <!-- 旋转标记 -->
        <path id="side-rot" d="M65,88 A3,3 0 1 1 65,87.5" fill="none" stroke="#00d4ff" stroke-width="0.8" opacity="0.6"/>
      </g>

      <!-- === 主视图:俯视涡流供弹机构 === -->
      <g id="main-view">
        <!-- 负压区大光晕 -->
        <circle id="np-glow-wide" cx="290" cy="300" r="60" fill="#00d4ff" opacity="0" filter="url(#glow-wide)"/>
        <circle id="np-glow-core" cx="290" cy="300" r="45" fill="url(#npGlow)" opacity="0"/>

        <!-- 涡流罩(外环,有缺口) -->
        <g id="vortex-cover">
          <!-- 涡流罩外环 -->
          <path id="vc-outer" fill="none" stroke="#1e3854" stroke-width="22" opacity="0.7"/>
          <!-- 涡流罩内壁 -->
          <path id="vc-inner" fill="none" stroke="#264a6a" stroke-width="1.5" opacity="0.5"/>
          <!-- 涡流罩外壁 -->
          <path id="vc-outer-edge" fill="none" stroke="#1a3050" stroke-width="1" opacity="0.4"/>
        </g>

        <!-- 气流粒子层 -->
        <g id="particles-layer"></g>

        <!-- 枪管 -->
        <g id="barrel-group">
          <rect id="barrel-body" x="453" y="287" width="460" height="26" rx="4" fill="url(#barrelGrad)" stroke="#1e3854" stroke-width="1.2"/>
          <!-- 枪管内壁高光 -->
          <line x1="458" y1="290" x2="908" y2="290" stroke="#2a4e6e" stroke-width="0.5" opacity="0.5"/>
          <line x1="458" y1="310" x2="908" y2="310" stroke="#0d1a2a" stroke-width="0.5" opacity="0.5"/>
          <!-- 枪口 -->
          <rect x="908" y="284" width="8" height="32" rx="2" fill="#1a2e44" stroke="#2a4a68" stroke-width="1"/>
        </g>

        <!-- 叶轮组(旋转) -->
        <g id="impeller-group">
          <!-- 叶轮底盘 -->
          <circle cx="290" cy="300" r="112" fill="#0e1a28" stroke="#1a3050" stroke-width="1" opacity="0.6"/>
          <!-- 叶片由JS生成 -->
          <g id="blades-group"></g>
          <!-- 叶轮轮毂 -->
          <circle cx="290" cy="300" r="26" fill="#162840" stroke="#2a4e6e" stroke-width="1.5"/>
          <circle cx="290" cy="300" r="10" fill="#1e3854" stroke="#3a6a8e" stroke-width="1"/>
          <!-- 旋转方向标记 -->
          <path d="M290,270 A30,30 0 0 1 310,278" fill="none" stroke="#00d4ff" stroke-width="1" opacity="0.4" id="rot-indicator"/>
        </g>

        <!-- 弹夹入口标记(俯视为中央圆环) -->
        <g id="mag-indicator">
          <circle cx="290" cy="300" r="16" fill="none" stroke="#ff9500" stroke-width="1.2" stroke-dasharray="3,2" opacity="0.6"/>
          <text x="290" y="260" text-anchor="middle" fill="#ff9500" font-size="9" font-family="Rajdhani" font-weight="600" opacity="0.7">MAGAZINE</text>
          <line x1="290" y1="264" x2="290" y2="282" stroke="#ff9500" stroke-width="0.8" opacity="0.4"/>
        </g>

        <!-- 子弹 -->
        <g id="bullet-group" opacity="0">
          <ellipse id="bullet-body" cx="290" cy="300" rx="9" ry="10" fill="#ff9500" filter="url(#glow-amber)"/>
          <ellipse cx="290" cy="296" rx="5" ry="4" fill="#ffb84d" opacity="0.6"/>
        </g>

        <!-- 子弹轨迹 -->
        <path id="bullet-trail" fill="none" stroke="#ff9500" stroke-width="2.5" opacity="0" stroke-linecap="round"/>

        <!-- 气流方向大箭头 -->
        <g id="flow-arrows" opacity="0">
          <path d="M230,240 Q260,250 270,280" fill="none" stroke="#00d4ff" stroke-width="1.2" opacity="0.35" stroke-dasharray="4,3"/>
          <path d="M350,240 Q320,255 310,280" fill="none" stroke="#00d4ff" stroke-width="1.2" opacity="0.35" stroke-dasharray="4,3"/>
          <path d="M370,330 Q350,360 310,370" fill="none" stroke="#00d4ff" stroke-width="1.2" opacity="0.35" stroke-dasharray="4,3"/>
        </g>

        <!-- 标注 -->
        <g id="annotations" font-family="Noto Sans SC" font-size="10" fill="#5a7898">
          <!-- 叶片后掠角标注 -->
          <g id="anno-sweep" opacity="0">
            <line x1="290" y1="300" x2="380" y2="300" stroke="#00ffa3" stroke-width="0.8" stroke-dasharray="3,2" opacity="0.5"/>
            <path d="M360,300 A20,20 0 0 0 355,290" fill="none" stroke="#00ffa3" stroke-width="1" opacity="0.7"/>
            <text x="385" y="295" fill="#00ffa3" font-size="9" font-family="Rajdhani" font-weight="600">30°</text>
            <text x="385" y="308" fill="#4a7060" font-size="8">后掠角</text>
          </g>
          <!-- 间隙标注 -->
          <g id="anno-gap" opacity="0">
            <line x1="405" y1="280" x2="405" y2="320" stroke="#00ffa3" stroke-width="0.6" opacity="0.4"/>
            <line x1="400" y1="282" x2="410" y2="282" stroke="#00ffa3" stroke-width="0.8" opacity="0.5"/>
            <line x1="400" y1="318" x2="410" y2="318" stroke="#00ffa3" stroke-width="0.8" opacity="0.5"/>
            <text x="415" y="303" fill="#00ffa3" font-size="9" font-family="Rajdhani" font-weight="600">0.5mm</text>
          </g>
          <!-- 负压腔深度标注 -->
          <g id="anno-depth" opacity="0">
            <line x1="250" y1="300" x2="250" y2="220" stroke="#00ffa3" stroke-width="0.6" opacity="0.4"/>
            <text x="225" y="255" fill="#00ffa3" font-size="9" font-family="Rajdhani" font-weight="600" transform="rotate(-90,225,255)">15mm</text>
            <text x="240" y="218" fill="#4a7060" font-size="8">负压腔</text>
          </g>
        </g>

        <!-- 切向吸气口标注 -->
        <g id="anno-port" opacity="0">
          <rect x="440" y="292" width="16" height="16" fill="none" stroke="#00d4ff" stroke-width="1" stroke-dasharray="2,2" rx="2"/>
          <text x="448" y="280" text-anchor="middle" fill="#00d4ff" font-size="8" font-family="Rajdhani" font-weight="600">TANGENTIAL PORT</text>
        </g>
      </g>

      <!-- 动态文字:当前阶段 -->
      <text id="phase-label" x="530" y="580" text-anchor="middle" fill="#00d4ff" font-size="13" font-family="Rajdhani" font-weight="700" letter-spacing="2" opacity="0.8"></text>

      <!-- 关键创新点提示 -->
      <g id="innovation-callout" opacity="0">
        <rect x="650" y="140" width="280" height="80" rx="6" fill="#0a1220" stroke="#00ffa3" stroke-width="1" opacity="0.9"/>
        <text x="665" y="160" fill="#00ffa3" font-size="10" font-family="Rajdhani" font-weight="700" letter-spacing="1">IFR CORE INSIGHT</text>
        <text x="665" y="178" fill="#8aa8c0" font-size="10" font-family="Noto Sans SC">以旋转气流(已有资源)替代推弹杆</text>
        <text x="665" y="195" fill="#8aa8c0" font-size="10" font-family="Noto Sans SC">气动力 = 零零件供弹 · 自清洁 · 免润滑</text>
        <text x="665" y="212" fill="#5a7898" font-size="9" font-family="Noto Sans SC">资源: 电机旋转 → 离心风机 → 负压吸弹</text>
      </g>
    </svg>
  </div>
  <div id="bottom-bar">
    <div class="phase-group">
      <div class="phase-btn" id="ph0"><span class="num">1</span>负压生成</div>
      <div class="phase-btn" id="ph1"><span class="num">2</span>子弹吸入</div>
      <div class="phase-btn" id="ph2"><span class="num">3</span>离心供弹</div>
    </div>
    <div class="ctrl-group">
      <label>转速</label>
      <input type="range" id="speed-slider" min="0.3" max="2.5" step="0.1" value="1">
      <span class="val" id="speed-val">1.0x</span>
    </div>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const NS = 'http://www.w3.org/2000/svg';
  const svg = document.getElementById('main-svg');

  /* ========== 配置 ========== */
  const C = {
    cx: 290, cy: 300,
    hubR: 26,
    bladeInR: 32,
    bladeOutR: 110,
    bladeCount: 8,
    sweepDeg: 30,
    vortexInR: 120,
    vortexOutR: 155,
    portHalfAngle: 7,
    barrelHW: 13,
    barrelStartX: 453,
    barrelEndX: 910,
    particleCount: 75,
    cycleTime: 9.5,
  };
  const sweepRad = C.sweepDeg * Math.PI / 180;
  const portHalfRad = C.portHalfAngle * Math.PI / 180;

  /* ========== 状态 ========== */
  let time = 0, speed = 1, lastTs = 0;
  let impAngle = 0;
  let phase = -1;
  let particles = [];
  let bulletActive = false, bulletR = 0, bulletTheta = 0;
  let bulletInBarrel = false, bulletBarrelX = 0;
  let bulletTrailPts = [];

  /* ========== DOM引用 ========== */
  const $blades = document.getElementById('blades-group');
  const $impGroup = document.getElementById('impeller-group');
  const $bulletGroup = document.getElementById('bullet-group');
  const $bulletBody = document.getElementById('bullet-body');
  const $bulletTrail = document.getElementById('bullet-trail');
  const $npGlowWide = document.getElementById('np-glow-wide');
  const $npGlowCore = document.getElementById('np-glow-core');
  const $particlesLayer = document.getElementById('particles-layer');
  const $phaseLabel = document.getElementById('phase-label');
  const $flowArrows = document.getElementById('flow-arrows');
  const $annoSweep = document.getElementById('anno-sweep');
  const $annoGap = document.getElementById('anno-gap');
  const $annoDepth = document.getElementById('anno-depth');
  const $annoPort = document.getElementById('anno-port');
  const $innovationCallout = document.getElementById('innovation-callout');
  const $sideBullet = document.getElementById('side-bullet');
  const $sideNp = document.getElementById('side-np');
  const $speedSlider = document.getElementById('speed-slider');
  const $speedVal = document.getElementById('speed-val');
  const $phaseBtns = [document.getElementById('ph0'), document.getElementById('ph1'), document.getElementById('ph2')];

  /* ========== 绘制涡流罩弧线 ========== */
  function arcPath(cx, cy, r, startDeg, endDeg) {
    const s = startDeg * Math.PI / 180, e = endDeg * Math.PI / 180;
    const x1 = cx + r * Math.cos(s), y1 = cy + r * Math.sin(s);
    const x2 = cx + r * Math.cos(e), y2 = cy + r * Math.sin(e);
    const large = (endDeg - startDeg) > 180 ? 1 : 0;
    return `M${x1},${y1} A${r},${r} 0 ${large} 1 ${x2},${y2}`;
  }
  // 涡流罩弧线(留出切向口缺口)
  const gapStart = 360 - C.portHalfAngle;
  const gapEnd = C.portHalfAngle;
  document.getElementById('vc-outer').setAttribute('d',
    arcPath(C.cx, C.cy, (C.vortexInR + C.vortexOutR) / 2, gapEnd, gapStart)
  );
  document.getElementById('vc-inner').setAttribute('d',
    arcPath(C.cx, C.cy, C.vortexInR, gapEnd, gapStart)
  );
  document.getElementById('vc-outer-edge').setAttribute('d',
    arcPath(C.cx, C.cy, C.vortexOutR, gapEnd, gapStart)
  );

  /* ========== 绘制叶轮叶片 ========== */
  function createBlades() {
    $blades.innerHTML = '';
    for (let i = 0; i < C.bladeCount; i++) {
      const baseAngle = (i / C.bladeCount) * Math.PI * 2;
      const inX = C.cx + C.bladeInR * Math.cos(baseAngle);
      const inY = C.cy + C.bladeInR * Math.sin(baseAngle);
      const tipAngle = baseAngle - sweepRad;
      const outX = C.cx + C.bladeOutR * Math.cos(tipAngle);
      const outY = C.cy + C.bladeOutR * Math.sin(tipAngle);
      // 控制点
      const midR = (C.bladeInR + C.bladeOutR) / 2;
      const midAngle = baseAngle - sweepRad * 0.4;
      const cpX = C.cx + midR * Math.cos(midAngle);
      const cpY = C.cy + midR * Math.sin(midAngle);

      // 叶片宽度方向
      const perpIn = baseAngle + Math.PI / 2;
      const perpOut = tipAngle + Math.PI / 2;
      const wIn = 5, wOut = 3;

      const d = [
        `M${inX + wIn * Math.cos(perpIn)},${inY + wIn * Math.sin(perpIn)}`,
        `Q${cpX + 4 * Math.cos(midAngle + Math.PI / 2)},${cpY + 4 * Math.sin(midAngle + Math.PI / 2)} ${outX + wOut * Math.cos(perpOut)},${outY + wOut * Math.sin(perpOut)}`,
        `L${outX - wOut * Math.cos(perpOut)},${outY - wOut * Math.sin(perpOut)}`,
        `Q${cpX - 4 * Math.cos(midAngle + Math.PI / 2)},${cpY - 4 * Math.sin(midAngle + Math.PI / 2)} ${inX - wIn * Math.cos(perpIn)},${inY - wIn * Math.sin(perpIn)}`,
        'Z'
      ].join(' ');

      const blade = document.createElementNS(NS, 'path');
      blade.setAttribute('d', d);
      blade.setAttribute('fill', '#1e3a58');
      blade.setAttribute('stroke', '#2e5a7e');
      blade.setAttribute('stroke-width', '0.8');
      blade.setAttribute('opacity', '0.85');
      $blades.appendChild(blade);
    }
  }
  createBlades();

  /* ========== 创建粒子 ========== */
  function makeParticle() {
    const el = document.createElementNS(NS, 'circle');
    el.setAttribute('r', '2.2');
    el.setAttribute('fill', '#00d4ff');
    el.setAttribute('opacity', '0');
    $particlesLayer.appendChild(el);
    return {
      el,
      angle: Math.random() * Math.PI * 2,
      radius: C.hubR + Math.random() * 10,
      angVel: 0,
      radVel: 0,
      life: 0,
      maxLife: 2.5 + Math.random() * 2,
      size: 1.5 + Math.random() * 1.5,
      inBarrel: false,
      barrelX: 0,
      active: true
    };
  }
  for (let i = 0; i < C.particleCount; i++) {
    const p = makeParticle();
    p.radius = C.hubR + Math.random() * (C.vortexInR - C.hubR);
    p.angle = Math.random() * Math.PI * 2;
    p.life = Math.random() * p.maxLife;
    particles.push(p);
  }

  /* ========== 速度控制 ========== */
  $speedSlider.addEventListener('input', () => {
    speed = parseFloat($speedSlider.value);
    $speedVal.textContent = speed.toFixed(1) + 'x';
  });

  /* ========== 动画循环 ========== */
  function setPhase(p) {
    if (p === phase) return;
    phase = p;
    $phaseBtns.forEach((b, i) => b.classList.toggle('active', i === p));
    const labels = ['负压生成 — 叶轮旋转 → 中心负压', '子弹吸入 — 负压吸引 → 子弹入腔', '离心供弹 — 气流裹挟 → 入管发射'];
    $phaseLabel.textContent = labels[p] || '';
  }

  function resetBullet() {
    bulletActive = false;
    bulletR = 0;
    bulletTheta = 0;
    bulletInBarrel = false;
    bulletBarrelX = 0;
    bulletTrailPts = [];
    $bulletGroup.setAttribute('opacity', '0');
    $bulletTrail.setAttribute('opacity', '0');
    $bulletTrail.setAttribute('d', '');
  }

  function update(dt) {
    const sDt = dt * speed;
    time += sDt;
    const cycleT = time % C.cycleTime;

    // 叶轮旋转
    const rotSpeed = 3.5 * speed; // rad/s
    impAngle += rotSpeed * dt;
    $impGroup.setAttribute('transform', `rotate(${impAngle * 180 / Math.PI},${C.cx},${C.cy})`);

    // 阶段判定
    let newPhase;
    if (cycleT < 2.5) newPhase = 0;
    else if (cycleT < 5.0) newPhase = 1;
    else newPhase = 2;
    setPhase(newPhase);

    // 负压光晕
    const npPhase0 = Math.min(1, cycleT / 2.0); // 前2秒渐入
    const npPulse = 0.7 + 0.3 * Math.sin(time * 4);
    const npOpacity = (newPhase >= 0 ? npPhase0 : 0) * npPulse * 0.35;
    $npGlowWide.setAttribute('opacity', npOpacity);
    $npGlowCore.setAttribute('opacity', npOpacity * 1.6);
    $sideNp.setAttribute('opacity', npOpacity * 1.2);

    // 气流箭头
    $flowArrows.setAttribute('opacity', npPhase0 * 0.7);

    // 标注
    const annoOpacity = Math.min(1, Math.max(0, (cycleT - 1.5) / 1.0));
    $annoSweep.setAttribute('opacity', annoOpacity * 0.8);
    $annoGap.setAttribute('opacity', annoOpacity * 0.8);
    $annoDepth.setAttribute('opacity', annoOpacity * 0.7);
    $annoPort.setAttribute('opacity', annoOpacity * 0.8);

    // 创新点提示
    const innovOp = Math.min(1, Math.max(0, (cycleT - 2.0) / 1.5)) * Math.min(1, Math.max(0, (C.cycleTime - 1.5 - cycleT) / 1.0));
    $innovationCallout.setAttribute('opacity', innovOp);

    // 侧视图子弹
    if (newPhase === 1) {
      const subT = (cycleT - 2.5) / 2.5;
      const by = 52 - subT * 16;
      $sideBullet.setAttribute('cy', by);
      $sideBullet.setAttribute('opacity', 1 - subT * 0.5);
    } else if (newPhase === 2) {
      $sideBullet.setAttribute('opacity', 0.2);
      $sideBullet.setAttribute('cx', 105 + ((cycleT - 5.0) / 4.5) * 55);
    } else {
      $sideBullet.setAttribute('opacity', 0);
      $sideBullet.setAttribute('cx', 105);
      $sideBullet.setAttribute('cy', 52);
    }

    // ========== 粒子更新 ==========
    const impOmega = rotSpeed;
    particles.forEach(p => {
      p.life += sDt;
      if (p.life > p.maxLife || p.radius > C.vortexOutR + 10) {
        // 重生
        p.radius = C.hubR + Math.random() * 8;
        p.angle = Math.random() * Math.PI * 2;
        p.life = 0;
        p.maxLife = 2.5 + Math.random() * 2;
        p.inBarrel = false;
        p.active = true;
      }

      if (p.inBarrel) {
        p.barrelX += 180 * sDt;
        if (p.barrelX > C.barrelEndX + 20) {
          p.life = p.maxLife + 1; // 触发重生
        }
        const opacity = Math.max(0, 0.6 - (p.barrelX - C.barrelStartX) / 600);
        p.el.setAttribute('cx', p.barrelX);
        p.el.setAttribute('cy', C.cy);
        p.el.setAttribute('opacity', opacity * npPhase0);
        p.el.setAttribute('r', p.size * 0.8);
        return;
      }

      // 涡旋内部运动
      const rFrac = (p.radius - C.hubR) / (C.vortexInR - C.hubR);
      // 角速度:内部跟随叶轮,外部因减速
      const dragFactor = Math.max(0.15, 1 - rFrac * 0.7);
      p.angVel = impOmega * dragFactor;
      p.angle += p.angVel * dt;

      // 径向速度:离心效应
      p.radVel = 15 + rFrac * 40;
      p.radius += p.radVel * sDt;

      // 检查是否到达切向口附近
      const angleNorm = ((p.angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
      const nearPort = angleNorm < portHalfRad * 2 || angleNorm > Math.PI * 2 - portHalfRad * 2;

      if (p.radius > C.vortexInR && nearPort) {
        // 进入枪管
        p.inBarrel = true;
        p.barrelX = C.barrelStartX;
      }

      const x = C.cx + p.radius * Math.cos(p.angle);
      const y = C.cy + p.radius * Math.sin(p.angle);
      const fadeIn = Math.min(1, p.life / 0.3);
      const fadeOut = Math.max(0, 1 - (p.radius - C.vortexInR + 10) / 20);
      const opacity = fadeIn * fadeOut * 0.65 * npPhase0;

      p.el.setAttribute('cx', x);
      p.el.setAttribute('cy', y);
      p.el.setAttribute('opacity', Math.max(0, opacity));
      p.el.setAttribute('r', p.size * (0.7 + rFrac * 0.5));
    });

    // ========== 子弹动画 ==========
    if (newPhase === 1 && cycleT >= 3.0) {
      // 子弹出现
      if (!bulletActive) {
        bulletActive = true;
        bulletR = 0;
        bulletTheta = Math.random() * Math.PI * 2;
        bulletTrailPts = [];
      }
      $bulletGroup.setAttribute('opacity', '1');
      // 子弹被吸向中心(实际在中心出现)
      const subPhase = (cycleT - 3.0) / 2.0;
      bulletR = 2 + subPhase * 8;
      bulletTheta += rotSpeed * 0.3 * dt;
    } else if (newPhase === 2) {
      if (!bulletActive) {
        bulletActive = true;
        bulletR = 10;
        bulletTheta = impAngle;
        bulletTrailPts = [];
      }
      $bulletGroup.setAttribute('opacity', '1');

      if (!bulletInBarrel) {
        // 螺旋向外
        const spiralSpeed = 55 * speed;
        bulletR += spiralSpeed * dt;
        bulletTheta += rotSpeed * 0.8 * dt;

        // 记录轨迹
        const bx = C.cx + bulletR * Math.cos(bulletTheta);
        const by = C.cy + bulletR * Math.sin(bulletTheta);
        bulletTrailPts.push({ x: bx, y: by });
        if (bulletTrailPts.length > 60) bulletTrailPts.shift();

        // 检查是否到达枪管入口
        const bAngleNorm = ((bulletTheta % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
        if (bulletR >= C.vortexInR - 5 && (bAngleNorm < 0.3 || bAngleNorm > Math.PI * 2 - 0.3)) {
          bulletInBarrel = true;
          bulletBarrelX = C.barrelStartX;
        }
      } else {
        // 在枪管中
        bulletBarrelX += 250 * speed * dt;
        bulletTrailPts.push({ x: bulletBarrelX, y: C.cy });
        if (bulletTrailPts.length > 60) bulletTrailPts.shift();

        if (bulletBarrelX > C.barrelEndX + 30) {
          resetBullet();
        }
      }
    } else if (newPhase === 0 && cycleT < 0.5) {
      resetBullet();
    }

    // 渲染子弹位置
    if (bulletActive) {
      let bx, by;
      if (bulletInBarrel) {
        bx = bulletBarrelX;
        by = C.cy;
      } else {
        bx = C.cx + bulletR * Math.cos(bulletTheta);
        by = C.cy + bulletR * Math.sin(bulletTheta);
      }
      $bulletBody.setAttribute('cx', bx);
      $bulletBody.setAttribute('cy', by);
      // 高光跟随
      const hl = $bulletGroup.querySelector('ellipse:nth-child(2)');
      if (hl) {
        hl.setAttribute('cx', bx);
        hl.setAttribute('cy', by - 4);
      }

      // 轨迹线
      if (bulletTrailPts.length > 2) {
        let d = `M${bulletTrailPts[0].x},${bulletTrailPts[0].y}`;
        for (let i = 1; i < bulletTrailPts.length; i++) {
          d += ` L${bulletTrailPts[i].x},${bulletTrailPts[i].y}`;
        }
        $bulletTrail.setAttribute('d', d);
        $bulletTrail.setAttribute('opacity', '0.5');
      }
    }
  }

  function loop(ts) {
    if (!lastTs) lastTs = ts;
    const dt = Math.min((ts - lastTs) / 1000, 0.05); // 限制最大dt
    lastTs = ts;
    update(dt);
    requestAnimationFrame(loop);
  }

  // 自动启动
  requestAnimationFrame(loop);
});
</script>
</body>
</html>

这是一个完整的离心涡流气动供弹原理动画实现,以下是核心设计说明:

视觉设计

  • 采用深色工程蓝图美学,以深海军蓝为底色,搭配青色(#00d4ff)气流粒子和琥珀色(#ff9500)子弹,形成强烈对比
  • 叶轮带有 30° 后掠角的弯曲叶片,旋转时清晰可见
  • 涡流罩为半透明弧形环,右侧留有切向吸气口缺口连接枪管
  • 负压区以脉冲呼吸光晕呈现,直观展示"吸力"来源

动画叙事(三阶段循环)

  1. 负压生成:叶轮高速旋转,气流粒子从中心螺旋向外扩散,中心负压光晕逐渐增强
  2. 子弹吸入:琥珀色子弹从弹夹入口出现,在负压吸引下进入涡流腔中心
  3. 离心供弹:子弹随旋转气流螺旋加速向外,经切向口进入枪管,最终高速射出

TRIZ 理想解呈现

  • 右侧浮动面板直接点明核心创新:以旋转气流(电机旋转的衍生资源)替代推弹杆,实现零零件供弹
  • 绿色标注(#00ffa3)标出关键参数:30° 后掠角、0.5mm 间隙、15mm 负压腔深
  • 左下角侧视剖图补充垂直维度的结构关系

交互性

  • 底部转速滑块可实时调节叶轮转速和动画速度(0.3x ~ 2.5x)
  • 三阶段指示器自动高亮当前阶段
  • 动画页面加载后立即自动播放,重载即重播
积分规则:第一轮对话扣减8分,后续每轮扣6分