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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +422 -495
index.html CHANGED
@@ -5,81 +5,28 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Advanced Photo to SVG Converter</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/potrace.min.js"></script>
 
 
9
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quantize.min.js"></script>
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
  <style>
12
- .dropzone {
13
- border: 3px dashed rgba(59, 130, 246, 0.5);
14
- transition: all 0.3s ease;
15
- }
16
- .dropzone.active {
17
- border-color: rgba(59, 130, 246, 1);
18
- background-color: rgba(59, 130, 246, 0.1);
19
- }
20
- .canvas-container {
21
- position: relative;
22
- overflow: hidden;
23
- }
24
- .comparison-slider {
25
- position: absolute;
26
- top: 0;
27
- left: 0;
28
- width: 50%;
29
- height: 100%;
30
- overflow: hidden;
31
- resize: horizontal;
32
- min-width: 10px;
33
- max-width: calc(100% - 10px);
34
- cursor: ew-resize;
35
- }
36
- .comparison-slider::-webkit-resizer {
37
- display: none;
38
- }
39
- .slider-handle {
40
- position: absolute;
41
- right: -5px;
42
- top: 50%;
43
- transform: translateY(-50%);
44
- width: 10px;
45
- height: 40px;
46
- background-color: rgba(255, 255, 255, 0.8);
47
- border-radius: 5px;
48
- cursor: ew-resize;
49
- z-index: 10;
50
- }
51
- .progress-bar {
52
- height: 5px;
53
- transition: width 0.3s ease;
54
- }
55
- .svg-preview {
56
- border: 1px solid #e5e7eb;
57
- 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>');
58
- background-size: 20px 20px;
59
- }
60
- .tooltip {
61
- position: relative;
62
- }
63
- .tooltip-text {
64
- visibility: hidden;
65
- width: 200px;
66
- background-color: #333;
67
- color: #fff;
68
- text-align: center;
69
- border-radius: 6px;
70
- padding: 5px;
71
- position: absolute;
72
- z-index: 1;
73
- bottom: 125%;
74
- left: 50%;
75
- transform: translateX(-50%);
76
- opacity: 0;
77
- transition: opacity 0.3s;
78
- }
79
- .tooltip:hover .tooltip-text {
80
- visibility: visible;
81
- opacity: 1;
82
- }
83
  </style>
84
  </head>
85
  <body class="bg-gray-50 min-h-screen">
@@ -95,251 +42,241 @@
95
  <!-- Upload Section -->
96
  <div class="space-y-4">
97
  <div class="flex items-center justify-between">
98
- <h2 class="text-xl font-semibold text-gray-800">Upload Image</h2>
99
- <div class="flex space-x-2">
100
- <button id="sample-btn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm hover:bg-blue-200 transition">
101
- <i class="fas fa-image mr-1"></i> Sample
102
- </button>
103
- <button id="reset-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm hover:bg-gray-200 transition">
104
- <i class="fas fa-redo mr-1"></i> Reset
105
- </button>
106
- </div>
107
- </div>
108
 
109
  <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer">
110
  <div class="flex flex-col items-center justify-center space-y-3">
111
  <i class="fas fa-cloud-upload-alt text-4xl text-blue-500"></i>
112
  <h3 class="text-lg font-medium text-gray-700">Drag & drop your photo here</h3>
113
  <p class="text-sm text-gray-500">or click to browse files</p>
114
- <input type="file" id="file-input" class="hidden" accept="image/*">
115
  <button id="browse-btn" class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
116
  Select Image
117
  </button>
118
  </div>
 
119
  </div>
120
 
121
  <div class="bg-gray-50 p-4 rounded-lg">
122
- <h4 class="text-sm font-medium text-gray-700 mb-2">Conversion Settings</h4>
123
  <div class="space-y-4">
124
  <div>
125
- <label class="block text-sm text-gray-600 mb-1">Detail Level</label>
126
- <input id="detail-slider" type="range" min="1" max="10" value="5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
127
- <div class="flex justify-between text-xs text-gray-500 mt-1">
128
- <span>Low</span>
129
- <span>Medium</span>
130
- <span>High</span>
131
- </div>
132
- </div>
133
-
134
- <div>
135
- <label class="block text-sm text-gray-600 mb-1">Color Palette</label>
136
  <select id="color-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
