分享图
动画工坊
引擎就绪
<!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 href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#04080f;--fg:#d8e2f0;--muted:#4a6080;--accent:#00d4ff;--accent2:#00ff88;--warn:#ff8800;--card:rgba(6,14,30,0.92);--border:rgba(0,212,255,0.18)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Noto Sans SC',sans-serif;height:100vh;overflow:hidden;display:flex;flex-direction:column}
canvas{display:block;flex:1;width:100%}
.hud-top{position:fixed;top:0;left:0;right:0;padding:14px 28px;background:linear-gradient(180deg,rgba(4,8,15,.96) 0%,rgba(4,8,15,0) 100%);z-index:10;display:flex;align-items:baseline;gap:14px;pointer-events:none}
.hud-top h1{font-family:'Rajdhani',sans-serif;font-weight:900;font-size:20px;letter-spacing:3px;color:var(--accent);text-transform:uppercase}
.hud-top .sub{font-size:12px;color:var(--muted);font-weight:300;letter-spacing:.5px}
.legend{position:fixed;top:56px;right:20px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 18px;z-index:10;backdrop-filter:blur(16px);pointer-events:none}
.legend-row{display:flex;align-items:center;gap:9px;margin-bottom:7px;font-size:11px;color:var(--fg);letter-spacing:.3px}
.legend-row:last-child{margin-bottom:0}
.ldot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
.controls{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px 26px;display:flex;gap:28px;align-items:flex-end;backdrop-filter:blur(18px);z-index:10}
.cg{display:flex;flex-direction:column;gap:5px}
.cg label{font-size:10px;color:var(--muted);font-weight:700;letter-spacing:1.2px;text-transform:uppercase;white-space:nowrap}
.cg input[type=range]{-webkit-appearance:none;width:130px;height:3px;background:rgba(0,212,255,.15);border-radius:2px;outline:none}
.cg input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;background:var(--accent);border-radius:50%;cursor:pointer;box-shadow:0 0 8px rgba(0,212,255,.5)}
.cg .val{font-family:'Rajdhani',sans-serif;font-size:15px;font-weight:700;color:var(--accent);text-align:center;min-width:36px}
.tbtn{background:transparent;border:1px solid var(--border);color:var(--muted);padding:7px 14px;border-radius:7px;cursor:pointer;font-family:'Noto Sans SC',sans-serif;font-size:11px;transition:all .25s;white-space:nowrap}
.tbtn.on{border-color:var(--accent);color:var(--accent);background:rgba(0,212,255,.07);box-shadow:0 0 10px rgba(0,212,255,.12)}
.tbtn:hover{border-color:var(--accent)}
.inset{position:fixed;bottom:90px;right:20px;width:200px;height:150px;background:var(--card);border:1px solid var(--border);border-radius:10px;z-index:10;overflow:hidden;backdrop-filter:blur(16px)}
.inset canvas{width:100%;height:100%}
.inset-label{position:absolute;bottom:6px;left:10px;font-size:9px;color:var(--muted);letter-spacing:.8px;text-transform:uppercase;pointer-events:none}
</style>
</head>
<body>

<div class="hud-top">
  <h1>IFR Micro-Vortex Air Bearing</h1>
  <span class="sub">最终理想解:微涡流气膜轴承 — 降噪防粘原理</span>
</div>

<div class="legend">
  <div class="legend-row"><div class="ldot" style="background:#00d4ff;box-shadow:0 0 5px #00d4ff"></div>主流场</div>
  <div class="legend-row"><div class="ldot" style="background:#00ff88;box-shadow:0 0 5px #00ff88"></div>微涡旋 / 气膜轴承</div>
  <div class="legend-row"><div class="ldot" style="background:#ff8800;box-shadow:0 0 5px #ff8800"></div>粉尘颗粒</div>
  <div class="legend-row"><div class="ldot" style="background:rgba(0,200,255,.25);border:1px solid #00c8ff"></div>超疏水微纳壁面</div>
</div>

<canvas id="mainCanvas"></canvas>

<div class="inset">
  <canvas id="insetCanvas"></canvas>
  <div class="inset-label">超疏水壁面 · 接触角 &gt;150°</div>
</div>

