thelip commited on
Commit
2ecd1bc
·
verified ·
1 Parent(s): 749355d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +199 -144
index.html CHANGED
@@ -6,9 +6,10 @@
6
  <title>Advanced Photo to SVG Converter</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <!-- Use the Potrace library that supports color tracing directly -->
9
- <script src="https://unpkg.com/potrace-wasm@0.3.0/dist/potrace-wasm.js"></script>
10
- <!-- Quantize is still useful for limiting the palette beforehand if desired, but potrace-wasm handles colors -->
11
- <script src="https://cdn.jsdelivr.net/npm/quantize@1.0.7/dist/quantize.min.js"></script>
 
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
13
  <style>
14
  .dropzone { border: 3px dashed rgba(59, 130, 246, 0.5); transition: all 0.3s ease; }
@@ -18,22 +19,31 @@
18
  .comparison-slider::-webkit-resizer { display: none; /* Hide default resizer */ }
19
  .slider-handle { position: absolute; right: -6px; /* Center handle over border */ top: 50%; transform: translateY(-50%); width: 10px; height: 40px; background-color: rgba(59, 130, 246, 0.8); border-radius: 3px; cursor: ew-resize; z-index: 10; border: 1px solid rgba(255, 255, 255, 0.5); }
20
  .progress-bar { height: 5px; transition: width 0.1s ease-out; }
