分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中心占位缩颈 · 引射原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=Noto+Sans+SC:wght@300;400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #080d18;
    --surface: #0e1525;
    --border: #1c2a42;
    --text: #c8d8e8;
    --text-muted: #5a7a9a;
    --accent: #00e0b0;
    --accent2: #f09030;
    --copper: #c8884a;
  }
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: 'Noto Sans SC', sans-serif; }
  body { display: flex; flex-direction: column; }
  header {
    padding: 12px 24px; display: flex; align-items: baseline; gap: 16px;
    background: linear-gradient(180deg, rgba(14,21,37,0.95), transparent);
    border-bottom: 1px solid var(--border); z-index: 2; flex-shrink: 0;
  }
  header h1 { font-family: 'Rajdhani', sans-serif; font-weight: 700; font-size: 22px; color: var(--accent); letter-spacing: 1px; }
  header span { font-size: 13px; color: var(--text-muted); font-weight: 300; }
  #canvas-wrap { flex: 1; position: relative; overflow: hidden; min-height: 0; }
  canvas { display: block; width: 100%; height: 100%; }
  #controls {
    flex-shrink: 0; padding: 10px 24px; display: flex; align-items: center; gap: 28px; flex-wrap: wrap;
    background: var(--surface); border-top: 1px solid var(--border); z-index: 2;
  }
  .ctrl-group { display: flex; align-items: center; gap: 8px; }
  .ctrl-group label { font-size: 12px; color: var(--text-muted); white-space: nowrap; font-family: 'Share Tech Mono', monospace; }
  .ctrl-group .val { font-family: 'Share Tech Mono', monospace; font-size: 13px; color: var(--accent); min-width: 38px; text-align: right; }
  input[type=range] {
    -webkit-appearance: none; appearance: none; width: 140px; height: 4px;
    background: var(--border); 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(--accent); border: 2px solid var(--bg); cursor: pointer;
  }
  button.ctrl-btn {
    font-family: 'Noto Sans SC', sans-serif; font-size: 12px; padding: 4px 14px;
    background: transparent; border: 1px solid var(--border); color: var(--text-muted);
    border-radius: 4px; cursor: pointer; transition: all 0.2s;
  }
  button.ctrl-btn:hover, button.ctrl-btn.active { border-color: var(--accent); color: var(--accent); }
  #info-badge {
    font-family: 'Share Tech Mono', monospace; font-size: 11px; color: var(--text-muted);
    margin-left: auto; display: flex; gap: 16px;
  }
  #info-badge .hl { color: var(--accent2); }
</style>
</head>
<body>
<header>
  <h1>CENTER OCCUPANCY VENTURI</h1>
  <span>梭形导流体 · 引射原理 — 流速与流通面积解耦的理想解</span>
</header>
<div id="canvas-wrap"><canvas id="c"></canvas></div>
<div id="controls">
  <div class="ctrl-group">
    <label>占位比</label>
    <input type="range" id="sl-ratio" min="25" max="60" value="45" step="1">
    <span class="val" id="v-ratio">45%</span>
  </div>
  <div class="ctrl-group">
    <label>入口流速</label>
    <input type="range" id="sl-speed" min="30" max="200" value="100" step="5">
    <span class="val" id="v-speed">1.0x</span>
  </div>
  <button class="ctrl-btn active" id="btn-anno">标注</button>
  <button class="ctrl-btn" id="btn-section">截面</button>
  <div id="info-badge">
    <span>加速比 <span class="hl" id="v-accel">1.82x</span></span>
    <span>环隙流速 <span class="hl" id="v-gap">1.82v₀</span></span>
  </div>
</div>

<script>
/* ===== 配置与状态 ===== */
const S = {
  ratio: 0.45,
  speed: 1.0,
  showAnno: true,
  showSection: false,
  time: 0
};

const PARTICLE_COUNT = 320;
const MICROJET_COUNT = 40;
const BG_DOTS = 60;

/* ===== Canvas 初始化 ===== */
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
let W, H, dpr;

function resize() {
  const wrap = document.getElementById('canvas-wrap');
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  W = wrap.clientWidth; H = wrap.clientHeight;
  cvs.width = W * dpr; cvs.height = H * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);

