Spaces:
Sleeping
Sleeping
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(); | |