aaurelions commited on
Commit
852934c
·
verified ·
1 Parent(s): b38fb77

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +250 -87
index.html CHANGED
@@ -1,90 +1,152 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>Futuristic Dataset Visualizer v5</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
10
  <style>
11
  @keyframes aurora {
12
- 0% { background-position: 0% 50%; }
13
- 50% { background-position: 100% 50%; }
14
- 100% { background-position: 0% 50%; }
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
- html { scroll-behavior: smooth; }
17
  body {
18
  font-family: 'SF Mono', 'Courier New', Courier, monospace;
19
  background-color: #0D1117;
20
  color: #E6EDF3;
21
  background-image: radial-gradient(at 27% 37%, hsla(215, 98%, 61%, 0.1) 0px, transparent 50%),
22
- radial-gradient(at 97% 21%, hsla(125, 98%, 72%, 0.1) 0px, transparent 50%);
23
  background-size: 250% 250%;
24
  animation: aurora 20s ease infinite;
25
  }
 
26
  .glass-card {
27
- background: rgba(17, 25, 40, 0.7);
28
  backdrop-filter: blur(16px) saturate(180%);
29
  -webkit-backdrop-filter: blur(16px) saturate(180%);
30
  border: 1px solid rgba(255, 255, 255, 0.1);
31
  }
 
32
  .btn-glow:hover {
33
  box-shadow: 0 0 20px rgba(59, 130, 246, 0.5), 0 0 8px rgba(59, 130, 246, 0.4);
34
  }
 
35
  .card-glow:hover {
36
  transform: translateY(-4px);
37
  box-shadow: 0 0 25px rgba(59, 130, 246, 0.3), 0 0 10px rgba(59, 130, 246, 0.2);
38
  }
39
- .modal { background: rgba(13, 17, 23, 0.85); backdrop-filter: blur(16px); }
40
- #modal-canvas { cursor: grab; touch-action: none; }
41
- #modal-canvas:active { cursor: grabbing; }
42
- /* Style for the thickness slider */
43
- input[type=range] { -webkit-appearance: none; background: transparent; }
44
- input[type=range]:focus { outline: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  input[type=range]::-webkit-slider-runnable-track {
46
- height: 4px; background: rgba(59, 130, 246, 0.5); border-radius: 5px;
 
 
47
  }
 
48
  input[type=range]::-webkit-slider-thumb {
49
- -webkit-appearance: none; height: 18px; width: 18px;
50
- border-radius: 50%; background: #E6EDF3;
51
- border: 2px solid #0D1117; margin-top: -7px;
 
 
 
 
52
  cursor: pointer;
53
  }
 
 
 
 
 
 
 
 
 
 
54
  </style>
55
  </head>
 
56
  <body class="overscroll-none">
57
 
58
  <div class="container mx-auto p-4 lg:p-8">
59
 
60
  <header class="text-center mb-10">
61
- <h1 class="text-3xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-teal-300 mb-2">Dataset Visualizer v5</h1>
 
 
62
  <p class="text-gray-400">Inspect, pan, and zoom your vision datasets with precision.</p>
63
  </header>
64
-
65
  <div class="glass-card p-4 sm:p-6 rounded-xl mb-10 max-w-3xl mx-auto">
66
  <div id="control-panel-content">
67
  <h2 class="text-2xl font-semibold text-blue-300 mb-4 text-center">Get Started</h2>
68
- <label for="zip-file" class="btn-glow bg-blue-600 text-white flex items-center justify-center w-full p-4 rounded-lg cursor-pointer text-xl transition-all duration-300">
69
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
 
 
 
 
 
70
  Upload dataset.zip
71
  </label>
72
  <input type="file" id="zip-file" accept=".zip" class="hidden">
73
- <p id="zip-file-name" class="text-center text-gray-500 mt-2 text-sm h-5"></p>
74
  <div id="status-container" class="h-6 text-center mt-2"></div>
75
  </div>
76
  <div id="labels-panel" class="hidden mt-4 pt-4 border-t border-gray-700">
77
  <h3 class="text-lg font-semibold text-blue-300 mb-2">Class Labels (Optional)</h3>
78
- <textarea id="yaml-paste" rows="4" class="w-full bg-gray-900 border border-gray-600 rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Paste COCO .yaml content here..."></textarea>
 
 
79
  </div>
80
  </div>
81
-
82
  <div id="image-container-wrapper">
83
- <div id="empty-state" class="text-center glass-card p-10 rounded-xl">
84
  <h3 class="text-2xl text-gray-400">Your visualizer is ready.</h3>
85
  <p class="text-gray-500">Upload a dataset to begin.</p>
86
  </div>
87
- <div id="image-container" class="hidden grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-6"></div>
 
88
  </div>
89
 
90
  <div id="pagination" class="flex justify-center items-center space-x-2 mt-10"></div>
@@ -94,35 +156,65 @@
94
  <div id="modal-backdrop" class="fixed inset-0 z-50 flex items-center justify-center hidden">
95
  <div class="modal w-full h-full relative flex items-center justify-center overflow-hidden">
96
  <div id="modal-spinner" class="absolute z-10">
97
- <svg class="animate-spin h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
 
 
 
 
 
 
98
  </div>
99
-
100
  <canvas id="modal-canvas" class="absolute top-0 left-0 w-full h-full"></canvas>
101
-
102
- <div id="modal-legend" class="absolute bottom-4 left-4 glass-card p-3 rounded-lg max-h-48 overflow-y-auto text-sm z-20 hidden"></div>
103
-
104
- <!-- Modal Controls -->
 
 
105
  <div class="absolute top-4 right-4 z-20 flex flex-col gap-2">
106
- <button id="modal-close" class="bg-red-600/80 text-white rounded-full w-10 h-10 flex items-center justify-center text-3xl font-bold">×</button>
 
107
  </div>
108
 
109
- <!-- NEW: Gallery Navigation -->
110
- <button id="prev-image-btn" class="absolute left-4 top-1/2 -translate-y-1/2 z-20 glass-card rounded-full p-3"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg></button>
111
- <button id="next-image-btn" class="absolute right-4 top-1/2 -translate-y-1/2 z-20 glass-card rounded-full p-3"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg></button>
 
 
 
 
 
 
 
 
 
112
 
113
  <div class="absolute bottom-4 right-4 z-20 glass-card rounded-lg flex items-center">
114
- <!-- NEW: Line Thickness Control -->
115
  <div class="p-3 flex items-center gap-3 border-r border-gray-600">
116
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /><path fill-rule="evenodd" d="M.458 10C3.732 4.943 9.522 3 10 3s6.268 1.943 9.542 7c-3.274 5.057-9.064 7-9.542 7S3.732 15.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" /></svg>
 
 
 
 
 
117
  <input type="range" id="line-thickness-slider" min="1" max="15" value="3" class="w-24">
118
  </div>
119
  <button id="zoom-out-btn" class="p-3 text-2xl">-</button>
120
  <button id="reset-view-btn" class="p-3">
121
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" /></svg>
 
 
 
 
122
  </button>
123
  <button id="zoom-in-btn" class="p-3 text-2xl">+</button>
124
  <button id="fullscreen-btn" class="p-3 ml-2 border-l border-gray-600">
125
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 110 2H5v2a1 1 0 11-2 0V4zm12 0a1 1 0 011 1v2a1 1 0 11-2 0V5h-2a1 1 0 110-2h4zM4 17a1 1 0 01-1-1v-2a1 1 0 112 0v2h2a1 1 0 110 2H4zm12 0a1 1 0 01-1-1h-4a1 1 0 110-2h2v-2a1 1 0 112 0v2z" clip-rule="evenodd" /></svg>
 
 
 
 
126
  </button>
127
  </div>
128
  </div>
@@ -138,7 +230,7 @@
138
  modal: {
139
  backdrop: document.getElementById('modal-backdrop'), canvas: document.getElementById('modal-canvas'),
140
  closeBtn: document.getElementById('modal-close'), legend: document.getElementById('modal-legend'),
141
- spinner: document.getElementById('modal-spinner'),
142
  zoomInBtn: document.getElementById('zoom-in-btn'), zoomOutBtn: document.getElementById('zoom-out-btn'),
143
  resetViewBtn: document.getElementById('reset-view-btn'), fullscreenBtn: document.getElementById('fullscreen-btn'),
144
  prevBtn: document.getElementById('prev-image-btn'), nextBtn: document.getElementById('next-image-btn'),
@@ -148,8 +240,8 @@
148
 
149
  let state = {
150
  allImageData: [], classNames: {}, classColors: {}, currentPage: 1, imagesPerPage: 20,
151
- modal: {
152
- transform: new DOMMatrix(), initialTransform: new DOMMatrix(), isPanning: false,
153
  lastPos: { x: 0, y: 0 }, img: null, labelText: '', currentItem: null,
154
  currentIndex: -1, lineWidth: 3
155
  }
@@ -171,12 +263,12 @@
171
  dom.modal.canvas.addEventListener('touchend', panEnd, { passive: false });
172
  dom.modal.zoomInBtn.addEventListener('click', () => applyZoom(1.2));
173
  dom.modal.zoomOutBtn.addEventListener('click', () => applyZoom(0.8));
174
- dom.modal.resetViewBtn.addEventListener('click', () => { state.modal.transform = state.modal.initialTransform.translate(0,0); redrawModalCanvas(); });
175
  dom.modal.fullscreenBtn.addEventListener('click', toggleFullscreen);
176
  dom.modal.thicknessSlider.addEventListener('input', handleThicknessChange);
177
  dom.modal.prevBtn.addEventListener('click', navigateImage.bind(null, -1));
178
  dom.modal.nextBtn.addEventListener('click', navigateImage.bind(null, 1));
179
- window.addEventListener('resize', () => { if(state.modal.img) showModal(state.modal.currentItem); });
180
  window.addEventListener('keydown', handleKeyPress);
181
  });
182
 
@@ -184,24 +276,58 @@
184
  async function handleZipUpload(event) {
185
  const file = event.target.files[0];
186
  if (!file) return;
187
-
188
  showStatus('Processing Zip...', 'loading');
189
  dom.zipFileNameDisplay.textContent = file.name;
190
  dom.labelsPanel.classList.remove('hidden');
191
 
192
  try {
193
  const zip = await JSZip.loadAsync(file);
194
- const imageFiles = {}, labelFiles = {};
 
 
 
195
  for (const filename in zip.files) {
196
  if (zip.files[filename].dir) continue;
197
- const baseName = filename.split('/').pop().split('.')[0];
198
- if (/\.(jpe?g|png|webp)$/i.test(filename)) imageFiles[baseName] = zip.files[filename];
199
- if (/\.txt$/i.test(filename)) labelFiles[baseName] = zip.files[filename];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
 
202
- state.allImageData = Object.keys(imageFiles)
203
- .filter(baseName => labelFiles[baseName])
204
- .map((baseName, index) => ({ id: baseName, image: imageFiles[baseName], label: labelFiles[baseName], originalIndex: index }));
205
 
206
  if (state.allImageData.length > 0) {
207
  dom.imageContainer.classList.remove('hidden');
@@ -210,19 +336,20 @@
210
  setupPagination();
211
  showStatus(`${state.allImageData.length} images loaded.`, 'success');
212
  } else {
213
- showStatus('No matching image/label pairs found.', 'error');
214
  }
215
  } catch (e) { showStatus('Error reading zip file.', 'error'); console.error(e); }
216
  }
217
 
 
218
  function handleYamlPaste(event) {
219
  const yamlString = event.target.value;
220
- if (!yamlString) { state.classNames = {}; if(state.allImageData.length > 0) renderPage(state.currentPage); return; }
221
  try {
222
  const data = jsyaml.load(yamlString);
223
  state.classNames = (data && Array.isArray(data.names)) ? data.names : {};
224
  showStatus('YAML class names updated.', 'success');
225
- if(state.allImageData.length > 0) renderPage(state.currentPage);
226
  } catch (e) { showStatus('Invalid YAML format.', 'warning'); }
227
  }
228
 
@@ -230,11 +357,12 @@
230
  state.modal.lineWidth = event.target.value;
231
  redrawModalCanvas();
232
  }
233
-
234
  function handleKeyPress(e) {
235
- if (dom.modal.backdrop.classList.contains('hidden')) return; // Modal not open
236
  if (e.key === 'ArrowLeft') navigateImage(-1);
237
  if (e.key === 'ArrowRight') navigateImage(1);
 
238
  }
239
 
240
  // --- UI & Drawing ---
@@ -245,11 +373,32 @@
245
 
246
  for (const item of paginatedItems) {
247
  const card = document.createElement('div');
248
- card.className = 'card-glow glass-card rounded-xl overflow-hidden transition-all duration-300 cursor-pointer';
249
- card.innerHTML = `<canvas class="w-full h-40 md:h-48 object-cover bg-gray-900/50"></canvas><h3 class="p-3 font-semibold text-sm text-blue-300 truncate">${item.image.name.split('/').pop()}</h3>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  dom.imageContainer.appendChild(card);
251
  card.addEventListener('click', () => showModal(item));
252
-
253
  const canvas = card.querySelector('canvas');
254
  const ctx = canvas.getContext('2d');
255
  const img = new Image();
@@ -257,7 +406,9 @@
257
  img.onload = async () => {
258
  canvas.width = img.width; canvas.height = img.height;
259
  ctx.drawImage(img, 0, 0);
260
- drawAnnotations(ctx, await item.label.async('string'), img.width, img.height);
 
 
261
  URL.revokeObjectURL(img.src);
262
  };
263
  }
@@ -265,11 +416,13 @@
265
  }
266
 
267
  function drawAnnotations(ctx, labelText, w, h, isModal = false) {
 
268
  const lines = labelText.trim().split('\n').filter(line => line.trim() !== '');
269
  if (lines.length === 0) return;
 
270
  const isDetection = lines[0].trim().split(' ').length === 5;
271
  const uniqueClasses = new Set();
272
-
273
  lines.forEach(line => {
274
  const parts = line.split(' ').map(parseFloat);
275
  const classId = parts[0];
@@ -278,44 +431,43 @@
278
  ctx.strokeStyle = color;
279
  const baseLineWidth = isModal ? state.modal.lineWidth : 2;
280
  ctx.lineWidth = baseLineWidth / (isModal ? state.modal.transform.a : 1);
281
-
282
  ctx.beginPath();
283
- if (isDetection) {
284
  const [, xc, yc, width, height] = parts;
285
  ctx.rect((xc - width / 2) * w, (yc - height / 2) * h, width * w, height * h);
286
- } else {
287
  const points = parts.slice(1);
288
  ctx.moveTo(points[0] * w, points[1] * h);
289
- for (let i = 2; i < points.length; i += 2) ctx.lineTo(points[i] * w, points[i+1] * h);
290
  ctx.closePath();
291
  }
292
  ctx.stroke();
293
  });
294
  if (isModal) buildLegend(uniqueClasses);
295
  }
296
-
297
  // --- Modal, Pan/Zoom, Gallery ---
298
  async function showModal(item) {
299
  state.modal.currentItem = item;
300
- // Find the index in the *master* list, not the paginated one
301
  state.modal.currentIndex = state.allImageData.findIndex(d => d.id === item.id);
302
 
303
  dom.modal.backdrop.classList.remove('hidden');
304
  dom.modal.spinner.classList.remove('hidden');
305
  dom.modal.legend.classList.add('hidden');
 
306
 
307
  const canvas = dom.modal.canvas;
308
  const ctx = canvas.getContext('2d');
309
  canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
310
  ctx.clearRect(0, 0, canvas.width, canvas.height);
311
 
312
- // Clean up previous image blob URL if it exists
313
  if (state.modal.img && state.modal.img.src.startsWith('blob:')) {
314
  URL.revokeObjectURL(state.modal.img.src);
315
  }
316
-
317
  state.modal.img = new Image();
318
- state.modal.labelText = await item.label.async('string');
319
 
320
  state.modal.img.onerror = () => {
321
  console.error("Modal image failed to load.");
@@ -324,27 +476,34 @@
324
  };
325
 
326
  state.modal.img.onload = () => {
327
- if (!state.modal.img || !state.modal.currentItem) return;
328
  canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
329
-
330
  const hRatio = canvas.width / state.modal.img.width;
331
  const vRatio = canvas.height / state.modal.img.height;
332
  const initialScale = Math.min(hRatio, vRatio) * 0.9;
333
-
334
  state.modal.transform = new DOMMatrix()
335
  .translate(canvas.width / 2, canvas.height / 2)
336
  .scale(initialScale, initialScale)
337
  .translate(-state.modal.img.width / 2, -state.modal.img.height / 2);
338
-
339
- state.modal.initialTransform = state.modal.transform.translate(0,0);
 
340
  dom.modal.spinner.classList.add('hidden');
341
  dom.modal.legend.classList.remove('hidden');
 
 
 
 
 
 
342
  redrawModalCanvas();
343
  };
344
-
345
  state.modal.img.src = URL.createObjectURL(await item.image.async('blob'));
346
  }
347
-
348
  function redrawModalCanvas() {
349
  if (!state.modal.img) return;
350
  const canvas = dom.modal.canvas, ctx = canvas.getContext('2d');
@@ -374,7 +533,7 @@
374
  state.modal.lastPos = { x: e.clientX, y: e.clientY };
375
  }
376
  function panEnd() { state.modal.isPanning = false; }
377
-
378
  function applyZoom(scaleAmount, clientX, clientY) {
379
  const canvas = dom.modal.canvas;
380
  const pt = new DOMPoint(clientX ?? canvas.clientWidth / 2, clientY ?? canvas.clientHeight / 2);
@@ -383,14 +542,14 @@
383
  .scaleSelf(scaleAmount, scaleAmount).translateSelf(-transformedPt.x, -transformedPt.y);
384
  redrawModalCanvas();
385
  }
386
-
387
  function handleZoom(e) { e.preventDefault(); applyZoom(e.deltaY > 0 ? 0.95 : 1.05, e.clientX, e.clientY); }
388
-
389
  function touchStart(e) {
390
  if (e.touches.length === 1) { e.preventDefault(); panStart(e.touches[0]); }
391
- if (e.touches.length === 2) { e.preventDefault(); state.modal.lastPos = { dist: Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY) };}
392
  }
393
-
394
  function touchMove(e) {
395
  e.preventDefault();
396
  if (e.touches.length === 1) panMove(e.touches[0]);
@@ -413,7 +572,8 @@
413
  }
414
 
415
  function toggleFullscreen() {
416
- if (!document.fullscreenElement) { dom.modal.backdrop.requestFullscreen().catch(err => alert(`Error: ${err.message}`));
 
417
  } else { document.exitFullscreen(); }
418
  }
419
 
@@ -435,9 +595,11 @@
435
  list.innerHTML += `<li class="flex items-center gap-2 mt-1"><span class="w-4 h-4 rounded-full border border-white/50" style="background-color:${generateColor(id)};"></span>${name}</li>`;
436
  });
437
  dom.modal.legend.appendChild(list);
 
 
438
  }
439
  }
440
-
441
  function setupPagination() {
442
  dom.paginationContainer.innerHTML = '';
443
  const pageCount = Math.ceil(state.allImageData.length / state.imagesPerPage);
@@ -453,7 +615,7 @@
453
  }
454
  updateActivePagination();
455
  }
456
-
457
  function updateActivePagination() {
458
  dom.paginationContainer.querySelectorAll('button').forEach(btn => {
459
  const isActive = parseInt(btn.dataset.page) === state.currentPage;
@@ -471,4 +633,5 @@
471
  }
472
  </script>
473
  </body>
 
474
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
7
+ <title>Futuristic Dataset Visualizer v6</title>
8
  <script src="https://cdn.tailwindcss.com"></script>
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
11
  <style>
12
  @keyframes aurora {
13
+ 0% {
14
+ background-position: 0% 50%;
15
+ }
16
+
17
+ 50% {
18
+ background-position: 100% 50%;
19
+ }
20
+
21
+ 100% {
22
+ background-position: 0% 50%;
23
+ }
24
+ }
25
+
26
+ html {
27
+ scroll-behavior: smooth;
28
  }
29
+
30
  body {
31
  font-family: 'SF Mono', 'Courier New', Courier, monospace;
32
  background-color: #0D1117;
33
  color: #E6EDF3;
34
  background-image: radial-gradient(at 27% 37%, hsla(215, 98%, 61%, 0.1) 0px, transparent 50%),
35
+ radial-gradient(at 97% 21%, hsla(125, 98%, 72%, 0.1) 0px, transparent 50%);
36
  background-size: 250% 250%;
37
  animation: aurora 20s ease infinite;
38
  }
39
+
40
  .glass-card {
41
+ background: rgba(17, 25, 40, 0.75);
42
  backdrop-filter: blur(16px) saturate(180%);
43
  -webkit-backdrop-filter: blur(16px) saturate(180%);
44
  border: 1px solid rgba(255, 255, 255, 0.1);
45
  }
46
+
47
  .btn-glow:hover {
48
  box-shadow: 0 0 20px rgba(59, 130, 246, 0.5), 0 0 8px rgba(59, 130, 246, 0.4);
49
  }
50
+
51
  .card-glow:hover {
52
  transform: translateY(-4px);
53
  box-shadow: 0 0 25px rgba(59, 130, 246, 0.3), 0 0 10px rgba(59, 130, 246, 0.2);
54
  }
55
+
56
+ .modal {
57
+ background: rgba(13, 17, 23, 0.85);
58
+ backdrop-filter: blur(16px);
59
+ }
60
+
61
+ #modal-canvas {
62
+ cursor: grab;
63
+ touch-action: none;
64
+ }
65
+
66
+ #modal-canvas:active {
67
+ cursor: grabbing;
68
+ }
69
+
70
+ input[type=range] {
71
+ -webkit-appearance: none;
72
+ background: transparent;
73
+ }
74
+
75
+ input[type=range]:focus {
76
+ outline: none;
77
+ }
78
+
79
  input[type=range]::-webkit-slider-runnable-track {
80
+ height: 4px;
81
+ background: rgba(59, 130, 246, 0.5);
82
+ border-radius: 5px;
83
  }
84
+
85
  input[type=range]::-webkit-slider-thumb {
86
+ -webkit-appearance: none;
87
+ height: 18px;
88
+ width: 18px;
89
+ border-radius: 50%;
90
+ background: #E6EDF3;
91
+ border: 2px solid #0D1117;
92
+ margin-top: -7px;
93
  cursor: pointer;
94
  }
95
+
96
+ .badge {
97
+ position: absolute;
98
+ top: 0.5rem;
99
+ font-size: 0.7rem;
100
+ font-weight: bold;
101
+ padding: 0.15rem 0.5rem;
102
+ border-radius: 0.375rem;
103
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
104
+ }
105
  </style>
106
  </head>
107
+
108
  <body class="overscroll-none">
109
 
110
  <div class="container mx-auto p-4 lg:p-8">
111
 
112
  <header class="text-center mb-10">
113
+ <h1
114
+ class="text-3xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-teal-300 mb-2">
115
+ Dataset Visualizer v6</h1>
116
  <p class="text-gray-400">Inspect, pan, and zoom your vision datasets with precision.</p>
117
  </header>
118
+
119
  <div class="glass-card p-4 sm:p-6 rounded-xl mb-10 max-w-3xl mx-auto">
120
  <div id="control-panel-content">
121
  <h2 class="text-2xl font-semibold text-blue-300 mb-4 text-center">Get Started</h2>
122
+ <label for="zip-file"
123
+ class="btn-glow bg-blue-600 text-white flex items-center justify-center w-full p-4 rounded-lg cursor-pointer text-xl transition-all duration-300">
124
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-3" fill="none" viewBox="0 0 24 24"
125
+ stroke="currentColor">
126
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
127
+ d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
128
+ </svg>
129
  Upload dataset.zip
130
  </label>
131
  <input type="file" id="zip-file" accept=".zip" class="hidden">
132
+ <p id="zip-file-name" class="text-center text-gray-500 mt-2 text-sm h-5"></p>
133
  <div id="status-container" class="h-6 text-center mt-2"></div>
134
  </div>
135
  <div id="labels-panel" class="hidden mt-4 pt-4 border-t border-gray-700">
136
  <h3 class="text-lg font-semibold text-blue-300 mb-2">Class Labels (Optional)</h3>
137
+ <textarea id="yaml-paste" rows="4"
138
+ class="w-full bg-gray-900 border border-gray-600 rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
139
+ placeholder="Paste COCO .yaml content here..."></textarea>
140
  </div>
141
  </div>
142
+
143
  <div id="image-container-wrapper">
144
+ <div id="empty-state" class="text-center glass-card p-10 rounded-xl">
145
  <h3 class="text-2xl text-gray-400">Your visualizer is ready.</h3>
146
  <p class="text-gray-500">Upload a dataset to begin.</p>
147
  </div>
148
+ <div id="image-container"
149
+ class="hidden grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-6"></div>
150
  </div>
151
 
152
  <div id="pagination" class="flex justify-center items-center space-x-2 mt-10"></div>
 
156
  <div id="modal-backdrop" class="fixed inset-0 z-50 flex items-center justify-center hidden">
157
  <div class="modal w-full h-full relative flex items-center justify-center overflow-hidden">
158
  <div id="modal-spinner" class="absolute z-10">
159
+ <svg class="animate-spin h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
160
+ viewBox="0 0 24 24">
161
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
162
+ <path class="opacity-75" fill="currentColor"
163
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
164
+ </path>
165
+ </svg>
166
  </div>
167
+
168
  <canvas id="modal-canvas" class="absolute top-0 left-0 w-full h-full"></canvas>
169
+
170
+ <div id="modal-info" class="absolute top-4 left-4 glass-card p-3 rounded-lg text-sm z-20 hidden"></div>
171
+ <div id="modal-legend"
172
+ class="absolute bottom-4 left-4 glass-card p-3 rounded-lg max-h-48 overflow-y-auto text-sm z-20 hidden">
173
+ </div>
174
+
175
  <div class="absolute top-4 right-4 z-20 flex flex-col gap-2">
176
+ <button id="modal-close"
177
+ class="bg-red-600/80 text-white rounded-full w-10 h-10 flex items-center justify-center text-3xl font-bold">×</button>
178
  </div>
179
 
180
+ <button id="prev-image-btn"
181
+ class="absolute left-4 top-1/2 -translate-y-1/2 z-20 glass-card rounded-full p-3"><svg
182
+ xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
183
+ stroke="currentColor">
184
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
185
+ </svg></button>
186
+ <button id="next-image-btn"
187
+ class="absolute right-4 top-1/2 -translate-y-1/2 z-20 glass-card rounded-full p-3"><svg
188
+ xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
189
+ stroke="currentColor">
190
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
191
+ </svg></button>
192
 
193
  <div class="absolute bottom-4 right-4 z-20 glass-card rounded-lg flex items-center">
 
194
  <div class="p-3 flex items-center gap-3 border-r border-gray-600">
195
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
196
+ <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
197
+ <path fill-rule="evenodd"
198
+ d="M.458 10C3.732 4.943 9.522 3 10 3s6.268 1.943 9.542 7c-3.274 5.057-9.064 7-9.542 7S3.732 15.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
199
+ clip-rule="evenodd" />
200
+ </svg>
201
  <input type="range" id="line-thickness-slider" min="1" max="15" value="3" class="w-24">
202
  </div>
203
  <button id="zoom-out-btn" class="p-3 text-2xl">-</button>
204
  <button id="reset-view-btn" class="p-3">
205
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
206
+ <path fill-rule="evenodd"
207
+ d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
208
+ clip-rule="evenodd" />
209
+ </svg>
210
  </button>
211
  <button id="zoom-in-btn" class="p-3 text-2xl">+</button>
212
  <button id="fullscreen-btn" class="p-3 ml-2 border-l border-gray-600">
213
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
214
+ <path fill-rule="evenodd"
215
+ d="M3 4a1 1 0 011-1h4a1 1 0 110 2H5v2a1 1 0 11-2 0V4zm12 0a1 1 0 011 1v2a1 1 0 11-2 0V5h-2a1 1 0 110-2h4zM4 17a1 1 0 01-1-1v-2a1 1 0 112 0v2h2a1 1 0 110 2H4zm12 0a1 1 0 01-1-1h-4a1 1 0 110-2h2v-2a1 1 0 112 0v2z"
216
+ clip-rule="evenodd" />
217
+ </svg>
218
  </button>
219
  </div>
220
  </div>
 
230
  modal: {
231
  backdrop: document.getElementById('modal-backdrop'), canvas: document.getElementById('modal-canvas'),
232
  closeBtn: document.getElementById('modal-close'), legend: document.getElementById('modal-legend'),
233
+ info: document.getElementById('modal-info'), spinner: document.getElementById('modal-spinner'),
234
  zoomInBtn: document.getElementById('zoom-in-btn'), zoomOutBtn: document.getElementById('zoom-out-btn'),
235
  resetViewBtn: document.getElementById('reset-view-btn'), fullscreenBtn: document.getElementById('fullscreen-btn'),
236
  prevBtn: document.getElementById('prev-image-btn'), nextBtn: document.getElementById('next-image-btn'),
 
240
 
241
  let state = {
242
  allImageData: [], classNames: {}, classColors: {}, currentPage: 1, imagesPerPage: 20,
243
+ modal: {
244
+ transform: new DOMMatrix(), initialTransform: new DOMMatrix(), isPanning: false,
245
  lastPos: { x: 0, y: 0 }, img: null, labelText: '', currentItem: null,
246
  currentIndex: -1, lineWidth: 3
247
  }
 
263
  dom.modal.canvas.addEventListener('touchend', panEnd, { passive: false });
264
  dom.modal.zoomInBtn.addEventListener('click', () => applyZoom(1.2));
265
  dom.modal.zoomOutBtn.addEventListener('click', () => applyZoom(0.8));
266
+ dom.modal.resetViewBtn.addEventListener('click', () => { state.modal.transform = state.modal.initialTransform.translate(0, 0); redrawModalCanvas(); });
267
  dom.modal.fullscreenBtn.addEventListener('click', toggleFullscreen);
268
  dom.modal.thicknessSlider.addEventListener('input', handleThicknessChange);
269
  dom.modal.prevBtn.addEventListener('click', navigateImage.bind(null, -1));
270
  dom.modal.nextBtn.addEventListener('click', navigateImage.bind(null, 1));
271
+ window.addEventListener('resize', () => { if (state.modal.img) showModal(state.modal.currentItem); });
272
  window.addEventListener('keydown', handleKeyPress);
273
  });
274
 
 
276
  async function handleZipUpload(event) {
277
  const file = event.target.files[0];
278
  if (!file) return;
279
+
280
  showStatus('Processing Zip...', 'loading');
281
  dom.zipFileNameDisplay.textContent = file.name;
282
  dom.labelsPanel.classList.remove('hidden');
283
 
284
  try {
285
  const zip = await JSZip.loadAsync(file);
286
+ const imageFiles = {};
287
+ const labelFiles = {};
288
+
289
+ // Separate files into images and labels
290
  for (const filename in zip.files) {
291
  if (zip.files[filename].dir) continue;
292
+ if (/\.(jpe?g|png|webp)$/i.test(filename)) {
293
+ imageFiles[filename] = zip.files[filename];
294
+ } else if (/\.txt$/i.test(filename)) {
295
+ labelFiles[filename] = zip.files[filename];
296
+ }
297
+ }
298
+
299
+ state.allImageData = [];
300
+ let index = 0;
301
+ // Iterate over all found images
302
+ for (const imagePath in imageFiles) {
303
+ const imageFile = imageFiles[imagePath];
304
+
305
+ // Construct possible label paths
306
+ const pathParts = imagePath.split('/');
307
+ const imageName = pathParts.pop();
308
+ const baseName = imageName.split('.').slice(0, -1).join('.');
309
+ const labelName = `${baseName}.txt`;
310
+
311
+ // A common structure is '/images/train/' paired with '/labels/train/'
312
+ const possibleLabelPath = [...pathParts, labelName].join('/').replace(/images/i, 'labels');
313
+
314
+ const labelFile = labelFiles[possibleLabelPath] || null;
315
+
316
+ // Determine the dataset split (train/val/test) from the path
317
+ const splitMatch = imagePath.match(/(train|val|validation|test|testing)/i);
318
+ const split = splitMatch ? splitMatch[0].replace('validation', 'val').replace('testing', 'test') : 'unknown';
319
+
320
+ state.allImageData.push({
321
+ id: imagePath, // Use the full path as a unique ID
322
+ image: imageFile,
323
+ label: labelFile,
324
+ split: split,
325
+ originalIndex: index++
326
+ });
327
  }
328
 
329
+ state.allImageData.sort((a, b) => a.id.localeCompare(b.id));
330
+
 
331
 
332
  if (state.allImageData.length > 0) {
333
  dom.imageContainer.classList.remove('hidden');
 
336
  setupPagination();
337
  showStatus(`${state.allImageData.length} images loaded.`, 'success');
338
  } else {
339
+ showStatus('No images found in the zip file.', 'error');
340
  }
341
  } catch (e) { showStatus('Error reading zip file.', 'error'); console.error(e); }
342
  }
343
 
344
+
345
  function handleYamlPaste(event) {
346
  const yamlString = event.target.value;
347
+ if (!yamlString) { state.classNames = {}; if (state.allImageData.length > 0) renderPage(state.currentPage); return; }
348
  try {
349
  const data = jsyaml.load(yamlString);
350
  state.classNames = (data && Array.isArray(data.names)) ? data.names : {};
351
  showStatus('YAML class names updated.', 'success');
352
+ if (state.allImageData.length > 0) renderPage(state.currentPage);
353
  } catch (e) { showStatus('Invalid YAML format.', 'warning'); }
354
  }
355
 
 
357
  state.modal.lineWidth = event.target.value;
358
  redrawModalCanvas();
359
  }
360
+
361
  function handleKeyPress(e) {
362
+ if (dom.modal.backdrop.classList.contains('hidden')) return;
363
  if (e.key === 'ArrowLeft') navigateImage(-1);
364
  if (e.key === 'ArrowRight') navigateImage(1);
365
+ if (e.key === 'Escape') closeModal();
366
  }
367
 
368
  // --- UI & Drawing ---
 
373
 
374
  for (const item of paginatedItems) {
375
  const card = document.createElement('div');
376
+ card.className = 'relative card-glow glass-card rounded-xl overflow-hidden transition-all duration-300 cursor-pointer';
377
+
378
+ const splitColors = {
379
+ train: 'bg-blue-600/90',
380
+ val: 'bg-purple-600/90',
381
+ test: 'bg-green-600/90',
382
+ };
383
+
384
+ const badgeColorClass = splitColors[item.split.toLowerCase()] || 'bg-gray-500/90';
385
+
386
+ const splitBadge = item.split !== 'unknown'
387
+ ? `<div class="badge left-2 ${badgeColorClass} text-white">${item.split.toUpperCase()}</div>`
388
+ : '';
389
+ const noLabelBadge = !item.label ? `<div class="badge right-2 bg-yellow-600/90 text-white">NO LABEL</div>` : '';
390
+
391
+ card.innerHTML = `
392
+ <div class="relative">
393
+ <canvas class="w-full h-40 md:h-48 object-cover bg-gray-900/50"></canvas>
394
+ ${splitBadge}
395
+ ${noLabelBadge}
396
+ </div>
397
+ <h3 class="p-3 font-semibold text-sm text-blue-300 truncate">${item.image.name.split('/').pop()}</h3>`;
398
+
399
  dom.imageContainer.appendChild(card);
400
  card.addEventListener('click', () => showModal(item));
401
+
402
  const canvas = card.querySelector('canvas');
403
  const ctx = canvas.getContext('2d');
404
  const img = new Image();
 
406
  img.onload = async () => {
407
  canvas.width = img.width; canvas.height = img.height;
408
  ctx.drawImage(img, 0, 0);
409
+ if (item.label) {
410
+ drawAnnotations(ctx, await item.label.async('string'), img.width, img.height);
411
+ }
412
  URL.revokeObjectURL(img.src);
413
  };
414
  }
 
416
  }
417
 
418
  function drawAnnotations(ctx, labelText, w, h, isModal = false) {
419
+ if (!labelText) return;
420
  const lines = labelText.trim().split('\n').filter(line => line.trim() !== '');
421
  if (lines.length === 0) return;
422
+
423
  const isDetection = lines[0].trim().split(' ').length === 5;
424
  const uniqueClasses = new Set();
425
+
426
  lines.forEach(line => {
427
  const parts = line.split(' ').map(parseFloat);
428
  const classId = parts[0];
 
431
  ctx.strokeStyle = color;
432
  const baseLineWidth = isModal ? state.modal.lineWidth : 2;
433
  ctx.lineWidth = baseLineWidth / (isModal ? state.modal.transform.a : 1);
434
+
435
  ctx.beginPath();
436
+ if (isDetection) { // Bounding Box: [class, x_center, y_center, width, height]
437
  const [, xc, yc, width, height] = parts;
438
  ctx.rect((xc - width / 2) * w, (yc - height / 2) * h, width * w, height * h);
439
+ } else { // Segmentation: [class, x1, y1, x2, y2, ...]
440
  const points = parts.slice(1);
441
  ctx.moveTo(points[0] * w, points[1] * h);
442
+ for (let i = 2; i < points.length; i += 2) ctx.lineTo(points[i] * w, points[i + 1] * h);
443
  ctx.closePath();
444
  }
445
  ctx.stroke();
446
  });
447
  if (isModal) buildLegend(uniqueClasses);
448
  }
449
+
450
  // --- Modal, Pan/Zoom, Gallery ---
451
  async function showModal(item) {
452
  state.modal.currentItem = item;
 
453
  state.modal.currentIndex = state.allImageData.findIndex(d => d.id === item.id);
454
 
455
  dom.modal.backdrop.classList.remove('hidden');
456
  dom.modal.spinner.classList.remove('hidden');
457
  dom.modal.legend.classList.add('hidden');
458
+ dom.modal.info.classList.add('hidden');
459
 
460
  const canvas = dom.modal.canvas;
461
  const ctx = canvas.getContext('2d');
462
  canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
463
  ctx.clearRect(0, 0, canvas.width, canvas.height);
464
 
 
465
  if (state.modal.img && state.modal.img.src.startsWith('blob:')) {
466
  URL.revokeObjectURL(state.modal.img.src);
467
  }
468
+
469
  state.modal.img = new Image();
470
+ state.modal.labelText = item.label ? await item.label.async('string') : '';
471
 
472
  state.modal.img.onerror = () => {
473
  console.error("Modal image failed to load.");
 
476
  };
477
 
478
  state.modal.img.onload = () => {
479
+ if (!state.modal.img || !state.modal.currentItem) return;
480
  canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
481
+
482
  const hRatio = canvas.width / state.modal.img.width;
483
  const vRatio = canvas.height / state.modal.img.height;
484
  const initialScale = Math.min(hRatio, vRatio) * 0.9;
485
+
486
  state.modal.transform = new DOMMatrix()
487
  .translate(canvas.width / 2, canvas.height / 2)
488
  .scale(initialScale, initialScale)
489
  .translate(-state.modal.img.width / 2, -state.modal.img.height / 2);
490
+
491
+ state.modal.initialTransform = state.modal.transform.translate(0, 0);
492
+
493
  dom.modal.spinner.classList.add('hidden');
494
  dom.modal.legend.classList.remove('hidden');
495
+ dom.modal.info.classList.remove('hidden');
496
+
497
+ // Populate modal info
498
+ const splitText = item.split !== 'unknown' ? `<span class="block mt-1 text-xs font-bold text-blue-300 bg-blue-900/50 px-2 py-1 rounded-full">${item.split.toUpperCase()}</span>` : '';
499
+ dom.modal.info.innerHTML = `<p class="truncate">${item.image.name.split('/').pop()}</p>${splitText}`;
500
+
501
  redrawModalCanvas();
502
  };
503
+
504
  state.modal.img.src = URL.createObjectURL(await item.image.async('blob'));
505
  }
506
+
507
  function redrawModalCanvas() {
508
  if (!state.modal.img) return;
509
  const canvas = dom.modal.canvas, ctx = canvas.getContext('2d');
 
533
  state.modal.lastPos = { x: e.clientX, y: e.clientY };
534
  }
535
  function panEnd() { state.modal.isPanning = false; }
536
+
537
  function applyZoom(scaleAmount, clientX, clientY) {
538
  const canvas = dom.modal.canvas;
539
  const pt = new DOMPoint(clientX ?? canvas.clientWidth / 2, clientY ?? canvas.clientHeight / 2);
 
542
  .scaleSelf(scaleAmount, scaleAmount).translateSelf(-transformedPt.x, -transformedPt.y);
543
  redrawModalCanvas();
544
  }
545
+
546
  function handleZoom(e) { e.preventDefault(); applyZoom(e.deltaY > 0 ? 0.95 : 1.05, e.clientX, e.clientY); }
547
+
548
  function touchStart(e) {
549
  if (e.touches.length === 1) { e.preventDefault(); panStart(e.touches[0]); }
550
+ if (e.touches.length === 2) { e.preventDefault(); state.modal.lastPos = { dist: Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY) }; }
551
  }
552
+
553
  function touchMove(e) {
554
  e.preventDefault();
555
  if (e.touches.length === 1) panMove(e.touches[0]);
 
572
  }
573
 
574
  function toggleFullscreen() {
575
+ if (!document.fullscreenElement) {
576
+ dom.modal.backdrop.requestFullscreen().catch(err => alert(`Error trying to enable full-screen mode: ${err.message} (${err.name})`));
577
  } else { document.exitFullscreen(); }
578
  }
579
 
 
595
  list.innerHTML += `<li class="flex items-center gap-2 mt-1"><span class="w-4 h-4 rounded-full border border-white/50" style="background-color:${generateColor(id)};"></span>${name}</li>`;
596
  });
597
  dom.modal.legend.appendChild(list);
598
+ } else {
599
+ dom.modal.legend.innerHTML = '<p class="text-gray-400">No annotations</p>';
600
  }
601
  }
602
+
603
  function setupPagination() {
604
  dom.paginationContainer.innerHTML = '';
605
  const pageCount = Math.ceil(state.allImageData.length / state.imagesPerPage);
 
615
  }
616
  updateActivePagination();
617
  }
618
+
619
  function updateActivePagination() {
620
  dom.paginationContainer.querySelectorAll('button').forEach(btn => {
621
  const isActive = parseInt(btn.dataset.page) === state.currentPage;
 
633
  }
634
  </script>
635
  </body>
636
+
637
  </html>