137
- <option value="auto">Auto-detect (recommended)</option>
138
- <option value="2">2 colors</option>
139
- <option value="4">4 colors</option>
140
- <option value="8">8 colors</option>
141
- <option value="16">16 colors</option>
142
  <option value="32">32 colors</option>
143
  <option value="64">64 colors</option>
144
- <option value="128">128 colors</option>
145
- <option value="256">256 colors</option>
146
  </select>
147
  </div>
148
 
149
- <div class="flex items-center space-x-4">
150
- <div class="flex-1">
151
- <label class="block text-sm text-gray-600 mb-1">Edge Threshold</label>
152
- <input id="edge-slider" type="range" min="1" max="100" value="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
153
- </div>
154
- <div class="tooltip">
155
- <i class="fas fa-info-circle text-gray-400"></i>
156
- <span class="tooltip-text">Adjusts how sensitive the converter is to edges in the image</span>
157
- </div>
158
  </div>
159
 
160
  <div class="flex items-center">
161
- <input id="smooth-checkbox" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
162
- <label for="smooth-checkbox" class="ml-2 block text-sm text-gray-700">Smooth curves</label>
 
 
 
 
163
  </div>
 
 
 
 
 
164
  </div>
165
  </div>
166
 
167
- <button id="convert-btn" class="w-full py-3 bg-blue-600 text-white rounded-md font-medium hover:bg-blue-700 transition flex items-center justify-center space-x-2">
168
  <i class="fas fa-magic"></i>
169
  <span>Convert to SVG</span>
170
  </button>
 
 
 
 
 
 
 
 
 
171
  </div>
172
 
173
- <!-- Preview Section -->
174
- <div class="space-y-4">
175
- <h2 class="text-xl font-semibold text-gray-800">Preview</h2>
176
-
177
- <div id="preview-container" class="bg-gray-100 rounded-lg flex items-center justify-center" style="min-height: 300px;">
178
- <p class="text-gray-500">Your SVG preview will appear here</p>
179
- </div>
180
 
181
- <div id="progress-container" class="hidden">
182
- <div class="flex justify-between text-sm text-gray-600 mb-1">
183
- <span>Processing image...</span>
184
- <span id="progress-percent">0%</span>
185
- </div>
186
- <div class="w-full bg-gray-200 rounded-full h-2.5">
187
- <div id="progress-bar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
188
- </div>
189
- </div>
190
 
191
- <div id="comparison-container" class="canvas-container hidden" style="height: 300px;">
192
- <img id="original-image" class="w-full h-full object-contain" src="" alt="Original">
193
- <div class="comparison-slider">
194
- <svg id="svg-output" class="w-full h-full" viewBox=""></svg>
195
- <div class="slider-handle"></div>
196
- </div>
197
- </div>
198
 
199
- <div id="download-section" class="hidden bg-blue-50 p-4 rounded-lg">
200
- <h4 class="text-sm font-medium text-blue-800 mb-2">Conversion Complete!</h4>
201
- <div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
202
- <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">
203
- <i class="fas fa-download"></i>
204
- <span>Download SVG</span>
205
- </button>
206
- <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">
207
- <i class="far fa-copy"></i>
208
- <span>Copy SVG Code</span>
209
- </button>
210
- </div>
211
- </div>
212
 
213
- <div id="stats-section" class="hidden bg-gray-50 p-4 rounded-lg">
214
- <div class="grid grid-cols-3 gap-4 text-center">
215
- <div>
216
- <p class="text-xs text-gray-500">Original Size</p>
217
- <p id="original-size" class="font-medium">-</p>
218
- </div>
219
- <div>
220
- <p class="text-xs text-gray-500">SVG Size</p>
221
- <p id="svg-size" class="font-medium">-</p>
222
- </div>
223
- <div>
224
- <p class="text-xs text-gray-500">Reduction</p>
225
- <p id="reduction" class="font-medium">-</p>
226
- </div>
227
- </div>
228
- </div>
229
- </div>
230
- </div>
231
- </div>
232
- </div>
233
 
