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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +98 -46
index.html CHANGED
@@ -3,7 +3,7 @@
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 v4</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>
@@ -39,6 +39,18 @@
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>
43
  </head>
44
  <body class="overscroll-none">
@@ -46,11 +58,10 @@
46
  <div class="container mx-auto p-4 lg:p-8">
47
 
48
  <header class="text-center mb-10">
49
- <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 v4</h1>
50
  <p class="text-gray-400">Inspect, pan, and zoom your vision datasets with precision.</p>
51
  </header>
52
 
53
- <!-- Control Panel -->
54
  <div class="glass-card p-4 sm:p-6 rounded-xl mb-10 max-w-3xl mx-auto">
55
  <div id="control-panel-content">
56
  <h2 class="text-2xl font-semibold text-blue-300 mb-4 text-center">Get Started</h2>
@@ -68,7 +79,6 @@
68
  </div>
69
  </div>
70
 
71
- <!-- Image Grid -->
72
  <div id="image-container-wrapper">
73
  <div id="empty-state" class="text-center glass-card p-10 rounded-xl">
74
  <h3 class="text-2xl text-gray-400">Your visualizer is ready.</h3>
@@ -83,12 +93,10 @@
83
  <!-- Modal -->
84
  <div id="modal-backdrop" class="fixed inset-0 z-50 flex items-center justify-center hidden">
85
  <div class="modal w-full h-full relative flex items-center justify-center overflow-hidden">
86
- <!-- Loading Spinner for Modal -->
87
  <div id="modal-spinner" class="absolute z-10">
88
  <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>
89
  </div>
90
 
91
- <!-- THIS IS THE HTML FIX: Added w-full and h-full classes -->
92
  <canvas id="modal-canvas" class="absolute top-0 left-0 w-full h-full"></canvas>
93
 
94
  <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>
@@ -97,14 +105,24 @@
97
  <div class="absolute top-4 right-4 z-20 flex flex-col gap-2">
98
  <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>
99
  </div>
 
 
 
 
 
100
  <div class="absolute bottom-4 right-4 z-20 glass-card rounded-lg flex items-center">
 
 
 
 
 
101
  <button id="zoom-out-btn" class="p-3 text-2xl">-</button>
102
  <button id="reset-view-btn" class="p-3">
103
  <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>
104
  </button>
105
  <button id="zoom-in-btn" class="p-3 text-2xl">+</button>
106
  <button id="fullscreen-btn" class="p-3 ml-2 border-l border-gray-600">
107
- <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>
108
  </button>
109
  </div>
110
  </div>
@@ -123,32 +141,44 @@
123
  spinner: document.getElementById('modal-spinner'),
124
  zoomInBtn: document.getElementById('zoom-in-btn'), zoomOutBtn: document.getElementById('zoom-out-btn'),
125
  resetViewBtn: document.getElementById('reset-view-btn'), fullscreenBtn: document.getElementById('fullscreen-btn'),
 
 
126
  }
127
  };
128
 
129
  let state = {
130
  allImageData: [], classNames: {}, classColors: {}, currentPage: 1, imagesPerPage: 20,
131
- modal: { transform: new DOMMatrix(), initialTransform: new DOMMatrix(), isPanning: false, lastPos: { x: 0, y: 0 }, img: null, labelText: ''}
 
 
 
 
132
  };
133
 
134
  // --- Event Listeners ---
135
- dom.zipFileInput.addEventListener('change', handleZipUpload);
136
- dom.yamlPasteArea.addEventListener('input', handleYamlPaste);
137
- dom.modal.backdrop.addEventListener('click', (e) => e.target === dom.modal.backdrop && closeModal());
138
- dom.modal.closeBtn.addEventListener('click', closeModal);
139
- dom.modal.canvas.addEventListener('mousedown', panStart);
140
- dom.modal.canvas.addEventListener('mousemove', panMove);
141
- dom.modal.canvas.addEventListener('mouseup', panEnd);
142
- dom.modal.canvas.addEventListener('mouseout', panEnd);
143
- dom.modal.canvas.addEventListener('wheel', handleZoom);
144
- dom.modal.canvas.addEventListener('touchstart', touchStart);
145
- dom.modal.canvas.addEventListener('touchmove', touchMove);
146
- dom.modal.canvas.addEventListener('touchend', panEnd);
147
- dom.modal.zoomInBtn.addEventListener('click', () => applyZoom(1.2));
148
- dom.modal.zoomOutBtn.addEventListener('click', () => applyZoom(0.8));
149
- dom.modal.resetViewBtn.addEventListener('click', () => { state.modal.transform = state.modal.initialTransform.translate(0,0); redrawModalCanvas(); });
150
- dom.modal.fullscreenBtn.addEventListener('click', toggleFullscreen);
151
- window.addEventListener('resize', () => { if(state.modal.img) showModal(state.modal.currentItem); });
 
 
 
 
 
 
152
 
