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