<div class="controls">
  <div class="cg">
    <label>涡流发生器迎角</label>
    <input type="range" id="sAngle" min="5" max="30" value="15" step="1">
    <span class="val" id="vAngle">15°</span>
  </div>
  <div class="cg">
    <label>流速倍率</label>
    <input type="range" id="sSpeed" min="0.3" max="2.5" value="1.0" step="0.1">
    <span class="val" id="vSpeed">1.0x</span>
  </div>
  <div class="cg">
    <label>粉尘密度</label>
    <input type="range" id="sDust" min="0" max="60" value="25" step="1">
    <span class="val" id="vDust">25</span>
  </div>
  <button class="tbtn on" id="btnFilm">气膜可视化</button>
  <button class="tbtn on" id="btnLabel">标注层</button>
</div>

<script>
/* ========== 全局状态 ========== */
const mc = document.getElementById('mainCanvas');
const cx = mc.getContext('2d');
const ic = document.getElementById('insetCanvas');
const ix = ic.getContext('2d');

let W, H;
let time = 0;
let vortexAngle = 15;
let flowMul = 1.0;
let dustMax = 25;
let showFilm = true;
let showLabel = true;

/* ========== 通道几何 ========== */
let CH = {};

function calcGeom() {
  W = mc.width = window.innerWidth;
  H = mc.height = window.innerHeight;
  ic.width = 200; ic.height = 150;

  const cy = H * 0.48;
  CH = {
    wL: W * 0.04,          // 宽段左
    wR: W * 0.27,          // 宽段右 / 收缩起点
    wT: cy - H * 0.23,     // 宽段顶壁
    wB: cy + H * 0.23,     // 宽段底壁
    nL: W * 0.45,          // 窄段左 / 收缩终点
    nR: W * 0.96,          // 窄段右
    nT: cy - H * 0.13,     // 窄段顶壁
    nB: cy + H * 0.13,     // 窄段底壁
    vgX: W * 0.255,        // 涡流发生器 X
    cy: cy
  };
}

/* 壁面 Y 坐标(smoothstep 插值)*/
function topY(x) {
  if (x <= CH.wR) return CH.wT;
  if (x >= CH.nL) return CH.nT;
  const t = (x - CH.wR) / (CH.nL - CH.wR);
  const s = t * t * (3 - 2 * t);
  return CH.wT + s * (CH.nT - CH.wT);
}
function botY(x) {
  if (x <= CH.wR) return CH.wB;
  if (x >= CH.nL) return CH.nB;
  const t = (x - CH.wR) / (CH.nL - CH.wR);
  const s = t * t * (3 - 2 * t);
  return CH.wB + s * (CH.nB - CH.wB);
}
function chanH(x) { return botY(x) - topY(x); }
function chanCY(x) { return (topY(x) + botY(x)) / 2; }

/* ========== 粒子系统 ========== */

/* 流线粒子 */
const FLOW_N = 260;
let flowP = [];

function mkFlow() {
  const x = CH.wL + Math.random() * (CH.nR - CH.wL) * 0.2;
  const ty = topY(x), by = botY(x);
  const margin = (by - ty) * 0.06;
  return {
    x, y: ty + margin + Math.random() * (by - ty - margin * 2),
    speed: 0.6 + Math.random() * 0.4,
    size: 1 + Math.random() * 1.2,
    alpha: 0.3 + Math.random() * 0.5
  };
}
function initFlow() { flowP = []; for (let i = 0; i < FLOW_N; i++) flowP.push(mkFlow()); }

function updateFlow(dt) {
  const base = 120 * flowMul;
  const wideH = CH.wB - CH.wT;
  for (let p of flowP) {
    const h = chanH(p.x);
    const vBase = base * (wideH / Math.max(h, 1));
    const relY = (p.y - topY(p.x)) / h;
    /* 壁面附近减速 */
    const dWall = Math.min(relY, 1 - relY);
    let blF = 1;
    if (dWall < 0.12) blF = 0.35 + 0.65 * (dWall / 0.12);
    /* 微涡旋扰动 */
    let vortY = 0;
    if (p.x > CH.vgX + 30 && dWall < 0.18 && dWall > 0) {
      const str = (1 - dWall / 0.18) * 0.45 * (vortexAngle / 15) * flowMul;
      vortY = Math.sin(time * 4 + p.x * 0.04 + relY * 6) * str * vBase * 0.15;
    }
    p.x += vBase * blF * p.speed * dt;
    p.y += vortY * dt;
    /* 流线约束:保持在通道内 */
    const ty = topY(p.x) + h * 0.02;
    const by = botY(p.x) - h * 0.02;
    if (p.y < ty) p.y = ty;
    if (p.y > by) p.y = by;
    /* 超出右端则重生 */
    if (p.x > CH.nR + 10) Object.assign(p, mkFlow());
  }
}

