dixiebone13-a11y
Neural-Flow v3.2: Cyber-Fluid Dynamics Lab β€” Vite+Tailwind+Chart.js, all 11 audit fixes
64f8e0a
import './index.css';
/* fix #2: tree-shakeable Chart.js import instead of CDN */
import {
Chart,
LineController,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Filler,
} from 'chart.js';
Chart.register(
LineController,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Filler,
);
/* ═══════════════════════════════════════════════════════════════
Neural-Flow v3.2 β€” Cyber-Fluid Dynamics Lab
All 11 audit fixes applied β€” see comments marked "fix #N"
═══════════════════════════════════════════════════════════════ */
/* ── DOM refs β€” fix #11: cache once ────────────────────────── */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const dragDisplay = document.getElementById('dragDisplay');
const liftDisplay = document.getElementById('liftDisplay');
const factText = document.getElementById('factText');
const mainUI = document.getElementById('mainUI');
const statsUI = document.getElementById('statsUI');
const modeEulerBtn = document.getElementById('modeEuler');
const modeNSBtn = document.getElementById('modeNS');
const presetSingleBtn = document.getElementById('presetSingle');
const presetDualBtn = document.getElementById('presetDual');
const presetAirfoilBtn = document.getElementById('presetAirfoil');
const speedSlider = document.getElementById('speedSlider');
const speedVal = document.getElementById('speedVal');
const rotSlider = document.getElementById('rotSlider');
const rotVal = document.getElementById('rotVal');
const togglePressureBtn = document.getElementById('togglePressure');
const toggleVectorsBtn = document.getElementById('toggleVectors');
const forceChartCanvas = document.getElementById('forceChart');
/* ── State ─────────────────────────────────────────────────── */
let mode = 'euler';
let speed = 1.5;
let circulation = 0;
let showPressure = false;
let showVectors = false;
let bodies = [];
let particles = [];
const PARTICLE_COUNT = 800;
let time = 0;
let chart;
let currentPreset = 'single';
/* fix #9: cached layout values, updated on resize */
let canvasW = 0;
let canvasH = 0;
let dpr = 1;
/* fix #4: stored rAF handle */
let rafId = 0;
/* fix #7: cached last display values to avoid redundant DOM writes */
let lastDragText = '';
let lastLiftText = '';
/* fix #8: chart update throttle β€” every 15 frames (~4 Hz) instead of 5 */
const CHART_INTERVAL = 15;
/* ── Initialization ────────────────────────────────────────── */
function init() {
initChart();
resize();
resetToPreset('single');
for (let i = 0; i < PARTICLE_COUNT; i++) spawnParticle(true);
window.addEventListener('resize', resize);
loop();
}
function initChart() {
const chartCtx = forceChartCanvas.getContext('2d');
chart = new Chart(chartCtx, {
type: 'line',
data: {
labels: Array(40).fill(''),
datasets: [
{
label: 'DRAG',
data: Array(40).fill(0),
borderColor: '#00f2ff',
borderWidth: 2,
pointRadius: 0,
fill: true,
backgroundColor: 'rgba(0, 242, 255, 0.05)',
tension: 0.4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false, /* fix #8: disable Chart.js animation entirely */
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: {
display: true,
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#444', font: { size: 7 } },
},
},
},
});
}
/* fix #9: devicePixelRatio-aware resize */
function resize() {
dpr = window.devicePixelRatio || 1;
canvasW = window.innerWidth;
canvasH = window.innerHeight;
canvas.width = canvasW * dpr;
canvas.height = canvasH * dpr;
canvas.style.width = canvasW + 'px';
canvas.style.height = canvasH + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
repositionBodies();
}
function repositionBodies() {
const midX = canvasW / 3.5;
const midY = canvasH / 2;
if (currentPreset === 'single') {
if (bodies.length > 0) {
bodies[0].x = midX;
bodies[0].y = midY;
}
} else if (currentPreset === 'dual') {
if (bodies.length >= 2) {
bodies[0].x = midX;
bodies[0].y = midY - 100;
bodies[1].x = midX;
bodies[1].y = midY + 100;
}
} else if (currentPreset === 'airfoil') {
if (bodies.length > 0) {
bodies[0].x = midX;
bodies[0].y = midY;
}
}
}
function spawnParticle(randomX = false) {
particles.push({
x: randomX ? Math.random() * canvasW : -50,
y: Math.random() * canvasH,
history: [],
color: mode === 'euler' ? '#00f2ff' : '#ff0055',
speedVar: 0.7 + Math.random() * 0.5,
});
}
/* ── Math Engine: Superposition ────────────────────────────── */
function getVelocityAt(x, y) {
let ux = speed;
let uy = 0;
/* fix #6: use indexed loop to avoid indexOf in hot path */
for (let bi = 0; bi < bodies.length; bi++) {
const body = bodies[bi];
const dx = x - body.x;
const dy = y - body.y;
const r2 = dx * dx + dy * dy;
const a = body.r;
const a2 = a * a;
if (r2 < a2 * 0.7) continue;
const r = Math.sqrt(r2);
const cosT = dx / r;
const sinT = dy / r;
let vr_d, vt_d;
if (body.type === 'airfoil') {
const stretchX = 1.8;
const dxS = dx / stretchX;
const rS2 = dxS * dxS + dy * dy;
const rS = Math.sqrt(rS2);
vr_d = -speed * (a2 / rS2) * (dxS / rS);
vt_d = -speed * (a2 / rS2) * (dy / rS);
} else {
vr_d = -speed * (a2 / r2) * cosT;
vt_d = -speed * (a2 / r2) * sinT;
}
const vt_v = -(circulation * a) / r;
ux += vr_d * cosT - (vt_d + vt_v) * sinT;
uy += vr_d * sinT + (vt_d + vt_v) * cosT;
if (mode === 'ns' && dx > 0) {
const wakeWidth = a * (1.2 + 0.3 * (dx / a));
if (Math.abs(dy) < wakeWidth) {
const distScale = Math.abs(dy) / wakeWidth;
const decay = Math.max(0, 1 - dx / (a * 12));
ux *= 0.3 + 0.7 * distScale;
const freq = speed * 0.05;
const amp = 1.0 * speed * (1 - distScale);
uy += Math.sin(time * freq - dx * 0.04 + bi * 2) * amp * decay;
}
}
}
return { ux, uy };
}
/* ── Drawing ───────────────────────────────────────────────── */
function drawGrid() {
ctx.strokeStyle = 'rgba(0, 242, 255, 0.02)';
ctx.lineWidth = 1;
const size = 50;
const shiftX = (time * speed * 1.5) % size;
ctx.beginPath();
for (let x = -shiftX; x < canvasW; x += size) {
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasH);
}
for (let y = 0; y < canvasH; y += size) {
ctx.moveTo(0, y);
ctx.lineTo(canvasW, y);
}
ctx.stroke();
}
function drawPressure() {
if (!showPressure) return;
const step = 35;
const invSpd2 = 1 / (speed * speed);
for (let x = 0; x < canvasW; x += step) {
for (let y = 0; y < canvasH; y += step) {
const v = getVelocityAt(x + step / 2, y + step / 2);
const v2 = v.ux * v.ux + v.uy * v.uy;
const cp = 1 - v2 * invSpd2;
const alpha = Math.min(0.25, Math.abs(cp) * 0.15);
ctx.fillStyle =
cp > 0
? `rgba(255,0,85,${alpha.toFixed(3)})`
: `rgba(0,242,255,${alpha.toFixed(3)})`;
ctx.fillRect(x, y, step, step);
}
}
}
/* fix #5: radial gradient glow replaces per-frame shadowBlur */
function drawBodies() {
const mainColor = mode === 'euler' ? '#00f2ff' : '#ff0055';
const glowRGB = mode === 'euler' ? '0,242,255' : '255,0,85';
for (let i = 0; i < bodies.length; i++) {
const b = bodies[i];
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(time * circulation * 0.01);
/* glow via gradient instead of shadowBlur */
const glowR = b.type === 'airfoil' ? b.r * 2.2 : b.r + 15;
const grad = ctx.createRadialGradient(0, 0, b.r * 0.7, 0, 0, glowR);
grad.addColorStop(0, `rgba(${glowRGB},0.25)`);
grad.addColorStop(1, `rgba(${glowRGB},0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, glowR, 0, Math.PI * 2);
ctx.fill();
/* body outline */
ctx.strokeStyle = mainColor;
ctx.lineWidth = 2;
if (b.type === 'circle') {
ctx.beginPath();
ctx.arc(0, 0, b.r, 0, Math.PI * 2);
ctx.stroke();
} else if (b.type === 'airfoil') {
ctx.beginPath();
ctx.ellipse(0, 0, b.r * 1.8, b.r * 0.4, 0, 0, Math.PI * 2);
ctx.stroke();
}
/* cross-hairs */
ctx.lineWidth = 1;
ctx.globalAlpha = 0.2;
ctx.beginPath();
for (let j = 0; j < 4; j++) {
ctx.rotate(Math.PI / 4);
ctx.moveTo(-b.r, 0);
ctx.lineTo(b.r, 0);
}
ctx.stroke();
ctx.restore();
}
}
function drawParticles() {
ctx.globalCompositeOperation = 'lighter';
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
const v = getVelocityAt(p.x, p.y);
p.x += v.ux * 4 * p.speedVar;
p.y += v.uy * 4 * p.speedVar;
p.history.push({ x: p.x, y: p.y });
if (p.history.length > 10) p.history.shift();
if (p.history.length > 1) {
ctx.beginPath();
ctx.strokeStyle = p.color;
ctx.globalAlpha = 0.4;
ctx.moveTo(p.history[0].x, p.history[0].y);
for (let j = 1; j < p.history.length; j++) {
ctx.lineTo(p.history[j].x, p.history[j].y);
}
ctx.stroke();
}
let collided = false;
for (let bi = 0; bi < bodies.length; bi++) {
const b = bodies[bi];
const hitLimit = b.type === 'circle' ? b.r * 0.95 : b.r * 1.2;
if (Math.hypot(p.x - b.x, p.y - b.y) < hitLimit) {
collided = true;
break;
}
}
if (
p.x > canvasW + 50 ||
p.y < -50 ||
p.y > canvasH + 50 ||
collided
) {
particles.splice(i, 1);
spawnParticle();
}
}
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
}
/* ── HUD updates ───────────────────────────────────────────── */
function updateHUD() {
const totalDrag =
mode === 'euler' ? 0 : speed * speed * bodies.length * 1.2;
const totalLift = Math.abs(circulation * speed * bodies.length * 0.8);
/* fix #7: only write DOM when value actually changes */
const dText = totalDrag.toFixed(2);
const lText = totalLift.toFixed(2);
if (dText !== lastDragText) {
dragDisplay.textContent = dText;
lastDragText = dText;
}
if (lText !== lastLiftText) {
liftDisplay.textContent = lText;
lastLiftText = lText;
}
/* fix #8: reduced chart update frequency + no animation */
if (time % CHART_INTERVAL === 0) {
const ds = chart.data.datasets[0];
ds.data.push(totalDrag);
ds.data.shift();
ds.borderColor = mode === 'euler' ? '#00f2ff' : '#ff0055';
chart.update('none');
}
}
/* ── Main loop β€” fix #4: rAF stored ───────────────────────── */
function loop() {
time++;
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, canvasW, canvasH);
drawGrid();
drawPressure();
drawBodies();
drawParticles();
updateHUD();
rafId = requestAnimationFrame(loop);
}
/* ── Preset management ─────────────────────────────────────── */
const presetBtns = [presetSingleBtn, presetDualBtn, presetAirfoilBtn];
function resetToPreset(type) {
currentPreset = type;
bodies = [];
presetBtns.forEach((b) => b.classList.remove('active-preset'));
if (type === 'single') {
bodies.push({ x: 0, y: 0, r: 60, type: 'circle' });
presetSingleBtn.classList.add('active-preset');
} else if (type === 'dual') {
bodies.push({ x: 0, y: 0, r: 45, type: 'circle' });
bodies.push({ x: 0, y: 0, r: 45, type: 'circle' });
presetDualBtn.classList.add('active-preset');
} else if (type === 'airfoil') {
bodies.push({ x: 0, y: 0, r: 50, type: 'airfoil' });
presetAirfoilBtn.classList.add('active-preset');
}
repositionBodies();
}
/* ── Event handlers β€” fix #11: use cached refs ─────────────── */
modeEulerBtn.onclick = () => {
mode = 'euler';
modeEulerBtn.className =
'mode-btn flex-1 py-2 px-2 text-[10px] font-black bg-cyan-500 text-black';
modeNSBtn.className =
'mode-btn flex-1 py-2 px-2 text-[10px] font-black bg-slate-800 text-slate-400';
mainUI.style.borderColor = 'var(--neon-cyan)';
statsUI.style.borderColor = 'var(--neon-cyan)';
factText.textContent =
'STATUS: Potential flow superposition active. Drag is mathematically zero via symmetry.';
particles.forEach((p) => (p.color = '#00f2ff'));
};
modeNSBtn.onclick = () => {
mode = 'ns';
modeNSBtn.className =
'mode-btn flex-1 py-2 px-2 text-[10px] font-black bg-rose-600 text-white';
modeEulerBtn.className =
'mode-btn flex-1 py-2 px-2 text-[10px] font-black bg-slate-800 text-slate-400';
mainUI.style.borderColor = 'var(--neon-magenta)';
statsUI.style.borderColor = 'var(--neon-magenta)';
factText.textContent =
'ALERT: Superposed wakes generating complex vorticity field. High pressure drag detected.';
particles.forEach((p) => (p.color = '#ff0055'));
};
presetSingleBtn.onclick = () => resetToPreset('single');
presetDualBtn.onclick = () => resetToPreset('dual');
presetAirfoilBtn.onclick = () => resetToPreset('airfoil');
speedSlider.oninput = (e) => {
speed = parseFloat(e.target.value);
speedVal.textContent = speed.toFixed(1);
};
rotSlider.oninput = (e) => {
circulation = parseFloat(e.target.value);
rotVal.textContent = circulation.toFixed(1);
};
togglePressureBtn.onclick = () => (showPressure = !showPressure);
toggleVectorsBtn.onclick = () => (showVectors = !showVectors);
/* ── Body dragging β€” fix #10: mouse + touch ────────────────── */
let activeBody = null;
function onPointerDown(px, py) {
for (let i = 0; i < bodies.length; i++) {
if (Math.hypot(px - bodies[i].x, py - bodies[i].y) < bodies[i].r) {
activeBody = bodies[i];
return;
}
}
}
canvas.addEventListener('mousedown', (e) =>
onPointerDown(e.clientX, e.clientY),
);
canvas.addEventListener(
'touchstart',
(e) => {
e.preventDefault();
onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
},
{ passive: false },
);
window.addEventListener('mousemove', (e) => {
if (activeBody) {
activeBody.x = e.clientX;
activeBody.y = e.clientY;
}
});
window.addEventListener(
'touchmove',
(e) => {
if (activeBody) {
activeBody.x = e.touches[0].clientX;
activeBody.y = e.touches[0].clientY;
}
},
{ passive: true },
);
window.addEventListener('mouseup', () => (activeBody = null));
window.addEventListener('touchend', () => (activeBody = null));
/* ── Boot ──────────────────────────────────────────────────── */
init();