234
- <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8 hidden" id="advanced-section">
235
- <div class="p-6">
236
- <h2 class="text-xl font-semibold text-gray-800 mb-4">Advanced SVG Options</h2>
237
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
238
- <div>
239
- <label class="block text-sm text-gray-600 mb-1">Optimization Level</label>
240
- <select id="optimize-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
241
- <option value="0">None</option>
242
- <option value="1" selected>Basic</option>
243
- <option value="2">Aggressive</option>
244
- <option value="3">Maximum</option>
245
- </select>
246
- </div>
247
- <div>
248
- <label class="block text-sm text-gray-600 mb-1">Background</label>
249
- <select id="bg-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
250
- <option value="transparent">Transparent</option>
251
- <option value="white">White</option>
252
- <option value="black">Black</option>
253
- <option value="custom">Custom Color</option>
254
- </select>
255
- <input id="bg-color" type="color" class="hidden mt-2 w-full h-8" value="#ffffff">
256
- </div>
257
- <div>
258
- <label class="block text-sm text-gray-600 mb-1">Output Quality</label>
259
- <select id="quality-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
260
- <option value="low">Low (faster)</option>
261
- <option value="medium" selected>Medium</option>
262
- <option value="high">High</option>
263
- <option value="ultra">Ultra (slowest)</option>
264
- </select>
265
- </div>
266
- </div>
267
- </div>
268
- </div>
269
 
270
- <div class="bg-white rounded-xl shadow-lg overflow-hidden hidden" id="svg-preview-section">
271
- <div class="p-6">
272
- <h2 class="text-xl font-semibold text-gray-800 mb-4">SVG Preview</h2>
273
- <div class="svg-preview rounded-lg overflow-auto p-4" style="max-height: 500px;">
274
- <pre id="svg-code" class="text-xs text-gray-800 bg-white p-2 overflow-auto"></pre>
275
- </div>
276
- </div>
277
- </div>
278
  </div>
279
 
 
 
 
280
  <script>
