Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DreamCanvas Pro | Advanced AI Imaging</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-body: #050507; | |
| --bg-panel: #0f0f12; | |
| --bg-element: #18181b; | |
| --bg-input: #09090b; | |
| --border: #27272a; | |
| --border-light: #3f3f46; | |
| --primary: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --accent: #ec4899; | |
| --text-main: #fafafa; | |
| --text-muted: #a1a1aa; | |
| --radius: 8px; | |
| --font-main: 'Inter', sans-serif; | |
| --font-mono: 'JetBrains Mono', monospace; | |
| --glass: rgba(15, 15, 18, 0.85); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| font-family: var(--font-main); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| height: 56px; | |
| background: var(--bg-panel); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| z-index: 50; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| font-size: 1.1rem; | |
| } | |
| .brand i { | |
| color: var(--accent); | |
| } | |
| .anycoder-badge { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| background: var(--bg-element); | |
| padding: 4px 10px; | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| transition: 0.2s; | |
| } | |
| .anycoder-badge:hover { | |
| border-color: var(--primary); | |
| color: var(--text-main); | |
| } | |
| /* --- Layout --- */ | |
| .workspace { | |
| display: flex; | |
| height: calc(100vh - 56px); | |
| } | |
| /* --- Sidebar (Controls) --- */ | |
| aside { | |
| width: 320px; | |
| background: var(--bg-panel); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow-y: auto; | |
| flex-shrink: 0; | |
| } | |
| .sidebar-section { | |
| padding: 20px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .section-title { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| /* Aspect Ratio Grid */ | |
| .ratio-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 8px; | |
| } | |
| .ratio-btn { | |
| background: var(--bg-element); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| height: 60px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| } | |
| .ratio-btn:hover { | |
| background: var(--border); | |
| } | |
| .ratio-btn.active { | |
| border-color: var(--primary); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| .ratio-preview { | |
| border: 1.5px solid var(--text-muted); | |
| border-radius: 2px; | |
| margin-bottom: 4px; | |
| } | |
| .ratio-btn.active .ratio-preview { | |
| border-color: var(--primary); | |
| } | |
| .r-1-1 { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .r-16-9 { | |
| width: 20px; | |
| height: 11px; | |
| } | |
| .r-9-16 { | |
| width: 11px; | |
| height: 20px; | |
| } | |
| .r-21-9 { | |
| width: 22px; | |
| height: 9px; | |
| } | |
| .ratio-label { | |
| font-size: 0.65rem; | |
| color: var(--text-muted); | |
| } | |
| /* Collapsible Settings */ | |
| details { | |
| margin-bottom: 10px; | |
| } | |
| summary { | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| padding: 8px 0; | |
| list-style: none; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| summary::-webkit-details-marker { | |
| display: none; | |
| } | |
| summary::after { | |
| content: '\f078'; | |
| font-family: "Font Awesome 6 Free"; | |
| font-weight: 900; | |
| font-size: 0.7rem; | |
| transition: transform 0.2s; | |
| } | |
| details[open] summary::after { | |
| transform: rotate(180deg); | |
| } | |
| .control-group { | |
| margin-top: 10px; | |
| display: grid; | |
| gap: 10px; | |
| } | |
| label { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| display: block; | |
| margin-bottom: 4px; | |
| } | |
| select, | |
| input[type="text"], | |
| input[type="number"] { | |
| width: 100%; | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| padding: 8px 12px; | |
| border-radius: var(--radius); | |
| font-family: var(--font-main); | |
| font-size: 0.85rem; | |
| } | |
| select:focus, | |
| input:focus { | |
| border-color: var(--primary); | |
| } | |
| .seed-row { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .icon-btn { | |
| width: 34px; | |
| background: var(--bg-element); | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .icon-btn:hover { | |
| color: var(--text-main); | |
| border-color: var(--text-muted); | |
| } | |
| /* Toggle Switch */ | |
| .toggle-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| padding: 8px; | |
| background: var(--bg-input); | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 40px; | |
| height: 22px; | |
| margin-bottom: 0; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: var(--bg-element); | |
| border: 1px solid var(--border); | |
| transition: .4s; | |
| border-radius: 34px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 14px; | |
| width: 14px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: var(--text-muted); | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked+.slider { | |
| background-color: rgba(99, 102, 241, 0.2); | |
| border-color: var(--primary); | |
| } | |
| input:checked+.slider:before { | |
| transform: translateX(18px); | |
| background-color: var(--primary); | |
| } | |
| /* --- Main Canvas --- */ | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: radial-gradient(circle at center, #121215 0%, #050507 100%); | |
| position: relative; | |
| } | |
| /* Gallery */ | |
| .gallery-container { | |
| flex: 1; | |
| padding: 30px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column-reverse; | |
| /* Newest at bottom, close to input */ | |
| gap: 30px; | |
| align-items: center; | |
| } | |
| .gallery-container::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .gallery-container::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| .image-card { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 12px; | |
| width: 100%; | |
| max-width: 800px; | |
| box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); | |
| animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .img-display { | |
| width: 100%; | |
| background: #000; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .img-display img { | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| opacity: 0; | |
| transition: opacity 0.5s; | |
| } | |
| .img-display img.loaded { | |
| opacity: 1; | |
| } | |
| .card-meta { | |
| margin-top: 12px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| gap: 15px; | |
| } | |
| .prompt-text { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| line-height: 1.5; | |
| font-family: var(--font-mono); | |
| } | |
| .card-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| /* Loading State */ | |
| .loader { | |
| position: absolute; | |
| width: 48px; | |
| height: 48px; | |
| border: 3px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 50%; | |
| border-top-color: var(--primary); | |
| animation: spin 1s ease-in-out infinite; | |
| z-index: 10; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| /* --- Input Area --- */ | |
| .input-area { | |
| background: var(--bg-panel); | |
| border-top: 1px solid var(--border); | |
| padding: 20px 30px; | |
| display: flex; | |
| gap: 15px; | |
| align-items: flex-start; | |
| z-index: 20; | |
| } | |
| .textarea-wrapper { | |
| flex: 1; | |
| position: relative; | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| transition: 0.2s; | |
| } | |
| .textarea-wrapper:focus-within { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | |
| } | |
| textarea { | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-main); | |
| padding: 15px; | |
| font-family: var(--font-main); | |
| font-size: 1rem; | |
| resize: none; | |
| height: 60px; | |
| min-height: 60px; | |
| max-height: 150px; | |
| } | |
| textarea::placeholder { | |
| color: #52525b; | |
| } | |
| .generate-btn { | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| height: 60px; | |
| padding: 0 30px; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| white-space: nowrap; | |
| } | |
| .generate-btn:hover { | |
| filter: brightness(1.1); | |
| transform: translateY(-1px); | |
| } | |
| .generate-btn:active { | |
| transform: translateY(0); | |
| } | |
| .generate-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| filter: grayscale(1); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .workspace { | |
| flex-direction: column; | |
| } | |
| aside { | |
| width: 100%; | |
| height: auto; | |
| max-height: 200px; | |
| border-right: none; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .input-area { | |
| padding: 15px; | |
| } | |
| .generate-btn span { | |
| display: none; | |
| } | |
| .generate-btn { | |
| padding: 0 20px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-bolt"></i> | |
| <span>DreamCanvas Pro</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-badge">Built with | |
| anycoder</a> | |
| </header> | |
| <div class="workspace"> | |
| <!-- Sidebar Controls --> | |
| <aside> | |
| <div class="sidebar-section"> | |
| <div class="section-title">Aspect Ratio</div> | |
| <div class="ratio-grid"> | |
| <div class="ratio-btn active" onclick="setRatio('1:1', this)"> | |
| <div class="ratio-preview r-1-1"></div> | |
| <span class="ratio-label">1:1</span> | |
| </div> | |
| <div class="ratio-btn" onclick="setRatio('16:9', this)"> | |
| <div class="ratio-preview r-16-9"></div> | |
| <span class="ratio-label">16:9</span> | |
| </div> | |
| <div class="ratio-btn" onclick="setRatio('9:16', this)"> | |
| <div class="ratio-preview r-9-16"></div> | |
| <span class="ratio-label">9:16</span> | |
| </div> | |
| <div class="ratio-btn" onclick="setRatio('21:9', this)"> | |
| <div class="ratio-preview r-21-9"></div> | |
| <span class="ratio-label">21:9</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="section-title">Generation Settings</div> | |
| <div class="toggle-row"> | |
| <div style="display:flex; flex-direction:column;"> | |
| <label style="margin:0; color: var(--text-main); font-weight:500;">Auto-Enhance Prompt</label> | |
| <span style="font-size: 0.65rem; color: var(--text-muted);">Allows AI to rewrite prompt</span> | |
| </div> | |
| <label class="toggle-switch"> | |
| <!-- Removed checked attribute to default to strict mode --> | |
| <input type="checkbox" id="magic-enhance"> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <div class="control-group"> | |
| <label>Model</label> | |
| <select id="model-select"> | |
| <option value="flux">Flux (Best for Text/Realism)</option> | |
| <option value="turbo">SDXL Turbo (Fast)</option> | |
| <option value="midjourney">Midjourney V6 Style</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Seed (Consistency)</label> | |
| <div class="seed-row"> | |
| <input type="number" id="seed-input" value="-1" placeholder="-1 for Random"> | |
| <button class="icon-btn" onclick="randomizeSeed()" title="New Random Seed"> | |
| <i class="fa-solid fa-dice"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <details> | |
| <summary>Camera & Lens</summary> | |
| <div class="control-group"> | |
| <label>Focal Length</label> | |
| <select id="camera-lens"> | |
| <option value="">Default</option> | |
| <option value="14mm wide angle lens">14mm Wide Angle</option> | |
| <option value="35mm lens">35mm Street</option> | |
| <option value="50mm lens">50mm Portrait</option> | |
| <option value="85mm lens">85mm Bokeh</option> | |
| <option value="200mm telephoto lens">200mm Telephoto</option> | |
| <option value="macro lens">Macro</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Aperture / Depth</label> | |
| <select id="camera-aperture"> | |
| <option value="">Default</option> | |
| <option value="f/1.8, shallow depth of field, bokeh">f/1.8 (Blurry BG)</option> | |
| <option value="f/8, deep depth of field, sharp focus">f/8 (Sharp All)</option> | |
| </select> | |
| </div> | |
| </details> | |
| <details> | |
| <summary>Lighting & Atmosphere</summary> | |
| <div class="control-group"> | |
| <label>Lighting Style</label> | |
| <select id="light-style"> | |
| <option value="">Default</option> | |
| <option value="cinematic lighting, dramatic shadows">Cinematic</option> | |
| <option value="golden hour, warm sunlight">Golden Hour</option> | |
| <option value="studio lighting, softbox, rim light">Studio Professional</option> | |
| <option value="neon lighting, cyberpunk, volumetric fog">Neon/Cyberpunk</option> | |
| <option value="natural lighting, overcast, soft">Natural/Soft</option> | |
| </select> | |
| </div> | |
| </details> | |
| <details> | |
| <summary>Artistic Style</summary> | |
| <div class="control-group"> | |
| <label>Medium</label> | |
| <select id="art-style"> | |
| <option value="photorealistic, 8k, highly detailed">Photorealistic (Default)</option> | |
| <option value="anime style, studio ghibli, vibrant">Anime</option> | |
| <option value="digital painting, concept art, artstation">Digital Art</option> | |
| <option value="oil painting, textured, impasto">Oil Painting</option> | |
| <option value="3d render, unreal engine 5, octane render">3D Render</option> | |
| <option value="pencil sketch, charcoal, graphite">Sketch</option> | |
| </select> | |
| </div> | |
| </details> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main> | |
| <div class="gallery-container" id="gallery"> | |
| <!-- Images will appear here --> | |
| <div style="text-align: center; color: var(--text-muted); margin-top: auto; margin-bottom: auto;"> | |
| <i class="fa-solid fa-layer-group" style="font-size: 3rem; margin-bottom: 15px; opacity: 0.2;"></i> | |
| <p>Start creating with DreamCanvas Pro</p> | |
| <p style="font-size: 0.8rem; margin-top: 8px; opacity: 0.5;">Strict Mode Active by Default</p> | |
| </div> | |
| </div> | |
| <div class="input-area"> | |
| <div class="textarea-wrapper"> | |
| <textarea id="prompt-input" placeholder="Describe your imagination... (e.g. 'A futuristic city on water')"></textarea> | |
| </div> | |
| <button class="generate-btn" id="gen-btn" onclick="generate()"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> | |
| <span>Generate</span> | |
| </button> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| // --- Application State --- | |
| let state = { | |
| ratio: '1:1', | |
| width: 1024, | |
| height: 1024, | |
| isGenerating: false | |
| }; | |
| // --- DOM Elements --- | |
| const els = { | |
| prompt: document.getElementById('prompt-input'), | |
| gallery: document.getElementById('gallery'), | |
| btn: document.getElementById('gen-btn'), | |
| seed: document.getElementById('seed-input'), | |
| model: document.getElementById('model-select'), | |
| lens: document.getElementById('camera-lens'), | |
| aperture: document.getElementById('camera-aperture'), | |
| lighting: document.getElementById('light-style'), | |
| style: document.getElementById('art-style'), | |
| enhance: document.getElementById('magic-enhance') | |
| }; | |
| // --- Logic --- | |
| function setRatio(ratioStr, btn) { | |
| state.ratio = ratioStr; | |
| // Update UI | |
| document.querySelectorAll('.ratio-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Set Dimensions | |
| switch (ratioStr) { | |
| case '1:1': state.width = 1024; state.height = 1024; break; | |
| case '16:9': state.width = 1280; state.height = 720; break; | |
| case '9:16': state.width = 720; state.height = 1280; break; | |
| case '21:9': state.width = 1408; state.height = 603; break; | |
| } | |
| } | |
| function randomizeSeed() { | |
| els.seed.value = Math.floor(Math.random() * 99999999); | |
| } | |
| function constructPrompt(basePrompt) { | |
| let parts = [basePrompt]; | |
| // Add Style | |
| if (els.style.value) parts.push(els.style.value); | |
| // Add Camera/Lighting | |
| if (els.lens.value) parts.push(els.lens.value); | |
| if (els.aperture.value) parts.push(els.aperture.value); | |
| if (els.lighting.value) parts.push(els.lighting.value); | |
| // NOTE: Removed manual appending of "masterpiece" tags. | |
| // We now rely on the API's enhance parameter to handle creativity vs strictness. | |
| return parts.join(', '); | |
| } | |
| async function generate() { | |
| if (state.isGenerating) return; | |
| const rawPrompt = els.prompt.value.trim(); | |
| if (!rawPrompt) { | |
| alert("Please enter a prompt."); | |
| return; | |
| } | |
| // Start UI Loading | |
| state.isGenerating = true; | |
| els.btn.disabled = true; | |
| els.btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> <span>Processing...</span>`; | |
| // Prepare Parameters | |
| const seed = els.seed.value && els.seed.value != "-1" ? els.seed.value : Math.floor(Math.random() * 999999); | |
| const fullPrompt = constructPrompt(rawPrompt); | |
| const encodedPrompt = encodeURIComponent(fullPrompt); | |
| const model = els.model.value; // 'flux', 'turbo', etc. | |
| // Determine enhancement mode (True = AI Rewrites prompt, False = Strict adherence) | |
| const isEnhanced = els.enhance.checked; | |
| // Pollinations API URL with explicit enhance flag | |
| const url = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=${state.width}&height=${state.height}&seed=${seed}&model=${model}&nologo=true&enhance=${isEnhanced}`; | |
| // Create Gallery Item | |
| const cardId = `card-${Date.now()}`; | |
| const cardHTML = ` | |
| <div class="image-card" id="${cardId}"> | |
| <div class="img-display" style="aspect-ratio: ${state.ratio.replace(':', '/')};"> | |
| <div class="loader"></div> | |
| <img src="${url}" alt="${rawPrompt}" crossorigin="anonymous"> | |
| </div> | |
| <div class="card-meta"> | |
| <div> | |
| <div style="font-size: 0.75rem; color: var(--primary); margin-bottom: 4px; font-weight:bold;"> | |
| ${state.ratio} • Seed: ${seed} • ${isEnhanced ? '<i class="fa-solid fa-wand-magic-sparkles"></i> Enhanced' : '<i class="fa-solid fa-lock"></i> Strict'} | |
| </div> | |
| <p class="prompt-text">${rawPrompt}</p> | |
| </div> | |
| <div class="card-actions"> | |
| <button class="icon-btn" onclick="downloadImage('${url}', '${seed}')" title="Download High Res"> | |
| <i class="fa-solid fa-download"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Remove "Start creating" placeholder if exists | |
| if (els.gallery.children.length > 0 && els.gallery.children[0].tagName === 'DIV' && els.gallery.children[0].style.textAlign === 'center') { | |
| els.gallery.innerHTML = ''; | |
| } | |
| // Insert new card | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = cardHTML; | |
| const newCard = tempDiv.firstElementChild; | |
| els.gallery.appendChild(newCard); | |
| // Handle Load Event | |
| const img = newCard.querySelector('img'); | |
| const loader = newCard.querySelector('.loader'); | |
| img.onload = () => { | |
| img.classList.add('loaded'); | |
| loader.style.display = 'none'; | |
| state.isGenerating = false; | |
| els.btn.disabled = false; | |
| els.btn.innerHTML = `<i class="fa-solid fa-wand-magic-sparkles"></i> <span>Generate</span>`; | |
| // Scroll to bottom of gallery to see new image | |
| els.gallery.scrollTop = els.gallery.scrollHeight; | |
| }; | |
| img.onerror = () => { | |
| loader.style.display = 'none'; | |
| newCard.innerHTML += `<div style="padding:20px; color: #ef4444; text-align:center;">Generation Failed. Try again.</div>`; | |
| state.isGenerating = false; | |
| els.btn.disabled = false; | |
| els.btn.innerHTML = `<i class="fa-solid fa-wand-magic-sparkles"></i> <span>Generate</span>`; | |
| }; | |
| } | |
| // Download Helper | |
| async function downloadImage(url, seed) { | |
| try { | |
| const response = await fetch(url |