PageSourceSearch

https://hicharlieocean.com/js/scene.js

js hicharlieocean.com collected 2026-09-29 07:25:52 UTC 5,682 bytes, 187 lines download raw bytes

1/* Hero: iridescent ocean wave field.
2   Custom-shader plane, GPU-displaced — cheap to run, pauses off-screen,
3   and bails out entirely (leaving the CSS gradient fallback) when WebGL
4   is unavailable or the visitor prefers reduced motion. */
5
6import * as THREE from "./vendor/three.module.min.js";
7
8const canvas = document.getElementById("ocean");
9const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
10
11if (canvas && !reducedMotion) {
12  try {
13    init();
14  } catch (err) {
15    console.warn("Hero scene disabled:", err);
16    canvas.remove(); // CSS fallback layer stays visible
17  }
18}
19
20function init() {
21  const isMobile = window.matchMedia("(max-width: 48rem)").matches;
22
23  const renderer = new THREE.WebGLRenderer({
24    canvas,
25    antialias: false,
26    alpha: false,
27    powerPreference: "low-power",
28  });
29  renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
30  renderer.setClearColor(0x070b14, 1);
31
32  const scene = new THREE.Scene();
33  scene.fog = new THREE.Fog(0x070b14, 4, 14);
34
35  const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40);
36  camera.position.set(0, 1.35, 4.6);
37  camera.lookAt(0, 0.1, 0);
38
39  // ShaderMaterial with fog:true needs the fog uniforms merged in manually
40  const uniforms = THREE.UniformsUtils.merge([
41    THREE.UniformsLib.fog,
42    {
43      uTime: { value: 0 },
44      uPointer: { value: new THREE.Vector2(0, 0) },
45    },
46  ]);
47
48  const segments = isMobile ? 110 : 190;
49  const geometry = new THREE.PlaneGeometry(26, 18, segments, segments);
50
51  const material = new THREE.ShaderMaterial({
52    uniforms,
53    fog: true,
54    vertexShader: /* glsl */ `
55      uniform float uTime;
56      uniform vec2 uPointer;
57      varying float vElev;
58      varying vec2 vUv;
59
60      #include <fog_pars_vertex>
61
62      // layered gerstner-ish waves
63      float wave(vec2 p, vec2 dir, float freq, float speed, float t) {
64        return sin(dot(p, normalize(dir)) * freq + t * speed);
65      }
66
67      void main() {
68        vUv = uv;
69        vec3 pos = position;
70        float t = uTime;
71
72        float e = 0.0;
73        e += 0.38 * wave(pos.xy, vec2( 1.0,  0.6), 0.55, 0.55, t);
74        e += 0.22 * wave(pos.xy, vec2(-0.7,  1.0), 0.95, 0.80, t);
75        e += 0.12 * wave(pos.xy, vec2( 0.3, -1.0), 1.70, 1.10, t);
76        e += 0.06 * wave(pos.xy, vec2(-1.0, -0.4), 3.10, 1.60, t);
77
78        // gentle swell toward the pointer
79        e += 0.18 * sin(pos.x * 0.4 + uPointer.x * 2.0 + t * 0.3)
80                  * cos(pos.y * 0.4 + uPointer.y * 2.0);
81
82        pos.z += e;
83        vElev = e;
84
85        vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
86        gl_Position = projectionMatrix * mvPosition;
87
88        #include <fog_vertex>
89      }
90    `,
91    fragmentShader: /* glsl */ `
92      uniform float uTime;
93      varying float vElev;
94      varying vec2 vUv;
95
96      #include <fog_pars_fragment>
97
98      void main() {
99        // deep ocean base
100        vec3 deep    = vec3(0.027, 0.043, 0.078);  // #070b14
101        vec3 mid     = vec3(0.055, 0.090, 0.180);
102
103        // iridescent spectrum, mapped along elevation + horizontal drift
104        float band = vUv.x * 1.6 + vElev * 0.9 + uTime * 0.025;
105        vec3 pink   = vec3(1.000, 0.369, 0.541);
106        vec3 amber  = vec3(1.000, 0.702, 0.361);
107        vec3 mint   = vec3(0.361, 0.882, 0.725);
108        vec3 azure  = vec3(0.361, 0.659, 1.000);
109        vec3 violet = vec3(0.690, 0.486, 1.000);
110
111        float b = fract(band);
112        vec3 spectrum =
113            mix(pink,  amber, smoothstep(0.00, 0.25, b)) ;
114        spectrum = mix(spectrum, mint,   smoothstep(0.25, 0.50, b));
115        spectrum = mix(spectrum, azure,  smoothstep(0.50, 0.75, b));
116        spectrum = mix(spectrum, violet, smoothstep(0.75, 1.00, b));
117
118        // crest highlight: spectrum shows on wave peaks only
119        float crest = smoothstep(0.05, 0.9, vElev);
120        vec3 color = mix(deep, mid, vElev * 0.5 + 0.5);
121        color = mix(color, spectrum * 0.9, crest * 0.7);
122
123        // faint overall shimmer so troughs aren't dead flat
124        color += spectrum * 0.06;
125
126        gl_FragColor = vec4(color, 1.0);
127        #include <fog_fragment>
128      }
129    `,
130  });
131
132  const mesh = new THREE.Mesh(geometry, material);
133  mesh.rotation.x = -Math.PI / 2.15;
134  mesh.position.y = -0.6;
135  scene.add(mesh);
136
137  // ---- sizing ----
138  function resize() {
139    const w = canvas.clientWidth;
140    const h = canvas.clientHeight;
141    if (canvas.width !== w || canvas.height !== h) {
142      renderer.setSize(w, h, false);
143      camera.aspect = w / h;
144      camera.updateProjectionMatrix();
145    }
146  }
147  window.addEventListener("resize", resize);
148
149  // ---- pointer parallax (desktop only; cheap lerp) ----
150  const pointerTarget = new THREE.Vector2();
151  if (!isMobile) {
152    window.addEventListener("pointermove", (e) => {
153      pointerTarget.set(
154        (e.clientX / window.innerWidth) * 2 - 1,
155        (e.clientY / window.innerHeight) * 2 - 1
156      );
157    }, { passive: true });
158  }
159
160  // ---- render loop: only while hero is on screen ----
161  let visible = true;
162  let rafId = null;
163  const clock = new THREE.Clock();
164
165  new IntersectionObserver(([entry]) => {
166    visible = entry.isIntersecting;
167    if (visible && rafId === null) loop();
168  }, { threshold: 0 }).observe(canvas);
169
170  document.addEventListener("visibilitychange", () => {
171    if (!document.hidden && visible && rafId === null) loop();
172  });
173
174  function loop() {
175    if (!visible || document.hidden) { rafId = null; return; }
176    rafId = requestAnimationFrame(loop);
177
178    uniforms.uTime.value = clock.getElapsedTime();
179    uniforms.uPointer.value.lerp(pointerTarget, 0.04);
180
181    resize();
182    renderer.render(scene, camera);
183  }
184
185  resize();
186  loop();
187}

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.