Goal
Build a real-time 2D incompressible fluid ("stable fluids" / Navier–Stokes) simulation with Three.js that visually matches neon ink/dye swirling on pure black — luminous cyan/green/blue tendrils, bright near-white cores, crisp vortices. It MUST be a genuine GPU solver using ping-pong framebuffers (NOT particles, NOT noise/metaballs/domain-warp fakes). Two quality targets: (a) the best possible look, and (b) a live UI to tune parameters.

Rendering pipeline for the best visual
Sim passes (each = full-screen 2x2 quad under OrthographicCamera(-1,1,1,-1,0,1), read/write alternating FBOs):
- velocity field (vec2 in RG of an RGBA texture) at low res (~256, DPR-aware)
- dye / colored density (RGBA) at high res (~1024–1444, DPR-aware)
- FBOs: type HalfFloatType (fallback FloatType), format RGBA, depthBuffer/stencil false. Detect float/half-float LINEAR filtering; if the device can't linear-filter them, drop dye resolution and show a small warning.
Per frame: advection (semi-Lagrangian backtrace for velocity AND dye) -> vorticity confinement (curl, then force ~ (N x curl)*epsilon to sharpen swirls) -> divergence -> clear pressure -> Jacobi pressure solve -> gradient subtraction (enforce incompressibility) -> splat (pointer).
CORRECTNESS THAT AFFECTS LOOK: apply an aspect-ratio correction so circular splats stay circular and flow isn't stretched on wide screens — scale x consistently in the splat offsets and the advection backtrace. Clamp dt (cap ~1/60) for stable advection.

Display / grading pass (this is what makes it glow, not just "colored smoke")
Render the dye texture as radiance into a tonemapped color over black; do NOT rely on additive clamping. Recommended expression (tweakable):
    d = dye.rgb;                       // accumulated colored density
    c = 1.0 - exp(-d * exposure);      // soft knee -> white-hot cores, never clips
    c = c / (1.0 + c);                 // gentle Reinhard
    outColor = pow(c, vec3(0.4545));   // gamma; empty areas -> pure black
This gives the bright cyan/green/blue zones with whitish centers from the reference. Keep a single "exposure" uniform so it's tweakable.

Optional bloom (toggle, default ON at low strength)
After the graded image: downsample to 2-3 half-res mips, separable Gaussian blur (9-tap), then additively composite back with a "bloom strength" uniform (0 disables). Keep it light so it reads as neon glow, not a smear.

Interaction & initial state
- Pointer down/move stamps a Gaussian DYE blob + a Gaussian VELOCITY blob along the drag delta at the pointer UV (u = x/width, v = 1 - y/height); skip zero-delta.
- Splat color from a palette [cyan #00e5ff, green #39ff88, blue #2a6bff] with slight per-stroke hue jitter, OR an optional "hue-cycle" mode that slowly shifts hue over time for automatic variety.
- On load, seed 4-8 random splats of mixed colors so the screen is never empty black (match the reference's busy composition).

Live control panel (custom, vanilla HTML/CSS — no GUI library, no image assets)
A collapsible glassmorphism panel top-right, styled to match the neon theme (semi-transparent dark, thin glowing accent). All controls bind to ONE `params` object read each frame; every tunable is a shader UNIFORM or per-frame JS var so changes apply live with no recompile. Controls:
Sliders: Vorticity epsilon (0–60), Pressure iterations (1–60), Velocity persistence (fades fast..never), Dye persistence (trails short..long), Splat size, Splat force, Exposure, Bloom strength + [on/off].
Color: swatches for the palette + a "hue-cycle" toggle + optional native color picker.
Quality: preset Low/Med/High that sets sim & dye resolution (and rebuilds FBOs).
Buttons: Clear, Random / Splash, Auto-emit (keeps splatting at a slowly orbiting point when idle), Save PNG.
FPS readout (small, optional).

Save PNG (must work)
Create the renderer with preserveDrawingBuffer:true; on click, renderer.setRenderTarget(null) then renderer.domElement.toDataURL('image/png') and trigger a download named fluid-<timestamp>.png.

Tuned starting defaults (so the first render already looks good; user can then explore)
simRes 256, dyeRes 1024, DPR cap 2, vorticity 25, pressureIters 24, velocity fade slow, dye fade medium (a stroke lingers ~1-3 s), splat size mid, exposure ~2.5, bloom 0.6 ON, palette mode (not hue-cycle), auto-emit OFF, seed-on-load ON.

Output format & constraints
- ONE self-contained .html file; Three.js via CDN ES-module import (three@0.16x). No external images or fonts. Name every pass with a short inline comment and group the GLSL into one passthrough vertex shader + these fragment shaders: advect, curl+force, divergence, jacobi, gradientSubtract, splat, display(grading), blurH, blurV, composite.
- After the code, add a 4-6 line "how to look better" note mapping each visual to its control (e.g. "more swirl sharpness -> vorticity; longer trails -> dye persistence; brighter cores -> exposure/bloom").

Acceptance checklist (verify before finishing)
- Open file: screen is already alive with seeded glowing swirls on black; dragging stirs them with crisp vortices and white-hot cores (tonemap working, no clipped white).
- Wide window: splats are circular, flow not stretched (aspect correction correct).
- UI: every slider changes the result in real time; Clear empties to black; Random/Splash reseeds; Auto-emit animates when idle; Save PNG downloads the current frame; panel collapses/expands.
- Low/Med/High quality actually resizes FBOs without errors; bloom toggle cleanly on/off.
- No console errors; float textures + NearestFilter for sim, LinearFilter for display/bloom.