21
- .svg-preview { border: 1px solid #e5e7eb; background-image: url('data:image/svg+xml;utf8,<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10" fill="%23f3f4f6"/><rect x="10" y="10" width="10" height="10" fill="%23f3f4f6"/></svg>'); background-size: 20px 20px; }
 
 
 
 
22
  .tooltip { position: relative; display: inline-block; }
23
- .tooltip .tooltip-text { visibility: hidden; width: 200px; background-color: #333; color: #fff; text-align: center; border-radius: 6px; padding: 5px; position: absolute; z-index: 50; bottom: 125%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; font-size: 0.75rem; }
24
  .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; }
25
  /* Loading spinner */
26
  .loader { border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid #3498db; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
27
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
28
- /* Ensure SVG scales correctly */
29
- #svg-output svg { display: block; width: 100%; height: 100%; }
 
 
 
 
 
30
  </style>
31
  </head>
32
  <body class="bg-gray-50 min-h-screen">
33
  <div class="container mx-auto px-4 py-8">
34
  <div class="text-center mb-8">
35
  <h1 class="text-3xl font-bold text-gray-800 mb-2">Advanced Photo to SVG Converter</h1>
36
- <p class="text-gray-600">Transform your photos into high-quality, realistic SVG vector graphics</p>
37
  </div>
38
 
39
  <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
@@ -74,7 +84,7 @@
74
  Color Count
75
  <span class="tooltip">
76
  <i class="fas fa-info-circle text-gray-400"></i>
77
- <span class="tooltip-text">Number of colors in the final SVG. Fewer colors = smaller file, more abstract. More colors = larger file, more detail.</span>
78
  </span>
79
  </label>
80
  <select id="color-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
@@ -88,10 +98,10 @@
88
 
89
  <div>
90
  <label for="detail-slider" class="block text-sm text-gray-600 mb-1 flex justify-between items-center">
91
- Detail Level (Turd Size) <span id="detail-value" class="font-mono text-xs"></span>
92
  <span class="tooltip">
93
  <i class="fas fa-info-circle text-gray-400"></i>
94
- <span class="tooltip-text">Controls smoothness vs detail. Higher values remove smaller 'speckles' (turds), potentially smoothing edges. Lower values keep more fine detail but can look noisy. Potrace 'turdsize' parameter.</span>
95
  </span>
96
  </label>
97
  <input id="detail-slider" type="range" min="0" max="10" value="2" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
@@ -102,14 +112,10 @@
102
  <label for="smooth-checkbox" class="ml-2 block text-sm text-gray-700">Optimize curves (slower, smoother)</label>
103
  <span class="tooltip ml-2">
104
  <i class="fas fa-info-circle text-gray-400"></i>
105
- <span class="tooltip-text">Apply curve optimization (Potrace 'opticurve'). Makes curves smoother but increases processing time.</span>
106
  </span>
107
  </div>
108
 
109
- <!-- Removed Edge Threshold as it's less relevant for multi-color layered tracing -->
110
- <!-- Removed Quality/Optimize settings as potrace-wasm handles this differently -->
111
- <!-- Removed Background setting for simplicity, Potrace handles background -->
112
-
113
  </div>
114
  </div>
115
 
@@ -132,7 +138,7 @@
132
  <div class="space-y-4">
133
  <h2 class="text-xl font-semibold text-gray-800">3. Preview & Download</h2>
134
 
135
- <div id="preview-placeholder" class="bg-gray-100 rounded-lg flex items-center justify-center text-center p-4" style="min-height: 300px;">
136
  <p class="text-gray-500">Upload an image and click Convert<br>to see the preview here.</p>
137
  </div>
138
 
@@ -142,24 +148,30 @@
142
  <p class="text-xs text-gray-500">(This can take a while for large images or many colors)</p>
143
  </div>
144
 
145
- <div id="comparison-container" class="canvas-container hidden relative bg-gray-100 rounded-lg overflow-hidden" style="height: 300px; aspect-ratio: 4/3;">
146
- <img id="original-image" class="absolute top-0 left-0 w-full h-full object-contain" src="" alt="Original">
 
 
 
 
 
147
  <div class="comparison-slider absolute top-0 left-0 w-1/2 h-full overflow-hidden">
148
- <div id="svg-output" class="absolute top-0 left-0 w-full h-full bg-white svg-preview">
149
- <!-- SVG will be loaded here -->
150
  </div>
151
  <div class="slider-handle"></div>
152
  </div>
153
  </div>
154
 
 
155
  <div id="download-section" class="hidden bg-blue-50 p-4 rounded-lg">
156
  <h4 class="text-sm font-medium text-blue-800 mb-2">Conversion Complete!</h4>
157
  <div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
158
- <button id="download-svg" class="flex-1 py-2 bg-white border border-blue-500 text-blue-600 rounded-md hover:bg-blue-50 transition flex items-center justify-center space-x-2">
159
  <i class="fas fa-download"></i>
160
  <span>Download SVG</span>
161
  </button>
162
- <button id="copy-svg" class="flex-1 py-2 bg-white border border-blue-500 text-blue-600 rounded-md hover:bg-blue-50 transition flex items-center justify-center space-x-2">
163
  <i class="far fa-copy"></i>
164
  <span>Copy SVG Code</span>
165
  </button>
@@ -170,23 +182,23 @@
170
  <div class="grid grid-cols-3 gap-4 text-center">
171
  <div>
172
  <p class="text-xs text-gray-500">Original Size</p>
173
- <p id="original-size" class="font-medium text-sm">-</p>
174
  </div>
175
  <div>
176
  <p class="text-xs text-gray-500">SVG Size</p>
177
- <p id="svg-size" class="font-medium text-sm">-</p>
178
  </div>
179
  <div>
180
  <p class="text-xs text-gray-500">Reduction</p>
181
- <p id="reduction" class="font-medium text-sm">-</p>
182
  </div>
183
  </div>
184
  </div>
185
  <div id="svg-preview-section" class="hidden bg-white border border-gray-200 rounded-lg mt-4">
186
  <div class="p-4">
187
  <h3 class="text-lg font-semibold text-gray-700 mb-2">Generated SVG Code</h3>
188
- <div class="svg-preview rounded-lg overflow-auto p-2 border border-gray-200" style="max-height: 200px;">
189
- <pre id="svg-code" class="text-xs text-gray-800 whitespace-pre-wrap break-all"></pre>
190
  </div>
191
  </div>
192
  </div>
@@ -202,7 +214,7 @@
202
 
203
  <script>
204
  document.addEventListener('DOMContentLoaded', function() {
205
- // DOM elements
206
  const fileInput = document.getElementById('file-input');
207
  const dropzone = document.getElementById('dropzone');
208
  const browseBtn = document.getElementById('browse-btn');
@@ -216,18 +228,18 @@
216
  const progressLabel = document.getElementById('progress-label');
217
  const comparisonContainer = document.getElementById('comparison-container');
218
  const originalImage = document.getElementById('original-image');
219
- const svgOutputContainer = document.getElementById('svg-output'); // Changed to container div
220
  const downloadSection = document.getElementById('download-section');
221
- const downloadSvgBtn = document.getElementById('download-svg'); // Renamed for clarity
222
- const copySvgBtn = document.getElementById('copy-svg'); // Renamed for clarity
223
  const statsSection = document.getElementById('stats-section');
224
- const originalSizeEl = document.getElementById('original-size'); // Renamed for clarity
225
- const svgSizeEl = document.getElementById('svg-size'); // Renamed for clarity
226
- const reductionEl = document.getElementById('reduction'); // Renamed for clarity
227
  const sampleBtn = document.getElementById('sample-btn');
228
  const resetBtn = document.getElementById('reset-btn');
229
  const svgPreviewSection = document.getElementById('svg-preview-section');
230
- const svgCodeEl = document.getElementById('svg-code'); // Renamed for clarity
231
  const detailSlider = document.getElementById('detail-slider');
232
  const detailValue = document.getElementById('detail-value');
233
  const colorSelect = document.getElementById('color-select');
@@ -239,7 +251,7 @@
239
 
240
  // State variables
241
  let originalFile = null;
242
- let originalImageDataUrl = null;
243
  let svgData = null;
244
  let imageWidth = 0;
245
  let imageHeight = 0;
@@ -267,8 +279,8 @@
267
  sliderHandle.addEventListener('mousedown', startSliderDrag);
268
  document.addEventListener('mousemove', dragSlider);
269
  document.addEventListener('mouseup', stopSliderDrag);
270
- sliderHandle.addEventListener('touchstart', startSliderDrag, { passive: false });
271
- document.addEventListener('touchmove', dragSlider, { passive: false });
272
  document.addEventListener('touchend', stopSliderDrag);
273
 
274
 
@@ -278,10 +290,12 @@
278
  const file = e.target.files?.[0];
279
  if (!file) return;
280
  processImageFile(file);
 
 
281
  }
282
 
283
  function handleDragOver(e) {
284
- e.preventDefault();
285
  e.stopPropagation();
286
  dropzone.classList.add('active');
287
  }
@@ -293,57 +307,64 @@
293
  }
294
 
295
  function handleDrop(e) {
296
- e.preventDefault();
297
  e.stopPropagation();
298
  dropzone.classList.remove('active');
299
  const file = e.dataTransfer?.files?.[0];
300
- if (file && file.type.match('image.*')) {
301
  processImageFile(file);
302
  } else {
303
- alert('Please drop a valid image file (PNG, JPG, WebP, BMP).');
304
  }
305
  }
306
 
307
  function processImageFile(file) {
308
  if (!file.type.match('image/(png|jpeg|webp|bmp)')) {
309
  alert('Unsupported file type. Please use PNG, JPG, WebP, or BMP.');
310
- resetInput();
311
  return;
312
  }
313
 
314
  originalFile = file;
315
  const reader = new FileReader();
316
 
 
 
 
 
 
317
  reader.onload = function(e) {
318
- originalImageDataUrl = e.target.result;
319
  const img = new Image();
320
  img.onload = () => {
321
  imageWidth = img.width;
322
  imageHeight = img.height;
 
 
323
  originalImage.src = originalImageDataUrl;
324
- // Set aspect ratio for container (optional, but helps layout)
325
- comparisonContainer.style.aspectRatio = `${imageWidth} / ${imageHeight}`;
326
 
 
327
  fileInfo.textContent = `Selected: ${file.name} (${imageWidth}x${imageHeight})`;
328
  originalSizeEl.textContent = formatFileSize(file.size);
329
- convertBtn.disabled = false;
330
- resetResultsUI(); // Clear previous results if a new image is loaded
331
  previewPlaceholder.classList.add('hidden'); // Hide placeholder
332
- comparisonContainer.classList.remove('hidden'); // Show container with original
333
- comparisonSlider.style.width = '50%'; // Reset slider position
334
- svgOutputContainer.innerHTML = ''; // Clear old SVG output
335
  };
336
  img.onerror = () => {
337
- alert('Could not load image file.');
338
  resetInput();
339
  };
340
- img.src = originalImageDataUrl;
341
  };
342
  reader.onerror = () => {
343
- alert('Could not read file.');
344
  resetInput();
345
  };
346
- reader.readAsDataURL(file);
347
  }
348
 
349
  async function convertToSvg() {
@@ -356,62 +377,54 @@
356
  convertBtn.disabled = true;
357
  loadingIndicator.classList.remove('hidden');
358
  progressContainer.classList.remove('hidden');
 
359
  comparisonContainer.classList.add('hidden'); // Hide comparison during processing
360
  downloadSection.classList.add('hidden');
361
  statsSection.classList.add('hidden');
362
  svgPreviewSection.classList.add('hidden');
363
  updateProgress(0, "Initializing...");
 
364
 
365
  // --- Get Settings ---
366
  const numColors = parseInt(colorSelect.value);
367
- const turdSize = parseInt(detailSlider.value); // Potrace 'turdsize' parameter
368
- const optimizeCurves = smoothCheckbox.checked; // Potrace 'opticurve' parameter
369
 
370
  try {
371
- // --- Load Potrace WASM ---
372
- // Check if already loaded - PotraceWasm loads globally
373
- if (typeof PotraceWasm === 'undefined' || !PotraceWasm.ready) {
374
- updateProgress(5, "Loading converter...");
375
- await PotraceWasm.load(); // Load the WASM module
376
  }
377
 
 
 
378
  updateProgress(10, "Processing image data...");
379
 
380
  // --- Prepare parameters for potrace-wasm ---
381
  const params = {
382
- // General
383
- // background: '#FFFFFF', // potrace-wasm often infers or makes transparent
384
- // color: '#000000', // Not used for posterize
385
- // threshold: 128, // Not used for posterize
386
-
387
- // Posterization (Color Tracing)
388
- posterize: true, // Enable color tracing
389
- steps: numColors, // Number of color layers
390
- // stepFunction: PotraceWasm.STEP_QUANTIZE, // default is QUANTIZE
391
-
392
- // Path decomposition and smoothing
393
- turdPolicy: PotraceWasm.TURD_SMOOTH, // Or MINORITY, ZERO etc.
394
- turdSize: turdSize, // Suppress specks smaller than this size (pixels)
395
- alphaMax: optimizeCurves ? 1.0 : 0, // Adjusts smoothness (corner threshold) - 1.0 is smoother
396
- optCurve: optimizeCurves, // Enable/disable curve optimization
397
- optTolerance: optimizeCurves ? 0.2 : 0, // Optimization tolerance when optcurve is true
398
-
399
- // Turn policy (how to handle corners)
400
- turnPolicy: PotraceWasm.TURN_MINORITY, // Or BLACK, WHITE, LEFT, RIGHT
401
  };
402
 
403
- updateProgress(20, "Tracing image (this may take time)...");
404
 
405
- // --- Perform Conversion using potrace-wasm ---
406
- // PotraceWasm.trace accepts ImageData, Canvas, Image, or URL
407
  const result = await PotraceWasm.trace(originalImageDataUrl, params);
408
 
409
- updateProgress(90, "Generating SVG...");
410
-
411
- svgData = result; // The result is the SVG string directly
412
 
413
- // Basic cleanup (optional, potrace-wasm output is often clean)
414
- svgData = svgData.replace(/<!--[\s\S]*?-->/g, ''); // Remove comments
415
 
416
  // --- Display Results ---
417
  displayResults(svgData);
@@ -419,31 +432,43 @@
419
 
420
  } catch (error) {
421
  console.error('SVG Conversion Error:', error);
422
- alert(`Conversion failed: ${error.message || error}`);
 
423
  updateProgress(0, "Error");
 
 
 
 
424
  } finally {
425
  // --- UI Updates: End Conversion ---
426
- convertBtn.disabled = false;
 
427
  loadingIndicator.classList.add('hidden');
428
- setTimeout(() => { // Hide progress bar after a short delay
429
- progressContainer.classList.add('hidden');
430
- }, 1000);
 
 
 
431
  }
432
  }
433
 
 
434
  function displayResults(generatedSvgData) {
435
- // Set SVG content in the preview container
436
  svgOutputContainer.innerHTML = generatedSvgData;
437
 
438
- // Ensure comparison view is visible
439
  comparisonContainer.classList.remove('hidden');
440
  previewPlaceholder.classList.add('hidden');
441
  loadingIndicator.classList.add('hidden');
 
442
 
443
- // Show download options
444
  downloadSection.classList.remove('hidden');
 
445
 
446
- // Calculate and show stats
447
  const svgSizeBytes = new Blob([generatedSvgData]).size;
448
  svgSizeEl.textContent = formatFileSize(svgSizeBytes);
449
 
@@ -453,10 +478,9 @@
453
  } else {
454
  reductionEl.textContent = '-';
455
  }
456
- statsSection.classList.remove('hidden');
457
 
458
- // Show SVG code preview
459
- svgCodeEl.textContent = generatedSvgData; // Use textContent for security
460
  svgPreviewSection.classList.remove('hidden');
461
  }
462
 
@@ -468,42 +492,65 @@
468
  }
