Skip to content

Commit d202c33

Browse files
mustbesimo2claude
andcommitted
feat: kern video quality + resilient scroll-reactive looping
Both video layers re-mastered in one pass from the colour source: debanded, upscaled to 2560x1440 (native-sharp on retina), contrast- adaptive sharpened, and loop-crossfaded over 10 frames so the 11.667s wrap is invisible; mono is a desaturated grade of the same stream so the layers are frame-aligned by construction. Playback is now velocity-reactive (loop accelerates to ~1.9x while scrolling, eases back to 1x), recovery hooks are persistent instead of once-only, loops pause off-screen and resume in sync, and reduced-motion gets a true static composition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5f70563 commit d202c33

4 files changed

Lines changed: 32 additions & 11 deletions

File tree

examples/kern-calibration/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ An original cinematic example website for the `cinematic-scroll` skill.
1010
- Scroll-scrubbed CSS radial-gradient masking: five soft colour blooms open cumulatively over each mechanical system as the scan plane crosses it, then a full-frame wash floods the whole unit in colour.
1111
- Frame-by-frame drift correction using `requestVideoFrameCallback`, playback-rate convergence and buffered hard-resync protection.
1212
- A genuinely full-viewport video stage using `object-fit: cover`; no framed media panel or transformed 3D ancestor.
13-
- Generated precision-mechanical hero imagery converted into a seamless 12-second, 30 fps master loop with perceptible camera movement.
13+
- Generated precision-mechanical hero imagery converted into a seamless 11.667-second, 30 fps, 1440p master loop — the seam is crossfaded over 10 frames so the wrap is invisible, and both layers are encoded from one processed master for guaranteed frame alignment.
14+
- Velocity-reactive playback: scrolling accelerates the loop up to ~1.9×, easing back to 1× at rest; the loops pause off-screen and resume in sync.
1415
- Muted inline autoplay plus pointer, touch, wheel, scroll, visibility and page-show recovery hooks.
1516
- Five selectable mechanical systems with live readings and macro crops.
1617
- Procedural Web Audio ambience behind an explicit user-controlled sound toggle.
@@ -21,11 +22,11 @@ An original cinematic example website for the `cinematic-scroll` skill.
2122

2223
| Asset | Role | Specification |
2324
|---|---|---|
24-
| `kern-mono.mp4` | Persistent base layer | 1920×1080, 30 fps, 12 s, H.264 |
25-
| `kern-color.mp4` | Scroll-masked colour layer | 1920×1080, 30 fps, 12 s, H.264 |
25+
| `kern-mono.mp4` | Persistent base layer | 2560×1440, 30 fps, 11.667 s, H.264, seamless crossfaded loop |
26+
| `kern-color.mp4` | Scroll-masked colour layer | 2560×1440, 30 fps, 11.667 s, H.264, seamless crossfaded loop |
2627
| `kern-calibration-unit.png` | Poster and macro source | Generated precision-mechanical plate |
2728

28-
Both videos share identical framing, duration, frame count and camera motion. The monochrome version is derived from the colour master.
29+
Both videos are encoded in a single pass from one debanded, upscaled and sharpened colour master (the monochrome layer is a desaturated grade of the same stream), so framing, duration, frame count and camera motion are identical by construction.
2930

3031
## Originality boundary
3132

2.39 MB
Binary file not shown.
1.96 MB
Binary file not shown.

examples/kern-calibration/index.html

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<div class="hero">
6363
<div class="horizon" aria-hidden="true"></div><div class="ambient" aria-hidden="true"></div>
6464
<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>
6666
<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>
6767
<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>
6868
<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,29 +139,49 @@
139139
function syncVideos(force=false){
140140
if(monoVideo.readyState<2||colorVideo.readyState<2||!monoVideo.duration||!colorVideo.duration)return;
141141
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));
144144
if(monoVideo.paused!==colorVideo.paused){monoVideo.paused?colorVideo.pause():colorVideo.play().catch(()=>{});}
145145
}
146146
const whenReady=(video)=>video.readyState>=2?Promise.resolve():new Promise(resolve=>video.addEventListener('loadeddata',resolve,{once:true}));
147147
async function startVideos(){
148148
try{
149149
monoVideo.defaultMuted=colorVideo.defaultMuted=true;
150150
await Promise.all([whenReady(monoVideo),whenReady(colorVideo)]);
151+
if(reduce){monoVideo.pause();colorVideo.pause();try{monoVideo.currentTime=0;colorVideo.currentTime=0}catch(_){}return;}
151152
colorVideo.currentTime=monoVideo.currentTime;
152153
await Promise.all([monoVideo.play(),colorVideo.play()]);
153154
syncVideos(true);
154155
}catch(_){/* posters remain visible until browser permits playback */}
155156
}
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()});
159170
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);
161178
startVideos();
162179
const smooth=t=>t<=0?0:t>=1?1:t*t*(3-2*t);
163180
function draw(){raf=0;const max=document.documentElement.scrollHeight-innerHeight;const global=max?scrollY/max:0;progress.style.transform=`scaleX(${global})`;
164181
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;
165185
if(innerWidth<701){
166186
// Mobile: no pin — the same bloom sequence rides plain page scroll (scroll-coupled, transform+mask only).
167187
const planeX=Math.max(0,Math.min(1,scrollY/(innerHeight*.72)))*100;

0 commit comments

Comments
 (0)