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