open-image-generation / index.html
linoyts's picture
linoyts HF Staff
Update index.html
4dcef86 verified
<!DOCTYPE html>
<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>