/* ======================================== G1 Moves — Showcase App ======================================== */ (function () { 'use strict'; // ------------------------------------ // State // ------------------------------------ let allClips = []; let baseUrl = ''; let activeCategory = 'all'; const isMobile = window.matchMedia('(max-width: 480px)').matches; const PAGE_SIZE = 12; let currentPage = 0; // ------------------------------------ // DOM refs // ------------------------------------ const grid = document.getElementById('clip-grid'); const emptyState = document.getElementById('empty-state'); const modal = document.getElementById('pipeline-modal'); // ------------------------------------ // Init // ------------------------------------ async function init() { try { const res = await fetch('data.json'); const data = await res.json(); allClips = data.clips; baseUrl = data.base_url; updateStats(data.stats); filterAndRender(); setupCategoryButtons(); setupModal(); setupScrollReveal(); } catch (err) { grid.innerHTML = '

Failed to load data.json

'; } } // ------------------------------------ // Stats // ------------------------------------ function updateStats(stats) { setText('stat-total', stats.total); setText('stat-dance', stats.dance); setText('stat-karate', stats.karate); setText('stat-bonus', stats.bonus); setText('stat-policies', stats.policies); } function setText(id, value) { const el = document.getElementById(id); if (el) el.textContent = value; } // ------------------------------------ // Filtering & Rendering // ------------------------------------ let filteredClips = []; let carouselIndex = 0; function filterAndRender() { filteredClips = activeCategory === 'all' ? allClips.slice() : allClips.filter(c => c.category === activeCategory); // Prioritize trained models first filteredClips.sort(function (a, b) { if (a.has_policy && !b.has_policy) return -1; if (!a.has_policy && b.has_policy) return 1; return 0; }); grid.innerHTML = ''; currentPage = 0; emptyState.classList.toggle('visible', filteredClips.length === 0); // Remove any existing pagination var oldNav = document.getElementById('page-nav'); if (oldNav) oldNav.remove(); if (isMobile) { carouselIndex = 0; renderCarousel(); } else { renderPage(); } } function totalPages() { return Math.ceil(filteredClips.length / PAGE_SIZE); } function renderPage() { grid.innerHTML = ''; var start = currentPage * PAGE_SIZE; var end = Math.min(start + PAGE_SIZE, filteredClips.length); var pageClips = filteredClips.slice(start, end); pageClips.forEach(function (clip, i) { var card = createCard(clip, start + i); grid.appendChild(card); }); observeLazyImages(); observeCardReveal(); updatePageNav(); // Scroll grid into view on page change if (currentPage > 0) { grid.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } function updatePageNav() { var oldNav = document.getElementById('page-nav'); if (oldNav) oldNav.remove(); var pages = totalPages(); if (pages <= 1) return; var nav = document.createElement('div'); nav.id = 'page-nav'; nav.className = 'page-nav'; // Prev button var prevBtn = document.createElement('button'); prevBtn.className = 'page-nav-btn'; prevBtn.disabled = currentPage === 0; prevBtn.innerHTML = ''; prevBtn.setAttribute('aria-label', 'Previous page'); prevBtn.addEventListener('click', function () { if (currentPage > 0) { currentPage--; renderPage(); } }); // Counter var counter = document.createElement('span'); counter.className = 'page-nav-counter'; counter.textContent = (currentPage + 1) + ' / ' + pages; // Next button var nextBtn = document.createElement('button'); nextBtn.className = 'page-nav-btn'; nextBtn.disabled = currentPage >= pages - 1; nextBtn.innerHTML = ''; nextBtn.setAttribute('aria-label', 'Next page'); nextBtn.addEventListener('click', function () { if (currentPage < pages - 1) { currentPage++; renderPage(); } }); nav.appendChild(prevBtn); nav.appendChild(counter); nav.appendChild(nextBtn); grid.parentNode.insertBefore(nav, grid.nextSibling); } // ------------------------------------ // Mobile carousel // ------------------------------------ function renderCarousel() { grid.innerHTML = ''; if (!filteredClips.length) return; var wrap = document.createElement('div'); wrap.className = 'carousel'; // Counter var counter = document.createElement('div'); counter.className = 'carousel-counter'; wrap.appendChild(counter); // Card container var cardWrap = document.createElement('div'); cardWrap.className = 'carousel-card-wrap'; wrap.appendChild(cardWrap); // Nav buttons var nav = document.createElement('div'); nav.className = 'carousel-nav'; var prevBtn = document.createElement('button'); prevBtn.className = 'carousel-btn'; prevBtn.innerHTML = '‹'; prevBtn.setAttribute('aria-label', 'Previous'); var nextBtn = document.createElement('button'); nextBtn.className = 'carousel-btn'; nextBtn.innerHTML = '›'; nextBtn.setAttribute('aria-label', 'Next'); nav.appendChild(prevBtn); nav.appendChild(nextBtn); wrap.appendChild(nav); grid.appendChild(wrap); function showCard(idx) { carouselIndex = idx; var clip = filteredClips[idx]; counter.textContent = (idx + 1) + ' / ' + filteredClips.length; prevBtn.disabled = idx === 0; nextBtn.disabled = idx === filteredClips.length - 1; cardWrap.innerHTML = ''; var card = createCard(clip, idx); card.classList.add('visible'); cardWrap.appendChild(card); // Load preview media immediately var lazyMedia = card.querySelector('img[data-src], video[data-src]'); if (lazyMedia && lazyMedia.dataset.src) { lazyMedia.src = lazyMedia.dataset.src; lazyMedia.removeAttribute('data-src'); } } prevBtn.addEventListener('click', function () { if (carouselIndex > 0) showCard(carouselIndex - 1); }); nextBtn.addEventListener('click', function () { if (carouselIndex < filteredClips.length - 1) showCard(carouselIndex + 1); }); // Swipe support var touchStartX = 0; wrap.addEventListener('touchstart', function (e) { touchStartX = e.touches[0].clientX; }, { passive: true }); wrap.addEventListener('touchend', function (e) { var dx = e.changedTouches[0].clientX - touchStartX; if (Math.abs(dx) > 50) { if (dx < 0 && carouselIndex < filteredClips.length - 1) showCard(carouselIndex + 1); if (dx > 0 && carouselIndex > 0) showCard(carouselIndex - 1); } }, { passive: true }); showCard(0); } // ------------------------------------ // Card creation // ------------------------------------ function createCard(clip, index) { const card = document.createElement('div'); card.className = 'clip-card'; card.dataset.clipId = clip.id; var viewerUrl = 'viewer.html?clip=' + encodeURIComponent(clip.id) + '&category=' + encodeURIComponent(clip.category); // Get preview media for cards — use uploaded policy MP4 renders from Space var stageData = clip.stages.policy || clip.stages.training || clip.stages.retarget || clip.stages.capture; var mediaSrc = stageData ? cardMediaUrl(stageData, clip) : null; var isVideo = mediaSrc && mediaSrc.endsWith('.mp4'); var mediaHtml; if (mediaSrc) { mediaHtml = isVideo ? '' : '' + escHtml(clip.name) + ''; } else { mediaHtml = '
Not available
'; } card.innerHTML = '
' + mediaHtml + '
' + formatDuration(clip.duration) + '
' + (clip.has_policy ? '
TRAINED
' : '') + '
' + '
' + '
' + escHtml(clip.name) + '
' + '
' + '' + clip.category + '' + '' + '' + clip.performer + '' + '
' + '
' + (clip.has_policy ? '' + '' + 'View' + '' : '') + '
' + '
'; // Media click → open modal card.querySelector('.card-media').addEventListener('click', function () { openModal(clip); }); return card; } // ------------------------------------ // Lazy loading // ------------------------------------ var lazyObserver = null; function observeLazyImages() { if (!lazyObserver) { lazyObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { var el = entry.target; if (el.dataset.src) { el.src = el.dataset.src; } lazyObserver.unobserve(el); } }); }, { rootMargin: '300px' }); } var lazyMedia = grid.querySelectorAll('img[data-src], video[data-src]'); for (var i = 0; i < lazyMedia.length; i++) { lazyObserver.observe(lazyMedia[i]); } } // ------------------------------------ // Card reveal on scroll // ------------------------------------ var revealObserver = null; function observeCardReveal() { if (!revealObserver) { revealObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add('visible'); revealObserver.unobserve(entry.target); } }); }, { threshold: 0.05, rootMargin: '40px' }); } var cards = grid.querySelectorAll('.clip-card:not(.visible)'); for (var i = 0; i < cards.length; i++) { // Stagger the transition delay cards[i].style.transitionDelay = (i * 0.04) + 's'; revealObserver.observe(cards[i]); } } // ------------------------------------ // Category buttons // ------------------------------------ function setupCategoryButtons() { var btns = document.querySelectorAll('.category-btn'); for (var i = 0; i < btns.length; i++) { btns[i].addEventListener('click', function () { setActiveBtn('.category-btn', this); activeCategory = this.dataset.category; filterAndRender(); }); } } function setActiveBtn(selector, activeEl) { var btns = document.querySelectorAll(selector); for (var i = 0; i < btns.length; i++) { btns[i].classList.toggle('active', btns[i] === activeEl); } } // ------------------------------------ // Modal // ------------------------------------ function setupModal() { var closeBtn = modal.querySelector('.modal-close'); if (closeBtn) { closeBtn.addEventListener('click', closeModal); } modal.addEventListener('click', function (e) { if (e.target === modal) closeModal(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); }); } function openModal(clip) { var stages = ['capture', 'retarget', 'training', 'policy']; modal.querySelector('.modal-title').textContent = clip.name; modal.querySelector('.modal-meta').textContent = clip.category + ' · ' + clip.performer + ' · ' + formatDuration(clip.duration) + ' · ' + clip.frames + ' frames @ ' + clip.fps + ' fps'; var modalGrid = modal.querySelector('.modal-grid'); modalGrid.innerHTML = stages.map(function (s) { var data = clip.stages[s]; var unavail = !data ? ' unavailable' : ''; var label = ''; var useViewer = s === 'policy' && clip.has_onnx; if (useViewer) { var iframeSrc = 'viewer.html?clip=' + encodeURIComponent(clip.id) + '&category=' + encodeURIComponent(clip.category) + '&embed=1'; return ''; } if (data) { var src = mediaUrl(data); var isVid = src && src.endsWith('.mp4'); var mediaTag = isVid ? '' : '' + escHtml(clip.name) + ' ' + s + ''; return ''; } return ''; }).join(''); modal.classList.add('open'); document.body.style.overflow = 'hidden'; } function closeModal() { modal.classList.remove('open'); document.body.style.overflow = ''; // Pause modal videos to stop background playback var videos = modal.querySelectorAll('video'); for (var i = 0; i < videos.length; i++) { videos[i].pause(); } } // ------------------------------------ // Scroll reveal for sections // ------------------------------------ function setupScrollReveal() { var sections = document.querySelectorAll('.reveal'); if (!sections.length) return; var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add('revealed'); observer.unobserve(entry.target); } }); }, { threshold: 0.1 }); for (var i = 0; i < sections.length; i++) { observer.observe(sections[i]); } } // ------------------------------------ // Utils // ------------------------------------ function mediaUrl(stageData) { var file = stageData.mp4 || stageData.gif; if (!file) return null; return baseUrl ? baseUrl + '/' + file : file; } function cardMediaUrl(stageData, clip) { // Use Space-hosted policy renders (uploaded by render_all_policies.js) if (clip && clip.has_policy) { return 'media/' + clip.category + '/' + clip.id + '/policy/' + clip.id + '_policy.mp4'; } var file = stageData.mp4 || stageData.gif; if (!file) return null; return baseUrl ? baseUrl + '/' + file : file; } function formatDuration(seconds) { var m = Math.floor(seconds / 60); var s = Math.round(seconds % 60); return m + ':' + (s < 10 ? '0' : '') + s; } function escHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ------------------------------------ // Citation copy // ------------------------------------ function initCitation() { const btn = document.querySelector('.citation-copy'); if (!btn) return; btn.addEventListener('click', () => { const code = document.querySelector('.citation-block code'); if (!code) return; navigator.clipboard.writeText(code.textContent.trim()).then(() => { btn.classList.add('copied'); btn.innerHTML = ''; setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = ''; }, 2000); }); }); } // ------------------------------------ // Boot // ------------------------------------ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { init(); initCitation(); }); } else { init(); initCitation(); } })();