your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { colorToHue, getCSSVar, getHexOfCardColor } from '../../helper';
3 import type { ContentComponentProps } from '../../types';
4 import { onMount, onDestroy, tick } from 'svelte';
5 let { item }: ContentComponentProps = $props();
6
7 let container: HTMLDivElement;
8 let fluidCanvas: HTMLCanvasElement;
9 let maskCanvas: HTMLCanvasElement;
10 let animationId: number;
11 let splatIntervalId: ReturnType<typeof setInterval>;
12 let maskDrawRaf = 0;
13 let maskReady = false;
14 let isInitialized = $state(false);
15 let resizeObserver: ResizeObserver | null = null;
16
17 // Pure hash function for shader keyword caching
18 function hashCode(s: string) {
19 if (s.length === 0) return 0;
20 let hash = 0;
21 for (let i = 0; i < s.length; i++) {
22 hash = (hash << 5) - hash + s.charCodeAt(i);
23 hash |= 0;
24 }
25 return hash;
26 }
27
28 // WebGL helper types
29 type CompileShaderFn = (type: number, source: string, keywords?: string[]) => WebGLShader;
30 type CreateProgramFn = (vs: WebGLShader, fs: WebGLShader) => WebGLProgram;
31 type GetUniformsFn = (program: WebGLProgram) => Record<string, WebGLUniformLocation | null>;
32
33 // Material class for shader programs with keyword variants
34 class Material {
35 private gl: WebGL2RenderingContext;
36 private compileShader: CompileShaderFn;
37 private createProgramFn: CreateProgramFn;
38 private getUniformsFn: GetUniformsFn;
39 vertexShader: WebGLShader;
40 fragmentShaderSource: string;
41 programs: Record<number, WebGLProgram> = {};
42 activeProgram: WebGLProgram | null = null;
43 uniforms: Record<string, WebGLUniformLocation | null> = {};
44
45 constructor(
46 gl: WebGL2RenderingContext,
47 vertexShader: WebGLShader,
48 fragmentShaderSource: string,
49 compileShader: CompileShaderFn,
50 createProgramFn: CreateProgramFn,
51 getUniformsFn: GetUniformsFn
52 ) {
53 this.gl = gl;
54 this.compileShader = compileShader;
55 this.createProgramFn = createProgramFn;
56 this.getUniformsFn = getUniformsFn;
57 this.vertexShader = vertexShader;
58 this.fragmentShaderSource = fragmentShaderSource;
59 }
60
61 setKeywords(keywords: string[]) {
62 let hash = 0;
63 for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]);
64
65 let program = this.programs[hash];
66 if (!program) {
67 const fragmentShader = this.compileShader(
68 this.gl.FRAGMENT_SHADER,
69 this.fragmentShaderSource,
70 keywords
71 );
72 program = this.createProgramFn(this.vertexShader, fragmentShader);
73 this.programs[hash] = program;
74 }
75
76 if (program === this.activeProgram) return;
77
78 this.uniforms = this.getUniformsFn(program);
79 this.activeProgram = program;
80 }
81
82 bind() {
83 this.gl.useProgram(this.activeProgram);
84 }
85 }
86
87 // Program class for simple shader programs
88 class Program {
89 private gl: WebGL2RenderingContext;
90 uniforms: Record<string, WebGLUniformLocation | null> = {};
91 program: WebGLProgram;
92
93 constructor(
94 gl: WebGL2RenderingContext,
95 vertexShader: WebGLShader,
96 fragmentShader: WebGLShader,
97 createProgramFn: CreateProgramFn,
98 getUniformsFn: GetUniformsFn
99 ) {
100 this.gl = gl;
101 this.program = createProgramFn(vertexShader, fragmentShader);
102 this.uniforms = getUniformsFn(this.program);
103 }
104
105 bind() {
106 this.gl.useProgram(this.program);
107 }
108 }
109
110 // Get text from card data
111 const text = $derived((item.cardData?.text as string) || 'hello');
112 const fontWeight = '900';
113 const fontFamily = 'Arial';
114 const fontSize = $derived(parseFloat(item.cardData?.fontSize) || 0.33);
115
116 // Draw text mask on overlay canvas
117 function drawOverlayCanvas() {
118 if (!maskCanvas || !container) return;
119
120 const width = container.clientWidth;
121 const height = container.clientHeight;
122 if (width === 0 || height === 0) return;
123
124 const dpr = window.devicePixelRatio || 1;
125
126 maskCanvas.width = width * dpr;
127 maskCanvas.height = height * dpr;
128
129 const ctx = maskCanvas.getContext('2d')!;
130 ctx.setTransform(1, 0, 0, 1, 0, 0);
131 ctx.globalCompositeOperation = 'source-over';
132 ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
133 ctx.scale(dpr, dpr);
134
135 //const color = getCSSVar('--color-base-900');
136
137 ctx.fillStyle = 'black';
138 ctx.fillRect(0, 0, width, height);
139
140 // Font size as percentage of container width
141 const textFontSize = Math.round(width * fontSize);
142 ctx.font = `${fontWeight} ${textFontSize}px ${fontFamily}`;
143
144 ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
145 ctx.lineWidth = 2;
146 ctx.textAlign = 'center';
147
148 const metrics = ctx.measureText(text);
149 let textY = height / 2;
150 if (
151 metrics.actualBoundingBoxAscent !== undefined &&
152 metrics.actualBoundingBoxDescent !== undefined
153 ) {
154 ctx.textBaseline = 'alphabetic';
155 textY = (height + metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2;
156 } else {
157 ctx.textBaseline = 'middle';
158 }
159
160 ctx.strokeText(text, width / 2, textY);
161 ctx.globalCompositeOperation = 'destination-out';
162 ctx.fillText(text, width / 2, textY);
163 ctx.globalCompositeOperation = 'source-over';
164 maskReady = true;
165 }
166
167 function scheduleMaskDraw() {
168 const width = container?.clientWidth ?? 0;
169 const height = container?.clientHeight ?? 0;
170 if (width > 0 && height > 0) {
171 drawOverlayCanvas();
172 return;
173 }
174 if (maskDrawRaf) return;
175 maskDrawRaf = requestAnimationFrame(() => {
176 maskDrawRaf = 0;
177 const nextWidth = container?.clientWidth ?? 0;
178 const nextHeight = container?.clientHeight ?? 0;
179 if (nextWidth === 0 || nextHeight === 0) {
180 scheduleMaskDraw();
181 return;
182 }
183 drawOverlayCanvas();
184 });
185 }
186
187 // Redraw overlay when text settings change (only after initialization)
188 $effect(() => {
189 // Access all reactive values to track them
190 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
191 text;
192 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
193 fontSize;
194 // Only redraw if already initialized
195 if (isInitialized) {
196 scheduleMaskDraw();
197 }
198 });
199
200 onMount(async () => {
201 // Wait for layout to settle
202 await tick();
203
204 const computedColor = getHexOfCardColor(item);
205 const hue = colorToHue(computedColor) / 360;
206
207 // Wait for a frame to ensure dimensions are set
208 requestAnimationFrame(() => {
209 initFluidSimulation(hue, hue - 0.3);
210 });
211
212 if (document.fonts?.ready) {
213 document.fonts.ready.then(() => {
214 if (isInitialized) scheduleMaskDraw();
215 });
216 }
217 });
218
219 onDestroy(() => {
220 if (animationId) cancelAnimationFrame(animationId);
221 if (splatIntervalId) clearInterval(splatIntervalId);
222 if (maskDrawRaf) cancelAnimationFrame(maskDrawRaf);
223 if (resizeObserver) resizeObserver.disconnect();
224 });
225
226 function initFluidSimulation(startHue: number, endHue: number) {
227 if (!fluidCanvas || !maskCanvas || !container) return;
228
229 maskReady = false;
230 scheduleMaskDraw();
231
232 // Simulation config
233 const config = {
234 SIM_RESOLUTION: 128,
235 DYE_RESOLUTION: 1024,
236 CAPTURE_RESOLUTION: 512,
237 DENSITY_DISSIPATION: 1.0,
238 VELOCITY_DISSIPATION: 0.1,
239 PRESSURE: 0.8,
240 PRESSURE_ITERATIONS: 20,
241 CURL: 30,
242 SPLAT_RADIUS: 0.25,
243 SPLAT_FORCE: 1000,
244 SHADING: true,
245 COLORFUL: true,
246 COLOR_UPDATE_SPEED: 10,
247 PAUSED: false,
248 BACK_COLOR: { r: 0, g: 0, b: 0 },
249 TRANSPARENT: false,
250 BLOOM: false,
251 BLOOM_ITERATIONS: 8,
252 BLOOM_RESOLUTION: 256,
253 BLOOM_INTENSITY: 0.8,
254 BLOOM_THRESHOLD: 0.8,
255 BLOOM_SOFT_KNEE: 0.7,
256 SUNRAYS: true,
257 SUNRAYS_RESOLUTION: 196,
258 SUNRAYS_WEIGHT: 1.0,
259 START_HUE: startHue,
260 END_HUE: endHue,
261 RENDER_SPEED: 0.4
262 };
263
264 function PointerPrototype() {
265 return {
266 id: -1,
267 texcoordX: 0,
268 texcoordY: 0,
269 prevTexcoordX: 0,
270 prevTexcoordY: 0,
271 deltaX: 0,
272 deltaY: 0,
273 down: false,
274 moved: false,
275 color: [0, 0, 0] as [number, number, number]
276 };
277 }
278
279 type Pointer = ReturnType<typeof PointerPrototype>;
280 let pointers: Pointer[] = [PointerPrototype()];
281 let splatStack: number[] = [];
282
283 const { gl: glMaybeNull, ext } = getWebGLContext(fluidCanvas);
284 if (!glMaybeNull) return;
285 const gl = glMaybeNull;
286
287 if (isMobile()) {
288 config.DYE_RESOLUTION = 512;
289 }
290 if (!ext.supportLinearFiltering) {
291 config.DYE_RESOLUTION = 512;
292 config.SHADING = false;
293 config.BLOOM = false;
294 config.SUNRAYS = false;
295 }
296
297 function getWebGLContext(canvas: HTMLCanvasElement) {
298 const params = {
299 alpha: true,
300 depth: false,
301 stencil: true,
302 antialias: false,
303 preserveDrawingBuffer: false
304 };
305
306 let gl = canvas.getContext('webgl2', params) as WebGL2RenderingContext | null;
307 const isWebGL2 = !!gl;
308 if (!isWebGL2) {
309 gl = (canvas.getContext('webgl', params) ||
310 canvas.getContext('experimental-webgl', params)) as WebGL2RenderingContext | null;
311 }
312
313 if (!gl) return { gl: null, ext: { supportLinearFiltering: false } as any };
314
315 let halfFloat: any;
316 let supportLinearFiltering = false;
317 if (isWebGL2) {
318 gl.getExtension('EXT_color_buffer_float');
319 supportLinearFiltering = !!gl.getExtension('OES_texture_float_linear');
320 } else {
321 halfFloat = gl.getExtension('OES_texture_half_float');
322 supportLinearFiltering = !!gl.getExtension('OES_texture_half_float_linear');
323 }
324
325 gl.clearColor(0.0, 0.0, 0.0, 1.0);
326
327 let halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat?.HALF_FLOAT_OES;
328 let fallbackToUnsignedByte = false;
329 if (!halfFloatTexType) {
330 halfFloatTexType = gl.UNSIGNED_BYTE;
331 supportLinearFiltering = true;
332 fallbackToUnsignedByte = true;
333 }
334 let formatRGBA: any;
335 let formatRG: any;
336 let formatR: any;
337
338 if (isWebGL2) {
339 if (fallbackToUnsignedByte) {
340 formatRGBA = { internalFormat: gl.RGBA8, format: gl.RGBA };
341 formatRG = { internalFormat: gl.RGBA8, format: gl.RGBA };
342 formatR = { internalFormat: gl.RGBA8, format: gl.RGBA };
343 } else {
344 formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType);
345 formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);
346 formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);
347 if (!formatRGBA) formatRGBA = { internalFormat: gl.RGBA8, format: gl.RGBA };
348 if (!formatRG) formatRG = { internalFormat: gl.RGBA8, format: gl.RGBA };
349 if (!formatR) formatR = { internalFormat: gl.RGBA8, format: gl.RGBA };
350 }
351 } else {
352 formatRGBA = { internalFormat: gl.RGBA, format: gl.RGBA };
353 formatRG = { internalFormat: gl.RGBA, format: gl.RGBA };
354 formatR = { internalFormat: gl.RGBA, format: gl.RGBA };
355 if (!fallbackToUnsignedByte) {
356 formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatRGBA;
357 formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatRG;
358 formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType) ?? formatR;
359 }
360 }
361
362 return {
363 gl,
364 ext: {
365 formatRGBA,
366 formatRG,
367 formatR,
368 halfFloatTexType,
369 supportLinearFiltering
370 }
371 };
372 }
373
374 function getSupportedFormat(
375 gl: WebGL2RenderingContext,
376 internalFormat: number,
377 format: number,
378 type: number
379 ): { internalFormat: number; format: number } | null {
380 if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
381 switch (internalFormat) {
382 case gl.R16F:
383 return getSupportedFormat(gl, gl.RG16F, gl.RG, type);
384 case gl.RG16F:
385 return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type);
386 default:
387 return null;
388 }
389 }
390 return { internalFormat, format };
391 }
392
393 function supportRenderTextureFormat(
394 gl: WebGL2RenderingContext,
395 internalFormat: number,
396 format: number,
397 type: number
398 ) {
399 if (!type) return false;
400 const texture = gl.createTexture();
401 gl.bindTexture(gl.TEXTURE_2D, texture);
402 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
403 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
404 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
405 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
406 gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
407
408 const fbo = gl.createFramebuffer();
409 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
410 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
411
412 const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
413 return status === gl.FRAMEBUFFER_COMPLETE;
414 }
415
416 function isMobile() {
417 return /Mobi|Android/i.test(navigator.userAgent);
418 }
419
420 function createWebGLProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader) {
421 const program = gl.createProgram()!;
422 gl.attachShader(program, vertexShader);
423 gl.attachShader(program, fragmentShader);
424 gl.linkProgram(program);
425
426 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
427 console.trace(gl.getProgramInfoLog(program));
428 }
429
430 return program;
431 }
432
433 function getUniforms(program: WebGLProgram) {
434 const uniforms: Record<string, WebGLUniformLocation | null> = {};
435 const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
436 for (let i = 0; i < uniformCount; i++) {
437 const uniformName = gl.getActiveUniform(program, i)!.name;
438 uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
439 }
440 return uniforms;
441 }
442
443 function compileShader(type: number, source: string, keywords?: string[]) {
444 source = addKeywords(source, keywords);
445
446 const shader = gl.createShader(type)!;
447 gl.shaderSource(shader, source);
448 gl.compileShader(shader);
449
450 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
451 console.trace(gl.getShaderInfoLog(shader));
452 }
453
454 return shader;
455 }
456
457 function addKeywords(source: string, keywords?: string[]) {
458 if (!keywords) return source;
459 let keywordsString = '';
460 keywords.forEach((keyword) => {
461 keywordsString += '#define ' + keyword + '\n';
462 });
463 return keywordsString + source;
464 }
465
466 const baseVertexShader = compileShader(
467 gl.VERTEX_SHADER,
468 `
469 precision highp float;
470 attribute vec2 aPosition;
471 varying vec2 vUv;
472 varying vec2 vL;
473 varying vec2 vR;
474 varying vec2 vT;
475 varying vec2 vB;
476 uniform vec2 texelSize;
477 void main () {
478 vUv = aPosition * 0.5 + 0.5;
479 vL = vUv - vec2(texelSize.x, 0.0);
480 vR = vUv + vec2(texelSize.x, 0.0);
481 vT = vUv + vec2(0.0, texelSize.y);
482 vB = vUv - vec2(0.0, texelSize.y);
483 gl_Position = vec4(aPosition, 0.0, 1.0);
484 }
485 `
486 );
487
488 const blurVertexShader = compileShader(
489 gl.VERTEX_SHADER,
490 `
491 precision highp float;
492 attribute vec2 aPosition;
493 varying vec2 vUv;
494 varying vec2 vL;
495 varying vec2 vR;
496 uniform vec2 texelSize;
497 void main () {
498 vUv = aPosition * 0.5 + 0.5;
499 float offset = 1.33333333;
500 vL = vUv - texelSize * offset;
501 vR = vUv + texelSize * offset;
502 gl_Position = vec4(aPosition, 0.0, 1.0);
503 }
504 `
505 );
506
507 const blurShader = compileShader(
508 gl.FRAGMENT_SHADER,
509 `
510 precision mediump float;
511 precision mediump sampler2D;
512 varying vec2 vUv;
513 varying vec2 vL;
514 varying vec2 vR;
515 uniform sampler2D uTexture;
516 void main () {
517 vec4 sum = texture2D(uTexture, vUv) * 0.29411764;
518 sum += texture2D(uTexture, vL) * 0.35294117;
519 sum += texture2D(uTexture, vR) * 0.35294117;
520 gl_FragColor = sum;
521 }
522 `
523 );
524
525 const copyShader = compileShader(
526 gl.FRAGMENT_SHADER,
527 `
528 precision mediump float;
529 precision mediump sampler2D;
530 varying highp vec2 vUv;
531 uniform sampler2D uTexture;
532 void main () {
533 gl_FragColor = texture2D(uTexture, vUv);
534 }
535 `
536 );
537
538 const clearShader = compileShader(
539 gl.FRAGMENT_SHADER,
540 `
541 precision mediump float;
542 precision mediump sampler2D;
543 varying highp vec2 vUv;
544 uniform sampler2D uTexture;
545 uniform float value;
546 void main () {
547 gl_FragColor = value * texture2D(uTexture, vUv);
548 }
549 `
550 );
551
552 const colorShader = compileShader(
553 gl.FRAGMENT_SHADER,
554 `
555 precision mediump float;
556 uniform vec4 color;
557 void main () {
558 gl_FragColor = color;
559 }
560 `
561 );
562
563 const displayShaderSource = `
564 precision highp float;
565 precision highp sampler2D;
566 varying vec2 vUv;
567 varying vec2 vL;
568 varying vec2 vR;
569 varying vec2 vT;
570 varying vec2 vB;
571 uniform sampler2D uTexture;
572 uniform sampler2D uBloom;
573 uniform sampler2D uSunrays;
574 uniform sampler2D uDithering;
575 uniform vec2 ditherScale;
576 uniform vec2 texelSize;
577 vec3 linearToGamma (vec3 color) {
578 color = max(color, vec3(0));
579 return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0));
580 }
581 void main () {
582 vec3 c = texture2D(uTexture, vUv).rgb;
583 #ifdef SHADING
584 vec3 lc = texture2D(uTexture, vL).rgb;
585 vec3 rc = texture2D(uTexture, vR).rgb;
586 vec3 tc = texture2D(uTexture, vT).rgb;
587 vec3 bc = texture2D(uTexture, vB).rgb;
588 float dx = length(rc) - length(lc);
589 float dy = length(tc) - length(bc);
590 vec3 n = normalize(vec3(dx, dy, length(texelSize)));
591 vec3 l = vec3(0.0, 0.0, 1.0);
592 float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
593 c *= diffuse;
594 #endif
595 #ifdef BLOOM
596 vec3 bloom = texture2D(uBloom, vUv).rgb;
597 #endif
598 #ifdef SUNRAYS
599 float sunrays = texture2D(uSunrays, vUv).r;
600 c *= sunrays;
601 #ifdef BLOOM
602 bloom *= sunrays;
603 #endif
604 #endif
605 #ifdef BLOOM
606 float noise = texture2D(uDithering, vUv * ditherScale).r;
607 noise = noise * 2.0 - 1.0;
608 bloom += noise / 255.0;
609 bloom = linearToGamma(bloom);
610 c += bloom;
611 #endif
612 float a = max(c.r, max(c.g, c.b));
613 gl_FragColor = vec4(c, a);
614 }
615 `;
616
617 const splatShader = compileShader(
618 gl.FRAGMENT_SHADER,
619 `
620 precision highp float;
621 precision highp sampler2D;
622 varying vec2 vUv;
623 uniform sampler2D uTarget;
624 uniform float aspectRatio;
625 uniform vec3 color;
626 uniform vec2 point;
627 uniform float radius;
628 void main () {
629 vec2 p = vUv - point.xy;
630 p.x *= aspectRatio;
631 vec3 splat = exp(-dot(p, p) / radius) * color;
632 vec3 base = texture2D(uTarget, vUv).xyz;
633 gl_FragColor = vec4(base + splat, 1.0);
634 }
635 `
636 );
637
638 const advectionShader = compileShader(
639 gl.FRAGMENT_SHADER,
640 `
641 precision highp float;
642 precision highp sampler2D;
643 varying vec2 vUv;
644 uniform sampler2D uVelocity;
645 uniform sampler2D uSource;
646 uniform vec2 texelSize;
647 uniform vec2 dyeTexelSize;
648 uniform float dt;
649 uniform float dissipation;
650 vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
651 vec2 st = uv / tsize - 0.5;
652 vec2 iuv = floor(st);
653 vec2 fuv = fract(st);
654 vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);
655 vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);
656 vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);
657 vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);
658 return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);
659 }
660 void main () {
661 #ifdef MANUAL_FILTERING
662 vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;
663 vec4 result = bilerp(uSource, coord, dyeTexelSize);
664 #else
665 vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
666 vec4 result = texture2D(uSource, coord);
667 #endif
668 float decay = 1.0 + dissipation * dt;
669 gl_FragColor = result / decay;
670 }`,
671 ext.supportLinearFiltering ? undefined : ['MANUAL_FILTERING']
672 );
673
674 const divergenceShader = compileShader(
675 gl.FRAGMENT_SHADER,
676 `
677 precision mediump float;
678 precision mediump sampler2D;
679 varying highp vec2 vUv;
680 varying highp vec2 vL;
681 varying highp vec2 vR;
682 varying highp vec2 vT;
683 varying highp vec2 vB;
684 uniform sampler2D uVelocity;
685 void main () {
686 float L = texture2D(uVelocity, vL).x;
687 float R = texture2D(uVelocity, vR).x;
688 float T = texture2D(uVelocity, vT).y;
689 float B = texture2D(uVelocity, vB).y;
690 vec2 C = texture2D(uVelocity, vUv).xy;
691 if (vL.x < 0.0) { L = -C.x; }
692 if (vR.x > 1.0) { R = -C.x; }
693 if (vT.y > 1.0) { T = -C.y; }
694 if (vB.y < 0.0) { B = -C.y; }
695 float div = 0.5 * (R - L + T - B);
696 gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
697 }
698 `
699 );
700
701 const curlShader = compileShader(
702 gl.FRAGMENT_SHADER,
703 `
704 precision mediump float;
705 precision mediump sampler2D;
706 varying highp vec2 vUv;
707 varying highp vec2 vL;
708 varying highp vec2 vR;
709 varying highp vec2 vT;
710 varying highp vec2 vB;
711 uniform sampler2D uVelocity;
712 void main () {
713 float L = texture2D(uVelocity, vL).y;
714 float R = texture2D(uVelocity, vR).y;
715 float T = texture2D(uVelocity, vT).x;
716 float B = texture2D(uVelocity, vB).x;
717 float vorticity = R - L - T + B;
718 gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
719 }
720 `
721 );
722
723 const vorticityShader = compileShader(
724 gl.FRAGMENT_SHADER,
725 `
726 precision highp float;
727 precision highp sampler2D;
728 varying vec2 vUv;
729 varying vec2 vL;
730 varying vec2 vR;
731 varying vec2 vT;
732 varying vec2 vB;
733 uniform sampler2D uVelocity;
734 uniform sampler2D uCurl;
735 uniform float curl;
736 uniform float dt;
737 void main () {
738 float L = texture2D(uCurl, vL).x;
739 float R = texture2D(uCurl, vR).x;
740 float T = texture2D(uCurl, vT).x;
741 float B = texture2D(uCurl, vB).x;
742 float C = texture2D(uCurl, vUv).x;
743 vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
744 force /= length(force) + 0.0001;
745 force *= curl * C;
746 force.y *= -1.0;
747 vec2 velocity = texture2D(uVelocity, vUv).xy;
748 velocity += force * dt;
749 velocity = min(max(velocity, -1000.0), 1000.0);
750 gl_FragColor = vec4(velocity, 0.0, 1.0);
751 }
752 `
753 );
754
755 const pressureShader = compileShader(
756 gl.FRAGMENT_SHADER,
757 `
758 precision mediump float;
759 precision mediump sampler2D;
760 varying highp vec2 vUv;
761 varying highp vec2 vL;
762 varying highp vec2 vR;
763 varying highp vec2 vT;
764 varying highp vec2 vB;
765 uniform sampler2D uPressure;
766 uniform sampler2D uDivergence;
767 void main () {
768 float L = texture2D(uPressure, vL).x;
769 float R = texture2D(uPressure, vR).x;
770 float T = texture2D(uPressure, vT).x;
771 float B = texture2D(uPressure, vB).x;
772 float C = texture2D(uPressure, vUv).x;
773 float divergence = texture2D(uDivergence, vUv).x;
774 float pressure = (L + R + B + T - divergence) * 0.25;
775 gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
776 }
777 `
778 );
779
780 const gradientSubtractShader = compileShader(
781 gl.FRAGMENT_SHADER,
782 `
783 precision mediump float;
784 precision mediump sampler2D;
785 varying highp vec2 vUv;
786 varying highp vec2 vL;
787 varying highp vec2 vR;
788 varying highp vec2 vT;
789 varying highp vec2 vB;
790 uniform sampler2D uPressure;
791 uniform sampler2D uVelocity;
792 void main () {
793 float L = texture2D(uPressure, vL).x;
794 float R = texture2D(uPressure, vR).x;
795 float T = texture2D(uPressure, vT).x;
796 float B = texture2D(uPressure, vB).x;
797 vec2 velocity = texture2D(uVelocity, vUv).xy;
798 velocity.xy -= vec2(R - L, T - B);
799 gl_FragColor = vec4(velocity, 0.0, 1.0);
800 }
801 `
802 );
803
804 const sunraysMaskShader = compileShader(
805 gl.FRAGMENT_SHADER,
806 `
807 precision highp float;
808 precision highp sampler2D;
809 varying vec2 vUv;
810 uniform sampler2D uTexture;
811 void main () {
812 vec4 c = texture2D(uTexture, vUv);
813 float br = max(c.r, max(c.g, c.b));
814 c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8);
815 gl_FragColor = c;
816 }
817 `
818 );
819
820 const sunraysShader = compileShader(
821 gl.FRAGMENT_SHADER,
822 `
823 precision highp float;
824 precision highp sampler2D;
825 varying vec2 vUv;
826 uniform sampler2D uTexture;
827 uniform float weight;
828 #define ITERATIONS 16
829 void main () {
830 float Density = 0.3;
831 float Decay = 0.95;
832 float Exposure = 0.7;
833 vec2 coord = vUv;
834 vec2 dir = vUv - 0.5;
835 dir *= 1.0 / float(ITERATIONS) * Density;
836 float illuminationDecay = 1.0;
837 float color = texture2D(uTexture, vUv).a;
838 for (int i = 0; i < ITERATIONS; i++) {
839 coord -= dir;
840 float col = texture2D(uTexture, coord).a;
841 color += col * illuminationDecay * weight;
842 illuminationDecay *= Decay;
843 }
844 gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0);
845 }
846 `
847 );
848
849 // Setup blit
850 gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
851 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW);
852 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
853 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
854 gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
855 gl.enableVertexAttribArray(0);
856
857 type FBO = {
858 texture: WebGLTexture;
859 fbo: WebGLFramebuffer;
860 width: number;
861 height: number;
862 texelSizeX: number;
863 texelSizeY: number;
864 attach: (id: number) => number;
865 };
866
867 type DoubleFBO = {
868 width: number;
869 height: number;
870 texelSizeX: number;
871 texelSizeY: number;
872 read: FBO;
873 write: FBO;
874 swap: () => void;
875 };
876
877 function blit(target: FBO | null, clear = false) {
878 if (target === null) {
879 gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
880 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
881 } else {
882 gl.viewport(0, 0, target.width, target.height);
883 gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
884 }
885 if (clear) {
886 gl.clearColor(0.0, 0.0, 0.0, 1.0);
887 gl.clear(gl.COLOR_BUFFER_BIT);
888 }
889 gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
890 }
891
892 let dye: DoubleFBO;
893 let velocity: DoubleFBO;
894 let divergence: FBO;
895 let curl: FBO;
896 let pressure: DoubleFBO;
897 let sunrays: FBO;
898 let sunraysTemp: FBO;
899
900 const blurProgram = new Program(
901 gl,
902 blurVertexShader,
903 blurShader,
904 createWebGLProgram,
905 getUniforms
906 );
907 const copyProgram = new Program(
908 gl,
909 baseVertexShader,
910 copyShader,
911 createWebGLProgram,
912 getUniforms
913 );
914 const clearProgram = new Program(
915 gl,
916 baseVertexShader,
917 clearShader,
918 createWebGLProgram,
919 getUniforms
920 );
921 const colorProgram = new Program(
922 gl,
923 baseVertexShader,
924 colorShader,
925 createWebGLProgram,
926 getUniforms
927 );
928 const splatProgram = new Program(
929 gl,
930 baseVertexShader,
931 splatShader,
932 createWebGLProgram,
933 getUniforms
934 );
935 const advectionProgram = new Program(
936 gl,
937 baseVertexShader,
938 advectionShader,
939 createWebGLProgram,
940 getUniforms
941 );
942 const divergenceProgram = new Program(
943 gl,
944 baseVertexShader,
945 divergenceShader,
946 createWebGLProgram,
947 getUniforms
948 );
949 const curlProgram = new Program(
950 gl,
951 baseVertexShader,
952 curlShader,
953 createWebGLProgram,
954 getUniforms
955 );
956 const vorticityProgram = new Program(
957 gl,
958 baseVertexShader,
959 vorticityShader,
960 createWebGLProgram,
961 getUniforms
962 );
963 const pressureProgram = new Program(
964 gl,
965 baseVertexShader,
966 pressureShader,
967 createWebGLProgram,
968 getUniforms
969 );
970 const gradienSubtractProgram = new Program(
971 gl,
972 baseVertexShader,
973 gradientSubtractShader,
974 createWebGLProgram,
975 getUniforms
976 );
977 const sunraysMaskProgram = new Program(
978 gl,
979 baseVertexShader,
980 sunraysMaskShader,
981 createWebGLProgram,
982 getUniforms
983 );
984 const sunraysProgram = new Program(
985 gl,
986 baseVertexShader,
987 sunraysShader,
988 createWebGLProgram,
989 getUniforms
990 );
991
992 const displayMaterial = new Material(
993 gl,
994 baseVertexShader,
995 displayShaderSource,
996 compileShader,
997 createWebGLProgram,
998 getUniforms
999 );
1000
1001 function getResolution(resolution: number) {
1002 let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;
1003 if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio;
1004 const min = Math.round(resolution);
1005 const max = Math.round(resolution * aspectRatio);
1006 if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min };
1007 else return { width: min, height: max };
1008 }
1009
1010 function createFBO(
1011 w: number,
1012 h: number,
1013 internalFormat: number,
1014 format: number,
1015 type: number,
1016 param: number
1017 ): FBO {
1018 gl.activeTexture(gl.TEXTURE0);
1019 const texture = gl.createTexture()!;
1020 gl.bindTexture(gl.TEXTURE_2D, texture);
1021 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
1022 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
1023 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1024 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1025 gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
1026
1027 const fbo = gl.createFramebuffer()!;
1028 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
1029 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
1030 gl.viewport(0, 0, w, h);
1031 gl.clear(gl.COLOR_BUFFER_BIT);
1032
1033 const texelSizeX = 1.0 / w;
1034 const texelSizeY = 1.0 / h;
1035
1036 return {
1037 texture,
1038 fbo,
1039 width: w,
1040 height: h,
1041 texelSizeX,
1042 texelSizeY,
1043 attach(id: number) {
1044 gl.activeTexture(gl.TEXTURE0 + id);
1045 gl.bindTexture(gl.TEXTURE_2D, texture);
1046 return id;
1047 }
1048 };
1049 }
1050
1051 function createDoubleFBO(
1052 w: number,
1053 h: number,
1054 internalFormat: number,
1055 format: number,
1056 type: number,
1057 param: number
1058 ): DoubleFBO {
1059 let fbo1 = createFBO(w, h, internalFormat, format, type, param);
1060 let fbo2 = createFBO(w, h, internalFormat, format, type, param);
1061
1062 return {
1063 width: w,
1064 height: h,
1065 texelSizeX: fbo1.texelSizeX,
1066 texelSizeY: fbo1.texelSizeY,
1067 get read() {
1068 return fbo1;
1069 },
1070 set read(value) {
1071 fbo1 = value;
1072 },
1073 get write() {
1074 return fbo2;
1075 },
1076 set write(value) {
1077 fbo2 = value;
1078 },
1079 swap() {
1080 const temp = fbo1;
1081 fbo1 = fbo2;
1082 fbo2 = temp;
1083 }
1084 };
1085 }
1086
1087 function resizeFBO(
1088 target: FBO,
1089 w: number,
1090 h: number,
1091 internalFormat: number,
1092 format: number,
1093 type: number,
1094 param: number
1095 ) {
1096 const newFBO = createFBO(w, h, internalFormat, format, type, param);
1097 copyProgram.bind();
1098 gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0));
1099 blit(newFBO);
1100 return newFBO;
1101 }
1102
1103 function resizeDoubleFBO(
1104 target: DoubleFBO,
1105 w: number,
1106 h: number,
1107 internalFormat: number,
1108 format: number,
1109 type: number,
1110 param: number
1111 ) {
1112 if (target.width === w && target.height === h) return target;
1113 target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param);
1114 target.write = createFBO(w, h, internalFormat, format, type, param);
1115 target.width = w;
1116 target.height = h;
1117 target.texelSizeX = 1.0 / w;
1118 target.texelSizeY = 1.0 / h;
1119 return target;
1120 }
1121
1122 function initFramebuffers() {
1123 const simRes = getResolution(config.SIM_RESOLUTION);
1124 const dyeRes = getResolution(config.DYE_RESOLUTION);
1125
1126 const texType = ext.halfFloatTexType;
1127 const rgba = ext.formatRGBA;
1128 const rg = ext.formatRG;
1129 const r = ext.formatR;
1130 const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
1131
1132 gl.disable(gl.BLEND);
1133
1134 if (!dye) {
1135 dye = createDoubleFBO(
1136 dyeRes.width,
1137 dyeRes.height,
1138 rgba.internalFormat,
1139 rgba.format,
1140 texType,
1141 filtering
1142 );
1143 } else {
1144 dye = resizeDoubleFBO(
1145 dye,
1146 dyeRes.width,
1147 dyeRes.height,
1148 rgba.internalFormat,
1149 rgba.format,
1150 texType,
1151 filtering
1152 );
1153 }
1154
1155 if (!velocity) {
1156 velocity = createDoubleFBO(
1157 simRes.width,
1158 simRes.height,
1159 rg.internalFormat,
1160 rg.format,
1161 texType,
1162 filtering
1163 );
1164 } else {
1165 velocity = resizeDoubleFBO(
1166 velocity,
1167 simRes.width,
1168 simRes.height,
1169 rg.internalFormat,
1170 rg.format,
1171 texType,
1172 filtering
1173 );
1174 }
1175
1176 divergence = createFBO(
1177 simRes.width,
1178 simRes.height,
1179 r.internalFormat,
1180 r.format,
1181 texType,
1182 gl.NEAREST
1183 );
1184 curl = createFBO(
1185 simRes.width,
1186 simRes.height,
1187 r.internalFormat,
1188 r.format,
1189 texType,
1190 gl.NEAREST
1191 );
1192 pressure = createDoubleFBO(
1193 simRes.width,
1194 simRes.height,
1195 r.internalFormat,
1196 r.format,
1197 texType,
1198 gl.NEAREST
1199 );
1200
1201 initSunraysFramebuffers();
1202 }
1203
1204 function initSunraysFramebuffers() {
1205 const res = getResolution(config.SUNRAYS_RESOLUTION);
1206 const texType = ext.halfFloatTexType;
1207 const r = ext.formatR;
1208 const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
1209
1210 sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering);
1211 sunraysTemp = createFBO(
1212 res.width,
1213 res.height,
1214 r.internalFormat,
1215 r.format,
1216 texType,
1217 filtering
1218 );
1219 }
1220
1221 function updateKeywords() {
1222 const displayKeywords: string[] = [];
1223 if (config.SHADING) displayKeywords.push('SHADING');
1224 if (config.SUNRAYS) displayKeywords.push('SUNRAYS');
1225 displayMaterial.setKeywords(displayKeywords);
1226 }
1227
1228 function scaleByPixelRatio(input: number) {
1229 const pixelRatio = window.devicePixelRatio || 1;
1230 return Math.floor(input * pixelRatio);
1231 }
1232
1233 function resizeCanvas() {
1234 const width = scaleByPixelRatio(fluidCanvas.clientWidth);
1235 const height = scaleByPixelRatio(fluidCanvas.clientHeight);
1236 if (fluidCanvas.width !== width || fluidCanvas.height !== height) {
1237 fluidCanvas.width = width;
1238 fluidCanvas.height = height;
1239 scheduleMaskDraw();
1240 return true;
1241 }
1242 return false;
1243 }
1244
1245 function HSVtoRGB(h: number, s: number, v: number) {
1246 let r = 0,
1247 g = 0,
1248 b = 0;
1249 const i = Math.floor(h * 6);
1250 const f = h * 6 - i;
1251 const p = v * (1 - s);
1252 const q = v * (1 - f * s);
1253 const t = v * (1 - (1 - f) * s);
1254
1255 switch (i % 6) {
1256 case 0:
1257 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1258 ((r = v), (g = t), (b = p));
1259 break;
1260 case 1:
1261 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1262 ((r = q), (g = v), (b = p));
1263 break;
1264 case 2:
1265 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1266 ((r = p), (g = v), (b = t));
1267 break;
1268 case 3:
1269 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1270 ((r = p), (g = q), (b = v));
1271 break;
1272 case 4:
1273 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1274 ((r = t), (g = p), (b = v));
1275 break;
1276 case 5:
1277 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1278 ((r = v), (g = p), (b = q));
1279 break;
1280 }
1281
1282 return { r, g, b };
1283 }
1284
1285 function generateColor() {
1286 const c = HSVtoRGB(
1287 Math.random() * (config.END_HUE - config.START_HUE) + config.START_HUE,
1288 1.0,
1289 1.0
1290 );
1291 c.r *= 0.15;
1292 c.g *= 0.15;
1293 c.b *= 0.15;
1294 return c;
1295 }
1296
1297 function correctRadius(radius: number) {
1298 const aspectRatio = fluidCanvas.width / fluidCanvas.height;
1299 if (aspectRatio > 1) radius *= aspectRatio;
1300 return radius;
1301 }
1302
1303 function splat(
1304 x: number,
1305 y: number,
1306 dx: number,
1307 dy: number,
1308 color: { r: number; g: number; b: number }
1309 ) {
1310 splatProgram.bind();
1311 gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
1312 gl.uniform1f(splatProgram.uniforms.aspectRatio, fluidCanvas.width / fluidCanvas.height);
1313 gl.uniform2f(splatProgram.uniforms.point, x, y);
1314 gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0);
1315 gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0));
1316 blit(velocity.write);
1317 velocity.swap();
1318
1319 gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
1320 gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
1321 blit(dye.write);
1322 dye.swap();
1323 }
1324
1325 function multipleSplats(amount: number) {
1326 for (let i = 0; i < amount; i++) {
1327 const color = generateColor();
1328 color.r *= 10.0;
1329 color.g *= 10.0;
1330 color.b *= 10.0;
1331 const x = Math.random();
1332 const y = Math.random() < 0.5 ? 0.95 : 0.05;
1333 const dx = 300 * (Math.random() - 0.5);
1334 const dy = 3000 * (Math.random() - 0.5);
1335 splat(x, y, dx, dy, color);
1336 }
1337 }
1338
1339 function splatPointer(pointer: Pointer) {
1340 const dx = pointer.deltaX * config.SPLAT_FORCE * 12;
1341 const dy = pointer.deltaY * config.SPLAT_FORCE * 12;
1342 splat(pointer.texcoordX, pointer.texcoordY, dx, dy, {
1343 r: pointer.color[0],
1344 g: pointer.color[1],
1345 b: pointer.color[2]
1346 });
1347 }
1348
1349 function step(dt: number) {
1350 gl.disable(gl.BLEND);
1351
1352 curlProgram.bind();
1353 gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1354 gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));
1355 blit(curl);
1356
1357 vorticityProgram.bind();
1358 gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1359 gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));
1360 gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1));
1361 gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
1362 gl.uniform1f(vorticityProgram.uniforms.dt, dt);
1363 blit(velocity.write);
1364 velocity.swap();
1365
1366 divergenceProgram.bind();
1367 gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1368 gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
1369 blit(divergence);
1370
1371 clearProgram.bind();
1372 gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0));
1373 gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);
1374 blit(pressure.write);
1375 pressure.swap();
1376
1377 pressureProgram.bind();
1378 gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1379 gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0));
1380 for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
1381 gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1));
1382 blit(pressure.write);
1383 pressure.swap();
1384 }
1385
1386 gradienSubtractProgram.bind();
1387 gl.uniform2f(
1388 gradienSubtractProgram.uniforms.texelSize,
1389 velocity.texelSizeX,
1390 velocity.texelSizeY
1391 );
1392 gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0));
1393 gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));
1394 blit(velocity.write);
1395 velocity.swap();
1396
1397 advectionProgram.bind();
1398 gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
1399 if (!ext.supportLinearFiltering) {
1400 gl.uniform2f(
1401 advectionProgram.uniforms.dyeTexelSize,
1402 velocity.texelSizeX,
1403 velocity.texelSizeY
1404 );
1405 }
1406 const velocityId = velocity.read.attach(0);
1407 gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId);
1408 gl.uniform1i(advectionProgram.uniforms.uSource, velocityId);
1409 gl.uniform1f(advectionProgram.uniforms.dt, dt);
1410 gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);
1411 blit(velocity.write);
1412 velocity.swap();
1413
1414 if (!ext.supportLinearFiltering) {
1415 gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY);
1416 }
1417 gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
1418 gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
1419 gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
1420 blit(dye.write);
1421 dye.swap();
1422 }
1423
1424 function blur(target: FBO, temp: FBO, iterations: number) {
1425 blurProgram.bind();
1426 for (let i = 0; i < iterations; i++) {
1427 gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0);
1428 gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0));
1429 blit(temp);
1430
1431 gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY);
1432 gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0));
1433 blit(target);
1434 }
1435 }
1436
1437 function applySunrays(source: FBO, mask: FBO, destination: FBO) {
1438 gl.disable(gl.BLEND);
1439 sunraysMaskProgram.bind();
1440 gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0));
1441 blit(mask);
1442
1443 sunraysProgram.bind();
1444 gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT);
1445 gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0));
1446 blit(destination);
1447 }
1448
1449 function drawColor(target: FBO | null, color: { r: number; g: number; b: number }) {
1450 colorProgram.bind();
1451 gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1);
1452 blit(target);
1453 }
1454
1455 function drawDisplay(target: FBO | null) {
1456 const width = target === null ? gl.drawingBufferWidth : target.width;
1457 const height = target === null ? gl.drawingBufferHeight : target.height;
1458
1459 displayMaterial.bind();
1460 if (config.SHADING) {
1461 gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height);
1462 }
1463 gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
1464 if (config.SUNRAYS) {
1465 gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3));
1466 }
1467 blit(target);
1468 }
1469
1470 function render(target: FBO | null) {
1471 if (config.SUNRAYS) {
1472 applySunrays(dye.read, dye.write, sunrays);
1473 blur(sunrays, sunraysTemp, 1);
1474 }
1475
1476 if (target === null || !config.TRANSPARENT) {
1477 gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1478 gl.enable(gl.BLEND);
1479 } else {
1480 gl.disable(gl.BLEND);
1481 }
1482
1483 if (!config.TRANSPARENT) {
1484 drawColor(target, {
1485 r: config.BACK_COLOR.r / 255,
1486 g: config.BACK_COLOR.g / 255,
1487 b: config.BACK_COLOR.b / 255
1488 });
1489 }
1490 drawDisplay(target);
1491 }
1492
1493 function wrap(value: number, min: number, max: number) {
1494 const range = max - min;
1495 if (range === 0) return min;
1496 return ((value - min) % range) + min;
1497 }
1498
1499 let lastUpdateTime = Date.now();
1500 let colorUpdateTimer = 0.0;
1501
1502 function updateColors(dt: number) {
1503 if (!config.COLORFUL) return;
1504 colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED;
1505 if (colorUpdateTimer >= 1) {
1506 colorUpdateTimer = wrap(colorUpdateTimer, 0, 1);
1507 pointers.forEach((p) => {
1508 const c = generateColor();
1509 p.color = [c.r, c.g, c.b];
1510 });
1511 }
1512 }
1513
1514 function applyInputs() {
1515 if (splatStack.length > 0) multipleSplats(splatStack.pop()!);
1516
1517 pointers.forEach((p) => {
1518 if (p.moved) {
1519 p.moved = false;
1520 splatPointer(p);
1521 }
1522 });
1523 }
1524
1525 function calcDeltaTime() {
1526 const now = Date.now();
1527 let dt = (now - lastUpdateTime) / 1000;
1528 // Allow up to ~30fps worth of time to pass per frame to handle browser throttling
1529 // This prevents the simulation from appearing to slow down when RAF is throttled
1530 dt = Math.min(dt, 0.033);
1531 lastUpdateTime = now;
1532 return dt;
1533 }
1534
1535 function update() {
1536 const dt = calcDeltaTime() * (config.RENDER_SPEED ?? 1.0);
1537 if (resizeCanvas()) initFramebuffers();
1538 if (!maskReady) {
1539 scheduleMaskDraw();
1540 gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
1541 gl.clearColor(0.0, 0.0, 0.0, 1.0);
1542 gl.clear(gl.COLOR_BUFFER_BIT);
1543 animationId = requestAnimationFrame(update);
1544 return;
1545 }
1546 updateColors(dt);
1547 applyInputs();
1548 if (!config.PAUSED) step(dt);
1549 render(null);
1550 animationId = requestAnimationFrame(update);
1551 }
1552
1553 function correctDeltaX(delta: number) {
1554 const aspectRatio = fluidCanvas.width / fluidCanvas.height;
1555 if (aspectRatio < 1) delta *= aspectRatio;
1556 return delta;
1557 }
1558
1559 function correctDeltaY(delta: number) {
1560 const aspectRatio = fluidCanvas.width / fluidCanvas.height;
1561 if (aspectRatio > 1) delta /= aspectRatio;
1562 return delta;
1563 }
1564
1565 function updatePointerDownData(pointer: Pointer, id: number, posX: number, posY: number) {
1566 pointer.id = id;
1567 pointer.down = true;
1568 pointer.moved = false;
1569 pointer.texcoordX = posX / fluidCanvas.width;
1570 pointer.texcoordY = 1.0 - posY / fluidCanvas.height;
1571 pointer.prevTexcoordX = pointer.texcoordX;
1572 pointer.prevTexcoordY = pointer.texcoordY;
1573 pointer.deltaX = 0;
1574 pointer.deltaY = 0;
1575 const c = generateColor();
1576 pointer.color = [c.r, c.g, c.b];
1577 }
1578
1579 function updatePointerMoveData(pointer: Pointer, posX: number, posY: number) {
1580 pointer.prevTexcoordX = pointer.texcoordX;
1581 pointer.prevTexcoordY = pointer.texcoordY;
1582 pointer.texcoordX = posX / fluidCanvas.width;
1583 pointer.texcoordY = 1.0 - posY / fluidCanvas.height;
1584 pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);
1585 pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);
1586 pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
1587 }
1588
1589 function updatePointerUpData(pointer: Pointer) {
1590 pointer.down = false;
1591 }
1592
1593 // Event handlers - use container so events work over both canvases
1594 container.addEventListener('mouseenter', (e) => {
1595 // Create a small burst when mouse enters the card
1596 const rect = container.getBoundingClientRect();
1597 const posX = scaleByPixelRatio(e.clientX - rect.left);
1598 const posY = scaleByPixelRatio(e.clientY - rect.top);
1599 const x = posX / fluidCanvas.width;
1600 const y = 1.0 - posY / fluidCanvas.height;
1601 const color = generateColor();
1602 color.r *= 10.0;
1603 color.g *= 10.0;
1604 color.b *= 10.0;
1605 splat(x, y, 300 * (Math.random() - 0.5), 300 * (Math.random() - 0.5), color);
1606 });
1607
1608 container.addEventListener('mousedown', (e) => {
1609 const rect = container.getBoundingClientRect();
1610 const posX = scaleByPixelRatio(e.clientX - rect.left);
1611 const posY = scaleByPixelRatio(e.clientY - rect.top);
1612 let pointer = pointers.find((p) => p.id === -1);
1613 if (!pointer) pointer = PointerPrototype();
1614 updatePointerDownData(pointer, -1, posX, posY);
1615 });
1616
1617 container.addEventListener('mousemove', (e) => {
1618 const pointer = pointers[0];
1619 const rect = container.getBoundingClientRect();
1620 const posX = scaleByPixelRatio(e.clientX - rect.left);
1621 const posY = scaleByPixelRatio(e.clientY - rect.top);
1622 updatePointerMoveData(pointer, posX, posY);
1623 // Always create swish effect on hover
1624 if (pointer.moved) {
1625 pointer.moved = false;
1626 // Generate a new color for visual interest
1627 const c = generateColor();
1628 pointer.color = [c.r, c.g, c.b];
1629 splat(
1630 pointer.texcoordX,
1631 pointer.texcoordY,
1632 pointer.deltaX * config.SPLAT_FORCE * 12,
1633 pointer.deltaY * config.SPLAT_FORCE * 12,
1634 {
1635 r: pointer.color[0],
1636 g: pointer.color[1],
1637 b: pointer.color[2]
1638 }
1639 );
1640 }
1641 });
1642
1643 container.addEventListener('mouseup', () => {
1644 updatePointerUpData(pointers[0]);
1645 });
1646
1647 container.addEventListener('touchstart', (e) => {
1648 e.preventDefault();
1649 const touches = e.targetTouches;
1650 while (touches.length >= pointers.length) pointers.push(PointerPrototype());
1651 for (let i = 0; i < touches.length; i++) {
1652 const rect = container.getBoundingClientRect();
1653 const posX = scaleByPixelRatio(touches[i].clientX - rect.left);
1654 const posY = scaleByPixelRatio(touches[i].clientY - rect.top);
1655 updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY);
1656 }
1657 });
1658
1659 container.addEventListener('touchmove', (e) => {
1660 e.preventDefault();
1661 const touches = e.targetTouches;
1662 for (let i = 0; i < touches.length; i++) {
1663 const pointer = pointers[i + 1];
1664 if (!pointer.down) continue;
1665 const rect = container.getBoundingClientRect();
1666 const posX = scaleByPixelRatio(touches[i].clientX - rect.left);
1667 const posY = scaleByPixelRatio(touches[i].clientY - rect.top);
1668 updatePointerMoveData(pointer, posX, posY);
1669 }
1670 });
1671
1672 container.addEventListener('touchend', (e) => {
1673 const touches = e.changedTouches;
1674 for (let i = 0; i < touches.length; i++) {
1675 const pointer = pointers.find((p) => p.id === touches[i].identifier);
1676 if (pointer) updatePointerUpData(pointer);
1677 }
1678 });
1679
1680 // Initialize
1681 updateKeywords();
1682 initFramebuffers();
1683 multipleSplats(25);
1684 update();
1685
1686 // Auto splat interval
1687 splatIntervalId = setInterval(() => {
1688 multipleSplats(5);
1689 }, 500);
1690
1691 // Resize observer - also triggers initial draw
1692 resizeObserver = new ResizeObserver(() => {
1693 resizeCanvas();
1694 maskReady = false;
1695 scheduleMaskDraw();
1696 });
1697 resizeObserver.observe(container);
1698
1699 // Mark as initialized after first resize callback
1700 isInitialized = true;
1701 }
1702</script>
1703
1704<div bind:this={container} class="relative h-full w-full overflow-hidden bg-black">
1705 <canvas bind:this={fluidCanvas} class="absolute h-full w-full"></canvas>
1706 <canvas bind:this={maskCanvas} class="absolute h-full w-full"></canvas>
1707</div>