Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Open Image Generation Progress</title> | |
| <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=Space+Mono:wght@400;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0a0b; | |
| --bg-secondary: #131316; | |
| --bg-card: #1a1a1f; | |
| --text-primary: #f5f5f7; | |
| --text-secondary: #8e8e93; | |
| --text-muted: #636366; | |
| --accent: #6366f1; | |
| --accent-glow: rgba(99, 102, 241, 0.3); | |
| --border: #2c2c2e; | |
| --year-2022: #f472b6; | |
| --year-2023: #fb923c; | |
| --year-2024: #4ade80; | |
| --year-2025: #60a5fa; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Syne', sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Grain overlay */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); | |
| opacity: 0.03; | |
| pointer-events: none; | |
| z-index: 1000; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 2rem 3rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| background: rgba(10, 10, 11, 0.85); | |
| backdrop-filter: blur(20px); | |
| z-index: 100; | |
| } | |
| .header-title { | |
| font-size: 1.1rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| letter-spacing: -0.02em; | |
| } | |
| .nav-links { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .nav-links a { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .hf-logo { | |
| height: 24px; | |
| width: auto; | |
| opacity: 0.8; | |
| transition: opacity 0.2s; | |
| } | |
| .hf-logo:hover { | |
| opacity: 1; | |
| } | |
| /* Prompt Section */ | |
| .prompt-section { | |
| padding: 3rem; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| .prompt-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| gap: 2rem; | |
| margin-bottom: 2rem; | |
| padding-bottom: 2rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .prompt-content { | |
| flex: 1; | |
| max-width: 900px; | |
| } | |
| .prompt-nav { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| flex-shrink: 0; | |
| } | |
| .nav-btn { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| border: 1px solid var(--border); | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s; | |
| font-size: 1.25rem; | |
| } | |
| .nav-btn:hover { | |
| background: var(--bg-card); | |
| border-color: var(--text-muted); | |
| } | |
| .prompt-indicator { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| min-width: 80px; | |
| text-align: center; | |
| } | |
| .prompt-label { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| margin-bottom: 0.75rem; | |
| } | |
| .prompt-text { | |
| font-size: clamp(1.25rem, 3vw, 1.75rem); | |
| font-weight: 600; | |
| line-height: 1.4; | |
| color: var(--text-primary); | |
| } | |
| /* Timeline */ | |
| .timeline { | |
| display: flex; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| margin-bottom: 3rem; | |
| flex-wrap: wrap; | |
| } | |
| .timeline-btn { | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 100px; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.8rem; | |
| transition: all 0.3s; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.25rem; | |
| } | |
| .timeline-btn .year { | |
| font-weight: 700; | |
| font-size: 0.9rem; | |
| } | |
| .timeline-btn .model { | |
| font-size: 0.7rem; | |
| opacity: 0.7; | |
| } | |
| .timeline-btn:hover { | |
| border-color: var(--text-muted); | |
| color: var(--text-primary); | |
| } | |
| .timeline-btn.active { | |
| background: var(--text-primary); | |
| color: var(--bg-primary); | |
| border-color: var(--text-primary); | |
| } | |
| .timeline-btn[data-year="2022"] { --btn-accent: var(--year-2022); } | |
| .timeline-btn[data-year="2023"] { --btn-accent: var(--year-2023); } | |
| .timeline-btn[data-year="2024"] { --btn-accent: var(--year-2024); } | |
| .timeline-btn[data-year="2025"] { --btn-accent: var(--year-2025); } | |
| .timeline-btn.active { | |
| background: var(--btn-accent); | |
| border-color: var(--btn-accent); | |
| color: var(--bg-primary); | |
| } | |
| /* Image Display */ | |
| .image-container { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| flex-wrap: wrap; | |
| } | |
| .image-card { | |
| background: var(--bg-card); | |
| border-radius: 16px; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| max-width: 350px; | |
| opacity: 0; | |
| transform: translateY(20px); | |
| animation: fadeInUp 0.5s ease forwards; | |
| cursor: pointer; | |
| } | |
| .image-card:nth-child(1) { animation-delay: 0s; } | |
| .image-card:nth-child(2) { animation-delay: 0.1s; } | |
| .image-card:nth-child(3) { animation-delay: 0.2s; } | |
| .image-card:nth-child(4) { animation-delay: 0.3s; } | |
| .image-card:nth-child(5) { animation-delay: 0.4s; } | |
| .image-card:nth-child(6) { animation-delay: 0.5s; } | |
| .image-card:nth-child(7) { animation-delay: 0.6s; } | |
| @keyframes fadeInUp { | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .image-card:hover { | |
| transform: translateY(-4px); | |
| border-color: var(--text-muted); | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); | |
| } | |
| .image-card img { | |
| width: 100%; | |
| aspect-ratio: 1; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .image-meta { | |
| padding: 1rem 1.25rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .model-name { | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| } | |
| .model-name a { | |
| color: var(--text-primary); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .model-name a:hover { | |
| color: var(--accent); | |
| text-decoration: underline; | |
| } | |
| .model-year { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.75rem; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 100px; | |
| background: var(--bg-secondary); | |
| } | |
| /* Compare Mode */ | |
| .view-toggle { | |
| display: flex; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .view-btn { | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| background: transparent; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| transition: all 0.2s; | |
| } | |
| .view-btn.active { | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| border-color: var(--text-muted); | |
| } | |
| /* Compare Grid - 4 columns for 7 models (4+3 layout, centered) */ | |
| .compare-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| max-width: 1300px; | |
| margin: 0 auto; | |
| } | |
| .compare-grid .image-card { | |
| max-width: none; | |
| width: calc(25% - 1.2rem); | |
| flex-shrink: 0; | |
| } | |
| .compare-grid .image-card img { | |
| aspect-ratio: 1; | |
| } | |
| /* Lightbox Modal */ | |
| .lightbox { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.95); | |
| z-index: 2000; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 1rem; | |
| } | |
| .lightbox.active { | |
| display: flex; | |
| } | |
| .lightbox-content { | |
| position: relative; | |
| max-width: 90vw; | |
| max-height: 90vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .lightbox-content img { | |
| max-width: 100%; | |
| max-height: calc(90vh - 80px); | |
| object-fit: contain; | |
| border-radius: 12px; | |
| } | |
| .lightbox-meta { | |
| margin-top: 1rem; | |
| text-align: center; | |
| color: var(--text-primary); | |
| } | |
| .lightbox-meta .model-name { | |
| font-size: 1.1rem; | |
| margin-bottom: 0.25rem; | |
| } | |
| .lightbox-meta .model-name a { | |
| color: var(--text-primary); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .lightbox-meta .model-name a:hover { | |
| color: var(--accent); | |
| text-decoration: underline; | |
| } | |
| .lightbox-meta .model-year { | |
| display: inline-block; | |
| } | |
| .lightbox-close { | |
| position: absolute; | |
| top: -50px; | |
| right: 0; | |
| width: 40px; | |
| height: 40px; | |
| border: none; | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| font-size: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s; | |
| } | |
| .lightbox-close:hover { | |
| background: var(--bg-secondary); | |
| transform: scale(1.1); | |
| } | |
| .lightbox-nav { | |
| position: absolute; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 50px; | |
| height: 50px; | |
| border: 1px solid var(--border); | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| font-size: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s; | |
| } | |
| .lightbox-nav:hover { | |
| background: var(--bg-secondary); | |
| border-color: var(--text-muted); | |
| } | |
| .lightbox-prev { | |
| left: -70px; | |
| } | |
| .lightbox-next { | |
| right: -70px; | |
| } | |
| /* Footer */ | |
| footer { | |
| padding: 4rem 3rem; | |
| border-top: 1px solid var(--border); | |
| text-align: center; | |
| margin-top: 6rem; | |
| } | |
| footer p { | |
| color: var(--text-muted); | |
| font-size: 0.875rem; | |
| } | |
| footer a { | |
| color: var(--text-secondary); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1100px) { | |
| .compare-grid .image-card { | |
| width: calc(33.333% - 1rem); | |
| } | |
| } | |
| @media (max-width: 900px) { | |
| .compare-grid .image-card { | |
| width: calc(50% - 0.75rem); | |
| } | |
| .compare-grid { | |
| gap: 1rem; | |
| } | |
| .lightbox-prev { | |
| left: 10px; | |
| } | |
| .lightbox-next { | |
| right: 10px; | |
| } | |
| .lightbox-nav { | |
| width: 40px; | |
| height: 40px; | |
| font-size: 1.25rem; | |
| background: rgba(26, 26, 31, 0.9); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| header { | |
| padding: 1rem 1.5rem; | |
| flex-direction: column; | |
| gap: 1rem; | |
| align-items: flex-start; | |
| } | |
| .header-title { | |
| font-size: 0.85rem; | |
| line-height: 1.4; | |
| } | |
| .nav-links { | |
| align-self: flex-end; | |
| margin-top: -2rem; | |
| } | |
| .prompt-section { | |
| padding: 1.5rem 1rem; | |
| } | |
| .prompt-row { | |
| flex-direction: column; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| padding-bottom: 1.5rem; | |
| } | |
| .prompt-text { | |
| font-size: 1.1rem; | |
| } | |
| .prompt-nav { | |
| align-self: flex-start; | |
| } | |
| .nav-btn { | |
| width: 40px; | |
| height: 40px; | |
| font-size: 1rem; | |
| } | |
| .prompt-indicator { | |
| font-size: 0.75rem; | |
| min-width: 60px; | |
| } | |
| .view-toggle { | |
| margin-bottom: 1.5rem; | |
| } | |
| .view-btn { | |
| padding: 0.4rem 0.75rem; | |
| font-size: 0.7rem; | |
| } | |
| .timeline { | |
| gap: 0.25rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .timeline-btn { | |
| padding: 0.5rem 0.75rem; | |
| } | |
| .timeline-btn .year { | |
| font-size: 0.8rem; | |
| } | |
| .timeline-btn .model { | |
| font-size: 0.6rem; | |
| } | |
| .image-card { | |
| border-radius: 12px; | |
| } | |
| .image-meta { | |
| padding: 0.75rem 1rem; | |
| } | |
| .model-name { | |
| font-size: 0.8rem; | |
| } | |
| .model-year { | |
| font-size: 0.65rem; | |
| padding: 0.2rem 0.5rem; | |
| } | |
| footer { | |
| padding: 2rem 1.5rem; | |
| margin-top: 3rem; | |
| } | |
| footer p { | |
| font-size: 0.75rem; | |
| } | |
| .lightbox-close { | |
| top: 10px; | |
| right: 10px; | |
| position: fixed; | |
| } | |
| .lightbox-content { | |
| max-width: 95vw; | |
| } | |
| } | |
| @media (max-width: 500px) { | |
| .compare-grid .image-card { | |
| width: calc(50% - 0.5rem); | |
| } | |
| .compare-grid { | |
| gap: 0.75rem; | |
| } | |
| .image-meta { | |
| padding: 0.5rem 0.75rem; | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 0.25rem; | |
| } | |
| .model-name { | |
| font-size: 0.7rem; | |
| } | |
| .model-year { | |
| font-size: 0.6rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1 class="header-title">From Nightmare Hands to Avocado Chairs: Evolution of Open-Source Image Generation</h1> | |
| <nav class="nav-links"> | |
| <a href="https://huggingface.co/spaces/linoyts/open-image-generation" target="_blank"> | |
| <img src="https://huggingface.co/spaces/linoyts/open-image-generation/resolve/main/hf-logo.png" alt="Hugging Face" class="hf-logo"> | |
| </a> | |
| </nav> | |
| </header> | |
| <main class="prompt-section"> | |
| <div class="prompt-row"> | |
| <div class="prompt-content"> | |
| <div class="prompt-label">Prompt</div> | |
| <p class="prompt-text" id="promptText"></p> | |
| </div> | |
| <div class="prompt-nav"> | |
| <button class="nav-btn" id="prevPrompt">←</button> | |
| <span class="prompt-indicator"><span id="promptNum">1</span> / <span id="totalPrompts">10</span></span> | |
| <button class="nav-btn" id="nextPrompt">→</button> | |
| </div> | |
| </div> | |
| <div class="view-toggle"> | |
| <button class="view-btn" data-view="single">Single Model</button> | |
| <button class="view-btn active" data-view="compare">Compare All</button> | |
| </div> | |
| <div class="timeline" id="timeline"></div> | |
| <div class="image-container" id="imageContainer"></div> | |
| </main> | |
| <!-- Lightbox Modal --> | |
| <div class="lightbox" id="lightbox"> | |
| <div class="lightbox-content"> | |
| <button class="lightbox-close" id="lightboxClose">×</button> | |
| <button class="lightbox-nav lightbox-prev" id="lightboxPrev">←</button> | |
| <button class="lightbox-nav lightbox-next" id="lightboxNext">→</button> | |
| <img src="" alt="" id="lightboxImg"> | |
| <div class="lightbox-meta"> | |
| <div class="model-name" id="lightboxModel"></div> | |
| <span class="model-year" id="lightboxYear"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| <p>Inspired by <a href="https://progress.openai.com" target="_blank">OpenAI's Progress highlights page</a></p> | |
| <p style="margin-top: 0.5rem;"> | |
| <a href="https://huggingface.co/spaces/linoyts/open-image-generation" target="_blank">Hosted on Hugging Face Spaces</a> | |
| </p> | |
| </footer> | |
| <script> | |
| // Base URL for HuggingFace Space files | |
| const BASE_URL = 'https://huggingface.co/spaces/linoyts/open-image-generation/resolve/main'; | |
| // Data | |
| const models = [ | |
| { id: 'dalle_mini', name: 'DALL-E Mini', year: '2022', folder: 'dalle_mini', link: 'https://huggingface.co/spaces/dalle-mini/dalle-mini' }, | |
| { id: 'sd15', name: 'SD 1.5', year: '2022', folder: 'sd1.5', link: 'https://huggingface.co/stable-diffusion-v1-5/stable-diffusion-v1-5' }, | |
| { id: 'sdxl', name: 'SDXL', year: '2023', folder: 'sdxl', link: 'https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0' }, | |
| { id: 'flux1', name: 'FLUX.1 Dev', year: '2024', folder: 'flux1_dev', link: 'https://huggingface.co/spaces/black-forest-labs/FLUX.1-dev' }, | |
| { id: 'zimage', name: 'Z-Image Turbo', year: '2025', folder: 'z_image', link: 'https://huggingface.co/spaces/Tongyi-MAI/Z-Image-Turbo' }, | |
| { id: 'qwen', name: 'Qwen-Image', year: '2025', folder: 'qwen_image', link: 'https://huggingface.co/spaces/Qwen/Qwen-Image-2512' }, | |
| { id: 'flux2', name: 'FLUX.2 Dev Turbo', year: '2025', folder: 'flux2_turbo', link: 'https://huggingface.co/spaces/multimodalart/FLUX.2-dev-turbo' }, | |
| ]; | |
| const prompts = [ | |
| { | |
| id: 'astronaut', | |
| text: 'A photograph of an astronaut riding a horse', | |
| file: 'astronaut_rides_horse' | |
| }, | |
| { | |
| id: 'avocado', | |
| text: 'An armchair in the shape of an avocado', | |
| file: 'avocado' | |
| }, | |
| { | |
| id: 'coffee', | |
| text: 'A coffee shop chalkboard sign that reads "Today\'s Special: Vanilla Latte $4.50"', | |
| file: 'coffee_shop' | |
| }, | |
| { | |
| id: 'hands', | |
| text: 'A close-up photograph of a person\'s hands playing a chord on an acoustic guitar', | |
| file: 'hands' | |
| }, | |
| { | |
| id: 'fisherman', | |
| text: 'Portrait photograph of an elderly fisherman with weathered skin, wearing a wool sweater, soft window light', | |
| file: 'fisherman' | |
| }, | |
| { | |
| id: 'golden', | |
| text: 'A golden retriever wearing red sunglasses sits in a turquoise kayak on a calm lake at sunset. Next to it, an orange tabby cat wearing a small purple top hat looks unimpressed. Mountains reflected in the water, golden hour lighting.', | |
| file: 'golden' | |
| }, | |
| { | |
| id: 'fox', | |
| text: 'A Japanese woodblock print of a fox in a bamboo forest, in the style of Hiroshige', | |
| file: 'fox' | |
| }, | |
| { | |
| id: 'gameshow', | |
| text: 'A man in a business suit standing in front of a large screen, shot from behind-the-scenes perspective of a game show set. The suit is split vertically down the middle: left half bright green, right half bright orange. The screen behind him is also split vertically: left half pink, right half blue. Realistic photography style, professional lighting, behind-the-scenes candid shot, game show studio environment, detailed fabric textures on the suit, crisp color separation', | |
| file: 'game_show' | |
| }, | |
| { | |
| id: 'whale', | |
| text: 'A massive blue whale swimming gracefully through the clouds above a small Italian village, casting a shadow over terracotta rooftops and narrow cobblestone streets. Elderly villagers look up in wonder from a piazza with a fountain. Late afternoon Mediterranean light, photorealistic, cinematic composition, sense of magical realism and scale', | |
| file: 'whale' | |
| }, | |
| { | |
| id: 'robot', | |
| text: 'A hyper-detailed oil painting in the style of the Dutch Golden Age masters, depicting a robot made of polished brass and copper sitting at a wooden table, peeling an orange. Dramatic chiaroscuro lighting from a single window, rich dark background, meticulous attention to metallic reflections and citrus texture, visible brushstrokes, museum quality, Vermeer meets steampunk', | |
| file: 'robot' | |
| } | |
| ]; | |
| // State | |
| let currentPromptIndex = 0; | |
| let currentModelIndex = models.length - 1; | |
| let viewMode = 'compare'; | |
| let lightboxImages = []; | |
| let lightboxCurrentIndex = 0; | |
| // DOM Elements | |
| const promptText = document.getElementById('promptText'); | |
| const promptNum = document.getElementById('promptNum'); | |
| const totalPrompts = document.getElementById('totalPrompts'); | |
| const timeline = document.getElementById('timeline'); | |
| const imageContainer = document.getElementById('imageContainer'); | |
| const prevBtn = document.getElementById('prevPrompt'); | |
| const nextBtn = document.getElementById('nextPrompt'); | |
| const viewBtns = document.querySelectorAll('.view-btn'); | |
| // Lightbox Elements | |
| const lightbox = document.getElementById('lightbox'); | |
| const lightboxImg = document.getElementById('lightboxImg'); | |
| const lightboxModel = document.getElementById('lightboxModel'); | |
| const lightboxYear = document.getElementById('lightboxYear'); | |
| const lightboxClose = document.getElementById('lightboxClose'); | |
| const lightboxPrev = document.getElementById('lightboxPrev'); | |
| const lightboxNext = document.getElementById('lightboxNext'); | |
| // Initialize | |
| function init() { | |
| totalPrompts.textContent = prompts.length; | |
| renderTimeline(); | |
| renderPrompt(); | |
| setupEventListeners(); | |
| setupLightbox(); | |
| // Hide timeline initially since we start in compare mode | |
| timeline.style.display = 'none'; | |
| } | |
| function renderTimeline() { | |
| timeline.innerHTML = models.map((model, index) => ` | |
| <button class="timeline-btn ${index === currentModelIndex ? 'active' : ''}" | |
| data-index="${index}" | |
| data-year="${model.year}"> | |
| <span class="year">${model.year}</span> | |
| <span class="model">${model.name}</span> | |
| </button> | |
| `).join(''); | |
| // Add click handlers | |
| document.querySelectorAll('.timeline-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| currentModelIndex = parseInt(btn.dataset.index); | |
| updateTimeline(); | |
| renderImages(); | |
| }); | |
| }); | |
| } | |
| function updateTimeline() { | |
| document.querySelectorAll('.timeline-btn').forEach((btn, index) => { | |
| btn.classList.toggle('active', index === currentModelIndex); | |
| }); | |
| } | |
| function renderPrompt() { | |
| const prompt = prompts[currentPromptIndex]; | |
| promptText.textContent = prompt.text; | |
| promptNum.textContent = currentPromptIndex + 1; | |
| renderImages(); | |
| } | |
| function renderImages() { | |
| const prompt = prompts[currentPromptIndex]; | |
| if (viewMode === 'compare') { | |
| renderCompareView(prompt); | |
| } else { | |
| renderSingleView(prompt); | |
| } | |
| } | |
| function renderSingleView(prompt) { | |
| const model = models[currentModelIndex]; | |
| const images = [ | |
| { src: `${BASE_URL}/${model.folder}/${prompt.file}.png`, model: model.name, year: model.year, link: model.link }, | |
| { src: `${BASE_URL}/${model.folder}/${prompt.file}_2.png`, model: model.name, year: model.year, link: model.link }, | |
| { src: `${BASE_URL}/${model.folder}/${prompt.file}_3.png`, model: model.name, year: model.year, link: model.link } | |
| ]; | |
| lightboxImages = images; | |
| imageContainer.className = 'image-container'; | |
| imageContainer.innerHTML = images.map((img, i) => ` | |
| <div class="image-card" data-index="${i}" style="animation-delay: ${i * 0.1}s"> | |
| <img src="${img.src}" alt="${prompt.text}" loading="lazy" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect fill=%22%231a1a1f%22 width=%22100%22 height=%22100%22/><text x=%2250%22 y=%2250%22 text-anchor=%22middle%22 fill=%22%23636366%22 font-size=%228%22>Not found</text></svg>'"> | |
| <div class="image-meta"> | |
| <span class="model-name"><a href="${img.link}" target="_blank" onclick="event.stopPropagation()">${img.model}</a></span> | |
| <span class="model-year">${img.year}</span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| attachImageClickHandlers(); | |
| } | |
| function renderCompareView(prompt) { | |
| lightboxImages = models.map(model => ({ | |
| src: `${BASE_URL}/${model.folder}/${prompt.file}.png`, | |
| model: model.name, | |
| year: model.year, | |
| link: model.link | |
| })); | |
| imageContainer.className = 'image-container compare-grid'; | |
| imageContainer.innerHTML = models.map((model, i) => ` | |
| <div class="image-card" data-index="${i}" style="animation-delay: ${i * 0.1}s"> | |
| <img src="${BASE_URL}/${model.folder}/${prompt.file}.png" alt="${prompt.text}" loading="lazy" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect fill=%22%231a1a1f%22 width=%22100%22 height=%22100%22/><text x=%2250%22 y=%2250%22 text-anchor=%22middle%22 fill=%22%23636366%22 font-size=%228%22>Not found</text></svg>'"> | |
| <div class="image-meta"> | |
| <span class="model-name"><a href="${model.link}" target="_blank" onclick="event.stopPropagation()">${model.name}</a></span> | |
| <span class="model-year">${model.year}</span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| attachImageClickHandlers(); | |
| } | |
| function attachImageClickHandlers() { | |
| document.querySelectorAll('.image-card').forEach(card => { | |
| card.addEventListener('click', (e) => { | |
| // Don't open lightbox if clicking on the link | |
| if (e.target.tagName === 'A') return; | |
| const index = parseInt(card.dataset.index); | |
| openLightbox(index); | |
| }); | |
| }); | |
| } | |
| function openLightbox(index) { | |
| lightboxCurrentIndex = index; | |
| updateLightboxContent(); | |
| lightbox.classList.add('active'); | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| function closeLightbox() { | |
| lightbox.classList.remove('active'); | |
| document.body.style.overflow = ''; | |
| } | |
| function updateLightboxContent() { | |
| const img = lightboxImages[lightboxCurrentIndex]; | |
| lightboxImg.src = img.src; | |
| lightboxModel.innerHTML = `<a href="${img.link}" target="_blank">${img.model}</a>`; | |
| lightboxYear.textContent = img.year; | |
| } | |
| function lightboxNavPrev() { | |
| lightboxCurrentIndex = (lightboxCurrentIndex - 1 + lightboxImages.length) % lightboxImages.length; | |
| updateLightboxContent(); | |
| } | |
| function lightboxNavNext() { | |
| lightboxCurrentIndex = (lightboxCurrentIndex + 1) % lightboxImages.length; | |
| updateLightboxContent(); | |
| } | |
| function setupLightbox() { | |
| lightboxClose.addEventListener('click', closeLightbox); | |
| lightboxPrev.addEventListener('click', lightboxNavPrev); | |
| lightboxNext.addEventListener('click', lightboxNavNext); | |
| lightbox.addEventListener('click', (e) => { | |
| if (e.target === lightbox) { | |
| closeLightbox(); | |
| } | |
| }); | |
| } | |
| function setupEventListeners() { | |
| prevBtn.addEventListener('click', () => { | |
| currentPromptIndex = (currentPromptIndex - 1 + prompts.length) % prompts.length; | |
| renderPrompt(); | |
| }); | |
| nextBtn.addEventListener('click', () => { | |
| currentPromptIndex = (currentPromptIndex + 1) % prompts.length; | |
| renderPrompt(); | |
| }); | |
| viewBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| viewMode = btn.dataset.view; | |
| viewBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Show/hide timeline in compare mode | |
| timeline.style.display = viewMode === 'compare' ? 'none' : 'flex'; | |
| renderImages(); | |
| }); | |
| }); | |
| // Keyboard navigation | |
| document.addEventListener('keydown', (e) => { | |
| // Lightbox navigation | |
| if (lightbox.classList.contains('active')) { | |
| if (e.key === 'Escape') { | |
| closeLightbox(); | |
| } else if (e.key === 'ArrowLeft') { | |
| lightboxNavPrev(); | |
| } else if (e.key === 'ArrowRight') { | |
| lightboxNavNext(); | |
| } | |
| return; | |
| } | |
| // Main navigation | |
| if (e.key === 'ArrowLeft') { | |
| currentPromptIndex = (currentPromptIndex - 1 + prompts.length) % prompts.length; | |
| renderPrompt(); | |
| } else if (e.key === 'ArrowRight') { | |
| currentPromptIndex = (currentPromptIndex + 1) % prompts.length; | |
| renderPrompt(); | |
| } else if (e.key === 'ArrowUp' && viewMode === 'single') { | |
| currentModelIndex = Math.min(currentModelIndex + 1, models.length - 1); | |
| updateTimeline(); | |
| renderImages(); | |
| } else if (e.key === 'ArrowDown' && viewMode === 'single') { | |
| currentModelIndex = Math.max(currentModelIndex - 1, 0); | |
| updateTimeline(); | |
| renderImages(); | |
| } | |
| }); | |
| } | |
| // Start | |
| init(); | |
| </script> | |
| </body> | |
| </html> |