/* ===== 几何参数(响应式) ===== */
function geo() {
  const px = W * 0.06, pxEnd = W * 0.94;
  const py = H * 0.22, pyEnd = H * 0.78;
  const pH = pyEnd - py, cY = (py + pyEnd) / 2;
  const mhh = (pH / 2) * Math.sqrt(S.ratio);
  const sLen = mhh * 2 * 3; // L/D = 3:1
  const noseX = cY - pH * 0.08; // 纵向中心对应x用noseX表示流方向起始
  // 流方向:x轴。noseX在左,tailX在右
  const nx = px + (pxEnd - px) * 0.22;
  const tx = nx + sLen;
  const maxTX = nx + sLen * 0.38; // 最大厚度位置
  return { px, pxEnd, py, pyEnd, pH, cY, mhh, sLen, nx, tx, maxTX, pW: pxEnd - px };
}

/* 梭形半高度轮廓函数 */
function spindleHH(x, g) {
  if (x <= g.nx || x >= g.tx) return 0;
  const t = (x - g.nx) / g.sLen;
  const mt = 0.38;
  let p;
  if (t <= mt) {
    const s = t / mt;
    p = Math.sin(s * Math.PI * 0.5);
  } else {
    const s = (t - mt) / (1 - mt);
    p = Math.cos(s * Math.PI * 0.5);
  }
  return p * g.mhh;
}

/* 梭形斜率(数值微分) */
function spindleSlope(x, g) {
  const dx = 2;
  return (spindleHH(x + dx, g) - spindleHH(x - dx, g)) / (2 * dx);
}

/* ===== 速度场 ===== */
function getVel(x, y, g) {
  const shh = spindleHH(x, g);
  const dist = y - g.cY;
  const absD = Math.abs(dist);
  const baseV = 1.6 * S.speed;
  let vx = baseV, vy = 0;

  if (shh > 0.5) {
    const effH = g.pH - 2 * shh;
    const aR = g.pH / Math.max(effH, 1);

    if (absD < shh + 2) {
      // 被导流体阻挡 — 强力推出
      vy = (dist >= 0 ? 1 : -1) * baseV * 4;
      vx = baseV * 0.4;
    } else if (absD < shh + 30) {
      // 过渡区
      const t = (absD - shh) / 30;
      vx = baseV * aR * (1 - (1 - t) * 0.15);
      const slope = spindleSlope(x, g);
      vy = (dist >= 0 ? 1 : -1) * Math.abs(slope) * baseV * (1 - t) * 2.5;
    } else {
      // 环隙主流区
      const gapHalf = g.pH / 2 - shh;
      const posInGap = (absD - shh) / Math.max(gapHalf, 1);
      vx = baseV * aR * (1 - posInGap * 0.25);
    }
  }

  // 尾部恢复区:逐步减速
  if (x > g.tx && x < g.tx + 120) {
    const recovery = (x - g.tx) / 120;
    const shh_tail = spindleHH(g.tx - 2, g);
    const effH_tail = g.pH - 2 * shh_tail;
    const aR_tail = g.pH / Math.max(effH_tail, 1);
    vx = baseV * (aR_tail + (1 - aR_tail) * recovery);
  }

  return { vx, vy };
}

/* ===== 速度→颜色映射 ===== */
function speedColor(speed, baseV) {
  const t = Math.min(1, Math.max(0, (speed / baseV - 1) / 1.8));
  // teal(0,224,176) → amber(240,144,48) → hot(239,71,111)
  let r, g, b;
  if (t < 0.5) {
    const s = t * 2;
    r = 0 + s * 240; g = 224 - s * 80; b = 176 - s * 128;
  } else {
    const s = (t - 0.5) * 2;
    r = 240 - s * 1; g = 144 - s * 73; b = 48 + s * 63;
  }
  return `rgb(${r|0},${g|0},${b|0})`;
}

/* ===== 粒子系统 ===== */
let particles = [], microjets = [], bgDots = [];