281
  document.addEventListener('DOMContentLoaded', function() {
282
  // DOM elements
283
  const fileInput = document.getElementById('file-input');
284
  const dropzone = document.getElementById('dropzone');
285
  const browseBtn = document.getElementById('browse-btn');
 
286
  const convertBtn = document.getElementById('convert-btn');
287
- const previewContainer = document.getElementById('preview-container');
 
288
  const progressContainer = document.getElementById('progress-container');
289
  const progressBar = document.getElementById('progress-bar');
290
  const progressPercent = document.getElementById('progress-percent');
 
291
  const comparisonContainer = document.getElementById('comparison-container');
292
  const originalImage = document.getElementById('original-image');
293
- const svgOutput = document.getElementById('svg-output');
294
  const downloadSection = document.getElementById('download-section');
295
- const downloadSvg = document.getElementById('download-svg');
296
- const copySvg = document.getElementById('copy-svg');
297
  const statsSection = document.getElementById('stats-section');
298
- const originalSize = document.getElementById('original-size');
299
- const svgSize = document.getElementById('svg-size');
300
- const reduction = document.getElementById('reduction');
301
  const sampleBtn = document.getElementById('sample-btn');
302
  const resetBtn = document.getElementById('reset-btn');
303
- const advancedSection = document.getElementById('advanced-section');
304
  const svgPreviewSection = document.getElementById('svg-preview-section');
305
- const svgCode = document.getElementById('svg-code');
306
  const detailSlider = document.getElementById('detail-slider');
 
307
  const colorSelect = document.getElementById('color-select');
308
- const edgeSlider = document.getElementById('edge-slider');
309
  const smoothCheckbox = document.getElementById('smooth-checkbox');
310
- const optimizeSelect = document.getElementById('optimize-select');
311
- const bgSelect = document.getElementById('bg-select');
312
- const bgColor = document.getElementById('bg-color');
313
- const qualitySelect = document.getElementById('quality-select');
314
 
315
- // Variables
 
 
 
 
316
  let originalFile = null;
317
- let originalImageData = null;
318
  let svgData = null;
 
 
 
 
 
 
319
 
320
- // Event listeners
321
  browseBtn.addEventListener('click', () => fileInput.click());
322
  fileInput.addEventListener('change', handleFileSelect);
323
- dropzone.addEventListener('dragover', handleDragOver);
324
- dropzone.addEventListener('dragleave', handleDragLeave);
325
- dropzone.addEventListener('drop', handleDrop);
326
  convertBtn.addEventListener('click', convertToSvg);
327
- downloadSvg.addEventListener('click', downloadSvgFile);
328
- copySvg.addEventListener('click', copySvgToClipboard);
329
  sampleBtn.addEventListener('click', loadSampleImage);
330
  resetBtn.addEventListener('click', resetConverter);
331
- bgSelect.addEventListener('change', toggleBgColorPicker);
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
- // Functions
334
  function handleFileSelect(e) {
335
- const file = e.target.files[0] || (e.dataTransfer && e.dataTransfer.files[0]);
336
  if (!file) return;
337
-
338
- if (!file.type.match('image.*')) {
339
- alert('Please select an image file');
340
- return;
341
- }
342
-
343
  processImageFile(file);
344
  }
345
 
@@ -359,326 +296,316 @@
359
  e.preventDefault();
360
  e.stopPropagation();
361
  dropzone.classList.remove('active');
362
-
363
- const file = e.dataTransfer.files[0];
364
- if (!file.type.match('image.*')) {
365
- alert('Please drop an image file');
366
- return;
367
  }
368
-
369
- processImageFile(file);
370
  }
371
 
372
  function processImageFile(file) {
 
 
 
 
 
 
373
  originalFile = file;
374
-
375
- // Display original image
376
  const reader = new FileReader();
 
377
  reader.onload = function(e) {
378
- originalImage.src = e.target.result;
379
-
380
- // Show file info
381
- originalSize.textContent = formatFileSize(file.size);
382
-
383
- // Create Image object to get dimensions
384
  const img = new Image();
385
- img.onload = function() {
386
- previewContainer.innerHTML = `
387
- <div class="text-center">
388
- <img src="${e.target.result}" class="max-h-64 mx-auto" alt="Preview">
389
- <p class="mt-2 text-sm text-gray-600">${img.width} × ${img.height} pixels</p>
390
- </div>
391
- `;
392
-
393
- // Show advanced options
394
- advancedSection.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
395
  };
396
- img.src = e.target.result;
 
 
 
 
397
  };
398
  reader.readAsDataURL(file);
399
  }
400
 
401
- function convertToSvg() {
402
- if (!originalFile) {
403
  alert('Please select an image first');
404
  return;
405
  }
406
 
407
- // Show progress
 
 
408
  progressContainer.classList.remove('hidden');
409
- previewContainer.classList.add('hidden');
410
- comparisonContainer.classList.add('hidden');
411
  downloadSection.classList.add('hidden');
412
  statsSection.classList.add('hidden');
413
  svgPreviewSection.classList.add('hidden');
414
-
415
- // Update progress
416
- updateProgress(10);
417
-
418
- // Get settings
419
- const detailLevel = parseInt(detailSlider.value);
420
- const colorCount = colorSelect.value === 'auto' ? 'auto' : parseInt(colorSelect.value);
421
- const edgeThreshold = parseInt(edgeSlider.value) / 100;
422
- const smoothCurves = smoothCheckbox.checked;
423
- const optimizeLevel = parseInt(optimizeSelect.value);
424
- const quality = qualitySelect.value;
425
-
426
- // Create canvas to process image
427
- const img = new Image();
428
- img.onload = function() {
429
- updateProgress(30);
430
-
431
- const canvas = document.createElement('canvas');
432
- const ctx = canvas.getContext('2d');
433
- canvas.width = img.width;
434
- canvas.height = img.height;
435
-
436
- // Draw image to canvas
437
- ctx.drawImage(img, 0, 0);
438
-
439
- // Get image data
440
- originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
441
-
442
- updateProgress(50);
443
-
444
- // Process image in chunks to avoid blocking UI
445
- setTimeout(() => {
446
- // Apply color quantization if needed
447
- let quantizedData = originalImageData;
448
- if (colorCount !== 'auto') {
449
- quantizedData = quantizeImage(originalImageData, colorCount);
450
- }
451
-
452
- updateProgress(70);
453
-
454
- // Convert to SVG using Potrace
455
- const potrace = new Potrace();
456
- potrace.setParameters({
457
- threshold: edgeThreshold,
458
- color: '#000000',
459
- background: '#ffffff',
460
- steps: detailLevel * 10,
461
- turdSize: Math.max(1, 10 - detailLevel),
462
- alphaMax: smoothCurves ? 1 : 0,
463
- optCurve: smoothCurves,
464
- optTolerance: smoothCurves ? 0.2 : 0
465
- });
466
-
467
- potrace.loadImageData(quantizedData);
468
-
469
- updateProgress(90);
470
-
471
- // Get SVG output
472
- svgData = potrace.getSVG();
473
-
474
- // Optimize SVG
475
- svgData = optimizeSvg(svgData, optimizeLevel);
476
-
477
- // Set viewBox
478
- const viewBox = `0 0 ${canvas.width} ${canvas.height}`;
479
- svgOutput.setAttribute('viewBox', viewBox);
480
- svgOutput.innerHTML = svgData;
481
-
482
- // Display results
483
- displayResults(svgData, canvas.width, canvas.height);
484
-
485
- updateProgress(100);
486
-
487
- // Hide progress after a delay
488
- setTimeout(() => {
489
- progressContainer.classList.add('hidden');
490
- }, 500);
491
- }, 100);
492
- };
493
-
494
- img.src = URL.createObjectURL(originalFile);
495
- }
496
-
497
- function quantizeImage(imageData, colorCount) {
498
- const pixels = imageData.data;
499
- const pixelCount = imageData.width * imageData.height;
500
- const pixelArray = [];
501
-
502
- // Convert image data to array of pixels
503
- for (let i = 0; i < pixelCount; i++) {
504
- const offset = i * 4;
505
- pixelArray.push([
506
- pixels[offset],
507
- pixels[offset + 1],
508
- pixels[offset + 2],
509
- pixels[offset + 3]
510
- ]);
511
- }
512
-
513
- // Quantize colors
514
- const cmap = MMCQ.quantize(pixelArray, colorCount);
515
- const palette = cmap.palette();
516
-
517
- // Create new image data with quantized colors
518
- const newImageData = new ImageData(imageData.width, imageData.height);
519
- const newPixels = newImageData.data;
520
-
521
- for (let i = 0; i < pixelCount; i++) {
522
- const offset = i * 4;
523
- const pixel = pixelArray[i];
524
- const color = cmap.map(pixel);
525
-
526
- newPixels[offset] = color[0];
527
- newPixels[offset + 1] = color[1];
528
- newPixels[offset + 2] = color[2];
529
- newPixels[offset + 3] = color[3] || 255;
530
- }
531
-
532
- return newImageData;
533
- }
534
-
535
- function optimizeSvg(svgData, level) {
536
- // Basic optimizations (always applied)
537
- svgData = svgData.replace(/\s+/g, ' '); // Remove extra whitespace
538
- svgData = svgData.replace(/>\s+</g, '><'); // Remove spaces between tags
539
-
540
- if (level >= 1) {
541
- // Remove unnecessary attributes
542
- svgData = svgData.replace(/xmlns:xlink="[^"]*"/g, '');
543
- svgData = svgData.replace(/xlink:href="[^"]*"/g, '');
544
- }
545
-
546
- if (level >= 2) {
547
- // Shorten path commands
548
- svgData = svgData.replace(/ ([mlhvcsqtaz]) /g, '$1');
549
- svgData = svgData.replace(/(\d)-(\d)/g, '$1 -$2'); // Fix negative numbers
550
- }
551
-
552
- if (level >= 3) {
553
- // More aggressive optimizations
554
- svgData = svgData.replace(/<g>\s*<\/g>/g, ''); // Remove empty groups
555
- svgData = svgData.replace(/fill-rule="evenodd"/g, ''); // Remove default fill-rule
556
  }
557
-
558
- return svgData;
559
  }
560
-
561
- function displayResults(svgData, width, height) {
562
- // Show comparison
 
 
 
563
  comparisonContainer.classList.remove('hidden');
564
-
 
 
565
  // Show download options
566
  downloadSection.classList.remove('hidden');
567
-
568
- // Show stats
569
- const svgSizeBytes = new Blob([svgData]).size;
570
- svgSize.textContent = formatFileSize(svgSizeBytes);
571
-
572
- const reductionPercent = ((originalFile.size - svgSizeBytes) / originalFile.size * 100).toFixed(1);
573
- reduction.textContent = `${reductionPercent}%`;
574
-
 
 
 
575
  statsSection.classList.remove('hidden');
576
-
577
  // Show SVG code preview
578
- svgCode.textContent = svgData;
579
  svgPreviewSection.classList.remove('hidden');
580
  }
581
-
582
- function updateProgress(percent) {
583
- progressBar.style.width = `${percent}%`;
584
- progressPercent.textContent = `${percent}%`;
 
 
585
  }
586
-
587
  function downloadSvgFile() {
588
  if (!svgData) return;
589
-
590
- const blob = new Blob([svgData], { type: 'image/svg+xml' });
591
  const url = URL.createObjectURL(blob);
592
-
593
  const a = document.createElement('a');
594
  a.href = url;
595
- a.download = originalFile.name.replace(/\.[^/.]+$/, '') + '.svg';
596
  document.body.appendChild(a);
597
  a.click();
598
  document.body.removeChild(a);
599
  URL.revokeObjectURL(url);
600
  }
601
-
602
  function copySvgToClipboard() {
603
  if (!svgData) return;
604
-
605
  navigator.clipboard.writeText(svgData)
606
  .then(() => {
607
- const originalText = copySvg.querySelector('span').textContent;
608
- copySvg.querySelector('span').textContent = 'Copied!';
609
-
610
  setTimeout(() => {
611
- copySvg.querySelector('span').textContent = originalText;
 
612
  }, 2000);
613
  })
614
  .catch(err => {
615
  console.error('Failed to copy SVG: ', err);
616
- alert('Failed to copy SVG to clipboard');
617
  });
618
  }
619
-
620
- function loadSampleImage() {
621
- // Sample image URL (replace with your own)
622
- const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&h=600&q=80';
623
-
624
- // Fetch the sample image
 
 
625
  fetch(sampleImageUrl)
626
- .then(response => response.blob())
 
 
 
627
  .then(blob => {
628
- const file = new File([blob], 'sample-image.jpg', { type: 'image/jpeg' });
 
 
 
629
  processImageFile(file);
630
  })
631
  .catch(error => {
632
  console.error('Error loading sample image:', error);
633
- alert('Failed to load sample image');
 
 
 
 
634
  });
635
  }
636
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  function resetConverter() {
638
- // Reset UI
639
- fileInput.value = '';
640
- previewContainer.innerHTML = '<p class="text-gray-500">Your SVG preview will appear here</p>';
641
- progressContainer.classList.add('hidden');
642
- comparisonContainer.classList.add('hidden');
643
- downloadSection.classList.add('hidden');
644
- statsSection.classList.add('hidden');
645
- svgPreviewSection.classList.add('hidden');
646
- advancedSection.classList.add('hidden');
647
-
648
- // Reset variables
649
- originalFile = null;
650
- originalImageData = null;
651
- svgData = null;
652
-
653
- // Reset form values
654
- detailSlider.value = 5;
655
- colorSelect.value = 'auto';
656
- edgeSlider.value = 50;
657
  smoothCheckbox.checked = true;
658
- optimizeSelect.value = '1';
659
- bgSelect.value = 'transparent';
660
- qualitySelect.value = 'medium';
661
- bgColor.classList.add('hidden');
662
- }
663
-
664
- function toggleBgColorPicker() {
665
- if (bgSelect.value === 'custom') {
666
- bgColor.classList.remove('hidden');
667
- } else {
668
- bgColor.classList.add('hidden');
669
- }
670
  }
671
-
672
  function formatFileSize(bytes) {
 
673
  if (bytes === 0) return '0 Bytes';
674
-
675
  const k = 1024;
676
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
677
  const i = Math.floor(Math.log(bytes) / Math.log(k));
678
-
679
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
680
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  });
682
  </script>
