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 */}
{/* Singularity Selection */}
{/* Visualization Settings */}
{/* Global Adjustments */}
{/* Active Singularities List */}
{/* Footer Info */}
{/* Main Content */}
);
}
StokesFlow
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
{[
{ 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
Scale All Magnitudes
Rotate All
Active
{singularities.map((s) => (
))}
{singularities.length === 0 && (
{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"
/>
)}
No singularities placed.
)}
Stokes flow describes fluid motion at very low Reynolds numbers where viscous forces dominate.