function createParticle(g, randomX) {
  const x = randomX ? g.px + Math.random() * g.pW : g.px - 5;
  let y;
  const shh = spindleHH(x, g);
  if (shh > 0) {
    const side = Math.random() > 0.5 ? 1 : -1;
    const gapHalf = g.pH / 2 - shh;
    y = g.cY + side * (shh + Math.random() * gapHalf);
  } else {
    y = g.py + 8 + Math.random() * (g.pH - 16);
  }
  return { x, y, vx: 0, vy: 0, speed: 0, size: 1.2 + Math.random() * 1.2, life: 1 };
}

function createMicrojet(g) {
  const tailX = g.tx + 4;
  // 从微射流槽位置发出
  const slots = [-0.6, -0.25, 0.25, 0.6]; // 相对中心的位置
  const slot = slots[Math.random() * slots.length | 0];
  const y = g.cY + slot * g.mhh * 0.7;
  const spread = (Math.random() - 0.5) * 0.6;
  return {
    x: tailX, y, vx: 3.5 * S.speed, vy: spread * S.speed,
    speed: 3.5 * S.speed, size: 1.0 + Math.random() * 0.8, life: 1, age: 0
  };
}

function initParticles() {
  const g = geo();
  particles = []; microjets = []; bgDots = [];
  for (let i = 0; i < PARTICLE_COUNT; i++) particles.push(createParticle(g, true));
  for (let i = 0; i < MICROJET_COUNT; i++) microjets.push(createMicrojet(g));
  for (let i = 0; i < BG_DOTS; i++) {
    bgDots.push({
      x: Math.random() * W, y: Math.random() * H,
      vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.5) * 0.1,
      size: 0.5 + Math.random() * 1, alpha: 0.05 + Math.random() * 0.12
    });
  }
}

function updateParticles(dt) {
  const g = geo();
  const baseV = 1.6 * S.speed;

  // 主流粒子
  for (const p of particles) {
    const v = getVel(p.x, p.y, g);
    p.vx = v.vx + (Math.random() - 0.5) * 0.2;
    p.vy = v.vy + (Math.random() - 0.5) * 0.15;
    p.x += p.vx * dt * 60;
    p.y += p.vy * dt * 60;
    p.speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);

    // 管壁碰撞
    if (p.y < g.py + 4) { p.y = g.py + 4; p.vy = Math.abs(p.vy) * 0.3; }
    if (p.y > g.pyEnd - 4) { p.y = g.pyEnd - 4; p.vy = -Math.abs(p.vy) * 0.3; }

    // 回收
    if (p.x > g.pxEnd + 10 || p.x < g.px - 20) {
      Object.assign(p, createParticle(g, false));
    }
  }

  // 微射流粒子
  for (const m of microjets) {
    m.age += dt;
    m.vx *= (1 - dt * 0.8);
    m.vy *= (1 - dt * 0.5);
    m.x += m.vx * dt * 60;
    m.y += m.vy * dt * 60;
    m.speed = Math.sqrt(m.vx * m.vx + m.vy * m.vy);
    m.life = Math.max(0, 1 - m.age / 2.2);

    if (m.life <= 0 || m.x > g.pxEnd + 5) {
      Object.assign(m, createMicrojet(g));
    }
  }

  // 背景粒子
  for (const d of bgDots) {
    d.x += d.vx; d.y += d.vy;
    if (d.x < 0) d.x = W; if (d.x > W) d.x = 0;
    if (d.y < 0) d.y = H; if (d.y > H) d.y = 0;
  }
}

/* ===== 绘图函数 ===== */
function drawBg() {
  // 渐变背景
  const grad = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, W * 0.7);
  grad.addColorStop(0, '#0e1628');
  grad.addColorStop(1, '#060a14');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);

  // 网格点
  ctx.fillStyle = 'rgba(40,65,100,0.15)';
  const gap = 30;
  for (let x = gap; x < W; x += gap) {
    for (let y = gap; y < H; y += gap) {
      ctx.fillRect(x - 0.5, y - 0.5, 1, 1);
    }
  }

  // 背景飘尘
  for (const d of bgDots) {
    ctx.beginPath();
    ctx.arc(d.x, d.y, d.size, 0, Math.PI * 2);
    ctx.fillStyle = `rgba(100,160,220,${d.alpha})`;
    ctx.fill();
  }
}