/* 微涡旋指示器 */
let vortices = [];
function initVortices() {
  vortices = [];
  const startX = CH.nL + 30;
  const endX = CH.nR - 60;
  const count = 14;
  for (let i = 0; i < count; i++) {
    const x = startX + (endX - startX) * (i / (count - 1));
    vortices.push({ x, phase: Math.random() * Math.PI * 2, r: 8 + Math.random() * 4 });
    vortices.push({ x, phase: Math.random() * Math.PI * 2, r: 8 + Math.random() * 4 }); // 底壁
  }
}

function drawVortices() {
  const speed = 3 + flowMul * 2;
  for (let i = 0; i < vortices.length; i++) {
    const v = vortices[i];
    const isTop = i % 2 === 0;
    const wallY = isTop ? topY(v.x) : botY(v.x);
    const cy = isTop ? wallY + chanH(v.x) * 0.07 : wallY - chanH(v.x) * 0.07;
    const rot = time * speed + v.phase;
    /* 绘制旋转螺旋 */
    cx.save();
    cx.translate(v.x, cy);
    cx.rotate(rot * (isTop ? 1 : -1));
    cx.globalAlpha = 0.7;
    cx.strokeStyle = '#00ff88';
    cx.lineWidth = 1.5;
    cx.shadowColor = '#00ff88';
    cx.shadowBlur = 8;
    cx.beginPath();
    for (let a = 0; a < Math.PI * 4; a += 0.15) {
      const rr = v.r * (a / (Math.PI * 4));
      const px = Math.cos(a) * rr;
      const py = Math.sin(a) * rr;
      a === 0 ? cx.moveTo(px, py) : cx.lineTo(px, py);
    }
    cx.stroke();
    /* 中心亮点 */
    cx.beginPath();
    cx.arc(0, 0, 2, 0, Math.PI * 2);
    cx.fillStyle = '#00ff88';
    cx.shadowBlur = 14;
    cx.fill();
    cx.restore();
  }
}

/* 粉尘粒子 */
let dustP = [];
function mkDust() {
  const x = CH.wL + Math.random() * (CH.nR - CH.wL) * 0.5;
  const ty = topY(x), by = botY(x);
  const m = (by - ty) * 0.08;
  return {
    x, y: ty + m + Math.random() * (by - ty - m * 2),
    vx: 0, vy: 0,
    size: 2.5 + Math.random() * 2,
    alpha: 0.6 + Math.random() * 0.4,
    bounced: 0,
    trail: []
  };
}
function initDust() { dustP = []; for (let i = 0; i < dustMax; i++) dustP.push(mkDust()); }

function updateDust(dt) {
  const base = 80 * flowMul;
  const wideH = CH.wB - CH.wT;
  while (dustP.length < dustMax) dustP.push(mkDust());
  while (dustP.length > dustMax) dustP.pop();

  for (let p of dustP) {
    const h = chanH(p.x);
    const relY = (p.y - topY(p.x)) / h;
    const dWallTop = relY;
    const dWallBot = 1 - relY;
    const dWall = Math.min(dWallTop, dWallBot);

    /* 基础水平速度 */
    const vBase = base * (wideH / Math.max(h, 1));
    p.vx = vBase * 0.7;

    /* 涡旋离心力:在窄段近壁处,将颗粒推向中心 */
    if (p.x > CH.vgX + 40 && dWall < 0.22) {
      const str = (1 - dWall / 0.22) * 180 * (vortexAngle / 15) * flowMul;
      const dir = dWallTop < dWallBot ? 1 : -1; // 指向中心
      p.vy += dir * str * dt;
    }

    /* 微弱随机游走 */
    p.vy += (Math.random() - 0.5) * 30 * dt;
    p.vy *= 0.96; // 阻尼

    p.x += p.vx * dt;
    p.y += p.vy * dt;

    /* 超疏水壁面反弹 */
    const ty = topY(p.x) + 4;
    const by = botY(p.x) - 4;
    if (p.y < ty) {
      p.y = ty + 1;
      p.vy = Math.abs(p.vy) * 0.75;
      p.bounced = 8;
    }
    if (p.y > by) {
      p.y = by - 1;
      p.vy = -Math.abs(p.vy) * 0.75;
      p.bounced = 8;
    }
    if (p.bounced > 0) p.bounced -= dt * 30;

    /* 记录轨迹 */
    p.trail.push({ x: p.x, y: p.y });
    if (p.trail.length > 18) p.trail.shift();

    if (p.x > CH.nR + 20) Object.assign(p, mkDust());
  }
}

