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.
Files changed (58) hide show
  1. package/README.md +107 -81
  2. package/dist/fancy-ui/animated-testimonials/README.md +44 -0
  3. package/dist/fancy-ui/animated-tooltip/README.md +40 -0
  4. package/dist/fancy-ui/apple-card-carousel/README.md +46 -0
  5. package/dist/fancy-ui/bento-grid/BentoGrid.test.d.ts +1 -0
  6. package/dist/fancy-ui/bento-grid/BentoGrid.test.js +109 -0
  7. package/dist/fancy-ui/border-beam/README.md +37 -0
  8. package/dist/fancy-ui/card-3d/CardContainer.test.d.ts +1 -0
  9. package/dist/fancy-ui/card-3d/CardContainer.test.js +118 -0
  10. package/dist/fancy-ui/card-spotlight/CardSpotlight.test.d.ts +1 -0
  11. package/dist/fancy-ui/card-spotlight/CardSpotlight.test.js +58 -0
  12. package/dist/fancy-ui/displacement-text/README.md +39 -0
  13. package/dist/fancy-ui/dock/README.md +1 -1
  14. package/dist/fancy-ui/editorial-engine/EditorialEngine.test.d.ts +1 -0
  15. package/dist/fancy-ui/editorial-engine/EditorialEngine.test.js +54 -0
  16. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +1250 -1108
  17. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +11 -5
  18. package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +48 -0
  19. package/dist/fancy-ui/fluid-cursor/README.md +57 -6
  20. package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +72 -0
  21. package/dist/fancy-ui/fluid-cursor/fluid-shared.js +125 -0
  22. package/dist/fancy-ui/fluid-cursor/fluid-shared.test.d.ts +1 -0
  23. package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +39 -0
  24. package/dist/fancy-ui/fluid-cursor/index.d.ts +1 -0
  25. package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +36 -0
  26. package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +926 -0
  27. package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +5 -3
  28. package/dist/fancy-ui/frosted-glass/README.md +4 -2
  29. package/dist/fancy-ui/index.d.ts +1 -0
  30. package/dist/fancy-ui/index.js +1 -0
  31. package/dist/fancy-ui/line-hover-link/README.md +37 -0
  32. package/dist/fancy-ui/line-reveal/LineReveal.test.d.ts +1 -0
  33. package/dist/fancy-ui/line-reveal/LineReveal.test.js +77 -0
  34. package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte +19 -2
  35. package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte.d.ts +4 -0
  36. package/dist/fancy-ui/liquid-glass/LiquidGlass.test.js +14 -0
  37. package/dist/fancy-ui/liquid-glass/README.md +7 -1
  38. package/dist/fancy-ui/liquid-text/LiquidText.svelte +1151 -0
  39. package/dist/fancy-ui/liquid-text/LiquidText.svelte.d.ts +37 -0
  40. package/dist/fancy-ui/liquid-text/LiquidText.test.d.ts +1 -0
  41. package/dist/fancy-ui/liquid-text/LiquidText.test.js +219 -0
  42. package/dist/fancy-ui/liquid-text/README.md +139 -0
  43. package/dist/fancy-ui/liquid-text/index.d.ts +2 -0
  44. package/dist/fancy-ui/liquid-text/index.js +1 -0
  45. package/dist/fancy-ui/marquee/README.md +57 -0
  46. package/dist/fancy-ui/matrix-rain/MatrixRain.test.d.ts +1 -0
  47. package/dist/fancy-ui/matrix-rain/MatrixRain.test.js +30 -0
  48. package/dist/fancy-ui/matrix-rain/README.md +42 -0
  49. package/dist/fancy-ui/noise-reveal/README.md +52 -0
  50. package/dist/fancy-ui/rainbow-button/README.md +36 -0
  51. package/dist/fancy-ui/registry.js +144 -20
  52. package/dist/fancy-ui/ripple-button/README.md +36 -0
  53. package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.d.ts +1 -0
  54. package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.js +45 -0
  55. package/dist/fancy-ui/terminal-text/README.md +48 -0
  56. package/dist/fancy-ui/terminal-text/TerminalText.test.d.ts +1 -0
  57. package/dist/fancy-ui/terminal-text/TerminalText.test.js +88 -0
  58. package/package.json +18 -4
@@ -1,8 +1,4 @@
1
- interface ColorRGB {
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 | Type | Default | Description |
34
- |---|---|---|---|
35
- | `fluidColor` | `string` (hex) | — | Fixed fluid color. Disables random cycling. |
36
- | `fluidColors` | `string[]` (hex) | — | Palette to cycle through on each splat. |
37
- | `colorIntensity` | `number` | `0.15` | Intensity multiplier applied to fluid colors (0–1). |
38
- | `backColor` | `{ r, g, b }` or `string` (hex) | `{ r: 0.5, g: 0, b: 0 }` | Background color — accepts RGB object or hex string. |
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>;