thelip commited on
Commit
fda99b1
·
verified ·
1 Parent(s): b8d8086

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +518 -571
index.html CHANGED
@@ -3,670 +3,617 @@
3
  <head>
4
  <meta charset="UTF-8">
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
- <!-- Switched to jsDelivr CDN -->
9
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/potrace-wasm.js" defer></script>
10
- <!-- Quantize is less critical now but kept for potential future use/reference -->
11
- <!-- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quantize.min.js" defer></script> -->
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
13
  <style>
14
- /* Styles remain the same as before - truncated for brevity */
15
- .dropzone { border: 3px dashed rgba(59, 130, 246, 0.5); transition: all 0.3s ease; }
16
- .dropzone.active { border-color: rgba(59, 130, 246, 1); background-color: rgba(59, 130, 246, 0.1); }
17
- .canvas-container { position: relative; overflow: hidden; user-select: none; /* Prevent text selection during drag */ }
18
- .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); }
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-el { 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">
50
- <div class="p-6">
51
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
52
- <!-- Upload Section -->
53
- <div class="space-y-4">
54
- <div class="flex items-center justify-between">
55
- <h2 class="text-xl font-semibold text-gray-800">1. Upload Image</h2>
56
- <div class="flex space-x-2">
57
- <button id="sample-btn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm hover:bg-blue-200 transition">
58
- <i class="fas fa-image mr-1"></i> Sample
59
- </button>
60
- <button id="reset-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm hover:bg-gray-200 transition">
61
- <i class="fas fa-redo mr-1"></i> Reset
62
- </button>
63
- </div>
64
- </div>
65
-
66
- <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer">
67
- <div class="flex flex-col items-center justify-center space-y-3">
68
- <i class="fas fa-cloud-upload-alt text-4xl text-blue-500"></i>
69
- <h3 class="text-lg font-medium text-gray-700">Drag & drop your photo here</h3>
70
- <p class="text-sm text-gray-500">or click to browse files</p>
71
- <input type="file" id="file-input" class="hidden" accept="image/png, image/jpeg, image/webp, image/bmp">
72
- <button id="browse-btn" class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
73
- Select Image
74
- </button>
75
- </div>
76
- <div id="file-info" class="mt-3 text-sm text-gray-600"></div>
77
  </div>
78
-
79
- <div class="bg-gray-50 p-4 rounded-lg">
80
- <h4 class="text-sm font-medium text-gray-700 mb-2">2. Conversion Settings</h4>
81
- <div class="space-y-4">
82
- <div>
83
- <label for="color-select" class="block text-sm text-gray-600 mb-1 flex justify-between items-center">
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">
91
- <option value="8">8 colors (Fastest)</option>
92
- <option value="16" selected>16 colors</option>
93
- <option value="32">32 colors</option>
94
- <option value="64">64 colors</option>
95
- <option value="128">128 colors (Slowest)</option>
96
- </select>
97
- </div>
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">
108
- </div>
109
-
110
- <div class="flex items-center">
111
- <input id="smooth-checkbox" type="checkbox" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
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
-
122
- <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>
123
- <i class="fas fa-magic"></i>
124
- <span>Convert to SVG</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  </button>
126
- <div id="progress-container" class="hidden mt-2">
127
- <div class="flex justify-between text-sm text-gray-600 mb-1">
128
- <span id="progress-label">Processing...</span>
129
- <span id="progress-percent">0%</span>
130
- </div>
131
- <div class="w-full bg-gray-200 rounded-full h-2.5">
132
- <div id="progress-bar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
133
- </div>
134
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  </div>
 
136
 
137
- <!-- Preview Section -->
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
-
145
- <div id="loading-indicator" class="hidden text-center py-10">
146
- <div class="loader"></div>
147
- <p class="text-gray-600 mt-2">Converting image, please wait...</p>
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>
178
- </div>
179
- </div>
180
-
181
- <div id="stats-section" class="hidden bg-gray-50 p-4 rounded-lg">
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>
205
  </div>
206
- </div>
207
- </div>
208
- </div>
209
 
210
- </div>
211
 
212
- <!-- DeepSite Footer -->
213
- <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>
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');
221
- const fileInfo = document.getElementById('file-info');
222
- const convertBtn = document.getElementById('convert-btn');
223
- const previewPlaceholder = document.getElementById('preview-placeholder');
224
- const loadingIndicator = document.getElementById('loading-indicator');
225
- const progressContainer = document.getElementById('progress-container');
226
- const progressBar = document.getElementById('progress-bar');
227
- const progressPercent = document.getElementById('progress-percent');
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');
246
- const smoothCheckbox = document.getElementById('smooth-checkbox');
247
-
248
- // Comparison Slider Elements
249
- const comparisonSlider = comparisonContainer.querySelector('.comparison-slider');
250
- const sliderHandle = comparisonContainer.querySelector('.slider-handle');
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;
258
- let isDraggingSlider = false;
259
-
260
- // --- Initialization ---
261
- detailValue.textContent = detailSlider.value; // Initial display
262
-
263
- // --- Event listeners ---
264
- browseBtn.addEventListener('click', () => fileInput.click());
265
- fileInput.addEventListener('change', handleFileSelect);
266
- dropzone.addEventListener('dragover', handleDragOver, false);
267
- dropzone.addEventListener('dragleave', handleDragLeave, false);
268
- dropzone.addEventListener('drop', handleDrop, false);
269
- convertBtn.addEventListener('click', convertToSvg);
270
- downloadSvgBtn.addEventListener('click', downloadSvgFile);
271
- copySvgBtn.addEventListener('click', copySvgToClipboard);
272
- sampleBtn.addEventListener('click', loadSampleImage);
273
- resetBtn.addEventListener('click', resetConverter);
274
- detailSlider.addEventListener('input', () => {
275
- detailValue.textContent = detailSlider.value;
276
- });
277
-
278
- // Comparison Slider Logic
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
-
287
- // --- Core Functions ---
288
-
289
- function handleFileSelect(e) {
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
- }
302
 
303
- function handleDragLeave(e) {
304
- e.preventDefault();
305
- e.stopPropagation();
306
- dropzone.classList.remove('active');
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() {
371
- if (!originalFile || !originalImageDataUrl) {
372
- alert('Please select an image first');
 
 
 
 
 
 
373
  return;
374
  }
375
 
376
- // --- UI Updates: Start Conversion ---
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
- // THIS IS THE CRITICAL CHECK
396
  if (typeof PotraceWasm === 'undefined') {
397
- // If this error happens, the script from the CDN likely failed to load or execute.
398
- console.error("PotraceWasm global object is not defined. Script load failed?");
399
- throw new Error("PotraceWasm library failed to load. Check browser console and network connection.");
400
  }
401
 
402
- updateProgress(5, "Loading converter module...");
403
- // PotraceWasm.load() might return a promise, ensure it's awaited.
404
- // It handles loading the actual WASM binary.
405
- await PotraceWasm.load();
406
- updateProgress(10, "Processing image data...");
407
-
408
- // --- Prepare parameters for potrace-wasm ---
409
- const params = {
410
- posterize: true,
411
- steps: numColors,
412
- turdPolicy: PotraceWasm.TURD_SMOOTH, // Common policy for smoothing speckles
413
- turdSize: turdSize, // Pixels: 0 keeps everything, >0 removes smaller areas
414
- alphaMax: optimizeCurves ? 1.0 : 0, // Corner smoothing threshold (0 = sharp corners)
415
- optCurve: optimizeCurves, // Enable Bezier curve optimization
416
- optTolerance: optimizeCurves ? 0.2 : 0, // How much curve optimization can deviate
417
- turnPolicy: PotraceWasm.TURN_MINORITY, // How to resolve ambiguities at path turns
418
- // background: '#ffffff', // Optional: Set explicit background (usually transparent is fine)
419
- // fillStrategy: PotraceWasm.FILL_REMOVE_LAST, // Experiment if needed
420
- // rangeDistribution: PotraceWasm.RANGE_AUTO, // Experiment if needed
421
- };
422
-
423
- updateProgress(20, `Tracing ${numColors} colors (turdSize=${turdSize})...`);
424
 
425
- // --- Perform Conversion ---
426
- // Use the stored Data URL
427
- const result = await PotraceWasm.trace(originalImageDataUrl, params);
428
 
429
- updateProgress(90, "Cleaning & preparing SVG...");
430
 
431
- // Store and slightly clean the SVG data
432
- svgData = result.replace(/<!--[\s\S]*?-->/g, '').trim(); // Remove comments and trim whitespace
433
 
434
- // --- Display Results ---
435
- displayResults(svgData);
436
- updateProgress(100, "Complete");
437
 
438
  } catch (error) {
439
- console.error('SVG Conversion Error:', error); // Log the full error object
440
- const errorMsg = (error instanceof Error) ? error.message : String(error);
441
- alert(`Conversion failed: ${errorMsg}`);
442
- updateProgress(0, "Error");
443
- // Ensure loading/progress indicators reflect the error state
444
- loadingIndicator.classList.add('hidden');
445
- progressContainer.classList.remove('hidden'); // Keep progress bar visible showing error state
446
- previewPlaceholder.classList.remove('hidden'); // Show placeholder again on error
447
- comparisonContainer.classList.add('hidden'); // Ensure comparison view is hidden on error
448
  } finally {
449
- // --- UI Updates: End Conversion ---
450
- // Re-enable button only if an image is still loaded
451
- convertBtn.disabled = !originalFile;
452
- loadingIndicator.classList.add('hidden');
453
- // Hide progress bar only on success after a delay
454
- if (svgData) {
455
- setTimeout(() => {
456
- progressContainer.classList.add('hidden');
457
- }, 1500);
458
- } else {
459
- // Don't automatically hide progress bar on error
460
- }
461
  }
462
- }
463
-
464
-
465
- function displayResults(generatedSvgData) {
466
- // Inject SVG content into the output container
467
- svgOutputContainer.innerHTML = generatedSvgData;
468
 
469
- // Make the comparison view visible and hide placeholders/loaders
470
- comparisonContainer.classList.remove('hidden');
471
- previewPlaceholder.classList.add('hidden');
472
- loadingIndicator.classList.add('hidden');
473
- comparisonSlider.style.width = '50%'; // Reset slider to midpoint
474
 
475
- // Show download & stats sections
476
- downloadSection.classList.remove('hidden');
477
- statsSection.classList.remove('hidden');
478
 
479
- // Calculate and display stats
480
- const svgSizeBytes = new Blob([generatedSvgData]).size;
481
- svgSizeEl.textContent = formatFileSize(svgSizeBytes);
 
 
482
 
 
 
 
483
  if (originalFile && originalFile.size > 0) {
484
- const reductionPercent = Math.max(0, (1 - svgSizeBytes / originalFile.size) * 100).toFixed(1);
485
- reductionEl.textContent = `${reductionPercent}%`;
 
 
486
  } else {
487
- reductionEl.textContent = '-';
488
  }
489
 
490
- // Display SVG code preview
491
- svgCodeEl.textContent = generatedSvgData; // Use textContent for security in <pre>
492
- svgPreviewSection.classList.remove('hidden');
493
- }
494
 
495
- function updateProgress(percent, label = "Processing...") {
496
- const p = Math.max(0, Math.min(100, Math.round(percent)));
497
- progressBar.style.width = `${p}%`;
498
- progressPercent.textContent = `${p}%`;
499
- progressLabel.textContent = label;
500
- }
501
-
502
- function downloadSvgFile() {
503
- if (!svgData) {
504
- alert("No SVG data available to download.");
505
  return;
506
  }
507
  try {
508
- const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
509
  const url = URL.createObjectURL(blob);
510
  const a = document.createElement('a');
511
  a.href = url;
512
- a.download = (originalFile?.name || 'converted-image').replace(/\.[^/.]+$/, '') + '.svg';
513
- document.body.appendChild(a); // Required for Firefox
514
  a.click();
515
  document.body.removeChild(a);
516
  URL.revokeObjectURL(url);
517
  } catch (error) {
518
- console.error("Download failed:", error);
519
- alert("Could not initiate download. Please try copying the code.");
520
  }
521
- }
522
 
523
- function copySvgToClipboard() {
524
- if (!svgData) {
525
- alert("No SVG data available to copy.");
526
  return;
527
  }
528
- navigator.clipboard.writeText(svgData)
529
- .then(() => {
530
- const originalText = copySvgBtn.querySelector('span').textContent;
531
- const icon = copySvgBtn.querySelector('i');
532
- const originalIconClass = icon.className;
533
-
534
- copySvgBtn.querySelector('span').textContent = 'Copied!';
535
- icon.className = 'fas fa-check text-green-500'; // Change icon to checkmark
536
- copySvgBtn.classList.add('bg-green-100');
537
-
538
- setTimeout(() => {
539
- copySvgBtn.querySelector('span').textContent = originalText;
540
- icon.className = originalIconClass; // Restore original icon
541
- copySvgBtn.classList.remove('bg-green-100');
542
- }, 2000);
543
- })
544
- .catch(err => {
545
- console.error('Failed to copy SVG: ', err);
546
- 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.');
547
- });
548
- }
549
-
550
- function loadSampleImage() {
551
- resetConverter(); // Reset first
552
- // Using a smaller image for quicker sample loading/processing by default
553
- const sampleImageUrl = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80'; // Example: Shoe
554
- // const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Example: Landscape
555
-
556
- // Show immediate feedback
557
- loadingIndicator.classList.remove('hidden');
558
- previewPlaceholder.classList.add('hidden');
559
- comparisonContainer.classList.add('hidden'); // Hide comparison view during load
560
- fileInfo.textContent = "Loading sample image...";
561
- convertBtn.disabled = true;
562
-
563
- fetch(sampleImageUrl)
564
  .then(response => {
565
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
566
  return response.blob();
567
  })
568
  .then(blob => {
569
- // Guess file extension from mime type if possible
570
- let imageType = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/jpeg';
571
- const extension = imageType.split('/')[1] || 'jpg';
572
- const fileName = `sample-image.${extension}`;
573
- const file = new File([blob], fileName, { type: imageType });
574
- processImageFile(file); // Process the fetched image blob as a file
575
  })
576
  .catch(error => {
577
- console.error('Error loading sample image:', error);
578
- alert(`Failed to load sample image: ${error.message}. Please try uploading manually.`);
579
- resetConverter(); // Reset fully on error
580
- })
581
- .finally(() => {
582
- loadingIndicator.classList.add('hidden'); // Hide loading indicator regardless of outcome
583
  });
584
- }
585
-
586
- // --- Reset Functions ---
587
- function resetInput() {
588
- fileInput.value = ''; // Clear file input
589
- originalFile = null;
590
- originalImageDataUrl = null;
591
- imageWidth = 0;
592
- imageHeight = 0;
593
- convertBtn.disabled = true; // Disable convert button
594
- fileInfo.textContent = ''; // Clear file info text
595
- originalImage.src = ''; // Clear original image preview
596
- }
597
-
598
- function resetResultsUI() {
599
- svgData = null;
600
- svgOutputContainer.innerHTML = ''; // Clear SVG preview
601
- comparisonContainer.classList.add('hidden'); // Hide comparison slider view
602
- previewPlaceholder.classList.remove('hidden'); // Show the initial placeholder
603
- downloadSection.classList.add('hidden');
604
- statsSection.classList.add('hidden');
605
- svgPreviewSection.classList.add('hidden');
606
- originalSizeEl.textContent = '-'; // Reset stats
607
- svgSizeEl.textContent = '-';
608
- reductionEl.textContent = '-';
609
- svgCodeEl.textContent = ''; // Clear SVG code view
610
- progressContainer.classList.add('hidden'); // Hide progress bar
611
- updateProgress(0); // Reset progress values
612
- loadingIndicator.classList.add('hidden'); // Hide loader
613
- }
614
-
615
- function resetConverter() {
616
- resetInput(); // Clear input-related things
617
- resetResultsUI(); // Clear output/result related things
618
-
619
- // Reset form controls to default values
620
- detailSlider.value = 2;
621
- detailValue.textContent = '2';
622
- colorSelect.value = '16'; // Default color selection
623
- smoothCheckbox.checked = true;
624
-
625
- console.log('Converter Reset');
626
- }
627
 
628
- // --- Utility Functions ---
629
- function formatFileSize(bytes) {
630
- if (bytes == null || typeof bytes !== 'number' || bytes < 0) return 'N/A';
631
- if (bytes === 0) return '0 Bytes';
632
- const k = 1024;
633
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
634
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
635
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
636
- }
637
-
638
- // --- Comparison Slider Functions ---
639
- function startSliderDrag(e) {
640
- // Prevent default only for touch to avoid scrolling page while dragging
641
- if (e.type === 'touchstart') e.preventDefault();
642
- isDraggingSlider = true;
643
- comparisonContainer.style.cursor = 'ew-resize';
644
- }
645
-
646
- function dragSlider(e) {
647
- if (!isDraggingSlider) return;
648
- // Prevent default only for touch to avoid scrolling page while dragging
649
- if (e.type === 'touchmove') e.preventDefault();
650
-
651
- const rect = comparisonContainer.getBoundingClientRect();
652
- // Use touch or mouse coordinates
653
- const clientX = e.clientX ?? e.touches?.[0]?.clientX;
654
- if (typeof clientX === 'undefined') return; // Exit if no coordinate data
655
 
656
- let offsetX = clientX - rect.left;
657
- // Clamp the offset to be within the container bounds
658
- let newWidth = Math.max(0, Math.min(rect.width, offsetX));
659
- let percentWidth = (newWidth / rect.width) * 100;
660
 
661
- comparisonSlider.style.width = `${percentWidth}%`;
662
- }
 
663
 
664
- function stopSliderDrag() {
665
- if (isDraggingSlider) {
666
- isDraggingSlider = false;
667
- comparisonContainer.style.cursor = 'default'; // Restore default cursor
 
 
 
 
 
668
  }
669
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
 
671
  }); // End DOMContentLoaded