/* ========== 绘制函数 ========== */

function drawBg() {
  /* 深色渐变背景 */
  const g = cx.createRadialGradient(W * 0.45, H * 0.48, H * 0.1, W * 0.45, H * 0.48, H * 0.9);
  g.addColorStop(0, '#0b1528');
  g.addColorStop(1, '#04080f');
  cx.fillStyle = g;
  cx.fillRect(0, 0, W, H);

  /* 点阵网格 */
  cx.fillStyle = 'rgba(0,180,255,0.04)';
  const sp = 40;
  for (let gx = sp; gx < W; gx += sp) {
    for (let gy = sp; gy < H; gy += sp) {
      cx.fillRect(gx - 0.5, gy - 0.5, 1, 1);
    }
  }
}

function drawChannel() {
  /* 通道内部填充 */
  cx.save();
  cx.beginPath();
  cx.moveTo(CH.wL, CH.wT);
  for (let x = CH.wR; x <= CH.nL; x += 2) cx.lineTo(x, topY(x));
  cx.lineTo(CH.nR, CH.nT);
  cx.lineTo(CH.nR, CH.nB);
  for (let x = CH.nL; x >= CH.wR; x -= 2) cx.lineTo(x, botY(x));
  cx.lineTo(CH.wL, CH.wB);
  cx.closePath();
  const ig = cx.createLinearGradient(0, CH.nT, 0, CH.nB);
  ig.addColorStop(0, '#060e1e');
  ig.addColorStop(0.5, '#0a1830');
  ig.addColorStop(1, '#060e1e');
  cx.fillStyle = ig;
  cx.fill();
  cx.restore();

  /* 壁面线 */
  cx.save();
  cx.lineWidth = 3;
  cx.strokeStyle = '#1a2e50';
  cx.shadowColor = 'rgba(0,180,255,0.15)';
  cx.shadowBlur = 6;
  /* 顶壁 */
  cx.beginPath();
  cx.moveTo(CH.wL, CH.wT);
  for (let x = CH.wR; x <= CH.nL; x += 2) cx.lineTo(x, topY(x));
  cx.lineTo(CH.nR, CH.nT);
  cx.stroke();
  /* 底壁 */
  cx.beginPath();
  cx.moveTo(CH.wL, CH.wB);
  for (let x = CH.wR; x <= CH.nL; x += 2) cx.lineTo(x, botY(x));
  cx.lineTo(CH.nR, CH.nB);
  cx.stroke();
  cx.restore();

  /* 超疏水壁面纹理(窄段) */
  cx.save();
  cx.globalAlpha = 0.12 + 0.04 * Math.sin(time * 1.2);
  const shimmer = cx.createLinearGradient(CH.nL, 0, CH.nR, 0);
  shimmer.addColorStop(0, '#00c8ff');
  shimmer.addColorStop(0.5, '#00e8ff');
  shimmer.addColorStop(1, '#00a0e0');
  cx.strokeStyle = shimmer;
  cx.lineWidth = 5;
  /* 顶壁内表面 */
  cx.beginPath();
  cx.moveTo(CH.nL, CH.nT + 3);
  cx.lineTo(CH.nR, CH.nT + 3);
  cx.stroke();
  /* 底壁内表面 */
  cx.beginPath();
  cx.moveTo(CH.nL, CH.nB - 3);
  cx.lineTo(CH.nR, CH.nB - 3);
  cx.stroke();
  cx.restore();

  /* 壁面微纳结构小齿(装饰) */
  cx.save();
  cx.globalAlpha = 0.08;
  cx.strokeStyle = '#00c8ff';
  cx.lineWidth = 0.8;
  for (let x = CH.nL + 10; x < CH.nR - 10; x += 8) {
    /* 顶壁 */
    cx.beginPath(); cx.moveTo(x, CH.nT); cx.lineTo(x, CH.nT + 6); cx.stroke();
    /* 底壁 */
    cx.beginPath(); cx.moveTo(x, CH.nB); cx.lineTo(x, CH.nB - 6); cx.stroke();
  }
  cx.restore();
}

