独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>窄风道气幕隔声防堵 — IFR原理动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#050a15;color:#c8d6e5;font-family:'IBM Plex Mono',monospace;overflow:hidden;height:100vh;display:flex;flex-direction:column}
#title-bar{padding:12px 24px;display:flex;align-items:center;gap:16px;background:rgba(5,10,21,0.95);border-bottom:1px solid rgba(34,211,238,0.12);flex-shrink:0}
#title-bar h1{font-family:'Rajdhani',sans-serif;font-weight:700;font-size:20px;color:#22d3ee;letter-spacing:2px;text-transform:uppercase}
#title-bar .subtitle{font-size:11px;color:#475569;font-weight:300}
.ifr-badge{margin-left:auto;padding:3px 12px;border:1px solid rgba(34,211,238,0.3);border-radius:4px;font-family:'Rajdhani',sans-serif;font-size:11px;color:#22d3ee;letter-spacing:1px;background:rgba(34,211,238,0.05)}
#canvas-wrap{flex:1;position:relative;min-height:0}
canvas{display:block;width:100%;height:100%}
#controls{padding:10px 24px;background:rgba(5,10,21,0.97);border-top:1px solid rgba(34,211,238,0.12);display:flex;align-items:center;gap:28px;flex-wrap:wrap;flex-shrink:0}
.cg{display:flex;align-items:center;gap:8px}
.cg label{font-size:11px;color:#64748b;white-space:nowrap}
.cg .val{font-size:11px;color:#22d3ee;min-width:36px;text-align:right;font-weight:500}
input[type=range]{-webkit-appearance:none;width:110px;height:3px;background:#1e293b;border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;background:#22d3ee;border-radius:50%;cursor:pointer;box-shadow:0 0 8px rgba(34,211,238,0.45)}
.legend{display:flex;gap:14px;margin-left:auto}
.li{display:flex;align-items:center;gap:5px;font-size:10px;color:#64748b}
.ld{width:7px;height:7px;border-radius:50%}
</style>
</head>
<body>
<div id="title-bar">
<h1>窄风道气幕隔声防堵原理</h1>
<span class="subtitle">双重隔离 · 源头消声 · 气幕防堵</span>
<span class="ifr-badge">IFR 最终理想解</span>
</div>
<div id="canvas-wrap"><canvas id="cv"></canvas></div>
<div id="controls">
<div class="cg"><label>气幕压力</label><input type="range" id="slP" min="0" max="100" value="82"><span class="val" id="vP">82%</span></div>
<div class="cg"><label>降噪增益</label><input type="range" id="slA" min="0" max="100" value="80"><span class="val" id="vA">80%</span></div>
<div class="legend">
<div class="li"><div class="ld" style="background:#fb923c;box-shadow:0 0 4px #fb923c"></div>主气流</div>
<div class="li"><div class="ld" style="background:#22d3ee;box-shadow:0 0 4px #22d3ee"></div>气幕射流</div>
<div class="li"><div class="ld" style="background:#d97706;box-shadow:0 0 4px #d97706"></div>粉尘</div>
<div class="li"><div class="ld" style="background:#f87171;box-shadow:0 0 4px #f87171"></div>噪声波</div>
<div class="li"><div class="ld" style="background:#4ade80;box-shadow:0 0 4px #4ade80"></div>反相声波</div>
</div>
</div>
<script>
/* ====== 画布初始化 ====== */
const cv = document.getElementById('cv');
const c = cv.getContext('2d');
const wrap = document.getElementById('canvas-wrap');
let W, H, dpr;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = wrap.clientWidth; H = wrap.clientHeight;
cv.width = W * dpr; cv.height = H * dpr;
cv.style.width = W + 'px'; cv.style.height = H + 'px';
c.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
/* ====== 虚拟坐标 (1400×800) ====== */
const VW = 1400, VH = 800;
const sx = x => x / VW * W;
const sy = y => y / VH * H;
const sl = l => l / VW * W;
/* ====== 风道几何 ====== */
const D = {
il: 70, ir: 230, it: 175, ib: 525, // 宽入口
cl: 230, cr: 320, // 收缩段
nl: 320, nr: 1120, nt: 245, nb: 495, // 窄段
wt: 14 // 壁厚
};
/* ====== 射流孔 ====== */
const jStart = 340, jEnd = 1100, jSp = 48;
const jHoles = [];
for (let x = jStart; x <= jEnd; x += jSp) jHoles.push(x);
const jetAngle = 15 * Math.PI / 180;
/* ====== 控制参数 ====== */
let cPressure = 0.82, ancGain = 0.80;
document.getElementById('slP').oninput = function() { cPressure = this.value / 100; document.getElementById('vP').textContent = this.value + '%'; };
document.getElementById('slA').oninput = function() { ancGain = this.value / 100; document.getElementById('vA').textContent = this.value + '%'; };
/* ====== 粒子系统 ====== */
let t = 0;
const flows = [], curtains = [], dusts = [], sparks = [];
// 获取某x处风道上下界(内壁)
function ductBounds(x) {
if (x < D.cl) return [D.it + 12, D.ib - 12];
if (x < D.cr) {
const r = (x - D.cl) / (D.cr - D.cl);
const s = r * r * (3 - 2 * r); // smoothstep
return [D.it + 12 + s * (D.nt + 14 - D.it - 12), D.ib - 12 - s * (D.ib - 12 - D.nb + 14)];
}
return [D.nt + 14, D.nb - 14];
}
// 气幕屏障位置
function curtainBarriers() {
const depth = (D.nb - D.nt) * 0.14 * cPressure;
return [D.nt + 14 + depth, D.nb - 14 - depth];
}
class Flow {
constructor() { this.reset(); }
reset() {
this.x = D.il - 10 + Math.random() * 40;
this.y = D.it + 30 + Math.random() * (D.ib - D.it - 60);
this.vx = 1.8 + Math.random() * 0.8;
this.vy = 0; this.ph = Math.random() * 6.28;
this.sz = 1.8 + Math.random() * 1.8;
}
update(dt) {
const [top, bot] = ductBounds(this.x);
const [ct, cb] = this.x >= D.nl ? curtainBarriers() : [top, bot];
const effTop = this.x >= D.nl ? ct : top;
const effBot = this.x >= D.nl ? cb : bot;
// 窄段加速
if (this.x >= D.nl) {
this.vx = 3.0 + Math.random() * 0.4;
this.vy += Math.sin(t * 6 + this.ph + this.x * 0.015) * 0.18;
} else if (this.x >= D.cl) {
this.vx = 2.2 + Math.random() * 0.3;
}
this.x += this.vx * dt * 60;
this.y += this.vy * dt * 60;
this.vy *= 0.93;
if (this.y < effTop + 2) { this.y = effTop + 2; this.vy = Math.abs(this.vy) * 0.4; }
if (this.y > effBot - 2) { this.y = effBot - 2; this.vy = -Math.abs(this.vy) * 0.4; }
if (this.x > D.nr + 30) this.reset();
}
}
class Curtain {
constructor(side, hx) { this.side = side; this.hx = hx; this.reset(); }
reset() {
this.x = this.hx; this.y = this.side === 't' ? D.nt + 2 : D.nb - 2;
const spd = 1.2 + Math.random() * 0.8;
this.vx = Math.cos(jetAngle) * spd;
this.vy = this.side === 't' ? Math.sin(jetAngle) * spd : -Math.sin(jetAngle) * spd;
this.sz = 1.3 + Math.random() * 1.2;
this.age = 0; this.maxAge = 2.5 + Math.random() * 1.5;
}
update(dt) {
this.age += dt;
const depth = (D.nb - D.nt) * 0.15;
this.x += this.vx * dt * 60 * cPressure;
this.y += this.vy * dt * 60 * cPressure;
this.vy *= 0.97;
if (this.side === 't' && this.y > D.nt + depth) { this.y = D.nt + depth; this.vy = 0; }
if (this.side === 'b' && this.y < D.nb - depth) { this.y = D.nb - depth; this.vy = 0; }
if (this.age > this.maxAge || this.x > D.nr + 5) this.reset();
}
get life() { return Math.max(0, 1 - this.age / this.maxAge); }
}
class Dust {
constructor() { this.reset(); }
reset() {
this.x = D.nl + Math.random() * (D.nr - D.nl) * 0.4;
this.y = (D.nt + D.nb) / 2 + (Math.random() - 0.5) * (D.nb - D.nt) * 0.4;
this.vx = 1.0 + Math.random() * 0.6;
this.vy = (Math.random() - 0.5) * 0.2;
this.sz = 2.5 + Math.random() * 2;
this.drift = Math.random() > 0.5 ? 1 : -1;
this.dep = false; this.depT = 0;
}
update(dt) {
if (this.dep) { this.depT += dt; if (this.depT > 3) this.reset(); return; }
this.x += this.vx * dt * 60;
this.vy += this.drift * 0.025;
this.y += this.vy * dt * 60;
const [ct, cb] = curtainBarriers();
const repF = cPressure * 0.4;
if (this.y < ct + 8 && this.y > D.nt) { this.vy += repF; this.drift = 1; spawnSparks(this.x, this.y, 't'); }
if (this.y > cb - 8 && this.y < D.nb) { this.vy -= repF; this.drift = -1; spawnSparks(this.x, this.y, 'b'); }
this.vy *= 0.94;
if (this.y <= D.nt + 4) { if (cPressure < 0.25) { this.dep = true; this.y = D.nt + 4; } else { this.vy = 0.5; } }
if (this.y >= D.nb - 4) { if (cPressure < 0.25) { this.dep = true; this.y = D.nb - 4; } else { this.vy = -0.5; } }
if (this.x > D.nr + 15) this.reset();
}
}
function spawnSparks(x, y, side) {
if (Math.random() > 0.15) return;
for (let i = 0; i < 3; i++) {
sparks.push({ x, y, vx: (Math.random() - 0.5) * 2, vy: side === 't' ? -(1 + Math.random() * 2) : (1 + Math.random() * 2), life: 1 });
}
}
/* ====== 初始化粒子 ====== */
function initParticles() {
flows.length = 0; curtains.length = 0; dusts.length = 0; sparks.length = 0;
for (let i = 0; i < 90; i++) flows.push(new Flow());
for (const hx of jHoles) { curtains.push(new Curtain('t', hx)); curtains.push(new Curtain('b', hx)); }
for (let i = 0; i < 22; i++) dusts.push(new Dust());
}
/* ====== 绘制函数 ====== */
function drawBg() {
c.fillStyle = '#050a15';
c.fillRect(0, 0, W, H);
// 细网格
c.strokeStyle = 'rgba(34,211,238,0.025)';
c.lineWidth = 0.5;
const gs = sl(50);
for (let x = 0; x < W; x += gs) { c.beginPath(); c.moveTo(x, 0); c.lineTo(x, H); c.stroke(); }
for (let y = 0; y < H; y += gs) { c.beginPath(); c.moveTo(0, y); c.lineTo(W, y); c.stroke(); }
}
function drawDuct() {
// 外壁(厚壁金属质感)
const wallGrad = c.createLinearGradient(0, sy(D.nt - D.wt), 0, sy(D.nt));
wallGrad.addColorStop(0, '#374151');
wallGrad.addColorStop(0.5, '#6b7280');
wallGrad.addColorStop(1, '#4b5563');
// 上壁 - 宽段
c.fillStyle = '#3f4756';
c.beginPath();
c.moveTo(sx(D.il), sy(D.it - D.wt));
c.lineTo(sx(D.il), sy(D.it));
c.lineTo(sx(D.cl), sy(D.it));
c.lineTo(sx(D.cr), sy(D.nt));
c.lineTo(sx(D.nr), sy(D.nt));
c.lineTo(sx(D.nr), sy(D.nt - D.wt));
c.lineTo(sx(D.cr), sy(D.nt - D.wt));
c.lineTo(sx(D.cl), sy(D.it - D.wt));
c.closePath();
c.fill();
// 上壁高光
c.strokeStyle = 'rgba(148,163,184,0.3)';
c.lineWidth = 1;
c.beginPath();
c.moveTo(sx(D.il), sy(D.it - D.wt + 1));
c.lineTo(sx(D.cl), sy(D.it - D.wt + 1));
c.lineTo(sx(D.cr), sy(D.nt - D.wt + 1));
c.lineTo(sx(D.nr), sy(D.nt - D.wt + 1));
c.stroke();
// 下壁
c.fillStyle = '#3f4756';
c.beginPath();
c.moveTo(sx(D.il), sy(D.ib));
c.lineTo(sx(D.il), sy(D.ib + D.wt));
c.lineTo(sx(D.cl), sy(D.ib + D.wt));
c.lineTo(sx(D.cr), sy(D.nb + D.wt));
c.lineTo(sx(D.nr), sy(D.nb + D.wt));
c.lineTo(sx(D.nr), sy(D.nb));
c.lineTo(sx(D.cr), sy(D.nb));
c.lineTo(sx(D.cl), sy(D.ib));
c.closePath();
c.fill();
// 下壁高光
c.strokeStyle = 'rgba(148,163,184,0.2)';
c.beginPath();
c.moveTo(sx(D.nr), sy(D.nb + D.wt - 1));
c.lineTo(sx(D.cr), sy(D.nb + D.wt - 1));
c.lineTo(sx(D.cl), sy(D.ib + D.wt - 1));
c.lineTo(sx(D.il), sy(D.ib + D.wt - 1));
c.stroke();
// 内壁线
c.strokeStyle = 'rgba(148,163,184,0.35)';
c.lineWidth = 1;
c.beginPath();
c.moveTo(sx(D.il), sy(D.it));
c.lineTo(sx(D.cl), sy(D.it));
c.quadraticCurveTo(sx(D.cl + 40), sy(D.it), sx(D.cr), sy(D.nt));
c.lineTo(sx(D.nr), sy(D.nt));
c.stroke();
c.beginPath();
c.moveTo(sx(D.il), sy(D.ib));
c.lineTo(sx(D.cl), sy(D.ib));
c.quadraticCurveTo(sx(D.cl + 40), sy(D.ib), sx(D.cr), sy(D.nb));
c.lineTo(sx(D.nr), sy(D.nb));
c.stroke();
// 射流孔
for (const hx of jHoles) {
// 上壁孔
c.fillStyle = '#22d3ee';
c.shadowColor = '#22d3ee';
c.shadowBlur = sl(6) * cPressure;
c.beginPath();
c.arc(sx(hx), sy(D.nt), sl(3), 0, Math.PI * 2);
c.fill();
// 下壁孔
c.beginPath();
c.arc(sx(hx), sy(D.nb), sl(3), 0, Math.PI * 2);
c.fill();
}
c.shadowBlur = 0;
// 入口/出口箭头区域标记
c.fillStyle = 'rgba(148,163,184,0.12)';
c.font = `${sl(11)}px 'IBM Plex Mono'`;
c.textAlign = 'center';
c.fillText('入口', sx(D.il + 30), sy(D.it - D.wt - 10));
c.fillText('出口', sx(D.nr - 30), sy(D.nt - D.wt - 10));
}
function drawCurtainGlow() {
if (cPressure < 0.05) return;
const depth = (D.nb - D.nt) * 0.14 * cPressure;
const alpha = cPressure * 0.35;
// 上气幕发光区
const gTop = c.createLinearGradient(0, sy(D.nt), 0, sy(D.nt + depth));
gTop.addColorStop(0, `rgba(34,211,238,${alpha})`);
gTop.addColorStop(0.6, `rgba(34,211,238,${alpha * 0.4})`);
gTop.addColorStop(1, 'rgba(34,211,238,0)');
c.fillStyle = gTop;
c.fillRect(sx(D.nl), sy(D.nt), sx(D.nr - D.nl), sy(depth));
// 下气幕发光区
const gBot = c.createLinearGradient(0, sy(D.nb), 0, sy(D.nb - depth));
gBot.addColorStop(0, `rgba(34,211,238,${alpha})`);
gBot.addColorStop(0.6, `rgba(34,211,238,${alpha * 0.4})`);
gBot.addColorStop(1, 'rgba(34,211,238,0)');
c.fillStyle = gBot;
c.fillRect(sx(D.nl), sy(D.nb - depth), sx(D.nr - D.nl), sy(depth));
// 脉冲光环
const pulse = 0.5 + 0.5 * Math.sin(t * 3);
c.strokeStyle = `rgba(34,211,238,${0.08 * cPressure * pulse})`;
c.lineWidth = sl(2);
c.beginPath();
c.moveTo(sx(D.nl), sy(D.nt + depth));
c.lineTo(sx(D.nr), sy(D.nt + depth));
c.stroke();
c.beginPath();
c.moveTo(sx(D.nl), sy(D.nb - depth));
c.lineTo(sx(D.nr), sy(D.nb - depth));
c.stroke();
}
function drawParticles() {
// 主气流
for (const p of flows) {
c.fillStyle = `rgba(251,146,60,0.75)`;
c.shadowColor = '#fb923c';
c.shadowBlur = sl(4);
c.beginPath();
c.arc(sx(p.x), sy(p.y), sl(p.sz), 0, Math.PI * 2);
c.fill();
}
c.shadowBlur = 0;
// 气幕射流
for (const p of curtains) {
const a = p.life * 0.85 * cPressure;
c.fillStyle = `rgba(34,211,238,${a})`;
c.shadowColor = '#22d3ee';
c.shadowBlur = sl(5) * cPressure;
c.beginPath();
c.arc(sx(p.x), sy(p.y), sl(p.sz), 0, Math.PI * 2);
c.fill();
}
c.shadowBlur = 0;
// 粉尘
for (const p of dusts) {
const a = p.dep ? Math.max(0, 1 - p.depT / 3) * 0.9 : 0.9;
c.fillStyle = `rgba(217,119,6,${a})`;
c.shadowColor = '#d97706';
c.shadowBlur = sl(3);
c.beginPath();
c.arc(sx(p.x), sy(p.y), sl(p.sz), 0, Math.PI * 2);
c.fill();
if (!p.dep) {
// 粉尘轨迹
c.strokeStyle = `rgba(217,119,6,${a * 0.25})`;
c.lineWidth = sl(0.8);
c.beginPath();
c.moveTo(sx(p.x), sy(p.y));
c.lineTo(sx(p.x - p.vx * 8), sy(p.y - p.vy * 8));
c.stroke();
}
}
c.shadowBlur = 0;
// 偏转火花
for (const s of sparks) {
const a = s.life * 0.8;
c.fillStyle = `rgba(34,211,238,${a})`;
c.beginPath();
c.arc(sx(s.x), sy(s.y), sl(1.2), 0, Math.PI * 2);
c.fill();
}
}
function drawExternalComponents() {
// 麦克风阵列(上方)
const micY = D.nt - D.wt - 45;
c.fillStyle = '#64748b';
c.font = `500 ${sl(10)}px 'IBM Plex Mono'`;
c.textAlign = 'center';
c.fillText('麦克风阵列', sx((D.nl + D.nr) / 2), sy(micY - 18));
for (let i = 0; i < 5; i++) {
const mx = D.nl + 80 + i * 150;
// 麦克风图标
c.fillStyle = '#94a3b8';
c.beginPath();
c.arc(sx(mx), sy(micY), sl(5), 0, Math.PI * 2);
c.fill();
c.fillStyle = '#475569';
c.beginPath();
c.arc(sx(mx), sy(micY), sl(3), 0, Math.PI * 2);
c.fill();
// 连接线到壁面
c.strokeStyle = 'rgba(148,163,184,0.15)';
c.lineWidth = 1;
c.setLineDash([3, 4]);
c.beginPath();
c.moveTo(sx(mx), sy(micY + 5));
c.lineTo(sx(mx), sy(D.nt - D.wt));
c.stroke();
c.setLineDash([]);
// 采集波纹
const waveR = sl(8 + Math.sin(t * 4 + i) * 3);
c.strokeStyle = `rgba(248,113,113,${0.25 + 0.15 * Math.sin(t * 4 + i)})`;
c.lineWidth = 1;
c.beginPath();
c.arc(sx(mx), sy(micY), waveR, 0, Math.PI * 2);
c.stroke();
}
// 反相声波发生器(下方)
const spkY = D.nb + D.wt + 55;
c.fillStyle = '#64748b';
c.font = `500 ${sl(10)}px 'IBM Plex Mono'`;
c.textAlign = 'center';
c.fillText('反相声波发生器', sx((D.nl + D.nr) / 2), sy(spkY + 25));
// 控制器
const ctrlX = (D.nl + D.nr) / 2;
const ctrlY = spkY;
c.fillStyle = '#1e293b';
c.strokeStyle = '#4ade80';
c.lineWidth = 1.5;
const cw = sl(70), ch = sl(28);
c.fillRect(sx(ctrlX) - cw / 2, sy(ctrlY) - ch / 2, cw, ch);
c.strokeRect(sx(ctrlX) - cw / 2, sy(ctrlY) - ch / 2, cw, ch);
c.fillStyle = '#4ade80';
c.font = `500 ${sl(9)}px 'IBM Plex Mono'`;
c.fillText('ANC 控制器', sx(ctrlX), sy(ctrlY + 3));
// 延迟标注
c.fillStyle = '#475569';
c.font = `300 ${sl(8)}px 'IBM Plex Mono'`;
c.fillText('延迟 < 1ms', sx(ctrlX), sy(ctrlY + 18));
// 扬声器
for (let i = 0; i < 3; i++) {
const spx = D.nl + 180 + i * 220;
c.fillStyle = '#4ade80';
c.shadowColor = '#4ade80';
c.shadowBlur = sl(4) * ancGain;
// 三角形扬声器
c.beginPath();
c.moveTo(sx(spx - 8), sy(spkY - 6));
c.lineTo(sx(spx + 8), sy(spkY));
c.lineTo(sx(spx - 8), sy(spkY + 6));
c.closePath();
c.fill();
c.shadowBlur = 0;
// 反相声波弧线
for (let j = 1; j <= 3; j++) {
const r = sl(10 + j * 10 + Math.sin(t * 5 + i + j) * 3);
const a = ancGain * (0.4 - j * 0.1);
c.strokeStyle = `rgba(74,222,128,${Math.max(0, a)})`;
c.lineWidth = sl(1.2);
c.beginPath();
c.arc(sx(spx + 8), sy(spkY), r, -0.5, 0.5);
c.stroke();
}
}
// 噪声波弧线(从壁面发出向上)
for (let i = 0; i < 4; i++) {
const nx = D.nl + 150 + i * 200;
const noiseAmp = (1 - cPressure * 0.4);
for (let j = 1; j <= 3; j++) {
const r = sl(8 + j * 9 + Math.sin(t * 4.5 + i * 0.7 + j) * 3);
const a = noiseAmp * (0.3 - j * 0.08);
c.strokeStyle = `rgba(248,113,113,${Math.max(0, a)})`;
c.lineWidth = sl(1);
c.beginPath();
c.arc(sx(nx), sy(D.nt - D.wt), r, Math.PI + 0.4, Math.PI * 2 - 0.4);
c.stroke();
}
}
}
function drawWavePanel() {
// 波形分析面板
const px = 1150, py = 170, pw = 230, ph = 380;
// 面板背景
c.fillStyle = 'rgba(15,23,42,0.85)';
c.strokeStyle = 'rgba(34,211,238,0.15)';
c.lineWidth = 1;
c.beginPath();
c.roundRect(sx(px), sy(py), sx(pw), sy(ph), sl(6));
c.fill();
c.stroke();
// 标题
c.fillStyle = '#22d3ee';
c.font = `700 ${sl(12)}px 'Rajdhani'`;
c.textAlign = 'center';
c.fillText('声波相消分析', sx(px + pw / 2), sy(py + 22));
const noiseAmp = 1 - cPressure * 0.35;
const antiAmp = noiseAmp * ancGain;
const resultAmp = Math.abs(noiseAmp - antiAmp);
const sections = [
{ label: '残余噪声', color: '#f87171', amp: noiseAmp, yOff: 50 },
{ label: '反相声波', color: '#4ade80', amp: antiAmp, yOff: 160 },
{ label: '叠加结果', color: '#e2e8f0', amp: resultAmp, yOff: 270 },
];
for (const sec of sections) {
const secY = py + sec.yOff;
// 标签
c.fillStyle = '#64748b';
c.font = `400 ${sl(9)}px 'IBM Plex Mono'`;
c.textAlign = 'left';
c.fillText(sec.label, sx(px + 12), sy(secY));
// 幅值
c.fillStyle = sec.color;
c.font = `500 ${sl(9)}px 'IBM Plex Mono'`;
c.textAlign = 'right';
c.fillText((sec.amp * 100).toFixed(0) + '%', sx(px + pw - 12), sy(secY));
// 波形
const waveY = secY + 30;
const waveH = 35;
c.strokeStyle = sec.color;
c.lineWidth = sl(1.5);
c.shadowColor = sec.color;
c.shadowBlur = sec.amp > 0.1 ? sl(4) : 0;
c.beginPath();
for (let i = 0; i <= pw - 24; i++) {
const x = px + 12 + i;
const phase = t * 4 + i * 0.06;
const y = waveY + waveH / 2 + Math.sin(phase) * waveH / 2 * sec.amp;
if (i === 0) c.moveTo(sx(x), sy(y));
else c.lineTo(sx(x), sy(y));
}
c.stroke();
c.shadowBlur = 0;
// 中线
c.strokeStyle = 'rgba(100,116,139,0.15)';
c.lineWidth = 0.5;
c.beginPath();
c.moveTo(sx(px + 12), sy(waveY + waveH / 2));
c.lineTo(sx(px + pw - 12), sy(waveY + waveH / 2));
c.stroke();
}
// IFR 效果指示
const ifrY = py + ph - 38;
const ifrScore = Math.max(0, (1 - resultAmp) * 100);
c.fillStyle = ifrScore > 80 ? '#22d3ee' : ifrScore > 50 ? '#fbbf24' : '#f87171';
c.font = `700 ${sl(14)}px 'Rajdhani'`;
c.textAlign = 'center';
c.fillText(`IFR 达成度 ${ifrScore.toFixed(0)}%`, sx(px + pw / 2), sy(ifrY));
// 进度条
const barW = pw - 30, barH = 6;
c.fillStyle = '#1e293b';
c.fillRect(sx(px + 15), sy(ifrY + 8), sx(barW), sy(barH));
c.fillStyle = ifrScore > 80 ? '#22d3ee' : ifrScore > 50 ? '#fbbf24' : '#f87171';
c.fillRect(sx(px + 15), sy(ifrY + 8), sx(barW * ifrScore / 100), sy(barH));
}
function drawLabels() {
c.textAlign = 'center';
// 收缩段标注
c.fillStyle = 'rgba(148,163,184,0.4)';
c.font = `300 ${sl(9)}px 'IBM Plex Mono'`;
c.fillText('收缩段', sx((D.cl + D.cr) / 2), sy(D.it - D.wt - 28));
// 窄段标注
c.fillText('窄风道', sx((D.nl + D.nr) / 2), sy(D.nb + D.wt + 85 + 38));
// 气幕层标注
if (cPressure > 0.2) {
const depth = (D.nb - D.nt) * 0.14 * cPressure;
c.fillStyle = `rgba(34,211,238,${0.5 * cPressure})`;
c.font = `500 ${sl(9)}px 'IBM Plex Mono'`;
c.save();
c.translate(sx(D.nr + 22), sy(D.nt + depth / 2));
c.rotate(-Math.PI / 2);
c.fillText('气幕隔离层', 0, 0);
c.restore();
c.save();
c.translate(sx(D.nr + 22), sy(D.nb - depth / 2));
c.rotate(-Math.PI / 2);
c.fillText('气幕隔离层', 0, 0);
c.restore();
}
// 主流区标注
c.fillStyle = 'rgba(251,146,60,0.4)';
c.font = `300 ${sl(9)}px 'IBM Plex Mono'`;
c.fillText('湍流核心区', sx((D.nl + D.nr) / 2), sy((D.nt + D.nb) / 2));
// 射流角度标注
if (cPressure > 0.3) {
const ax = D.nl + 60;
c.strokeStyle = 'rgba(34,211,238,0.3)';
c.lineWidth = 1;
c.setLineDash([2, 3]);
// 上壁
c.beginPath();
c.moveTo(sx(ax), sy(D.nt));
c.lineTo(sx(ax + 40), sy(D.nt));
c.stroke();
c.beginPath();
c.moveTo(sx(ax), sy(D.nt));
c.lineTo(sx(ax + 40 * Math.cos(jetAngle)), sy(D.nt + 40 * Math.sin(jetAngle)));
c.stroke();
c.setLineDash([]);
// 角度弧
c.strokeStyle = 'rgba(34,211,238,0.35)';
c.beginPath();
c.arc(sx(ax), sy(D.nt), sl(18), 0, jetAngle);
c.stroke();
c.fillStyle = 'rgba(34,211,238,0.5)';
c.font = `300 ${sl(8)}px 'IBM Plex Mono'`;
c.textAlign = 'left';
c.fillText('15°', sx(ax + 20), sy(D.nt + 14));
}
// 粉尘偏转标注
c.fillStyle = 'rgba(217,119,6,0.45)';
c.font = `300 ${sl(8)}px 'IBM Plex Mono'`;
c.textAlign = 'center';
c.fillText('粉尘偏转', sx(D.nl + 200), sy(D.nb - (D.nb - D.nt) * 0.14 * cPressure - 12));
// 方向箭头 - 主气流
c.strokeStyle = 'rgba(251,146,60,0.3)';
c.lineWidth = sl(1.5);
const arY = (D.nt + D.nb) / 2;
for (let i = 0; i < 3; i++) {
const arX = D.il + 20 + i * 60;
c.beginPath();
c.moveTo(sx(arX), sy(arY));
c.lineTo(sx(arX + 20), sy(arY));
c.lineTo(sx(arX + 15), sy(arY - 5));
c.moveTo(sx(arX + 20), sy(arY));
c.lineTo(sx(arX + 15), sy(arY + 5));
c.stroke();
}
}
function drawFlowArrows() {
// 大气流动方向箭头(入口左侧)
c.strokeStyle = 'rgba(251,146,60,0.2)';
c.lineWidth = sl(2);
const ay = (D.it + D.ib) / 2;
c.beginPath();
c.moveTo(sx(30), sy(ay));
c.lineTo(sx(D.il - 5), sy(ay));
c.stroke();
c.beginPath();
c.moveTo(sx(D.il - 5), sy(ay));
c.lineTo(sx(D.il - 12), sy(ay - 8));
c.moveTo(sx(D.il - 5), sy(ay));
c.lineTo(sx(D.il - 12), sy(ay + 8));
c.stroke();
c.fillStyle = 'rgba(251,146,60,0.25)';
c.font = `300 ${sl(9)}px 'IBM Plex Mono'`;
c.textAlign = 'center';
c.fillText('主气流', sx(48), sy(ay - 12));
// 出口箭头
c.strokeStyle = 'rgba(148,163,184,0.15)';
c.beginPath();
c.moveTo(sx(D.nr + 5), sy(ay));
c.lineTo(sx(D.nr + 40), sy(ay));
c.stroke();
c.beginPath();
c.moveTo(sx(D.nr + 40), sy(ay));
c.lineTo(sx(D.nr + 33), sy(ay - 7));
c.moveTo(sx(D.nr + 40), sy(ay));
c.lineTo(sx(D.nr + 33), sy(ay + 7));
c.stroke();
}
/* ====== 主动画循环 ====== */
let lastTime = 0;
function animate(now) {
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
t += dt;
// 更新粒子
for (const p of flows) p.update(dt);
for (const p of curtains) p.update(dt);
for (const p of dusts) p.update(dt);
// 火花更新
for (let i = sparks.length - 1; i >= 0; i--) {
const s = sparks[i];
s.x += s.vx * dt * 60;
s.y += s.vy * dt * 60;
s.life -= dt * 3.5;
if (s.life <= 0) sparks.splice(i, 1);
}
// 绘制
drawBg();
drawFlowArrows();
drawCurtainGlow();
drawParticles();
drawDuct();
drawExternalComponents();
drawWavePanel();
drawLabels();
requestAnimationFrame(animate);
}
/* ====== 初始化与自动播放 ====== */
function init() {
resize();
initParticles();
lastTime = performance.now();
requestAnimationFrame(animate);
}
// 页面加载后自动播放,重开即播
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.addEventListener('load', init);
</script>
</body>
</html>
这个动画实现了以下核心内容:
视觉化原理展示(IFR 导向):
气幕隔离层 — 青色(cyan)发光带沿窄风道内壁流动,清晰呈现"气垫扫帚"效应:将湍流主气流与壁面物理隔离,从源头消除摩擦噪声产生条件,同时阻止粉尘接触壁面沉积。射流孔以 15° 顺流倾角喷射,带有角度标注。
粉尘偏转效果 — 棕色粉尘颗粒在靠近壁面时被气幕屏障弹回核心区,碰撞瞬间产生青色火花,直观展示防堵机制。当气幕压力降至极低时,粉尘才会沉积到壁面。
有源降噪相消 — 右侧波形面板实时展示三组波形:残余噪声(红)、反相声波(绿)、叠加结果(白),通过振幅变化清晰呈现声波相消原理。外部麦克风阵列和反相发生器以图标 + 波纹动画呈现。
交互设计:
- 气幕压力滑块 — 调节气幕强度,实时影响隔离层厚度、粉尘偏转效果和噪声振幅
- 降噪增益滑块 — 调节 ANC 强度,实时影响反相声波振幅和叠加结果
- IFR 达成度指标 — 右下角综合评分,两项参数均拉满时达到最高理想解状态
自动播放 — 页面加载后动画立即自动运行,所有粒子系统和波形持续循环,无需手动触发。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
