// Tabletop State Management class TabletopState { constructor() { this.scale = 1; this.translateX = 0; this.translateY = 0; this.isDragging = false; this.startX = 0; this.startY = 0; this.selectedObject = null; this.isDarkMode = false; this.viewMode = 'chronological'; // chronological, categorical, groups this.objects = []; this.filteredObjects = []; } setTransform(x, y, scale) { this.translateX = x; this.translateY = y; this.scale = scale; this.updateTransform(); } updateTransform() { const surface = document.getElementById('surface'); surface.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`; } } // Initialize state const state = new TabletopState(); // Sample data for demonstration const sampleData = { magazines: [ { id: 'mag-1', type: 'magazine', title: 'Design Systems Quarterly', excerpt: 'Exploring the latest trends in digital design systems', date: '2024-01', color: 'blue', pages: [ { type: 'cover', content: 'Cover Design' }, { type: 'page', content: 'Understanding Design Tokens' }, { type: 'page', content: 'Component Library Best Practices' }, { type: 'back', content: 'Resources & Tools' } ] }, { id: 'mag-2', type: 'magazine', title: 'Creative Coding', excerpt: 'Art meets technology in creative programming', date: '2024-02', color: 'purple', pages: [ { type: 'cover', content: 'Creative Coding Cover' }, { type: 'page', content: 'Generative Art Techniques' }, { type: 'page', content: 'Interactive Installations' }, { type: 'back', content: 'Gallery Showcase' } ] } ], resources: [ { id: 'res-1', type: 'resource', title: 'Development Tools', items: ['Code Editors', 'Version Control', 'Testing Frameworks', 'Deployment Tools'] }, { id: 'res-2', type: 'resource', title: 'Design Assets', items: ['Icons & Illustrations', 'Color Palettes', 'Typography', 'Stock Photos'] } ], art: [ { id: 'art-1', type: 'art', title: 'Generative Landscape', image: 'http://static.photos/nature/640x360/42', description: 'Algorithmic nature scenes' }, { id: 'art-2', type: 'art', title: 'Abstract Geometry', image: 'http://static.photos/abstract/640x360/133', description: 'Mathematical beauty in art' } ], demos: [ { id: 'demo-1', type: 'demo', title: 'Interactive Dashboard', preview: 'http://static.photos/technology/640x360/1' } ] }; // Generate objects from sample data function generateObjects() { const objects = []; // Generate magazines sampleData.magazines.forEach((magazine, index) => { objects.push({ ...magazine, x: 200 + (index % 3) * 300, y: 200 + Math.floor(index / 3) * 400, rotation: Math.random() * 15 - 7.5 }); }); // Generate resources sampleData.resources.forEach((resource, index) => { objects.push({ ...resource, x: 600 + (index % 2) * 200, y: 800 + Math.floor(index / 2) * 200, rotation: Math.random() * 10 - 5 }); }); // Generate art sampleData.art.forEach((art, index) => { objects.push({ ...art, x: 1000 + (index % 2) * 250, y: 300 + Math.floor(index / 2) * 300, rotation: 0 }); }); // Generate demos sampleData.demos.forEach((demo, index) => { objects.push({ ...demo, x: 1200, y: 600, rotation: 0 }); }); state.objects = objects; state.filteredObjects = [...objects]; } // Render objects on tabletop function renderObjects() { const container = document.getElementById('objectsContainer'); container.innerHTML = ''; state.filteredObjects.forEach(obj => { const element = createTabletopObject(obj); container.appendChild(element); }); } // Create individual tabletop object function createTabletopObject(obj) { const wrapper = document.createElement('div'); wrapper.className = 'tabletop-object absolute cursor-pointer'; wrapper.style.left = `${obj.x}px`; wrapper.style.top = `${obj.y}px`; wrapper.style.transform = `rotate(${obj.rotation}deg)`; wrapper.dataset.objectId = obj.id; wrapper.dataset.objectType = obj.type; wrapper.tabIndex = 0; wrapper.setAttribute('role', 'button'); wrapper.setAttribute('aria-label', `${obj.title} - ${obj.type}`); let content = ''; switch(obj.type) { case 'magazine': content = `
${obj.title}
${obj.date}
`; break; case 'resource': content = `
${obj.title}
`; break; case 'art': content = `
${obj.title}
${obj.title}
`; break; case 'demo': content = `
${obj.title}
${obj.title}
`; break; } wrapper.innerHTML = content; // Add event listeners wrapper.addEventListener('click', () => handleObjectClick(obj)); wrapper.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { handleObjectClick(obj); } }); // Add drag functionality let isDragging = false; let startX, startY, initialX, initialY; wrapper.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; initialX = obj.x; initialY = obj.y; wrapper.classList.add('dragging'); e.stopPropagation(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = (e.clientX - startX) / state.scale; const dy = (e.clientY - startY) / state.scale; obj.x = initialX + dx; obj.y = initialY + dy; wrapper.style.left = `${obj.x}px`; wrapper.style.top = `${obj.y}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; wrapper.classList.remove('dragging'); saveLayout(); } }); return wrapper; } // Handle object click function handleObjectClick(obj) { state.selectedObject = obj; switch(obj.type) { case 'magazine': openMagazineViewer(obj); break; case 'resource': openResourceViewer(obj); break; case 'art': openArtViewer(obj); break; case 'demo': openDemoViewer(obj); break; } } // Magazine Viewer function openMagazineViewer(magazine) { const modal = document.getElementById('magazineModal'); const content = document.getElementById('magazineContent'); content.innerHTML = ` `; modal.classList.remove('hidden'); modal.classList.add('flex'); } // Resource Viewer (placeholder) function openResourceViewer(resource) { alert(`Opening ${resource.title}\nItems: ${resource.items.join(', ')}`); } // Art Viewer (placeholder) function openArtViewer(art) { alert(`Viewing ${art.title}\nDescription: ${art.description}`); } // Demo Viewer (placeholder) function openDemoViewer(demo) { alert(`Launching ${demo.title}`); } // Pan and Zoom functionality function setupPanZoom() { const tabletop = document.getElementById('tabletop'); let lastX = 0; let lastY = 0; // Pan with mouse tabletop.addEventListener('mousedown', (e) => { if (e.target === tabletop || e.target.id === 'surface') { state.isDragging = true; state.startX = e.clientX - state.translateX; state.startY = e.clientY - state.translateY; tabletop.style.cursor = 'grabbing'; } }); document.addEventListener('mousemove', (e) => { if (!state.isDragging) return; e.preventDefault(); state.translateX = e.clientX - state.startX; state.translateY = e.clientY - state.startY; state.updateTransform(); }); document.addEventListener('mouseup', () => { state.isDragging = false; tabletop.style.cursor = 'grab'; }); // Zoom with mouse wheel tabletop.addEventListener('wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; const newScale = Math.min(Math.max(state.scale * delta, 0.5), 3); const rect = tabletop.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; state.translateX = x - (x - state.translateX) * (newScale / state.scale); state.translateY = y - (y - state.translateY) * (newScale / state.scale); state.scale = newScale; state.updateTransform(); }); // Zoom controls document.getElementById('zoomIn').addEventListener('click', () => { state.scale = Math.min(state.scale * 1.2, 3); state.updateTransform(); }); document.getElementById('zoomOut').addEventListener('click', () => { state.scale = Math.max(state.scale * 0.8, 0.5); state.updateTransform(); }); document.getElementById('resetView').addEventListener('click', () => { state.setTransform(0, 0, 1); }); } // Touch support function setupTouchSupport() { const tabletop = document.getElementById('tabletop'); let touchStartX, touchStartY; let initialDistance = 0; let initialScale = 1; tabletop.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; initialDistance = Math.sqrt(dx * dx + dy * dy); initialScale = state.scale; } else if (e.touches.length === 1) { touchStartX = e.touches[0].clientX - state.translateX; touchStartY = e.touches[0].clientY - state.translateY; } }); tabletop.addEventListener('touchmove', (e) => { e.preventDefault(); if (e.touches.length === 2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const distance = Math.sqrt(dx * dx + dy * dy); const scale = (distance / initialDistance) * initialScale; state.scale = Math.min(Math.max(scale, 0.5), 3); state.updateTransform(); } else if (e.touches.length === 1) { state.translateX = e.touches[0].clientX - touchStartX; state.translateY = e.touches[0].clientY - touchStartY; state.updateTransform(); } }); } // Search functionality function setupSearch() { const searchBtn = document.getElementById('searchBtn'); const searchModal = document.getElementById('searchModal'); const searchInput = document.getElementById('searchInput'); const searchResults = document.getElementById('searchResults'); searchBtn.addEventListener('click', () => { searchModal.classList.remove('hidden'); searchModal.classList.add('flex'); searchInput.focus(); }); searchModal.addEventListener('click', (e) => { if (e.target === searchModal) { searchModal.classList.add('hidden'); searchModal.classList.remove('flex'); } }); searchInput.addEventListener('input', (e) => { const query = e.target.value.toLowerCase(); if (query.length < 2) { searchResults.innerHTML = ''; return; } const results = state.objects.filter(obj => obj.title.toLowerCase().includes(query) || (obj.excerpt && obj.excerpt.toLowerCase().includes(query)) ); searchResults.innerHTML = results.map(obj => `
${obj.title}
${obj.type}
`).join(''); }); } // Focus on object by ID function focusOnObject(id) { const obj = state.objects.find(o => o.id === id); if (!obj) return; state.setTransform( -(obj.x - window.innerWidth / 4), -(obj.y - window.innerHeight / 4), 1.5 ); document.getElementById('searchModal').classList.add('hidden'); document.getElementById('searchModal').classList.remove('flex'); } // Theme toggle function setupThemeToggle() { const themeToggle = document.getElementById('themeToggle'); const html = document.documentElement; // Check for saved theme preference or default to light mode const savedTheme = localStorage.getItem('theme'); if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { html.classList.add('dark'); state.isDarkMode = true; } themeToggle.addEventListener('click', () => { html.classList.toggle('dark'); state.isDarkMode = !state.isDarkMode; localStorage.setItem('theme', state.isDarkMode ? 'dark' : 'light'); }); } // View mode toggle function setupViewMode() { const viewModeBtn = document.getElementById('viewModeBtn'); viewModeBtn.addEventListener('click', () => { const modes = ['chronological', 'categorical', 'groups']; const currentIndex = modes.indexOf(state.viewMode); state.viewMode = modes[(currentIndex + 1) % modes.length]; renderObjects(); }); } // Close magazine modal document.getElementById('closeMagazine').addEventListener('click', () => { const modal = document.getElementById('magazineModal'); modal.classList.add('hidden'); modal.classList.remove('flex'); }); // Keyboard navigation function setupKeyboardNav() { document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { // Close any open modals const modals = document.querySelectorAll('.modal:not(.hidden)'); modals.forEach(modal => { modal.classList.add('hidden'); modal.classList.remove('flex'); }); } if (e.key === '/' && !e.ctrlKey && !e.metaKey) { e.preventDefault(); document.getElementById('searchBtn').click(); } // Arrow keys for navigation if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { e.preventDefault(); const step = e.shiftKey ? 50 : 10; switch(e.key) { case 'ArrowUp': state.translateY += step; break; case 'ArrowDown': state.translateY -= step; break; case 'ArrowLeft': state.translateX += step; break; case 'ArrowRight': state.translateX -= step; break; } state.updateTransform(); } }); } // Save layout to localStorage function saveLayout() { localStorage.setItem('tabletopLayout', JSON.stringify({ objects: state.objects, viewMode: state.viewMode, transform: { x: state.translateX, y: state.translateY, scale: state.scale } })); } // Load layout from localStorage function loadLayout() { const saved = localStorage.getItem('tabletopLayout'); if (saved) { const data = JSON.parse(saved); state.objects = data.objects || state.objects; state.viewMode = data.viewMode || state.viewMode; if (data.transform) { state.setTransform(data.transform.x, data.transform.y, data.transform.scale); } } } // Initialize everything document.addEventListener('DOMContentLoaded', () => { generateObjects(); loadLayout(); renderObjects(); setupPanZoom(); setupTouchSupport(); setupSearch(); setupThemeToggle(); setupViewMode(); setupKeyboardNav(); // Close magazine modal on escape document.getElementById('magazineModal').addEventListener('click', (e) => { if (e.target === document.getElementById('magazineModal')) { document.getElementById('closeMagazine').click(); } }); }); // Handle window resize window.addEventListener('resize', () => { saveLayout(); }); // Save layout periodically setInterval(saveLayout, 10000); // Offline capability if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => { console.log('Service worker registration failed'); }); }); }