function drawVortexGenerators() {
  const vgX = CH.vgX;
  const vgH = H * 0.03;
  const angleRad = vortexAngle * Math.PI / 180;
  const count = 6;

  cx.save();
  for (let i = 0; i < count; i++) {
    const frac = (i + 0.5) / count;
    const yTop = CH.wT + (CH.wB - CH.wT) * frac * 0.15 + (CH.wB - CH.wT) * 0.02;
    const yBot = CH.wB - (CH.wB - CH.wT) * frac * 0.15 - (CH.wB - CH.wT) * 0.02;

    /* 顶壁发生器 */
    const tx = vgX - i * 2;
    const ty = topY(tx);
    cx.save();
    cx.translate(tx, ty);
    cx.rotate(angleRad);
    cx.fillStyle = '#1e3a60';
    cx.strokeStyle = '#00d4ff';
    cx.lineWidth = 1.2;
    cx.shadowColor = '#00d4ff';
    cx.shadowBlur = 6;
    cx.beginPath();
    cx.moveTo(0, 0);
    cx.lineTo(vgH * 0.5, -vgH);
    cx.lineTo(-vgH * 0.15, -vgH * 0.6);
    cx.closePath();
    cx.fill(); cx.stroke();
    cx.restore();

    /* 底壁发生器 */
    cx.save();
    cx.translate(tx, botY(tx));
    cx.rotate(-angleRad);
    cx.fillStyle = '#1e3a60';
    cx.strokeStyle = '#00d4ff';
    cx.lineWidth = 1.2;
    cx.shadowColor = '#00d4ff';
    cx.shadowBlur = 6;
    cx.beginPath();
    cx.moveTo(0, 0);
    cx.lineTo(vgH * 0.5, vgH);
    cx.lineTo(-vgH * 0.15, vgH * 0.6);
    cx.closePath();
    cx.fill(); cx.stroke();
    cx.restore();
  }
  cx.restore();
}

function drawAirFilm() {
  if (!showFilm) return;
  cx.save();
  const filmThick = (CH.nB - CH.nT) * 0.06;
  const pulse = 0.55 + 0.15 * Math.sin(time * 2.5);

  /* 顶壁气膜 */
  for (let x = CH.nL + 10; x < CH.nR - 10; x += 3) {
    const wy = topY(x);
    const g = cx.createLinearGradient(x, wy, x, wy + filmThick);
    g.addColorStop(0, `rgba(0,255,136,${0.02 * pulse})`);
    g.addColorStop(0.5, `rgba(0,255,200,${0.12 * pulse})`);
    g.addColorStop(1, `rgba(0,255,136,0)`);
    cx.fillStyle = g;
    cx.fillRect(x, wy, 3, filmThick);
  }
  /* 底壁气膜 */
  for (let x = CH.nL + 10; x < CH.nR - 10; x += 3) {
    const wy = botY(x);
    const g = cx.createLinearGradient(x, wy, x, wy - filmThick);
    g.addColorStop(0, `rgba(0,255,136,${0.02 * pulse})`);
    g.addColorStop(0.5, `rgba(0,255,200,${0.12 * pulse})`);
    g.addColorStop(1, `rgba(0,255,136,0)`);
    cx.fillStyle = g;
    cx.fillRect(x, wy - filmThick, 3, filmThick);
  }
  cx.restore();
}

function drawFlowParticles() {
  cx.save();
  for (let p of flowP) {
    cx.globalAlpha = p.alpha * 0.7;
    cx.fillStyle = '#00d4ff';
    cx.shadowColor = '#00d4ff';
    cx.shadowBlur = 4;
    cx.beginPath();
    cx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    cx.fill();
  }
  cx.restore();
}

function drawDustParticles() {
  cx.save();
  for (let p of dustP) {
    /* 轨迹 */
    if (p.trail.length > 2) {
      cx.beginPath();
      cx.moveTo(p.trail[0].x, p.trail[0].y);
      for (let i = 1; i < p.trail.length; i++) cx.lineTo(p.trail[i].x, p.trail[i].y);
      cx.strokeStyle = `rgba(255,136,0,${0.15})`;
      cx.lineWidth = 1;
      cx.stroke();
    }
    /* 粒子本体 */
    const b = Math.max(0, p.bounced);
    cx.globalAlpha = p.alpha;
    cx.fillStyle = b > 0 ? '#ffcc00' : '#ff8800';
    cx.shadowColor = b > 0 ? '#ffcc00' : '#ff8800';
    cx.shadowBlur = b > 0 ? 12 : 6;
    cx.beginPath();
    cx.arc(p.x, p.y, p.size * (1 + b * 0.06), 0, Math.PI * 2);
    cx.fill();
  }
  cx.restore();
}