469
 
470
  function downloadSvgFile() {
471
- if (!svgData) return;
472
- const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
473
- const url = URL.createObjectURL(blob);
474
- const a = document.createElement('a');
475
- a.href = url;
476
- a.download = (originalFile?.name || 'image').replace(/\.[^/.]+$/, '') + '.svg';
477
- document.body.appendChild(a);
478
- a.click();
479
- document.body.removeChild(a);
480
- URL.revokeObjectURL(url);
 
 
 
 
 
 
 
 
481
  }
482
 
483
  function copySvgToClipboard() {
484
- if (!svgData) return;
 
 
 
485
  navigator.clipboard.writeText(svgData)
486
  .then(() => {
487
  const originalText = copySvgBtn.querySelector('span').textContent;
 
 
 
488
  copySvgBtn.querySelector('span').textContent = 'Copied!';
 
489
  copySvgBtn.classList.add('bg-green-100');
 
490
  setTimeout(() => {
491
  copySvgBtn.querySelector('span').textContent = originalText;
 
492
  copySvgBtn.classList.remove('bg-green-100');
493
  }, 2000);
494
  })
495
  .catch(err => {
496
  console.error('Failed to copy SVG: ', err);
497
- alert('Failed to copy SVG to clipboard. You might need to grant permission.');
498
  });
499
  }
