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,15 +1,24 @@
1
1
  <script lang="ts">
2
2
  import { cn } from "../../utils.js";
3
3
  import { onMount } from "svelte";
4
+ import {
5
+ type ColorRGB,
6
+ type Pointer,
7
+ type FluidCursorHandle,
8
+ type FluidRenderLevel,
9
+ pointerPrototype,
10
+ hexToRgb,
11
+ HSVtoRGB,
12
+ scaleRadiusForContainer,
13
+ wrap,
14
+ } from "./fluid-shared.js";
15
+ import { startWebGpuFluid } from "./webgpu-engine.js";
4
16
 
5
17
  // Module-level singleton registry
6
18
  let activeInstance: (() => void) | null = null;
7
-
8
- interface ColorRGB {
9
- r: number;
10
- g: number;
11
- b: number;
12
- }
19
+ // Monotonic mount counter so async (WebGPU) startups that finish out of
20
+ // order cannot let an older instance destroy a newer one.
21
+ let mountCounter = 0;
13
22
 
14
23
  interface Props {
15
24
  simResolution?: number;
@@ -37,6 +46,16 @@
37
46
  splatOnMount?: boolean;
38
47
  allowMultiple?: boolean;
39
48
  contained?: boolean;
49
+ hdr?: boolean;
50
+ hdrBoost?: number;
51
+ /**
52
+ * Called once the fluid engine is live, with an imperative handle to
53
+ * drive the simulation programmatically (trace a path via
54
+ * `moveTo`/`penUp`, fire a one-off `burst`) and read back the actual
55
+ * `renderLevel`. Pair with `interactive={false}` to drive it entirely
56
+ * without a real cursor.
57
+ */
58
+ onReady?: (handle: FluidCursorHandle) => void;
40
59
  }
41
60
 
42
61
  let {
@@ -65,63 +84,121 @@
65
84
  splatOnMount = false,
66
85
  allowMultiple = false,
67
86
  contained = true,
87
+ hdr = false,
88
+ hdrBoost = 1.5,
89
+ onReady,
68
90
  }: Props = $props();
69
91
 
70
- function hexToRgb(hex: string): ColorRGB {
71
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
72
- if (!result) {
73
- console.warn(
74
- `[FluidCursor] Invalid hex color: "${hex}". Expected format: "#rrggbb". Falling back to white.`
75
- );
76
- return { r: 1, g: 1, b: 1 };
77
- }
78
- return {
79
- r: parseInt(result[1], 16) / 255,
80
- g: parseInt(result[2], 16) / 255,
81
- b: parseInt(result[3], 16) / 255,
82
- };
83
- }
84
-
85
92
  const clampedColorIntensity = Math.max(0, Math.min(1, colorIntensity));
93
+ const clampedHdrBoost = Math.max(1, Math.min(4, hdrBoost));
86
94
 
87
95
  const resolvedBackColor: ColorRGB =
88
96
  typeof backColor === "string" ? hexToRgb(backColor) : backColor;
89
97
 
90
98
  let canvasRef: HTMLCanvasElement;
91
99
 
