1// WebGL ocean background for the Segmentation research card. 2// Adapted from the three.js webgl_shaders_ocean example (classic Water + Sky on 3// the WebGL renderer). We deliberately avoid the WebGPU renderer here: pairing 4// WebGPURenderer with the example WaterMesh/SkyMesh pulls in a second Three.js 5// instance whose node materials never bind to the renderer, and on many 6// GPU/driver combos (e.g. Mesa Intel) WebGPU reports an adapter yet paints 7// nothing â so the card silently came up blank. Plain WebGL is the same 8// single-instance path the Instance Metrics card already uses reliably. 9// The camera slowly orbits so the water and sky drift behind the card content. 10// Renders only while the Research section is showing, and holds still under 11// reduced motion. 12import * as THREE from "https://esm.sh/[email protected]"; 13import { Sky } from "https://esm.sh/[email protected]/examples/jsm/objects/Sky.js"; 14import { Water } from "https://esm.sh/[email protected]/examples/jsm/objects/Water.js"; 15 16const card = document.querySelector('.research-card[data-id="segmentation"]'); 17const WATER_NORMALS_URL = 18 "https://cdn.jsdelivr.net/gh/mrdoob/three.js@r169/examples/textures/waternormals.jpg"; 19 20const reduceMotion = window.matchMedia( 21 "(prefers-reduced-motion: reduce)" 22).matches; 23 24let renderer, 25 scene, 26 camera, 27 water, 28 running = false, 29 ready = false; 30 31function inResearch() { 32 return ( 33 document.documentElement.getAttribute("data-section") === "research" || 34 document.querySelector("#research.section.active") !== null 35 ); 36} 37 38let sizedW = 0, 39 sizedH = 0; 40 41function sizeToCard() { 42 const w = Math.max(1, card.clientWidth); 43 const h = Math.max(1, card.clientHeight); 44 if (w === sizedW && h === sizedH) { 45 return; 46 } 47 sizedW = w; 48 sizedH = h; 49 renderer.setSize(w, h, false); 50 camera.aspect = w / h; 51 camera.updateProjectionMatrix(); 52} 53 54function init(canvas) { 55 renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas }); 56 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); 57 renderer.toneMapping = THREE.ACESFilmicToneMapping; 58 renderer.toneMappingExposure = 0.35; 59 60 scene = new THREE.Scene(); 61 camera = new THREE.PerspectiveCamera(55, 1, 1, 20_000); 62 63 const sun = new THREE.Vector3(); 64 65 // Water 66 const waterNormals = new THREE.TextureLoader().load(WATER_NORMALS_URL); 67 waterNormals.wrapS = THREE.RepeatWrapping; 68 waterNormals.wrapT = THREE.RepeatWrapping; 69 70 water = new Water(new THREE.PlaneGeometry(10_000, 10_000), { 71 distortionScale: 3.7, 72 fog: false, 73 sunColor: 0xff_ff_ff, 74 sunDirection: new THREE.Vector3(), 75 textureHeight: 512, 76 textureWidth: 512, 77 waterColor: 0x00_1e_2f, 78 waterNormals, 79 }); 80 water.rotation.x = -Math.PI / 2; 81 scene.add(water); 82 83 // Sky 84 const sky = new Sky(); 85 sky.scale.setScalar(10_000); 86 scene.add(sky); 87 const skyU = sky.material.uniforms; 88 skyU.turbidity.value = 10; 89 skyU.rayleigh.value = 2; 90 skyU.mieCoefficient.value = 0.005; 91 skyU.mieDirectionalG.value = 0.8; 92 93 const elevation = 3, 94 azimuth = 175; 95 const pmrem = new THREE.PMREMGenerator(renderer); 96 const sceneEnv = new THREE.Scene(); 97 let renderTarget; 98 99 function updateSun() { 100 const phi = THREE.MathUtils.degToRad(90 - elevation); 101 const theta = THREE.MathUtils.degToRad(azimuth); 102 sun.setFromSphericalCoords(1, phi, theta); 103 skyU.sunPosition.value.copy(sun); 104 water.material.uniforms.sunDirection.value.copy(sun).normalize(); 105 106 if (renderTarget) { 107 renderTarget.dispose(); 108 } 109 sceneEnv.add(sky); 110 renderTarget = pmrem.fromScene(sceneEnv); 111 scene.add(sky); 112 scene.environment = renderTarget.texture; 113 } 114 115 updateSun(); 116 ready = true; 117 sizeToCard(); 118 // First paint runs at the card's tiny home-section size; don't let a hiccup 119 // here abort init (and with it the section-switch observer set up below). 120 try { 121 renderer.render(scene, camera); 122 } catch { 123 /* the loop paints properly once Research is shown */ 124 } 125 ensureRunning(); 126 127 window.addEventListener("resize", () => { 128 if (ready) { 129 sizeToCard(); 130 } 131 }); 132} 133 134function frame() { 135 if (!(ready && inResearch())) { 136 running = false;
137 return; 138 } 139 sizeToCard(); // card has real dimensions once its section is visible 140 const t = performance.now() * 0.001; 141 const ang = reduceMotion ? 2.6 : t * 0.04; 142 const radius = 120; 143 camera.position.set(Math.cos(ang) * radius, 22, Math.sin(ang) * radius); 144 camera.lookAt(0, 6, 0); 145 water.material.uniforms.time.value = reduceMotion ? 0 : t; 146 renderer.render(scene, camera); 147 if (reduceMotion) { 148 running = false; 149 return; 150 } 151 requestAnimationFrame(frame); 152} 153 154function ensureRunning() { 155 if (running || !ready || !inResearch()) { 156 return; 157 } 158 running = true; 159 requestAnimationFrame(frame); 160} 161 162if (card) { 163 const canvas = document.createElement("canvas"); 164 canvas.className = "research-card-bg-canvas research-card-ocean"; 165 canvas.setAttribute("aria-hidden", "true"); 166 card.insertBefore(canvas, card.firstChild); 167 card.classList.add("research-card--bg"); 168 169 // Register the section-switch observer *before* init so the render loop can 170 // still be kicked off even if init throws partway through. 171 const observer = new MutationObserver(ensureRunning); 172 observer.observe(document.documentElement, { 173 attributeFilter: ["data-section"], 174 attributes: true, 175 }); 176 177 init(canvas); 178}
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.