153
  // --- Core Logic ---
154
  async function handleZipUpload(event) {
@@ -165,13 +195,13 @@
165
  for (const filename in zip.files) {
166
  if (zip.files[filename].dir) continue;
167
  const baseName = filename.split('/').pop().split('.')[0];
168
- if (/\.(jpe?g|png)$/i.test(filename)) imageFiles[baseName] = zip.files[filename];
169
  if (/\.txt$/i.test(filename)) labelFiles[baseName] = zip.files[filename];
170
  }
171
 
172
  state.allImageData = Object.keys(imageFiles)
173
  .filter(baseName => labelFiles[baseName])
174
- .map(baseName => ({ id: baseName, image: imageFiles[baseName], label: labelFiles[baseName] }));
175
 
176
  if (state.allImageData.length > 0) {
177
  dom.imageContainer.classList.remove('hidden');
@@ -190,12 +220,23 @@
190
  if (!yamlString) { state.classNames = {}; if(state.allImageData.length > 0) renderPage(state.currentPage); return; }
191
  try {
192
  const data = jsyaml.load(yamlString);
193
- state.classNames = (data && data.names) ? data.names : {};
194
  showStatus('YAML class names updated.', 'success');
195
  if(state.allImageData.length > 0) renderPage(state.currentPage);
196
  } catch (e) { showStatus('Invalid YAML format.', 'warning'); }
197
  }
198
 
 
 
 
 
 
 
 
 
 
 
 
199
  // --- UI & Drawing ---
200
  async function renderPage(page) {
201
  state.currentPage = page;
@@ -235,7 +276,9 @@
235
  uniqueClasses.add(classId);
236
  const color = generateColor(classId);
237
  ctx.strokeStyle = color;
238
- ctx.lineWidth = (isModal ? 3 : 2) / (isModal ? state.modal.transform.a : 1);
 
 
239
  ctx.beginPath();
240
  if (isDetection) {
241
  const [, xc, yc, width, height] = parts;
@@ -251,21 +294,26 @@
251
  if (isModal) buildLegend(uniqueClasses);
252
  }
253
 
254
- // --- Modal & Pan/Zoom (IMPROVED & FIXED) ---
255
  async function showModal(item) {
 
 
 
 
256
  dom.modal.backdrop.classList.remove('hidden');
257
  dom.modal.spinner.classList.remove('hidden');
258
  dom.modal.legend.classList.add('hidden');
259
 
260
  const canvas = dom.modal.canvas;
261
  const ctx = canvas.getContext('2d');
262
- // Thanks to the HTML fix, canvas.clientWidth/Height are now correct.
263
- // We clear any previous drawing immediately.
264
- canvas.width = canvas.clientWidth;
265
- canvas.height = canvas.clientHeight;
266
  ctx.clearRect(0, 0, canvas.width, canvas.height);
267
 
268
- state.modal.currentItem = item;
 
 
 
 
269
  state.modal.img = new Image();
270
  state.modal.labelText = await item.label.async('string');
271
 
@@ -276,27 +324,21 @@
276
  };
277
 
278
  state.modal.img.onload = () => {
279
- if (!state.modal.img) return; // Modal was closed before image loaded.
280
-
281
- // Set canvas resolution to its displayed size. This is crucial.
282
- canvas.width = canvas.clientWidth;
283
- canvas.height = canvas.clientHeight;
284
 
285
  const hRatio = canvas.width / state.modal.img.width;
286
  const vRatio = canvas.height / state.modal.img.height;
287
  const initialScale = Math.min(hRatio, vRatio) * 0.9;
288
 
289
- // Calculate the transformation to center and scale the image.
290
  state.modal.transform = new DOMMatrix()
291
  .translate(canvas.width / 2, canvas.height / 2)
292
  .scale(initialScale, initialScale)
293
  .translate(-state.modal.img.width / 2, -state.modal.img.height / 2);
294
 
295
  state.modal.initialTransform = state.modal.transform.translate(0,0);
296
-
297
  dom.modal.spinner.classList.add('hidden');
298
  dom.modal.legend.classList.remove('hidden');
299
-
300
  redrawModalCanvas();
301
  };
302
 
@@ -315,6 +357,14 @@
315
  ctx.restore();
316
  }
317
 
 
 
 
 
 
 
 
 
318
  function panStart(e) { e.preventDefault(); state.modal.isPanning = true; state.modal.lastPos = { x: e.clientX, y: e.clientY }; }
319
  function panMove(e) {
320
  e.preventDefault();
@@ -334,7 +384,7 @@
334
  redrawModalCanvas();
335
  }
336
 
337
- function handleZoom(e) { e.preventDefault(); applyZoom(e.deltaY > 0 ? 0.9 : 1.1, e.clientX, e.clientY); }
338
 
339
  function touchStart(e) {
340
  if (e.touches.length === 1) { e.preventDefault(); panStart(e.touches[0]); }
@@ -357,11 +407,13 @@
357
  URL.revokeObjectURL(state.modal.img.src);
358
  }
359
  state.modal.img = null;
 
 
360
  if (document.fullscreenElement) document.exitFullscreen();
361
  }