672
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Robust Photo to SVG Converter</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
+ <!-- Use jsDelivr CDN for potentially better reliability -->
9
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/potrace-wasm.js" defer></script>
 
 
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
  <style>
12
+ /* General */
13
+ body { font-family: sans-serif; }
14
+ /* Dropzone */
15
+ .dropzone { border: 3px dashed #cbd5e1; transition: all 0.3s ease; background-color: #f8fafc; }
16
+ .dropzone.active { border-color: #3b82f6; background-color: #eff6ff; }
17
+ /* Loader */
18
+ .loader { border: 4px solid #e5e7eb; border-radius: 50%; border-top: 4px solid #3b82f6; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
19
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
20
+ /* Comparison Slider */
21
+ .comparison-container { position: relative; overflow: hidden; user-select: none; background-color: #f3f4f6; aspect-ratio: 4 / 3; /* Default aspect ratio */ }
22
+ .comparison-slider-wrapper { position: absolute; top: 0; left: 0; width: 50%; height: 100%; overflow: hidden; cursor: ew-resize; border-right: 3px solid rgba(59, 130, 246, 0.6); }
23
+ .comparison-handle { position: absolute; top: 50%; right: -7.5px; transform: translateY(-50%); width: 12px; 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); }
24
+ .comparison-handle::before, .comparison-handle::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); width: 2px; height: 8px; background-color: white; border-radius: 1px; }
25
+ .comparison-handle::before { top: 10px; }
26
+ .comparison-handle::after { bottom: 10px; }
27
+ /* Ensure images/SVG scale nicely */
28
+ .comparison-container img,
29
+ .comparison-container #svg-output-wrapper svg { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; margin: auto; position: absolute; top: 0; left: 0; right: 0; bottom: 0; }
30
+ /* SVG Preview Background */
31
+ .svg-bg-pattern { background-image: linear-gradient(45deg, #e5e7eb 25%, transparent 25%), linear-gradient(-45deg, #e5e7eb 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e5e7eb 75%), linear-gradient(-45deg, transparent 75%, #e5e7eb 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; border: 1px solid #d1d5db; }
32
+ /* Tooltip */
33
  .tooltip { position: relative; display: inline-block; }
34
+ .tooltip .tooltip-text { visibility: hidden; width: 220px; background-color: #374151; color: #fff; text-align: center; border-radius: 6px; padding: 6px 8px; position: absolute; z-index: 50; bottom: 130%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; font-size: 0.75rem; line-height: 1.3; }
35
  .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; }
36
+ /* Code Preview */
37
+ #svg-code-container { background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.375rem; max-height: 250px; overflow: auto; padding: 0.75rem; }
38
+ #svg-code-el { font-family: monospace; font-size: 0.8rem; color: #1f2937; white-space: pre-wrap; word-break: break-all; }
 
 
 
 
 
 
 
39
  </style>
40
  </head>
41
+ <body class="bg-gray-100 min-h-screen p-4 md:p-8">
42
+ <div class="container mx-auto max-w-6xl bg-white rounded-lg shadow-xl overflow-hidden">
43
+ <header class="bg-gray-800 text-white p-4 text-center">
44
+ <h1 class="text-2xl font-bold">Photo to SVG Converter</h1>
45
+ <p class="text-sm text-gray-300">Convert raster images to colorful vector graphics</p>
46
+ </header>
47
+
48
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
49
+
50
+ <!-- Left Column: Upload & Settings -->
51
+ <section class="space-y-6">
52
+ <div>
53
+ <h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">1. Upload Image</h2>
54
+ <div id="dropzone" class="dropzone rounded-lg p-6 text-center cursor-pointer">
55
+ <div class="flex flex-col items-center justify-center space-y-2 text-gray-600">
56
+ <i class="fas fa-upload text-4xl text-blue-500"></i>
57
+ <p class="font-medium">Drag & drop image here</p>
58
+ <p class="text-sm">or</p>
59
+ <button id="browse-btn" type="button" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition text-sm">
60
+ Browse Files
61
+ </button>
62
+ <input type="file" id="file-input" class="hidden" accept="image/png, image/jpeg, image/webp, image/bmp">
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  </div>
64
+ </div>
65
+ <div id="file-info" class="mt-2 text-sm text-gray-600 h-5"></div> <!-- Placeholder for text -->
66
+ <div class="mt-2 flex justify-end space-x-2">
67
+ <button id="sample-btn" title="Load a sample image" class="px-2 py-1 bg-indigo-100 text-indigo-700 rounded text-xs hover:bg-indigo-200 transition"><i class="fas fa-image"></i> Load Sample</button>
68
+ <button id="reset-btn" title="Reset all settings and previews" class="px-2 py-1 bg-gray-200 text-gray-700 rounded text-xs hover:bg-gray-300 transition"><i class="fas fa-redo"></i> Reset</button>
69
+ </div>
70
+ </div>
71
+
72
+ <div id="settings-block" class="space-y-4 opacity-50 pointer-events-none"> <!-- Disabled initially -->
73
+ <h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">2. Configure Conversion</h2>
74
+ <div>
75
+ <label for="color-count-select" class="block text-sm font-medium text-gray-700 mb-1 flex justify-between items-center">
76
+ Number of Colors
77
+ <span class="tooltip">
78
+ <i class="fas fa-info-circle text-gray-400 cursor-help"></i>
79
+ <span class="tooltip-text">Controls the number of distinct color layers in the output SVG. More colors mean more detail but larger file size and longer processing. (Potrace 'steps')</span>
80
+ </span>
81
+ </label>
82
+ <select id="color-count-select" class="w-full p-2 border border-gray-300 rounded-md text-sm shadow-sm focus:ring-blue-500 focus:border-blue-500">
83
+ <option value="8">8 Colors (Fast, Abstract)</option>
84
+ <option value="16" selected>16 Colors</option>
85
+ <option value="32">32 Colors</option>
86
+ <option value="64">64 Colors (Detailed)</option>
87
+ <option value="128">128 Colors (Very Detailed, Slow)</option>
88
+ </select>
89
+ </div>
90
+ <div>
91
+ <label for="detail-slider" class="block text-sm font-medium text-gray-700 mb-1 flex justify-between items-center">
92
+ Detail Level (Speckle Removal) <span id="detail-value-label" class="font-mono text-xs bg-gray-200 px-1.5 py-0.5 rounded">2</span>
93
+ <span class="tooltip">
94
+ <i class="fas fa-info-circle text-gray-400 cursor-help"></i>
95
+ <span class="tooltip-text">Higher values remove smaller color areas (speckles/'turds'), resulting in a cleaner but potentially less detailed image. 0 keeps all details. (Potrace 'turdsize')</span>
96
+ </span>
97
+ </label>
98
+ <input id="detail-slider" type="range" min="0" max="20" value="2" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
99
+ </div>
100
+ <div class="flex items-center pt-2">
101
+ <input id="smooth-curves-checkbox" type="checkbox" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
102
+ <label for="smooth-curves-checkbox" class="ml-2 block text-sm font-medium text-gray-700">Smooth Curves (Recommended)</label>
103
+ <span class="tooltip ml-2">
104
+ <i class="fas fa-info-circle text-gray-400 cursor-help"></i>
105
+ <span class="tooltip-text">Optimizes the curves in the SVG paths for smoother results. Slightly increases processing time. (Potrace 'opticurve')</span>
106
+ </span>
107
+ </div>
108
+ </div>
109
+
110
+ <div>
111
+ <h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">3. Generate SVG</h2>
112
+ <button id="convert-btn" type="button" class="w-full py-2.5 px-4 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>
113
+ <i class="fas fa-magic"></i>
114
+ <span>Convert to SVG</span>
115
+ </button>
116
+ <div id="progress-container" class="hidden mt-3">
117
+ <div class="flex justify-between text-sm text-gray-600 mb-1">
118
+ <span id="progress-label">Processing...</span>
119
+ <span id="progress-percent">0%</span>
120
  </div>
121
+ <div class="w-full bg-gray-200 rounded-full h-2">
122
+ <div id="progress-bar" class="bg-blue-600 h-2 rounded-full transition-width duration-150 ease-linear" style="width: 0%"></div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </section>
127
+
128
+ <!-- Right Column: Preview & Results -->
129
+ <section class="space-y-6">
130
+ <h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">Preview & Results</h2>
131
+
132
+ <!-- Loading Indicator -->
133
+ <div id="loading-indicator" class="hidden text-center py-10">
134
+ <div class="loader"></div>
135
+ <p class="text-gray-600 mt-2 animate-pulse">Generating SVG, please wait...</p>
136
+ <p class="text-xs text-gray-500">(Complex images may take some time)</p>
137
+ </div>
138
+
139
+ <!-- Placeholder -->
140
+ <div id="preview-placeholder" class="svg-bg-pattern rounded-lg flex items-center justify-center text-center p-4" style="min-height: 300px;">
141
+ <p class="text-gray-500">Upload an image to begin</p>
142
+ </div>
143
+
144
+ <!-- Comparison Viewer -->
145
+ <div id="comparison-container" class="comparison-container hidden svg-bg-pattern rounded-lg overflow-hidden" style="min-height: 300px;">
146
+ <!-- Base Layer: Original Image -->
147
+ <div class="absolute inset-0 flex items-center justify-center">
148
+ <img id="original-image-preview" src="#" alt="Original" class="opacity-0 transition-opacity duration-300">
149
+ </div>
150
+ <!-- Top Layer: SVG Output with Slider -->
151
+ <div class="comparison-slider-wrapper">
152
+ <div id="svg-output-wrapper" class="absolute inset-0 bg-white flex items-center justify-center">
153
+ <!-- SVG will be injected here -->
154
+ </div>
155
+ <div class="comparison-handle"></div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Download & Copy Actions -->
160
+ <div id="results-actions" class="hidden bg-green-50 p-4 rounded-lg border border-green-200">
161
+ <h3 class="text-sm font-medium text-green-800 mb-3">Success!</h3>
162
+ <div class="flex flex-col sm:flex-row gap-3">
163
+ <button id="download-svg-btn" type="button" class="flex-1 py-2 px-4 bg-white border border-green-600 text-green-700 rounded-md hover:bg-green-100 transition flex items-center justify-center space-x-2 text-sm font-medium">
164
+ <i class="fas fa-download"></i>
165
+ <span>Download SVG</span>
166
  </button>
167
+ <button id="copy-svg-btn" type="button" class="flex-1 py-2 px-4 bg-white border border-green-600 text-green-700 rounded-md hover:bg-green-100 transition flex items-center justify-center space-x-2 text-sm font-medium">
168
+ <i class="far fa-copy"></i>
169
+ <span data-original-text="Copy SVG Code">Copy SVG Code</span>
170
+ </button>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Stats Display -->
175
+ <div id="stats-section" class="hidden bg-gray-50 p-3 rounded-lg border border-gray-200">
176
+ <div class="grid grid-cols-3 gap-3 text-center">
177
+ <div>
178
+ <p class="text-xs text-gray-500">Original Size</p>
179
+ <p id="original-size-el" class="font-medium text-sm text-gray-800">-</p>
180
+ </div>
181
+ <div>
182
+ <p class="text-xs text-gray-500">SVG Size</p>
183
+ <p id="svg-size-el" class="font-medium text-sm text-gray-800">-</p>
184
+ </div>
185
+ <div>
186
+ <p class="text-xs text-gray-500">Reduction</p>
187
+ <p id="reduction-el" class="font-medium text-sm text-green-600">-</p>
188
+ </div>
189
  </div>
190
+ </div>
191
 
192
+ <!-- SVG Code Preview -->
193
+ <div id="svg-code-preview-section" class="hidden">
194
+ <h3 class="text-md font-semibold text-gray-700 mb-2">Generated SVG Code</h3>
195
+ <div id="svg-code-container">
196
+ <pre id="svg-code-el"></pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  </div>
198
+ </div>
199
+ </section>
200
+ </div> <!-- End Grid -->
201
 
202
+ </div> <!-- End Container -->
203
 
204
+ <!-- DeepSite Footer (Optional) -->
205
+ <p style="font-size: 11px; color: #555; text-align: center; margin-top: 1rem; padding-bottom: 0.5rem;">Powered by Potrace WASM</p>
206
 
207
  <script>
208
+ document.addEventListener('DOMContentLoaded', () => {
209
+ // --- DOM Element References ---
210
+ const elements = {
211
+ dropzone: document.getElementById('dropzone'),
212
+ fileInput: document.getElementById('file-input'),
213
+ browseBtn: document.getElementById('browse-btn'),
214
+ fileInfo: document.getElementById('file-info'),
215
+ sampleBtn: document.getElementById('sample-btn'),
216
+ resetBtn: document.getElementById('reset-btn'),
217
+ settingsBlock: document.getElementById('settings-block'),
218
+ colorCountSelect: document.getElementById('color-count-select'),
219
+ detailSlider: document.getElementById('detail-slider'),
220
+ detailValueLabel: document.getElementById('detail-value-label'),
221
+ smoothCurvesCheckbox: document.getElementById('smooth-curves-checkbox'),
222
+ convertBtn: document.getElementById('convert-btn'),
223
+ progressContainer: document.getElementById('progress-container'),
224
+ progressLabel: document.getElementById('progress-label'),
225
+ progressPercent: document.getElementById('progress-percent'),
226
+ progressBar: document.getElementById('progress-bar'),
227
+ loadingIndicator: document.getElementById('loading-indicator'),
228
+ previewPlaceholder: document.getElementById('preview-placeholder'),
229
+ comparisonContainer: document.getElementById('comparison-container'),
230
+ originalImagePreview: document.getElementById('original-image-preview'),
231
+ svgOutputWrapper: document.getElementById('svg-output-wrapper'),
232
+ comparisonSliderWrapper: document.querySelector('.comparison-slider-wrapper'),
233
+ comparisonHandle: document.querySelector('.comparison-handle'),
234
+ resultsActions: document.getElementById('results-actions'),
235
+ downloadSvgBtn: document.getElementById('download-svg-btn'),
236
+ copySvgBtn: document.getElementById('copy-svg-btn'),
237
+ copySvgBtnText: document.querySelector('#copy-svg-btn span'),
238
+ statsSection: document.getElementById('stats-section'),
239
+ originalSizeEl: document.getElementById('original-size-el'),
240
+ svgSizeEl: document.getElementById('svg-size-el'),
241
+ reductionEl: document.getElementById('reduction-el'),
242
+ svgCodePreviewSection: document.getElementById('svg-code-preview-section'),
243
+ svgCodeEl: document.getElementById('svg-code-el'),
244
+ };
245
+
246
+ // --- State Variables ---
247
  let originalFile = null;
248
+ let originalImageDataUrl = null;
249
+ let svgResultData = null;
250
+ let isSliderDragging = false;
251
+ let originalImageDimensions = { width: 0, height: 0 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
 
 
 
 
 
253
 
254
+ // --- Utility Functions ---
255
+ const formatFileSize = (bytes) => {
256
+ if (bytes == null || typeof bytes !== 'number' || bytes < 0) return 'N/A';
257
+ if (bytes === 0) return '0 Bytes';
258
+ const k = 1024;
259
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
260
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
261
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
262
+ };
263
 
264
+ const updateProgress = (percent, label = "Processing...") => {
265
+ const p = Math.max(0, Math.min(100, Math.round(percent)));
266
+ elements.progressBar.style.width = `${p}%`;
267
+ elements.progressPercent.textContent = `${p}%`;
268
+ elements.progressLabel.textContent = label;
269
+ elements.progressContainer.classList.remove('hidden');
270
+ };
271
+
272
+ const showAlert = (message, type = 'error') => {
273
+ // Simple alert for now, could be replaced with a nicer modal/toast
274
+ console[type === 'error' ? 'error' : 'warn'](message);
275
+ alert(`[${type.toUpperCase()}] ${message}`);
276
+ };
277
+
278
+ // --- UI State Management ---
279
+ const showLoadingState = (isLoading) => {
280
+ elements.loadingIndicator.classList.toggle('hidden', !isLoading);
281
+ elements.convertBtn.disabled = isLoading;
282
+ // Hide preview areas during loading
283
+ if (isLoading) {
284
+ elements.previewPlaceholder.classList.add('hidden');
285
+ elements.comparisonContainer.classList.add('hidden');
286
+ elements.resultsActions.classList.add('hidden');
287
+ elements.statsSection.classList.add('hidden');
288
+ elements.svgCodePreviewSection.classList.add('hidden');
289
+ }
290
+ };
291
+
292
+ const enableSettings = (isEnabled) => {
293
+ elements.settingsBlock.classList.toggle('opacity-50', !isEnabled);
294
+ elements.settingsBlock.classList.toggle('pointer-events-none', !isEnabled);
295
+ elements.convertBtn.disabled = !isEnabled || !originalFile; // Also check if file exists
296
+ };
297
+
298
+ const resetUI = (fullReset = true) => {
299
+ console.log('Resetting UI...');
300
+ showLoadingState(false);
301
+ elements.progressContainer.classList.add('hidden');
302
+ elements.previewPlaceholder.classList.remove('hidden');
303
+ elements.comparisonContainer.classList.add('hidden');
304
+ elements.resultsActions.classList.add('hidden');
305
+ elements.statsSection.classList.add('hidden');
306
+ elements.svgCodePreviewSection.classList.add('hidden');
307
+ elements.fileInfo.textContent = '';
308
+ elements.originalImagePreview.src = '#'; // Clear image src
309
+ elements.originalImagePreview.classList.add('opacity-0');
310
+ elements.svgOutputWrapper.innerHTML = '';
311
+ elements.svgCodeEl.textContent = '';
312
+ elements.originalSizeEl.textContent = '-';
313
+ elements.svgSizeEl.textContent = '-';
314
+ elements.reductionEl.textContent = '-';
315
+ elements.copySvgBtnText.textContent = elements.copySvgBtnText.dataset.originalText; // Reset copy button text
316
+ elements.comparisonSliderWrapper.style.width = '50%'; // Reset slider visual
317
+
318
+
319
+ if (fullReset) {
320
+ originalFile = null;
321
+ originalImageDataUrl = null;
322
+ svgResultData = null;
323
+ originalImageDimensions = { width: 0, height: 0 };
324
+ elements.fileInput.value = ''; // Clear file input selection
325
+ // Reset settings to defaults
326
+ elements.colorCountSelect.value = '16';
327
+ elements.detailSlider.value = '2';
328
+ elements.detailValueLabel.textContent = '2';
329
+ elements.smoothCurvesCheckbox.checked = true;
330
+ enableSettings(false); // Disable settings block
331
  } else {
332
+ // Partial reset (e.g., before conversion starts)
333
+ svgResultData = null;
334
+ enableSettings(true); // Keep settings enabled if file is loaded
335
  }
336
+ };
337
 
338
+ // --- Event Handlers ---
339
+ const handleFileSelect = (file) => {
340
+ if (!file) return;
341
+ if (!file.type.match('image/(png|jpeg|webp|bmp)')) {
342
+ showAlert('Invalid file type. Please select a PNG, JPG, WebP, or BMP image.');
343
+ resetUI(true); // Full reset if invalid file
344
  return;
345
  }
346
 
347
+ resetUI(false); // Reset results, keep settings potentially enabled
348
  originalFile = file;
 
349
 
350
+ elements.fileInfo.textContent = `Loading ${file.name}...`;
351
+ enableSettings(false); // Disable settings while reading
352
+
353
+ const reader = new FileReader();
354
+ reader.onload = (e) => {
355
+ originalImageDataUrl = e.target.result;
356
 
 
 
357
  const img = new Image();
358
  img.onload = () => {
359
+ originalImageDimensions = { width: img.width, height: img.height };
360
+ elements.fileInfo.textContent = `${originalFile.name} (${img.width}x${img.height})`;
361
+ elements.originalSizeEl.textContent = formatFileSize(originalFile.size);
362
+ elements.originalImagePreview.src = originalImageDataUrl;
363
+ elements.originalImagePreview.classList.remove('opacity-0');
364
+
365
+ // Adjust comparison container aspect ratio (optional but nice)
366
+ const aspectRatio = img.width / img.height;
367
+ elements.comparisonContainer.style.aspectRatio = `${aspectRatio}`;
368
+
369
+
370
+ elements.previewPlaceholder.classList.add('hidden');
371
+ // Don't show comparison container yet, wait for conversion attempt
372
+ enableSettings(true); // Enable settings and convert button
 
 
373
  };
374
  img.onerror = () => {
375
+ showAlert('Could not read image dimensions. File may be corrupt.');
376
+ resetUI(true);
377
  };
378
+ img.src = originalImageDataUrl;
379
  };
380
  reader.onerror = () => {
381
+ showAlert('Failed to read the file.');
382
+ resetUI(true);
383
  };
384
+ reader.readAsDataURL(file);
385
+ };
386
+
387
+ const handleDrop = (e) => {
388
+ e.preventDefault();
389
+ e.stopPropagation();
390
+ elements.dropzone.classList.remove('active');
391
+ const file = e.dataTransfer?.files?.[0];
392
+ if (file) {
393
+ handleFileSelect(file);
394
+ }
395
+ };
396
+
397
+ const handleDragOver = (e) => {
398
+ e.preventDefault();
399
+ e.stopPropagation();
400
+ elements.dropzone.classList.add('active');
401
+ };
402
 
403
+ const handleDragLeave = (e) => {
404
+ e.preventDefault();
405
+ e.stopPropagation();
406
+ elements.dropzone.classList.remove('active');
407
+ };
408
+
409
+ const convertToSvg = async () => {
410
+ if (!originalFile || !originalImageDataUrl) {
411
+ showAlert('Please upload an image first.');
412
  return;
413
  }
414
 
415
+ resetResultsUI(); // Clear previous results before starting
416
+ showLoadingState(true);
 
 
 
 
 
 
 
417
  updateProgress(0, "Initializing...");
 
418
 
419
+ const params = {
420
+ steps: parseInt(elements.colorCountSelect.value, 10),
421
+ turdSize: parseInt(elements.detailSlider.value, 10),
422
+ optCurve: elements.smoothCurvesCheckbox.checked,
423
+ // --- Standard Potrace Params (Refer to potrace-wasm docs for more) ---
424
+ posterize: true, // Essential for color tracing
425
+ turdPolicy: PotraceWasm.TURD_SMOOTH,
426
+ alphaMax: elements.smoothCurvesCheckbox.checked ? 1.0 : 0, // Adjusts smoothness (corner threshold)
427
+ optTolerance: elements.smoothCurvesCheckbox.checked ? 0.2 : 0, // Optimization tolerance
428
+ turnPolicy: PotraceWasm.TURN_MINORITY,
429
+ // background: '#FFFFFF', // Can set explicit background if needed
430
+ };
431
 
432
  try {
 
 
433
  if (typeof PotraceWasm === 'undefined') {
434
+ throw new Error("PotraceWasm library is not available. Check network connection or browser console.");
 
 
435
  }
436
 
437
+ updateProgress(5, "Loading WASM module...");
438
+ await PotraceWasm.load(); // Ensure WASM binary is loaded & ready
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
+ updateProgress(15, `Tracing ${params.steps} colors...`);
441
+ console.log("Potrace Parameters:", params);
 
442
 
443
+ const svgString = await PotraceWasm.trace(originalImageDataUrl, params);
444
 
445
+ updateProgress(95, "Finalizing SVG...");
446
+ svgResultData = svgString.replace(/<!--[\s\S]*?-->/g, '').trim(); // Clean comments
447
 
448
+ displayResults();
449
+ updateProgress(100, "Complete!");
450
+ setTimeout(() => elements.progressContainer.classList.add('hidden'), 1500); // Hide progress after success
451
 
452
  } catch (error) {
453
+ console.error("SVG Conversion Error:", error);
454
+ showAlert(`Conversion failed: ${error.message || error}`);
455
+ resetResultsUI(true); // Show placeholder again on error
456
+ elements.progressContainer.classList.remove('hidden'); // Keep progress bar visible
457
+ updateProgress(0, "Error!"); // Show error status
 
 
 
 
458
  } finally {
459
+ showLoadingState(false); // Hide loader, re-enable button
 
 
 
 
 
 
 
 
 
 
 
460
  }
461
+ };
 
 
 
 
 
462
 
463
+ const displayResults = () => {
464
+ if (!svgResultData) return;
 
 
 
465
 
466
+ // Inject SVG
467
+ elements.svgOutputWrapper.innerHTML = svgResultData;
 
468
 
469
+ // Show relevant sections
470
+ elements.comparisonContainer.classList.remove('hidden');
471
+ elements.resultsActions.classList.remove('hidden');
472
+ elements.statsSection.classList.remove('hidden');
473
+ elements.svgCodePreviewSection.classList.remove('hidden');
474
 
475
+ // Calculate and show stats
476
+ const svgSizeBytes = new Blob([svgResultData]).size;
477
+ elements.svgSizeEl.textContent = formatFileSize(svgSizeBytes);
478
  if (originalFile && originalFile.size > 0) {
479
+ const reductionPercent = Math.max(0, (1 - svgSizeBytes / originalFile.size) * 100);
480
+ elements.reductionEl.textContent = `${reductionPercent.toFixed(1)}%`;
481
+ elements.reductionEl.classList.toggle('text-red-600', reductionPercent < 0); // Red if larger
482
+ elements.reductionEl.classList.toggle('text-green-600', reductionPercent >= 0);
483
  } else {
484
+ elements.reductionEl.textContent = '-';
485
  }
486
 
487
+ // Show SVG code
488
+ elements.svgCodeEl.textContent = svgResultData;
 
 
489
 
490
+ // Ensure comparison container is visible and placeholder is hidden
491
+ elements.previewPlaceholder.classList.add('hidden');
492
+ elements.loadingIndicator.classList.add('hidden');
493
+ };
494
+
495
+ const downloadSvg = () => {
496
+ if (!svgResultData) {
497
+ showAlert("No SVG generated yet.", "warning");
 
 
498
  return;
499
  }
500
  try {
501
+ const blob = new Blob([svgResultData], { type: 'image/svg+xml;charset=utf-8' });
502
  const url = URL.createObjectURL(blob);
503
  const a = document.createElement('a');
504
  a.href = url;
505
+ a.download = `${originalFile.name.replace(/\.[^/.]+$/, '')}_${elements.colorCountSelect.value}colors.svg`;
506
+ document.body.appendChild(a);
507
  a.click();
508
  document.body.removeChild(a);
509
  URL.revokeObjectURL(url);
510
  } catch (error) {
511
+ showAlert("Failed to initiate download.", "error");
512
+ console.error("Download error:", error);
513
  }
514
+ };
515
 
516
+ const copySvgCode = () => {
517
+ if (!svgResultData) {
518
+ showAlert("No SVG generated yet.", "warning");
519
  return;
520
  }
521
+ navigator.clipboard.writeText(svgResultData).then(() => {
522
+ const originalText = elements.copySvgBtnText.dataset.originalText;
523
+ elements.copySvgBtnText.textContent = 'Copied!';
524
+ elements.copySvgBtn.classList.add('bg-green-100');
525
+ setTimeout(() => {
526
+ elements.copySvgBtnText.textContent = originalText;
527
+ elements.copySvgBtn.classList.remove('bg-green-100');
528
+ }, 2000);
529
+ }).catch(err => {
530
+ showAlert('Failed to copy text. Please try manually.', 'error');
531
+ console.error('Clipboard copy error:', err);
532
+ });
533
+ };
534
+
535
+ const loadSample = () => {
536
+ console.log("Loading sample image...");
537
+ resetUI(true); // Full reset before loading sample
538
+ showLoadingState(true); // Show loader while fetching
539
+ elements.fileInfo.textContent = "Fetching sample image...";
540
+
541
+ // Sample image URL (consider using a smaller one for faster testing)
542
+ const sampleUrl = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Shoe example
543
+
544
+ fetch(sampleUrl)
 
 
 
 
 
 
 
 
 
 
 
 
545
  .then(response => {
546
+ if (!response.ok) throw new Error(`HTTP error ${response.status}`);
547
  return response.blob();
548
  })
549
  .then(blob => {
550
+ const fileName = 'sample_image.jpg'; // Give it a name
551
+ const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
552
+ showLoadingState(false); // Hide loader before processing
553
+ handleFileSelect(file); // Process the fetched blob as a file
 
 
554
  })
555
  .catch(error => {
556
+ showLoadingState(false);
557
+ showAlert(`Failed to load sample image: ${error.message}`, 'error');
558
+ resetUI(true);
 
 
 
559
  });
560
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
+ // --- Slider Logic ---
563
+ const startDrag = (e) => {
564
+ if (e.target !== elements.comparisonHandle) return;
565
+ if (e.type === 'touchstart') e.preventDefault(); // Prevent scroll on touch
566
+ isSliderDragging = true;
567
+ elements.comparisonContainer.style.cursor = 'ew-resize';
568
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
+ const drag = (e) => {
571
+ if (!isSliderDragging) return;
572
+ if (e.type === 'touchmove') e.preventDefault(); // Prevent scroll on touch
 
573
 
574
+ const rect = elements.comparisonContainer.getBoundingClientRect();
575
+ const clientX = e.clientX ?? e.touches?.[0]?.clientX;
576
+ if (typeof clientX === 'undefined') return;
577
 
578
+ let offsetX = clientX - rect.left;
579
+ let newWidthPercent = Math.max(0, Math.min(100, (offsetX / rect.width) * 100));
580
+ elements.comparisonSliderWrapper.style.width = `${newWidthPercent}%`;
581
+ };
582
+
583
+ const stopDrag = () => {
584
+ if (isSliderDragging) {
585
+ isSliderDragging = false;
586
+ elements.comparisonContainer.style.cursor = 'default';
587
  }
588
+ };
589
+
590
+
591
+ // --- Event Listener Setup ---
592
+ elements.browseBtn.addEventListener('click', () => elements.fileInput.click());
593
+ elements.fileInput.addEventListener('change', (e) => handleFileSelect(e.target.files[0]));
594
+ elements.dropzone.addEventListener('dragover', handleDragOver);
595
+ elements.dropzone.addEventListener('dragleave', handleDragLeave);
596
+ elements.dropzone.addEventListener('drop', handleDrop);
597
+ elements.convertBtn.addEventListener('click', convertToSvg);
598
+ elements.downloadSvgBtn.addEventListener('click', downloadSvg);
599
+ elements.copySvgBtn.addEventListener('click', copySvgCode);
600
+ elements.sampleBtn.addEventListener('click', loadSample);
601
+ elements.resetBtn.addEventListener('click', () => resetUI(true));
602
+ elements.detailSlider.addEventListener('input', (e) => {
603
+ elements.detailValueLabel.textContent = e.target.value;
604
+ });
605
+
606
+ // Slider Listeners
607
+ elements.comparisonContainer.addEventListener('mousedown', startDrag);
608
+ document.addEventListener('mousemove', drag); // Listen on document for wider drag area
609
+ document.addEventListener('mouseup', stopDrag);
610
+ elements.comparisonContainer.addEventListener('touchstart', startDrag, { passive: false });
611
+ document.addEventListener('touchmove', drag, { passive: false });
612
+ document.addEventListener('touchend', stopDrag);
613
+
614
+ // --- Initial Setup ---
615
+ resetUI(true); // Start with a clean slate
616
+ elements.copySvgBtnText.dataset.originalText = elements.copySvgBtnText.textContent; // Store original button text
617
 
618
  }); // End DOMContentLoaded
619
  </script>