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