362
 
363
  function toggleFullscreen() {
364
- if (!document.fullscreenElement) { dom.modal.backdrop.requestFullscreen().catch(err => alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`));
365
  } else { document.exitFullscreen(); }
366
  }
367
 
@@ -380,7 +432,7 @@
380
  const list = document.createElement('ul');
381
  classIds.forEach(id => {
382
  const name = state.classNames[id] || `Class ${id}`;
383
- list.innerHTML += `<li class="flex items-center gap-2"><span class="w-4 h-4 rounded-full border border-white/50" style="background-color:${generateColor(id)};"></span>${name}</li>`;
384
  });
385
  dom.modal.legend.appendChild(list);
386
  }
 
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>
 
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">
 
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>
 
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>
 
93
  <!-- Modal -->
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>
 
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>
 
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'),
145
+ thicknessSlider: document.getElementById('line-thickness-slider'),
146
  }
147
  };
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
+ }
156
  };
157
 
158
  // --- Event Listeners ---
159
+ document.addEventListener('DOMContentLoaded', () => {
160
+ dom.zipFileInput.addEventListener('change', handleZipUpload);
161
+ dom.yamlPasteArea.addEventListener('input', handleYamlPaste);
162
+ dom.modal.backdrop.addEventListener('click', (e) => e.target === dom.modal.backdrop && closeModal());
163
+ dom.modal.closeBtn.addEventListener('click', closeModal);
164
+ dom.modal.canvas.addEventListener('mousedown', panStart);
165
+ dom.modal.canvas.addEventListener('mousemove', panMove);
166
+ dom.modal.canvas.addEventListener('mouseup', panEnd);
167
+ dom.modal.canvas.addEventListener('mouseout', panEnd);
168
+ dom.modal.canvas.addEventListener('wheel', handleZoom);
169
+ dom.modal.canvas.addEventListener('touchstart', touchStart, { passive: false });
170
+ dom.modal.canvas.addEventListener('touchmove', touchMove, { passive: false });
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
 
183
  // --- Core Logic ---
184
  async function handleZipUpload(event) {
 
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');
 
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
 
229
+ function handleThicknessChange(event) {
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 ---
241
  async function renderPage(page) {
242
  state.currentPage = page;
 
276
  uniqueClasses.add(classId);
277
  const color = generateColor(classId);
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;
 
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
 
 
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
 
 
357
  ctx.restore();
358
  }
359
 
360
+ function navigateImage(direction) {
361
+ if (state.modal.currentIndex === -1) return;
362
+ let newIndex = state.modal.currentIndex + direction;
363
+ if (newIndex < 0) newIndex = state.allImageData.length - 1;
364
+ if (newIndex >= state.allImageData.length) newIndex = 0;
365
+ showModal(state.allImageData[newIndex]);
366
+ }
367
+
368
  function panStart(e) { e.preventDefault(); state.modal.isPanning = true; state.modal.lastPos = { x: e.clientX, y: e.clientY }; }
369
  function panMove(e) {
370
  e.preventDefault();
 
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]); }
 
407
  URL.revokeObjectURL(state.modal.img.src);
408
  }
409
  state.modal.img = null;
410
+ state.modal.currentItem = null;
411
+ state.modal.currentIndex = -1;
412
  if (document.fullscreenElement) document.exitFullscreen();
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
 
 
432
  const list = document.createElement('ul');
433
  classIds.forEach(id => {
434
  const name = state.classNames[id] || `Class ${id}`;
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
  }