CoolFace
Apppublic

onfroy2/tabletop-gallery-explorer

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
script.js643 linesDownload Raw Back to root
1// Tabletop State Management2class TabletopState {3    constructor() {4        this.scale = 1;5        this.translateX = 0;6        this.translateY = 0;7        this.isDragging = false;8        this.startX = 0;9        this.startY = 0;10        this.selectedObject = null;11        this.isDarkMode = false;12        this.viewMode = 'chronological'; // chronological, categorical, groups13        this.objects = [];14        this.filteredObjects = [];15    }16 17    setTransform(x, y, scale) {18        this.translateX = x;19        this.translateY = y;20        this.scale = scale;21        this.updateTransform();22    }23 24    updateTransform() {25        const surface = document.getElementById('surface');26        surface.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`;27    }28}29 30// Initialize state31const state = new TabletopState();32 33// Sample data for demonstration34const sampleData = {35    magazines: [36        {37            id: 'mag-1',38            type: 'magazine',39            title: 'Design Systems Quarterly',40            excerpt: 'Exploring the latest trends in digital design systems',41            date: '2024-01',42            color: 'blue',43            pages: [44                { type: 'cover', content: 'Cover Design' },45                { type: 'page', content: 'Understanding Design Tokens' },46                { type: 'page', content: 'Component Library Best Practices' },47                { type: 'back', content: 'Resources & Tools' }48            ]49        },50        {51            id: 'mag-2',52            type: 'magazine',53            title: 'Creative Coding',54            excerpt: 'Art meets technology in creative programming',55            date: '2024-02',56            color: 'purple',57            pages: [58                { type: 'cover', content: 'Creative Coding Cover' },59                { type: 'page', content: 'Generative Art Techniques' },60                { type: 'page', content: 'Interactive Installations' },61                { type: 'back', content: 'Gallery Showcase' }62            ]63        }64    ],65    resources: [66        {67            id: 'res-1',68            type: 'resource',69            title: 'Development Tools',70            items: ['Code Editors', 'Version Control', 'Testing Frameworks', 'Deployment Tools']71        },72        {73            id: 'res-2',74            type: 'resource',75            title: 'Design Assets',76            items: ['Icons & Illustrations', 'Color Palettes', 'Typography', 'Stock Photos']77        }78    ],79    art: [80        {81            id: 'art-1',82            type: 'art',83            title: 'Generative Landscape',84            image: 'http://static.photos/nature/640x360/42',85            description: 'Algorithmic nature scenes'86        },87        {88            id: 'art-2',89            type: 'art',90            title: 'Abstract Geometry',91            image: 'http://static.photos/abstract/640x360/133',92            description: 'Mathematical beauty in art'93        }94    ],95    demos: [96        {97            id: 'demo-1',98            type: 'demo',99            title: 'Interactive Dashboard',100            preview: 'http://static.photos/technology/640x360/1'101        }102    ]103};104 105// Generate objects from sample data106function generateObjects() {107    const objects = [];108    109    // Generate magazines110    sampleData.magazines.forEach((magazine, index) => {111        objects.push({112            ...magazine,113            x: 200 + (index % 3) * 300,114            y: 200 + Math.floor(index / 3) * 400,115            rotation: Math.random() * 15 - 7.5116        });117    });118    119    // Generate resources120    sampleData.resources.forEach((resource, index) => {121        objects.push({122            ...resource,123            x: 600 + (index % 2) * 200,124            y: 800 + Math.floor(index / 2) * 200,125            rotation: Math.random() * 10 - 5126        });127    });128    129    // Generate art130    sampleData.art.forEach((art, index) => {131        objects.push({132            ...art,133            x: 1000 + (index % 2) * 250,134            y: 300 + Math.floor(index / 2) * 300,135            rotation: 0136        });137    });138    139    // Generate demos140    sampleData.demos.forEach((demo, index) => {141        objects.push({142            ...demo,143            x: 1200,144            y: 600,145            rotation: 0146        });147    });148    149    state.objects = objects;150    state.filteredObjects = [...objects];151}152 153// Render objects on tabletop154function renderObjects() {155    const container = document.getElementById('objectsContainer');156    container.innerHTML = '';157    158    state.filteredObjects.forEach(obj => {159        const element = createTabletopObject(obj);160        container.appendChild(element);161    });162}163 164// Create individual tabletop object165function createTabletopObject(obj) {166    const wrapper = document.createElement('div');167    wrapper.className = 'tabletop-object absolute cursor-pointer';168    wrapper.style.left = `${obj.x}px`;169    wrapper.style.top = `${obj.y}px`;170    wrapper.style.transform = `rotate(${obj.rotation}deg)`;171    wrapper.dataset.objectId = obj.id;172    wrapper.dataset.objectType = obj.type;173    wrapper.tabIndex = 0;174    wrapper.setAttribute('role', 'button');175    wrapper.setAttribute('aria-label', `${obj.title} - ${obj.type}`);176    177    let content = '';178    179    switch(obj.type) {180        case 'magazine':181            content = `182                <div class="magazine-spine w-32 h-44 rounded shadow-xl relative">183                    <div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>184                    <div class="p-3 h-full flex flex-col justify-between">185                        <div class="text-white text-xs font-bold transform -rotate-90 origin-center whitespace-nowrap">186                            ${obj.title}187                        </div>188                        <div class="text-white/70 text-xs">189                            ${obj.date}190                        </div>191                    </div>192                </div>193            `;194            break;195            196        case 'resource':197            content = `198                <div class="file-cabinet w-24 h-32 rounded shadow-lg relative">199                    <div class="absolute inset-2 bg-gray-700 rounded">200                        <div class="p-2">201                            <div class="bg-gray-600 h-1 mb-2 rounded"></div>202                            <div class="bg-gray-600 h-1 mb-2 rounded"></div>203                            <div class="bg-gray-600 h-1 mb-2 rounded"></div>204                            <div class="text-white text-xs mt-4">${obj.title}</div>205                        </div>206                    </div>207                </div>208            `;209            break;210            211        case 'art':212            content = `213                <div class="art-frame shadow-2xl">214                    <div class="art-canvas w-48 h-32 relative overflow-hidden">215                        <img src="${obj.image}" alt="${obj.title}" class="w-full h-full object-cover">216                        <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2">217                            <div class="text-white text-xs font-semibold">${obj.title}</div>218                        </div>219                    </div>220                </div>221            `;222            break;223            224        case 'demo':225            content = `226                <div class="device-mockup shadow-2xl relative floating">227                    <div class="device-screen w-64 h-40 relative overflow-hidden">228                        <img src="${obj.preview}" alt="${obj.title}" class="w-full h-full object-cover">229                    </div>230                    <div class="absolute -bottom-6 left-0 right-0 text-center">231                        <div class="text-gray-700 dark:text-gray-300 text-sm bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-full px-3 py-1 inline-block">232                            ${obj.title}233                        </div>234                    </div>235                </div>236            `;237            break;238    }239    240    wrapper.innerHTML = content;241    242    // Add event listeners243    wrapper.addEventListener('click', () => handleObjectClick(obj));244    wrapper.addEventListener('keydown', (e) => {245        if (e.key === 'Enter' || e.key === ' ') {246            handleObjectClick(obj);247        }248    });249    250    // Add drag functionality251    let isDragging = false;252    let startX, startY, initialX, initialY;253    254    wrapper.addEventListener('mousedown', (e) => {255        isDragging = true;256        startX = e.clientX;257        startY = e.clientY;258        initialX = obj.x;259        initialY = obj.y;260        wrapper.classList.add('dragging');261        e.stopPropagation();262    });263    264    document.addEventListener('mousemove', (e) => {265        if (!isDragging) return;266        267        const dx = (e.clientX - startX) / state.scale;268        const dy = (e.clientY - startY) / state.scale;269        270        obj.x = initialX + dx;271        obj.y = initialY + dy;272        273        wrapper.style.left = `${obj.x}px`;274        wrapper.style.top = `${obj.y}px`;275    });276    277    document.addEventListener('mouseup', () => {278        if (isDragging) {279            isDragging = false;280            wrapper.classList.remove('dragging');281            saveLayout();282        }283    });284    285    return wrapper;286}287 288// Handle object click289function handleObjectClick(obj) {290    state.selectedObject = obj;291    292    switch(obj.type) {293        case 'magazine':294            openMagazineViewer(obj);295            break;296        case 'resource':297            openResourceViewer(obj);298            break;299        case 'art':300            openArtViewer(obj);301            break;302        case 'demo':303            openDemoViewer(obj);304            break;305    }306}307 308// Magazine Viewer309function openMagazineViewer(magazine) {310    const modal = document.getElementById('magazineModal');311    const content = document.getElementById('magazineContent');312    313    content.innerHTML = `314        <magazine-viewer 315            title="${magazine.title}"316            pages='${JSON.stringify(magazine.pages)}'317            color="${magazine.color}">318        </magazine-viewer>319    `;320    321    modal.classList.remove('hidden');322    modal.classList.add('flex');323}324 325// Resource Viewer (placeholder)326function openResourceViewer(resource) {327    alert(`Opening ${resource.title}\nItems: ${resource.items.join(', ')}`);328}329 330// Art Viewer (placeholder)331function openArtViewer(art) {332    alert(`Viewing ${art.title}\nDescription: ${art.description}`);333}334 335// Demo Viewer (placeholder)336function openDemoViewer(demo) {337    alert(`Launching ${demo.title}`);338}339 340// Pan and Zoom functionality341function setupPanZoom() {342    const tabletop = document.getElementById('tabletop');343    let lastX = 0;344    let lastY = 0;345    346    // Pan with mouse347    tabletop.addEventListener('mousedown', (e) => {348        if (e.target === tabletop || e.target.id === 'surface') {349            state.isDragging = true;350            state.startX = e.clientX - state.translateX;351            state.startY = e.clientY - state.translateY;352            tabletop.style.cursor = 'grabbing';353        }354    });355    356    document.addEventListener('mousemove', (e) => {357        if (!state.isDragging) return;358        359        e.preventDefault();360        state.translateX = e.clientX - state.startX;361        state.translateY = e.clientY - state.startY;362        state.updateTransform();363    });364    365    document.addEventListener('mouseup', () => {366        state.isDragging = false;367        tabletop.style.cursor = 'grab';368    });369    370    // Zoom with mouse wheel371    tabletop.addEventListener('wheel', (e) => {372        e.preventDefault();373        374        const delta = e.deltaY > 0 ? 0.9 : 1.1;375        const newScale = Math.min(Math.max(state.scale * delta, 0.5), 3);376        377        const rect = tabletop.getBoundingClientRect();378        const x = e.clientX - rect.left;379        const y = e.clientY - rect.top;380        381        state.translateX = x - (x - state.translateX) * (newScale / state.scale);382        state.translateY = y - (y - state.translateY) * (newScale / state.scale);383        state.scale = newScale;384        385        state.updateTransform();386    });387    388    // Zoom controls389    document.getElementById('zoomIn').addEventListener('click', () => {390        state.scale = Math.min(state.scale * 1.2, 3);391        state.updateTransform();392    });393    394    document.getElementById('zoomOut').addEventListener('click', () => {395        state.scale = Math.max(state.scale * 0.8, 0.5);396        state.updateTransform();397    });398    399    document.getElementById('resetView').addEventListener('click', () => {400        state.setTransform(0, 0, 1);401    });402}403 404// Touch support405function setupTouchSupport() {406    const tabletop = document.getElementById('tabletop');407    let touchStartX, touchStartY;408    let initialDistance = 0;409    let initialScale = 1;410    411    tabletop.addEventListener('touchstart', (e) => {412        if (e.touches.length === 2) {413            const dx = e.touches[0].clientX - e.touches[1].clientX;414            const dy = e.touches[0].clientY - e.touches[1].clientY;415            initialDistance = Math.sqrt(dx * dx + dy * dy);416            initialScale = state.scale;417        } else if (e.touches.length === 1) {418            touchStartX = e.touches[0].clientX - state.translateX;419            touchStartY = e.touches[0].clientY - state.translateY;420        }421    });422    423    tabletop.addEventListener('touchmove', (e) => {424        e.preventDefault();425        426        if (e.touches.length === 2) {427            const dx = e.touches[0].clientX - e.touches[1].clientX;428            const dy = e.touches[0].clientY - e.touches[1].clientY;429            const distance = Math.sqrt(dx * dx + dy * dy);430            const scale = (distance / initialDistance) * initialScale;431            432            state.scale = Math.min(Math.max(scale, 0.5), 3);433            state.updateTransform();434        } else if (e.touches.length === 1) {435            state.translateX = e.touches[0].clientX - touchStartX;436            state.translateY = e.touches[0].clientY - touchStartY;437            state.updateTransform();438        }439    });440}441 442// Search functionality443function setupSearch() {444    const searchBtn = document.getElementById('searchBtn');445    const searchModal = document.getElementById('searchModal');446    const searchInput = document.getElementById('searchInput');447    const searchResults = document.getElementById('searchResults');448    449    searchBtn.addEventListener('click', () => {450        searchModal.classList.remove('hidden');451        searchModal.classList.add('flex');452        searchInput.focus();453    });454    455    searchModal.addEventListener('click', (e) => {456        if (e.target === searchModal) {457            searchModal.classList.add('hidden');458            searchModal.classList.remove('flex');459        }460    });461    462    searchInput.addEventListener('input', (e) => {463        const query = e.target.value.toLowerCase();464        465        if (query.length < 2) {466            searchResults.innerHTML = '';467            return;468        }469        470        const results = state.objects.filter(obj => 471            obj.title.toLowerCase().includes(query) || 472            (obj.excerpt && obj.excerpt.toLowerCase().includes(query))473        );474        475        searchResults.innerHTML = results.map(obj => `476            <div class="p-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg cursor-pointer transition-colors"477                 onclick="focusOnObject('${obj.id}')">478                <div class="font-semibold">${obj.title}</div>479                <div class="text-sm text-gray-500 dark:text-gray-400">${obj.type}</div>480            </div>481        `).join('');482    });483}484 485// Focus on object by ID486function focusOnObject(id) {487    const obj = state.objects.find(o => o.id === id);488    if (!obj) return;489    490    state.setTransform(491        -(obj.x - window.innerWidth / 4),492        -(obj.y - window.innerHeight / 4),493        1.5494    );495    496    document.getElementById('searchModal').classList.add('hidden');497    document.getElementById('searchModal').classList.remove('flex');498}499 500// Theme toggle501function setupThemeToggle() {502    const themeToggle = document.getElementById('themeToggle');503    const html = document.documentElement;504    505    // Check for saved theme preference or default to light mode506    const savedTheme = localStorage.getItem('theme');507    if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {508        html.classList.add('dark');509        state.isDarkMode = true;510    }511    512    themeToggle.addEventListener('click', () => {513        html.classList.toggle('dark');514        state.isDarkMode = !state.isDarkMode;515        localStorage.setItem('theme', state.isDarkMode ? 'dark' : 'light');516    });517}518 519// View mode toggle520function setupViewMode() {521    const viewModeBtn = document.getElementById('viewModeBtn');522    523    viewModeBtn.addEventListener('click', () => {524        const modes = ['chronological', 'categorical', 'groups'];525        const currentIndex = modes.indexOf(state.viewMode);526        state.viewMode = modes[(currentIndex + 1) % modes.length];527        528        renderObjects();529    });530}531 532// Close magazine modal533document.getElementById('closeMagazine').addEventListener('click', () => {534    const modal = document.getElementById('magazineModal');535    modal.classList.add('hidden');536    modal.classList.remove('flex');537});538 539// Keyboard navigation540function setupKeyboardNav() {541    document.addEventListener('keydown', (e) => {542        if (e.key === 'Escape') {543            // Close any open modals544            const modals = document.querySelectorAll('.modal:not(.hidden)');545            modals.forEach(modal => {546                modal.classList.add('hidden');547                modal.classList.remove('flex');548            });549        }550        551        if (e.key === '/' && !e.ctrlKey && !e.metaKey) {552            e.preventDefault();553            document.getElementById('searchBtn').click();554        }555        556        // Arrow keys for navigation557        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {558            e.preventDefault();559            const step = e.shiftKey ? 50 : 10;560            561            switch(e.key) {562                case 'ArrowUp':563                    state.translateY += step;564                    break;565                case 'ArrowDown':566                    state.translateY -= step;567                    break;568                case 'ArrowLeft':569                    state.translateX += step;570                    break;571                case 'ArrowRight':572                    state.translateX -= step;573                    break;574            }575            576            state.updateTransform();577        }578    });579}580 581// Save layout to localStorage582function saveLayout() {583    localStorage.setItem('tabletopLayout', JSON.stringify({584        objects: state.objects,585        viewMode: state.viewMode,586        transform: {587            x: state.translateX,588            y: state.translateY,589            scale: state.scale590        }591    }));592}593 594// Load layout from localStorage595function loadLayout() {596    const saved = localStorage.getItem('tabletopLayout');597    if (saved) {598        const data = JSON.parse(saved);599        state.objects = data.objects || state.objects;600        state.viewMode = data.viewMode || state.viewMode;601        602        if (data.transform) {603            state.setTransform(data.transform.x, data.transform.y, data.transform.scale);604        }605    }606}607 608// Initialize everything609document.addEventListener('DOMContentLoaded', () => {610    generateObjects();611    loadLayout();612    renderObjects();613    setupPanZoom();614    setupTouchSupport();615    setupSearch();616    setupThemeToggle();617    setupViewMode();618    setupKeyboardNav();619    620    // Close magazine modal on escape621    document.getElementById('magazineModal').addEventListener('click', (e) => {622        if (e.target === document.getElementById('magazineModal')) {623            document.getElementById('closeMagazine').click();624        }625    });626});627 628// Handle window resize629window.addEventListener('resize', () => {630    saveLayout();631});632 633// Save layout periodically634setInterval(saveLayout, 10000);635 636// Offline capability637if ('serviceWorker' in navigator) {638    window.addEventListener('load', () => {639        navigator.serviceWorker.register('/sw.js').catch(() => {640            console.log('Service worker registration failed');641        });642    });643}