683
- <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=thelip/tosvg" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
684
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
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/[email protected]/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/[email protected]/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; }
15
+ .dropzone.active { border-color: rgba(59, 130, 246, 1); background-color: rgba(59, 130, 246, 0.1); }
16
+ .canvas-container { position: relative; overflow: hidden; user-select: none; /* Prevent text selection during drag */ }
17
+ .comparison-slider { position: absolute; top: 0; left: 0; width: 50%; height: 100%; overflow: hidden; resize: horizontal; min-width: 10px; max-width: calc(100% - 10px); cursor: ew-resize; border-right: 2px solid rgba(255, 255, 255, 0.7); }
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">
 
42
  <!-- Upload Section -->
43
  <div class="space-y-4">
44
  <div class="flex items-center justify-between">
45
+ <h2 class="text-xl font-semibold text-gray-800">1. Upload Image</h2>
46
+ <div class="flex space-x-2">
47
+ <button id="sample-btn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm hover:bg-blue-200 transition">
48
+ <i class="fas fa-image mr-1"></i> Sample
49
+ </button>
50
+ <button id="reset-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm hover:bg-gray-200 transition">
51
+ <i class="fas fa-redo mr-1"></i> Reset
52
+ </button>
53
+ </div>
54
+ </div>
55
 
56
  <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer">
