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