/* 噪声抑制可视化:在窄段壁面画被抑制的声波弧线 */
function drawNoiseSuppression() {
  cx.save();
  const waveX = CH.nL + (CH.nR - CH.nL) * 0.35;
  const waveCount = 3;
  for (let i = 0; i < waveCount; i++) {
    const ox = waveX + i * 35;
    /* 顶壁声波 */
    const wy = topY(ox);
    const alpha = 0.08 * (1 - i / waveCount);
    cx.strokeStyle = `rgba(0,200,255,${alpha})`;
    cx.lineWidth = 1;
    cx.beginPath();
    cx.arc(ox, wy, 12 + i * 8, Math.PI * 0.15, Math.PI * 0.85);
    cx.stroke();
    /* 底壁声波 */
    const wy2 = botY(ox);
    cx.beginPath();
    cx.arc(ox, wy2, 12 + i * 8, -Math.PI * 0.85, -Math.PI * 0.15);
    cx.stroke();
  }
  cx.restore();
}

/* 流向箭头 */
function drawFlowArrows() {
  cx.save();
  cx.globalAlpha = 0.25;
  cx.strokeStyle = '#00d4ff';
  cx.fillStyle = '#00d4ff';
  cx.lineWidth = 1.5;
  const positions = [
    { x: CH.wL + 60, y: CH.cy },
    { x: (CH.wR + CH.nL) / 2, y: CH.cy },
    { x: CH.nL + (CH.nR - CH.nL) * 0.3, y: CH.cy },
    { x: CH.nL + (CH.nR - CH.nL) * 0.65, y: CH.cy }
  ];
  for (let p of positions) {
    const len = 18 + 10 * flowMul;
    cx.beginPath();
    cx.moveTo(p.x - len, p.y);
    cx.lineTo(p.x + len, p.y);
    cx.stroke();
    cx.beginPath();
    cx.moveTo(p.x + len, p.y);
    cx.lineTo(p.x + len - 6, p.y - 4);
    cx.lineTo(p.x + len - 6, p.y + 4);
    cx.closePath();
    cx.fill();
  }
  cx.restore();
}

/* 标注层 */
function drawLabels() {
  if (!showLabel) return;
  cx.save();
  cx.font = '600 12px "Noto Sans SC", sans-serif';
  cx.textAlign = 'left';

  const labels = [
    { text: '微涡流发生器阵列', tx: CH.vgX - 40, ty: CH.wT - 28, lx: CH.vgX, ly: CH.wT + 5, color: '#00d4ff' },
    { text: '迎角 ' + vortexAngle + '°', tx: CH.vgX + 40, ty: CH.wB + 30, lx: CH.vgX + 10, ly: CH.wB - 5, color: '#00d4ff' },
    { text: '有序微涡旋', tx: CH.nL + 60, ty: CH.nT - 32, lx: CH.nL + 90, ly: CH.nT + chanH(CH.nL + 90) * 0.07, color: '#00ff88' },
    { text: '气膜轴承', tx: CH.nL + (CH.nR - CH.nL) * 0.4, ty: CH.nT - 32, lx: CH.nL + (CH.nR - CH.nL) * 0.4 + 10, ly: CH.nT + 8, color: '#00ff88' },
    { text: '超疏水壁面 θ>150°', tx: CH.nL + (CH.nR - CH.nL) * 0.6, ty: CH.nB + 22, lx: CH.nL + (CH.nR - CH.nL) * 0.6 + 10, ly: CH.nB - 4, color: '#00c8ff' },
    { text: '粉尘离心甩出', tx: CH.nL + (CH.nR - CH.nL) * 0.25 - 20, ty: CH.nB + 22, lx: CH.nL + (CH.nR - CH.nL) * 0.25, ly: CH.cy + 10, color: '#ff8800' },
  ];

  for (let lb of labels) {
    /* 引线 */
    cx.strokeStyle = lb.color;
    cx.globalAlpha = 0.4;
    cx.lineWidth = 0.8;
    cx.setLineDash([3, 3]);
    cx.beginPath();
    cx.moveTo(lb.tx + 10, lb.ty + 5);
    cx.lineTo(lb.lx, lb.ly);
    cx.stroke();
    cx.setLineDash([]);

    /* 文字 */
    cx.globalAlpha = 0.85;
    cx.fillStyle = lb.color;
    cx.shadowColor = lb.color;
    cx.shadowBlur = 6;
    cx.fillText(lb.text, lb.tx, lb.ty);
    cx.shadowBlur = 0;
  }

  /* 收缩段标注 */
  cx.globalAlpha = 0.3;
  cx.font = '700 11px "Rajdhani", sans-serif';
  cx.fillStyle = '#4a6080';
  cx.textAlign = 'center';
  cx.fillText('CONVERGING SECTION', (CH.wR + CH.nL) / 2, CH.wB + 50);
  cx.fillText('NARROW CHANNEL', (CH.nL + CH.nR) / 2, CH.nB + 50);

  cx.restore();
}

