Skip to content

Commit 9f2a908

Browse files
committed
fix: harden hero autoplay and integrate mobile macro view
1 parent 65e0150 commit 9f2a908

4 files changed

Lines changed: 68 additions & 13 deletions

File tree

examples/kern-calibration/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ python3 -m http.server 8080
4343

4444
```bash
4545
node examples/kern-calibration/test-immersive.mjs
46+
node examples/kern-calibration/test-mobile.mjs
4647
python3 examples/kern-calibration/test-video-motion.py
4748
```

examples/kern-calibration/index.html

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
.method-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:clamp(40px,9vw,140px);align-items:start}.steps{border-top:1px solid var(--ink)}.step{display:grid;grid-template-columns:46px 1fr;gap:20px;border-bottom:1px solid var(--line);padding:23px 0}.step b{font:10px/1 "Courier New",monospace;color:#008b7e}.step h3{font-size:clamp(1.1rem,2vw,1.7rem);margin:0 0 7px;font-weight:500}.step p{font-size:12px;line-height:1.5;color:var(--muted);margin:0;max-width:46ch}.final{min-height:90svh;display:grid;place-items:center;text-align:center;background:var(--paper-hi)}.final h2{max-width:12ch}.final a{display:inline-flex;margin-top:25px;border:1px solid var(--ink);padding:15px 22px;text-decoration:none;font:9px/1 "Courier New",monospace;letter-spacing:.16em;text-transform:uppercase;transition:background .3s var(--ease),color .3s var(--ease)}
5353
.reveal{opacity:0;transform:translateY(28px);transition:opacity 1s var(--ease),transform 1s var(--ease)}.reveal.in{opacity:1;transform:none}
5454
@media(max-width:980px){.panel{width:225px}.panel-left{top:14%}.panel-right{bottom:13%}.hero-title h1{font-size:clamp(3rem,10vw,6.5rem)}.cards{grid-template-columns:1fr}.card{min-height:240px}.card h3{margin-top:55px}.method-grid{grid-template-columns:1fr}}
55-
@media(max-width:700px){.hero-track{height:auto}.hero{height:auto;min-height:100svh;position:relative;padding:105px 18px 42px;display:flex;flex-direction:column}.specimen-wrap{position:relative;inset:auto;height:44svh;order:1;margin-inline:-18px}.specimen-stage{width:100vw;height:44svh;max-width:none}.hero-title{position:relative;left:auto;bottom:auto;order:0;margin-bottom:5px}.hero-title h1{font-size:clamp(3.2rem,18vw,5.8rem)}.hero-title p{font-size:12px}.panel{position:relative;left:auto!important;right:auto!important;top:auto!important;bottom:auto!important;width:100%;opacity:1!important;transform:none!important;background:transparent;backdrop-filter:none}.panel-left{order:2}.panel-right{order:3;margin-top:35px}.index,.scroll-cue,.connector{display:none}.scan-plane{top:0;bottom:0}.macro{height:180px}.nav-right a{display:none}.cards{grid-template-columns:1fr}.section{padding-inline:18px}.section h2{font-size:clamp(2.8rem,15vw,5rem)}}
55+
@media(max-width:700px){.hero-track{height:auto}.hero{height:auto;min-height:100svh;position:relative;padding:92px 18px 34px;display:grid;grid-template-columns:1fr;grid-template-areas:"title" "visual" "index"}.specimen-wrap{grid-area:visual;position:relative;inset:auto;height:50svh;margin-inline:-18px}.specimen-stage{width:100vw;height:50svh;max-width:none}.hero-title{grid-area:title;position:relative;left:auto;bottom:auto;margin-bottom:8px}.hero-title h1{font-size:clamp(3.2rem,18vw,5.8rem)}.hero-title p{font-size:12px}.panel{position:relative;left:auto!important;right:auto!important;top:auto!important;bottom:auto!important;width:100%;opacity:1!important;transform:none!important;background:transparent;backdrop-filter:none}.panel-left{grid-area:index;margin-top:18px}.panel-right{grid-area:visual;position:relative!important;z-index:35;align-self:end;justify-self:end;width:min(48vw,190px);margin:0 -8px 10px 0;padding:8px;background:rgba(234,233,226,.8);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}.index,.scroll-cue,.connector{display:none}.scan-plane{top:0;bottom:0}.macro{height:106px;margin-top:8px}.macro-meta{font-size:6px}.nav-right a{display:none}.cards{grid-template-columns:1fr}.section{padding-inline:18px}.section h2{font-size:clamp(2.8rem,15vw,5rem)}}
5656
@media(hover:hover){.specimen-btn:hover{color:#007e73}.card:hover .card-orbit{transform:scale(1.2) rotate(45deg)}.final a:hover{background:var(--ink);color:var(--paper-hi)}}
5757
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{animation:none!important;transition-duration:.01ms!important}.hero-track{height:auto}.hero{position:relative}.specimen.scan{-webkit-mask-image:none;mask-image:none;opacity:1}.scan-plane,.connector,.bloom-ring{display:none}.panel{opacity:1;transform:none}.reveal{opacity:1;transform:none}}
5858
</style>
@@ -139,23 +139,27 @@
139139
}
140140
function ping(freq=310){if(!audioCtx)return;const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.frequency.value=freq;o.type='sine';g.gain.setValueAtTime(.025,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(.0001,audioCtx.currentTime+.22);o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+.23)}
141141
document.querySelectorAll('.specimen-btn').forEach(b=>b.addEventListener('click',()=>setSpecimen(+b.dataset.index,true)));
142-
function syncVideos(force=false){
142+
function syncVideos(){
143143
if(monoVideo.readyState<2||colorVideo.readyState<2||!monoVideo.duration||!colorVideo.duration)return;
144144
const delta=monoVideo.currentTime-colorVideo.currentTime, drift=Math.abs(delta);
145-
if((force||drift>.12)&&!colorVideo.seeking){colorVideo.currentTime=monoVideo.currentTime;colorVideo.playbackRate=monoVideo.playbackRate;}
145+
if(drift>.12&&monoVideo.readyState>=3&&colorVideo.readyState>=3&&!colorVideo.seeking){colorVideo.currentTime=monoVideo.currentTime;colorVideo.playbackRate=monoVideo.playbackRate;}
146146
else colorVideo.playbackRate=monoVideo.playbackRate+Math.max(-.08,Math.min(.08,delta));
147147
if(monoVideo.paused!==colorVideo.paused){monoVideo.paused?colorVideo.pause():colorVideo.play().catch(()=>{});}
148148
}
149149
const whenReady=(video)=>video.readyState>=2?Promise.resolve():new Promise(resolve=>video.addEventListener('loadeddata',resolve,{once:true}));
150-
async function startVideos(){
151-
try{
150+
let startPromise=null;
151+
function startVideos(){
152+
if(reduce){monoVideo.pause();colorVideo.pause();try{monoVideo.currentTime=0;colorVideo.currentTime=0}catch(_){}return Promise.resolve();}
153+
if(!monoVideo.paused&&!colorVideo.paused&&monoVideo.readyState>=2&&colorVideo.readyState>=2)return Promise.resolve();
154+
if(startPromise)return startPromise;
155+
startPromise=(async()=>{
152156
monoVideo.defaultMuted=colorVideo.defaultMuted=true;
153157
await Promise.all([whenReady(monoVideo),whenReady(colorVideo)]);
154-
if(reduce){monoVideo.pause();colorVideo.pause();try{monoVideo.currentTime=0;colorVideo.currentTime=0}catch(_){}return;}
155-
colorVideo.currentTime=monoVideo.currentTime;
158+
if(Math.abs(monoVideo.currentTime-colorVideo.currentTime)>.08)colorVideo.currentTime=monoVideo.currentTime;
156159
await Promise.all([monoVideo.play(),colorVideo.play()]);
157-
syncVideos(true);
158-
}catch(_){/* posters remain visible until browser permits playback */}
160+
syncVideos();
161+
})().catch(()=>{/* posters remain visible until browser permits playback */}).finally(()=>{startPromise=null});
162+
return startPromise;
159163
}
160164
// Velocity-reactive loop: the mechanism spins faster while the user scrolls, easing back to 1× at rest.
161165
let velRate=1,lastScrollT=0,lastScrollYv=scrollY,heroVisible=true;
@@ -167,7 +171,7 @@
167171
}
168172
syncVideos();
169173
}
170-
monoVideo.addEventListener('playing',()=>syncVideos(true));monoVideo.addEventListener('timeupdate',()=>syncVideos());monoVideo.addEventListener('seeking',()=>syncVideos(true));
174+
monoVideo.addEventListener('playing',syncVideos);monoVideo.addEventListener('timeupdate',syncVideos);monoVideo.addEventListener('seeking',syncVideos);
171175
if('requestVideoFrameCallback' in monoVideo){const frameSync=()=>{tick();monoVideo.requestVideoFrameCallback(frameSync)};monoVideo.requestVideoFrameCallback(frameSync)}else setInterval(tick,80);
172176
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&heroVisible)startVideos()});
173177
addEventListener('pageshow',startVideos,{once:true});

