import React, { useState } from 'react'; import { Settings2, Wind, Waves, CircleDot, ArrowRight, RotateCw, Activity, Trash2, Info, Download } from 'lucide-react'; import { Visualizer } from './components/Visualizer'; import { Singularity, Vector2 } from './physics'; export default function App() { const [singularities, setSingularities] = useState([ { id: 'initial-stokeslet', type: 'stokeslet', pos: { x: 0, y: 0 }, strength: { x: 1, y: 0 }, orientation: 0 } ]); const [selectedType, setSelectedType] = useState('stokeslet'); const [showStreamlines, setShowStreamlines] = useState(true); const [showHeatmap, setShowHeatmap] = useState(true); const [showVectors, setShowVectors] = useState(false); const [zoom, setZoom] = useState(1.0); const [heatmapSaturation, setHeatmapSaturation] = useState(2.5); const [offset, setOffset] = useState({ x: 0, y: 0 }); const addSingularity = (pos: Vector2) => { const newSingularity: Singularity = { id: Math.random().toString(36).substr(2, 9), type: selectedType, pos, strength: { x: 1, y: 0 }, orientation: 0 }; setSingularities([...singularities, newSingularity]); }; const updateSingularity = (id: string, updates: Partial) => { setSingularities(prev => prev.map(s => s.id === id ? { ...s, ...updates } : s)); }; const removeSingularity = (id: string) => { setSingularities(prev => prev.filter(s => s.id !== id)); }; const clearAll = () => setSingularities([]); const exportToHTML = () => { const physicsCode = ` const getVelocityAt = (p, singularities) => { let vx = 0, vy = 0; for (const s of singularities) { const dx = p.x - s.pos.x, dy = p.y - s.pos.y; const r2 = dx * dx + dy * dy + 0.01, r = Math.sqrt(r2); const r3 = r2 * r, r5 = r3 * r2, r7 = r5 * r2; const angle = s.orientation || 0, mag = s.strength.x; switch (s.type) { case 'stokeslet': { const fx = mag * Math.cos(angle), fy = mag * Math.sin(angle); vx += (fx / r + (fx * dx * dx + fy * dx * dy) / r3); vy += (fy / r + (fx * dx * dy + fy * dy * dy) / r3); break; } case 'rotlet': vx += (-mag * dy) / r3; vy += (mag * dx) / r3; break; case 'stresslet': { const nx = Math.cos(angle), ny = Math.sin(angle); const Sxx = mag * (nx * nx - 0.333), Syy = mag * (ny * ny - 0.333), Sxy = mag * (nx * ny); const x_S_x = dx * dx * Sxx + 2 * dx * dy * Sxy + dy * dy * Syy; const factor = -3 * x_S_x / r5; vx += dx * factor; vy += dy * factor; break; } case 'source-doublet': { const ax = mag * Math.cos(angle), ay = mag * Math.sin(angle); const a_dot_r = ax * dx + ay * dy; vx += -ax / r3 + (3 * a_dot_r * dx) / r5; vy += -ay / r3 + (3 * a_dot_r * dy) / r5; break; } case 'force-dipole': { const nx = Math.cos(angle), ny = Math.sin(angle), n_dot_r = nx * dx + ny * dy; const common = mag / r3, radial = -3 * mag * n_dot_r / r5; vx += common * nx + radial * dx; vy += common * ny + radial * dy; break; } case 'stokes-quadrupole': { const nx = Math.cos(angle), ny = Math.sin(angle), n_dot_r = nx * dx + ny * dy; const factor = mag * (3 / r5 - 15 * n_dot_r * n_dot_r / r7); vx += dx * factor; vy += dy * factor; break; } } } return { x: vx, y: vy }; }; `; const htmlContent = ` Stokes Flow Visualizer
`; const blob = new Blob([htmlContent], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'stokes-flow-visualizer.html'; a.click(); URL.revokeObjectURL(url); }; return (
{/* Sidebar */}

StokesFlow

{/* Singularity Selection */}

Singularities

{[ { id: 'stokeslet', label: 'Stokeslet', icon: ArrowRight, desc: 'Point force' }, { id: 'rotlet', label: 'Rotlet', icon: RotateCw, desc: 'Point torque' }, { id: 'stresslet', label: 'Stresslet', icon: Activity, desc: 'Force dipole' }, { id: 'source-doublet', label: 'Source Doublet', icon: Waves, desc: 'Potential dipole' }, { id: 'force-dipole', label: 'Force Dipole', icon: ArrowRight, desc: 'Symmetric gradient' }, { id: 'stokes-quadrupole', label: 'Quadrupole', icon: Activity, desc: 'Higher order' }, ].map((type) => ( ))}
{/* Visualization Settings */}

Visualization

{[ { id: 'heatmap', label: 'Velocity Heatmap', state: showHeatmap, setter: setShowHeatmap }, { id: 'streamlines', label: 'Streamlines', state: showStreamlines, setter: setShowStreamlines }, { id: 'vectors', label: 'Vector Field', state: showVectors, setter: setShowVectors }, ].map((opt) => ( ))}
Heatmap Saturation {heatmapSaturation.toFixed(1)}
setHeatmapSaturation(parseFloat(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-orange-500" />
Zoom {(zoom * 100).toFixed(0)}%
setZoom(parseFloat(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500" />
{/* Global Adjustments */}

Global Adjustments

Scale All Magnitudes
Rotate All
{/* Active Singularities List */}

Active

{singularities.map((s) => (
{s.type} ({s.pos.x.toFixed(1)}, {s.pos.y.toFixed(1)})
Mag updateSingularity(s.id, { strength: { ...s.strength, x: parseFloat(e.target.value) } })} className="w-16 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-500" />
{(s.type === 'stokeslet' || s.type === 'source-doublet' || s.type === 'stresslet' || s.type === 'force-dipole' || s.type === 'stokes-quadrupole') && (
Rot updateSingularity(s.id, { orientation: parseFloat(e.target.value) })} className="w-16 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-pink-500" />
)}
))} {singularities.length === 0 && (
No singularities placed.
)}
{/* Footer Info */}

Stokes flow describes fluid motion at very low Reynolds numbers where viscous forces dominate.

{/* Main Content */}
); }