1// lemming.js â Three.js Chief Lemming for the Summit Valley Landing Page. 2// Scroll-fade: lemming hides when promo-wrap scrolls over its head. 3// Hero events: dispatches sv-hero-hidden / sv-hero-revealed so weather, bubble, 4// and other layers can stagger their reveal sequence. 5 6import * as THREE from './lib/three.module.js'; 7import { GLTFLoader } from './lib/GLTFLoader.js'; 8 9console.log('[lemming] module loaded, THREE:', !!THREE); 10 11const canvas = document.getElementById('lemming-canvas'); 12console.log('[lemming] canvas element:', canvas, 'size:', canvas?.offsetWidth, canvas?.offsetHeight); 13 14const isMobile = window.innerWidth <= 768; 15 16let renderer; 17try { 18 // On mobile: no antialias = much lighter GPU load 19 renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: !isMobile }); 20 console.log('[lemming] WebGLRenderer created OK'); 21} catch (e) { 22 console.error('[lemming] WebGLRenderer FAILED:', e); 23 throw e; 24} 25// Cap pixel ratio: 1 on mobile, 1.5 on tablets (iPad landscape = dPR 2 â saves ~44% GPU work), full on desktop. 26const isTablet = !isMobile && window.innerWidth <= 1194; 27renderer.setPixelRatio(isMobile ? 1 : isTablet ? Math.min(window.devicePixelRatio, 1.5) : window.devicePixelRatio); 28renderer.outputColorSpace = THREE.SRGBColorSpace; 29renderer.shadowMap.enabled = false; 30 31const scene = new THREE.Scene(); 32 33// Mobile: 360px tall canvas â matches CSS height so legs aren't clipped 34function sceneH() { 35 if (isMobile) return 360; 36 return Math.max(280, window.innerWidth * 915 / 1719); 37} 38 39const camera = new THREE.PerspectiveCamera(40, window.innerWidth / sceneH(), 0.1, 100); 40camera.position.set(0, 1.0, isMobile ? 2.8 : 4.5); 41camera.lookAt(0, 0.4, 0); 42 43// Lighting â warm outdoor feel 44const ambient = new THREE.AmbientLight(0xfff8f0, 1.4); 45scene.add(ambient); 46const sun = new THREE.DirectionalLight(0xfff5e0, 2.0); 47sun.position.set(3, 8, 5); 48scene.add(sun); 49const fill = new THREE.DirectionalLight(0xd0e8ff, 0.6); 50fill.position.set(-3, 2, -2); 51scene.add(fill); 52 53// ââ State âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 54const loader = new GLTFLoader(); 55 56let mixer = null; 57let model = null; 58let idleAction = null; 59let charHeadPoint = null; 60 61const promoEl = document.querySelector('.promo-wrap'); 62 63// ââ Hero visibility ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 64// prevCovered: tracks last known state so events fire on change, not every tick. 65// heroActive: pauses Three.js render loop when hero is fully hidden â saves GPU 66// for work the user literally cannot see. 67let prevCovered = false; 68let heroActive = true; // hero visible on page load 69 70// ââ Scroll-fade: hide lemming when promo-wrap scrolls over its head âââââââââââ 71function checkCoverage() { 72 if (!charHeadPoint || !promoEl) return; 73 const projected = charHeadPoint.clone().project(camera); 74 const canvasH = sceneH(); 75 const headScreenY = (-projected.y * 0.5 + 0.5) * canvasH; 76 const promoTop = promoEl.getBoundingClientRect().top; 77 const isCovered = promoTop <= headScreenY; 78 79 canvas.style.opacity = isCovered ? '0' : '1'; 80 81 // Only fire hero events when the covered state actually changes 82 if (isCovered !== prevCovered) { 83 prevCovered = isCovered; 84 85 if (isCovered) { 86 // ââ Hero hiding ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 87 // Dispatch immediately so weather/bubble layers start their fade-out. 88 // Pause Three.js loop AFTER the CSS opacity fade completes (~400ms) so 89 // the canvas is never frozen mid-transition â it fades out smoothly first. 90 window.dispatchEvent(new CustomEvent('sv-hero-hidden')); 91 setTimeout(() => { heroActive = false; }, 420); 92 } else { 93 // ââ Hero revealing âââââââââââââââââââââââââââââââââââââââââââââââââââ 94 // Resume Three.js FIRST so the canvas has a rendered frame before the 95 // opacity fade-in begins. Weather + bubble layers stagger behind via 96 // their own event listeners (250ms and 500ms delays). 97 heroActive = true; 98 window.dispatchEvent(new CustomEvent('sv-hero-revealed')); 99 } 100 } 101} 102 103window.addEventListener('scroll', checkCoverage, { passive: true }); 104 105// ââ Load idle GLB âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 106loader.load( 107 'assets/glb/Chief Lemming - Breathing Idle.glb', 108 (gltf) => { 109 model = gltf.scene;
110 model.scale.setScalar(isMobile ? 0.45 : 0.45); 111 model.position.set(-0.35, isMobile ? -0.7 : -1.25, -0.3); 112 scene.add(model); 113 114 // Fix transparency/depth sorting 115 model.traverse((child) => { 116 if (child.isMesh) { 117 child.material.depthWrite = true; 118 child.material.depthTest = true; 119 if (child.material.opacity >= 0.99) { 120 child.material.transparent = false; 121 } 122 child.renderOrder = 0; 123 } 124 }); 125 126 if (gltf.animations.length > 0) { 127 mixer = new THREE.AnimationMixer(model); 128 idleAction = mixer.clipAction(gltf.animations[0]); 129 idleAction.play(); 130 } 131 132 // Compute head point from bounding box top-center (same as original app) 133 const box = new THREE.Box3().setFromObject(model); 134 charHeadPoint = new THREE.Vector3( 135 (box.min.x + box.max.x) / 2, 136 box.max.y, 137 (box.min.z + box.max.z) / 2 138 ); 139 checkCoverage(); 140 141 // React clip not loaded â no click interaction on landing page. 142 }, 143 undefined, 144 (err) => console.error('[lemming] GLB load FAILED:', err) 145); 146 147// No click/hover interaction on the landing page â pointer-events: none in CSS. 148 149// ââ Resize â canvas height = scene height (not full viewport) âââââââââââââââââ 150function onResize() { 151 const w = window.innerWidth; 152 const h = sceneH(); 153 renderer.setSize(w, h, false); 154 camera.aspect = w / h; 155 camera.updateProjectionMatrix(); 156} 157onResize(); 158window.addEventListener('resize', onResize); 159 160// ââ IntersectionObserver â pause render loop when canvas is off-screen âââââââââ 161// Note: the canvas is position:fixed so it never actually leaves the viewport. 162// heroActive (above) is the primary pause mechanism for the hidden-hero state. 163// This observer is kept as a lightweight safety net for any edge cases. 164let lemmingVisible = true; 165const visObserver = new IntersectionObserver( 166 (entries) => { lemmingVisible = entries[0].isIntersecting; }, 167 { threshold: 0 } 168); 169visObserver.observe(canvas); 170 171// ââ Render loop âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 172const clock = new THREE.Clock(); 173(function animate() { 174 requestAnimationFrame(animate); 175 if (!lemmingVisible) return; // canvas off-screen (edge case safety) 176 if (!heroActive) return; // hero hidden under promo â GPU freed 177 const delta = clock.getDelta(); 178 if (mixer) mixer.update(delta); 179 renderer.render(scene, camera); 180})();
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.