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.