500
 
501
  function loadSampleImage() {
502
  resetConverter(); // Reset first
503
- const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Smaller sample for faster processing
504
- loadingIndicator.classList.remove('hidden'); // Show temporary loading
 
 
 
 
505
  previewPlaceholder.classList.add('hidden');
 
506
  fileInfo.textContent = "Loading sample image...";
 
507
 
508
  fetch(sampleImageUrl)
509
  .then(response => {
@@ -511,88 +558,96 @@
511
  return response.blob();
512
  })
513
  .then(blob => {
514
- // Ensure blob type is correct, default to jpeg if necessary
515
  let imageType = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/jpeg';
516
- const fileName = 'sample-image.' + (imageType.split('/')[1] || 'jpg');
 
517
  const file = new File([blob], fileName, { type: imageType });
518
- processImageFile(file);
519
  })
520
  .catch(error => {
521
  console.error('Error loading sample image:', error);
522
- alert('Failed to load sample image. Please check the URL or try uploading manually.');
523
  resetConverter(); // Reset fully on error
524
  })
525
  .finally(() => {
526
- loadingIndicator.classList.add('hidden'); // Hide temp loading
527
  });
528
  }
529
 
 
530
  function resetInput() {
531
- fileInput.value = '';
532
  originalFile = null;
533
  originalImageDataUrl = null;
534
  imageWidth = 0;
535
  imageHeight = 0;
536
- convertBtn.disabled = true;
537
- fileInfo.textContent = '';
 
538
  }
539
 
540
  function resetResultsUI() {
541
  svgData = null;
542
- svgOutputContainer.innerHTML = '';
543
- comparisonContainer.classList.add('hidden');
544
- previewPlaceholder.classList.remove('hidden');
545
  downloadSection.classList.add('hidden');
546
  statsSection.classList.add('hidden');
547
  svgPreviewSection.classList.add('hidden');
548
- originalSizeEl.textContent = '-';
549
  svgSizeEl.textContent = '-';
550
  reductionEl.textContent = '-';
551
- svgCodeEl.textContent = '';
552
- progressContainer.classList.add('hidden');
553
- updateProgress(0);
 
554
  }
555
 
556
  function resetConverter() {
557
- resetInput();
558
- resetResultsUI();
559
 
560
- // Reset form values to default
561
  detailSlider.value = 2;
562
  detailValue.textContent = '2';
563
- colorSelect.value = '16';
564
  smoothCheckbox.checked = true;
565
 
566
  console.log('Converter Reset');
567
  }
568
 
 
569
  function formatFileSize(bytes) {
570
- if (bytes < 0 || typeof bytes !== 'number') return 'N/A';
571
  if (bytes === 0) return '0 Bytes';
572
  const k = 1024;
573
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
574
- const i = Math.floor(Math.log(bytes) / Math.log(k));
575
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
576
  }
577
 
578
  // --- Comparison Slider Functions ---
579
  function startSliderDrag(e) {
580
- e.preventDefault(); // Prevent text selection, etc.
 
581
  isDraggingSlider = true;
582
- comparisonContainer.style.cursor = 'ew-resize'; // Indicate dragging
583
  }
584
 
585
  function dragSlider(e) {
586
  if (!isDraggingSlider) return;
587
- e.preventDefault();
 
588
 
589
  const rect = comparisonContainer.getBoundingClientRect();
590
- // Handle both mouse and touch events
591
  const clientX = e.clientX ?? e.touches?.[0]?.clientX;
592
- if (typeof clientX === 'undefined') return; // Exit if no coordinate
593
 
594
  let offsetX = clientX - rect.left;
595
- let newWidth = Math.max(0, Math.min(rect.width, offsetX)); // Clamp between 0 and container width
 
596
  let percentWidth = (newWidth / rect.width) * 100;
597
 
598
  comparisonSlider.style.width = `${percentWidth}%`;
@@ -605,7 +660,7 @@
605
  }
606
  }
607
 
608
- });
609
  </script>
610
  </body>
611
  </html>
 
6
  <title>Advanced Photo to SVG Converter</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <!-- Use the Potrace library that supports color tracing directly -->
9
+ <!-- Added defer attribute -->
10
+ <script src="https://unpkg.com/potrace-wasm@0.3.0/dist/potrace-wasm.js" defer></script>
11
+ <!-- Quantize is less critical now but kept for potential future use/reference -->
12
+ <!-- <script src="https://cdn.jsdelivr.net/npm/quantize@1.0.7/dist/quantize.min.js" defer></script> -->
13
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
14
  <style>
15
  .dropzone { border: 3px dashed rgba(59, 130, 246, 0.5); transition: all 0.3s ease; }
 
19
  .comparison-slider::-webkit-resizer { display: none; /* Hide default resizer */ }
20
  .slider-handle { position: absolute; right: -6px; /* Center handle over border */ top: 50%; transform: translateY(-50%); width: 10px; height: 40px; background-color: rgba(59, 130, 246, 0.8); border-radius: 3px; cursor: ew-resize; z-index: 10; border: 1px solid rgba(255, 255, 255, 0.5); }
