PageSourceSearch

https://pupwife.com/scripts/dependencies/webglCaustics.js

js pupwife.com collected 2026-09-30 15:05:17 UTC 14,270 bytes, 366 lines download raw bytes

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.