g1-moves / app.js
exptech's picture
Upload app.js with huggingface_hub
ee101ed verified
/* ========================================
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 = '<p style="padding:2rem;color:#888;font-family:monospace;">Failed to load data.json</p>';
}
}
// ------------------------------------
// 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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>';
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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
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 = '&#8249;';
prevBtn.setAttribute('aria-label', 'Previous');
var nextBtn = document.createElement('button');
nextBtn.className = 'carousel-btn';
nextBtn.innerHTML = '&#8250;';
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
? '<video data-src="' + mediaSrc + '" autoplay muted loop playsinline preload="none" class="lazy-video"></video>'
: '<img data-src="' + mediaSrc + '" alt="' + escHtml(clip.name) + '" loading="lazy">';
} else {
mediaHtml = '<div class="card-placeholder">Not available</div>';
}
card.innerHTML =
'<div class="card-media">' +
mediaHtml +
'<div class="card-duration">' + formatDuration(clip.duration) + '</div>' +
(clip.has_policy ? '<div class="card-badge">TRAINED</div>' : '') +
'</div>' +
'<div class="card-info">' +
'<div class="card-title">' + escHtml(clip.name) + '</div>' +
'<div class="card-meta">' +
'<span class="card-category">' + clip.category + '</span>' +
'<span class="card-separator"></span>' +
'<span>' + clip.performer + '</span>' +
'</div>' +
'<div class="card-actions">' +
(clip.has_policy
? '<a class="card-view-btn" href="' + viewerUrl + '" title="Run policy in browser">' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>' +
'View' +
'</a>'
: '') +
'</div>' +
'</div>';
// 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 = '<div class="modal-stage-label">' + s.toUpperCase() + '</div>';
var useViewer = s === 'policy' && clip.has_onnx;
if (useViewer) {
var iframeSrc = 'viewer.html?clip=' + encodeURIComponent(clip.id) +
'&category=' + encodeURIComponent(clip.category) + '&embed=1';
return '<div class="modal-stage">' +
label +
'<iframe src="' + iframeSrc + '" style="width:100%;height:100%;border:none;min-height:300px;" allowtransparency="true"></iframe>' +
'</div>';
}
if (data) {
var src = mediaUrl(data);
var isVid = src && src.endsWith('.mp4');
var mediaTag = isVid
? '<video src="' + src + '" autoplay muted loop playsinline></video>'
: '<img src="' + src + '" alt="' + escHtml(clip.name) + ' ' + s + '" loading="lazy">';
return '<div class="modal-stage' + unavail + '">' +
label + mediaTag +
'</div>';
}
return '<div class="modal-stage' + unavail + '">' +
label +
'<div class="modal-placeholder">Not yet available</div>' +
'</div>';
}).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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
}, 2000);
});
});
}
// ------------------------------------
// Boot
// ------------------------------------
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { init(); initCitation(); });
} else {
init();
initCitation();
}
})();