21
  .progress-bar { height: 5px; transition: width 0.1s ease-out; }
22
+ .svg-preview-bg { /* Renamed from .svg-preview to avoid conflict */
23
+ border: 1px solid #e5e7eb;
24
+ background-image: url('data:image/svg+xml;utf8,<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10" fill="%23f3f4f6"/><rect x="10" y="10" width="10" height="10" fill="%23f3f4f6"/></svg>');
25
+ background-size: 20px 20px;
26
+ }
27
  .tooltip { position: relative; display: inline-block; }
28
+ .tooltip .tooltip-text { visibility: hidden; width: 200px; background-color: #333; color: #fff; text-align: center; border-radius: 6px; padding: 5px; position: absolute; z-index: 50; bottom: 125%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; font-size: 0.75rem; line-height: 1.2; }
29
  .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; }
30
  /* Loading spinner */
31
  .loader { border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid #3498db; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
32
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
33
+ /* Ensure SVG scales correctly within its container */
34
+ #svg-output svg { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; /* Maintain aspect ratio */ margin: auto; /* Center if smaller than container */ }
35
+ #original-image { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; margin: auto; }
36
+
37
+ /* Style for the SVG code preview */
38
+ #svg-code-container { background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.375rem; max-height: 200px; overflow: auto; padding: 0.5rem; }
39
+ #svg-code { font-family: monospace; font-size: 0.75rem; color: #1f2937; white-space: pre-wrap; word-break: break-all; }
40
  </style>
41
  </head>
42
  <body class="bg-gray-50 min-h-screen">
43
  <div class="container mx-auto px-4 py-8">
44
  <div class="text-center mb-8">
45
  <h1 class="text-3xl font-bold text-gray-800 mb-2">Advanced Photo to SVG Converter</h1>
46
+ <p class="text-gray-600">Transform photos into multi-color vector graphics using Potrace</p>
47
  </div>
48
 
49
  <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
 
84
  Color Count
85
  <span class="tooltip">
86
  <i class="fas fa-info-circle text-gray-400"></i>
87
+ <span class="tooltip-text">Number of colors in the final SVG. Fewer colors = smaller file, more abstract. More colors = larger file, more detail. (Potrace 'steps')</span>
88
  </span>
89
  </label>
90
  <select id="color-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
 
98
 
99
  <div>
100
  <label for="detail-slider" class="block text-sm text-gray-600 mb-1 flex justify-between items-center">
101
+ Detail Preservation <span id="detail-value" class="font-mono text-xs bg-gray-200 px-1 rounded"></span>
102
  <span class="tooltip">
103
  <i class="fas fa-info-circle text-gray-400"></i>
104
+ <span class="tooltip-text">Controls removal of small speckles ('turds'). 0 keeps all details (can be noisy). Higher values remove larger speckles, smoothing edges but losing fine detail. (Potrace 'turdsize')</span>
105
  </span>
106
  </label>
107
  <input id="detail-slider" type="range" min="0" max="10" value="2" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
 
112
  <label for="smooth-checkbox" class="ml-2 block text-sm text-gray-700">Optimize curves (slower, smoother)</label>
113
  <span class="tooltip ml-2">
114
  <i class="fas fa-info-circle text-gray-400"></i>
115
+ <span class="tooltip-text">Apply curve optimization (Potrace 'opticurve'). Makes curves smoother but increases processing time. Recommended for less jagged results.</span>
116
  </span>
117
  </div>
118
 
 
 
 
 
119
  </div>
120
  </div>
121
 
 
138
  <div class="space-y-4">
139
  <h2 class="text-xl font-semibold text-gray-800">3. Preview & Download</h2>
140
 
141
+ <div id="preview-placeholder" class="svg-preview-bg rounded-lg flex items-center justify-center text-center p-4" style="min-height: 300px; height: 300px;"> <!-- Set explicit height -->
142
  <p class="text-gray-500">Upload an image and click Convert<br>to see the preview here.</p>
143
  </div>
144
 
 
148
  <p class="text-xs text-gray-500">(This can take a while for large images or many colors)</p>
149
  </div>
150
 
151
+ <!-- Container for both Original and SVG -->
152
+ <div id="comparison-container" class="canvas-container hidden relative bg-gray-100 rounded-lg overflow-hidden svg-preview-bg" style="height: 300px;">
153
+ <!-- Original Image Layer -->
154
+ <div class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
155
+ <img id="original-image" src="" alt="Original">
156
+ </div>
157
+ <!-- SVG Layer with Slider -->
158
  <div class="comparison-slider absolute top-0 left-0 w-1/2 h-full overflow-hidden">
159
+ <div id="svg-output" class="absolute top-0 left-0 w-full h-full bg-white flex items-center justify-center">
160
+ <!-- SVG content will be injected here -->
161
  </div>
162
  <div class="slider-handle"></div>
163
  </div>
164
  </div>
165
 
166
+
167
  <div id="download-section" class="hidden bg-blue-50 p-4 rounded-lg">
168
  <h4 class="text-sm font-medium text-blue-800 mb-2">Conversion Complete!</h4>
169
  <div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
170
+ <button id="download-svg-btn" class="flex-1 py-2 bg-white border border-blue-500 text-blue-600 rounded-md hover:bg-blue-50 transition flex items-center justify-center space-x-2">
171
  <i class="fas fa-download"></i>
172
  <span>Download SVG</span>
173
  </button>
174
+ <button id="copy-svg-btn" class="flex-1 py-2 bg-white border border-blue-500 text-blue-600 rounded-md hover:bg-blue-50 transition flex items-center justify-center space-x-2">
175
  <i class="far fa-copy"></i>
176
  <span>Copy SVG Code</span>
177
  </button>
 
182
  <div class="grid grid-cols-3 gap-4 text-center">
183
  <div>
184
  <p class="text-xs text-gray-500">Original Size</p>
185
+ <p id="original-size-el" class="font-medium text-sm">-</p>
186
  </div>
187
  <div>
188
  <p class="text-xs text-gray-500">SVG Size</p>
189
+ <p id="svg-size-el" class="font-medium text-sm">-</p>
190
  </div>
191
  <div>
192
  <p class="text-xs text-gray-500">Reduction</p>
193
+ <p id="reduction-el" class="font-medium text-sm">-</p>
194
  </div>
195
  </div>
196
  </div>
197
  <div id="svg-preview-section" class="hidden bg-white border border-gray-200 rounded-lg mt-4">
198
  <div class="p-4">
199
  <h3 class="text-lg font-semibold text-gray-700 mb-2">Generated SVG Code</h3>
200
+ <div id="svg-code-container">
201
+ <pre id="svg-code-el"></pre> <!-- Use pre for formatting -->
202
  </div>
203
  </div>
204
  </div>
 
214
 
215
  <script>
216
  document.addEventListener('DOMContentLoaded', function() {
217
+ // DOM elements (using more descriptive names)
218
  const fileInput = document.getElementById('file-input');
219
  const dropzone = document.getElementById('dropzone');
220
  const browseBtn = document.getElementById('browse-btn');
 
228
  const progressLabel = document.getElementById('progress-label');
229
  const comparisonContainer = document.getElementById('comparison-container');
230
  const originalImage = document.getElementById('original-image');
231
+ const svgOutputContainer = document.getElementById('svg-output');
232
  const downloadSection = document.getElementById('download-section');
233
+ const downloadSvgBtn = document.getElementById('download-svg-btn');
234
+ const copySvgBtn = document.getElementById('copy-svg-btn');
235
  const statsSection = document.getElementById('stats-section');
236
+ const originalSizeEl = document.getElementById('original-size-el');
237
+ const svgSizeEl = document.getElementById('svg-size-el');
238
+ const reductionEl = document.getElementById('reduction-el');
239
  const sampleBtn = document.getElementById('sample-btn');
240
  const resetBtn = document.getElementById('reset-btn');
241
  const svgPreviewSection = document.getElementById('svg-preview-section');
242
+ const svgCodeEl = document.getElementById('svg-code-el');
243
  const detailSlider = document.getElementById('detail-slider');
244
  const detailValue = document.getElementById('detail-value');
245
  const colorSelect = document.getElementById('color-select');
 
251
 
252
  // State variables
253
  let originalFile = null;
254
+ let originalImageDataUrl = null; // Store the Data URL for Potrace
255
  let svgData = null;
256
  let imageWidth = 0;
257
  let imageHeight = 0;
 
279
  sliderHandle.addEventListener('mousedown', startSliderDrag);
280
  document.addEventListener('mousemove', dragSlider);
281
  document.addEventListener('mouseup', stopSliderDrag);
282
+ sliderHandle.addEventListener('touchstart', startSliderDrag, { passive: false }); // Use non-passive for preventDefault
283
+ document.addEventListener('touchmove', dragSlider, { passive: false }); // Use non-passive for preventDefault
284
  document.addEventListener('touchend', stopSliderDrag);
285
 
286
 
 
290
  const file = e.target.files?.[0];
291
  if (!file) return;
292
  processImageFile(file);
293
+ // Reset input value so selecting the same file again triggers 'change'
294
+ e.target.value = null;
295
  }
296
 
297
  function handleDragOver(e) {
298
+ e.preventDefault(); // Necessary to allow drop
299
  e.stopPropagation();
300
  dropzone.classList.add('active');
301
  }
 
307
  }
308
 
309
  function handleDrop(e) {
310
+ e.preventDefault(); // Prevent default browser behavior (opening file)
311
  e.stopPropagation();
312
  dropzone.classList.remove('active');
313
  const file = e.dataTransfer?.files?.[0];
314
+ if (file) {
315
  processImageFile(file);
316
  } else {
317
+ console.warn("No file found in drop event.");
318
  }
319
  }
320
 
321
  function processImageFile(file) {
322
  if (!file.type.match('image/(png|jpeg|webp|bmp)')) {
323
  alert('Unsupported file type. Please use PNG, JPG, WebP, or BMP.');
324
+ resetInput(); // Clear any invalid selection
325
  return;
326
  }
327
 
328
  originalFile = file;
329
  const reader = new FileReader();
330
 
331
+ // Show temporary loading state while reading file
332
+ fileInfo.textContent = `Loading ${file.name}...`;
333
+ convertBtn.disabled = true;
334
+ resetResultsUI(); // Clear previous results immediately
335
+
336
  reader.onload = function(e) {
337
+ originalImageDataUrl = e.target.result; // Store Data URL
338
  const img = new Image();
339
  img.onload = () => {
340
  imageWidth = img.width;
341
  imageHeight = img.height;
342
+
343
+ // Set original image source for comparison view
344
  originalImage.src = originalImageDataUrl;
345
+ // Adjust container height dynamically or keep fixed? Keeping fixed for now.
346
+ // comparisonContainer.style.height = `${Math.min(300, imageHeight)}px`; // Example dynamic height adjustment
347
 
348
+ // Update UI
349
  fileInfo.textContent = `Selected: ${file.name} (${imageWidth}x${imageHeight})`;
350
  originalSizeEl.textContent = formatFileSize(file.size);
351
+ convertBtn.disabled = false; // Enable conversion
 
352
  previewPlaceholder.classList.add('hidden'); // Hide placeholder
353
+ comparisonContainer.classList.remove('hidden'); // Show initial comparison view (original only)
354
+ comparisonSlider.style.width = '0%'; // Start slider showing only original
355
+ svgOutputContainer.innerHTML = ''; // Clear any old SVG output visually
356
  };
357
  img.onerror = () => {
358
+ alert('Could not load image dimensions. The file might be corrupted.');
359
  resetInput();
360
  };
361
+ img.src = originalImageDataUrl; // Load image to get dimensions
362
  };
363
  reader.onerror = () => {
364
+ alert('Error reading file.');
365
  resetInput();
366
  };
367
+ reader.readAsDataURL(file); // Read file as Data URL
368
  }
369
 
370
  async function convertToSvg() {
 
377
  convertBtn.disabled = true;
378
  loadingIndicator.classList.remove('hidden');
379
  progressContainer.classList.remove('hidden');
380
+ previewPlaceholder.classList.add('hidden'); // Ensure placeholder is hidden
381
  comparisonContainer.classList.add('hidden'); // Hide comparison during processing
382
  downloadSection.classList.add('hidden');
383
  statsSection.classList.add('hidden');
384
  svgPreviewSection.classList.add('hidden');
385
  updateProgress(0, "Initializing...");
386
+ svgData = null; // Reset SVG data state
387
 
388
  // --- Get Settings ---
389
  const numColors = parseInt(colorSelect.value);
390
+ const turdSize = parseInt(detailSlider.value);
391
+ const optimizeCurves = smoothCheckbox.checked;
392
 
393
  try {
394
+ // --- Check & Load Potrace WASM ---
395
+ if (typeof PotraceWasm === 'undefined') {
396
+ throw new Error("PotraceWasm library failed to load. Check browser console and network connection.");
 
 
397
  }
398
 
399
+ updateProgress(5, "Loading converter module...");
400
+ await PotraceWasm.load(); // Ensures WASM is ready
401
  updateProgress(10, "Processing image data...");
402
 
403
  // --- Prepare parameters for potrace-wasm ---
404
  const params = {
405
+ posterize: true,
406
+ steps: numColors,
407
+ turdPolicy: PotraceWasm.TURD_SMOOTH, // Common policy for smoothing speckles
408
+ turdSize: turdSize, // Pixels: 0 keeps everything, >0 removes smaller areas
409
+ alphaMax: optimizeCurves ? 1.0 : 0, // Corner smoothing threshold (0 = sharp corners)
410
+ optCurve: optimizeCurves, // Enable Bezier curve optimization
411
+ optTolerance: optimizeCurves ? 0.2 : 0, // How much curve optimization can deviate
412
+ turnPolicy: PotraceWasm.TURN_MINORITY, // How to resolve ambiguities at path turns
413
+ // background: '#ffffff', // Optional: Set explicit background (usually transparent is fine)
414
+ // fillStrategy: PotraceWasm.FILL_REMOVE_LAST, // Experiment if needed
415
+ // rangeDistribution: PotraceWasm.RANGE_AUTO, // Experiment if needed
 
 
 
 
 
 
 
 
416
  };
417
 
418
+ updateProgress(20, `Tracing ${numColors} colors (turdSize=${turdSize})...`);
419
 
420
+ // --- Perform Conversion ---
421
+ // Use the stored Data URL
422
  const result = await PotraceWasm.trace(originalImageDataUrl, params);
423
 
424
+ updateProgress(90, "Cleaning & preparing SVG...");
 
 
425
 
426
+ // Store and slightly clean the SVG data
427
+ svgData = result.replace(/<!--[\s\S]*?-->/g, '').trim(); // Remove comments and trim whitespace
428
 
429
  // --- Display Results ---
430
  displayResults(svgData);
 
432
 
433
  } catch (error) {
434
  console.error('SVG Conversion Error:', error);
435
+ const errorMsg = (error instanceof Error) ? error.message : String(error);
436
+ alert(`Conversion failed: ${errorMsg}`);
437
  updateProgress(0, "Error");
438
+ // Ensure loading/progress indicators reflect the error state
439
+ loadingIndicator.classList.add('hidden');
440
+ progressContainer.classList.remove('hidden'); // Keep progress bar visible showing error state
441
+ previewPlaceholder.classList.remove('hidden'); // Show placeholder again on error
442
  } finally {
443
  // --- UI Updates: End Conversion ---
444
+ // Re-enable button only if an image is still loaded
445
+ convertBtn.disabled = !originalFile;
446
  loadingIndicator.classList.add('hidden');
447
+ // Hide progress bar only on success after a delay
448
+ if (svgData) {
449
+ setTimeout(() => {
450
+ progressContainer.classList.add('hidden');
451
+ }, 1500);
452
+ }
453
  }
454
  }
455
 
456
+
457
  function displayResults(generatedSvgData) {
458
+ // Inject SVG content into the output container
459
  svgOutputContainer.innerHTML = generatedSvgData;
460
 
461
+ // Make the comparison view visible and hide placeholders/loaders
462
  comparisonContainer.classList.remove('hidden');
463
  previewPlaceholder.classList.add('hidden');
464
  loadingIndicator.classList.add('hidden');
465
+ comparisonSlider.style.width = '50%'; // Reset slider to midpoint
466
 
467
+ // Show download & stats sections
468
  downloadSection.classList.remove('hidden');
469
+ statsSection.classList.remove('hidden');
470
 
471
+ // Calculate and display stats
472
  const svgSizeBytes = new Blob([generatedSvgData]).size;
473
  svgSizeEl.textContent = formatFileSize(svgSizeBytes);
474
 
 
478
  } else {
479
  reductionEl.textContent = '-';
480
  }
 
481
 
482
+ // Display SVG code preview
483
+ svgCodeEl.textContent = generatedSvgData; // Use textContent for security in <pre>
484
  svgPreviewSection.classList.remove('hidden');
485
  }
