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