From 753b58e8669a429232eb9961a9ae4376c9e4f256 Mon Sep 17 00:00:00 2001 From: Jay Verstraete <104129830+jayf0x@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:51:27 +0200 Subject: [PATCH 1/3] fix: sync #dpr immediately after controller reinit on alphaEnabled toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When alphaEnabled (or webGPUEnabled) changes, the init effect tears down the old controller and creates a new one. The ResizeObserver only fires when the container *size* changes, so it never re-fires for a toggle — leaving the simulation with the default #dpr=1 until something else triggers a resize. With wrong #dpr the pointer-to-UV coordinate transform is off: px = (cssX * 1) / (cssW * DPR * qualityDpr) -- wrong px = (cssX * DPR * qualityDpr) / (cssW * DPR * qualityDpr) -- correct Fix: call controller.resize(initW, initH) immediately after construction so the correct DPR is pushed into the simulation from frame 0, matching the behaviour the user already sees when changing pixelRatio. Also null-guard FluidController#resize for the async WebGPU path where #sim is temporarily null while the adapter request is in flight. Closes #16 Co-Authored-By: Claude Sonnet 4.6 --- src/fluid-controller.ts | 2 +- src/react/useFluid.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/fluid-controller.ts b/src/fluid-controller.ts index 1e83512..a3a4026 100644 --- a/src/fluid-controller.ts +++ b/src/fluid-controller.ts @@ -134,7 +134,7 @@ export class FluidController { if (this.#worker) { this.#worker.postMessage({ type: 'resize', width, height, dpr: effectiveDpr }); } else { - this.#sim!.resize(width, height, effectiveDpr); + this.#sim?.resize(width, height, effectiveDpr); } } diff --git a/src/react/useFluid.ts b/src/react/useFluid.ts index fdef1b2..11f2e9b 100644 --- a/src/react/useFluid.ts +++ b/src/react/useFluid.ts @@ -80,6 +80,14 @@ export function useFluid( }); controllerRef.current = controller; + // Immediately sync dimensions so #dpr is correct from the first frame. + // The ResizeObserver only fires when the container *size* changes, so it + // won't re-fire on an alphaEnabled/webGPUEnabled toggle where the container + // hasn't moved. Without this call the simulation keeps #dpr=1 and all + // pointer-to-UV coordinate transforms are wrong until something else + // triggers a resize. + if (initW > 0 && initH > 0) controller.resize(initW, initH); + // Forward container resizes to the simulation — reads clampedDprRef so pixelRatio changes are picked up const ro = new ResizeObserver((entries) => { for (const entry of entries) { From 68a41bc1c819b6f1537da2aea3ee893c9b27440a Mon Sep 17 00:00:00 2001 From: Jay Verstraete <104129830+jayf0x@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:55:43 +0200 Subject: [PATCH 2/3] fix: seed mouse position on first handleMove to avoid streak from (0,0) A fresh FluidSimulation starts with #mouse.{x,y,targetX,targetY} = 0. The first handleMove call computes dx = currentX - 0, producing a large velocity vector that draws a fluid streak from the top-left corner to the cursor on every controller reinit (alphaEnabled toggle, etc.). Fix: skip the dx/dy calculation on the very first handleMove and just seed the mouse position instead, so subsequent moves only measure real deltas. Co-Authored-By: Claude Sonnet 4.6 --- src/core/simulation.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/simulation.ts b/src/core/simulation.ts index fb08417..9f095b0 100644 --- a/src/core/simulation.ts +++ b/src/core/simulation.ts @@ -131,6 +131,7 @@ export class FluidSimulation { #config: FluidConfig; #mouse: MouseState = { x: 0, y: 0, dx: 0, dy: 0, targetX: 0, targetY: 0, moved: false }; + #mouseSeeded = false; #source: Source | null = null; @@ -219,6 +220,13 @@ export class FluidSimulation { } handleMove(x: number, y: number, strength = 1): void { + if (!this.#mouseSeeded) { + // Seed position on first call so the initial delta isn't measured from (0,0). + this.#mouse.x = this.#mouse.targetX = x; + this.#mouse.y = this.#mouse.targetY = y; + this.#mouseSeeded = true; + return; + } this.#mouse.moved = true; this.#mouse.dx = (x - this.#mouse.targetX) * strength; this.#mouse.dy = (y - this.#mouse.targetY) * strength; From 5afed0da07a242761f62783db9ef8d6d4326bc54 Mon Sep 17 00:00:00 2001 From: Jay Verstraete <104129830+jayf0x@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:56:36 +0200 Subject: [PATCH 3/3] Update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 98e0d2c..b30fa87 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "scripts": { "build": "vite build", "dev": "vite build --watch", + "dev:demo": "bun run --cwd ./demo dev", "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage",