Spaces:
Runtime error
Runtime error
| <!DOCTYPE html> | |
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>AR PIXELS – AI Image Generator</title> | |
| <!-- Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,1,0" /> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Noto Sans', 'sans-serif'], | |
| display: ['Space Grotesk', 'sans-serif'], | |
| }, | |
| colors: { | |
| primary: '#8B5CF6', | |
| secondary: '#A78BFA', | |
| dark: '#0F0F13', | |
| surface: '#18181B', | |
| }, | |
| animation: { | |
| 'fade-in': 'fadeIn 0.5s ease-out', | |
| 'slide-up': 'slideUp 0.4s ease-out', | |
| 'pulse-slow': 'pulse 3s infinite', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom Styles & Utilities */ | |
| :root { | |
| --glass-border: rgba(255, 255, 255, 0.08); | |
| --glass-bg: rgba(24, 24, 27, 0.65); | |
| } | |
| body { | |
| -webkit-tap-highlight-color: transparent; | |
| overscroll-behavior-y: none; | |
| } | |
| .glass { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid var(--glass-border); | |
| } | |
| .glass-panel { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .hide-scrollbar::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .hide-scrollbar { | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| /* Animations */ | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| from { transform: translateY(20px); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| /* Range Slider Styling */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 20px; | |
| width: 20px; | |
| border-radius: 50%; | |
| background: #8B5CF6; | |
| margin-top: -8px; | |
| box-shadow: 0 0 10px rgba(139, 92, 246, 0.5); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 2px; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(-100px); | |
| z-index: 9999; | |
| transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .toast.show { | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| /* Masonry Grid */ | |
| .masonry-grid { | |
| column-count: 2; | |
| column-gap: 12px; | |
| } | |
| .masonry-item { | |
| break-inside: avoid; | |
| margin-bottom: 12px; | |
| } | |
| .loader { | |
| border: 3px solid rgba(255,255,255,0.1); | |
| border-left-color: #8B5CF6; | |
| border-radius: 50%; | |
| width: 24px; | |
| height: 24px; | |
| animation: spin 1s linear infinite; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 dark:bg-dark text-gray-800 dark:text-white h-screen w-screen overflow-hidden transition-colors duration-300"> | |
| <!-- Toast Container --> | |
| <div id="toast-container" class="toast glass px-6 py-3 rounded-full flex items-center gap-3 shadow-2xl min-w-[300px]"> | |
| <span id="toast-icon" class="material-symbols-rounded text-primary">info</span> | |
| <p id="toast-message" class="text-sm font-medium">Notification</p> | |
| </div> | |
| <!-- MAIN APP CONTAINER --> | |
| <div id="app" class="h-full w-full relative"> | |
| <!-- SCREEN 1: ONBOARDING --> | |
| <section id="screen-onboarding" class="h-full w-full flex flex-col items-center justify-center relative z-50 bg-dark"> | |
| <!-- Animated Background Tiles --> | |
| <div class="absolute inset-0 grid grid-cols-3 gap-2 opacity-20 overflow-hidden pointer-events-none"> | |
| <div class="bg-gradient-to-br from-purple-500 to-blue-500 rounded-xl h-40 w-full animate-pulse-slow"></div> | |
| <div class="bg-gradient-to-br from-pink-500 to-rose-500 rounded-xl h-64 w-full mt-10"></div> | |
| <div class="bg-gradient-to-br from-indigo-500 to-cyan-500 rounded-xl h-48 w-full"></div> | |
| <div class="bg-gradient-to-br from-amber-500 to-orange-500 rounded-xl h-56 w-full -mt-12"></div> | |
| <div class="bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl h-40 w-full"></div> | |
| <div class="bg-gradient-to-br from-fuchsia-500 to-purple-600 rounded-xl h-60 w-full mt-8"></div> | |
| </div> | |
| <div class="absolute inset-0 bg-gradient-to-t from-dark via-dark/80 to-transparent"></div> | |
| <div class="z-10 flex flex-col items-center text-center px-6 animate-slide-up"> | |
| <div class="w-20 h-20 rounded-2xl bg-gradient-to-tr from-primary to-blue-500 flex items-center justify-center shadow-lg shadow-purple-500/30 mb-6"> | |
| <span class="material-symbols-rounded text-4xl text-white">auto_awesome</span> | |
| </div> | |
| <h1 class="font-display text-3xl font-bold tracking-tight mb-2">AR PIXELS</h1> | |
| <h2 class="text-4xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-blue-400 mb-6 leading-tight"> | |
| Imagine.<br>Type.<br>Create. | |
| </h2> | |
| <p class="text-gray-400 mb-10 max-w-xs">Turn your wildest dreams into breathtaking reality with AI-powered pixel art.</p> | |
| <button onclick="router.navigate('create')" class="group relative w-full max-w-xs overflow-hidden rounded-full bg-primary p-4 transition-all hover:bg-purple-600 active:scale-95"> | |
| <div class="absolute inset-0 -translate-x-full group-hover:animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/20 to-transparent"></div> | |
| <span class="relative font-bold text-white tracking-wide">Get Started</span> | |
| </button> | |
| </div> | |
| </section> | |
| <!-- SCREEN 2: CREATE (Main) --> | |
| <section id="screen-create" class="h-full w-full flex flex-col hidden"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center px-6 pt-12 pb-4 glass sticky top-0 z-20"> | |
| <div class="flex items-center gap-2"> | |
| <div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-primary to-blue-500 flex items-center justify-center"> | |
| <span class="material-symbols-rounded text-sm text-white">auto_awesome</span> | |
| </div> | |
| <span class="font-display font-bold text-lg">AR PIXELS</span> | |
| </div> | |
| <button onclick="router.navigate('settings')" class="p-2 rounded-full hover:bg-white/10 transition-colors"> | |
| <span class="material-symbols-rounded">settings</span> | |
| </button> | |
| </header> | |
| <!-- Scrollable Content --> | |
| <div class="flex-1 overflow-y-auto hide-scrollbar px-6 pb-32 pt-4"> | |
| <!-- Prompt Area --> | |
| <div class="mb-6 relative"> | |
| <label class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2 block">Prompt</label> | |
| <textarea id="prompt-input" rows="4" class="w-full bg-gray-200 dark:bg-white/5 border border-transparent dark:border-white/10 rounded-2xl p-4 text-base focus:outline-none focus:border-primary transition-colors resize-none placeholder-gray-500" placeholder="A futuristic city with flying cars in cyberpunk style, neon lights..."></textarea> | |
| <button onclick="app.surpriseMe()" class="absolute bottom-3 right-3 flex items-center gap-1 bg-white/10 hover:bg-white/20 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border border-white/5 backdrop-blur-sm"> | |
| <span class="material-symbols-rounded text-sm text-primary">shuffle</span> | |
| Surprise Me | |
| </button> | |
| </div> | |
| <!-- Aspect Ratio --> | |
| <div class="mb-8"> | |
| <label class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 block">Aspect Ratio</label> | |
| <div class="grid grid-cols-3 gap-3"> | |
| <button onclick="app.setRatio('9:16')" id="ratio-9-16" class="ratio-btn active flex flex-col items-center justify-center gap-2 p-3 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-all"> | |
| <div class="w-3 h-5 border-2 border-current rounded-sm opacity-50"></div> | |
| <span class="text-xs font-medium">9:16</span> | |
| </button> | |
| <button onclick="app.setRatio('1:1')" id="ratio-1-1" class="ratio-btn flex flex-col items-center justify-center gap-2 p-3 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-all"> | |
| <div class="w-4 h-4 border-2 border-current rounded-sm opacity-50"></div> | |
| <span class="text-xs font-medium">1:1</span> | |
| </button> | |
| <button onclick="app.setRatio('16:9')" id="ratio-16-9" class="ratio-btn flex flex-col items-center justify-center gap-2 p-3 rounded-xl border border-white/10 bg-white/5 hover:bg-white/10 transition-all"> | |
| <div class="w-5 h-3 border-2 border-current rounded-sm opacity-50"></div> | |
| <span class="text-xs font-medium">16:9</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Guidance Scale --> | |
| <div class="mb-6"> | |
| <div class="flex justify-between items-end mb-3"> | |
| <label class="text-xs font-bold text-gray-500 uppercase tracking-wider">Guidance Scale</label> | |
| <span id="guidance-value" class="text-primary font-display font-bold text-lg">7.5</span> | |
| </div> | |
| <input type="range" id="guidance-slider" min="1" max="20" step="0.5" value="7.5" class="w-full" oninput="app.updateSlider(this.value)"> | |
| <div class="flex justify-between mt-2 text-[10px] text-gray-500"> | |
| <span>Creative</span> | |
| <span>Balanced</span> | |
| <span>Strict</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Generate Button Area --> | |
| <div class="absolute bottom-[80px] left-0 w-full px-6 pb-4 pt-6 bg-gradient-to-t from-gray-100 dark:from-dark via-gray-100 dark:via-dark to-transparent z-10"> | |
| <button id="btn-generate" onclick="app.generateImage()" class="w-full bg-gradient-to-r from-primary to-blue-600 p-4 rounded-xl font-bold text-white shadow-lg shadow-primary/25 active:scale-95 transition-all flex items-center justify-center gap-2"> | |
| <span class="material-symbols-rounded">draw</span> | |
| <span>Generate Art</span> | |
| </button> | |
| </div> | |
| </section> | |
| <!-- SCREEN 3: RESULT DETAIL --> | |
| <section id="screen-result" class="fixed inset-0 z-[60] bg-dark hidden"> | |
| <!-- Fullscreen Image --> | |
| <div class="absolute inset-0 flex items-center justify-center bg-black"> | |
| <img id="result-image" src="" alt="Generated" class="max-w-full max-h-full object-contain opacity-0 transition-opacity duration-500"> | |
| </div> | |
| <!-- Top Controls --> | |
| <div class="absolute top-0 left-0 right-0 p-6 flex justify-between items-center bg-gradient-to-b from-black/80 to-transparent"> | |
| <button onclick="router.back()" class="w-10 h-10 rounded-full glass flex items-center justify-center text-white active:scale-90 transition-transform"> | |
| <span class="material-symbols-rounded">arrow_back</span> | |
| </button> | |
| <button onclick="app.downloadImage()" class="w-10 h-10 rounded-full glass flex items-center justify-center text-white active:scale-90 transition-transform"> | |
| <span class="material-symbols-rounded">download</span> | |
| </button> | |
| </div> | |
| <!-- Bottom Sheet --> | |
| <div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black via-black/90 to-transparent pt-20"> | |
| <div class="glass-panel p-4 rounded-2xl mb-4 border border-white/10"> | |
| <p class="text-[10px] text-gray-400 uppercase tracking-widest mb-1">Prompt Used</p> | |
| <p id="result-prompt" class="text-sm text-gray-200 leading-relaxed line-clamp-3">A cyberpunk city...</p> | |
| </div> | |
| <button onclick="router.back()" class="w-full bg-white text-black p-4 rounded-xl font-bold active:scale-95 transition-transform flex items-center justify-center gap-2"> | |
| <span class="material-symbols-rounded">refresh</span> | |
| Generate Another | |
| </button> | |
| </div> | |
| </section> | |
| <!-- SCREEN 4: GALLERY --> | |
| <section id="screen-gallery" class="h-full w-full hidden flex flex-col"> | |
| <header class="px-6 pt-12 pb-4 glass sticky top-0 z-20"> | |
| <h2 class="font-display font-bold text-2xl">Community</h2> | |
| </header> | |
| <div class="flex-1 overflow-y-auto hide-scrollbar p-4 pb-24"> | |
| <div id="gallery-grid" class="masonry-grid"> | |
| <!-- Gallery items injected via JS --> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- SCREEN 5: HISTORY --> | |
| <section id="screen-history" class="h-full w-full hidden flex flex-col"> | |
| <header class="flex justify-between items-end px-6 pt-12 pb-4 glass sticky top-0 z-20"> | |
| <h2 class="font-display font-bold text-2xl">My Creations</h2> | |
| <button onclick="app.clearHistory()" class="text-xs text-red-400 font-medium hover:text-red-300">Clear All</button> | |
| </header> | |
| <div class="flex-1 overflow-y-auto hide-scrollbar p-4 pb-24"> | |
| <div id="history-list" class="space-y-3"> | |
| <!-- History items injected via JS --> | |
| <div class="text-center mt-20 text-gray-500 text-sm hidden" id="empty-history"> | |
| No creations yet.<br>Start imagining! | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- SCREEN 6: SETTINGS --> | |
| <section id="screen-settings" class="h-full w-full hidden flex flex-col bg-white dark:bg-dark"> | |
| <header class="flex items-center gap-4 px-6 pt-12 pb-4 glass sticky top-0 z-20"> | |
| <button onclick="router.back()" class="active:scale-90 transition-transform"> | |
| <span class="material-symbols-rounded text-2xl">arrow_back</span> | |
| </button> | |
| <h2 class="font-display font-bold text-2xl">Settings</h2> | |
| </header> | |
| <div class="p-6 space-y-8"> | |
| <!-- Preferences --> | |
| <div> | |
| <h3 class="text-xs font-bold text-primary uppercase tracking-wider mb-4">Preferences</h3> | |
| <div class="glass-panel rounded-xl overflow-hidden"> | |
| <div class="flex items-center justify-between p-4 border-b border-white/5"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-rounded text-gray-400">dark_mode</span> | |
| <span class="font-medium">Dark Mode</span> | |
| </div> | |
| <button id="toggle-theme" onclick="app.toggleTheme()" class="w-12 h-6 bg-primary rounded-full relative transition-colors"> | |
| <div class="w-4 h-4 bg-white rounded-full absolute top-1 right-1 transition-all"></div> | |
| </button> | |
| </div> | |
| <div class="flex items-center justify-between p-4"> | |
| <div class="flex items-center gap-3"> | |
| <span class="material-symbols-rounded text-gray-400">notifications</span> | |
| <span class="font-medium">Notifications</span> | |
| </div> | |
| <button onclick="app.toggleNotif()" class="w-12 h-6 bg-primary rounded-full relative transition-colors" id="toggle-notif"> | |
| <div class="w-4 h-4 bg-white rounded-full absolute top-1 right-1 transition-all"></div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- About --> | |
| <div> | |
| <h3 class="text-xs font-bold text-primary uppercase tracking-wider mb-4">Support & About</h3> | |
| <div class="glass-panel rounded-xl overflow-hidden"> | |
| <button class="w-full flex items-center justify-between p-4 border-b border-white/5 hover:bg-white/5 text-left"> | |
| <span class="font-medium">Help Center</span> | |
| <span class="material-symbols-rounded text-gray-500 text-sm">chevron_right</span> | |
| </button> | |
| <button class="w-full flex items-center justify-between p-4 border-b border-white/5 hover:bg-white/5 text-left"> | |
| <span class="font-medium">Privacy Policy</span> | |
| <span class="material-symbols-rounded text-gray-500 text-sm">chevron_right</span> | |
| </button> | |
| <button class="w-full flex items-center justify-between p-4 hover:bg-white/5 text-left"> | |
| <span class="font-medium">Report a Bug</span> | |
| <span class="material-symbols-rounded text-gray-500 text-sm">chevron_right</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="text-center pt-8 opacity-50"> | |
| <p class="font-display font-bold text-sm tracking-widest">AR PIXELS</p> | |
| <p class="text-xs mt-1">Version 1.0.0</p> | |
| <p class="text-xs mt-4 text-primary">Founder: AR BRAINCODE</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- BOTTOM NAVIGATION --> | |
| <nav id="bottom-nav" class="fixed bottom-0 left-0 w-full glass z-40 hidden pb-safe"> | |
| <div class="grid grid-cols-3 h-16 items-center"> | |
| <button onclick="router.navigate('create')" id="nav-create" class="flex flex-col items-center gap-1 text-primary transition-colors"> | |
| <span class="material-symbols-rounded text-2xl filled-icon">add_circle</span> | |
| <span class="text-[10px] font-medium">Create</span> | |
| </button> | |
| <button onclick="router.navigate('gallery')" id="nav-gallery" class="flex flex-col items-center gap-1 text-gray-500 hover:text-gray-300 transition-colors"> | |
| <span class="material-symbols-rounded text-2xl">explore</span> | |
| <span class="text-[10px] font-medium">Gallery</span> | |
| </button> | |
| <button onclick="router.navigate('history')" id="nav-history" class="flex flex-col items-center gap-1 text-gray-500 hover:text-gray-300 transition-colors"> | |
| <span class="material-symbols-rounded text-2xl">history</span> | |
| <span class="text-[10px] font-medium">History</span> | |
| </button> | |
| </div> | |
| <!-- Safe area spacer for iPhone X+ --> | |
| <div class="h-4 w-full"></div> | |
| </nav> | |
| </div> | |
| <!-- JAVASCRIPT LOGIC --> | |
| <script> | |
| // --- DATA & STATE --- | |
| const config = { | |
| ratios: { | |
| '9:16': { w: 720, h: 1280 }, | |
| '1:1': { w: 1024, h: 1024 }, | |
| '16:9': { w: 1280, h: 720 } | |
| }, | |
| surprisePrompts: [ | |
| "A futuristic neon city with flying cars in cyberpunk style, detailed, 8k", | |
| "A cute robot gardener watering plants in a solarpunk greenhouse, soft lighting", | |
| "An astronaut meditating on Mars, galaxy background, surreal art", | |
| "Detailed portrait of a bioluminescent forest creature, fantasy style", | |
| "Vintage polaroid of a 1980s arcade, vaporwave aesthetic", | |
| "A majestic lion made of clouds in a sunset sky, cinematic lighting" | |
| ], | |
| galleryImages: [ | |
| { url: "https://image.pollinations.ai/prompt/cyberpunk%20girl?width=400&height=600&model=flux", h: "h-64" }, | |
| { url: "https://image.pollinations.ai/prompt/abstract%20fluid%20art?width=400&height=400&model=flux", h: "h-40" }, | |
| { url: "https://image.pollinations.ai/prompt/retro%20car%20sunset?width=400&height=300&model=flux", h: "h-32" }, | |
| { url: "https://image.pollinations.ai/prompt/space%20station?width=400&height=500&model=flux", h: "h-48" }, | |
| { url: "https://image.pollinations.ai/prompt/fantasy%20castle?width=400&height=600&model=flux", h: "h-56" }, | |
| { url: "https://image.pollinations.ai/prompt/underwater%20city?width=400&height=400&model=flux", h: "h-40" } | |
| ] | |
| }; | |
| const state = { | |
| currentScreen: 'onboarding', | |
| ratio: '9:16', | |
| guidance: 7.5, | |
| notifications: true, | |
| isGenerating: false | |
| }; | |
| // --- ROUTER --- | |
| const router = { | |
| historyStack: [], | |
| navigate(screenId) { | |
| // Haptic feedback | |
| if(navigator.vibrate) navigator.vibrate(10); | |
| // Add to history if moving forward normally | |
| if(screenId !== this.historyStack[this.historyStack.length - 1]) { | |
| this.historyStack.push(screenId); | |
| } | |
| // Hide all screens | |
| document.querySelectorAll('section').forEach(el => el.classList.add('hidden')); | |
| // Show new screen | |
| const screen = document.getElementById(`screen-${screenId}`); | |
| screen.classList.remove('hidden'); | |
| // Animation reset | |
| screen.classList.remove('animate-fade-in'); | |
| void screen.offsetWidth; // trigger reflow | |
| screen.classList.add('animate-fade-in'); | |
| // Update State | |
| state.currentScreen = screenId; | |
| // Handle Navbar | |
| const nav = document.getElementById('bottom-nav'); | |
| if(['create', 'gallery', 'history'].includes(screenId)) { | |
| nav.classList.remove('hidden'); | |
| this.updateNavHighlight(screenId); | |
| } else { | |
| nav.classList.add('hidden'); | |
| } | |
| // Load Data triggers | |
| if(screenId === 'history') app.loadHistory(); | |
| if(screenId === 'gallery') app.loadGallery(); | |
| }, | |
| back() { | |
| if(this.historyStack.length > 1) { | |
| this.historyStack.pop(); | |
| const prevScreen = this.historyStack[this.historyStack.length - 1]; | |
| this.navigate(prevScreen); | |
| // Remove duplicate push from navigate | |
| this.historyStack.pop(); | |
| } else { | |
| this.navigate('create'); | |
| } | |
| }, | |
| updateNavHighlight(screenId) { | |
| document.querySelectorAll('#bottom-nav button').forEach(btn => { | |
| btn.classList.remove('text-primary'); | |
| btn.classList.add('text-gray-500'); | |
| btn.querySelector('span.material-symbols-rounded').classList.remove('filled-icon'); | |
| }); | |
| const activeBtn = document.getElementById(`nav-${screenId}`); | |
| if(activeBtn) { | |
| activeBtn.classList.remove('text-gray-500'); | |
| activeBtn.classList.add('text-primary'); | |
| activeBtn.querySelector('span.material-symbols-rounded').classList.add('filled-icon'); | |
| } | |
| } | |
| }; | |
| // --- CORE LOGIC --- | |
| const app = { | |
| init() { | |
| // Check local storage for theme | |
| if (localStorage.getItem('theme') === 'light') { | |
| document.documentElement.classList.remove('dark'); | |
| } | |
| // Initial load | |
| this.loadGallery(); | |
| }, | |
| showToast(msg, type = 'info') { | |
| const toast = document.getElementById('toast-container'); | |
| const text = document.getElementById('toast-message'); | |
| const icon = document.getElementById('toast-icon'); | |
| text.innerText = msg; | |
| if(type === 'success') { icon.innerText = 'check_circle'; icon.className = 'material-symbols-rounded text-green-400'; } | |
| else if(type === 'error') { icon.innerText = 'error'; icon.className = 'material-symbols-rounded text-red-400'; } | |
| else { icon.innerText = 'info'; icon.className = 'material-symbols-rounded text-primary'; } | |
| toast.classList.add('show'); | |
| if(navigator.vibrate) navigator.vibrate(20); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| }, | |
| setRatio(ratio) { | |
| state.ratio = ratio; | |
| document.querySelectorAll('.ratio-btn').forEach(btn => { | |
| btn.classList.remove('border-primary', 'text-primary', 'bg-primary/10'); | |
| btn.classList.add('border-white/10', 'bg-white/5'); | |
| }); | |
| const activeBtn = document.getElementById(`ratio-${ratio.replace(':','-')}`); | |
| activeBtn.classList.remove('border-white/10', 'bg-white/5'); | |
| activeBtn.classList.add('border-primary', 'text-primary', 'bg-primary/10'); | |
| if(navigator.vibrate) navigator.vibrate(5); | |
| }, | |
| updateSlider(val) { | |
| state.guidance = val; | |
| document.getElementById('guidance-value').innerText = val; | |
| }, | |
| surpriseMe() { | |
| const prompt = config.surprisePrompts[Math.floor(Math.random() * config.surprisePrompts.length)]; | |
| document.getElementById('prompt-input').value = prompt; | |
| if(navigator.vibrate) navigator.vibrate(10); | |
| }, | |
| async generateImage() { | |
| const promptInput = document.getElementById('prompt-input'); | |
| const prompt = promptInput.value.trim(); | |
| const btn = document.getElementById('btn-generate'); | |
| const btnText = btn.querySelector('span:last-child'); | |
| const btnIcon = btn.querySelector('span:first-child'); | |
| if(!prompt) { | |
| this.showToast('Please enter a prompt first', 'error'); | |
| return; | |
| } | |
| // UI Loading State | |
| state.isGenerating = true; | |
| btn.disabled = true; | |
| btnText.innerText = "Dreaming..."; | |
| btnIcon.innerText = "hourglass_empty"; | |
| btnIcon.classList.add('animate-spin'); | |
| const { w, h } = config.ratios[state.ratio]; | |
| const seed = Math.floor(Math.random() * 100000); | |
| const encodedPrompt = encodeURIComponent(prompt); | |
| // Pollinations API URL | |
| const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=${w}&height=${h}&seed=${seed}&nologo=true&model=flux`; | |
| try { | |
| // Preload image | |
| await new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(img); | |
| img.onerror = () => reject(new Error("Generation failed")); | |
| img.src = imageUrl; | |
| }); | |
| // Success | |
| const resultImg = document.getElementById('result-image'); | |
| resultImg.src = imageUrl; | |
| resultImg.style.opacity = '0'; | |
| document.getElementById('result-prompt').innerText = prompt; | |
| // Save to history | |
| this.saveToHistory({ | |
| url: imageUrl, | |
| prompt: prompt, | |
| timestamp: new Date().getTime() | |
| }); | |
| // Navigate | |
| router.navigate('result'); | |
| // Fade in image | |
| setTimeout(() => { resultImg.style.opacity = '1'; }, 100); | |
| this.showToast('Image generated successfully!', 'success'); | |
| } catch (e) { | |
| this.showToast('Failed to generate. Try again.', 'error'); | |
| } finally { | |
| // Reset UI | |
| state.isGenerating = false; | |
| btn.disabled = false; | |
| btnText.innerText = "Generate Art"; | |
| btnIcon.innerText = "draw"; | |
| btnIcon.classList.remove('animate-spin'); | |
| } | |
| }, | |
| saveToHistory(item) { | |
| let history = JSON.parse(localStorage.getItem('ar_pixels_history') || '[]'); | |
| history.unshift(item); // Add to beginning | |
| if(history.length > 20) history.pop(); // Limit to 20 | |
| localStorage.setItem('ar_pixels_history', JSON.stringify(history)); | |
| }, | |
| loadHistory() { | |
| const history = JSON.parse(localStorage.getItem('ar_pixels_history') || '[]'); | |
| const container = document.getElementById('history-list'); | |
| const emptyMsg = document.getElementById('empty-history'); | |
| // Keep the empty message element, remove others | |
| Array.from(container.children).forEach(child => { | |
| if(child.id !== 'empty-history') child.remove(); | |
| }); | |
| if(history.length === 0) { | |
| emptyMsg.classList.remove('hidden'); | |
| return; | |
| } | |
| emptyMsg.classList.add('hidden'); | |
| history.forEach(item => { | |
| const el = document.createElement('div'); | |
| el.className = "flex items-center gap-4 bg-white/5 p-3 rounded-xl border border-white/5 active:scale-98 transition-transform cursor-pointer"; | |
| el.onclick = () => { | |
| document.getElementById('result-image').src = item.url; | |
| document.getElementById('result-prompt').innerText = item.prompt; | |
| router.navigate('result'); | |
| setTimeout(() => { document.getElementById('result-image').style.opacity = '1'; }, 100); | |
| }; | |
| el.innerHTML = ` | |
| <img src="${item.url}" class="w-16 h-16 rounded-lg object-cover bg-gray-800"> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-sm font-medium text-white truncate">${item.prompt}</p> | |
| <p class="text-xs text-gray-500 mt-1">${new Date(item.timestamp).toLocaleDateString()}</p> | |
| </div> | |
| <span class="material-symbols-rounded text-gray-500">chevron_right</span> | |
| `; | |
| container.appendChild(el); | |
| }); | |
| }, | |
| clearHistory() { | |
| if(confirm("Delete all history?")) { | |
| localStorage.removeItem('ar_pixels_history'); | |
| this.loadHistory(); | |
| this.showToast('History cleared', 'info'); | |
| } | |
| }, | |
| loadGallery() { | |
| const container = document.getElementById('gallery-grid'); | |
| if(container.children.length > 0) return; // Prevent duplicates | |
| config.galleryImages.forEach(item => { | |
| const div = document.createElement('div'); | |
| div.className = "masonry-item rounded-xl overflow-hidden relative group"; | |
| div.innerHTML = ` | |
| <img src="${item.url}" loading="lazy" class="w-full h-auto object-cover bg-gray-800 transition-transform duration-700 group-hover:scale-110"> | |
| <div class="absolute inset-0 bg-black/20 group-hover:bg-black/0 transition-colors"></div> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| }, | |
| async downloadImage() { | |
| const img = document.getElementById('result-image'); | |
| try { | |
| const response = await fetch(img.src); | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| a.download = `AR-PIXELS-${Date.now()}.jpg`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| this.showToast('Image saved to device', 'success'); | |
| } catch (e) { | |
| this.showToast('Download failed', 'error'); | |
| } | |
| }, | |
| toggleTheme() { | |
| const html = document.documentElement; | |
| const toggle = document.getElementById('toggle-theme').querySelector('div'); | |
| if (html.classList.contains('dark')) { | |
| html.classList.remove('dark'); | |
| localStorage.setItem('theme', 'light'); | |
| toggle.style.right = 'auto'; | |
| toggle.style.left = '4px'; | |
| } else { | |
| html.classList.add('dark'); | |
| localStorage.setItem('theme', 'dark'); | |
| toggle.style.left = 'auto'; | |
| toggle.style.right = '4px'; | |
| } | |
| if(navigator.vibrate) navigator.vibrate(10); | |
| }, | |
| toggleNotif() { | |
| state.notifications = !state.notifications; | |
| const toggle = document.getElementById('toggle-notif').querySelector('div'); | |
| if (state.notifications) { | |
| toggle.style.left = 'auto'; | |
| toggle.style.right = '4px'; | |
| this.showToast('Notifications On', 'success'); | |
| } else { | |
| toggle.style.right = 'auto'; | |
| toggle.style.left = '4px'; | |
| this.showToast('Notifications Off', 'info'); | |
| } | |
| if(navigator.vibrate) navigator.vibrate(10); | |
| } | |
| }; | |
| // Initialize App | |
| window.addEventListener('load', () => { | |
| app.init(); | |
| // Highlight default aspect ratio | |
| app.setRatio('9:16'); | |
| // Setup Theme Toggle visual state | |
| if(!document.documentElement.classList.contains('dark')) { | |
| const t = document.getElementById('toggle-theme').querySelector('div'); | |
| t.style.right = 'auto'; t.style.left = '4px'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |