独立渲染引擎就绪引擎就绪
<!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分
等待动画代码生成...
