|
62 | 62 | <div class="hero"> |
63 | 63 | <div class="horizon" aria-hidden="true"></div><div class="ambient" aria-hidden="true"></div> |
64 | 64 | <div class="hero-title"><div class="kicker">Precision calibration / Unit 01</div><h1>Kern<br/>Index</h1><p>A mechanical reference assembly measured under load. Scroll to reveal its five critical systems in colour.</p></div> |
65 | | - <div class="index" aria-hidden="true"><div class="num">01</div><small>Tolerance ±0.002 μm<br/>Cycle 12.000 s</small></div> |
| 65 | + <div class="index" aria-hidden="true"><div class="num">01</div><small>Tolerance ±0.002 μm<br/>Cycle 11.667 s</small></div> |
66 | 66 | <div class="specimen-wrap" aria-hidden="true"><div class="specimen-stage"><video class="specimen base" src="./assets/kern-mono.mp4" poster="./assets/kern-calibration-unit.png" muted loop playsinline webkit-playsinline autoplay preload="auto" disablepictureinpicture></video><video class="specimen scan" src="./assets/kern-color.mp4" poster="./assets/kern-calibration-unit.png" muted loop playsinline webkit-playsinline autoplay preload="auto" disablepictureinpicture></video><div class="scan-plane"></div><div class="bloom-ring"></div><div class="bloom-ring"></div><div class="bloom-ring"></div><div class="bloom-ring"></div><div class="bloom-ring"></div><div class="target"><i></i></div><div class="connector"></div></div></div> |
67 | 67 | <aside class="panel panel-left" aria-label="Calibration data"><div class="panel-head"><span>System index</span><span class="blink">Scan active</span></div><h2 id="speciesName">Copper coil</h2><div class="latin" id="speciesLatin">Electromagnetic impulse stage</div><div class="metrics"><div class="metric"><b id="metricVital">96</b><span>Stability</span></div><div class="metric"><b id="metricSignal">91</b><span>Torque</span></div><div class="metric"><b id="metricRisk">04</b><span>Drift</span></div></div><div class="specimen-list" role="list"><button class="specimen-btn active" data-index="0" type="button">01 Copper coil</button><button class="specimen-btn" data-index="1" type="button">02 Outer gimbal</button><button class="specimen-btn" data-index="2" type="button">03 Escapement</button><button class="specimen-btn" data-index="3" type="button">04 Ruby bearing</button><button class="specimen-btn" data-index="4" type="button">05 Cyan emitter</button></div></aside> |
68 | 68 | <aside class="panel panel-right" aria-label="Macro optical view"><div class="panel-head"><span>Macro optics</span><span id="plateLabel">Plate 01/A</span></div><div class="macro"><img src="./assets/kern-calibration-unit.png" alt="Magnified precision mechanism" /></div><div class="macro-meta"><span id="magnification">MAG 4.8×</span><span>Tracking locked</span></div></aside> |
|
139 | 139 | function syncVideos(force=false){ |
140 | 140 | if(monoVideo.readyState<2||colorVideo.readyState<2||!monoVideo.duration||!colorVideo.duration)return; |
141 | 141 | const delta=monoVideo.currentTime-colorVideo.currentTime, drift=Math.abs(delta); |
142 | | - if((force||drift>.12)&&!colorVideo.seeking){colorVideo.currentTime=monoVideo.currentTime;colorVideo.playbackRate=1;} |
143 | | - else colorVideo.playbackRate=1+Math.max(-.08,Math.min(.08,delta)); |
| 142 | + if((force||drift>.12)&&!colorVideo.seeking){colorVideo.currentTime=monoVideo.currentTime;colorVideo.playbackRate=monoVideo.playbackRate;} |
| 143 | + else colorVideo.playbackRate=monoVideo.playbackRate+Math.max(-.08,Math.min(.08,delta)); |
144 | 144 | if(monoVideo.paused!==colorVideo.paused){monoVideo.paused?colorVideo.pause():colorVideo.play().catch(()=>{});} |
145 | 145 | } |
146 | 146 | const whenReady=(video)=>video.readyState>=2?Promise.resolve():new Promise(resolve=>video.addEventListener('loadeddata',resolve,{once:true})); |
147 | 147 | async function startVideos(){ |
148 | 148 | try{ |
149 | 149 | monoVideo.defaultMuted=colorVideo.defaultMuted=true; |
150 | 150 | await Promise.all([whenReady(monoVideo),whenReady(colorVideo)]); |
| 151 | + if(reduce){monoVideo.pause();colorVideo.pause();try{monoVideo.currentTime=0;colorVideo.currentTime=0}catch(_){}return;} |
151 | 152 | colorVideo.currentTime=monoVideo.currentTime; |
152 | 153 | await Promise.all([monoVideo.play(),colorVideo.play()]); |
153 | 154 | syncVideos(true); |
154 | 155 | }catch(_){/* posters remain visible until browser permits playback */} |
155 | 156 | } |
156 | | - monoVideo.addEventListener('playing',()=>syncVideos(true));monoVideo.addEventListener('timeupdate',()=>syncVideos());monoVideo.addEventListener('seeking',()=>syncVideos(true));monoVideo.addEventListener('ended',()=>syncVideos(true)); |
157 | | - if('requestVideoFrameCallback' in monoVideo){const frameSync=()=>{syncVideos();monoVideo.requestVideoFrameCallback(frameSync)};monoVideo.requestVideoFrameCallback(frameSync)}else setInterval(syncVideos,80); |
158 | | - document.addEventListener('visibilitychange',()=>{if(!document.hidden)startVideos()}); |
| 157 | + // Velocity-reactive loop: the mechanism spins faster while the user scrolls, easing back to 1× at rest. |
| 158 | + let velRate=1,lastScrollT=0,lastScrollYv=scrollY,heroVisible=true; |
| 159 | + function tick(){ |
| 160 | + if(!reduce&&!monoVideo.paused){ |
| 161 | + if(performance.now()-lastScrollT>140)velRate+=(1-velRate)*.08; |
| 162 | + const r=monoVideo.playbackRate+(velRate-monoVideo.playbackRate)*.18; |
| 163 | + monoVideo.playbackRate=Math.abs(r-1)<.005?1:r; |
| 164 | + } |
| 165 | + syncVideos(); |
| 166 | + } |
| 167 | + monoVideo.addEventListener('playing',()=>syncVideos(true));monoVideo.addEventListener('timeupdate',()=>syncVideos());monoVideo.addEventListener('seeking',()=>syncVideos(true)); |
| 168 | + if('requestVideoFrameCallback' in monoVideo){const frameSync=()=>{tick();monoVideo.requestVideoFrameCallback(frameSync)};monoVideo.requestVideoFrameCallback(frameSync)}else setInterval(tick,80); |
| 169 | + document.addEventListener('visibilitychange',()=>{if(!document.hidden&&heroVisible)startVideos()}); |
159 | 170 | addEventListener('pageshow',startVideos,{once:true}); |
160 | | - ['pointerdown','touchstart','wheel','scroll'].forEach(type=>addEventListener(type,()=>{if(monoVideo.paused||colorVideo.paused)startVideos()},{once:true,passive:true})); |
| 171 | + // Persistent recovery: any interaction restarts a stalled loop while the hero is on screen. |
| 172 | + ['pointerdown','touchstart','wheel','scroll'].forEach(type=>addEventListener(type,()=>{if(heroVisible&&!reduce&&(monoVideo.paused||colorVideo.paused))startVideos()},{passive:true})); |
| 173 | + // Decode budget: pause both loops when the hero leaves the viewport, resume in sync on return. |
| 174 | + new IntersectionObserver(es=>{heroVisible=es[0].isIntersecting; |
| 175 | + if(reduce)return; |
| 176 | + if(heroVisible)startVideos();else{monoVideo.pause();colorVideo.pause();} |
| 177 | + },{rootMargin:'15%'}).observe(stage); |
161 | 178 | startVideos(); |
162 | 179 | const smooth=t=>t<=0?0:t>=1?1:t*t*(3-2*t); |
163 | 180 | function draw(){raf=0;const max=document.documentElement.scrollHeight-innerHeight;const global=max?scrollY/max:0;progress.style.transform=`scaleX(${global})`; |
164 | 181 | if(reduce)return; |
| 182 | + const now=performance.now(),dt=now-lastScrollT; |
| 183 | + if(dt>0&&dt<400)velRate=1+Math.min(.9,(Math.abs(scrollY-lastScrollYv)/dt*1000)/2400); |
| 184 | + lastScrollT=now;lastScrollYv=scrollY; |
165 | 185 | if(innerWidth<701){ |
166 | 186 | // Mobile: no pin — the same bloom sequence rides plain page scroll (scroll-coupled, transform+mask only). |
167 | 187 | const planeX=Math.max(0,Math.min(1,scrollY/(innerHeight*.72)))*100; |
|
0 commit comments