function drawPipe(g) {
  // 管道半透明填充
  const pg = ctx.createLinearGradient(0, g.py, 0, g.pyEnd);
  pg.addColorStop(0, 'rgba(20,35,60,0.35)');
  pg.addColorStop(0.5, 'rgba(15,25,45,0.15)');
  pg.addColorStop(1, 'rgba(20,35,60,0.35)');
  ctx.fillStyle = pg;
  ctx.fillRect(g.px, g.py, g.pW, g.pH);

  // 管壁线
  ctx.strokeStyle = 'rgba(60,100,150,0.5)';
  ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(g.px, g.py); ctx.lineTo(g.pxEnd, g.py); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(g.px, g.pyEnd); ctx.lineTo(g.pxEnd, g.pyEnd); ctx.stroke();

  // 管壁发光
  const glowA = 0.12 + 0.04 * Math.sin(S.time * 1.5);
  ctx.strokeStyle = `rgba(0,224,176,${glowA})`;
  ctx.lineWidth = 4;
  ctx.beginPath(); ctx.moveTo(g.px, g.py); ctx.lineTo(g.pxEnd, g.py); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(g.px, g.pyEnd); ctx.lineTo(g.pxEnd, g.pyEnd); ctx.stroke();

  // 入口/出口端面
  ctx.strokeStyle = 'rgba(60,100,150,0.3)';
  ctx.lineWidth = 1.5;
  ctx.beginPath(); ctx.moveTo(g.px, g.py); ctx.lineTo(g.px, g.pyEnd); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(g.pxEnd, g.py); ctx.lineTo(g.pxEnd, g.pyEnd); ctx.stroke();
}

