1// ð webglCaustics.js - soft rainbow caustics overlay 2(() => { 3 function notifyCausticsReady() { 4 if (typeof window.__causticsTryPendingTexture === 'function') { 5 window.__causticsTryPendingTexture(); 6 } 7 try { 8 window.dispatchEvent(new CustomEvent('pupwife:webgl-caustics-ready')); 9 } catch (e) { 10 window.dispatchEvent(new Event('pupwife:webgl-caustics-ready')); 11 } 12 } 13 14 function findCausticsCanvas(overlay) { 15 if (!overlay) return null; 16 var tagged = overlay.querySelector('canvas[data-site-bg="caustics"]'); 17 if (tagged) return tagged; 18 var legacy = overlay.querySelector('canvas:not([data-site-bg="horizon"]):not([data-site-bg="clouds"])'); 19 if (legacy) { 20 legacy.dataset.siteBg = 'caustics'; 21 return legacy; 22 } 23 return null; 24 } 25 26 function applyCausticsTuning(tuning) { 27 const mat = window.__webglCausticsMaterial; 28 if (!mat || !mat.uniforms) return; 29 if (window.SiteBackgroundTuning && typeof window.SiteBackgroundTuning.mergeSettings === 'function') { 30 tuning = window.SiteBackgroundTuning.mergeSettings({ caustics: tuning }).caustics; 31 } 32 const u = mat.uniforms; 33 if (u.uBaseStrength) u.uBaseStrength.value = tuning.strength; 34 if (u.uPatternScale) u.uPatternScale.value = tuning.patternScale; 35 if (u.uAlphaBase) u.uAlphaBase.value = tuning.alpha; 36 if (u.uShimmerAmt) u.uShimmerAmt.value = tuning.shimmer; 37 if (u.uColorSpeed) u.uColorSpeed.value = tuning.colorSpeed; 38 if (u.uNoiseScale) u.uNoiseScale.value = tuning.noiseScale; 39 } 40 41 window.CausticsBackground = { 42 applyTuning: applyCausticsTuning 43 }; 44 45 if (window.__webglCausticsInitialized) { 46 if (window.__webglCausticsMaterial) { 47 notifyCausticsReady(); 48 if (window.SiteBackgroundTuning && typeof window.SiteBackgroundTuning.readTuning === 'function') { 49 applyCausticsTuning(window.SiteBackgroundTuning.readTuning().caustics); 50 } 51 } 52 return; 53 } 54 55 // ----- scene, camera, renderer ----- 56 const overlay = document.getElementById("webgl-overlay"); 57 if (!overlay) { 58 console.warn('webglCaustics: webgl-overlay element not found'); 59 return; 60 } 61 62 if (typeof THREE === 'undefined') { 63 console.warn('webglCaustics: THREE is not loaded'); 64 return; 65 } 66 67 if (findCausticsCanvas(overlay)) { 68 window.__webglCausticsInitialized = true; 69 if (window.__webglCausticsMaterial) { 70 notifyCausticsReady(); 71 if (window.SiteBackgroundTuning && typeof window.SiteBackgroundTuning.readTuning === 'function') { 72 applyCausticsTuning(window.SiteBackgroundTuning.readTuning().caustics); 73 } 74 } 75 return; 76 } 77 78 // Check if canvas creation is disabled 79 if (overlay.dataset.disableCanvas === "true") { 80 return; // Exit early without creating canvas 81 } 82 83 window.__webglCausticsInitialized = true; 84 85 function readViewportCss() { 86 if (window.PupwifeViewportSize && typeof window.PupwifeViewportSize.css === 'function') { 87 return window.PupwifeViewportSize.css(); 88 } 89 return { width: window.innerWidth, height: window.innerHeight }; 90 } 91 92 function readViewportDpr() { 93 if (window.PupwifeViewportSize && typeof window.PupwifeViewportSize.pixelRatio === 'function') { 94 return window.PupwifeViewportSize.pixelRatio(2); 95 } 96 return window.devicePixelRatio; 97 } 98 99 const viewport = readViewportCss(); 100 const scene = new THREE.Scene(); 101 const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); 102 const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); 103 renderer.setSize(viewport.width, viewport.height); 104 renderer.setPixelRatio(readViewportDpr()); 105 renderer.setClearColor(0x000000, 0); // Transparent background 106 renderer.domElement.dataset.siteBg = 'caustics'; 107 overlay.appendChild(renderer.domElement); 108 109 // Flag to disable arc on certain pages 110 const disableArc = overlay.dataset.disableArc === "true"; 111 112 // ----- shader material ----- 113 const causticsMaterial = new THREE.ShaderMaterial({ 114 transparent: true, 115 blending: THREE.AdditiveBlending, 116 uniforms: { 117 uTime: { value: 0 }, 118 uFadeIn: { value: 0.0 }, // Fade in progress (0-1) 119 uResolution: { value: new THREE.Vector2(viewport.width, viewport.height) }, 120 uRipplePositions: { value: [ new THREE.Vector2(-10000, -10000), new THREE.Vector2(-10000, -10000), new THREE.Vector2(-10000, -10000) ] }, 121 uRippleTimes: { value: [ -10000, -10000, -10000 ] }, 122 // optional noise texture to reduce tiling / add randomness 123 uNoiseTex: { value: null }, 124 uUseNoiseTex: { value: 0.0 }, 125 uNoiseScale: { value: 0.69 }, 126 uPatternScale: { value: 2.8 }, 127 uBaseStrength: { value: 0.6 }, 128 uAlphaBase: { value: 0.4 }, 129 uShimmerAmt: { value: 0.1 }, 130 uColorSpeed: { value: 0.3 } 131 }, 132 vertexShader: ` 133 varying vec2 vUv; 134 void main() { 135 vUv = uv;
136 gl_Position = vec4(position, 1.0); 137 } 138 `, 139 fragmentShader: ` 140 uniform float uTime; 141 uniform vec2 uResolution; 142 uniform vec2 uRipplePositions[3]; 143 uniform float uRippleTimes[3]; 144 uniform sampler2D uNoiseTex; 145 uniform float uUseNoiseTex; 146 uniform float uNoiseScale; 147 uniform float uPatternScale; 148 uniform float uBaseStrength; 149 uniform float uAlphaBase; 150 uniform float uShimmerAmt; 151 uniform float uColorSpeed; 152 uniform float uFadeIn; // Fade in progress 153 varying vec2 vUv; 154 155 // Ripple params 156 const float RIPPLE_SPEED = 2.2; 157 const float RIPPLE_WAVELENGTH = 0.12; 158 const float RIPPLE_AMPLITUDE = 0.018; 159 const float RIPPLE_ATTENUATION = 3.5; 160 const float RIPPLE_LIFETIME = 1.6; 161 162 float fastExp(float x) { 163 return 1.0 / (1.0 + x + 0.48*x*x + 0.235*x*x*x); 164 } 165 166 vec2 rippleFor(vec2 uv, vec2 center, float startTime) { 167 float t = uTime - startTime; 168 if (t <= 0.0 || t > RIPPLE_LIFETIME) return vec2(0.0); 169 vec2 dir = uv - center; 170 float dist = length(dir); 171 if (dist < 0.0001) return vec2(0.0); 172 if (dist > 0.6) return vec2(0.0); 173 float wave = dist - (t * RIPPLE_SPEED); 174 float r = sin(wave / RIPPLE_WAVELENGTH) * RIPPLE_AMPLITUDE; 175 r *= fastExp(dist * RIPPLE_ATTENUATION); 176 r *= 1.0 - (t / RIPPLE_LIFETIME); 177 return normalize(dir) * r; 178 } 179 180 void main() { 181 vec2 st = vUv * uResolution / min(uResolution.x, uResolution.y); 182 183 // Apply ripples to uv (normalized) 184 vec2 totalOffset = vec2(0.0); 185 for (int i = 0; i < 3; i++) { 186 vec2 center = uRipplePositions[i] / uResolution; 187 totalOffset += rippleFor(vUv, center, uRippleTimes[i]); 188 } 189 190 // subtle base wiggly distortion (caustics) 191 // smaller-scale pattern: increase frequency and add animated offsets 192 vec2 displaced = st + totalOffset * 0.5; 193 // time-varying offset to break strict tiling 194 displaced += 0.008 * vec2( 195 sin(uTime*0.28 + displaced.y*5.0), 196 cos(uTime*0.28 + displaced.x*5.0) 197 ); 198 199 // More connected caustics pattern with flowing waves 200 vec2 p = displaced * uPatternScale; 201 p += vec2(sin(uTime*0.08), cos(uTime*0.06)) * 0.08; 202 203 // Create flowing wave patterns that connect better 204 float pattern = 0.0; 205 float amp = 0.8; 206 vec2 q = p; 207 208 // First layer: large flowing waves 209 float wave1 = sin(q.x * 3.14) * sin(q.y * 3.14); 210 pattern += wave1 * amp; 211 212 // Second layer: medium waves with slight rotation 213 float angle1 = uTime * 0.015; 214 mat2 rot1 = mat2(cos(angle1), -sin(angle1), sin(angle1), cos(angle1)); 215 vec2 q2 = rot1 * q * 1.4; 216 float wave2 = sin(q2.x * 4.2) * sin(q2.y * 4.2); 217 pattern += wave2 * amp * 0.6; 218 219 // Third layer: fine details 220 float angle2 = uTime * 0.025; 221 mat2 rot2 = mat2(cos(angle2), -sin(angle2), sin(angle2), cos(angle2)); 222 vec2 q3 = rot2 * q * 2.1; 223 float wave3 = sin(q3.x * 6.28) * sin(q3.y * 6.28); 224 pattern += wave3 * amp * 0.3; 225 226 // optional user-supplied noise texture: sample and blend in to reduce repetition 227 if (uUseNoiseTex > 0.5) { 228 // sample texture using a wrapping coordinate so large-scale tiling is softened 229 // uNoiseScale controls tiling: larger values -> smaller visible features 230 vec2 tcoord = fract(displaced * uNoiseScale + vec2(uTime*0.01)); 231 float tx = texture2D(uNoiseTex, tcoord).r; 232 // Increase contrast of the noise texture and reduce dark areas 233 tx = smoothstep(0.2, 0.8, tx); // Makes dark areas more transparent 234 pattern = mix(pattern, tx, 0.7); // Reduced noise influence 235 } else { 236 // small random perturb to break exact repeats 237 float rnd = fract(sin(dot(displaced.xy, vec2(12.9898,78.233))) * 43758.5453) * 0.06; 238 pattern += rnd; 239 } 240 241 float wave = clamp(pattern, 0.0, 1.0); 242 243 // Rainbow caustics with shimmery white base and color tints 244 // Base shimmering white 245 vec3 baseWhite = vec3(0.95, 0.98, 1.0); 246 247 // Color tints that flash in and out 248 float timePhase = uTime * uColorSpeed; 249 vec3 redTint = vec3(1.0, 0.7, 0.7) * (0.3 + 0.2 * sin(timePhase + wave * 6.28)); 250 vec3 greenTint = vec3(0.7, 1.0, 0.7) * (0.3 + 0.2 * sin(timePhase + wave * 6.28 + 2.09)); 251 vec3 blueTint = vec3(0.7, 0.7, 1.0) * (0.3 + 0.2 * sin(timePhase + wave * 6.28 + 4.18)); 252 253 // Blend the tints with the base white 254 vec3 color = baseWhite + redTint + greenTint + blueTint; 255 256 // Add extra shimmer effect 257 float shimmer = uShimmerAmt * sin(uTime * 2.0 + wave * 12.56); 258 color += vec3(shimmer); 259 260 // Ensure colors stay bright and vibrant 261 color = clamp(color, 0.0, 1.0); 262
263 // Rainbow caustics strength and alpha (no fish buddy glow here) 264 float baseStrength = uBaseStrength; 265 266 // Calculate alpha based on pattern intensity with shimmer 267 float patternBrightness = (pattern - 0.0) / 0.6; // Adjusted for better visibility 268 patternBrightness = max(0.0, patternBrightness); 269 270 // Add shimmer to alpha as well 271 float alphaShimmer = 0.1 * sin(uTime * 1.5 + wave * 8.0); 272 float alpha = (uAlphaBase + alphaShimmer) * patternBrightness; 273 274 // Apply fade in to color and alpha (caustics only, no fish buddy effects) 275 gl_FragColor = vec4(color * baseStrength, alpha * uFadeIn); 276 } 277 `, 278 transparent: true 279 }); 280 281 // ----- plane to cover screen ----- 282 const plane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), causticsMaterial); 283 // Start with the plane completely transparent 284 plane.visible = false; 285 scene.add(plane); 286 287 // expose the material so a small helper file can manage loading textures 288 // (keeps this file focused on the shader and runtime loop) 289 window.__webglCausticsMaterial = causticsMaterial; 290 // flag updated by the external loader when a GIF is being animated 291 window.__webglCausticsGifAnimating = false; 292 notifyCausticsReady(); 293 // expose function to show caustics when texture is ready 294 let fadeStartTime = 0; 295 let hasShown = false; // Prevent re-triggering fade 296 const FADE_DURATION = 5; // seconds 297 window.__webglShowCaustics = () => { 298 if (hasShown) return; // Only fade in once 299 hasShown = true; 300 plane.visible = true; 301 fadeStartTime = causticsMaterial.uniforms.uTime.value; 302 }; 303 304 // ----- ripple state (JS-side) ----- 305 const MAX_RIPPLES = 3; 306 const ripples = new Array(MAX_RIPPLES).fill(0).map(() => ({ pos: new THREE.Vector2(-10000, -10000), time: -10000 })); 307 let nextRipple = 0; 308 309 // Click to spawn ripples 310 document.addEventListener('click', (e) => { 311 if(!overlay) return; 312 const rect = overlay.getBoundingClientRect(); 313 const x = e.clientX - rect.left; 314 const y = e.clientY - rect.top; 315 ripples[nextRipple].pos.set(x, rect.height - y); 316 // Use the shader's uTime baseline so ripple timing matches the animation clock 317 ripples[nextRipple].time = causticsMaterial.uniforms.uTime.value; 318 319 // Update uniforms by writing new arrays (ensures Three picks up the changes) 320 causticsMaterial.uniforms.uRipplePositions.value = ripples.map(r => r.pos.clone()); 321 causticsMaterial.uniforms.uRippleTimes.value = ripples.map(r => r.time); 322 323 nextRipple = (nextRipple + 1) % MAX_RIPPLES; 324 }); 325 326 // ----- handle resizing ----- 327 window.addEventListener("resize", () => { 328 const vp = readViewportCss(); 329 renderer.setSize(vp.width, vp.height); 330 renderer.setPixelRatio(readViewportDpr()); 331 causticsMaterial.uniforms.uResolution.value.set(vp.width, vp.height); 332 }); 333 334 // ----- animation loop ----- 335 const clock = new THREE.Clock(); 336 function animate() { 337 const currentTime = clock.getElapsedTime(); 338 causticsMaterial.uniforms.uTime.value = currentTime; 339 340 // Handle fade in animation 341 if (plane.visible && fadeStartTime > 0) { 342 const fadeProgress = Math.min(1.0, (currentTime - fadeStartTime) / FADE_DURATION); 343 causticsMaterial.uniforms.uFadeIn.value = fadeProgress; 344 if (fadeProgress >= 1.0) { 345 fadeStartTime = 0; // Animation complete 346 } 347 } 348 349 // if an animated GIF is used as the noise texture, force texture update each frame 350 if (window.__webglCausticsGifAnimating) { 351 const mat = window.__webglCausticsMaterial || causticsMaterial; 352 const tex = mat && mat.uniforms && mat.uniforms.uNoiseTex && mat.uniforms.uNoiseTex.value; 353 if (tex) tex.needsUpdate = true; 354 } 355 // Fish buddy tracking removed - caustics only now 356 357 renderer.render(scene, camera); 358 requestAnimationFrame(animate); 359 } 360 361 animate(); 362 363 if (window.SiteBackgroundTuning && typeof window.SiteBackgroundTuning.readTuning === 'function') {
364 applyCausticsTuning(window.SiteBackgroundTuning.readTuning().caustics); 365 } 366})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.