Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PV Panel Segmentation β Model Comparison</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0e17; | |
| --bg-secondary: #111827; | |
| --bg-card: #1a2235; | |
| --bg-hover: #243049; | |
| --border: #2a3a55; | |
| --text-primary: #e8ecf4; | |
| --text-secondary: #8b95a8; | |
| --text-muted: #5a6578; | |
| --accent: #3b82f6; | |
| --accent-glow: rgba(59, 130, 246, 0.25); | |
| --success: #10b981; | |
| --warning: #f59e0b; | |
| --danger: #ef4444; | |
| --glass: rgba(26, 34, 53, 0.75); | |
| --glass-border: rgba(255, 255, 255, 0.06); | |
| --radius: 12px; | |
| --radius-sm: 8px; | |
| --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', -apple-system, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* ββ Header ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 24px; | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| z-index: 100; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .logo { | |
| font-size: 18px; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.5px; | |
| } | |
| .header-stats { | |
| display: flex; | |
| gap: 20px; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| .stat-value { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| /* ββ Controls ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 7px 14px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| color: var(--text-primary); | |
| border-radius: var(--radius-sm); | |
| font-size: 13px; | |
| font-family: inherit; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| white-space: nowrap; | |
| } | |
| .btn:hover { | |
| background: var(--bg-hover); | |
| border-color: var(--accent); | |
| } | |
| .btn.active { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: white; | |
| box-shadow: 0 0 12px var(--accent-glow); | |
| } | |
| .btn-nav { | |
| width: 36px; | |
| height: 36px; | |
| padding: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| border-radius: 50%; | |
| } | |
| .search-box { | |
| padding: 8px 14px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| color: var(--text-primary); | |
| border-radius: var(--radius-sm); | |
| font-size: 13px; | |
| font-family: inherit; | |
| width: 220px; | |
| transition: all var(--transition); | |
| } | |
| .search-box:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px var(--accent-glow); | |
| } | |
| .search-box::placeholder { color: var(--text-muted); } | |
| /* ββ Main Layout βββββββββββββββββββββββββββββββββββββββββ */ | |
| .main { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| /* ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .sidebar { | |
| width: 260px; | |
| background: var(--bg-secondary); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| flex-shrink: 0; | |
| transition: width var(--transition); | |
| } | |
| .sidebar.collapsed { | |
| width: 0; | |
| overflow: hidden; | |
| border-right: none; | |
| } | |
| .sidebar-header { | |
| padding: 12px 16px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .sidebar-header span { | |
| font-size: 12px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.8px; | |
| color: var(--text-secondary); | |
| } | |
| .image-grid { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px; | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 6px; | |
| align-content: start; | |
| } | |
| .image-grid::-webkit-scrollbar { width: 4px; } | |
| .image-grid::-webkit-scrollbar-track { background: transparent; } | |
| .image-grid::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } | |
| .thumb { | |
| aspect-ratio: 1; | |
| border-radius: 6px; | |
| overflow: hidden; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| transition: all var(--transition); | |
| position: relative; | |
| } | |
| .thumb:hover { border-color: var(--accent); transform: scale(1.04); } | |
| .thumb.active { border-color: var(--accent); box-shadow: 0 0 10px var(--accent-glow); } | |
| .thumb img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .thumb-label { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: rgba(0,0,0,0.7); | |
| color: white; | |
| font-size: 8px; | |
| padding: 2px 4px; | |
| text-overflow: ellipsis; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| } | |
| /* ββ Content Area ββββββββββββββββββββββββββββββββββββββββ */ | |
| .content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .toolbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 20px; | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .toolbar-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .toolbar-center { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .image-counter { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .image-counter .current { | |
| color: var(--text-primary); | |
| font-weight: 600; | |
| } | |
| .toolbar-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .opacity-slider { | |
| width: 100px; | |
| accent-color: var(--accent); | |
| cursor: pointer; | |
| } | |
| .opacity-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| min-width: 32px; | |
| } | |
| /* ββ Comparison Grid βββββββββββββββββββββββββββββββββββββ */ | |
| .comparison-area { | |
| flex: 1; | |
| overflow: auto; | |
| padding: 20px; | |
| } | |
| .comparison-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 16px; | |
| height: 100%; | |
| min-height: 0; | |
| } | |
| .comparison-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| transition: all var(--transition); | |
| } | |
| .comparison-card:hover { | |
| border-color: var(--accent); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| } | |
| .card-header { | |
| padding: 10px 14px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| flex-shrink: 0; | |
| } | |
| .card-title { | |
| font-size: 13px; | |
| font-weight: 600; | |
| } | |
| .card-badge { | |
| font-size: 10px; | |
| padding: 2px 8px; | |
| border-radius: 20px; | |
| background: rgba(59, 130, 246, 0.15); | |
| color: var(--accent); | |
| font-weight: 500; | |
| } | |
| .card-body { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| background: #0d1117; | |
| min-height: 0; | |
| } | |
| .card-body img { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| image-rendering: pixelated; | |
| } | |
| .overlay-container { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .overlay-container .mask-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| mix-blend-mode: screen; | |
| pointer-events: none; | |
| } | |
| /* ββ Loading / Empty States ββββββββββββββββββββββββββββββ */ | |
| .loading-state, .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: var(--text-muted); | |
| gap: 12px; | |
| font-size: 14px; | |
| } | |
| .spinner { | |
| width: 32px; | |
| height: 32px; | |
| border: 3px solid var(--border); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .empty-icon { | |
| font-size: 48px; | |
| opacity: 0.3; | |
| } | |
| /* ββ Responsive ββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 1400px) { | |
| .comparison-grid { grid-template-columns: repeat(3, 1fr); } | |
| } | |
| @media (max-width: 900px) { | |
| .comparison-grid { grid-template-columns: repeat(2, 1fr); } | |
| .sidebar { width: 200px; } | |
| } | |
| /* ββ Keyboard hints ββββββββββββββββββββββββββββββββββββββ */ | |
| .kbd { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-width: 20px; | |
| height: 20px; | |
| padding: 0 5px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| font-family: inherit; | |
| } | |
| /* ββ View mode βββββββββββββββββββββββββββββββββββββββββββ */ | |
| .comparison-grid.view-overlay .card-body { background: transparent; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ββ Header βββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <div class="logo">⬑ PV Seg Compare</div> | |
| <div class="header-stats"> | |
| <span>Images: <span class="stat-value" id="totalImages">β</span></span> | |
| <span>Models: <span class="stat-value">4</span></span> | |
| </div> | |
| </div> | |
| <div class="header-right"> | |
| <input type="text" class="search-box" id="searchInput" placeholder="Search imagesβ¦" autocomplete="off"> | |
| <button class="btn" id="toggleSidebar" title="Toggle sidebar">β°</button> | |
| </div> | |
| </header> | |
| <!-- ββ Main βββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="main"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <span>Image Browser</span> | |
| <span id="filteredCount"></span> | |
| </div> | |
| <div class="image-grid" id="imageGrid"></div> | |
| </aside> | |
| <!-- Content --> | |
| <div class="content"> | |
| <div class="toolbar"> | |
| <div class="toolbar-left"> | |
| <button class="btn btn-nav" id="prevBtn" title="Previous (β)">βΉ</button> | |
| <div class="image-counter"> | |
| <span class="current" id="currentIdx">0</span> | |
| <span>/ <span id="totalFiltered">0</span></span> | |
| </div> | |
| <button class="btn btn-nav" id="nextBtn" title="Next (β)">βΊ</button> | |
| </div> | |
| <div class="toolbar-center"> | |
| <span id="currentFilename" style="font-size: 13px; color: var(--text-secondary);"></span> | |
| </div> | |
| <div class="toolbar-right"> | |
| <button class="btn" id="toggleOverlay"> | |
| <span>Overlay</span> | |
| </button> | |
| <label class="opacity-label">Ξ±</label> | |
| <input type="range" class="opacity-slider" id="opacitySlider" min="0" max="100" value="50"> | |
| <span class="opacity-label" id="opacityValue">50%</span> | |
| <span style="color: var(--text-muted); font-size: 11px; margin-left: 8px;"> | |
| <span class="kbd">β</span> <span class="kbd">β</span> navigate | |
| </span> | |
| </div> | |
| </div> | |
| <div class="comparison-area"> | |
| <div class="comparison-grid" id="comparisonGrid"> | |
| <!-- Filled by JS --> | |
| <div class="empty-state" id="emptyState"> | |
| <div class="empty-icon">π</div> | |
| <div>Loading manifestβ¦</div> | |
| <div class="spinner"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ State ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let manifest = null; | |
| let filteredImages = []; | |
| let currentIndex = 0; | |
| let overlayMode = false; | |
| let maskOpacity = 0.5; | |
| // Model display colors for overlay mode | |
| const MODEL_COLORS = { | |
| 'segnet': '#3b82f6', | |
| 'unet': '#10b981', | |
| 'segformer_b0': '#f59e0b', | |
| 'segformer_b5': '#ef4444', | |
| }; | |
| const MODEL_DISPLAY_NAMES = { | |
| 'segnet': 'SegNet (CNN)', | |
| 'unet': 'UNet', | |
| 'segformer_b0': 'SegFormer-B0', | |
| 'segformer_b5': 'SegFormer-B5', | |
| }; | |
| // ββ DOM refs βββββββββββββββββββββββββββββββββββββββββββββ | |
| const searchInput = document.getElementById('searchInput'); | |
| const imageGrid = document.getElementById('imageGrid'); | |
| const comparisonGrid = document.getElementById('comparisonGrid'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const currentIdxEl = document.getElementById('currentIdx'); | |
| const totalFilteredEl = document.getElementById('totalFiltered'); | |
| const totalImagesEl = document.getElementById('totalImages'); | |
| const filteredCountEl = document.getElementById('filteredCount'); | |
| const currentFilenameEl = document.getElementById('currentFilename'); | |
| const prevBtn = document.getElementById('prevBtn'); | |
| const nextBtn = document.getElementById('nextBtn'); | |
| const toggleOverlayBtn = document.getElementById('toggleOverlay'); | |
| const opacitySlider = document.getElementById('opacitySlider'); | |
| const opacityValueEl = document.getElementById('opacityValue'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const toggleSidebarBtn = document.getElementById('toggleSidebar'); | |
| // ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function init() { | |
| try { | |
| const resp = await fetch('predictions/manifest.json'); | |
| manifest = await resp.json(); | |
| totalImagesEl.textContent = manifest.images.length.toLocaleString(); | |
| filteredImages = manifest.images; | |
| renderSidebar(); | |
| showImage(0); | |
| } catch (e) { | |
| emptyState.innerHTML = ` | |
| <div class="empty-icon">β οΈ</div> | |
| <div>Could not load manifest.json</div> | |
| <div style="font-size: 12px; color: var(--text-muted);"> | |
| Run: <code>python -m http.server 8000</code> from the pv_panel_models directory | |
| </div> | |
| `; | |
| } | |
| } | |
| // ββ Sidebar rendering ββββββββββββββββββββββββββββββββββββ | |
| function renderSidebar() { | |
| imageGrid.innerHTML = ''; | |
| filteredCountEl.textContent = `${filteredImages.length}`; | |
| totalFilteredEl.textContent = filteredImages.length.toLocaleString(); | |
| // Only render visible portion for performance (virtual scroll would be better but keep it simple) | |
| const fragment = document.createDocumentFragment(); | |
| const maxVisible = Math.min(filteredImages.length, 300); // Show first 300 for perf | |
| for (let i = 0; i < maxVisible; i++) { | |
| const img = filteredImages[i]; | |
| const thumb = document.createElement('div'); | |
| thumb.className = 'thumb' + (i === currentIndex ? ' active' : ''); | |
| thumb.dataset.index = i; | |
| thumb.innerHTML = ` | |
| <img src="${img.original_path}" loading="lazy" alt="${img.filename}"> | |
| <div class="thumb-label">${img.filename.replace('.jpg','')}</div> | |
| `; | |
| thumb.addEventListener('click', () => { | |
| currentIndex = parseInt(thumb.dataset.index); | |
| showImage(currentIndex); | |
| }); | |
| fragment.appendChild(thumb); | |
| } | |
| if (filteredImages.length > maxVisible) { | |
| const more = document.createElement('div'); | |
| more.style.cssText = 'grid-column: 1/-1; text-align:center; padding:8px; font-size:11px; color:var(--text-muted)'; | |
| more.textContent = `+${(filteredImages.length - maxVisible).toLocaleString()} more (use search to filter)`; | |
| fragment.appendChild(more); | |
| } | |
| imageGrid.appendChild(fragment); | |
| } | |
| // ββ Show image βββββββββββββββββββββββββββββββββββββββββββ | |
| function showImage(index) { | |
| if (!manifest || filteredImages.length === 0) return; | |
| currentIndex = Math.max(0, Math.min(index, filteredImages.length - 1)); | |
| const img = filteredImages[currentIndex]; | |
| currentIdxEl.textContent = (currentIndex + 1).toLocaleString(); | |
| currentFilenameEl.textContent = img.filename; | |
| // Update active thumbnail | |
| document.querySelectorAll('.thumb').forEach((t, i) => { | |
| t.classList.toggle('active', i === currentIndex); | |
| }); | |
| // Scroll active thumb into view | |
| const activeThumb = document.querySelector('.thumb.active'); | |
| if (activeThumb) { | |
| activeThumb.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| // Build comparison cards | |
| comparisonGrid.innerHTML = ''; | |
| // Original image card | |
| const origCard = createCard('Original', 'source', img.original_path, null); | |
| comparisonGrid.appendChild(origCard); | |
| // Model prediction cards | |
| const modelKeys = manifest.model_short_names || Object.keys(img.masks); | |
| for (const key of modelKeys) { | |
| if (!img.masks[key]) continue; | |
| const name = MODEL_DISPLAY_NAMES[key] || key; | |
| const maskPath = img.masks[key]; | |
| const card = createCard(name, key, img.original_path, maskPath); | |
| comparisonGrid.appendChild(card); | |
| } | |
| } | |
| function createCard(title, key, originalPath, maskPath) { | |
| const card = document.createElement('div'); | |
| card.className = 'comparison-card'; | |
| const badge = key === 'source' ? 'RGB' : 'mask'; | |
| const badgeColor = key === 'source' ? 'rgba(16,185,129,0.15)' : 'rgba(59,130,246,0.15)'; | |
| const badgeTextColor = key === 'source' ? '#10b981' : '#3b82f6'; | |
| card.innerHTML = ` | |
| <div class="card-header"> | |
| <span class="card-title">${title}</span> | |
| <span class="card-badge" style="background:${badgeColor}; color:${badgeTextColor}">${badge}</span> | |
| </div> | |
| <div class="card-body"></div> | |
| `; | |
| const body = card.querySelector('.card-body'); | |
| if (key === 'source') { | |
| // Just the original image | |
| const imgEl = new Image(); | |
| imgEl.src = originalPath; | |
| imgEl.alt = title; | |
| body.appendChild(imgEl); | |
| } else if (overlayMode && maskPath) { | |
| // Overlay: original + mask on top | |
| const container = document.createElement('div'); | |
| container.className = 'overlay-container'; | |
| const origImg = new Image(); | |
| origImg.src = originalPath; | |
| origImg.alt = 'original'; | |
| const maskImg = new Image(); | |
| maskImg.src = maskPath; | |
| maskImg.alt = 'mask'; | |
| maskImg.className = 'mask-overlay'; | |
| maskImg.style.opacity = maskOpacity; | |
| // Tint the mask with model color | |
| const color = MODEL_COLORS[key] || '#3b82f6'; | |
| maskImg.style.filter = `opacity(1)`; | |
| maskImg.style.mixBlendMode = 'screen'; | |
| container.appendChild(origImg); | |
| container.appendChild(maskImg); | |
| body.appendChild(container); | |
| } else if (maskPath) { | |
| // Just the mask | |
| const imgEl = new Image(); | |
| imgEl.src = maskPath; | |
| imgEl.alt = title; | |
| body.appendChild(imgEl); | |
| } | |
| return card; | |
| } | |
| // ββ Search βββββββββββββββββββββββββββββββββββββββββββββββ | |
| searchInput.addEventListener('input', () => { | |
| const query = searchInput.value.trim().toLowerCase(); | |
| if (!manifest) return; | |
| if (query === '') { | |
| filteredImages = manifest.images; | |
| } else { | |
| filteredImages = manifest.images.filter(img => | |
| img.filename.toLowerCase().includes(query) | |
| ); | |
| } | |
| currentIndex = 0; | |
| renderSidebar(); | |
| if (filteredImages.length > 0) { | |
| showImage(0); | |
| } else { | |
| comparisonGrid.innerHTML = ` | |
| <div class="empty-state" style="grid-column:1/-1"> | |
| <div class="empty-icon">π</div> | |
| <div>No images match "${searchInput.value}"</div> | |
| </div> | |
| `; | |
| currentIdxEl.textContent = '0'; | |
| currentFilenameEl.textContent = ''; | |
| } | |
| }); | |
| // ββ Navigation βββββββββββββββββββββββββββββββββββββββββββ | |
| prevBtn.addEventListener('click', () => showImage(currentIndex - 1)); | |
| nextBtn.addEventListener('click', () => showImage(currentIndex + 1)); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.target.tagName === 'INPUT') return; | |
| if (e.key === 'ArrowLeft') { e.preventDefault(); showImage(currentIndex - 1); } | |
| if (e.key === 'ArrowRight') { e.preventDefault(); showImage(currentIndex + 1); } | |
| if (e.key === 'o' || e.key === 'O') { toggleOverlayBtn.click(); } | |
| }); | |
| // ββ Overlay toggle βββββββββββββββββββββββββββββββββββββββ | |
| toggleOverlayBtn.addEventListener('click', () => { | |
| overlayMode = !overlayMode; | |
| toggleOverlayBtn.classList.toggle('active', overlayMode); | |
| comparisonGrid.classList.toggle('view-overlay', overlayMode); | |
| showImage(currentIndex); | |
| }); | |
| // ββ Opacity ββββββββββββββββββββββββββββββββββββββββββββββ | |
| opacitySlider.addEventListener('input', () => { | |
| maskOpacity = opacitySlider.value / 100; | |
| opacityValueEl.textContent = `${opacitySlider.value}%`; | |
| // Update all overlays live | |
| document.querySelectorAll('.mask-overlay').forEach(el => { | |
| el.style.opacity = maskOpacity; | |
| }); | |
| }); | |
| // ββ Sidebar toggle βββββββββββββββββββββββββββββββββββββββ | |
| toggleSidebarBtn.addEventListener('click', () => { | |
| sidebar.classList.toggle('collapsed'); | |
| }); | |
| // ββ Start ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |