Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RGB Spectrum Generator</title> | |
| <!-- Import FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --accent: #3b82f6; | |
| --accent-hover: #2563eb; | |
| --border: #334155; | |
| --success: #10b981; | |
| --font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: var(--font-family); | |
| background-color: var(--bg-color); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| header { | |
| background-color: rgba(30, 41, 59, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| } | |
| .brand i { | |
| color: var(--accent); | |
| font-size: 1.5rem; | |
| } | |
| .anycoder-link { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--accent); | |
| border-color: var(--accent); | |
| background-color: rgba(59, 130, 246, 0.1); | |
| } | |
| /* Main Content */ | |
| main { | |
| flex: 1; | |
| padding: 2rem; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| .controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: var(--card-bg); | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .control-group { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| } | |
| .status-text { | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| border: none; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.2s ease; | |
| font-size: 0.95rem; | |
| } | |
| .btn-primary { | |
| background-color: var(--accent); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--accent-hover); | |
| transform: translateY(-1px); | |
| } | |
| .btn-secondary { | |
| background-color: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-primary); | |
| } | |
| .btn-secondary:hover { | |
| background-color: rgba(255, 255, 255, 0.05); | |
| border-color: var(--text-secondary); | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| /* Progress Bar */ | |
| .progress-container { | |
| width: 100%; | |
| height: 6px; | |
| background-color: var(--bg-color); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| margin-top: 1rem; | |
| display: none; /* Hidden by default */ | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background-color: var(--success); | |
| width: 0%; | |
| transition: width 0.1s linear; | |
| } | |
| /* Grid Layout */ | |
| .gallery { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| /* Card Design */ | |
| .color-card { | |
| background-color: var(--card-bg); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| display: flex; | |
| flex-direction: column; | |
| animation: fadeIn 0.5s ease forwards; | |
| opacity: 0; | |
| } | |
| @keyframes fadeIn { | |
| to { opacity: 1; } | |
| } | |
| .color-card:hover { | |
| transform: translateY(-4px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); | |
| border-color: var(--accent); | |
| } | |
| .color-preview { | |
| height: 180px; | |
| width: 100%; | |
| position: relative; | |
| background-image: | |
| linear-gradient(45deg, #ccc 25%, transparent 25%), | |
| linear-gradient(-45deg, #ccc 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #ccc 75%), | |
| linear-gradient(-45deg, transparent 75%, #ccc 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0px; | |
| } | |
| .color-swatch { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .color-info { | |
| padding: 1rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .color-values { | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .filename-label { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.5rem; | |
| font-size: 0.9rem; | |
| } | |
| .card-actions { | |
| margin-top: 0.5rem; | |
| } | |
| .btn-sm { | |
| padding: 0.4rem 0.8rem; | |
| font-size: 0.8rem; | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| background-color: var(--card-bg); | |
| border: 1px solid var(--accent); | |
| color: var(--text-primary); | |
| padding: 1rem 1.5rem; | |
| border-radius: 8px; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5); | |
| transform: translateY(150%); | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .toast.show { | |
| transform: translateY(0); | |
| } | |
| .toast i { | |
| color: var(--success); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .controls { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .control-group { | |
| flex-direction: column; | |
| width: 100%; | |
| } | |
| .btn { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .gallery { | |
| grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-palette"></i> | |
| <span>RGB Spectrum Gen</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| <i class="fa-solid fa-code"></i> Built with anycoder | |
| </a> | |
| </header> | |
| <main> | |
| <section class="controls"> | |
| <div class="control-group"> | |
| <div> | |
| <h2 style="margin-bottom: 0.5rem;">Spectrum Generator</h2> | |
| <p class="status-text" id="statusText">Ready to generate 500 unique shades.</p> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <button id="generateBtn" class="btn btn-primary"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> Generate Images | |
| </button> | |
| <button id="downloadAllBtn" class="btn btn-secondary" disabled> | |
| <i class="fa-solid fa-download"></i> Download All | |
| </button> | |
| </div> | |
| <div class="progress-container" id="progressContainer"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| </div> | |
| </section> | |
| <section id="gallery" class="gallery"> | |
| <!-- Images will be injected here --> | |
| </section> | |
| </main> | |
| <div id="toast" class="toast"> | |
| <i class="fa-solid fa-circle-check"></i> | |
| <span id="toastMessage">Operation successful</span> | |
| </div> | |
| <script> | |
| /** | |
| * RGB Spectrum Generator | |
| * Uses HTML5 Canvas to generate 256x256 PNGs | |
| */ | |
| const CONFIG = { | |
| count: 500, | |
| width: 256, | |
| height: 256, | |
| delayBetweenDownloads: 150, // ms to prevent browser blocking | |
| }; | |
| const elements = { | |
| gallery: document.getElementById('gallery'), | |
| generateBtn: document.getElementById('generateBtn'), | |
| downloadAllBtn: document.getElementById('downloadAllBtn'), | |
| statusText: document.getElementById('statusText'), | |
| progressBar: document.getElementById('progressBar'), | |
| progressContainer: document.getElementById('progressContainer'), | |
| toast: document.getElementById('toast'), | |
| toastMessage: document.getElementById('toastMessage') | |
| }; | |
| // Store generated data to avoid re-rendering canvas | |
| let generatedColors = []; | |
| /** | |
| * Convert HSL to RGB object | |
| * h, s, l are in [0, 1] | |
| * returns {r, g, b} in [0, 255] | |
| */ | |
| function hslToRgb(h, s, l) { | |
| let r, g, b; | |
| if (s === 0) { | |
| r = g = b = l; // achromatic | |
| } else { | |
| const hue2rgb = (p, q, t) => { | |
| if (t < 0) t += 1; | |
| if (t > 1) t -= 1; | |
| if (t < 1/6) return p + (q - p) * 6 * t; | |
| if (t < 1/2) return q; | |
| if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; | |
| return p; | |
| }; | |
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
| const p = 2 * l - q; | |
| r = hue2rgb(p, q, h + 1/3); | |
| g = hue2rgb(p, q, h); | |
| b = hue2rgb(p, q, h - 1/3); | |
| } | |
| return { | |
| r: Math.round(r * 255), | |
| g: Math.round(g * 255), | |
| b: Math.round(b * 255) | |
| }; | |
| } | |
| /** | |
| * Generate Color Palette | |
| * Creates 500 colors distributed across the spectrum with varying lightness (shades) | |
| */ | |
| function generateColorPalette() { | |
| const colors = []; | |
| for (let i = 0; i < CONFIG.count; i++) { | |
| // Distribute Hue evenly 0 -> 1 | |
| const h = i / CONFIG.count; | |
| // Oscillate Saturation between 0.6 and 1.0 for vibrancy | |
| const s = 0.6 + (Math.sin(i * 0.1) * 0.2 + 0.2); | |
| // Oscillate Lightness between 0.2 and 0.8 to get shades | |
| // Using cosine to ensure we start and end reasonably visible | |
| const l = 0.5 + (Math.cos(i * 0.05) * 0.3); | |
| const rgb = hslToRgb(h, s, l); | |
| // Create CSS string | |
| const cssString = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; | |
| // Create Filename: rgb(r,g,b).png | |
| const filename = `rgb(${rgb.r},${rgb.g},${rgb.b}).png`; | |
| colors.push({ | |
| rgb: rgb, | |
| css: cssString, | |
| filename: filename, | |
| id: i | |
| }); | |
| } | |
| return colors; | |
| } | |
| /** | |
| * Create a 256x256 PNG Data URL from a color string | |
| */ | |
| function createImageDataURL(colorCss) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = CONFIG.width; | |
| canvas.height = CONFIG.height; | |
| const ctx = canvas.getContext('2d'); | |
| // Fill background | |
| ctx.fillStyle = colorCss; | |
| ctx.fillRect(0, 0, CONFIG.width, CONFIG.height); | |
| return canvas.toDataURL('image/png'); | |
| } | |
| /** | |
| * Render the Grid | |
| */ | |
| async function renderGallery() { | |
| elements.gallery.innerHTML = ''; | |
| elements.statusText.textContent = `Generating ${CONFIG.count} images...`; | |
| elements.progressContainer.style.display = 'block'; | |
| elements.generateBtn.disabled = true; | |
| elements.downloadAllBtn.disabled = true; | |
| generatedColors = generateColorPalette(); | |
| // Process in batches to keep UI responsive | |
| const batchSize = 20; | |
| let processed = 0; | |
| for (let i = 0; i < generatedColors.length; i += batchSize) { | |
| const batch = generatedColors.slice(i, i + batchSize); | |
| const fragment = document.createDocumentFragment(); | |
| batch.forEach(color => { | |
| const card = document.createElement('article'); | |
| card.className = 'color-card'; | |
| // Generate DataURL | |
| const dataUrl = createImageDataURL(color.css); | |
| card.innerHTML = ` | |
| <div class="color-preview"> | |
| <div class="color-swatch" style="background-color: ${color.css};"></div> | |
| </div> | |
| <div class="color-info"> | |
| <div class="filename-label">${color.filename}</div> | |
| <div class="color-values"> | |
| R: ${color.rgb.r} G: ${color.rgb.g} B: ${color.rgb.b} | |
| </div> | |
| <div class="card-actions"> | |
| <button class="btn btn-secondary btn-sm" onclick="downloadSingle('${dataUrl}', '${color.filename}')"> | |
| <i class="fa-solid fa-download"></i> Download | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| fragment.appendChild(card); | |
| }); | |
| elements.gallery.appendChild(fragment); | |
| // Update Progress | |
| processed += batch.length; | |
| const percent = (processed / CONFIG.count) * 100; | |
| elements.progressBar.style.width = `${percent}%`; | |
| // Allow UI to breathe | |
| await new Promise(r => setTimeout(r, 10)); | |
| } | |
| elements.statusText.textContent = `Generated ${CONFIG.count} images successfully.`; | |
| elements.generateBtn.disabled = false; | |
| elements.generateBtn.innerHTML = '<i class="fa-solid fa-rotate-right"></i> Regenerate'; | |
| elements.downloadAllBtn.disabled = false; | |
| showToast('Images generated successfully!'); | |
| } | |
| /** | |
| * Trigger a single file download | |
| */ | |
| window.downloadSingle = function(dataUrl, filename) { | |
| const a = document.createElement('a'); | |
| a.href = dataUrl; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| }; | |
| /** | |
| * Download all images with a delay | |
| */ | |
| async function downloadAll() { | |
| if (generatedColors.length === 0) return; | |
| elements.downloadAllBtn.disabled = true; | |
| elements.generateBtn.disabled = true; | |
| elements.statusText.textContent = 'Downloading images... Please wait.'; | |
| elements.progressContainer.style.display = 'block'; | |
| elements.progressBar.style.width = '0%'; | |
| // We need to regenerate DataURLs or store them. | |
| // For efficiency, let's just grab them from the DOM or regenerate on fly. | |
| // Regenerating is safer to ensure we have the raw data. | |
| for (let i = 0; i < generatedColors.length; i++) { | |
| const color = generatedColors[i]; | |
| const dataUrl = createImageDataURL(color.css); | |
| downloadSingle(dataUrl, color.filename); | |
| // Update Progress | |
| const percent = ((i + 1) / generatedColors.length) * 100; | |
| elements.progressBar.style.width = `${percent}%`; | |
| // Delay to prevent browser crash/blocking | |
| await new Promise(r => setTimeout(r, CONFIG.delayBetweenDownloads)); | |
| } | |
| elements.statusText.textContent = 'Download sequence complete.'; | |
| elements.downloadAllBtn.disabled = false; | |
| elements.generateBtn.disabled = false; | |
| showToast('Download sequence complete!'); | |
| }; | |
| /** | |
| * Show Toast Notification | |
| */ | |
| function showToast(msg) { | |
| elements.toastMessage.textContent = msg; | |
| elements.toast.classList.add('show'); | |
| setTimeout(() => { | |
| elements.toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Event Listeners | |
| elements.generateBtn.addEventListener('click', renderGallery); | |
| elements.downloadAllBtn.addEventListener('click', downloadAll); | |
| </script> | |
| </body> | |
| </html> |