57
  <div class="flex flex-col items-center justify-center space-y-3">
58
  <i class="fas fa-cloud-upload-alt text-4xl text-blue-500"></i>
59
  <h3 class="text-lg font-medium text-gray-700">Drag & drop your photo here</h3>
60
  <p class="text-sm text-gray-500">or click to browse files</p>
61
+ <input type="file" id="file-input" class="hidden" accept="image/png, image/jpeg, image/webp, image/bmp">
62
  <button id="browse-btn" class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
63
  Select Image
64
  </button>
65
  </div>
66
+ <div id="file-info" class="mt-3 text-sm text-gray-600"></div>
67
  </div>
68
 
69
  <div class="bg-gray-50 p-4 rounded-lg">
70
+ <h4 class="text-sm font-medium text-gray-700 mb-2">2. Conversion Settings</h4>
71
  <div class="space-y-4">
72
  <div>
73
+ <label for="color-select" class="block text-sm text-gray-600 mb-1 flex justify-between items-center">
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">
81
+ <option value="8">8 colors (Fastest)</option>
82
+ <option value="16" selected>16 colors</option>
 
 
 
83
  <option value="32">32 colors</option>
84
  <option value="64">64 colors</option>
85
+ <option value="128">128 colors (Slowest)</option>
 
86
  </select>