92
- onMount(() => {
93
- const canvas = canvasRef;
94
- if (!canvas) return;
100
+ // Cached pre-scaled colors to avoid re-parsing hex on every splat
101
+ let colorIndex = 0;
102
+ let cachedFluidColorHex: string | undefined;
103
+ let cachedFluidColor: ColorRGB | null = null;
104
+ let cachedFluidColorsRef: string[] | undefined;
105
+ let cachedFluidColorsScaled: ColorRGB[] = [];
95
106
 
96
- interface Pointer {
97
- id: number;
98
- texcoordX: number;
99
- texcoordY: number;
100
- prevTexcoordX: number;
101
- prevTexcoordY: number;
102
- deltaX: number;
103
- deltaY: number;
104
- down: boolean;
105
- moved: boolean;
106
- color: ColorRGB;
107
+ function getScaledColor(hex: string): ColorRGB {
108
+ const { r, g, b } = hexToRgb(hex);
109
+ return {
110
+ r: r * clampedColorIntensity,
111
+ g: g * clampedColorIntensity,
112
+ b: b * clampedColorIntensity,
113
+ };
114
+ }
115
+
116
+ function getCachedFluidColor(hex: string): ColorRGB {
117
+ if (cachedFluidColor === null || cachedFluidColorHex !== hex) {
118
+ cachedFluidColorHex = hex;
119
+ cachedFluidColor = getScaledColor(hex);
120
+ }
121
+ return cachedFluidColor;
122
+ }
123
+
124
+ function getCachedFluidColors(colors: string[]): ColorRGB[] {
125
+ if (cachedFluidColorsRef !== colors) {
126
+ cachedFluidColorsRef = colors;
127
+ cachedFluidColorsScaled = colors.map(getScaledColor);
107
128
  }
129
+ return cachedFluidColorsScaled;
130
+ }
131
+
132
+ function generateColor(): ColorRGB {
133
+ if (fluidColor) {
134
+ return getCachedFluidColor(fluidColor);
135
+ }
136
+ if (fluidColors && fluidColors.length > 0) {
137
+ const colors = getCachedFluidColors(fluidColors);
138
+ const idx = colorIndex % colors.length;
139
+ colorIndex++;
140
+ return colors[idx];
141
+ }
142
+ const c = HSVtoRGB(Math.random(), 1.0, 1.0);
143
+ c.r *= clampedColorIntensity;
144
+ c.g *= clampedColorIntensity;
145
+ c.b *= clampedColorIntensity;
146
+ return c;
147
+ }
108
148
 
109
- function pointerPrototype(): Pointer {
110
- return {
111
- id: -1,
112
- texcoordX: 0,
113
- texcoordY: 0,
114
- prevTexcoordX: 0,
115
- prevTexcoordY: 0,
116
- deltaX: 0,
117
- deltaY: 0,
118
- down: false,
119
- moved: false,
120
- color: { r: 0, g: 0, b: 0 },
149
+ // Singleton wiring shared by the WebGL and WebGPU paths.
150
+ function registerInstance(cleanup: () => void): () => void {
151
+ if (!allowMultiple) {
152
+ if (activeInstance) {
153
+ if (import.meta.env.DEV) {
154
+ console.warn(
155
+ "[FluidCursor] Destroying previous instance. Only one instance is allowed by default. Use `allowMultiple={true}` to opt out of singleton behavior."
156
+ );
157
+ }
158
+ activeInstance();
159
+ }
160
+ activeInstance = cleanup;
161
+ return () => {
162
+ cleanup();
163
+ if (activeInstance === cleanup) {
164
+ activeInstance = null;
165
+ }
121
166
  };
122
167
  }
168
+ return cleanup;
169
+ }
170
+
171
+ // Inert handle for environments where no renderer came up at all, so a
172
+ // consumer waiting on onReady can tell "unsupported" from "still starting".
173
+ const noRendererHandle: FluidCursorHandle = {
174
+ moveTo() {},
175
+ penUp() {},
176
+ burst() {},
177
+ renderLevel: "none",
178
+ };
179
+
180
+ // Hand the handle to the consumer. Guarded because the WebGPU path invokes
181
+ // this inside its startup promise chain: an exception from the callback would
182
+ // otherwise be caught as a WebGPU setup failure, retrying WebGL on a canvas
183
+ // already claimed by WebGPU and dropping the registered cleanup. Re-thrown
184
+ // asynchronously so it still surfaces as an unhandled error.
185
+ function notifyReady(handle: FluidCursorHandle) {
186
+ if (!onReady) return;
187
+ try {
188
+ onReady(handle);
189
+ } catch (error) {
190
+ queueMicrotask(() => {
191
+ throw error;
192
+ });
193
+ }
194
+ }
195
+
196
+ onMount(() => {
197
+ const canvas = canvasRef;
198
+ if (!canvas) return;
123
199
 
124
- const pointers: Pointer[] = [pointerPrototype()];
200
+ const mountToken = ++mountCounter;
201
+ const pointers = [pointerPrototype()];
125
202
 
126
203
  const config = {
127
204
  SIM_RESOLUTION: simResolution,
@@ -141,270 +218,373 @@
141
218
  TRANSPARENT: transparent,
142
219
  };
143
220
 
144
- // Get WebGL context
145
- const context = getWebGLContext(canvas);
146
- if (!context.gl || !context.ext) return;
221
+ // HDR path: WebGPU engine (rgba16float backbuffer, display-p3,
222
+ // extended tone mapping). Falls back to the WebGL path below when
223
+ // WebGPU is unavailable or initialization fails.
224
+ if (hdr && typeof navigator !== "undefined" && "gpu" in navigator) {
225
+ let disposed = false;
226
+ let activeCleanup: (() => void) | null = null;
227
+ startWebGpuFluid(canvas, {
228
+ simResolution: config.SIM_RESOLUTION,
229
+ dyeResolution: config.DYE_RESOLUTION,
230
+ densityDissipation: config.DENSITY_DISSIPATION,
231
+ velocityDissipation: config.VELOCITY_DISSIPATION,
232
+ pressure: config.PRESSURE,
233
+ pressureIterations: config.PRESSURE_ITERATIONS,
234
+ curl: config.CURL,
235
+ splatRadius: config.SPLAT_RADIUS,
236
+ splatForce: config.SPLAT_FORCE,
237
+ shading: config.SHADING,
238
+ colorUpdateSpeed: config.COLOR_UPDATE_SPEED,
239
+ exposure: clampedHdrBoost,
240
+ contained,
241
+ interactive,
242
+ autoSplat,
243
+ autoSplatInterval,
244
+ pauseWhenHidden,
245
+ splatOnMount,
246
+ generateColor,
247
+ })
248
+ .then((handle) => {
249
+ if (disposed) {
250
+ handle?.cleanup();
251
+ return;
252
+ }
253
+ // A newer singleton instance mounted while we were starting up:
254
+ // registering now would destroy it (newest-mount-wins would
255
+ // invert). Discard this stale completion instead.
256
+ if (!allowMultiple && mountToken !== mountCounter) {
257
+ handle?.cleanup();
258
+ return;
259
+ }
260
+ if (!handle) {
261
+ // WebGPU unavailable: the WebGL fallback surfaces its own
262
+ // handle through onReady.
263
+ activeCleanup = startWebGl() ?? null;
264
+ return;
265
+ }
266
+ activeCleanup = registerInstance(handle.cleanup);
267
+ notifyReady(handle);
268
+ })
269
+ .catch((error) => {
270
+ // Unexpected failure while wiring the WebGPU path: fall back
271
+ // to WebGL instead of leaving a permanently blank canvas.
272
+ if (import.meta.env.DEV) {
273
+ console.warn("[FluidCursor] WebGPU setup failed, falling back to WebGL:", error);
274
+ }
275
+ if (!disposed) activeCleanup = startWebGl() ?? null;
276
+ });
277
+ return () => {
278
+ disposed = true;
279
+ activeCleanup?.();
280
+ };
281
+ }
147
282
 
148
- // These are guaranteed non-null after the check above
149
- const gl = context.gl;
150
- const ext = context.ext;
283
+ return startWebGl();
284
+
285
+ // WebGL engine path (default, and fallback when WebGPU is missing).
286
+ function startWebGl(): (() => void) | undefined {
287
+ // Wide-gamut probe result, surfaced through the handle's renderLevel.
288
+ let renderLevel: FluidRenderLevel = "webgl-sdr";
289
+ // Dedicated synthetic pointer driven programmatically through the
290
+ // handle. It lives alongside the mouse pointer in the same list, so
291
+ // applyInputs splats both and the two inputs coexist. Only created
292
+ // when a consumer asked for the handle: an unused extra pointer would
293
+ // still consume the shared fluidColors sequence in updateColors.
294
+ const autopilotPointer = onReady ? pointerPrototype() : null;
295
+ if (autopilotPointer) pointers.push(autopilotPointer);
296
+ let penWasUp = true;
297
+
298
+ // Get WebGL context
299
+ const context = getWebGLContext(canvas);
300
+ if (!context.gl || !context.ext) {
301
+ // No renderer at all (WebGPU already failed if it was tried).
302
+ notifyReady(noRendererHandle);
303
+ return;
304
+ }
305
+
306
+ // These are guaranteed non-null after the check above
307
+ const gl = context.gl;
308
+ const ext = context.ext;
309
+
310
+ // If no linear filtering, reduce resolution
311
+ if (!ext.supportLinearFiltering) {
312
+ config.DYE_RESOLUTION = 256;
313
+ config.SHADING = false;
314
+ }
315
+
316
+ function getWebGLContext(canvas: HTMLCanvasElement) {
317
+ const params = {
318
+ alpha: true,
319
+ depth: false,
320
+ stencil: false,
321
+ antialias: false,
322
+ preserveDrawingBuffer: false,
323
+ };
151
324
 
152
- // If no linear filtering, reduce resolution
153
- if (!ext.supportLinearFiltering) {
154
- config.DYE_RESOLUTION = 256;
155
- config.SHADING = false;
156
- }
325
+ let gl = canvas.getContext("webgl2", params) as WebGL2RenderingContext | null;
157
326
 
158
- function getWebGLContext(canvas: HTMLCanvasElement) {
159
- const params = {
160
- alpha: true,
161
- depth: false,
162
- stencil: false,
163
- antialias: false,
164
- preserveDrawingBuffer: false,
165
- };
327
+ if (!gl) {
328
+ gl = (canvas.getContext("webgl", params) ||
329
+ canvas.getContext("experimental-webgl", params)) as WebGL2RenderingContext | null;
330
+ }
166
331
 
167
- let gl = canvas.getContext("webgl2", params) as WebGL2RenderingContext | null;
332
+ if (!gl) {
333
+ console.error("Unable to initialize WebGL.");
334
+ return { gl: null, ext: null };
335
+ }
168
336
 
169
- if (!gl) {
170
- gl = (canvas.getContext("webgl", params) ||
171
- canvas.getContext("experimental-webgl", params)) as WebGL2RenderingContext | null;
172
- }
337
+ // HDR fallback: wide-gamut backbuffer when the WebGPU engine is
338
+ // unavailable. Colors are reinterpreted in P3 (more saturated).
339
+ // The property is read back because assigning an unsupported color
340
+ // space silently leaves the buffer in sRGB.
341
+ if (hdr && "drawingBufferColorSpace" in gl) {
342
+ try {
343
+ gl.drawingBufferColorSpace = "display-p3";
344
+ if (gl.drawingBufferColorSpace === "display-p3") {
345
+ renderLevel = "webgl-p3";
346
+ }
347
+ } catch {
348
+ // Unsupported color space: buffer stays sRGB.
349
+ }
350
+ if (import.meta.env.DEV) {
351
+ console.info(
352
+ renderLevel === "webgl-p3"
353
+ ? "[FluidCursor] HDR level: webgl-p3 (wide gamut fallback)"
354
+ : "[FluidCursor] HDR level: webgl-sdr (display-p3 drawing buffer rejected)"
355
+ );
356
+ }
357
+ }
173
358
 
174
- if (!gl) {
175
- console.error("Unable to initialize WebGL.");
176
- return { gl: null, ext: null };
177
- }
359
+ const isWebGL2 = "drawBuffers" in gl;
178
360
 
179
- const isWebGL2 = "drawBuffers" in gl;
361
+ let supportLinearFiltering = false;
362
+ let halfFloat = null;
180
363
 
181
- let supportLinearFiltering = false;
182
- let halfFloat = null;
364
+ if (isWebGL2) {
365
+ (gl as WebGL2RenderingContext).getExtension("EXT_color_buffer_float");
366
+ supportLinearFiltering = !!(gl as WebGL2RenderingContext).getExtension(
367
+ "OES_texture_float_linear"
368
+ );
369
+ } else {
370
+ halfFloat = gl.getExtension("OES_texture_half_float");
371
+ supportLinearFiltering = !!gl.getExtension("OES_texture_half_float_linear");
372
+ }
183
373
 
184
- if (isWebGL2) {
185
- (gl as WebGL2RenderingContext).getExtension("EXT_color_buffer_float");
186
- supportLinearFiltering = !!(gl as WebGL2RenderingContext).getExtension(
187
- "OES_texture_float_linear"
188
- );
189
- } else {
190
- halfFloat = gl.getExtension("OES_texture_half_float");
191
- supportLinearFiltering = !!gl.getExtension("OES_texture_half_float_linear");
192
- }
374
+ gl.clearColor(0, 0, 0, 1);
193
375
 
194
- gl.clearColor(0, 0, 0, 1);
376
+ const halfFloatTexType = isWebGL2
377
+ ? (gl as WebGL2RenderingContext).HALF_FLOAT
378
+ : (halfFloat && halfFloat.HALF_FLOAT_OES) || 0;
195
379
 
196
- const halfFloatTexType = isWebGL2
197
- ? (gl as WebGL2RenderingContext).HALF_FLOAT
198
- : (halfFloat && halfFloat.HALF_FLOAT_OES) || 0;
380
+ let formatRGBA: { internalFormat: number; format: number } | null;
381
+ let formatRG: { internalFormat: number; format: number } | null;
382
+ let formatR: { internalFormat: number; format: number } | null;
199
383
 
200
- let formatRGBA: { internalFormat: number; format: number } | null;
201
- let formatRG: { internalFormat: number; format: number } | null;
202
- let formatR: { internalFormat: number; format: number } | null;
384
+ if (isWebGL2) {
385
+ formatRGBA = getSupportedFormat(
386
+ gl,
387
+ (gl as WebGL2RenderingContext).RGBA16F,
388
+ gl.RGBA,
389
+ halfFloatTexType
390
+ );
391
+ formatRG = getSupportedFormat(
392
+ gl,
393
+ (gl as WebGL2RenderingContext).RG16F,
394
+ (gl as WebGL2RenderingContext).RG,
395
+ halfFloatTexType
396
+ );
397
+ formatR = getSupportedFormat(
398
+ gl,
399
+ (gl as WebGL2RenderingContext).R16F,
400
+ (gl as WebGL2RenderingContext).RED,
401
+ halfFloatTexType
402
+ );
403
+ } else {
404
+ formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
405
+ formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
406
+ formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
407
+ }
203
408
 
204
- if (isWebGL2) {
205
- formatRGBA = getSupportedFormat(
206
- gl,
207
- (gl as WebGL2RenderingContext).RGBA16F,
208
- gl.RGBA,
209
- halfFloatTexType
210
- );
211
- formatRG = getSupportedFormat(
212
- gl,
213
- (gl as WebGL2RenderingContext).RG16F,
214
- (gl as WebGL2RenderingContext).RG,
215
- halfFloatTexType
216
- );
217
- formatR = getSupportedFormat(
409
+ return {
218
410
  gl,
219
- (gl as WebGL2RenderingContext).R16F,
220
- (gl as WebGL2RenderingContext).RED,
221
- halfFloatTexType
222
- );
223
- } else {
224
- formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
225
- formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
226
- formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
227
- }
228
-
229
- return {
230
- gl,
231
- ext: {
232
- formatRGBA,
233
- formatRG,
234
- formatR,
235
- halfFloatTexType,
236
- supportLinearFiltering,
237
- },
238
- };
239
- }
411
+ ext: {
412
+ formatRGBA,
413
+ formatRG,
414
+ formatR,
415
+ halfFloatTexType,
416
+ supportLinearFiltering,
417
+ },
418
+ };
419
+ }
240
420
 
241
- function getSupportedFormat(
242
- gl: WebGLRenderingContext | WebGL2RenderingContext,
243
- internalFormat: number,
244
- format: number,
245
- type: number
246
- ): { internalFormat: number; format: number } | null {
247
- if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
248
- if ("drawBuffers" in gl) {
249
- const gl2 = gl as WebGL2RenderingContext;
250
- switch (internalFormat) {
251
- case gl2.R16F:
252
- return getSupportedFormat(gl2, gl2.RG16F, gl2.RG, type);
253
- case gl2.RG16F:
254
- return getSupportedFormat(gl2, gl2.RGBA16F, gl2.RGBA, type);
255
- default:
256
- return null;
421
+ function getSupportedFormat(
422
+ gl: WebGLRenderingContext | WebGL2RenderingContext,
423
+ internalFormat: number,
424
+ format: number,
425
+ type: number
426
+ ): { internalFormat: number; format: number } | null {
427
+ if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
428
+ if ("drawBuffers" in gl) {
429
+ const gl2 = gl as WebGL2RenderingContext;
430
+ switch (internalFormat) {
431
+ case gl2.R16F:
432
+ return getSupportedFormat(gl2, gl2.RG16F, gl2.RG, type);
433
+ case gl2.RG16F:
434
+ return getSupportedFormat(gl2, gl2.RGBA16F, gl2.RGBA, type);
435
+ default:
436
+ return null;
437
+ }
257
438
  }
439
+ return null;
258
440
  }
259
- return null;
441
+ return { internalFormat, format };
260
442
  }
261
- return { internalFormat, format };
262
- }
263
-
264
- function supportRenderTextureFormat(
265
- gl: WebGLRenderingContext | WebGL2RenderingContext,
266
- internalFormat: number,
267
- format: number,
268
- type: number
269
- ) {
270
- const texture = gl.createTexture();
271
- if (!texture) return false;
272
-
273
- gl.bindTexture(gl.TEXTURE_2D, texture);
274
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
275
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
276
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
277
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
278
- gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
279
-
280
- const fbo = gl.createFramebuffer();
281
- if (!fbo) return false;
282
-
283
- gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
284
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
285
- const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
286
- return status === gl.FRAMEBUFFER_COMPLETE;
287
- }
288
443
 
289
- function hashCode(s: string) {
290
- if (!s.length) return 0;
291
- let hash = 0;
292
- for (let i = 0; i < s.length; i++) {
293
- hash = (hash << 5) - hash + s.charCodeAt(i);
294
- hash |= 0;
295
- }
296
- return hash;
297
- }
444
+ function supportRenderTextureFormat(
445
+ gl: WebGLRenderingContext | WebGL2RenderingContext,
446
+ internalFormat: number,
447
+ format: number,
448
+ type: number
449
+ ) {
450
+ const texture = gl.createTexture();
451
+ if (!texture) return false;
298
452
 
299
- function addKeywords(source: string, keywords: string[] | null) {
300
- if (!keywords) return source;
301
- let keywordsString = "";
302
- for (const keyword of keywords) {
303
- keywordsString += `#define ${keyword}\n`;
304
- }
305
- return keywordsString + source;
306
- }
453
+ gl.bindTexture(gl.TEXTURE_2D, texture);
454
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
455
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
456
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
457
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
458
+ gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
307
459
 
308
- function compileShader(
309
- type: number,
310
- source: string,
311
- keywords: string[] | null = null
312
- ): WebGLShader | null {
313
- const shaderSource = addKeywords(source, keywords);
314
- const shader = gl.createShader(type);
315
- if (!shader) return null;
316
- gl.shaderSource(shader, shaderSource);
317
- gl.compileShader(shader);
318
- return shader;
319
- }
460
+ const fbo = gl.createFramebuffer();
461
+ if (!fbo) return false;
320
462
 
321
- function createProgram(
322
- vertexShader: WebGLShader | null,
323
- fragmentShader: WebGLShader | null
324
- ): WebGLProgram | null {
325
- if (!vertexShader || !fragmentShader) return null;
326
- const program = gl.createProgram();
327
- if (!program) return null;
328
- gl.attachShader(program, vertexShader);
329
- gl.attachShader(program, fragmentShader);
330
- gl.linkProgram(program);
331
- return program;
332
- }
463
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
464
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
465
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
466
+ return status === gl.FRAMEBUFFER_COMPLETE;
467
+ }
333
468
 
334
- function getUniforms(program: WebGLProgram) {
335
- const uniforms: Record<string, WebGLUniformLocation | null> = {};
336
- const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
337
- for (let i = 0; i < uniformCount; i++) {
338
- const uniformInfo = gl.getActiveUniform(program, i);
339
- if (uniformInfo) {
340
- uniforms[uniformInfo.name] = gl.getUniformLocation(program, uniformInfo.name);
469
+ function hashCode(s: string) {
470
+ if (!s.length) return 0;
471
+ let hash = 0;
472
+ for (let i = 0; i < s.length; i++) {
473
+ hash = (hash << 5) - hash + s.charCodeAt(i);
474
+ hash |= 0;
341
475
  }
476
+ return hash;
342
477
  }
343
- return uniforms;
344
- }
345
-
346
- class Program {
347
- program: WebGLProgram | null;
348
- uniforms: Record<string, WebGLUniformLocation | null>;
349
478
 
350
- constructor(vertexShader: WebGLShader | null, fragmentShader: WebGLShader | null) {
351
- this.program = createProgram(vertexShader, fragmentShader);
352
- this.uniforms = this.program ? getUniforms(this.program) : {};
479
+ function addKeywords(source: string, keywords: string[] | null) {
480
+ if (!keywords) return source;
481
+ let keywordsString = "";
482
+ for (const keyword of keywords) {
483
+ keywordsString += `#define ${keyword}\n`;
484
+ }
485
+ return keywordsString + source;
486
+ }
487
+
488
+ function compileShader(
489
+ type: number,
490
+ source: string,
491
+ keywords: string[] | null = null
492
+ ): WebGLShader | null {
493
+ const shaderSource = addKeywords(source, keywords);
494
+ const shader = gl.createShader(type);
495
+ if (!shader) return null;
496
+ gl.shaderSource(shader, shaderSource);
497
+ gl.compileShader(shader);
498
+ return shader;
499
+ }
500
+
501
+ function createProgram(
502
+ vertexShader: WebGLShader | null,
503
+ fragmentShader: WebGLShader | null
504
+ ): WebGLProgram | null {
505
+ if (!vertexShader || !fragmentShader) return null;
506
+ const program = gl.createProgram();
507
+ if (!program) return null;
508
+ gl.attachShader(program, vertexShader);
509
+ gl.attachShader(program, fragmentShader);
510
+ gl.linkProgram(program);
511
+ return program;
512
+ }
513
+
514
+ function getUniforms(program: WebGLProgram) {
515
+ const uniforms: Record<string, WebGLUniformLocation | null> = {};
516
+ const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
517
+ for (let i = 0; i < uniformCount; i++) {
518
+ const uniformInfo = gl.getActiveUniform(program, i);
519
+ if (uniformInfo) {
520
+ uniforms[uniformInfo.name] = gl.getUniformLocation(program, uniformInfo.name);
521
+ }
522
+ }
523
+ return uniforms;
353
524
  }
354
525
 
355
- bind() {
356
- if (this.program) gl.useProgram(this.program);
357
- }
358
- }
526
+ class Program {
527
+ program: WebGLProgram | null;
528
+ uniforms: Record<string, WebGLUniformLocation | null>;
359
529
 
360
- class Material {
361
- vertexShader: WebGLShader | null;
362
- fragmentShaderSource: string;
363
- programs: Record<number, WebGLProgram | null>;
364
- activeProgram: WebGLProgram | null;
365
- uniforms: Record<string, WebGLUniformLocation | null>;
530
+ constructor(vertexShader: WebGLShader | null, fragmentShader: WebGLShader | null) {
531
+ this.program = createProgram(vertexShader, fragmentShader);
532
+ this.uniforms = this.program ? getUniforms(this.program) : {};
533
+ }
366
534
 
367
- constructor(vertexShader: WebGLShader | null, fragmentShaderSource: string) {
368
- this.vertexShader = vertexShader;
369
- this.fragmentShaderSource = fragmentShaderSource;
370
- this.programs = {};
371
- this.activeProgram = null;
372
- this.uniforms = {};
535
+ bind() {
536
+ if (this.program) gl.useProgram(this.program);
537
+ }
373
538
  }
374
539
 
375
- setKeywords(keywords: string[]) {
376
- let hash = 0;
377
- for (const kw of keywords) {
378
- hash += hashCode(kw);
379
- }
380
- let program = this.programs[hash];
381
- if (program == null) {
382
- const fragmentShader = compileShader(
383
- gl.FRAGMENT_SHADER,
384
- this.fragmentShaderSource,
385
- keywords
386
- );
387
- program = createProgram(this.vertexShader, fragmentShader);
388
- this.programs[hash] = program;
540
+ class Material {
541
+ vertexShader: WebGLShader | null;
542
+ fragmentShaderSource: string;
543
+ programs: Record<number, WebGLProgram | null>;
544
+ activeProgram: WebGLProgram | null;
545
+ uniforms: Record<string, WebGLUniformLocation | null>;
546
+
547
+ constructor(vertexShader: WebGLShader | null, fragmentShaderSource: string) {
548
+ this.vertexShader = vertexShader;
549
+ this.fragmentShaderSource = fragmentShaderSource;
550
+ this.programs = {};
551
+ this.activeProgram = null;
552
+ this.uniforms = {};
389
553
  }
390
- if (program === this.activeProgram) return;
391
- if (program) {
392
- this.uniforms = getUniforms(program);
554
+
555
+ setKeywords(keywords: string[]) {
556
+ let hash = 0;
557
+ for (const kw of keywords) {
558
+ hash += hashCode(kw);
559
+ }
560
+ let program = this.programs[hash];
561
+ if (program == null) {
562
+ const fragmentShader = compileShader(
563
+ gl.FRAGMENT_SHADER,
564
+ this.fragmentShaderSource,
565
+ keywords
566
+ );
567
+ program = createProgram(this.vertexShader, fragmentShader);
568
+ this.programs[hash] = program;
569
+ }
570
+ if (program === this.activeProgram) return;
571
+ if (program) {
572
+ this.uniforms = getUniforms(program);
573
+ }
574
+ this.activeProgram = program;
393
575
  }
394
- this.activeProgram = program;
395
- }
396
576
 
397
- bind() {
398
- if (this.activeProgram) {
399
- gl.useProgram(this.activeProgram);
577
+ bind() {
578
+ if (this.activeProgram) {
579
+ gl.useProgram(this.activeProgram);
580
+ }
400
581
  }
401
582
  }
402
- }
403
583
 
404
- // Shaders
405
- const baseVertexShader = compileShader(
406
- gl.VERTEX_SHADER,
407
- `
584
+ // Shaders
585
+ const baseVertexShader = compileShader(
586
+ gl.VERTEX_SHADER,
587
+ `
408
588
  precision highp float;
409
589
  attribute vec2 aPosition;
410
590
  varying vec2 vUv;
@@ -423,11 +603,11 @@
423
603
  gl_Position = vec4(aPosition, 0.0, 1.0);
424
604
  }
425
605
  `
426
- );
606
+ );
427
607
 
428
- const copyShader = compileShader(
429
- gl.FRAGMENT_SHADER,
430
- `
608
+ const copyShader = compileShader(
609
+ gl.FRAGMENT_SHADER,
610
+ `
431
611
  precision mediump float;
432
612
  precision mediump sampler2D;
433
613
  varying highp vec2 vUv;
@@ -437,11 +617,11 @@
437
617
  gl_FragColor = texture2D(uTexture, vUv);
438
618
  }
439
619
  `
440
- );
620
+ );
441
621
 
442
- const clearShader = compileShader(
443
- gl.FRAGMENT_SHADER,
444
- `
622
+ const clearShader = compileShader(
623
+ gl.FRAGMENT_SHADER,
624
+ `
445
625
  precision mediump float;
446
626
  precision mediump sampler2D;
447
627
  varying highp vec2 vUv;
@@ -452,9 +632,9 @@
452
632
  gl_FragColor = value * texture2D(uTexture, vUv);
453
633
  }
454
634
  `
455
- );
635
+ );
456
636
 
457
- const displayShaderSource = `
637
+ const displayShaderSource = `
458
638
  precision highp float;
459
639
  precision highp sampler2D;
460
640
  varying vec2 vUv;
@@ -495,9 +675,9 @@
495
675
  }
496
676
  `;
497
677
 
498
- const splatShader = compileShader(
499
- gl.FRAGMENT_SHADER,
500
- `
678
+ const splatShader = compileShader(
679
+ gl.FRAGMENT_SHADER,
680
+ `
501
681
  precision highp float;
502
682
  precision highp sampler2D;
503
683
  varying vec2 vUv;
@@ -515,11 +695,11 @@
515
695
  gl_FragColor = vec4(base + splat, 1.0);
516
696
  }
517
697
  `
518
- );
698
+ );
519
699
 
520
- const advectionShader = compileShader(
521
- gl.FRAGMENT_SHADER,
522
- `
700
+ const advectionShader = compileShader(
701
+ gl.FRAGMENT_SHADER,
702
+ `
523
703
  precision highp float;
524
704
  precision highp sampler2D;
525
705
  varying vec2 vUv;
@@ -555,12 +735,12 @@
555
735
  gl_FragColor = result / decay;
556
736
  }
557
737
  `,
558
- ext.supportLinearFiltering ? null : ["MANUAL_FILTERING"]
559
- );
738
+ ext.supportLinearFiltering ? null : ["MANUAL_FILTERING"]
739
+ );
560
740
 
561
- const divergenceShader = compileShader(
562
- gl.FRAGMENT_SHADER,
563
- `
741
+ const divergenceShader = compileShader(
742
+ gl.FRAGMENT_SHADER,
743
+ `
564
744
  precision mediump float;
565
745
  precision mediump sampler2D;
566
746
  varying highp vec2 vUv;
@@ -586,11 +766,11 @@
586
766
  gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
587
767
  }
588
768
  `
589
- );
769
+ );
590
770
 
591
- const curlShader = compileShader(
592
- gl.FRAGMENT_SHADER,
593
- `
771
+ const curlShader = compileShader(
772
+ gl.FRAGMENT_SHADER,
773
+ `
594
774
  precision mediump float;
595
775
  precision mediump sampler2D;
596
776
  varying highp vec2 vUv;
@@ -609,11 +789,11 @@
609
789
  gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
610
790
  }
611
791
  `
612
- );
792
+ );
613
793
 
614
- const vorticityShader = compileShader(
615
- gl.FRAGMENT_SHADER,
616
- `
794
+ const vorticityShader = compileShader(
795
+ gl.FRAGMENT_SHADER,
796
+ `
617
797
  precision highp float;
618
798
  precision highp sampler2D;
619
799
  varying vec2 vUv;
@@ -644,11 +824,11 @@
644
824
  gl_FragColor = vec4(velocity, 0.0, 1.0);
645
825
  }
646
826
  `
647
- );
827
+ );
648
828
 
649
- const pressureShader = compileShader(
650
- gl.FRAGMENT_SHADER,
651
- `
829
+ const pressureShader = compileShader(
830
+ gl.FRAGMENT_SHADER,
831
+ `
652
832
  precision mediump float;
653
833
  precision mediump sampler2D;
654
834
  varying highp vec2 vUv;
@@ -670,11 +850,11 @@
670
850
  gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
671
851
  }
672
852
  `
673
- );
853
+ );
674
854
 
675
- const gradientSubtractShader = compileShader(
676
- gl.FRAGMENT_SHADER,
677
- `
855
+ const gradientSubtractShader = compileShader(
856
+ gl.FRAGMENT_SHADER,
857
+ `
678
858
  precision mediump float;
679
859
  precision mediump sampler2D;
680
860
  varying highp vec2 vUv;
@@ -695,874 +875,836 @@
695
875
  gl_FragColor = vec4(velocity, 0.0, 1.0);
696
876
  }
697
877
  `
698
- );
699
-
700
- // Fullscreen Triangles
701
- interface FBO {
702
- texture: WebGLTexture;
703
- fbo: WebGLFramebuffer;
704
- width: number;
705
- height: number;
706
- texelSizeX: number;
707
- texelSizeY: number;
708
- attach: (id: number) => number;
709
- }
878
+ );
710
879
 
711
- interface DoubleFBO {
712
- width: number;
713
- height: number;
714
- texelSizeX: number;
715
- texelSizeY: number;
716
- read: FBO;
717
- write: FBO;
718
- swap: () => void;
719
- }
880
+ // Fullscreen Triangles
881
+ interface FBO {
882
+ texture: WebGLTexture;
883
+ fbo: WebGLFramebuffer;
884
+ width: number;
885
+ height: number;
886
+ texelSizeX: number;
887
+ texelSizeY: number;
888
+ attach: (id: number) => number;
889
+ }
890
+
891
+ interface DoubleFBO {
892
+ width: number;
893
+ height: number;
894
+ texelSizeX: number;
895
+ texelSizeY: number;
896
+ read: FBO;
897
+ write: FBO;
898
+ swap: () => void;
899
+ }
900
+
901
+ const blit = (() => {
902
+ const buffer = gl.createBuffer()!;
903
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
904
+ gl.bufferData(
905
+ gl.ARRAY_BUFFER,
906
+ new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
907
+ gl.STATIC_DRAW
908
+ );
909
+ const elemBuffer = gl.createBuffer()!;
910
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elemBuffer);
911
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
912
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
913
+ gl.enableVertexAttribArray(0);
914
+
915
+ return (target: FBO | null, doClear = false) => {
916
+ if (!gl) return;
917
+ if (!target) {
918
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
919
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
920
+ } else {
921
+ gl.viewport(0, 0, target.width, target.height);
922
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
923
+ }
924
+ if (doClear) {
925
+ gl.clearColor(0, 0, 0, 1);
926
+ gl.clear(gl.COLOR_BUFFER_BIT);
927
+ }
928
+ gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
929
+ };
930
+ })();
931
+
932
+ // FBO variables
933
+ let dye: DoubleFBO;
934
+ let velocity: DoubleFBO;
935
+ let divergenceFBO: FBO;
936
+ let curlFBO: FBO;
937
+ let pressureFBO: DoubleFBO;
938
+
939
+ // WebGL Programs
940
+ const copyProgram = new Program(baseVertexShader, copyShader);
941
+ const clearProgram = new Program(baseVertexShader, clearShader);
942
+ const splatProgram = new Program(baseVertexShader, splatShader);
943
+ const advectionProgram = new Program(baseVertexShader, advectionShader);
944
+ const divergenceProgram = new Program(baseVertexShader, divergenceShader);
945
+ const curlProgram = new Program(baseVertexShader, curlShader);
946
+ const vorticityProgram = new Program(baseVertexShader, vorticityShader);
947
+ const pressureProgram = new Program(baseVertexShader, pressureShader);
948
+ const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader);
949
+ const displayMaterial = new Material(baseVertexShader, displayShaderSource);
950
+
951
+ // FBO creation
952
+ function createFBO(
953
+ w: number,
954
+ h: number,
955
+ internalFormat: number,
956
+ format: number,
957
+ type: number,
958
+ param: number
959
+ ): FBO {
960
+ gl.activeTexture(gl.TEXTURE0);
961
+ const texture = gl.createTexture()!;
962
+ gl.bindTexture(gl.TEXTURE_2D, texture);
963
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
964
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
965
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
966
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
967
+ gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
968
+ const fbo = gl.createFramebuffer()!;
969
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
970
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
971
+ gl.viewport(0, 0, w, h);
972
+ gl.clear(gl.COLOR_BUFFER_BIT);
973
+
974
+ const texelSizeX = 1 / w;
975
+ const texelSizeY = 1 / h;
720
976
 
721
- const blit = (() => {
722
- const buffer = gl.createBuffer()!;
723
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
724
- gl.bufferData(
725
- gl.ARRAY_BUFFER,
726
- new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
727
- gl.STATIC_DRAW
728
- );
729
- const elemBuffer = gl.createBuffer()!;
730
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elemBuffer);
731
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
732
- gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
733
- gl.enableVertexAttribArray(0);
734
-
735
- return (target: FBO | null, doClear = false) => {
736
- if (!gl) return;
737
- if (!target) {
738
- gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
739
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
740
- } else {
741
- gl.viewport(0, 0, target.width, target.height);
742
- gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
743
- }
744
- if (doClear) {
745
- gl.clearColor(0, 0, 0, 1);
746
- gl.clear(gl.COLOR_BUFFER_BIT);
747
- }
748
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
749
- };
750
- })();
751
-
752
- // FBO variables
753
- let dye: DoubleFBO;
754
- let velocity: DoubleFBO;
755
- let divergenceFBO: FBO;
756
- let curlFBO: FBO;
757
- let pressureFBO: DoubleFBO;
758
-
759
- // WebGL Programs
760
- const copyProgram = new Program(baseVertexShader, copyShader);
761
- const clearProgram = new Program(baseVertexShader, clearShader);
762
- const splatProgram = new Program(baseVertexShader, splatShader);
763
- const advectionProgram = new Program(baseVertexShader, advectionShader);
764
- const divergenceProgram = new Program(baseVertexShader, divergenceShader);
765
- const curlProgram = new Program(baseVertexShader, curlShader);
766
- const vorticityProgram = new Program(baseVertexShader, vorticityShader);
767
- const pressureProgram = new Program(baseVertexShader, pressureShader);
768
- const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader);
769
- const displayMaterial = new Material(baseVertexShader, displayShaderSource);
770
-
771
- // FBO creation
772
- function createFBO(
773
- w: number,
774
- h: number,
775
- internalFormat: number,
776
- format: number,
777
- type: number,
778
- param: number
779
- ): FBO {
780
- gl.activeTexture(gl.TEXTURE0);
781
- const texture = gl.createTexture()!;
782
- gl.bindTexture(gl.TEXTURE_2D, texture);
783
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
784
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
785
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
786
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
787
- gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
788
- const fbo = gl.createFramebuffer()!;
789
- gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
790
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
791
- gl.viewport(0, 0, w, h);
792
- gl.clear(gl.COLOR_BUFFER_BIT);
793
-
794
- const texelSizeX = 1 / w;
795
- const texelSizeY = 1 / h;
796
-
797
- return {
798
- texture,
799
- fbo,
800
- width: w,
801
- height: h,
802
- texelSizeX,
803
- texelSizeY,
804
- attach(id: number) {
805
- gl.activeTexture(gl.TEXTURE0 + id);
806
- gl.bindTexture(gl.TEXTURE_2D, texture);
807
- return id;
808
- },
809
- };
810
- }
977
+ return {
978
+ texture,
979
+ fbo,
980
+ width: w,
981
+ height: h,
982
+ texelSizeX,
983
+ texelSizeY,
984
+ attach(id: number) {
985
+ gl.activeTexture(gl.TEXTURE0 + id);
986
+ gl.bindTexture(gl.TEXTURE_2D, texture);
987
+ return id;
988
+ },
989
+ };
990
+ }
811
991
 
812
- function createDoubleFBO(
813
- w: number,
814
- h: number,
815
- internalFormat: number,
816
- format: number,
817
- type: number,
818
- param: number
819
- ): DoubleFBO {
820
- const fbo1 = createFBO(w, h, internalFormat, format, type, param);
821
- const fbo2 = createFBO(w, h, internalFormat, format, type, param);
822
- return {
823
- width: w,
824
- height: h,
825
- texelSizeX: fbo1.texelSizeX,
826
- texelSizeY: fbo1.texelSizeY,
827
- read: fbo1,
828
- write: fbo2,
829
- swap() {
830
- const tmp = this.read;
831
- this.read = this.write;
832
- this.write = tmp;
833
- },
834
- };
835
- }
992
+ function createDoubleFBO(
993
+ w: number,
994
+ h: number,
995
+ internalFormat: number,
996
+ format: number,
997
+ type: number,
998
+ param: number
999
+ ): DoubleFBO {
1000
+ const fbo1 = createFBO(w, h, internalFormat, format, type, param);
1001
+ const fbo2 = createFBO(w, h, internalFormat, format, type, param);
1002
+ return {
1003
+ width: w,
1004
+ height: h,
1005
+ texelSizeX: fbo1.texelSizeX,
1006
+ texelSizeY: fbo1.texelSizeY,
1007
+ read: fbo1,
1008
+ write: fbo2,
1009
+ swap() {
1010
+ const tmp = this.read;
1011
+ this.read = this.write;
1012
+ this.write = tmp;
1013
+ },
1014
+ };
1015
+ }
836
1016
 
837
- function resizeFBO(
838
- target: FBO,
839
- w: number,
840
- h: number,
841
- internalFormat: number,
842
- format: number,
843
- type: number,
844
- param: number
845
- ) {
846
- const newFBO = createFBO(w, h, internalFormat, format, type, param);
847
- copyProgram.bind();
848
- if (copyProgram.uniforms.uTexture)
849
- gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0));
850
- blit(newFBO, false);
851
- return newFBO;
852
- }
1017
+ function resizeFBO(
1018
+ target: FBO,
1019
+ w: number,
1020
+ h: number,
1021
+ internalFormat: number,
1022
+ format: number,
1023
+ type: number,
1024
+ param: number
1025
+ ) {
1026
+ const newFBO = createFBO(w, h, internalFormat, format, type, param);
1027
+ copyProgram.bind();
1028
+ if (copyProgram.uniforms.uTexture)
1029
+ gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0));
1030
+ blit(newFBO, false);
1031
+ return newFBO;
1032
+ }
1033
+
1034
+ function resizeDoubleFBO(
1035
+ target: DoubleFBO,
1036
+ w: number,
1037
+ h: number,
1038
+ internalFormat: number,
1039
+ format: number,
1040
+ type: number,
1041
+ param: number
1042
+ ) {
1043
+ if (target.width === w && target.height === h) return target;
1044
+ target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param);
1045
+ target.write = createFBO(w, h, internalFormat, format, type, param);
1046
+ target.width = w;
1047
+ target.height = h;
1048
+ target.texelSizeX = 1 / w;
1049
+ target.texelSizeY = 1 / h;
1050
+ return target;
1051
+ }
1052
+
1053
+ function initFramebuffers() {
1054
+ const simRes = getResolution(config.SIM_RESOLUTION!);
1055
+ const dyeRes = getResolution(config.DYE_RESOLUTION!);
1056
+
1057
+ const texType = ext.halfFloatTexType;
1058
+ const rgba = ext.formatRGBA!;
1059
+ const rg = ext.formatRG!;
1060
+ const r = ext.formatR!;
1061
+ const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
1062
+ gl.disable(gl.BLEND);
1063
+
1064
+ if (!dye) {
1065
+ dye = createDoubleFBO(
1066
+ dyeRes.width,
1067
+ dyeRes.height,
1068
+ rgba.internalFormat,
1069
+ rgba.format,
1070
+ texType,
1071
+ filtering
1072
+ );
1073
+ } else {
1074
+ dye = resizeDoubleFBO(
1075
+ dye,
1076
+ dyeRes.width,
1077
+ dyeRes.height,
1078
+ rgba.internalFormat,
1079
+ rgba.format,
1080
+ texType,
1081
+ filtering
1082
+ );
1083
+ }
853
1084
 
854
- function resizeDoubleFBO(
855
- target: DoubleFBO,
856
- w: number,
857
- h: number,
858
- internalFormat: number,
859
- format: number,
860
- type: number,
861
- param: number
862
- ) {
863
- if (target.width === w && target.height === h) return target;
864
- target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param);
865
- target.write = createFBO(w, h, internalFormat, format, type, param);
866
- target.width = w;
867
- target.height = h;
868
- target.texelSizeX = 1 / w;
869
- target.texelSizeY = 1 / h;
870
- return target;
871
- }
1085
+ if (!velocity) {
1086
+ velocity = createDoubleFBO(
1087
+ simRes.width,
1088
+ simRes.height,
1089
+ rg.internalFormat,
1090
+ rg.format,
1091
+ texType,
1092
+ filtering
1093
+ );
1094
+ } else {
1095
+ velocity = resizeDoubleFBO(
1096
+ velocity,
1097
+ simRes.width,
1098
+ simRes.height,
1099
+ rg.internalFormat,
1100
+ rg.format,
1101
+ texType,
1102
+ filtering
1103
+ );
1104
+ }
872
1105
 
873
- function initFramebuffers() {
874
- const simRes = getResolution(config.SIM_RESOLUTION!);
875
- const dyeRes = getResolution(config.DYE_RESOLUTION!);
876
-
877
- const texType = ext.halfFloatTexType;
878
- const rgba = ext.formatRGBA!;
879
- const rg = ext.formatRG!;
880
- const r = ext.formatR!;
881
- const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
882
- gl.disable(gl.BLEND);
883
-
884
- if (!dye) {
885
- dye = createDoubleFBO(
886
- dyeRes.width,
887
- dyeRes.height,
888
- rgba.internalFormat,
889
- rgba.format,
890
- texType,
891
- filtering
892
- );
893
- } else {
894
- dye = resizeDoubleFBO(
895
- dye,
896
- dyeRes.width,
897
- dyeRes.height,
898
- rgba.internalFormat,
899
- rgba.format,
1106
+ divergenceFBO = createFBO(
1107
+ simRes.width,
1108
+ simRes.height,
1109
+ r.internalFormat,
1110
+ r.format,
900
1111
  texType,
901
- filtering
1112
+ gl.NEAREST
902
1113
  );
903
- }
904
-
905
- if (!velocity) {
906
- velocity = createDoubleFBO(
1114
+ curlFBO = createFBO(
907
1115
  simRes.width,
908
1116
  simRes.height,
909
- rg.internalFormat,
910
- rg.format,
1117
+ r.internalFormat,
1118
+ r.format,
911
1119
  texType,
912
- filtering
1120
+ gl.NEAREST
913
1121
  );
914
- } else {
915
- velocity = resizeDoubleFBO(
916
- velocity,
1122
+ pressureFBO = createDoubleFBO(
917
1123
  simRes.width,
918
1124
  simRes.height,
919
- rg.internalFormat,
920
- rg.format,
1125
+ r.internalFormat,
1126
+ r.format,
921
1127
  texType,
922
- filtering
1128
+ gl.NEAREST
923
1129
  );
924
1130
  }
925
1131
 
926
- divergenceFBO = createFBO(
927
- simRes.width,
928
- simRes.height,
929
- r.internalFormat,
930
- r.format,
931
- texType,
932
- gl.NEAREST
933
- );
934
- curlFBO = createFBO(
935
- simRes.width,
936
- simRes.height,
937
- r.internalFormat,
938
- r.format,
939
- texType,
940
- gl.NEAREST
941
- );
942
- pressureFBO = createDoubleFBO(
943
- simRes.width,
944
- simRes.height,
945
- r.internalFormat,
946
- r.format,
947
- texType,
948
- gl.NEAREST
949
- );
950
- }
951
-
952
- function updateKeywords() {
953
- const displayKeywords: string[] = [];
954
- if (config.SHADING) displayKeywords.push("SHADING");
955
- displayMaterial.setKeywords(displayKeywords);
956
- }
957
-
958
- function getResolution(resolution: number) {
959
- const w = gl.drawingBufferWidth;
960
- const h = gl.drawingBufferHeight;
961
- const aspectRatio = w / h;
962
- const aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
963
- const min = Math.round(resolution);
964
- const max = Math.round(resolution * aspect);
965
- if (w > h) {
966
- return { width: max, height: min };
967
- }
968
- return { width: min, height: max };
969
- }
970
-
971
- function scaleByPixelRatio(input: number) {
972
- const pixelRatio = window.devicePixelRatio || 1;
973
- return Math.floor(input * pixelRatio);
974
- }
975
-
976
- let canvasRectCache: DOMRect | null = null;
977
- function updateCanvasRectCache() {
978
- if (contained && canvas) canvasRectCache = canvas.getBoundingClientRect();
979
- }
980
- if (contained) {
981
- window.addEventListener("resize", updateCanvasRectCache, { passive: true });
982
- window.addEventListener("scroll", updateCanvasRectCache, { passive: true });
983
- updateCanvasRectCache();
984
- }
985
-
986
- function getCanvasPos(clientX: number, clientY: number): { x: number; y: number } {
987
- if (contained) {
988
- if (!canvasRectCache) updateCanvasRectCache();
989
- const rect = canvasRectCache!;
990
- return {
991
- x: scaleByPixelRatio(clientX - rect.left),
992
- y: scaleByPixelRatio(clientY - rect.top),
993
- };
1132
+ function updateKeywords() {
1133
+ const displayKeywords: string[] = [];
1134
+ if (config.SHADING) displayKeywords.push("SHADING");
1135
+ displayMaterial.setKeywords(displayKeywords);
994
1136
  }
995
- return { x: scaleByPixelRatio(clientX), y: scaleByPixelRatio(clientY) };
996
- }
997
1137
 
998
- // Simulation Setup
999
- updateKeywords();
1000
- initFramebuffers();
1001
-
1002
- let lastUpdateTime = Date.now();
1003
- let colorUpdateTimer = 0.0;
1004
- let colorIndex = 0;
1005
- let animationFrameId: number;
1006
-
1007
- // Cached pre-scaled colors to avoid re-parsing hex on every splat
1008
- let cachedFluidColorHex: string | undefined;
1009
- let cachedFluidColor: ColorRGB | null = null;
1010
- let cachedFluidColorsRef: string[] | undefined;
1011
- let cachedFluidColorsScaled: ColorRGB[] = [];
1012
-
1013
- function getScaledColor(hex: string): ColorRGB {
1014
- const { r, g, b } = hexToRgb(hex);
1015
- return {
1016
- r: r * clampedColorIntensity,
1017
- g: g * clampedColorIntensity,
1018
- b: b * clampedColorIntensity,
1019
- };
1020
- }
1138
+ function getResolution(resolution: number) {
1139
+ const w = gl.drawingBufferWidth;
1140
+ const h = gl.drawingBufferHeight;
1141
+ const aspectRatio = w / h;
1142
+ const aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
1143
+ const min = Math.round(resolution);
1144
+ const max = Math.round(resolution * aspect);
1145
+ if (w > h) {
1146
+ return { width: max, height: min };
1147
+ }
1148
+ return { width: min, height: max };
1149
+ }
1021
1150
 
1022
- function getCachedFluidColor(hex: string): ColorRGB {
1023
- if (cachedFluidColor === null || cachedFluidColorHex !== hex) {
1024
- cachedFluidColorHex = hex;
1025
- cachedFluidColor = getScaledColor(hex);
1151
+ function scaleByPixelRatio(input: number) {
1152
+ const pixelRatio = window.devicePixelRatio || 1;
1153
+ return Math.floor(input * pixelRatio);
1026
1154
  }
1027
- return cachedFluidColor;
1028
- }
1029
1155
 
1030
- function getCachedFluidColors(colors: string[]): ColorRGB[] {
1031
- if (cachedFluidColorsRef !== colors) {
1032
- cachedFluidColorsRef = colors;
1033
- cachedFluidColorsScaled = colors.map(getScaledColor);
1156
+ let canvasRectCache: DOMRect | null = null;
1157
+ function updateCanvasRectCache() {
1158
+ if (contained && canvas) canvasRectCache = canvas.getBoundingClientRect();
1159
+ }
1160
+ if (contained) {
1161
+ window.addEventListener("resize", updateCanvasRectCache, { passive: true });
1162
+ window.addEventListener("scroll", updateCanvasRectCache, { passive: true });
1163
+ updateCanvasRectCache();
1164
+ }
1165
+
1166
+ function getCanvasPos(clientX: number, clientY: number): { x: number; y: number } {
1167
+ if (contained) {
1168
+ if (!canvasRectCache) updateCanvasRectCache();
1169
+ const rect = canvasRectCache!;
1170
+ return {
1171
+ x: scaleByPixelRatio(clientX - rect.left),
1172
+ y: scaleByPixelRatio(clientY - rect.top),
1173
+ };
1174
+ }
1175
+ return { x: scaleByPixelRatio(clientX), y: scaleByPixelRatio(clientY) };
1034
1176
  }
1035
- return cachedFluidColorsScaled;
1036
- }
1037
1177
 
1038
- let isVisible = true;
1039
-
1040
- function updateFrame() {
1041
- if (!isVisible) return;
1042
- const dt = calcDeltaTime();
1043
- if (resizeCanvas()) initFramebuffers();
1044
- updateColors(dt);
1045
- applyInputs();
1046
- step(dt);
1047
- render(null);
1048
- animationFrameId = requestAnimationFrame(updateFrame);
1049
- }
1178
+ // Simulation Setup
1179
+ updateKeywords();
1180
+ initFramebuffers();
1050
1181
 
1051
- function calcDeltaTime() {
1052
- const now = Date.now();
1053
- let dt = (now - lastUpdateTime) / 1000;
1054
- dt = Math.min(dt, 0.016666);
1055
- lastUpdateTime = now;
1056
- return dt;
1057
- }
1182
+ let lastUpdateTime = Date.now();
1183
+ let colorUpdateTimer = 0.0;
1184
+ let animationFrameId: number;
1058
1185
 
1059
- function resizeCanvas() {
1060
- const width = scaleByPixelRatio(canvas.clientWidth);
1061
- const height = scaleByPixelRatio(canvas.clientHeight);
1062
- if (canvas.width !== width || canvas.height !== height) {
1063
- canvas.width = width;
1064
- canvas.height = height;
1065
- return true;
1066
- }
1067
- return false;
1068
- }
1186
+ let isVisible = true;
1069
1187
 
1070
- function updateColors(dt: number) {
1071
- colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED;
1072
- if (colorUpdateTimer >= 1) {
1073
- colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);
1074
- pointers.forEach((p) => {
1075
- p.color = generateColor();
1076
- });
1188
+ function updateFrame() {
1189
+ if (!isVisible) return;
1190
+ const dt = calcDeltaTime();
1191
+ if (resizeCanvas()) initFramebuffers();
1192
+ updateColors(dt);
1193
+ applyInputs();
1194
+ step(dt);
1195
+ render(null);
1196
+ animationFrameId = requestAnimationFrame(updateFrame);
1077
1197
  }
1078
- }
1079
1198
 
1080
- function applyInputs() {
1081
- for (const p of pointers) {
1082
- if (p.moved) {
1083
- p.moved = false;
1084
- splatPointer(p);
1085
- }
1199
+ function calcDeltaTime() {
1200
+ const now = Date.now();
1201
+ let dt = (now - lastUpdateTime) / 1000;
1202
+ dt = Math.min(dt, 0.016666);
1203
+ lastUpdateTime = now;
1204
+ return dt;
1086
1205
  }
1087
- }
1088
-
1089
- function step(dt: number) {
1090
- gl.disable(gl.BLEND);
1091
1206
 
1092
- // Curl
1093
- curlProgram.bind();
1094
- if (curlProgram.uniforms.texelSize) {
1095
- gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1096
- }
1097
- if (curlProgram.uniforms.uVelocity) {
1098
- gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));
1207
+ function resizeCanvas() {
1208
+ const width = scaleByPixelRatio(canvas.clientWidth);
1209
+ const height = scaleByPixelRatio(canvas.clientHeight);
1210
+ if (canvas.width !== width || canvas.height !== height) {
1211
+ canvas.width = width;
1212
+ canvas.height = height;
1213
+ return true;
1214
+ }
1215
+ return false;
1216
+ }
1217
+
1218
+ function updateColors(dt: number) {
1219
+ colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED;
1220
+ if (colorUpdateTimer >= 1) {
1221
+ colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);
1222
+ pointers.forEach((p) => {
1223
+ // The synthetic pointer's color belongs to the handle: pulling
1224
+ // from the generator here would advance the shared fluidColors
1225
+ // index twice per update and overwrite explicit stroke colors.
1226
+ if (p === autopilotPointer) return;
1227
+ p.color = generateColor();
1228
+ });
1229
+ }
1099
1230
  }
1100
- blit(curlFBO);
1101
1231
 
1102
- // Vorticity
1103
- vorticityProgram.bind();
1104
- if (vorticityProgram.uniforms.texelSize) {
1105
- gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1106
- }
1107
- if (vorticityProgram.uniforms.uVelocity) {
1108
- gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));
1109
- }
1110
- if (vorticityProgram.uniforms.uCurl) {
1111
- gl.uniform1i(vorticityProgram.uniforms.uCurl, curlFBO.attach(1));
1112
- }
1113
- if (vorticityProgram.uniforms.curl) {
1114
- gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
1115
- }
1116
- if (vorticityProgram.uniforms.dt) {
1117
- gl.uniform1f(vorticityProgram.uniforms.dt, dt);
1232
+ function applyInputs() {
1233
+ for (const p of pointers) {
1234
+ if (p.moved) {
1235
+ p.moved = false;
1236
+ splatPointer(p);
1237
+ }
1238
+ }
1118
1239
  }
1119
- blit(velocity.write);
1120
- velocity.swap();
1121
1240
 
1122
- // Divergence
1123
- divergenceProgram.bind();
1124
- if (divergenceProgram.uniforms.texelSize) {
1125
- gl.uniform2f(
1126
- divergenceProgram.uniforms.texelSize,
1127
- velocity.texelSizeX,
1128
- velocity.texelSizeY
1129
- );
1130
- }
1131
- if (divergenceProgram.uniforms.uVelocity) {
1132
- gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
1133
- }
1134
- blit(divergenceFBO);
1241
+ function step(dt: number) {
1242
+ gl.disable(gl.BLEND);
1135
1243
 
1136
- // Clear pressure
1137
- clearProgram.bind();
1138
- if (clearProgram.uniforms.uTexture) {
1139
- gl.uniform1i(clearProgram.uniforms.uTexture, pressureFBO.read.attach(0));
1140
- }
1141
- if (clearProgram.uniforms.value) {
1142
- gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);
1143
- }
1144
- blit(pressureFBO.write);
1145
- pressureFBO.swap();
1244
+ // Curl
1245
+ curlProgram.bind();
1246
+ if (curlProgram.uniforms.texelSize) {
1247
+ gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1248
+ }
1249
+ if (curlProgram.uniforms.uVelocity) {
1250
+ gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));
1251
+ }
1252
+ blit(curlFBO);
1253
+
1254
+ // Vorticity
1255
+ vorticityProgram.bind();
1256
+ if (vorticityProgram.uniforms.texelSize) {
1257
+ gl.uniform2f(
1258
+ vorticityProgram.uniforms.texelSize,
1259
+ velocity.texelSizeX,
1260
+ velocity.texelSizeY
1261
+ );
1262
+ }
1263
+ if (vorticityProgram.uniforms.uVelocity) {
1264
+ gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));
1265
+ }
1266
+ if (vorticityProgram.uniforms.uCurl) {
1267
+ gl.uniform1i(vorticityProgram.uniforms.uCurl, curlFBO.attach(1));
1268
+ }
1269
+ if (vorticityProgram.uniforms.curl) {
1270
+ gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
1271
+ }
1272
+ if (vorticityProgram.uniforms.dt) {
1273
+ gl.uniform1f(vorticityProgram.uniforms.dt, dt);
1274
+ }
1275
+ blit(velocity.write);
1276
+ velocity.swap();
1277
+
1278
+ // Divergence
1279
+ divergenceProgram.bind();
1280
+ if (divergenceProgram.uniforms.texelSize) {
1281
+ gl.uniform2f(
1282
+ divergenceProgram.uniforms.texelSize,
1283
+ velocity.texelSizeX,
1284
+ velocity.texelSizeY
1285
+ );
1286
+ }
1287
+ if (divergenceProgram.uniforms.uVelocity) {
1288
+ gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
1289
+ }
1290
+ blit(divergenceFBO);
1146
1291
 
1147
- // Pressure
1148
- pressureProgram.bind();
1149
- if (pressureProgram.uniforms.texelSize) {
1150
- gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1151
- }
1152
- if (pressureProgram.uniforms.uDivergence) {
1153
- gl.uniform1i(pressureProgram.uniforms.uDivergence, divergenceFBO.attach(0));
1154
- }
1155
- for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
1156
- if (pressureProgram.uniforms.uPressure) {
1157
- gl.uniform1i(pressureProgram.uniforms.uPressure, pressureFBO.read.attach(1));
1292
+ // Clear pressure
1293
+ clearProgram.bind();
1294
+ if (clearProgram.uniforms.uTexture) {
1295
+ gl.uniform1i(clearProgram.uniforms.uTexture, pressureFBO.read.attach(0));
1296
+ }
1297
+ if (clearProgram.uniforms.value) {
1298
+ gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);
1158
1299
  }
1159
1300
  blit(pressureFBO.write);
1160
1301
  pressureFBO.swap();
1161
- }
1162
1302
 
1163
- // Gradient Subtract
1164
- gradienSubtractProgram.bind();
1165
- if (gradienSubtractProgram.uniforms.texelSize) {
1166
- gl.uniform2f(
1167
- gradienSubtractProgram.uniforms.texelSize,
1168
- velocity.texelSizeX,
1169
- velocity.texelSizeY
1170
- );
1171
- }
1172
- if (gradienSubtractProgram.uniforms.uPressure) {
1173
- gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressureFBO.read.attach(0));
1174
- }
1175
- if (gradienSubtractProgram.uniforms.uVelocity) {
1176
- gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));
1177
- }
1178
- blit(velocity.write);
1179
- velocity.swap();
1303
+ // Pressure
1304
+ pressureProgram.bind();
1305
+ if (pressureProgram.uniforms.texelSize) {
1306
+ gl.uniform2f(
1307
+ pressureProgram.uniforms.texelSize,
1308
+ velocity.texelSizeX,
1309
+ velocity.texelSizeY
1310
+ );
1311
+ }
1312
+ if (pressureProgram.uniforms.uDivergence) {
1313
+ gl.uniform1i(pressureProgram.uniforms.uDivergence, divergenceFBO.attach(0));
1314
+ }
1315
+ for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
1316
+ if (pressureProgram.uniforms.uPressure) {
1317
+ gl.uniform1i(pressureProgram.uniforms.uPressure, pressureFBO.read.attach(1));
1318
+ }
1319
+ blit(pressureFBO.write);
1320
+ pressureFBO.swap();
1321
+ }
1180
1322
 
1181
- // Advection - velocity
1182
- advectionProgram.bind();
1183
- if (advectionProgram.uniforms.texelSize) {
1184
- gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1185
- }
1186
- if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize) {
1187
- gl.uniform2f(
1188
- advectionProgram.uniforms.dyeTexelSize,
1189
- velocity.texelSizeX,
1190
- velocity.texelSizeY
1191
- );
1192
- }
1193
- const velocityId = velocity.read.attach(0);
1194
- if (advectionProgram.uniforms.uVelocity) {
1195
- gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);
1196
- }
1197
- if (advectionProgram.uniforms.uSource) {
1198
- gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);
1199
- }
1200
- if (advectionProgram.uniforms.dt) {
1201
- gl.uniform1f(advectionProgram.uniforms.dt, dt);
1202
- }
1203
- if (advectionProgram.uniforms.dissipation) {
1204
- gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);
1205
- }
1206
- blit(velocity.write);
1207
- velocity.swap();
1323
+ // Gradient Subtract
1324
+ gradienSubtractProgram.bind();
1325
+ if (gradienSubtractProgram.uniforms.texelSize) {
1326
+ gl.uniform2f(
1327
+ gradienSubtractProgram.uniforms.texelSize,
1328
+ velocity.texelSizeX,
1329
+ velocity.texelSizeY
1330
+ );
1331
+ }
1332
+ if (gradienSubtractProgram.uniforms.uPressure) {
1333
+ gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressureFBO.read.attach(0));
1334
+ }
1335
+ if (gradienSubtractProgram.uniforms.uVelocity) {
1336
+ gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));
1337
+ }
1338
+ blit(velocity.write);
1339
+ velocity.swap();
1340
+
1341
+ // Advection - velocity
1342
+ advectionProgram.bind();
1343
+ if (advectionProgram.uniforms.texelSize) {
1344
+ gl.uniform2f(
1345
+ advectionProgram.uniforms.texelSize,
1346
+ velocity.texelSizeX,
1347
+ velocity.texelSizeY
1348
+ );
1349
+ }
1350
+ if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize) {
1351
+ gl.uniform2f(
1352
+ advectionProgram.uniforms.dyeTexelSize,
1353
+ velocity.texelSizeX,
1354
+ velocity.texelSizeY
1355
+ );
1356
+ }
1357
+ const velocityId = velocity.read.attach(0);
1358
+ if (advectionProgram.uniforms.uVelocity) {
1359
+ gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);
1360
+ }
1361
+ if (advectionProgram.uniforms.uSource) {
1362
+ gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);
1363
+ }
1364
+ if (advectionProgram.uniforms.dt) {
1365
+ gl.uniform1f(advectionProgram.uniforms.dt, dt);
1366
+ }
1367
+ if (advectionProgram.uniforms.dissipation) {
1368
+ gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);
1369
+ }
1370
+ blit(velocity.write);
1371
+ velocity.swap();
1208
1372
 
1209
- // Advection - dye
1210
- if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize) {
1211
- gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY);
1212
- }
1213
- if (advectionProgram.uniforms.uVelocity) {
1214
- gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
1215
- }
1216
- if (advectionProgram.uniforms.uSource) {
1217
- gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
1218
- }
1219
- if (advectionProgram.uniforms.dissipation) {
1220
- gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
1373
+ // Advection - dye
1374
+ if (!ext.supportLinearFiltering && advectionProgram.uniforms.dyeTexelSize) {
1375
+ gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY);
1376
+ }
1377
+ if (advectionProgram.uniforms.uVelocity) {
1378
+ gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
1379
+ }
1380
+ if (advectionProgram.uniforms.uSource) {
1381
+ gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
1382
+ }
1383
+ if (advectionProgram.uniforms.dissipation) {
1384
+ gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
1385
+ }
1386
+ blit(dye.write);
1387
+ dye.swap();
1221
1388
  }
1222
- blit(dye.write);
1223
- dye.swap();
1224
- }
1225
-
1226
- function render(target: FBO | null) {
1227
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1228
- gl.enable(gl.BLEND);
1229
- drawDisplay(target);
1230
- }
1231
1389
 
1232
- function drawDisplay(target: FBO | null) {
1233
- const width = target ? target.width : gl.drawingBufferWidth;
1234
- const height = target ? target.height : gl.drawingBufferHeight;
1235
- displayMaterial.bind();
1236
- if (config.SHADING && displayMaterial.uniforms.texelSize) {
1237
- gl.uniform2f(displayMaterial.uniforms.texelSize, 1 / width, 1 / height);
1390
+ function render(target: FBO | null) {
1391
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1392
+ gl.enable(gl.BLEND);
1393
+ drawDisplay(target);
1238
1394
  }
1239
- if (displayMaterial.uniforms.uTexture) {
1240
- gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
1395
+
1396
+ function drawDisplay(target: FBO | null) {
1397
+ const width = target ? target.width : gl.drawingBufferWidth;
1398
+ const height = target ? target.height : gl.drawingBufferHeight;
1399
+ displayMaterial.bind();
1400
+ if (config.SHADING && displayMaterial.uniforms.texelSize) {
1401
+ gl.uniform2f(displayMaterial.uniforms.texelSize, 1 / width, 1 / height);
1402
+ }
1403
+ if (displayMaterial.uniforms.uTexture) {
1404
+ gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
1405
+ }
1406
+ blit(target, false);
1241
1407
  }
1242
- blit(target, false);
1243
- }
1244
1408
 
1245
- // Interaction
1246
- function splatPointer(pointer: Pointer) {
1247
- const dx = pointer.deltaX * config.SPLAT_FORCE;
1248
- const dy = pointer.deltaY * config.SPLAT_FORCE;
1249
- splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
1250
- }
1409
+ // Interaction
1410
+ function splatPointer(pointer: Pointer) {
1411
+ const dx = pointer.deltaX * config.SPLAT_FORCE;
1412
+ const dy = pointer.deltaY * config.SPLAT_FORCE;
1413
+ splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
1414
+ }
1251
1415
 
1252
- function clickSplat(pointer: Pointer) {
1253
- const color = generateColor();
1254
- color.r *= 10;
1255
- color.g *= 10;
1256
- color.b *= 10;
1257
- const dx = 10 * (Math.random() - 0.5);
1258
- const dy = 30 * (Math.random() - 0.5);
1259
- splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
1260
- }
1416
+ function clickSplat(pointer: Pointer) {
1417
+ const color = generateColor();
1418
+ color.r *= 10;
1419
+ color.g *= 10;
1420
+ color.b *= 10;
1421
+ const dx = 10 * (Math.random() - 0.5);
1422
+ const dy = 30 * (Math.random() - 0.5);
1423
+ splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
1424
+ }
1425
+
1426
+ function splat(x: number, y: number, dx: number, dy: number, color: ColorRGB) {
1427
+ splatProgram.bind();
1428
+ if (splatProgram.uniforms.uTarget) {
1429
+ gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
1430
+ }
1431
+ if (splatProgram.uniforms.aspectRatio) {
1432
+ gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);
1433
+ }
1434
+ if (splatProgram.uniforms.point) {
1435
+ gl.uniform2f(splatProgram.uniforms.point, x, y);
1436
+ }
1437
+ if (splatProgram.uniforms.color) {
1438
+ gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0);
1439
+ }
1440
+ if (splatProgram.uniforms.radius) {
1441
+ let radius = correctRadius(config.SPLAT_RADIUS / 100)!;
1442
+ if (contained) {
1443
+ radius = scaleRadiusForContainer(radius, canvas.clientHeight, window.innerHeight);
1444
+ }
1445
+ gl.uniform1f(splatProgram.uniforms.radius, radius);
1446
+ }
1447
+ blit(velocity.write);
1448
+ velocity.swap();
1261
1449
 
1262
- function splat(x: number, y: number, dx: number, dy: number, color: ColorRGB) {
1263
- splatProgram.bind();
1264
- if (splatProgram.uniforms.uTarget) {
1265
- gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
1266
- }
1267
- if (splatProgram.uniforms.aspectRatio) {
1268
- gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);
1269
- }
1270
- if (splatProgram.uniforms.point) {
1271
- gl.uniform2f(splatProgram.uniforms.point, x, y);
1450
+ if (splatProgram.uniforms.uTarget) {
1451
+ gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
1452
+ }
1453
+ if (splatProgram.uniforms.color) {
1454
+ gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
1455
+ }
1456
+ blit(dye.write);
1457
+ dye.swap();
1458
+ }
1459
+
1460
+ function multipleSplats(steps: number) {
1461
+ // Simulate a cursor sweep along a random arc — each step on its own frame
1462
+ // so the velocity field has time to evolve between injections.
1463
+ const angle = Math.random() * Math.PI * 2;
1464
+ const cx = 0.25 + Math.random() * 0.5;
1465
+ const cy = 0.25 + Math.random() * 0.5;
1466
+ const arcSpan = Math.PI * (0.8 + Math.random() * 0.8);
1467
+ let i = 0;
1468
+
1469
+ function step() {
1470
+ if (i >= steps) return;
1471
+ const t = i / Math.max(1, steps - 1);
1472
+ const a = angle + t * arcSpan;
1473
+ const r = 0.15 + 0.05 * Math.sin(t * Math.PI);
1474
+ const x = cx + Math.cos(a) * r;
1475
+ const y = cy + Math.sin(a) * r;
1476
+ const color = generateColor();
1477
+ splat(
1478
+ x,
1479
+ y,
1480
+ -Math.sin(a) * config.SPLAT_FORCE * 0.5,
1481
+ Math.cos(a) * config.SPLAT_FORCE * 0.5,
1482
+ color
1483
+ );
1484
+ i++;
1485
+ if (i < steps) requestAnimationFrame(step);
1486
+ }
1487
+ requestAnimationFrame(step);
1488
+ }
1489
+
1490
+ function correctRadius(radius: number) {
1491
+ const aspectRatio = canvas.width / canvas.height;
1492
+ if (aspectRatio > 1) radius *= aspectRatio;
1493
+ return radius;
1494
+ }
1495
+
1496
+ function updatePointerDownData(pointer: Pointer, id: number, posX: number, posY: number) {
1497
+ pointer.id = id;
1498
+ pointer.down = true;
1499
+ pointer.moved = false;
1500
+ pointer.texcoordX = posX / canvas.width;
1501
+ pointer.texcoordY = 1 - posY / canvas.height;
1502
+ pointer.prevTexcoordX = pointer.texcoordX;
1503
+ pointer.prevTexcoordY = pointer.texcoordY;
1504
+ pointer.deltaX = 0;
1505
+ pointer.deltaY = 0;
1506
+ pointer.color = generateColor();
1507
+ }
1508
+
1509
+ function updatePointerMoveData(
1510
+ pointer: Pointer,
1511
+ posX: number,
1512
+ posY: number,
1513
+ color: ColorRGB
1514
+ ) {
1515
+ pointer.prevTexcoordX = pointer.texcoordX;
1516
+ pointer.prevTexcoordY = pointer.texcoordY;
1517
+ pointer.texcoordX = posX / canvas.width;
1518
+ pointer.texcoordY = 1 - posY / canvas.height;
1519
+ pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX)!;
1520
+ pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY)!;
1521
+ pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
1522
+ pointer.color = color;
1523
+ }
1524
+
1525
+ function correctDeltaX(delta: number) {
1526
+ const aspectRatio = canvas.width / canvas.height;
1527
+ if (aspectRatio < 1) delta *= aspectRatio;
1528
+ return delta;
1529
+ }
1530
+
1531
+ function correctDeltaY(delta: number) {
1532
+ const aspectRatio = canvas.width / canvas.height;
1533
+ if (aspectRatio > 1) delta /= aspectRatio;
1534
+ return delta;
1535
+ }
1536
+
1537
+ // Programmatic drive surface (exposed through onReady).
1538
+ //
1539
+ // x,y in [0,1], top-left origin. `updatePointerMoveData` applies
1540
+ // `texcoordY = 1 - posY / height`, so feeding it a top-left pixel Y
1541
+ // yields the same path a real mouse event's clientY takes. This is the
1542
+ // only Y flip on the WebGL path.
1543
+ function autopilotMoveTo(x: number, y: number, color?: ColorRGB) {
1544
+ if (!autopilotPointer) return;
1545
+ const posX = x * canvas.width;
1546
+ const posY = y * canvas.height;
1547
+ if (penWasUp) {
1548
+ // First move of a new stroke: reposition only (mirrors
1549
+ // updatePointerDownData) so the jump from the previous stroke's
1550
+ // end never draws a connecting streak.
1551
+ penWasUp = false;
1552
+ autopilotPointer.down = true;
1553
+ autopilotPointer.moved = false;
1554
+ autopilotPointer.texcoordX = posX / canvas.width;
1555
+ autopilotPointer.texcoordY = 1 - posY / canvas.height;
1556
+ autopilotPointer.prevTexcoordX = autopilotPointer.texcoordX;
1557
+ autopilotPointer.prevTexcoordY = autopilotPointer.texcoordY;
1558
+ autopilotPointer.deltaX = 0;
1559
+ autopilotPointer.deltaY = 0;
1560
+ // A stroke with no explicit color takes a fresh generated one, as
1561
+ // a real pointer-down does: the prototype's black would inject
1562
+ // invisible dye.
1563
+ autopilotPointer.color = color ?? generateColor();
1564
+ return;
1565
+ }
1566
+ updatePointerMoveData(autopilotPointer, posX, posY, color ?? autopilotPointer.color);
1272
1567
  }
1273
- if (splatProgram.uniforms.color) {
1274
- gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0);
1568
+ function autopilotPenUp() {
1569
+ // `moved` is deliberately left alone: applyInputs consumes and clears
1570
+ // it on the next frame, so clearing it here would swallow the final
1571
+ // segment of a stroke finished synchronously before that frame.
1572
+ penWasUp = true;
1573
+ if (autopilotPointer) autopilotPointer.down = false;
1275
1574
  }
1276
- if (splatProgram.uniforms.radius) {
1277
- gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100)!);
1575
+ // Top-left origin in, bottom-up texcoords out: flip y and negate dy.
1576
+ function autopilotBurst(x: number, y: number, dx: number, dy: number, color: ColorRGB) {
1577
+ splat(x, 1 - y, dx, -dy, color);
1278
1578
  }
1279
- blit(velocity.write);
1280
- velocity.swap();
1281
1579
 
1282
- if (splatProgram.uniforms.uTarget) {
1283
- gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
1284
- }
1285
- if (splatProgram.uniforms.color) {
1286
- gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
1580
+ // Event Listeners
1581
+ function handleMouseDown(e: MouseEvent) {
1582
+ const pointer = pointers[0];
1583
+ const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
1584
+ updatePointerDownData(pointer, -1, posX, posY);
1585
+ clickSplat(pointer);
1287
1586
  }
1288
- blit(dye.write);
1289
- dye.swap();
1290
- }
1291
1587
 
1292
- function multipleSplats(steps: number) {
1293
- // Simulate a cursor sweep along a random arc — each step on its own frame
1294
- // so the velocity field has time to evolve between injections.
1295
- const angle = Math.random() * Math.PI * 2;
1296
- const cx = 0.25 + Math.random() * 0.5;
1297
- const cy = 0.25 + Math.random() * 0.5;
1298
- const arcSpan = Math.PI * (0.8 + Math.random() * 0.8);
1299
- let i = 0;
1300
-
1301
- function step() {
1302
- if (i >= steps) return;
1303
- const t = i / Math.max(1, steps - 1);
1304
- const a = angle + t * arcSpan;
1305
- const r = 0.15 + 0.05 * Math.sin(t * Math.PI);
1306
- const x = cx + Math.cos(a) * r;
1307
- const y = cy + Math.sin(a) * r;
1588
+ function handleFirstMouseMove(e: MouseEvent) {
1589
+ const pointer = pointers[0];
1590
+ const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
1308
1591
  const color = generateColor();
1309
- splat(
1310
- x,
1311
- y,
1312
- -Math.sin(a) * config.SPLAT_FORCE * 0.5,
1313
- Math.cos(a) * config.SPLAT_FORCE * 0.5,
1314
- color
1315
- );
1316
- i++;
1317
- if (i < steps) requestAnimationFrame(step);
1592
+ updatePointerMoveData(pointer, posX, posY, color);
1593
+ document.body.removeEventListener("mousemove", handleFirstMouseMove);
1318
1594
  }
1319
- requestAnimationFrame(step);
1320
- }
1321
-
1322
- function correctRadius(radius: number) {
1323
- const aspectRatio = canvas.width / canvas.height;
1324
- if (aspectRatio > 1) radius *= aspectRatio;
1325
- return radius;
1326
- }
1327
1595
 
1328
- function updatePointerDownData(pointer: Pointer, id: number, posX: number, posY: number) {
1329
- pointer.id = id;
1330
- pointer.down = true;
1331
- pointer.moved = false;
1332
- pointer.texcoordX = posX / canvas.width;
1333
- pointer.texcoordY = 1 - posY / canvas.height;
1334
- pointer.prevTexcoordX = pointer.texcoordX;
1335
- pointer.prevTexcoordY = pointer.texcoordY;
1336
- pointer.deltaX = 0;
1337
- pointer.deltaY = 0;
1338
- pointer.color = generateColor();
1339
- }
1340
-
1341
- function updatePointerMoveData(pointer: Pointer, posX: number, posY: number, color: ColorRGB) {
1342
- pointer.prevTexcoordX = pointer.texcoordX;
1343
- pointer.prevTexcoordY = pointer.texcoordY;
1344
- pointer.texcoordX = posX / canvas.width;
1345
- pointer.texcoordY = 1 - posY / canvas.height;
1346
- pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX)!;
1347
- pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY)!;
1348
- pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
1349
- pointer.color = color;
1350
- }
1351
-
1352
- function correctDeltaX(delta: number) {
1353
- const aspectRatio = canvas.width / canvas.height;
1354
- if (aspectRatio < 1) delta *= aspectRatio;
1355
- return delta;
1356
- }
1357
-
1358
- function correctDeltaY(delta: number) {
1359
- const aspectRatio = canvas.width / canvas.height;
1360
- if (aspectRatio > 1) delta /= aspectRatio;
1361
- return delta;
1362
- }
1363
-
1364
- function generateColor(): ColorRGB {
1365
- if (fluidColor) {
1366
- return getCachedFluidColor(fluidColor);
1367
- }
1368
- if (fluidColors && fluidColors.length > 0) {
1369
- const colors = getCachedFluidColors(fluidColors);
1370
- const idx = colorIndex % colors.length;
1371
- colorIndex++;
1372
- return colors[idx];
1373
- }
1374
- const c = HSVtoRGB(Math.random(), 1.0, 1.0);
1375
- c.r *= clampedColorIntensity;
1376
- c.g *= clampedColorIntensity;
1377
- c.b *= clampedColorIntensity;
1378
- return c;
1379
- }
1380
-
1381
- function HSVtoRGB(h: number, s: number, v: number): ColorRGB {
1382
- let r = 0;
1383
- let g = 0;
1384
- let b = 0;
1385
- const i = Math.floor(h * 6);
1386
- const f = h * 6 - i;
1387
- const p = v * (1 - s);
1388
- const q = v * (1 - f * s);
1389
- const t = v * (1 - (1 - f) * s);
1390
-
1391
- switch (i % 6) {
1392
- case 0:
1393
- r = v;
1394
- g = t;
1395
- b = p;
1396
- break;
1397
- case 1:
1398
- r = q;
1399
- g = v;
1400
- b = p;
1401
- break;
1402
- case 2:
1403
- r = p;
1404
- g = v;
1405
- b = t;
1406
- break;
1407
- case 3:
1408
- r = p;
1409
- g = q;
1410
- b = v;
1411
- break;
1412
- case 4:
1413
- r = t;
1414
- g = p;
1415
- b = v;
1416
- break;
1417
- case 5:
1418
- r = v;
1419
- g = p;
1420
- b = q;
1421
- break;
1422
- }
1423
- return { r, g, b };
1424
- }
1425
-
1426
- function wrap(value: number, min: number, max: number) {
1427
- const range = max - min;
1428
- if (range === 0) return min;
1429
- return ((value - min) % range) + min;
1430
- }
1431
-
1432
- // Event Listeners
1433
- function handleMouseDown(e: MouseEvent) {
1434
- const pointer = pointers[0];
1435
- const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
1436
- updatePointerDownData(pointer, -1, posX, posY);
1437
- clickSplat(pointer);
1438
- }
1439
-
1440
- function handleFirstMouseMove(e: MouseEvent) {
1441
- const pointer = pointers[0];
1442
- const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
1443
- const color = generateColor();
1444
- updatePointerMoveData(pointer, posX, posY, color);
1445
- document.body.removeEventListener("mousemove", handleFirstMouseMove);
1446
- }
1447
-
1448
- function handleMouseMove(e: MouseEvent) {
1449
- const pointer = pointers[0];
1450
- const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
1451
- updatePointerMoveData(pointer, posX, posY, pointer.color);
1452
- }
1453
-
1454
- function handleFirstTouchStart(e: TouchEvent) {
1455
- const touches = e.targetTouches;
1456
- const pointer = pointers[0];
1457
- for (let i = 0; i < touches.length; i++) {
1458
- const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
1459
- updatePointerDownData(pointer, touches[i].identifier, posX, posY);
1596
+ function handleMouseMove(e: MouseEvent) {
1597
+ const pointer = pointers[0];
1598
+ const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
1599
+ updatePointerMoveData(pointer, posX, posY, pointer.color);
1460
1600
  }
1461
- document.body.removeEventListener("touchstart", handleFirstTouchStart);
1462
- }
1463
1601
 
1464
- function handleTouchStart(e: TouchEvent) {
1465
- const touches = e.targetTouches;
1466
- const pointer = pointers[0];
1467
- for (let i = 0; i < touches.length; i++) {
1468
- const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
1469
- updatePointerDownData(pointer, touches[i].identifier, posX, posY);
1602
+ function handleFirstTouchStart(e: TouchEvent) {
1603
+ const touches = e.targetTouches;
1604
+ const pointer = pointers[0];
1605
+ for (let i = 0; i < touches.length; i++) {
1606
+ const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
1607
+ updatePointerDownData(pointer, touches[i].identifier, posX, posY);
1608
+ }
1609
+ document.body.removeEventListener("touchstart", handleFirstTouchStart);
1470
1610
  }
1471
- }
1472
1611
 
1473
- function handleTouchMove(e: TouchEvent) {
1474
- const touches = e.targetTouches;
1475
- const pointer = pointers[0];
1476
- for (let i = 0; i < touches.length; i++) {
1477
- const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
1478
- updatePointerMoveData(pointer, posX, posY, pointer.color);
1612
+ function handleTouchStart(e: TouchEvent) {
1613
+ const touches = e.targetTouches;
1614
+ const pointer = pointers[0];
1615
+ for (let i = 0; i < touches.length; i++) {
1616
+ const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
1617
+ updatePointerDownData(pointer, touches[i].identifier, posX, posY);
1618
+ }
1479
1619
  }
1480
- }
1481
-
1482
- // Add event listeners
1483
- if (interactive) {
1484
- window.addEventListener("mousedown", handleMouseDown);
1485
- document.body.addEventListener("mousemove", handleFirstMouseMove);
1486
- window.addEventListener("mousemove", handleMouseMove);
1487
- document.body.addEventListener("touchstart", handleFirstTouchStart);
1488
- window.addEventListener("touchstart", handleTouchStart, false);
1489
- window.addEventListener("touchmove", handleTouchMove, false);
1490
- }
1491
-
1492
- // Pause animation when scrolled out of view
1493
- let observer: IntersectionObserver | null = null;
1494
- if (pauseWhenHidden) {
1495
- observer = new IntersectionObserver(
1496
- ([entry]) => {
1497
- const wasVisible = isVisible;
1498
- isVisible = entry.isIntersecting;
1499
- if (isVisible && !wasVisible) {
1500
- lastUpdateTime = Date.now();
1501
- animationFrameId = requestAnimationFrame(updateFrame);
1502
- }
1503
- },
1504
- { threshold: 0 }
1505
- );
1506
- observer.observe(canvas);
1507
- }
1508
-
1509
- // Start animation
1510
- updateFrame();
1511
- if (splatOnMount) multipleSplats(Math.floor(Math.random() * 6) + 10);
1512
-
1513
- // Auto-splat timer
1514
- let autoSplatTimer: ReturnType<typeof setInterval> | null = null;
1515
- if (autoSplat) {
1516
- autoSplatTimer = setInterval(() => {
1517
- const color = generateColor();
1518
- splat(
1519
- Math.random(),
1520
- Math.random(),
1521
- (Math.random() - 0.5) * 200,
1522
- (Math.random() - 0.5) * 200,
1523
- color
1524
- );
1525
- }, autoSplatInterval);
1526
- }
1527
1620
 
1528
- // Cleanup
1529
- function cleanup() {
1530
- cancelAnimationFrame(animationFrameId);
1531
- if (observer) observer.disconnect();
1532
- if (autoSplatTimer) clearInterval(autoSplatTimer);
1533
- if (interactive) {
1534
- window.removeEventListener("mousedown", handleMouseDown);
1535
- document.body.removeEventListener("mousemove", handleFirstMouseMove);
1536
- window.removeEventListener("mousemove", handleMouseMove);
1537
- document.body.removeEventListener("touchstart", handleFirstTouchStart);
1538
- window.removeEventListener("touchstart", handleTouchStart);
1539
- window.removeEventListener("touchmove", handleTouchMove);
1540
- }
1541
- if (contained) {
1542
- window.removeEventListener("resize", updateCanvasRectCache);
1543
- window.removeEventListener("scroll", updateCanvasRectCache);
1621
+ function handleTouchMove(e: TouchEvent) {
1622
+ const touches = e.targetTouches;
1623
+ const pointer = pointers[0];
1624
+ for (let i = 0; i < touches.length; i++) {
1625
+ const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
1626
+ updatePointerMoveData(pointer, posX, posY, pointer.color);
1627
+ }
1544
1628
  }
1545
- }
1546
1629
 
1547
- if (!allowMultiple) {
1548
- if (activeInstance) {
1549
- if (import.meta.env.DEV) {
1550
- console.warn(
1551
- "[FluidCursor] Destroying previous instance. Only one instance is allowed by default. Use `allowMultiple={true}` to opt out of singleton behavior."
1630
+ // Add event listeners
1631
+ if (interactive) {
1632
+ window.addEventListener("mousedown", handleMouseDown);
1633
+ document.body.addEventListener("mousemove", handleFirstMouseMove);
1634
+ window.addEventListener("mousemove", handleMouseMove);
1635
+ document.body.addEventListener("touchstart", handleFirstTouchStart);
1636
+ window.addEventListener("touchstart", handleTouchStart, false);
1637
+ window.addEventListener("touchmove", handleTouchMove, false);
1638
+ }
1639
+
1640
+ // Pause animation when scrolled out of view
1641
+ let observer: IntersectionObserver | null = null;
1642
+ if (pauseWhenHidden) {
1643
+ observer = new IntersectionObserver(
1644
+ ([entry]) => {
1645
+ const wasVisible = isVisible;
1646
+ isVisible = entry.isIntersecting;
1647
+ if (isVisible && !wasVisible) {
1648
+ lastUpdateTime = Date.now();
1649
+ animationFrameId = requestAnimationFrame(updateFrame);
1650
+ }
1651
+ },
1652
+ { threshold: 0 }
1653
+ );
1654
+ observer.observe(canvas);
1655
+ }
1656
+
1657
+ // Start animation
1658
+ updateFrame();
1659
+ if (splatOnMount) multipleSplats(Math.floor(Math.random() * 6) + 10);
1660
+
1661
+ // Auto-splat timer
1662
+ let autoSplatTimer: ReturnType<typeof setInterval> | null = null;
1663
+ if (autoSplat) {
1664
+ autoSplatTimer = setInterval(() => {
1665
+ const color = generateColor();
1666
+ splat(
1667
+ Math.random(),
1668
+ Math.random(),
1669
+ (Math.random() - 0.5) * 200,
1670
+ (Math.random() - 0.5) * 200,
1671
+ color
1552
1672
  );
1673
+ }, autoSplatInterval);
1674
+ }
1675
+
1676
+ // Cleanup
1677
+ function cleanup() {
1678
+ cancelAnimationFrame(animationFrameId);
1679
+ if (observer) observer.disconnect();
1680
+ if (autoSplatTimer) clearInterval(autoSplatTimer);
1681
+ if (interactive) {
1682
+ window.removeEventListener("mousedown", handleMouseDown);
1683
+ document.body.removeEventListener("mousemove", handleFirstMouseMove);
1684
+ window.removeEventListener("mousemove", handleMouseMove);
1685
+ document.body.removeEventListener("touchstart", handleFirstTouchStart);
1686
+ window.removeEventListener("touchstart", handleTouchStart);
1687
+ window.removeEventListener("touchmove", handleTouchMove);
1553
1688
  }
1554
- activeInstance();
1555
- }
1556
- activeInstance = cleanup;
1557
- return () => {
1558
- cleanup();
1559
- if (activeInstance === cleanup) {
1560
- activeInstance = null;
1689
+ if (contained) {
1690
+ window.removeEventListener("resize", updateCanvasRectCache);
1691
+ window.removeEventListener("scroll", updateCanvasRectCache);
1561
1692
  }
1562
- };
1563
- }
1693
+ }
1564
1694
 
1565
- return cleanup;
1695
+ // Register before notifying: a throwing callback must not cost us the
1696
+ // cleanup registration (listeners, rAF loop and GL resources would
1697
+ // otherwise outlive the component).
1698
+ const dispose = registerInstance(cleanup);
1699
+ notifyReady({
1700
+ moveTo: autopilotMoveTo,
1701
+ penUp: autopilotPenUp,
1702
+ burst: autopilotBurst,
1703
+ renderLevel,
1704
+ });
1705
+
1706
+ return dispose;
1707
+ }
1566
1708
  });
1567
1709
  </script>
1568
1710