fancy-ui-svelte 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -81
- package/dist/fancy-ui/animated-testimonials/README.md +44 -0
- package/dist/fancy-ui/animated-tooltip/README.md +40 -0
- package/dist/fancy-ui/apple-card-carousel/README.md +46 -0
- package/dist/fancy-ui/bento-grid/BentoGrid.test.d.ts +1 -0
- package/dist/fancy-ui/bento-grid/BentoGrid.test.js +109 -0
- package/dist/fancy-ui/border-beam/README.md +37 -0
- package/dist/fancy-ui/card-3d/CardContainer.test.d.ts +1 -0
- package/dist/fancy-ui/card-3d/CardContainer.test.js +118 -0
- package/dist/fancy-ui/card-spotlight/CardSpotlight.test.d.ts +1 -0
- package/dist/fancy-ui/card-spotlight/CardSpotlight.test.js +58 -0
- package/dist/fancy-ui/displacement-text/README.md +39 -0
- package/dist/fancy-ui/dock/README.md +1 -1
- package/dist/fancy-ui/editorial-engine/EditorialEngine.test.d.ts +1 -0
- package/dist/fancy-ui/editorial-engine/EditorialEngine.test.js +54 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +1250 -1108
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +11 -5
- package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +48 -0
- package/dist/fancy-ui/fluid-cursor/README.md +57 -6
- package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +72 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.js +125 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.d.ts +1 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +39 -0
- package/dist/fancy-ui/fluid-cursor/index.d.ts +1 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +36 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +926 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +5 -3
- package/dist/fancy-ui/frosted-glass/README.md +4 -2
- package/dist/fancy-ui/index.d.ts +1 -0
- package/dist/fancy-ui/index.js +1 -0
- package/dist/fancy-ui/line-hover-link/README.md +37 -0
- package/dist/fancy-ui/line-reveal/LineReveal.test.d.ts +1 -0
- package/dist/fancy-ui/line-reveal/LineReveal.test.js +77 -0
- package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte +19 -2
- package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte.d.ts +4 -0
- package/dist/fancy-ui/liquid-glass/LiquidGlass.test.js +14 -0
- package/dist/fancy-ui/liquid-glass/README.md +7 -1
- package/dist/fancy-ui/liquid-text/LiquidText.svelte +1151 -0
- package/dist/fancy-ui/liquid-text/LiquidText.svelte.d.ts +37 -0
- package/dist/fancy-ui/liquid-text/LiquidText.test.d.ts +1 -0
- package/dist/fancy-ui/liquid-text/LiquidText.test.js +219 -0
- package/dist/fancy-ui/liquid-text/README.md +139 -0
- package/dist/fancy-ui/liquid-text/index.d.ts +2 -0
- package/dist/fancy-ui/liquid-text/index.js +1 -0
- package/dist/fancy-ui/marquee/README.md +57 -0
- package/dist/fancy-ui/matrix-rain/MatrixRain.test.d.ts +1 -0
- package/dist/fancy-ui/matrix-rain/MatrixRain.test.js +30 -0
- package/dist/fancy-ui/matrix-rain/README.md +42 -0
- package/dist/fancy-ui/noise-reveal/README.md +52 -0
- package/dist/fancy-ui/rainbow-button/README.md +36 -0
- package/dist/fancy-ui/registry.js +144 -20
- package/dist/fancy-ui/ripple-button/README.md +36 -0
- package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.d.ts +1 -0
- package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.js +45 -0
- package/dist/fancy-ui/terminal-text/README.md +48 -0
- package/dist/fancy-ui/terminal-text/TerminalText.test.d.ts +1 -0
- package/dist/fancy-ui/terminal-text/TerminalText.test.js +88 -0
- package/package.json +18 -4
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
r: number;
|
|
3
|
-
g: number;
|
|
4
|
-
b: number;
|
|
5
|
-
}
|
|
1
|
+
import { type ColorRGB, type FluidCursorHandle } from "./fluid-shared.js";
|
|
6
2
|
interface Props {
|
|
7
3
|
simResolution?: number;
|
|
8
4
|
dyeResolution?: number;
|
|
@@ -29,6 +25,16 @@ interface Props {
|
|
|
29
25
|
splatOnMount?: boolean;
|
|
30
26
|
allowMultiple?: boolean;
|
|
31
27
|
contained?: boolean;
|
|
28
|
+
hdr?: boolean;
|
|
29
|
+
hdrBoost?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Called once the fluid engine is live, with an imperative handle to
|
|
32
|
+
* drive the simulation programmatically (trace a path via
|
|
33
|
+
* `moveTo`/`penUp`, fire a one-off `burst`) and read back the actual
|
|
34
|
+
* `renderLevel`. Pair with `interactive={false}` to drive it entirely
|
|
35
|
+
* without a real cursor.
|
|
36
|
+
*/
|
|
37
|
+
onReady?: (handle: FluidCursorHandle) => void;
|
|
32
38
|
}
|
|
33
39
|
declare const FluidCursor: import("svelte").Component<Props, {}, "">;
|
|
34
40
|
type FluidCursor = ReturnType<typeof FluidCursor>;
|
|
@@ -66,6 +66,35 @@ describe("FluidCursor", () => {
|
|
|
66
66
|
vi.unstubAllGlobals();
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
|
+
describe("hdr props", () => {
|
|
70
|
+
it("renders without error with hdr={true} when WebGPU is unavailable", () => {
|
|
71
|
+
const { container } = render(FluidCursor, { props: { hdr: true } });
|
|
72
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
it("renders without error when hdrBoost is out of [1,4] range (clamped internally)", () => {
|
|
75
|
+
const { container } = render(FluidCursor, { props: { hdr: true, hdrBoost: 100 } });
|
|
76
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
it("falls back cleanly when WebGPU is present but no adapter is returned", async () => {
|
|
79
|
+
const requestAdapter = vi.fn(async () => null);
|
|
80
|
+
// Define `gpu` on the real navigator instead of replacing it, so the
|
|
81
|
+
// Navigator prototype (userAgent, etc.) stays intact.
|
|
82
|
+
Object.defineProperty(navigator, "gpu", {
|
|
83
|
+
value: { requestAdapter },
|
|
84
|
+
configurable: true,
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const { container } = render(FluidCursor, { props: { hdr: true } });
|
|
88
|
+
// Let the async WebGPU attempt resolve and fall back.
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
90
|
+
expect(requestAdapter).toHaveBeenCalled();
|
|
91
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
delete navigator.gpu;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
69
98
|
describe("color props", () => {
|
|
70
99
|
it("renders without error when fluidColor is provided", () => {
|
|
71
100
|
const { container } = render(FluidCursor, { props: { fluidColor: "#00ffcc" } });
|
|
@@ -98,4 +127,23 @@ describe("FluidCursor", () => {
|
|
|
98
127
|
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
99
128
|
});
|
|
100
129
|
});
|
|
130
|
+
describe("onReady handle", () => {
|
|
131
|
+
// jsdom provides no WebGL context, so this exercises the no-renderer path.
|
|
132
|
+
it('reports renderLevel "none" when no renderer is available', () => {
|
|
133
|
+
const onReady = vi.fn();
|
|
134
|
+
render(FluidCursor, { props: { onReady } });
|
|
135
|
+
expect(onReady).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(onReady.mock.calls[0][0].renderLevel).toBe("none");
|
|
137
|
+
});
|
|
138
|
+
it("hands over an inert handle that is safe to call", () => {
|
|
139
|
+
const onReady = vi.fn();
|
|
140
|
+
render(FluidCursor, { props: { onReady } });
|
|
141
|
+
const handle = onReady.mock.calls[0][0];
|
|
142
|
+
expect(() => {
|
|
143
|
+
handle.moveTo(0.5, 0.5);
|
|
144
|
+
handle.penUp();
|
|
145
|
+
handle.burst(0.5, 0.5, 10, 10, { r: 1, g: 0, b: 0 });
|
|
146
|
+
}).not.toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
101
149
|
});
|
|
@@ -5,6 +5,7 @@ A WebGL-based fluid simulation that follows the cursor, creating beautiful flowi
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- WebGL2 fluid simulation with WebGL1 fallback
|
|
8
|
+
- Optional HDR mode rendered by a WebGPU engine (wide gamut + extended tone mapping)
|
|
8
9
|
- Mouse and touch support
|
|
9
10
|
- Configurable simulation parameters
|
|
10
11
|
- Transparent background support
|
|
@@ -23,6 +24,7 @@ A WebGL-based fluid simulation that follows the cursor, creating beautiful flowi
|
|
|
23
24
|
### WebGL Implementation
|
|
24
25
|
|
|
25
26
|
The component implements a full Navier-Stokes fluid simulation with:
|
|
27
|
+
|
|
26
28
|
- Curl/vorticity computation
|
|
27
29
|
- Pressure solving with iterative Jacobi method
|
|
28
30
|
- Advection for both velocity and dye
|
|
@@ -30,12 +32,12 @@ The component implements a full Navier-Stokes fluid simulation with:
|
|
|
30
32
|
|
|
31
33
|
## Color Props
|
|
32
34
|
|
|
33
|
-
| Prop
|
|
34
|
-
|
|
35
|
-
| `fluidColor`
|
|
36
|
-
| `fluidColors`
|
|
37
|
-
| `colorIntensity` | `number`
|
|
38
|
-
| `backColor`
|
|
35
|
+
| Prop | Type | Default | Description |
|
|
36
|
+
| ---------------- | ------------------------------- | ------------------------ | ---------------------------------------------------- |
|
|
37
|
+
| `fluidColor` | `string` (hex) | — | Fixed fluid color. Disables random cycling. |
|
|
38
|
+
| `fluidColors` | `string[]` (hex) | — | Palette to cycle through on each splat. |
|
|
39
|
+
| `colorIntensity` | `number` | `0.15` | Intensity multiplier applied to fluid colors (0–1). |
|
|
40
|
+
| `backColor` | `{ r, g, b }` or `string` (hex) | `{ r: 0.5, g: 0, b: 0 }` | Background color — accepts RGB object or hex string. |
|
|
39
41
|
|
|
40
42
|
**Priority**: `fluidColor` > `fluidColors` > random HSV
|
|
41
43
|
|
|
@@ -52,8 +54,57 @@ The component implements a full Navier-Stokes fluid simulation with:
|
|
|
52
54
|
<FluidCursor backColor="#1a1a2e" />
|
|
53
55
|
```
|
|
54
56
|
|
|
57
|
+
### Contained-mode splat scaling
|
|
58
|
+
|
|
59
|
+
`splatRadius` is defined as a fraction of the canvas, which would make the
|
|
60
|
+
effect look tiny inside small containers (e.g. docs demos). In `contained`
|
|
61
|
+
mode both engines grow the aspect-corrected radius with the
|
|
62
|
+
viewport/container height ratio — sub-linearly in apparent size (×√k), so a
|
|
63
|
+
small demo reads bigger without mimicking fullscreen 1:1 — capped at ~30% of
|
|
64
|
+
the container height (`scaleRadiusForContainer` in `fluid-shared.ts`).
|
|
65
|
+
Fullscreen and viewport-sized containers are unaffected.
|
|
66
|
+
|
|
55
67
|
### Event Handling
|
|
56
68
|
|
|
57
69
|
- Window-level mouse/touch events for full-screen tracking
|
|
58
70
|
- First-move handlers to start animation only when user interacts
|
|
59
71
|
- Proper cleanup of all event listeners on component unmount
|
|
72
|
+
|
|
73
|
+
## HDR Mode
|
|
74
|
+
|
|
75
|
+
```svelte
|
|
76
|
+
<FluidCursor hdr hdrBoost={2} fluidColors={["#a142ff", "#42cfff"]} />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With `hdr` enabled the component tries a WebGPU engine (`webgpu-engine.ts`, a
|
|
80
|
+
WGSL port of the same simulation) that renders into an `rgba16float` canvas
|
|
81
|
+
configured with `colorSpace: "display-p3"` and `toneMapping: { mode: "extended" }`.
|
|
82
|
+
On an HDR display, dye values pushed above `1.0` by `hdrBoost` render brighter
|
|
83
|
+
than SDR white; on a wide-gamut (P3) display colors are noticeably more
|
|
84
|
+
saturated even without HDR headroom.
|
|
85
|
+
|
|
86
|
+
| Prop | Type | Default | Description |
|
|
87
|
+
| ---------- | --------- | ------- | ------------------------------------------------------------------------------------ |
|
|
88
|
+
| `hdr` | `boolean` | `false` | Opt into the WebGPU HDR engine with automatic fallback. |
|
|
89
|
+
| `hdrBoost` | `number` | `1.5` | Display exposure multiplier, clamped to `[1, 4]`. Only applied by the WebGPU engine. |
|
|
90
|
+
|
|
91
|
+
### Support cascade
|
|
92
|
+
|
|
93
|
+
The component degrades silently (the level is logged to the console in dev):
|
|
94
|
+
|
|
95
|
+
1. **`webgpu-hdr`** — WebGPU available: float16 backbuffer, P3, extended tone
|
|
96
|
+
mapping (Chrome/Edge 129+, Safari 26+; browsers that ignore `toneMapping`
|
|
97
|
+
still get the float16 + P3 rendering, clamped to SDR).
|
|
98
|
+
2. **`webgl-p3`** — no WebGPU: the regular WebGL engine with
|
|
99
|
+
`drawingBufferColorSpace = "display-p3"` (Chrome/Edge 104+, Safari 16.4+,
|
|
100
|
+
Firefox 132+). Wide gamut only, no extra brightness; `hdrBoost` is ignored.
|
|
101
|
+
3. **`sdr`** — everything else: the unchanged default rendering.
|
|
102
|
+
|
|
103
|
+
Notes:
|
|
104
|
+
|
|
105
|
+
- Colors are intentionally reinterpreted in P3 space rather than converted
|
|
106
|
+
from sRGB — the resulting saturation push is the point of the effect.
|
|
107
|
+
- The glow requires an HDR-capable display (`matchMedia("(dynamic-range: high)")`).
|
|
108
|
+
On SDR displays HDR mode is still safe, just clamped.
|
|
109
|
+
- With `hdr={false}` (the default) the WebGPU code path is never taken and
|
|
110
|
+
rendering is identical to previous versions.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface ColorRGB {
|
|
2
|
+
r: number;
|
|
3
|
+
g: number;
|
|
4
|
+
b: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Pointer {
|
|
7
|
+
id: number;
|
|
8
|
+
texcoordX: number;
|
|
9
|
+
texcoordY: number;
|
|
10
|
+
prevTexcoordX: number;
|
|
11
|
+
prevTexcoordY: number;
|
|
12
|
+
deltaX: number;
|
|
13
|
+
deltaY: number;
|
|
14
|
+
down: boolean;
|
|
15
|
+
moved: boolean;
|
|
16
|
+
color: ColorRGB;
|
|
17
|
+
}
|
|
18
|
+
export declare function pointerPrototype(): Pointer;
|
|
19
|
+
export declare function hexToRgb(hex: string): ColorRGB;
|
|
20
|
+
export declare function HSVtoRGB(h: number, s: number, v: number): ColorRGB;
|
|
21
|
+
export declare function wrap(value: number, min: number, max: number): number;
|
|
22
|
+
/** Sim/dye grid size for a given base resolution and drawing buffer dimensions. */
|
|
23
|
+
export declare function getSimResolution(resolution: number, bufferWidth: number, bufferHeight: number): {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function correctRadius(radius: number, width: number, height: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* In contained mode the splat radius is a fraction of the canvas, so the same
|
|
30
|
+
* settings that look right fullscreen produce a tiny effect inside a small
|
|
31
|
+
* demo container. Grow the radius with the viewport/container ratio —
|
|
32
|
+
* sub-linearly in apparent size (the gaussian's characteristic length is
|
|
33
|
+
* sqrt(radius), so `radius * k` scales the length by sqrt(k)): a small demo
|
|
34
|
+
* reads bigger without mimicking fullscreen 1:1. Must be applied to the
|
|
35
|
+
* final, aspect-corrected radius; the 0.09 cap keeps the characteristic
|
|
36
|
+
* length under ~30% of the container height.
|
|
37
|
+
*/
|
|
38
|
+
export declare function scaleRadiusForContainer(radius: number, containerHeight: number, viewportHeight: number): number;
|
|
39
|
+
export declare function correctDeltaX(delta: number, width: number, height: number): number;
|
|
40
|
+
export declare function correctDeltaY(delta: number, width: number, height: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* How the fluid is actually being rendered, surfaced as data so callers can
|
|
43
|
+
* react (e.g. tell true HDR output apart from a clamped fallback):
|
|
44
|
+
* - "webgpu-hdr": WebGPU with extended tone mapping active AND a display
|
|
45
|
+
* reporting `(dynamic-range: high)` — true HDR output.
|
|
46
|
+
* - "webgpu-sdr": WebGPU float16 + P3, but the browser clamped tone mapping or
|
|
47
|
+
* the output display is SDR.
|
|
48
|
+
* - "webgl-p3": WebGL fallback with a display-p3 drawing buffer.
|
|
49
|
+
* - "webgl-sdr": WebGL fallback, plain sRGB.
|
|
50
|
+
* - "none": no GPU rendering available at all; the handle is inert.
|
|
51
|
+
*
|
|
52
|
+
* Sampled once when the handle is created: it does not update if the window
|
|
53
|
+
* later moves to a display with a different dynamic range.
|
|
54
|
+
*/
|
|
55
|
+
export type FluidRenderLevel = "webgpu-hdr" | "webgpu-sdr" | "webgl-p3" | "webgl-sdr" | "none";
|
|
56
|
+
/**
|
|
57
|
+
* Imperative control surface handed to `FluidCursor`'s `onReady` callback so a
|
|
58
|
+
* parent can drive the simulation programmatically (trace a path, fire a
|
|
59
|
+
* one-off burst) and read back which render path actually engaged. Coordinates
|
|
60
|
+
* are in [0,1] with the ORIGIN AT THE TOP-LEFT (CSS convention); velocity is
|
|
61
|
+
* derived from consecutive `moveTo` calls, so smooth paths feel like a real
|
|
62
|
+
* cursor. Any engine-specific Y flip is internal.
|
|
63
|
+
*/
|
|
64
|
+
export interface FluidCursorHandle {
|
|
65
|
+
/** Drive the synthetic pointer like a mouse. The next `moveTo` after `penUp` repositions without drawing. */
|
|
66
|
+
moveTo(x: number, y: number, color?: ColorRGB): void;
|
|
67
|
+
/** Lift the pen so the next `moveTo` repositions without producing a connecting streak. */
|
|
68
|
+
penUp(): void;
|
|
69
|
+
/** One-off impulse (click-like burst). Same coord convention; dx/dy in engine velocity units. */
|
|
70
|
+
burst(x: number, y: number, dx: number, dy: number, color: ColorRGB): void;
|
|
71
|
+
readonly renderLevel: FluidRenderLevel;
|
|
72
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Pure helpers shared by the WebGL and WebGPU fluid engines.
|
|
2
|
+
export function pointerPrototype() {
|
|
3
|
+
return {
|
|
4
|
+
id: -1,
|
|
5
|
+
texcoordX: 0,
|
|
6
|
+
texcoordY: 0,
|
|
7
|
+
prevTexcoordX: 0,
|
|
8
|
+
prevTexcoordY: 0,
|
|
9
|
+
deltaX: 0,
|
|
10
|
+
deltaY: 0,
|
|
11
|
+
down: false,
|
|
12
|
+
moved: false,
|
|
13
|
+
color: { r: 0, g: 0, b: 0 },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function hexToRgb(hex) {
|
|
17
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
18
|
+
if (!result) {
|
|
19
|
+
console.warn(`[FluidCursor] Invalid hex color: "${hex}". Expected format: "#rrggbb". Falling back to white.`);
|
|
20
|
+
return { r: 1, g: 1, b: 1 };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
r: parseInt(result[1], 16) / 255,
|
|
24
|
+
g: parseInt(result[2], 16) / 255,
|
|
25
|
+
b: parseInt(result[3], 16) / 255,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function HSVtoRGB(h, s, v) {
|
|
29
|
+
let r = 0;
|
|
30
|
+
let g = 0;
|
|
31
|
+
let b = 0;
|
|
32
|
+
const i = Math.floor(h * 6);
|
|
33
|
+
const f = h * 6 - i;
|
|
34
|
+
const p = v * (1 - s);
|
|
35
|
+
const q = v * (1 - f * s);
|
|
36
|
+
const t = v * (1 - (1 - f) * s);
|
|
37
|
+
switch (i % 6) {
|
|
38
|
+
case 0:
|
|
39
|
+
r = v;
|
|
40
|
+
g = t;
|
|
41
|
+
b = p;
|
|
42
|
+
break;
|
|
43
|
+
case 1:
|
|
44
|
+
r = q;
|
|
45
|
+
g = v;
|
|
46
|
+
b = p;
|
|
47
|
+
break;
|
|
48
|
+
case 2:
|
|
49
|
+
r = p;
|
|
50
|
+
g = v;
|
|
51
|
+
b = t;
|
|
52
|
+
break;
|
|
53
|
+
case 3:
|
|
54
|
+
r = p;
|
|
55
|
+
g = q;
|
|
56
|
+
b = v;
|
|
57
|
+
break;
|
|
58
|
+
case 4:
|
|
59
|
+
r = t;
|
|
60
|
+
g = p;
|
|
61
|
+
b = v;
|
|
62
|
+
break;
|
|
63
|
+
case 5:
|
|
64
|
+
r = v;
|
|
65
|
+
g = p;
|
|
66
|
+
b = q;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return { r, g, b };
|
|
70
|
+
}
|
|
71
|
+
export function wrap(value, min, max) {
|
|
72
|
+
const range = max - min;
|
|
73
|
+
if (range === 0)
|
|
74
|
+
return min;
|
|
75
|
+
return ((value - min) % range) + min;
|
|
76
|
+
}
|
|
77
|
+
/** Sim/dye grid size for a given base resolution and drawing buffer dimensions. */
|
|
78
|
+
export function getSimResolution(resolution, bufferWidth, bufferHeight) {
|
|
79
|
+
const aspectRatio = bufferWidth / bufferHeight;
|
|
80
|
+
const aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
|
|
81
|
+
const min = Math.round(resolution);
|
|
82
|
+
const max = Math.round(resolution * aspect);
|
|
83
|
+
if (bufferWidth > bufferHeight) {
|
|
84
|
+
return { width: max, height: min };
|
|
85
|
+
}
|
|
86
|
+
return { width: min, height: max };
|
|
87
|
+
}
|
|
88
|
+
export function correctRadius(radius, width, height) {
|
|
89
|
+
const aspectRatio = width / height;
|
|
90
|
+
if (aspectRatio > 1)
|
|
91
|
+
return radius * aspectRatio;
|
|
92
|
+
return radius;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* In contained mode the splat radius is a fraction of the canvas, so the same
|
|
96
|
+
* settings that look right fullscreen produce a tiny effect inside a small
|
|
97
|
+
* demo container. Grow the radius with the viewport/container ratio —
|
|
98
|
+
* sub-linearly in apparent size (the gaussian's characteristic length is
|
|
99
|
+
* sqrt(radius), so `radius * k` scales the length by sqrt(k)): a small demo
|
|
100
|
+
* reads bigger without mimicking fullscreen 1:1. Must be applied to the
|
|
101
|
+
* final, aspect-corrected radius; the 0.09 cap keeps the characteristic
|
|
102
|
+
* length under ~30% of the container height.
|
|
103
|
+
*/
|
|
104
|
+
export function scaleRadiusForContainer(radius, containerHeight, viewportHeight) {
|
|
105
|
+
if (containerHeight <= 0 || viewportHeight <= 0)
|
|
106
|
+
return radius;
|
|
107
|
+
const k = viewportHeight / containerHeight;
|
|
108
|
+
if (k <= 1)
|
|
109
|
+
return radius;
|
|
110
|
+
// The cap only limits the auto-scaling — a caller-provided radius that is
|
|
111
|
+
// already larger must never be shrunk by entering a small container.
|
|
112
|
+
return Math.max(radius, Math.min(radius * k, 0.09));
|
|
113
|
+
}
|
|
114
|
+
export function correctDeltaX(delta, width, height) {
|
|
115
|
+
const aspectRatio = width / height;
|
|
116
|
+
if (aspectRatio < 1)
|
|
117
|
+
return delta * aspectRatio;
|
|
118
|
+
return delta;
|
|
119
|
+
}
|
|
120
|
+
export function correctDeltaY(delta, width, height) {
|
|
121
|
+
const aspectRatio = width / height;
|
|
122
|
+
if (aspectRatio > 1)
|
|
123
|
+
return delta / aspectRatio;
|
|
124
|
+
return delta;
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { scaleRadiusForContainer, correctRadius, getSimResolution } from "./fluid-shared.js";
|
|
3
|
+
describe("scaleRadiusForContainer", () => {
|
|
4
|
+
it("keeps the radius unchanged at viewport size (fullscreen)", () => {
|
|
5
|
+
expect(scaleRadiusForContainer(0.002, 900, 900)).toBe(0.002);
|
|
6
|
+
});
|
|
7
|
+
it("keeps the radius unchanged when the container is larger than the viewport", () => {
|
|
8
|
+
expect(scaleRadiusForContainer(0.002, 1800, 900)).toBe(0.002);
|
|
9
|
+
});
|
|
10
|
+
it("grows the radius linearly with the viewport/container ratio (sqrt(k) in apparent size)", () => {
|
|
11
|
+
// Container 4x smaller than viewport: radius x4, characteristic
|
|
12
|
+
// length x2 in container fractions.
|
|
13
|
+
expect(scaleRadiusForContainer(0.002, 250, 1000)).toBeCloseTo(0.008, 10);
|
|
14
|
+
});
|
|
15
|
+
it("caps the radius so a splat stays under ~30% of a tiny container", () => {
|
|
16
|
+
expect(scaleRadiusForContainer(0.01, 50, 1000)).toBe(0.09);
|
|
17
|
+
});
|
|
18
|
+
it("never shrinks a caller-provided radius already above the cap", () => {
|
|
19
|
+
expect(scaleRadiusForContainer(0.2, 250, 1000)).toBe(0.2);
|
|
20
|
+
});
|
|
21
|
+
it("is defensive about degenerate dimensions", () => {
|
|
22
|
+
expect(scaleRadiusForContainer(0.002, 0, 1000)).toBe(0.002);
|
|
23
|
+
expect(scaleRadiusForContainer(0.002, 250, 0)).toBe(0.002);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe("correctRadius", () => {
|
|
27
|
+
it("stretches the radius by aspect ratio on landscape canvases", () => {
|
|
28
|
+
expect(correctRadius(0.01, 200, 100)).toBe(0.02);
|
|
29
|
+
});
|
|
30
|
+
it("leaves portrait canvases unchanged", () => {
|
|
31
|
+
expect(correctRadius(0.01, 100, 200)).toBe(0.01);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("getSimResolution", () => {
|
|
35
|
+
it("puts the larger grid dimension along the larger canvas axis", () => {
|
|
36
|
+
expect(getSimResolution(128, 1000, 500)).toEqual({ width: 256, height: 128 });
|
|
37
|
+
expect(getSimResolution(128, 500, 1000)).toEqual({ width: 128, height: 256 });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -3,3 +3,4 @@ export { default as FluidCursor } from "./FluidCursor.svelte";
|
|
|
3
3
|
* @deprecated Use `FluidCursor` instead. `FluidCursorAdvanced` has been merged into `FluidCursor`.
|
|
4
4
|
*/
|
|
5
5
|
export { default as FluidCursorAdvanced } from "./FluidCursor.svelte";
|
|
6
|
+
export type { FluidCursorHandle, FluidRenderLevel } from "./fluid-shared.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ColorRGB, type FluidCursorHandle } from "./fluid-shared.js";
|
|
2
|
+
export interface WebGpuFluidOptions {
|
|
3
|
+
simResolution: number;
|
|
4
|
+
dyeResolution: number;
|
|
5
|
+
densityDissipation: number;
|
|
6
|
+
velocityDissipation: number;
|
|
7
|
+
pressure: number;
|
|
8
|
+
pressureIterations: number;
|
|
9
|
+
curl: number;
|
|
10
|
+
splatRadius: number;
|
|
11
|
+
splatForce: number;
|
|
12
|
+
shading: boolean;
|
|
13
|
+
colorUpdateSpeed: number;
|
|
14
|
+
/** Display exposure multiplier — values above 1 create the HDR glow. */
|
|
15
|
+
exposure: number;
|
|
16
|
+
contained: boolean;
|
|
17
|
+
interactive: boolean;
|
|
18
|
+
autoSplat: boolean;
|
|
19
|
+
autoSplatInterval: number;
|
|
20
|
+
pauseWhenHidden: boolean;
|
|
21
|
+
splatOnMount: boolean;
|
|
22
|
+
generateColor: () => ColorRGB;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Full WebGPU setup for FluidCursor: engine + pointer wiring + rAF loop +
|
|
26
|
+
* visibility pause + auto splats. Resolves with a {@link FluidCursorHandle}
|
|
27
|
+
* (augmented with `cleanup`), or with `null` when WebGPU is unavailable
|
|
28
|
+
* (caller falls back to the WebGL path).
|
|
29
|
+
*
|
|
30
|
+
* Handle coordinates are top-left origin in [0,1]; WebGPU texcoords already
|
|
31
|
+
* use a top-origin convention, so no Y flip happens here (the WebGL path owns
|
|
32
|
+
* its own flip).
|
|
33
|
+
*/
|
|
34
|
+
export declare function startWebGpuFluid(canvas: HTMLCanvasElement, opts: WebGpuFluidOptions): Promise<(FluidCursorHandle & {
|
|
35
|
+
cleanup(): void;
|
|
36
|
+
}) | null>;
|