function drawSpindle(g) {
  // 绘制梭形导流体
  const steps = 80;
  ctx.beginPath();
  // 上半弧
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const x = g.nx + t * g.sLen;
    const hh = spindleHH(x, g);
    const y = g.cY - hh;
    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  // 下半弧(反向)
  for (let i = steps; i >= 0; i--) {
    const t = i / steps;
    const x = g.nx + t * g.sLen;
    const hh = spindleHH(x, g);
    const y = g.cY + hh;
    ctx.lineTo(x, y);
  }
  ctx.closePath();

  // 铜色渐变填充
  const sg = ctx.createLinearGradient(0, g.cY - g.mhh, 0, g.cY + g.mhh);
  sg.addColorStop(0, '#d4a870');
  sg.addColorStop(0.3, '#e8c490');
  sg.addColorStop(0.5, '#f0d8a8');
  sg.addColorStop(0.7, '#d4a060');
  sg.addColorStop(1, '#8b6030');
  ctx.fillStyle = sg;
  ctx.fill();

  // 边框
  ctx.strokeStyle = 'rgba(200,160,100,0.6)';
  ctx.lineWidth = 1.5;
  ctx.stroke();

  // 高光线
  ctx.beginPath();
  for (let i = 5; i <= steps - 5; i++) {
    const t = i / steps;
    const x = g.nx + t * g.sLen;
    const hh = spindleHH(x, g);
    const y = g.cY - hh * 0.55;
    if (i === 5) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  ctx.strokeStyle = 'rgba(255,240,200,0.25)';
  ctx.lineWidth = 2;
  ctx.stroke();

  // 微射流槽(尾部小缺口)
  const slots = [-0.55, -0.2, 0.2, 0.55];
  ctx.fillStyle = '#0e1525';
  for (const s of slots) {
    const sy = g.cY + s * g.mhh * 0.75;
    const sx = g.tx - 8;
    ctx.beginPath();
    ctx.moveTo(sx, sy - 2.5);
    ctx.lineTo(g.tx + 2, sy);
    ctx.lineTo(sx, sy + 2.5);
    ctx.closePath();
    ctx.fill();
  }
}

function drawSupportRods(g) {
  const rodX = g.nx + g.sLen * 0.38; // 最大厚度处
  const hh = spindleHH(rodX, g);
  const sweep = 15 * Math.PI / 180; // 15°后掠角

  ctx.lineWidth = 3;
  ctx.lineCap = 'round';

  // 上支撑杆
  const topStartY = g.cY - hh;
  const topEndY = g.py;
  const dxTop = (topStartY - topEndY) * Math.tan(sweep);
  const rodGrad1 = ctx.createLinearGradient(rodX, topEndY, rodX + dxTop, topStartY);
  rodGrad1.addColorStop(0, 'rgba(100,140,180,0.7)');
  rodGrad1.addColorStop(1, 'rgba(160,130,90,0.7)');
  ctx.strokeStyle = rodGrad1;
  ctx.beginPath();
  ctx.moveTo(rodX, topEndY + 2);
  ctx.lineTo(rodX + dxTop, topStartY);
  ctx.stroke();

  // 下支撑杆
  const botStartY = g.cY + hh;
  const botEndY = g.pyEnd;
  const dxBot = (botEndY - botStartY) * Math.tan(sweep);
  const rodGrad2 = ctx.createLinearGradient(rodX, botStartY, rodX + dxBot, botEndY);
  rodGrad2.addColorStop(0, 'rgba(160,130,90,0.7)');
  rodGrad2.addColorStop(1, 'rgba(100,140,180,0.7)');
  ctx.strokeStyle = rodGrad2;
  ctx.beginPath();
  ctx.moveTo(rodX + dxBot, botStartY);
  ctx.lineTo(rodX, botEndY - 2);
  ctx.stroke();

  // 流线型杆截面(椭圆端头)
  ctx.fillStyle = 'rgba(140,160,190,0.5)';
  ctx.beginPath(); ctx.ellipse(rodX, topEndY + 2, 4, 2, 0, 0, Math.PI * 2); ctx.fill();
  ctx.beginPath(); ctx.ellipse(rodX, botEndY - 2, 4, 2, 0, 0, Math.PI * 2); ctx.fill();
}

function drawParticles(g) {
  const baseV = 1.6 * S.speed;

  // 发光层
  ctx.globalCompositeOperation = 'lighter';
  for (const p of particles) {
    const c = speedColor(p.speed, baseV);
    const a = 0.12;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * 3.5, 0, Math.PI * 2);
    ctx.fillStyle = c.replace('rgb', 'rgba').replace(')', `,${a})`);
    ctx.fill();
  }
  ctx.globalCompositeOperation = 'source-over';

  // 核心层 — 流线短划
  for (const p of particles) {
    const c = speedColor(p.speed, baseV);
    const len = Math.min(p.speed * 3.5, 22);
    const angle = Math.atan2(p.vy, p.vx);
    const x1 = p.x - Math.cos(angle) * len * 0.5;
    const y1 = p.y - Math.sin(angle) * len * 0.5;
    const x2 = p.x + Math.cos(angle) * len * 0.5;
    const y2 = p.y + Math.sin(angle) * len * 0.5;
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.strokeStyle = c;
    ctx.lineWidth = p.size;
    ctx.lineCap = 'round';
    ctx.stroke();
  }

  // 微射流粒子
  ctx.globalCompositeOperation = 'lighter';
  for (const m of microjets) {
    const a = m.life * 0.8;
    // 光晕
    ctx.beginPath();
    ctx.arc(m.x, m.y, m.size * 4, 0, Math.PI * 2);
    ctx.fillStyle = `rgba(255,214,10,${a * 0.15})`;
    ctx.fill();
    // 核心
    const mLen = Math.min(m.speed * 3, 16);
    const mAngle = Math.atan2(m.vy, m.vx);
    ctx.beginPath();
    ctx.moveTo(m.x - Math.cos(mAngle) * mLen * 0.5, m.y - Math.sin(mAngle) * mLen * 0.5);
    ctx.lineTo(m.x + Math.cos(mAngle) * mLen * 0.5, m.y + Math.sin(mAngle) * mLen * 0.5);
    ctx.strokeStyle = `rgba(255,220,60,${a})`;
    ctx.lineWidth = m.size;
    ctx.lineCap = 'round';
    ctx.stroke();
  }
  ctx.globalCompositeOperation = 'source-over';
}

function drawAccelZoneGlow(g) {
  // 环形加速区发光
  const pulse = 0.5 + 0.3 * Math.sin(S.time * 2.5);
  const cx = (g.nx + g.tx) / 2;
  const rw = g.sLen * 0.4;
  const rh = g.mhh + 30;

  const ag = ctx.createRadialGradient(cx, g.cY, g.mhh * 0.3, cx, g.cY, rh + 20);
  ag.addColorStop(0, `rgba(0,224,176,0)`);
  ag.addColorStop(0.4, `rgba(0,224,176,${0.03 * pulse})`);
  ag.addColorStop(0.7, `rgba(240,144,48,${0.05 * pulse})`);
  ag.addColorStop(1, `rgba(240,144,48,0)`);
  ctx.fillStyle = ag;
  ctx.fillRect(cx - rw - 30, g.cY - rh - 30, (rw + 30) * 2, (rh + 30) * 2);
}

function drawAnnotations(g) {
  if (!S.showAnno) return;
  const pulse = 0.6 + 0.4 * Math.sin(S.time * 2);

  ctx.font = '600 12px "Noto Sans SC", sans-serif';
  ctx.textAlign = 'left';

  // 标注辅助函数
  function anno(x, y, text, color, lineToX, lineToY) {
    // 连接线
    ctx.beginPath();
    ctx.moveTo(lineToX, lineToY);
    ctx.lineTo(x - 4, y);
    ctx.strokeStyle = color.replace('1)', '0.4)');
    ctx.lineWidth = 1;
    ctx.setLineDash([3, 3]);
    ctx.stroke();
    ctx.setLineDash([]);

    // 脉冲圆点
    ctx.beginPath();
    ctx.arc(lineToX, lineToY, 3 * pulse, 0, Math.PI * 2);
    ctx.fillStyle = color.replace('1)', `${0.5 * pulse})`);
    ctx.fill();

    // 文字背景
    const tw = ctx.measureText(text).width;
    ctx.fillStyle = 'rgba(8,13,24,0.75)';
    ctx.fillRect(x - 4, y - 13, tw + 10, 18);

    // 文字
    ctx.fillStyle = color;
    ctx.fillText(text, x, y);
  }

  // 1. 梭形导流体
  anno(g.nx + g.sLen * 0.2, g.cY - g.mhh - 28,
    '梭形导流体 (L/D=3:1)', 'rgba(0,224,176,1)',
    g.nx + g.sLen * 0.35, g.cY - g.mhh + 5);

  // 2. 环形加速区
  const gapTop = g.cY - g.mhh - 8;
  anno(g.nx + g.sLen * 0.55, gapTop - 24,
    '环形加速区 (流速↑)', 'rgba(240,144,48,1)',
    g.nx + g.sLen * 0.55, gapTop + 4);

  // 3. 微射流槽
  anno(g.tx + 14, g.cY - g.mhh * 0.3 - 14,
    '微射流槽 (引射混合)', 'rgba(255,214,10,1)',
    g.tx - 2, g.cY - g.mhh * 0.2);

  // 4. 流线型支撑杆
  const rodX = g.nx + g.sLen * 0.38;
  anno(rodX + 20, g.py - 16,
    '后掠支撑杆 (15°)', 'rgba(140,180,220,1)',
    rodX + 5, g.py + 4);

  // 5. 大尺寸异物通过示意
  const passX = g.tx + 60;
  ctx.beginPath();
  ctx.arc(passX, g.cY + g.mhh + 25, 10, 0, Math.PI * 2);
  ctx.strokeStyle = `rgba(0,224,176,${0.4 * pulse})`;
  ctx.lineWidth = 1.5;
  ctx.setLineDash([4, 4]);
  ctx.stroke();
  ctx.setLineDash([]);
  ctx.font = '500 11px "Noto Sans SC", sans-serif';
  ctx.fillStyle = 'rgba(0,224,176,0.7)';
  ctx.textAlign = 'center';
  ctx.fillText('异物可通过', passX, g.cY + g.mhh + 50);

  // 6. 流向箭头
  ctx.fillStyle = 'rgba(100,160,220,0.35)';
  ctx.font = '500 11px "Share Tech Mono", monospace';
  ctx.textAlign = 'left';
  ctx.fillText('FLOW →', g.px + 8, g.py - 8);
}