486
 
 
492
  }
493
 
494
  function downloadSvgFile() {
495
+ if (!svgData) {
496
+ alert("No SVG data available to download.");
497
+ return;
498
+ }
499
+ try {
500
+ const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
501
+ const url = URL.createObjectURL(blob);
502
+ const a = document.createElement('a');
503
+ a.href = url;
504
+ a.download = (originalFile?.name || 'converted-image').replace(/\.[^/.]+$/, '') + '.svg';
505
+ document.body.appendChild(a); // Required for Firefox
506
+ a.click();
507
+ document.body.removeChild(a);
508
+ URL.revokeObjectURL(url);
509
+ } catch (error) {
510
+ console.error("Download failed:", error);
511
+ alert("Could not initiate download. Please try copying the code.");
512
+ }
513
  }
514
 
515
  function copySvgToClipboard() {
516
+ if (!svgData) {
517
+ alert("No SVG data available to copy.");
518
+ return;
519
+ }
520
  navigator.clipboard.writeText(svgData)
521
  .then(() => {
522
  const originalText = copySvgBtn.querySelector('span').textContent;
523
+ const icon = copySvgBtn.querySelector('i');
524
+ const originalIconClass = icon.className;
525
+
526
  copySvgBtn.querySelector('span').textContent = 'Copied!';
527
+ icon.className = 'fas fa-check text-green-500'; // Change icon to checkmark
528
  copySvgBtn.classList.add('bg-green-100');
529
+
530
  setTimeout(() => {
531
  copySvgBtn.querySelector('span').textContent = originalText;
532
+ icon.className = originalIconClass; // Restore original icon
533
  copySvgBtn.classList.remove('bg-green-100');
534
  }, 2000);
535
  })
536
  .catch(err => {
537
  console.error('Failed to copy SVG: ', err);
538
+ alert('Failed to copy SVG to clipboard. Your browser might not support this, or permission was denied. You can manually copy from the code preview.');
539
  });
540
  }
