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