/* ======================================== 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 ? '' : '