541
 
542
  function loadSampleImage() {
543
  resetConverter(); // Reset first
544
+ // Using a smaller image for quicker sample loading/processing by default
545
+ const sampleImageUrl = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80'; // Example: Shoe
546
+ // const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Example: Landscape
547
+
548
+ // Show immediate feedback
549
+ loadingIndicator.classList.remove('hidden');
550
  previewPlaceholder.classList.add('hidden');
551
+ comparisonContainer.classList.add('hidden'); // Hide comparison view during load
552
  fileInfo.textContent = "Loading sample image...";
553
+ convertBtn.disabled = true;
554
 
555
  fetch(sampleImageUrl)
556
  .then(response => {
 
558
  return response.blob();
559
  })
560
  .then(blob => {
561
+ // Guess file extension from mime type if possible
562
  let imageType = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/jpeg';
563
+ const extension = imageType.split('/')[1] || 'jpg';
564
+ const fileName = `sample-image.${extension}`;
565
  const file = new File([blob], fileName, { type: imageType });
566
+ processImageFile(file); // Process the fetched image blob as a file
567
  })
568
  .catch(error => {
569
  console.error('Error loading sample image:', error);
570
+ alert(`Failed to load sample image: ${error.message}. Please try uploading manually.`);
571
  resetConverter(); // Reset fully on error
572
  })
573
  .finally(() => {
574
+ loadingIndicator.classList.add('hidden'); // Hide loading indicator regardless of outcome
575
  });
576
  }
