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