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