/* ========== 超疏水壁面放大插图 ========== */
function drawInset() {
  const w = 200, h = 150;
  ix.clearRect(0, 0, w, h);

  /* 背景 */
  ix.fillStyle = '#060e1e';
  ix.fillRect(0, 0, w, h);

  /* 微纳结构柱状阵列 */
  ix.save();
  ix.strokeStyle = 'rgba(0,200,255,0.35)';
  ix.lineWidth = 1;
  const baseY = h * 0.75;
  const pillarW = 3, pillarH = 18, gap = 9;
  for (let x = 15; x < w - 10; x += gap) {
    ix.fillStyle = 'rgba(0,180,255,0.15)';
    ix.fillRect(x, baseY - pillarH, pillarW, pillarH);
    ix.strokeRect(x, baseY - pillarH, pillarW, pillarH);
    /* 纳米级二级结构 */
    ix.fillStyle = 'rgba(0,220,255,0.08)';
    ix.fillRect(x + 0.5, baseY - pillarH - 5, pillarW - 1, 5);
  }
  /* 基底 */
  ix.fillStyle = '#1a2e50';
  ix.fillRect(0, baseY, w, h - baseY);
  ix.restore();

  /* 液滴/粉尘颗粒 — 接触角 >150° */
  const dropX = w * 0.5;
  const dropBaseY = baseY - pillarH - 4;
  const dropR = 16;
  /* 极小接触面 */
  ix.save();
  ix.globalAlpha = 0.8;
  const dg = ix.createRadialGradient(dropX - 3, dropBaseY - dropR * 0.9, 2, dropX, dropBaseY - dropR * 0.5, dropR);
  dg.addColorStop(0, 'rgba(255,160,50,0.9)');
  dg.addColorStop(1, 'rgba(255,100,0,0.4)');
  ix.fillStyle = dg;
  ix.beginPath();
  /* 近球形液滴,底部极小接触面 */
  const contactW = 4; // 接触面半宽
  ix.moveTo(dropX - contactW, dropBaseY);
  ix.bezierCurveTo(
    dropX - contactW - 2, dropBaseY - dropR * 0.5,
    dropX - dropR * 0.9, dropBaseY - dropR * 1.1,
    dropX, dropBaseY - dropR * 1.2
  );
  ix.bezierCurveTo(
    dropX + dropR * 0.9, dropBaseY - dropR * 1.1,
    dropX + contactW + 2, dropBaseY - dropR * 0.5,
    dropX + contactW, dropBaseY
  );
  ix.closePath();
  ix.fill();

  /* 接触角标注线 */
  ix.strokeStyle = 'rgba(0,255,136,0.6)';
  ix.lineWidth = 0.8;
  ix.setLineDash([2, 2]);
  /* 水平线 */
  ix.beginPath();
  ix.moveTo(dropX - 25, dropBaseY);
  ix.lineTo(dropX + 25, dropBaseY);
  ix.stroke();
  /* 切线 */
  const tangentAngle = 155 * Math.PI / 180;
  const tLen = 22;
  ix.beginPath();
  ix.moveTo(dropX + contactW, dropBaseY);
  ix.lineTo(dropX + contactW + Math.cos(tangentAngle) * tLen, dropBaseY - Math.sin(tangentAngle) * tLen);
  ix.stroke();
  ix.setLineDash([]);

  /* 角度弧 */
  ix.strokeStyle = '#00ff88';
  ix.lineWidth = 1;
  ix.beginPath();
  ix.arc(dropX + contactW, dropBaseY, 12, -tangentAngle, 0);
  ix.stroke();

  /* 角度值 */
  ix.fillStyle = '#00ff88';
  ix.font = '700 10px "Rajdhani", sans-serif';
  ix.textAlign = 'left';
  ix.fillText('>150°', dropX + contactW + 14, dropBaseY - 4);

  /* 反弹箭头 */
  const bounceT = (time * 1.5) % 1;
  if (bounceT < 0.5) {
    const bt = bounceT * 2;
    ix.globalAlpha = 1 - bt;
    ix.strokeStyle = '#ffcc00';
    ix.lineWidth = 1.5;
    const arrowY = dropBaseY - dropR * 1.2 - 6 - bt * 20;
    ix.beginPath();
    ix.moveTo(dropX, dropBaseY - dropR * 1.2 - 6);
    ix.lineTo(dropX, arrowY);
    ix.stroke();
    ix.beginPath();
    ix.moveTo(dropX, arrowY);
    ix.lineTo(dropX - 4, arrowY + 5);
    ix.moveTo(dropX, arrowY);
    ix.lineTo(dropX + 4, arrowY + 5);
    cx.stroke();
  }
  ix.restore();
}