87
  </div>
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">
98
  </div>
99
 
100
  <div class="flex items-center">
101
+ <input id="smooth-checkbox" type="checkbox" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
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
 
116
+ <button id="convert-btn" class="w-full py-3 bg-blue-600 text-white rounded-md font-medium hover:bg-blue-700 transition flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
117
  <i class="fas fa-magic"></i>
118
  <span>Convert to SVG</span>
119
  </button>
120
+ <div id="progress-container" class="hidden mt-2">
121
+ <div class="flex justify-between text-sm text-gray-600 mb-1">
122
+ <span id="progress-label">Processing...</span>
123
+ <span id="progress-percent">0%</span>
124
+ </div>
125
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
126
+ <div id="progress-bar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
127
+ </div>
128
+ </div>
129
  </div>
130
 
131
+ <!-- Preview Section -->
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
 
139
+ <div id="loading-indicator" class="hidden text-center py-10">
140
+ <div class="loader"></div>
141
+ <p class="text-gray-600 mt-2">Converting image, please wait...</p>
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>
166
+ </div>
167
+ </div>
 
 
 
 
 
 
 
168
 
169
+ <div id="stats-section" class="hidden bg-gray-50 p-4 rounded-lg">
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>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
 
 
 
 
 
 
 
197
 
 
 
 
 
 
 
 
 
