const video = document.querySelector('#hero-video'); const toggle = document.querySelector('#video-toggle'); const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); function updateVideoButton() { if (video.ended) { toggle.innerHTML = '↻ Replay video'; toggle.setAttribute('aria-label', 'Replay background video'); return; } toggle.innerHTML = video.paused ? '▶ Play video' : 'Ⅱ Pause video'; toggle.setAttribute('aria-label', video.paused ? 'Play background video' : 'Pause background video'); } toggle.addEventListener('click', () => { if (video.paused) video.play().catch(updateVideoButton); else video.pause(); }); video.addEventListener('play', updateVideoButton); video.addEventListener('pause', updateVideoButton); video.addEventListener('ended', updateVideoButton); function applyMotionPreference() { if (reducedMotion.matches) { video.autoplay = false; video.pause(); } else if (!video.ended) video.play().catch(updateVideoButton); updateVideoButton(); } reducedMotion.addEventListener('change', applyMotionPreference); applyMotionPreference(); const sectionMenu = document.querySelector('.section-menu'); const sectionLabel = document.querySelector('#current-section'); const sectionLinks = [...document.querySelectorAll('.section-menu-links a')]; const sections = sectionLinks.map(link => document.querySelector(link.hash)); let scrollPending = false; function updateCurrentSection() { const threshold = document.querySelector('.header').offsetHeight + 100; let index = 0; sections.forEach((section, i) => { if (section.getBoundingClientRect().top <= threshold) index = i; }); if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2) index = sections.length - 1; sectionLabel.textContent = sectionLinks[index].textContent; sectionLinks.forEach((link, i) => { if (i === index) link.setAttribute('aria-current', 'location'); else link.removeAttribute('aria-current'); }); scrollPending = false; } window.addEventListener('scroll', () => { if (!scrollPending) { scrollPending = true; requestAnimationFrame(updateCurrentSection); } }, { passive: true }); window.addEventListener('resize', updateCurrentSection); window.addEventListener('pageshow', updateCurrentSection); sectionLinks.forEach(link => link.addEventListener('click', () => { sectionMenu.open = false; })); document.addEventListener('click', event => { if (!sectionMenu.contains(event.target)) sectionMenu.open = false; }); document.addEventListener('keydown', event => { if (event.key === 'Escape' && sectionMenu.open) { sectionMenu.open = false; sectionMenu.querySelector('summary').focus(); } }); updateCurrentSection(); document.querySelector('#copy-citation').addEventListener('click', async () => { const status = document.querySelector('#copy-status'); try { await navigator.clipboard.writeText(document.querySelector('#citation-text').textContent); status.textContent = 'Citation copied.'; } catch { status.textContent = 'Please select and copy the citation above.'; } }); // Extend narrow strips of the live frame into the fading margins. const edgeCanvas = document.querySelector('.video-edge-extension'); const edgeContext = edgeCanvas.getContext('2d'); let lastEdgeFrame = -Infinity; function drawEdgeExtension() { if (!edgeContext || video.readyState < 2) return; const width = video.videoWidth; const height = video.videoHeight; const strip = Math.max(1, Math.round(width * .012)); const frame = video.getBoundingClientRect(); const canvas = edgeCanvas.getBoundingClientRect(); const scale = edgeCanvas.width / canvas.width; const left = (frame.left - canvas.left) * scale; const frameWidth = frame.width * scale; const right = left + frameWidth; edgeContext.clearRect(0, 0, edgeCanvas.width, edgeCanvas.height); edgeContext.drawImage(video, 0, 0, width, height, left, 0, frameWidth, 540); if (left > 0) edgeContext.drawImage(video, 0, 0, strip, height, 0, 0, left, 540); if (right < edgeCanvas.width) edgeContext.drawImage(video, width - strip, 0, strip, height, right, 0, edgeCanvas.width - right, 540); } if ('requestVideoFrameCallback' in video) { const updateEdges = (now) => { if (now - lastEdgeFrame > 80 || video.paused || video.ended) { drawEdgeExtension(); lastEdgeFrame = now; } video.requestVideoFrameCallback(updateEdges); }; video.requestVideoFrameCallback(updateEdges); } else video.addEventListener('timeupdate', drawEdgeExtension); ['loadeddata', 'seeked', 'pause', 'ended'].forEach(event => video.addEventListener(event, drawEdgeExtension)); drawEdgeExtension(); window.addEventListener('resize', drawEdgeExtension); const comparison = document.querySelector('.benchmark-comparison'); const benchmarkRows = [...document.querySelectorAll('.benchmark-row')]; const benchmarkTooltip = document.querySelector('#benchmark-tooltip'); let activeBenchmarkRow = null; const benchmarkDetails = new Map(benchmarkRows.map(row => [row, JSON.parse(row.dataset.benchmarkDetail)])); function positionBenchmarkTooltip(event) { if (!activeBenchmarkRow || benchmarkTooltip.hidden) return; const anchor = activeBenchmarkRow.getBoundingClientRect(); const pointer = event && event.type.startsWith('pointer') && event.pointerType !== 'touch'; let left = pointer ? event.clientX + 16 : anchor.left; let top = pointer ? event.clientY + 18 : anchor.bottom + 10; const bounds = benchmarkTooltip.getBoundingClientRect(); if (top + bounds.height > innerHeight - 12) top = (pointer ? event.clientY : anchor.top) - bounds.height - 12; benchmarkTooltip.style.left = Math.max(12, Math.min(left, innerWidth - bounds.width - 12)) + 'px'; benchmarkTooltip.style.top = Math.max(12, top) + 'px'; } function hideBenchmarkTooltip() { benchmarkTooltip.hidden = true; comparison.classList.remove('has-highlight'); benchmarkRows.forEach(row => { row.classList.remove('is-highlighted'); row.removeAttribute('aria-describedby'); }); activeBenchmarkRow = null; } function showBenchmarkTooltip(row, event) { const detail = benchmarkDetails.get(row); activeBenchmarkRow = row; benchmarkTooltip.replaceChildren(); function add(tag, text, className) { const element = document.createElement(tag); element.textContent = text; if (className) element.className = className; benchmarkTooltip.appendChild(element); return element; } add('h4', detail.method); add('p', detail.benchmark, 'tip-benchmark'); add('p', detail.value + '%', 'tip-value'); if (detail.details.length) { const list = add('dl', ''); detail.details.forEach(([label, value]) => { const term = document.createElement('dt'); const result = document.createElement('dd'); term.textContent = label; result.textContent = value + '%'; list.append(term, result); }); } add('p', detail.comparison, 'tip-comparison'); if (detail.note) add('p', detail.note, 'tip-note'); add('p', detail.source, 'tip-source'); comparison.classList.add('has-highlight'); benchmarkRows.forEach(other => { other.classList.toggle('is-highlighted', other === row); other.removeAttribute('aria-describedby'); }); row.setAttribute('aria-describedby', 'benchmark-tooltip'); benchmarkTooltip.hidden = false; positionBenchmarkTooltip(event); } benchmarkRows.forEach(row => { row.addEventListener('pointerenter', event => { if (event.pointerType !== 'touch') showBenchmarkTooltip(row, event); }); row.addEventListener('pointermove', positionBenchmarkTooltip); row.addEventListener('pointerleave', event => { if (event.pointerType !== 'touch') hideBenchmarkTooltip(); }); row.addEventListener('focus', event => showBenchmarkTooltip(row, event)); row.addEventListener('blur', hideBenchmarkTooltip); row.addEventListener('click', event => showBenchmarkTooltip(row, event)); }); document.addEventListener('pointerdown', event => { if (!event.target.closest('.benchmark-row')) hideBenchmarkTooltip(); }); document.addEventListener('keydown', event => { if (event.key === 'Escape') hideBenchmarkTooltip(); }); window.addEventListener('scroll', hideBenchmarkTooltip, { passive: true }); window.addEventListener('resize', hideBenchmarkTooltip); // Lazy-load visible previews; expanded playback always uses the full 4K file. const demoTrack = document.querySelector('#demo-track'); const demoDialog = document.querySelector('.demo-dialog'); const expandedDemo = document.querySelector('#expanded-demo'); const demoStatus = document.querySelector('.demo-playback-status'); const previewToggle = document.querySelector('#preview-toggle'); const previews = [...demoTrack.querySelectorAll('video')]; const visiblePreviews = new Set(); let previewsPaused = reducedMotion.matches; let demoOpener = null; function updatePreviews() { previewToggle.textContent = previewsPaused ? 'Play previews' : 'Pause previews'; previews.forEach(preview => { if (visiblePreviews.has(preview) && !previewsPaused && !demoDialog.open && !document.hidden && !preview.ended) { if (!preview.getAttribute('src')) preview.src = preview.dataset.src; preview.play().catch(() => {}); } else preview.pause(); }); } const previewObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) visiblePreviews.add(entry.target); else visiblePreviews.delete(entry.target); }); updatePreviews(); }, { threshold: .25 }); previews.forEach(preview => previewObserver.observe(preview)); previewToggle.addEventListener('click', () => { previewsPaused = !previewsPaused; if (!previewsPaused) previews.forEach(preview => { if (preview.ended) preview.currentTime = 0; }); updatePreviews(); }); reducedMotion.addEventListener('change', () => { previewsPaused = reducedMotion.matches; updatePreviews(); }); document.addEventListener('visibilitychange', updatePreviews); updatePreviews(); const galleryPrev = document.querySelector('.gallery-prev'); const galleryNext = document.querySelector('.gallery-next'); function updateGalleryArrows() { galleryPrev.disabled = demoTrack.scrollLeft < 2; galleryNext.disabled = demoTrack.scrollLeft + demoTrack.clientWidth >= demoTrack.scrollWidth - 2; } function scrollGallery(direction) { const cards = demoTrack.querySelectorAll('.demo-card'); const step = cards[1].offsetLeft - cards[0].offsetLeft; demoTrack.scrollBy({ left: direction * step, behavior: reducedMotion.matches ? 'instant' : 'smooth' }); } galleryPrev.addEventListener('click', () => scrollGallery(-1)); galleryNext.addEventListener('click', () => scrollGallery(1)); demoTrack.addEventListener('scroll', updateGalleryArrows, { passive: true }); window.addEventListener('resize', updateGalleryArrows); updateGalleryArrows(); demoTrack.querySelectorAll('.demo-open').forEach(button => button.addEventListener('click', () => { demoOpener = button; document.querySelector('#demo-dialog-title').textContent = button.dataset.title; demoStatus.textContent = ''; expandedDemo.poster = button.querySelector('video').poster; expandedDemo.src = button.dataset.video; demoDialog.showModal(); updatePreviews(); expandedDemo.play().catch(() => { if (demoDialog.open) demoStatus.textContent = 'Press play to start the video.'; }); })); document.querySelector('.demo-close').addEventListener('click', () => demoDialog.close()); demoDialog.addEventListener('click', event => { const rect = demoDialog.getBoundingClientRect(); if (event.target === demoDialog && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) demoDialog.close(); }); demoDialog.addEventListener('close', () => { expandedDemo.pause(); expandedDemo.removeAttribute('src'); expandedDemo.load(); demoOpener?.focus({ preventScroll: true }); updatePreviews(); }); expandedDemo.addEventListener('error', () => { if (demoDialog.open && expandedDemo.getAttribute('src')) demoStatus.textContent = 'The video could not be loaded. Please close it and try again.'; }); // Play inline as soon as the method animation enters the viewport. const methodVideo = document.querySelector('#method-video'); const methodToggle = document.querySelector('#method-toggle'); let methodVisible = false; let methodManuallyPaused = false; methodVideo.muted = true; function updateMethodToggle() { methodToggle.textContent = methodVideo.ended ? 'Replay animation' : methodVideo.paused ? 'Play animation' : 'Pause animation'; } function playVisibleMethod() { if (methodVisible && !document.hidden && !methodManuallyPaused && !methodVideo.ended) { methodVideo.play().catch(updateMethodToggle); } else methodVideo.pause(); } const methodObserver = new IntersectionObserver(entries => { entries.forEach(entry => { const entering = entry.isIntersecting && !methodVisible; methodVisible = entry.isIntersecting; if (entering) { methodManuallyPaused = false; if (methodVideo.ended) methodVideo.currentTime = 0; } playVisibleMethod(); }); }, { threshold: .15 }); methodObserver.observe(methodVideo); methodVideo.addEventListener('canplay', playVisibleMethod); ['play', 'pause', 'ended'].forEach(event => methodVideo.addEventListener(event, updateMethodToggle)); methodToggle.addEventListener('click', () => { if (methodVideo.paused) { methodManuallyPaused = false; if (methodVideo.ended) methodVideo.currentTime = 0; methodVideo.play().catch(updateMethodToggle); } else { methodManuallyPaused = true; methodVideo.pause(); } }); document.addEventListener('visibilitychange', playVisibleMethod); // Egocentric examples play independently when scrolled into view. document.querySelectorAll('.ego-example').forEach(figure => { const clip = figure.querySelector('video'); const button = figure.querySelector('.ego-toggle'); let visible = false; let manuallyPaused = false; const updateLabel = () => { button.textContent = clip.ended ? 'Replay video' : clip.paused ? 'Play video' : 'Pause video'; }; const updatePlayback = () => { if (visible && !document.hidden && !manuallyPaused && !clip.ended) { if (!clip.getAttribute('src')) clip.src = clip.dataset.egoSrc; clip.muted = true; clip.play().catch(updateLabel); } else clip.pause(); }; new IntersectionObserver(entries => { entries.forEach(entry => { const entering = entry.isIntersecting && !visible; visible = entry.isIntersecting; if (entering) { manuallyPaused = false; if (clip.ended) clip.currentTime = 0; } updatePlayback(); }); }, { threshold: .2 }).observe(clip); button.addEventListener('click', () => { manuallyPaused = !clip.paused; if (clip.ended) clip.currentTime = 0; updatePlayback(); }); ['play', 'pause', 'ended'].forEach(event => clip.addEventListener(event, updateLabel)); document.addEventListener('visibilitychange', updatePlayback); }); // Keep the contents rail in document order, with one active subsection. const contentsMenu = document.querySelector('.contents-menu'); const sidebarLinks = [...document.querySelectorAll('.sidebar-nav a')]; const sidebarTargets = sidebarLinks.map(link => document.querySelector(link.hash)); const desktopContents = matchMedia('(min-width: 1101px)'); function setContentsMode() { contentsMenu.open = desktopContents.matches; } setContentsMode(); desktopContents.addEventListener('change', setContentsMode); let contentsPending = false; function updateContents() { const threshold = document.querySelector('.header').offsetHeight + 110; let active = 0; sidebarTargets.forEach((target, index) => { if (target.getBoundingClientRect().top <= threshold) active = index; }); if (scrollY + innerHeight >= document.documentElement.scrollHeight - 2) active = sidebarLinks.length - 1; sidebarLinks.forEach((link, index) => { if (index === active) link.setAttribute('aria-current', 'location'); else link.removeAttribute('aria-current'); }); document.querySelectorAll('.sidebar-nav > ul > li').forEach(item => item.classList.toggle('is-active-group', item.contains(sidebarLinks[active]))); contentsPending = false; } window.addEventListener('scroll', () => { if (!contentsPending) { contentsPending = true; requestAnimationFrame(updateContents); } }, { passive: true }); window.addEventListener('resize', updateContents); window.addEventListener('pageshow', updateContents); sidebarLinks.forEach(link => link.addEventListener('click', () => { if (!desktopContents.matches) contentsMenu.open = false; })); updateContents(); // An illustrative, orthonormal DCT of one latent dimension (not experimental data). (() => { const panel = document.querySelector('.spectral-explorer'); if (!panel) return; const stages = [...panel.querySelectorAll('.spectral-stage')]; const slider = panel.querySelector('#spectral-k'); const play = panel.querySelector('.spectral-play'); const count = 16; const signal = Array.from({length: count}, (_, n) => .6 * Math.cos(Math.PI * (n + .5) / count) + .3 * Math.cos(2 * Math.PI * (n + .5) / count) + .16 * Math.cos(9 * Math.PI * (n + .5) / count) + .09 * Math.cos(13 * Math.PI * (n + .5) / count)); const coefficients = signal.map((_, k) => Math.sqrt((k === 0 ? 1 : 2) / count) * signal.reduce((sum, value, n) => sum + value * Math.cos(Math.PI * (n + .5) * k / count), 0)); const details = [ ['Encode interactions over time', 'WING-LAM encodes successive frame pairs. The resulting sequence contains both slowly varying structure and rapid temporal changes.'], ['Separate temporal frequencies', 'A temporal DCT expresses each latent dimension as frequency coefficients, ordered from slow to fast variation. No coefficients have been discarded at this stage.'], ['Retain the shared slow structure', 'Only the first K coefficients form the guidance target. At inference, a predictor estimates these coefficients from the current context; the action model uses them to generate fine-grained controls.'] ]; let active = 0, running = false, visible = false, timer; const axis = ''; panel.querySelector('.spectral-signal').innerHTML = axis + ``; function drawSpectrum(selector, truncate) { panel.querySelector(selector).innerHTML = axis + coefficients.map((c,k) => ``).join('') + 'lowhigh'; } drawSpectrum('.spectral-spectrum', false); drawSpectrum('.spectral-retained', true); function select(index) { active = index; stages.forEach((button, i) => button.setAttribute('aria-pressed', String(i === index))); panel.querySelector('.spectral-detail-title').textContent = details[index][0]; panel.querySelector('.spectral-detail-copy').textContent = details[index][1]; } function schedule() { clearInterval(timer); if (running && visible && !document.hidden) timer = setInterval(() => select((active + 1) % stages.length), 2000); play.textContent = running ? 'Pause sequence' : 'Play sequence'; play.setAttribute('aria-pressed', String(running)); } stages.forEach((button, index) => button.addEventListener('click', () => {running = false; schedule(); select(index);})); slider.addEventListener('input', () => { running = false; schedule(); select(2); panel.querySelector('#spectral-k-value').value = `K = ${slider.value} / ${count}`; drawSpectrum('.spectral-retained', true); }); play.addEventListener('click', () => {running = !running; schedule();}); let entered = false; new IntersectionObserver(entries => { visible = entries[0].isIntersecting; if (visible && !entered) {entered = true; running = !matchMedia('(prefers-reduced-motion: reduce)').matches;} schedule(); }, {threshold: .25}).observe(panel); document.addEventListener('visibilitychange', schedule); })(); // Frame-pair presentation: preserve the original decoded trajectories as images. (() => { const gallery = document.querySelector('.interaction-gallery'); if (!gallery) return; const stages = [...gallery.querySelectorAll('.interaction-stages button')]; const examples = [...gallery.querySelectorAll('.interaction-images')]; const play = gallery.querySelector('.interaction-play'); const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); let active = 0, running = !reducedMotion.matches, visible = false, timer; function select(index) { active = index; stages.forEach((button, i) => button.setAttribute('aria-pressed', String(i === index))); examples.forEach(example => [...example.children].forEach((img, i) => { img.hidden = i !== index; })); } function schedule() { clearTimeout(timer); play.textContent = running ? 'Pause sequence' : 'Play sequence'; play.setAttribute('aria-pressed', String(running)); if (running && visible && !document.hidden) timer = setTimeout(() => { select((active + 1) % 3); schedule(); }, active === 2 ? 3000 : 2000); } stages.forEach((button, index) => button.addEventListener('click', () => { running = false; select(index); schedule(); })); play.addEventListener('click', () => { running = !running; schedule(); }); new IntersectionObserver(entries => { visible = entries[0].isIntersecting; schedule(); }, {threshold: .15}).observe(gallery); document.addEventListener('visibilitychange', schedule); reducedMotion.addEventListener('change', () => { if (reducedMotion.matches) running = false; schedule(); }); schedule(); })(); // Load only the selected original clip; thumbnails keep the gallery lightweight. (() => { const gallery = document.querySelector('.generalization-gallery'); if (!gallery) return; const tasks = { insertion: { title: 'Battery Insertion', types: ['app', 'spatial'] }, assembly: { title: 'Battery Assembly', types: ['app', 'bg', 'spatial'] }, pack3: { title: 'Pack Objects', types: ['app', 'bg', 'spatial', 'geo'] }, stack: { title: 'Stack Cups', types: ['app', 'bg', 'spatial', 'geo'] } }; const labels = { app: 'Appearance', bg: 'Background', spatial: 'Spatial', geo: 'Geometry' }; const tabs = [...gallery.querySelectorAll('[data-task]')]; const previews = gallery.querySelector('.gen-previews'); const video = gallery.querySelector('.gen-video'); const error = gallery.querySelector('.gen-error'); let task = 'pack3', type = 'app', visible = false, loaded = false, resume = true; function playIfVisible() { if (!visible || document.hidden) { video.pause(); return; } if (!loaded) { video.src = `https://mikuz12.github.io/wing/gen/${task}_${type}.MP4`; loaded = true; video.load(); } if (resume) video.play().catch(() => {}); } function selectClip(next) { video.pause(); type = next; loaded = false; resume = true; error.hidden = true; error.querySelector('a').href = `https://mikuz12.github.io/wing/gen/${task}_${type}.MP4`; video.poster = `https://mikuz12.github.io/wing/assets/generalization/${task}_${type}.jpg`; video.setAttribute('aria-label', `${tasks[task].title}: ${labels[type]} generalization`); gallery.querySelector('.gen-title').textContent = tasks[task].title; gallery.querySelector('.gen-type').textContent = `${labels[type]} generalization`; [...previews.children].forEach(button => button.setAttribute('aria-pressed', String(button.dataset.type === type))); playIfVisible(); } function selectTask(next) { task = next; tabs.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.task === task))); previews.replaceChildren(); tasks[task].types.forEach(key => { const button = document.createElement('button'); button.type = 'button'; button.className = 'gen-preview'; button.dataset.type = key; button.setAttribute('aria-label', `Play ${tasks[task].title}: ${labels[key]} generalization`); const img = document.createElement('img'); img.src = `https://mikuz12.github.io/wing/assets/generalization/${task}_${key}.jpg`; img.alt = ''; img.width = 480; img.height = 270; const label = document.createElement('span'); label.textContent = labels[key]; button.append(img, label); button.addEventListener('click', () => selectClip(key)); previews.append(button); }); previews.scrollTop = 0; previews.scrollLeft = 0; selectClip(tasks[task].types.includes(type) ? type : tasks[task].types[0]); } tabs.forEach(button => button.addEventListener('click', () => selectTask(button.dataset.task))); video.addEventListener('error', () => { error.hidden = false; }); new IntersectionObserver(entries => { const next = entries[0].isIntersecting; if (!next && visible && loaded) resume = !video.paused; visible = next; playIfVisible(); }, {threshold: .15}).observe(video); document.addEventListener('visibilitychange', () => { if (document.hidden && visible && loaded) resume = !video.paused; playIfVisible(); }); selectTask(task); })(); // Reveal editorial blocks once as they enter the reading viewport. (() => { const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); if (reducedMotion.matches || !('IntersectionObserver' in window)) return; const blocks = [...document.querySelectorAll('.reading-content > section > :not(section):not(.method-story), .reading-content > section > .benchmark-comparison, .reading-content > section > .demo-gallery, .method-chapter > *, .experiment-chapter > *')]; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) reveal(entry.target); }); }, {threshold: 0, rootMargin: '0px 0px -40px 0px'}); function reveal(block) { block.classList.remove('scroll-reveal-pending'); observer.unobserve(block); } blocks.forEach(block => { // Leave the current viewport and restored scroll positions immediately readable. if (block.getBoundingClientRect().top < innerHeight) return; block.classList.add('scroll-reveal', 'scroll-reveal-pending'); observer.observe(block); }); document.addEventListener('focusin', event => { const block = event.target.closest('.scroll-reveal-pending'); if (block) reveal(block); }); reducedMotion.addEventListener('change', () => { if (reducedMotion.matches) { blocks.forEach(reveal); observer.disconnect(); } }); window.addEventListener('beforeprint', () => { blocks.forEach(reveal); observer.disconnect(); }); })();