/* ========== 主动画循环 ========== */
let lastTs = 0;

function animate(ts) {
  const dt = Math.min((ts - lastTs) / 1000, 0.05);
  lastTs = ts;
  time += dt;

  updateFlow(dt);
  updateDust(dt);

  /* 清屏 & 绘制 */
  cx.clearRect(0, 0, W, H);
  drawBg();
  drawChannel();
  drawFlowArrows();
  drawAirFilm();
  drawNoiseSuppression();
  drawFlowParticles();
  drawVortices();
  drawDustParticles();
  drawVortexGenerators();
  drawLabels();
  drawInset();

  requestAnimationFrame(animate);
}

/* ========== 控件 ========== */
document.getElementById('sAngle').addEventListener('input', function() {
  vortexAngle = +this.value;
  document.getElementById('vAngle').textContent = vortexAngle + '°';
});
document.getElementById('sSpeed').addEventListener('input', function() {
  flowMul = +this.value;
  document.getElementById('vSpeed').textContent = flowMul.toFixed(1) + 'x';
});
document.getElementById('sDust').addEventListener('input', function() {
  dustMax = +this.value;
  document.getElementById('vDust').textContent = dustMax;
});
document.getElementById('btnFilm').addEventListener('click', function() {
  showFilm = !showFilm;
  this.classList.toggle('on', showFilm);
});
document.getElementById('btnLabel').addEventListener('click', function() {
  showLabel = !showLabel;
  this.classList.toggle('on', showLabel);
});

/* ========== 初始化 ========== */
function init() {
  calcGeom();
  initFlow();
  initVortices();
  initDust();
  lastTs = performance.now();
  requestAnimationFrame(animate);
}

window.addEventListener('resize', () => {
  calcGeom();
  initVortices();
});

window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

这个动画实现了以下核心要素:

视觉设计方向:采用深空科技美学——深色背景搭配青色(主流场)、霓虹绿(微涡旋/气膜轴承)、琥珀橙(粉尘颗粒)三色体系,营造高端 CFD 可视化质感。

IFR 理想解呈现

  • 微涡流发生器:在收缩段上游以 15° 迎角安装,用发光三角形标识,滑块可调角度
  • 有序微涡旋:窄流道壁面绘制旋转螺旋动画,直观展示"气膜轴承"的形成
  • 气膜轴承:绿色渐变脉冲光层铺在壁面与主流之间,是破题关键——用"气膜"替代实壁面
  • 粉尘离心甩出:橙色颗粒靠近壁面时被涡旋离心力推向中心,接触超疏水壁面时反弹闪黄光
  • 超疏水壁面:窄段壁面以微纳齿纹 + 闪烁纹理呈现,右下角插图放大展示 >150° 接触角原理

交互控制:底部面板可调涡流发生器迎角、流速倍率、粉尘密度,以及切换气膜可视化和标注层显示。所有动画页面加载即自动播放。

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