examples/kern-calibration/test-immersive.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
99

1010
try {
1111
await page.goto(pathToFileURL(join(here, 'index.html')).href);
12-
await page.waitForTimeout(1800);
12+
await page.waitForFunction(() => [...document.querySelectorAll('video.specimen')].every((video) => video.readyState >= 2 && !video.paused), { timeout: 5000 });
1313

1414
const initial = await page.evaluate(() => {
1515
const stage = document.querySelector('.specimen-stage');
@@ -55,8 +55,11 @@ try {
5555
assert.ok(t1[1] - t0[1] > 0.7, 'colour video did not visibly advance');
5656
assert.ok(Math.abs(t1[0] - t1[1]) < 0.08, `video drift ${Math.abs(t1[0] - t1[1])}s exceeds boundary tolerance`);
5757

58-
await page.evaluate(() => scrollTo(0, document.querySelector('.hero-track').offsetHeight * 0.48));
59-
await page.waitForTimeout(250);
58+
await page.evaluate(() => {
59+
document.documentElement.style.scrollBehavior = 'auto';
60+
scrollTo(0, document.querySelector('.hero-track').offsetHeight * 0.48);
61+
});
62+
await page.waitForTimeout(500);
6063
const revealed = await page.evaluate(() => getComputedStyle(document.querySelector('video.specimen.scan')).getPropertyValue('--b0').trim());
6164
assert.notEqual(revealed, initial.bloom, 'scroll must open the colour blooms');
6265
assert.ok(parseFloat(revealed) > 0, `bloom radius ${revealed} did not grow on scroll`);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { chromium } from 'playwright-core';
2+
import assert from 'node:assert/strict';
3+
import { fileURLToPath, pathToFileURL } from 'node:url';
4+
import { dirname, join } from 'node:path';
5+
6+
const here = dirname(fileURLToPath(import.meta.url));
7+
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome' });
8+
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
9+
10+
try {
11+
await page.goto(pathToFileURL(join(here, 'index.html')).href, { waitUntil: 'load' });
12+
await page.waitForTimeout(2500);
13+
14+
const state = await page.evaluate(() => {
15+
const wrap = document.querySelector('.specimen-wrap').getBoundingClientRect();
16+
const zoom = document.querySelector('.panel-right').getBoundingClientRect();
17+
const videos = [...document.querySelectorAll('video.specimen')];
18+
const overlapWidth = Math.max(0, Math.min(wrap.right, zoom.right) - Math.max(wrap.left, zoom.left));
19+
const overlapHeight = Math.max(0, Math.min(wrap.bottom, zoom.bottom) - Math.max(wrap.top, zoom.top));
20+
return {
21+
wrap: { top: wrap.top, right: wrap.right, bottom: wrap.bottom, left: wrap.left },
22+
zoom: { top: zoom.top, right: zoom.right, bottom: zoom.bottom, left: zoom.left, area: zoom.width * zoom.height },
23+
overlapArea: overlapWidth * overlapHeight,
24+
videos: videos.map((video) => ({
25+
paused: video.paused,
26+
readyState: video.readyState,
27+
currentTime: video.currentTime,
28+
})),
29+
};
30+
});
31+
32+
for (const video of state.videos) {
33+
assert.equal(video.paused, false, 'mobile hero video must autoplay');
34+
assert.ok(video.readyState >= 2, `mobile hero video is not decodable: readyState ${video.readyState}`);
35+
}
36+
assert.ok(state.overlapArea >= state.zoom.area * 0.8, `macro optics must visually share the mobile hero: overlap ${state.overlapArea}/${state.zoom.area}`);
37+
38+
const before = state.videos.map((video) => video.currentTime);
39+
await page.waitForTimeout(900);
40+
const after = await page.evaluate(() => [...document.querySelectorAll('video.specimen')].map((video) => video.currentTime));
41+
assert.ok(after[0] - before[0] > 0.5, 'mobile monochrome video did not advance');
42+
assert.ok(after[1] - before[1] > 0.5, 'mobile colour video did not advance');
43+
44+
console.log('PASS mobile autoplay and integrated macro-optics contract');
45+
} finally {
46+
await browser.close();
47+
}

0 commit comments

Comments
 (0)