577
 
578
+ // --- Reset Functions ---
579
  function resetInput() {
580
+ fileInput.value = ''; // Clear file input
581
  originalFile = null;
582
  originalImageDataUrl = null;
583
  imageWidth = 0;
584
  imageHeight = 0;
585
+ convertBtn.disabled = true; // Disable convert button
586
+ fileInfo.textContent = ''; // Clear file info text
587
+ originalImage.src = ''; // Clear original image preview
588
  }
589
 
590
  function resetResultsUI() {
591
  svgData = null;
592
+ svgOutputContainer.innerHTML = ''; // Clear SVG preview
593
+ comparisonContainer.classList.add('hidden'); // Hide comparison slider view
594
+ previewPlaceholder.classList.remove('hidden'); // Show the initial placeholder
595
  downloadSection.classList.add('hidden');
596
  statsSection.classList.add('hidden');
597
  svgPreviewSection.classList.add('hidden');
598
+ originalSizeEl.textContent = '-'; // Reset stats
599
  svgSizeEl.textContent = '-';
600
  reductionEl.textContent = '-';
601
+ svgCodeEl.textContent = ''; // Clear SVG code view
602
+ progressContainer.classList.add('hidden'); // Hide progress bar
603
+ updateProgress(0); // Reset progress values
604
+ loadingIndicator.classList.add('hidden'); // Hide loader
605
  }
606
 
607
  function resetConverter() {
608
+ resetInput(); // Clear input-related things
609
+ resetResultsUI(); // Clear output/result related things
610
 
611
+ // Reset form controls to default values
612
  detailSlider.value = 2;
613
  detailValue.textContent = '2';
614
+ colorSelect.value = '16'; // Default color selection
615
  smoothCheckbox.checked = true;
616
 
617
  console.log('Converter Reset');
618
  }
619
 
620
+ // --- Utility Functions ---
621
  function formatFileSize(bytes) {
622
+ if (bytes == null || typeof bytes !== 'number' || bytes < 0) return 'N/A';
623
  if (bytes === 0) return '0 Bytes';
624
  const k = 1024;
625
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
626
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
627
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
628
  }
629
 
630
  // --- Comparison Slider Functions ---
631
  function startSliderDrag(e) {
632
+ // Prevent default only for touch to avoid scrolling page while dragging
633
+ if (e.type === 'touchstart') e.preventDefault();
634
  isDraggingSlider = true;
635
+ comparisonContainer.style.cursor = 'ew-resize';
636
  }
637
 
638
  function dragSlider(e) {
639
  if (!isDraggingSlider) return;
640
+ // Prevent default only for touch to avoid scrolling page while dragging
641
+ if (e.type === 'touchmove') e.preventDefault();
642
 
643
  const rect = comparisonContainer.getBoundingClientRect();
644
+ // Use touch or mouse coordinates
645
  const clientX = e.clientX ?? e.touches?.[0]?.clientX;
646
+ if (typeof clientX === 'undefined') return; // Exit if no coordinate data
647
 
648
  let offsetX = clientX - rect.left;
649
+ // Clamp the offset to be within the container bounds
650
+ let newWidth = Math.max(0, Math.min(rect.width, offsetX));
651
  let percentWidth = (newWidth / rect.width) * 100;
652
 
653
  comparisonSlider.style.width = `${percentWidth}%`;
 
660
  }
661
  }
662
 
663
+ }); // End DOMContentLoaded
664
  </script>
665
  </body>
666
  </html>