function drawCrossSection(g) {
  if (!S.showSection) return;
  // 右上角截面视图
  const sz = Math.min(130, W * 0.1, H * 0.22);
  const cx = W - sz - 24;
  const cy = 24 + sz / 2;
  const pipeR = sz / 2 - 4;
  const spindleR = pipeR * Math.sqrt(S.ratio);

  // 背景框
  ctx.fillStyle = 'rgba(8,13,24,0.85)';
  ctx.strokeStyle = 'rgba(60,100,150,0.4)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.roundRect(cx - sz / 2 - 8, cy - sz / 2 - 22, sz + 16, sz + 30, 6);
  ctx.fill(); ctx.stroke();

  ctx.font = '600 10px "Noto Sans SC", sans-serif';
  ctx.fillStyle = 'rgba(140,180,220,0.7)';
  ctx.textAlign = 'center';
  ctx.fillText('截面视图', cx, cy - sz / 2 - 8);

  // 管道圆
  ctx.beginPath();
  ctx.arc(cx, cy, pipeR, 0, Math.PI * 2);
  ctx.strokeStyle = 'rgba(60,100,150,0.6)';
  ctx.lineWidth = 2;
  ctx.stroke();
  ctx.fillStyle = 'rgba(15,25,45,0.4)';
  ctx.fill();

  // 环形加速区高亮
  const pulse = 0.5 + 0.3 * Math.sin(S.time * 2.5);
  ctx.beginPath();
  ctx.arc(cx, cy, pipeR, 0, Math.PI * 2);
  ctx.arc(cx, cy, spindleR, 0, Math.PI * 2, true);
  ctx.fillStyle = `rgba(240,144,48,${0.08 + 0.04 * pulse})`;
  ctx.fill();

  // 导流体圆
  const sg = ctx.createRadialGradient(cx - spindleR * 0.2, cy - spindleR * 0.2, 0, cx, cy, spindleR);
  sg.addColorStop(0, '#e8c490');
  sg.addColorStop(1, '#8b6030');
  ctx.beginPath();
  ctx.arc(cx, cy, spindleR, 0, Math.PI * 2);
  ctx.fillStyle = sg;
  ctx.fill();
  ctx.strokeStyle = 'rgba(200,160,100,0.6)';
  ctx.lineWidth = 1;
  ctx.stroke();

  // 3根支撑杆(120°间隔)
  for (let i = 0; i < 3; i++) {
    const a = (i * 120 - 90) * Math.PI / 180;
    ctx.beginPath();
    ctx.moveTo(cx + Math.cos(a) * spindleR, cy + Math.sin(a) * spindleR);
    ctx.lineTo(cx + Math.cos(a) * pipeR, cy + Math.sin(a) * pipeR);
    ctx.strokeStyle = 'rgba(100,140,180,0.6)';
    ctx.lineWidth = 2;
    ctx.stroke();
  }

  // 占位比标注
  ctx.font = '500 10px "Share Tech Mono", monospace';
  ctx.fillStyle = 'rgba(0,224,176,0.8)';
  ctx.textAlign = 'center';
  ctx.fillText(`${(S.ratio * 100).toFixed(0)}%`, cx, cy + 4);
}

function drawVelocityLegend(g) {
  const lx = g.px + 8;
  const ly = g.pyEnd + 18;
  const lw = 120, lh = 8;

  ctx.font = '500 10px "Share Tech Mono", monospace';
  ctx.fillStyle = 'rgba(140,180,220,0.5)';
  ctx.textAlign = 'left';
  ctx.fillText('v₀', lx - 2, ly - 4);

  const grad = ctx.createLinearGradient(lx, 0, lx + lw, 0);
  grad.addColorStop(0, 'rgb(0,224,176)');
  grad.addColorStop(0.5, 'rgb(240,144,48)');
  grad.addColorStop(1, 'rgb(239,71,111)');
  ctx.fillStyle = grad;
  ctx.fillRect(lx, ly, lw, lh);
  ctx.strokeStyle = 'rgba(60,100,150,0.3)';
  ctx.lineWidth = 0.5;
  ctx.strokeRect(lx, ly, lw, lh);

  ctx.fillStyle = 'rgba(140,180,220,0.4)';
  ctx.textAlign = 'left'; ctx.fillText('慢', lx, ly + lh + 12);
  ctx.textAlign = 'right'; ctx.fillText('快', lx + lw, ly + lh + 12);
}