198
  </div>
199
 
200
+ <!-- DeepSite Footer -->
201
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=thelip/tosvg" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p>
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');
209
+ const fileInfo = document.getElementById('file-info');
210
  const convertBtn = document.getElementById('convert-btn');
211
+ const previewPlaceholder = document.getElementById('preview-placeholder');
212
+ const loadingIndicator = document.getElementById('loading-indicator');
213
  const progressContainer = document.getElementById('progress-container');
214
  const progressBar = document.getElementById('progress-bar');
215
  const progressPercent = document.getElementById('progress-percent');
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');
 
234
  const smoothCheckbox = document.getElementById('smooth-checkbox');
 
 
 
 
235
 
236
+ // Comparison Slider Elements
237
+ const comparisonSlider = comparisonContainer.querySelector('.comparison-slider');
238
+ const sliderHandle = comparisonContainer.querySelector('.slider-handle');
239
+
240
+ // State variables
241
  let originalFile = null;
242
+ let originalImageDataUrl = null;
243
  let svgData = null;
244
+ let imageWidth = 0;
245
+ let imageHeight = 0;
246
+ let isDraggingSlider = false;
247
+
248
+ // --- Initialization ---
249
+ detailValue.textContent = detailSlider.value; // Initial display
250
 
251
+ // --- Event listeners ---
252
  browseBtn.addEventListener('click', () => fileInput.click());
253
  fileInput.addEventListener('change', handleFileSelect);
254
+ dropzone.addEventListener('dragover', handleDragOver, false);
255
+ dropzone.addEventListener('dragleave', handleDragLeave, false);
256
+ dropzone.addEventListener('drop', handleDrop, false);
257
  convertBtn.addEventListener('click', convertToSvg);
258
+ downloadSvgBtn.addEventListener('click', downloadSvgFile);
259
+ copySvgBtn.addEventListener('click', copySvgToClipboard);
260
  sampleBtn.addEventListener('click', loadSampleImage);
261
  resetBtn.addEventListener('click', resetConverter);
262
+ detailSlider.addEventListener('input', () => {
263
+ detailValue.textContent = detailSlider.value;
264
+ });
265
+
266
+ // Comparison Slider Logic
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
+
275
+ // --- Core Functions ---
276
 
 
277
  function handleFileSelect(e) {
278
+ const file = e.target.files?.[0];
279
  if (!file) return;
 
 
 
 
 
 
280
  processImageFile(file);
281
  }
282
 
 
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() {
350
+ if (!originalFile || !originalImageDataUrl) {
351
  alert('Please select an image first');
352
  return;
353
  }
354
 
355
+ // --- UI Updates: Start Conversion ---
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);
418
+ updateProgress(100, "Complete");
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
+
450
+ if (originalFile && originalFile.size > 0) {
451
+ const reductionPercent = Math.max(0, (1 - svgSizeBytes / originalFile.size) * 100).toFixed(1);
452
+ reductionEl.textContent = `${reductionPercent}%`;
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
+
463
+ function updateProgress(percent, label = "Processing...") {
464
+ const p = Math.max(0, Math.min(100, Math.round(percent)));
465
+ progressBar.style.width = `${p}%`;
466
+ progressPercent.textContent = `${p}%`;
467
+ progressLabel.textContent = label;
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 => {
510
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
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}%`;
599
+ }
600
+
601
+ function stopSliderDrag() {
602
+ if (isDraggingSlider) {
603
+ isDraggingSlider = false;
604
+ comparisonContainer.style.cursor = 'default'; // Restore default cursor
605
+ }
606
+ }
607
+
608
  });
609
  </script>
610
+ </body>
611
  </html>