PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:06:06 UTC 39,415 bytes, 1,011 lines download raw bytes

1/**
2 * Horizon background — sky cubemap (Rioki-style ray lookup) + procedural ocean below.
3 * Inspired by Rioki's GLSL skybox (https://www.rioki.org/2013/03/07/glsl-skybox.html)
4 * and tuxalin/water-shader (https://github.com/tuxalin/water-shader) — MIT.
5 *
6 * Sky: procedural volumetric clouds (animated). Optional static cubemap if
7 * HORIZON_USE_CUBEMAP_SKY is true (see cubemap.png strip paths below).
8 * Water normal layers: water.jpg + water2.jpg (tiled, parallax-scrolled)
9 */
10(function (global) {
11    'use strict';
12
13    /** Tried in order — use whichever filename you upload. */
14    var CUBEMAP_STRIP_PATHS = [
15        '/images/backgrounds/horizon/cubemap.png',
16        '/images/backgrounds/horizon/cubemap.webp',
17        '/images/backgrounds/horizon/cubemap.jpg',
18        '/images/backgrounds/horizon/cubemap-strip.webp',
19        '/images/backgrounds/horizon/cubemap-strip.jpg',
20        '/images/backgrounds/horizon/cubemap-strip.png'
21    ];
22
23    /** Strip slot index → Three.js face index (px, nx, py, ny, pz, nz). Reorder if seams look wrong. */
24    var STRIP_SLOT_TO_CUBE_FACE = [0, 1, 2, 3, 4, 5];
25
26    /** Cap GPU face size when slicing huge strips (e.g. 16MB sources). 0 = no cap. */
27    var STRIP_MAX_FACE_PX = 2048;
28
29    /** Half-angle FOV (tangent). ~0.53 = original; ~0.62 = slightly zoomed out. */
30    var HORIZON_TAN_HALF_FOV = 0.62;
31    /** Slightly narrow vertical spread (matches old 0.72 vs 1.0 xy feel). */
32    var HORIZON_VERTICAL_SCALE = 0.88;
33
34    var WATER_NORMAL_PATHS = {
35        layer1: '/images/backgrounds/horizon/water.jpg',
36        layer2: '/images/backgrounds/horizon/water2.jpg'
37    };
38    /** Set false to sample water2.jpg as the fast parallax layer (must tile seamlessly). */
39    var WATER_LAYER2_USES_SEAMLESS_FALLBACK = false;
40
41    /** false = animated procedural volumetric clouds; true = static cubemap strip. */
42    var HORIZON_USE_CUBEMAP_SKY = false;
43
44    /** Cubemap / sky brightness multiplier (reflections use the same sampleSky path). */
45    var HORIZON_SKY_EXPOSURE = 0.92;
46    var HORIZON_WATER_SKY_REFLECT = 0.97;
47    /** How much normals bend reflections (1 = full; lower = calmer). Textures stay sharp. */
48    var HORIZON_NORMAL_INFLUENCE = 0.64;
49    /** Procedural swell tilt layered on top of texture normals. */
50    var HORIZON_WAVE_SWELL = 2.65;
51    /** Rolling wave height on the water plane. */
52    var HORIZON_WAVE_DISPLACE = 1.05;
53    /** Gerstner horizontal pull (0 = sine height only, ~0.6 = rolling swell). */
54    var HORIZON_WAVE_STEEPNESS = 0.58;
55
56    var CUBEMAP_FACE_PATHS = [
57        '/images/backgrounds/horizon/px.webp',
58        '/images/backgrounds/horizon/nx.webp',
59        '/images/backgrounds/horizon/py.webp',
60        '/images/backgrounds/horizon/ny.webp',
61        '/images/backgrounds/horizon/pz.webp',
62        '/images/backgrounds/horizon/nz.webp'
63    ];
64
65    var CUBEMAP_FACE_FALLBACK_EXTS = ['.jpg', '.png'];
66
67    function applyCubemapColorSpace(tex) {
68        if (tex && THREE.sRGBEncoding !== undefined) {
69            tex.encoding = THREE.sRGBEncoding;
70        }
71        return tex;
72    }
73
74    function loadImage(url) {
75        return new Promise(function (resolve) {
76            var img = new Image();
77            img.crossOrigin = 'anonymous';
78            img.onload = function () { resolve(img); };
79            img.onerror = function () { resolve(null); };
80            img.src = url;
81        });
82    }
83
84    function loadFirstImage(paths) {
85        var chain = Promise.resolve(null);
86        for (var i = 0; i < paths.length; i++) {
87            (function (url) {
88                chain = chain.then(function (prev) {
89                    if (prev) return prev;
90                    return loadImage(url);
91                });
92            })(paths[i]);
93        }
94        return chain;
95    }
96
97    function cubeTextureFromStripImage(img) {
98        var slots = 6;
99        var faceW = Math.floor(img.width / slots);
100        var faceH = img.height;
101        if (faceW < 1 || faceW * slots !== img.width) {
102            console.warn('[horizon] Cubemap strip width should divide evenly into 6 faces');
103            return null;
104        }
105        if (Math.abs(faceW - faceH) > 2) {
106            console.warn('[horizon] Cubemap strip faces should be square (height = one face width)');
107        }
108
109        var outSize = faceW;
110        if (STRIP_MAX_FACE_PX > 0 && outSize > STRIP_MAX_FACE_PX) {
111            outSize = STRIP_MAX_FACE_PX;
112        }
113
114        var faceCanvases = [];
115        for (var s = 0; s < slots; s++) {
116            var canvas = document.createElement('canvas');
117            canvas.width = outSize;
118            canvas.height = outSize;
119            var ctx = canvas.getContext('2d');
120            if (ctx) {
121                ctx.drawImage(img, s * faceW, 0, faceW, faceH, 0, 0, outSize, outSize);
122            }
123            faceCanvases.push(canvas);
124        }
125
126        var ordered = [];
127        for (var f = 0; f < 6; f++) {
128            var slot = STRIP_SLOT_TO_CUBE_FACE[f];
129            ordered.push(faceCanvases[slot >= 0 && slot < 6 ? slot : f]);
130        }
131
132        var tex = new THREE.CubeTexture(ordered);
133        tex.needsUpdate = true;
134        return applyCubemapColorSpace(tex);
135    }
136
137    function loadCubemapFromStrip() {
138        return loadFirstImage(CUBEMAP_STRIP_PATHS).then(function (img) {
139            if (!img || typeof THREE === 'undefined') return null;
140            return cubeTextureFromStripImage(img);
141        });
142    }
143
144    function loadCubemapFromFaces() {
145        return new Promise(function (resolve) {
146            if (typeof THREE === 'undefined' || !THREE.CubeTextureLoader) {
147                resolve(null);
148                return;
149            }
150
151            function tryPaths(paths) {
152                return new Promise(function (res) {
153                    var loader = new THREE.CubeTextureLoader();
154                    loader.load(
155                        paths,
156                        function (tex) { res(applyCubemapColorSpace(tex)); },
157                        undefined,
158                        function () { res(null); }
159                    );
160                });
161            }
162
163            tryPaths(CUBEMAP_FACE_PATHS).then(function (tex) {
164                if (tex) {
165                    resolve(tex);
166                    return;
167                }
168                var i = 0;
169                function nextExt() {
170                    if (i >= CUBEMAP_FACE_FALLBACK_EXTS.length) {
171                        resolve(null);
172                        return;
173                    }
174                    var ext = CUBEMAP_FACE_FALLBACK_EXTS[i];
175                    i += 1;
176                    var alt = CUBEMAP_FACE_PATHS.map(function (p) {
177                        return p.replace(/\.webp$/i, ext);
178                    });
179                    tryPaths(alt).then(function (altTex) {
180                        if (altTex) resolve(altTex);
181                        else nextExt();
182                    });
183                }
184                nextExt();
185            });
186        });
187    }
188
189    function applyCubemapToMaterial(tex) {
190        if (!state.material || !tex) return;
191        var prev = state.material.uniforms.uSkybox.value;
192        if (prev && prev.dispose) prev.dispose();
193        state.material.uniforms.uSkybox.value = tex;
194        state.material.uniforms.uUseCubemap.value = 1;
195    }
196
197    function ensureCubemapLoaded() {
198        if (state.material && state.material.uniforms.uUseCubemap.value > 0.5) {
199            return Promise.resolve(state.material.uniforms.uSkybox.value);
200        }
201        return loadCubemap().then(function (tex) {
202            if (tex) applyCubemapToMaterial(tex);
203            return tex;
204        });
205    }
206
207    function configureWaterNormalTexture(tex) {
208        if (!tex) return;
209        tex.wrapS = THREE.RepeatWrapping;
210        tex.wrapT = THREE.RepeatWrapping;
211        tex.minFilter = THREE.LinearFilter;
212        tex.magFilter = THREE.LinearFilter;
213        tex.generateMipmaps = false;
214        if (THREE.LinearEncoding !== undefined) {
215            tex.encoding = THREE.LinearEncoding;
216        }
217    }
218
219    function loadWaterNormalTextures() {
220        if (typeof THREE === 'undefined') {
221            return Promise.resolve(false);
222        }
223        if (assetCache.water1) {
224            return Promise.resolve(applyWaterTexturesToMaterial([
225                assetCache.water1,
226                assetCache.water2
227            ]));
228        }
229        return preloadWaterNormalImages().then(function (images) {
230            return applyWaterTexturesToMaterial(images);
231        });
232    }
233
234    function ensureWaterNormalsLoaded() {
235        if (state.material && state.material.uniforms.uUseWaterNormals.value > 0.5) {
236            return Promise.resolve(true);
237        }
238        return loadWaterNormalTextures();
239    }
240
241    function ensureHorizonAssetsLoaded() {
242        var tasks = [ensureWaterNormalsLoaded()];
243        if (HORIZON_USE_CUBEMAP_SKY) {
244            tasks.push(ensureCubemapLoaded());
245        } else if (state.material) {
246            state.material.uniforms.uUseCubemap.value = 0;
247        }
248        return Promise.all(tasks);
249    }
250
251    function loadCubemap() {
252        return loadCubemapFromStrip().then(function (stripTex) {
253            if (stripTex) return stripTex;
254            return loadCubemapFromFaces();
255        });
256    }
257
258    var VERT = [
259        'uniform float uAspect;',
260        'uniform float uTanFov;',
261        'uniform float uVerticalScale;',
262        'varying vec3 vRayDir;',
263        'void main() {',
264        '  vec2 uv = position.xy;',
265        '  vRayDir = normalize(vec3(',
266        '    uv.x * uAspect * uTanFov,',
267        '    uv.y * uTanFov * uVerticalScale,',
268        '    1.0',
269        '  ));',
270        '  gl_Position = vec4(position.xy, 0.0, 1.0);',
271        '}'
272    ].join('\n');
273
274    var FRAG = [
275        'precision highp float;',
276        'uniform float uTime;',
277        'uniform float uFadeIn;',
278        'uniform float uHorizon;',
279        'uniform float uUseCubemap;',
280        'uniform samplerCube uSkybox;',
281        'uniform sampler2D uWaterNormal1;',
282        'uniform sampler2D uWaterNormal2;',
283        'uniform float uUseWaterNormals;',
284        'uniform float uUseWaterNormal2;',
285        'uniform float uSkyExposure;',
286        'uniform float uWaterSkyReflect;',
287        'uniform float uNormalInfluence;',
288        'uniform float uWaveSwell;',
289        'uniform float uWaveDisplace;',
290        'uniform float uWaveSteepness;',
291        'uniform float uShaderQuality;',
292        'uniform vec2 uResolution;',
293        'varying vec3 vRayDir;',
294
295        'float hash(vec2 p) {',
296        '  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);',
297        '}',
298
299        'float noise(vec2 p) {',
300        '  vec2 i = floor(p);',
301        '  vec2 f = fract(p);',
302        '  f = f * f * (3.0 - 2.0 * f);',
303        '  float a = hash(i);',
304        '  float b = hash(i + vec2(1.0, 0.0));',
305        '  float c = hash(i + vec2(0.0, 1.0));',
306        '  float d = hash(i + vec2(1.0, 1.0));',
307        '  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);',
308        '}',
309
310        'float fbm(vec2 p) {',
311        '  float v = 0.0;',
312        '  float a = 0.5;',
313        '  mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);',
314        '  for (int i = 0; i < 5; i++) {',
315        '    v += a * noise(p);',
316        '    p = rot * p * 2.02 + 17.0;',
317        '    a *= 0.5;',
318        '  }',
319        '  return v;',
320        '}',
321
322        'float hash31(vec3 p) {',
323        '  p = fract(p * 0.1031);',
324        '  p += dot(p, p.yzx + 33.33);',
325        '  return fract((p.x + p.y) * p.z);',
326        '}',
327
328        'float noise3(vec3 p) {',
329        '  vec3 i = floor(p);',
330        '  vec3 f = fract(p);',
331        '  f = f * f * (3.0 - 2.0 * f);',
332        '  float n000 = hash31(i);',
333        '  float n100 = hash31(i + vec3(1.0, 0.0, 0.0));',
334        '  float n010 = hash31(i + vec3(0.0, 1.0, 0.0));',
335        '  float n110 = hash31(i + vec3(1.0, 1.0, 0.0));',
336        '  float n001 = hash31(i + vec3(0.0, 0.0, 1.0));',
337        '  float n101 = hash31(i + vec3(1.0, 0.0, 1.0));',
338        '  float n011 = hash31(i + vec3(0.0, 1.0, 1.0));',
339        '  float n111 = hash31(i + vec3(1.0, 1.0, 1.0));',
340        '  float nx00 = mix(n000, n100, f.x);',
341        '  float nx10 = mix(n010, n110, f.x);',
342        '  float nx01 = mix(n001, n101, f.x);',
343        '  float nx11 = mix(n011, n111, f.x);',
344        '  float nxy0 = mix(nx00, nx10, f.y);',
345        '  float nxy1 = mix(nx01, nx11, f.y);',
346        '  return mix(nxy0, nxy1, f.z);',
347        '}',
348
349        'float fbm3(vec3 p) {',
350        '  float v = 0.0;',
351        '  float a = 0.5;',
352        '  for (int i = 0; i < 4; i++) {',
353        '    v += a * noise3(p);',
354        '    p = p * 2.04 + vec3(17.0, 23.0, 11.0);',
355        '    a *= 0.5;',
356        '  }',
357        '  return v;',
358        '}',
359
360        'vec3 horizonSunDir() {',
361        '  return normalize(vec3(0.28, 0.78, 0.18));',
362        '}',
363
364        'float cloudDensity(vec3 p) {',
365        '  float wind = uTime * 0.022;',
366        '  p.xz += vec2(wind, wind * 0.55);',
367        '  p.x += sin(uTime * 0.06 + p.z * 0.35) * 0.06;',
368        '  float shape = fbm3(p * 0.48);',
369        '  float detail = fbm3(p * 1.55 + vec3(0.0, uTime * 0.035, 0.0)) * 0.38;',
370        '  float d = shape + detail;',
371        '  float heightMask = smoothstep(0.05, 1.4, p.y);',
372        '  d *= heightMask;',
373        '  return smoothstep(0.28, 0.58, d);',
374        '}',
375
376        'float skyCloudBillboards(vec3 rd) {',
377        '  if (rd.y < 0.04) return 0.0;',
378        '  vec2 uv = rd.xz / max(rd.y + 0.14, 0.1);',
379        '  uv += vec2(uTime * 0.014, uTime * 0.007);',
380        '  float base = fbm(uv * 0.72 + vec2(uTime * 0.003, 0.0));',
381        '  float detail = fbm(uv * 1.85 + vec2(-uTime * 0.006, uTime * 0.004));',
382        '  float billow = smoothstep(0.34, 0.62, base * 0.72 + detail * 0.38);',
383        '  billow *= smoothstep(0.05, 0.72, rd.y);',
384        '  return billow;',
385        '}',
386
387        'vec3 skyGradient(vec3 rd) {',
388        '  vec3 sunDir = horizonSunDir();',
389        '  float t = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);',
390        '  vec3 zenith = vec3(0.16, 0.38, 0.72);',
391        '  vec3 horizonCol = vec3(0.82, 0.68, 0.58);',
392        '  vec3 base = mix(horizonCol, zenith, pow(t, 0.72));',
393        '  float sunDisk = pow(max(dot(rd, sunDir), 0.0), 160.0);',
394        '  float sunHalo = pow(max(dot(rd, sunDir), 0.0), 10.0);',
395        '  base += vec3(1.0, 0.92, 0.8) * (sunDisk * 0.75 + sunHalo * 0.06);',
396        '  return base;',
397        '}',
398
399        'vec3 volumetricSky(vec3 rd) {',
400        '  vec3 sunDir = horizonSunDir();',
401        '  vec3 col = skyGradient(rd);',
402        '  vec3 cloudLit = vec3(0.94, 0.96, 1.0);',
403        '  vec3 cloudShade = vec3(0.52, 0.6, 0.74);',
404        '  float billows = skyCloudBillboards(rd) * clamp(uShaderQuality * 1.35, 0.0, 1.0);',
405        '  col = mix(col, cloudLit, billows * 0.88);',
406        '  if (rd.y <= 0.02 || uShaderQuality < 0.22) return col;',
407        '  float transmittance = 1.0;',
408        '  float marchT = 0.55;',
409        '  int marchSteps = int(mix(2.0, 5.0, clamp(uShaderQuality, 0.0, 1.0) + 0.001));',
410        '  for (int i = 0; i < 5; i++) {',
411        '    if (i >= marchSteps) break;',
412        '    vec3 p = rd * marchT + vec3(0.0, 0.18, 0.0);',
413        '    float dens = cloudDensity(p);',
414        '    float lightAmt = 0.42 + 0.58 * max(dot(normalize(p + vec3(0.001)), sunDir), 0.0);',
415        '    float shadow = cloudDensity(p + sunDir * 0.38);',
416        '    lightAmt *= 1.0 - shadow * 0.5;',
417        '    vec3 scatter = mix(cloudShade, cloudLit, lightAmt);',
418        '    col = mix(col, scatter, dens * transmittance * 0.38);',
419        '    transmittance *= 1.0 - dens * 0.16;',
420        '    marchT += 0.32;',
421        '  }',
422        '  return col;',
423        '}',
424
425        'vec3 proceduralSky(vec3 rd) {',
426        '  return volumetricSky(normalize(rd));',
427        '}',
428
429        'vec3 sampleSky(vec3 rd) {',
430        '  vec3 c = proceduralSky(normalize(rd));',
431        '  if (uUseCubemap > 0.5) {',
432        '    c = textureCube(uSkybox, vec3(rd.x, rd.y, -rd.z)).rgb;',
433        '    c = pow(c, vec3(1.0 / 2.2));',
434        '  }',
435        '  return c * uSkyExposure;',
436        '}',
437
438        'void gerstnerWave(vec2 xz, float t, vec2 dir, float amp, float waveLen, float speed, float steep,',
439        '  inout vec3 disp, inout vec3 tanX, inout vec3 tanZ) {',
440        '  float k = 6.2831853 / max(waveLen, 0.35);',
441        '  float w = k * (dot(dir, xz) - speed * t);',
442        '  float s = sin(w);',
443        '  float c = cos(w);',
444        '  float qa = steep * amp;',
445        '  disp.x += dir.x * qa * c;',
446        '  disp.y += amp * s;',
447        '  disp.z += dir.y * qa * c;',
448        '  float wa = k * amp;',
449        '  float qwa = steep * wa;',
450        '  tanX += vec3(-qwa * dir.x * dir.x * c, dir.x * wa * s, -qwa * dir.x * dir.y * c);',
451        '  tanZ += vec3(-qwa * dir.x * dir.y * c, dir.y * wa * s, -qwa * dir.y * dir.y * c);',
452        '}',
453
454        'void gerstnerOcean(vec2 xz, float t, out vec3 disp, out vec3 n, out float slope) {',
455        '  disp = vec3(0.0);',
456        '  vec3 tanX = vec3(1.0, 0.0, 0.0);',
457        '  vec3 tanZ = vec3(0.0, 0.0, 1.0);',
458        '  float steep = clamp(uWaveSteepness, 0.0, 1.15);',
459        '  gerstnerWave(xz, t, normalize(vec2(0.84, 0.54)), 0.46, 10.5, 0.62, steep, disp, tanX, tanZ);',
460        '  gerstnerWave(xz, t, normalize(vec2(-0.48, 0.88)), 0.30, 6.8, 0.76, steep * 0.92, disp, tanX, tanZ);',
461        '  gerstnerWave(xz, t, normalize(vec2(0.32, -0.95)), 0.18, 4.1, 1.02, steep * 0.78, disp, tanX, tanZ);',
462        '  gerstnerWave(xz, t, normalize(vec2(-0.68, -0.73)), 0.09, 2.0, 1.28, steep * 0.62, disp, tanX, tanZ);',
463        '  gerstnerWave(xz, t, normalize(vec2(0.12, 0.99)), 0.045, 1.1, 1.55, steep * 0.45, disp, tanX, tanZ);',
464        '  n = normalize(cross(tanZ, tanX));',
465        '  slope = 1.0 - clamp(n.y, 0.0, 1.0);',
466        '}',
467
468        'vec3 proceduralWaveNormal(vec2 xz) {',
469        '  vec3 disp;',
470        '  vec3 n;',
471        '  float slope;',
472        '  gerstnerOcean(xz, uTime, disp, n, slope);',
473        '  return normalize(vec3(n.xy * uWaveSwell, max(n.z, 0.12)));',
474        '}',
475
476        'vec3 unpackNormalMap(vec3 rgb) {',
477        '  return normalize(rgb * 2.0 - 1.0);',
478        '}',
479
480        'vec3 applyNormalInfluence(vec3 n, float influence) {',
481        '  return normalize(mix(vec3(0.0, 1.0, 0.0), n, influence));',
482        '}',
483
484        'vec3 waterNormalFromTextures(vec2 xz, vec3 rd) {',
485        '  vec2 parallax = rd.xz / max(abs(rd.y), 0.06);',
486        '  vec2 base = xz * 0.11;',
487        '  vec2 uv1 = base * 1.15 + parallax * 0.04 + vec2(uTime * 0.011, uTime * 0.006);',
488        '  vec2 uv2 = base * 2.35 + parallax * 0.095 + vec2(-uTime * 0.019, uTime * 0.014);',
489        '  vec3 n1 = unpackNormalMap(texture2D(uWaterNormal1, uv1).rgb);',
490        '  vec3 n2From1 = unpackNormalMap(texture2D(uWaterNormal1, uv2).rgb);',
491        '  vec3 n2From2 = unpackNormalMap(texture2D(uWaterNormal2, uv2).rgb);',
492        '  vec3 n2 = mix(n2From1, n2From2, step(0.5, uUseWaterNormal2));',
493        '  return normalize(vec3(n1.xy + n2.xy, n1.z * n2.z));',
494        '}',
495
496        'vec3 waveNormal(vec2 xz, vec3 rd) {',
497        '  vec3 proc = proceduralWaveNormal(xz);',
498        '  vec3 n = proc;',
499        '  if (uUseWaterNormals > 0.5) {',
500        '    vec3 texN = waterNormalFromTextures(xz, rd);',
501        '    n = normalize(vec3(texN.xy + proc.xy * uWaveSwell, texN.z * proc.z));',
502        '  }',
503        '  return applyNormalInfluence(n, uNormalInfluence);',
504        '}',
505
506        'vec3 shadeWater(vec3 rd, vec3 eye) {',
507        '  vec3 col = sampleSky(rd);',
508        '  if (rd.y < -0.001) {',
509        '    float t = -eye.y / rd.y;',
510        '    if (t > 0.0) {',
511        '      vec3 hit = eye + rd * t;',
512        '      vec2 xz = hit.xz * 0.35;',
513        '      float grazing = smoothstep(-0.3, -0.012, rd.y);',
514        '      float distFade = smoothstep(2.5, 20.0, length(xz));',
515        '      float horizonFade = clamp(max(grazing, distFade * 0.88), 0.0, 1.0);',
516        '      vec3 gerstnerDisp;',
517        '      vec3 procN;',
518        '      float waveSlope;',
519        '      gerstnerOcean(xz, uTime, gerstnerDisp, procN, waveSlope);',
520        '      float dispFade = 1.0 - grazing * 0.72;',
521        '      vec2 xzWaves = xz + gerstnerDisp.xz * uWaveSteepness * dispFade;',
522        '      hit.y += gerstnerDisp.y * uWaveDisplace * dispFade;',
523        '      gerstnerOcean(xzWaves, uTime, gerstnerDisp, procN, waveSlope);',
524        '      procN = normalize(vec3(procN.xy * uWaveSwell, max(procN.z, 0.1)));',
525        '      vec3 n = waveNormal(xzWaves, rd);',
526        '      n = normalize(mix(n, procN, 0.55));',
527        '      n = normalize(mix(n, vec3(0.0, 1.0, 0.0), horizonFade * 0.76));',
528        '      vec3 refl = reflect(rd, n);',
529        '      vec3 skyRefl = sampleSky(refl);',
530        '      vec3 mirrorSky = sampleSky(normalize(vec3(rd.x, max(-rd.y, 0.001), rd.z)));',
531        '      skyRefl = mix(skyRefl, mirrorSky, horizonFade * 0.82);',
532        '      float fresnel = pow(1.0 - max(dot(-rd, n), 0.0), 2.25);',
533        '      fresnel = clamp(fresnel + 0.4, 0.0, 1.0);',
534        '      fresnel = mix(fresnel, 1.0, horizonFade * 0.96);',
535        '      vec3 shallow = mirrorSky * 0.38 + vec3(0.03, 0.04, 0.05);',
536        '      vec3 deep = mirrorSky * 0.16 + vec3(0.01, 0.025, 0.04);',
537        '      float depth = clamp(length(xz) * 0.055, 0.0, 1.0);',
538        '      vec3 waterBody = mix(shallow, deep, depth * 0.55);',
539        '      vec3 sunDir = horizonSunDir();',
540        '      float swellShade = clamp(dot(procN, sunDir) * 0.42 + 0.94, 0.82, 1.14);',
541        '      float crest = smoothstep(0.22, 0.58, waveSlope + procN.y * 0.35);',
542        '      float trough = smoothstep(0.42, 0.08, procN.y);',
543        '      float spec = pow(max(dot(refl, sunDir), 0.0), 68.0);',
544        '      spec *= (1.0 + crest * 2.8) * (1.0 - horizonFade * 0.85);',
545        '      col = mix(waterBody, skyRefl, fresnel * uWaterSkyReflect);',
546        '      col *= mix(1.0, swellShade, (1.0 - horizonFade) * 0.62);',
547        '      col = mix(col, mirrorSky, horizonFade * 0.55);',
548        '      col += skyRefl * fresnel * 0.07 * (1.0 - horizonFade);',
549        '      col += vec3(1.0, 0.95, 0.85) * spec * 0.58;',
550        '      col += mix(mirrorSky, vec3(1.0), 0.42) * crest * 0.16 * (1.0 - horizonFade);',
551        '      col *= mix(1.0, 0.9, trough * 0.35 * (1.0 - horizonFade));',
552        '    }',
553        '  }',
554        '  return col;',
555        '}',
556
557        'void main() {',
558        '  vec2 uv = gl_FragCoord.xy / uResolution;',
559        '  vec3 rd = normalize(vRayDir);',
560        '  vec3 eye = vec3(0.0, 1.65, -2.4);',
561        '  vec3 skyCol = sampleSky(rd);',
562        '  vec3 waterCol = shadeWater(rd, eye);',
563        '  float blend = smoothstep(uHorizon - 0.045, uHorizon + 0.014, uv.y);',
564        '  vec3 col = mix(waterCol, skyCol, blend);',
565        '  float horizonGlow = exp(-abs(uv.y - uHorizon) * 34.0) * 0.045;',
566        '  col += vec3(1.0, 0.88, 0.72) * horizonGlow;',
567        '  gl_FragColor = vec4(col, uFadeIn);',
568        '}'
569    ].join('\n');
570
571    var state = {
572        renderer: null,
573        material: null,
574        scene: null,
575        camera: null,
576        raf: 0,
577        running: false,
578        overlay: null,
579        warmupFrames: 0,
580        targetPixelRatio: 1,
581        tuning: null
582    };
583
584    var assetCache = {
585        water1: null,
586        water2: null,
587        waterPromise: null
588    };
589
590    function qualityToShaderUniform(quality) {
591        var q = Math.max(0, Math.min(100, Number(quality) || 40));
592        return q / 100;
593    }
594
595    function resolutionScaleToRatio(scale) {
596        var s = Math.max(25, Math.min(100, Number(scale) || 45));
597        return s / 100;
598    }
599
600    function readHorizonTuningValues() {
601        return state.tuning || readHorizonTuning();
602    }
603
604    function computeTargetPixelRatio() {
605        var tuning = readHorizonTuningValues();
606        var dpr;
607        if (global.PupwifeViewportSize && typeof global.PupwifeViewportSize.pixelRatio === 'function') {
608            dpr = global.PupwifeViewportSize.pixelRatio(2);
609        } else {
610            dpr = Math.min(window.devicePixelRatio || 1, 2);
611        }
612        return Math.max(0.5, dpr * resolutionScaleToRatio(tuning.resolutionScale));
613    }
614
615    function shouldDeferWaterLayer2() {
616        var tuning = readHorizonTuningValues();
617        return Number(tuning.shaderQuality) < 55;
618    }
619
620    function yieldToMain() {
621        return new Promise(function (resolve) {
622            if (typeof requestAnimationFrame === 'function') {
623                requestAnimationFrame(function () {
624                    setTimeout(resolve, 0);
625                });
626            } else {
627                setTimeout(resolve, 0);
628            }
629        });
630    }
631
632    function textureFromImage(img) {
633        if (!img || typeof THREE === 'undefined') return null;
634        var tex = new THREE.Texture(img);
635        tex.needsUpdate = true;
636        configureWaterNormalTexture(tex);
637        return tex;
638    }
639
640    function preloadWaterNormalImages() {
641        if (assetCache.waterPromise) return assetCache.waterPromise;
642        var deferLayer2 = shouldDeferWaterLayer2();
643        assetCache.waterPromise = loadImage(WATER_NORMAL_PATHS.layer1).then(function (layer1) {
644            if (layer1) assetCache.water1 = layer1;
645            if (deferLayer2 || WATER_LAYER2_USES_SEAMLESS_FALLBACK) {
646                return [layer1, null];
647            }
648            return loadImage(WATER_NORMAL_PATHS.layer2).then(function (layer2) {
649                if (layer2) assetCache.water2 = layer2;
650                return [layer1, layer2];
651            });
652        });
653        return assetCache.waterPromise;
654    }
655
656    function preloadWaterLayer2IfNeeded() {
657        if (assetCache.water2 || WATER_LAYER2_USES_SEAMLESS_FALLBACK) {
658            return Promise.resolve(assetCache.water2);
659        }
660        return loadImage(WATER_NORMAL_PATHS.layer2).then(function (layer2) {
661            if (layer2) {
662                assetCache.water2 = layer2;
663                if (state.material && state.material.uniforms.uUseWaterNormals.value > 0.5) {
664                    applyWaterTexturesToMaterial([assetCache.water1, layer2]);
665                }
666            }
667            return layer2;
668        });
669    }
670
671    function applyWaterTexturesToMaterial(textures) {
672        if (!state.material || !textures[0]) return false;
673        var tex1 = textureFromImage(textures[0]);
674        var tex2 = textures[1] ? textureFromImage(textures[1]) : null;
675        if (!tex1) return false;
676        var u = state.material.uniforms;
677        if (u.uWaterNormal1.value && u.uWaterNormal1.value.dispose) {
678            u.uWaterNormal1.value.dispose();
679        }
680        if (u.uWaterNormal2.value && u.uWaterNormal2.value.dispose) {
681            u.uWaterNormal2.value.dispose();
682        }
683        u.uWaterNormal1.value = tex1;
684        u.uWaterNormal2.value = tex2 || tex1;
685        u.uUseWaterNormals.value = 1;
686        u.uUseWaterNormal2.value = tex2 ? 1 : 0;
687        scheduleWaterMipmapBuild(tex1);
688        if (tex2) scheduleWaterMipmapBuild(tex2);
689        return true;
690    }
691
692    function scheduleWaterMipmapBuild(tex) {
693        if (!tex) return;
694        var build = function () {
695            if (!tex || !state.material) return;
696            tex.generateMipmaps = true;
697            tex.minFilter = THREE.LinearMipmapLinearFilter;
698            tex.needsUpdate = true;
699        };
700        if (typeof requestIdleCallback === 'function') {
701            requestIdleCallback(build, { timeout: 1200 });
702        } else {
703            setTimeout(build, 48);
704        }
705    }
706
707    function hideCausticsCanvas() {
708        var overlay = document.getElementById('webgl-overlay');
709        if (!overlay) return;
710        overlay.querySelectorAll('canvas').forEach(function (canvas) {
711            if (canvas.dataset.siteBg !== 'horizon') {
712                canvas.style.display = 'none';
713            }
714        });
715    }
716
717    function hide() {
718        if (state.renderer && state.renderer.domElement) {
719            state.renderer.domElement.style.display = 'none';
720        }
721    }
722
723    function stopLoop() {
724        state.running = false;
725        if (state.raf) {
726            cancelAnimationFrame(state.raf);
727            state.raf = 0;
728        }
729    }
730
731    function destroy(removeCanvas) {
732        stopLoop();
733        if (state.renderer) {
734            if (state.material) {
735                var u = state.material.uniforms;
736                if (u.uSkybox.value && u.uSkybox.value.dispose) u.uSkybox.value.dispose();
737                if (u.uWaterNormal1.value && u.uWaterNormal1.value.dispose) u.uWaterNormal1.value.dispose();
738                if (u.uWaterNormal2.value && u.uWaterNormal2.value.dispose) u.uWaterNormal2.value.dispose();
739                state.material.dispose();
740            }
741            if (removeCanvas !== false && state.renderer.domElement.parentNode) {
742                state.renderer.domElement.parentNode.removeChild(state.renderer.domElement);
743            }
744            state.renderer.dispose();
745        }
746        state.renderer = null;
747        state.material = null;
748        state.scene = null;
749        state.camera = null;
750        delete global.__webglHorizonInitialized;
751        delete global.__horizonBackgroundReady;
752    }
753
754    function readViewportCss() {
755        if (global.PupwifeViewportSize && typeof global.PupwifeViewportSize.css === 'function') {
756            return global.PupwifeViewportSize.css();
757        }
758        return { width: global.innerWidth, height: global.innerHeight };
759    }
760
761    function resize() {
762        if (!state.renderer || !state.material) return;
763        var vp = readViewportCss();
764        var w = vp.width;
765        var h = vp.height;
766        state.targetPixelRatio = computeTargetPixelRatio();
767        state.renderer.setSize(w, h);
768        if (state.warmupFrames >= 4) {
769            state.renderer.setPixelRatio(state.targetPixelRatio);
770        }
771        state.material.uniforms.uResolution.value.set(w, h);
772        state.material.uniforms.uAspect.value = w / Math.max(h, 1);
773        var horizon = 0.38 + (h / Math.max(w, 1)) * 0.04;
774        state.material.uniforms.uHorizon.value = Math.min(0.52, Math.max(0.32, horizon));
775    }
776
777    function tick(startTime) {
778        if (!state.running || !state.material || !state.renderer) return;
779        var t = (performance.now() - startTime) * 0.001;
780        state.material.uniforms.uTime.value = t;
781        if (state.material.uniforms.uFadeIn.value < 1) {
782            state.material.uniforms.uFadeIn.value = Math.min(1, state.material.uniforms.uFadeIn.value + 0.012);
783        }
784        if (state.warmupFrames < 4) {
785            var ramp = 0.35 + state.warmupFrames * 0.22;
786            state.renderer.setPixelRatio(Math.max(1, state.targetPixelRatio * ramp));
787            state.warmupFrames += 1;
788        }
789        state.renderer.render(state.scene, state.camera);
790        state.raf = requestAnimationFrame(function () { tick(startTime); });
791    }
792
793    function warmCompileShader() {
794        if (!state.renderer || !state.scene || !state.camera) return Promise.resolve();
795        state.renderer.setPixelRatio(1);
796        state.renderer.setSize(2, 2, false);
797        state.renderer.render(state.scene, state.camera);
798        return yieldToMain();
799    }
800
801    function buildScene() {
802        var overlay = document.getElementById('webgl-overlay');
803        if (!overlay || overlay.dataset.disableCanvas === 'true') {
804            return Promise.resolve();
805        }
806        if (typeof THREE === 'undefined') {
807            console.warn('webglHorizonScene: THREE is not loaded');
808            return Promise.resolve();
809        }
810
811        hideCausticsCanvas();
812
813        var existing = overlay.querySelector('canvas[data-site-bg="horizon"]');
814        if (existing && state.renderer && state.renderer.domElement === existing) {
815            global.__webglHorizonInitialized = true;
816            existing.style.display = '';
817            state.running = true;
818            tick(performance.now());
819            markHorizonReady();
820            return finishHorizonAssets();
821        }
822
823        destroy(false);
824
825        global.__webglHorizonInitialized = true;
826        state.overlay = overlay;
827        state.warmupFrames = 0;
828        state.tuning = readHorizonTuning();
829        state.targetPixelRatio = computeTargetPixelRatio();
830
831        var initVp = readViewportCss();
832        var scene = new THREE.Scene();
833        var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
834        var renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false, powerPreference: 'high-performance' });
835        renderer.setClearColor(0x000000, 0);
836        renderer.setPixelRatio(1);
837        renderer.domElement.dataset.siteBg = 'horizon';
838        overlay.appendChild(renderer.domElement);
839
840        var material = new THREE.ShaderMaterial({
841            transparent: true,
842            depthWrite: false,
843            uniforms: {
844                uTime: { value: 0 },
845                uFadeIn: { value: 0 },
846                uHorizon: { value: 0.42 },
847                uUseCubemap: { value: 0 },
848                uSkybox: { value: null },
849                uWaterNormal1: { value: null },
850                uWaterNormal2: { value: null },
851                uUseWaterNormals: { value: 0 },
852                uUseWaterNormal2: { value: 0 },
853                uSkyExposure: { value: HORIZON_SKY_EXPOSURE },
854                uWaterSkyReflect: { value: HORIZON_WATER_SKY_REFLECT },
855                uNormalInfluence: { value: HORIZON_NORMAL_INFLUENCE },
856                uWaveSwell: { value: HORIZON_WAVE_SWELL },
857                uWaveDisplace: { value: HORIZON_WAVE_DISPLACE },
858                uWaveSteepness: { value: HORIZON_WAVE_STEEPNESS },
859                uShaderQuality: { value: qualityToShaderUniform(readHorizonTuning().shaderQuality) },
860                uResolution: { value: new THREE.Vector2(initVp.width, initVp.height) },
861                uAspect: { value: initVp.width / Math.max(initVp.height, 1) },
862                uTanFov: { value: HORIZON_TAN_HALF_FOV },
863                uVerticalScale: { value: HORIZON_VERTICAL_SCALE }
864            },
865            vertexShader: VERT,
866            fragmentShader: FRAG
867        });
868
869        var mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
870        scene.add(mesh);
871
872        state.scene = scene;
873        state.camera = camera;
874        state.renderer = renderer;
875        state.material = material;
876        applyHorizonTuning(readHorizonTuning());
877
878        resize();
879        if (!state.__resizeBound) {
880            state.__resizeBound = true;
881            window.addEventListener('resize', resize);
882        }
883
884        return warmCompileShader().then(function () {
885            resize();
886            state.running = true;
887            tick(performance.now());
888            markHorizonReady();
889            return finishHorizonAssets();
890        });
891    }
892
893    function init() {
894        if (global.__webglHorizonInitialized && state.renderer) {
895            hideCausticsCanvas();
896            if (state.renderer.domElement) state.renderer.domElement.style.display = '';
897            state.running = true;
898            tick(performance.now());
899            markHorizonReady();
900            return finishHorizonAssets();
901        }
902
903        return yieldToMain().then(buildScene);
904    }
905
906    function markHorizonReady() {
907        if (global.__horizonBackgroundReady) return;
908        global.__horizonBackgroundReady = true;
909        var queue = global.__horizonBackgroundWhenReadyQueue;
910        if (queue && queue.length) {
911            for (var i = 0; i < queue.length; i++) {
912                try { queue[i](); } catch (e) { /* ignore */ }
913            }
914            global.__horizonBackgroundWhenReadyQueue = null;
915        }
916        try {
917            global.dispatchEvent(new CustomEvent('pupwife:horizon-background-ready'));
918        } catch (err) {
919            global.dispatchEvent(new Event('pupwife:horizon-background-ready'));
920        }
921    }
922
923    function whenReady() {
924        if (global.__horizonBackgroundReady) return Promise.resolve();
925        return new Promise(function (resolve) {
926            if (global.__horizonBackgroundReady) {
927                resolve();
928                return;
929            }
930            global.__horizonBackgroundWhenReadyQueue = global.__horizonBackgroundWhenReadyQueue || [];
931            global.__horizonBackgroundWhenReadyQueue.push(resolve);
932            global.addEventListener('pupwife:horizon-background-ready', function onReady() {
933                global.removeEventListener('pupwife:horizon-background-ready', onReady);
934                resolve();
935            });
936        });
937    }
938
939    function completeHorizonInit() {
940        return finishHorizonAssets();
941    }
942
943    function finishHorizonAssets() {
944        return ensureHorizonAssetsLoaded().then(function () {
945            return yieldToMain();
946        }).then(function () {
947            if (state.renderer && state.scene && state.camera) {
948                resize();
949                state.renderer.render(state.scene, state.camera);
950            }
951            if (shouldDeferWaterLayer2()) {
952                return preloadWaterLayer2IfNeeded().catch(function () { return null; });
953            }
954        });
955    }
956
957    function readHorizonTuning() {
958        if (global.SiteBackgroundTuning && typeof global.SiteBackgroundTuning.readTuning === 'function') {
959            return global.SiteBackgroundTuning.readTuning().horizon;
960        }
961        return {
962            skyExposure: HORIZON_SKY_EXPOSURE,
963            waterSkyReflect: HORIZON_WATER_SKY_REFLECT,
964            normalInfluence: HORIZON_NORMAL_INFLUENCE,
965            waveSwell: HORIZON_WAVE_SWELL,
966            waveDisplace: HORIZON_WAVE_DISPLACE,
967            waveSteepness: HORIZON_WAVE_STEEPNESS,
968            fieldOfView: HORIZON_TAN_HALF_FOV,
969            verticalScale: HORIZON_VERTICAL_SCALE,
970            shaderQuality: 40,
971            resolutionScale: 45
972        };
973    }
974
975    function applyHorizonTuning(tuning) {
976        if (global.SiteBackgroundTuning && typeof global.SiteBackgroundTuning.mergeSettings === 'function') {
977            tuning = global.SiteBackgroundTuning.mergeSettings({ horizon: tuning }).horizon;
978        }
979        state.tuning = tuning;
980        if (!state.material || !state.material.uniforms) return;
981        var u = state.material.uniforms;
982        u.uSkyExposure.value = tuning.skyExposure;
983        u.uWaterSkyReflect.value = tuning.waterSkyReflect;
984        u.uNormalInfluence.value = tuning.normalInfluence;
985        u.uWaveSwell.value = tuning.waveSwell;
986        u.uWaveDisplace.value = tuning.waveDisplace;
987        u.uWaveSteepness.value = tuning.waveSteepness;
988        u.uTanFov.value = tuning.fieldOfView;
989        u.uVerticalScale.value = tuning.verticalScale;
990        u.uShaderQuality.value = qualityToShaderUniform(tuning.shaderQuality);
991        resize();
992    }
993
994    function preload() {
995        var tasks = [preloadWaterNormalImages()];
996        if (HORIZON_USE_CUBEMAP_SKY) {
997            tasks.push(loadCubemap());
998        }
999        return Promise.all(tasks).catch(function () { return null; });
1000    }
1001
1002    global.HorizonBackground = {
1003        init: init,
1004        preload: preload,
1005        whenReady: whenReady,
1006        applyTuning: applyHorizonTuning,
1007        destroy: destroy,
1008        stop: stopLoop,
1009        hide: hide
1010    };
1011})(typeof window !== 'undefined' ? window : this);

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.