function drawFlowArrows(g) {
  // 入口流动方向箭头
  const arrowY = g.cY;
  const arrowX = g.px + 16;
  ctx.beginPath();
  ctx.moveTo(arrowX, arrowY - 6);
  ctx.lineTo(arrowX + 10, arrowY);
  ctx.lineTo(arrowX, arrowY + 6);
  ctx.fillStyle = 'rgba(0,224,176,0.3)';
  ctx.fill();
}

/* ===== 主绘制 ===== */
function draw() {
  const g = geo();
  ctx.clearRect(0, 0, W, H);
  drawBg();
  drawPipe(g);
  drawAccelZoneGlow(g);
  drawParticles(g);
  drawSupportRods(g);
  drawSpindle(g);
  drawFlowArrows(g);
  drawAnnotations(g);
  drawCrossSection(g);
  drawVelocityLegend(g);
}

/* ===== 动画循环 ===== */
let lastTime = 0;
function animate(now) {
  const dt = Math.min((now - lastTime) / 1000, 0.05) || 0.016;
  lastTime = now;
  S.time += dt;
  updateParticles(dt);
  draw();
  requestAnimationFrame(animate);
}

/* ===== 控件 ===== */
const slRatio = document.getElementById('sl-ratio');
const slSpeed = document.getElementById('sl-speed');
const vRatio = document.getElementById('v-ratio');
const vSpeed = document.getElementById('v-speed');
const vAccel = document.getElementById('v-accel');
const vGap = document.getElementById('v-gap');
const btnAnno = document.getElementById('btn-anno');
const btnSection = document.getElementById('btn-section');

function updateInfo() {
  const aR = 1 / (1 - S.ratio);
  vAccel.textContent = aR.toFixed(2) + 'x';
  vGap.textContent = aR.toFixed(2) + 'v₀';
}

slRatio.addEventListener('input', () => {
  S.ratio = parseInt(slRatio.value) / 100;
  vRatio.textContent = slRatio.value + '%';
  updateInfo();
});

slSpeed.addEventListener('input', () => {
  S.speed = parseInt(slSpeed.value) / 100;
  vSpeed.textContent = S.speed.toFixed(1) + 'x';
});

btnAnno.addEventListener('click', () => {
  S.showAnno = !S.showAnno;
  btnAnno.classList.toggle('active', S.showAnno);
});

btnSection.addEventListener('click', () => {
  S.showSection = !S.showSection;
  btnSection.classList.toggle('active', S.showSection);
});

/* ===== 初始化 ===== */
function init() {
  resize();
  initParticles();
  updateInfo();
  requestAnimationFrame(animate);
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}
</script>
</body>
</html>

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

视觉表现

  • 深色工程制图风格背景,配合点阵网格和飘尘粒子营造纵深感
  • 梭形导流体采用铜色金属渐变,带有高光条模拟曲面反光
  • 流体粒子以速度映射色带(青绿→琥珀→玫红)呈现,采用流线短划渲染方式,模拟 CFD 可视化效果
  • 环形加速区有脉动辉光引导视觉聚焦
  • 微射流粒子以金色高亮显示,从尾部射流槽喷出后逐渐混合衰减

交互功能

  • 占位比滑块:实时调节导流体截面占比(25%-60%),观察流速加速比变化
  • 入口流速滑块:调节基础流速
  • 标注按钮:切换关键部件的标注显示(梭体、环隙、射流槽、支撑杆、异物通过示意)
  • 截面按钮:右上角显示管道横截面视图,直观展示中心占位的空间关系
  • 底部实时显示加速比和环隙流速

IFR 思想体现

  • 动画直接呈现理想解运作状态,不做前后对比
  • 粒子在环隙中加速的颜色变化和辉光效果,直观呈现"局部窄以提速"
  • 异物通过标注点明"整体宽以排障"的矛盾破除
  • 微射流金色粒子展示高压引射如何"巧妙利用现有资源"拖动低速流体
积分规则:第一轮对话扣减8分,后续每轮扣6分