tosvg / index.html
thelip's picture
Update index.html
b8d8086 verified
raw
history blame
40.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Photo to SVG Converter</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Switched to jsDelivr CDN -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/potrace-wasm.js" defer></script>
<!-- Quantize is less critical now but kept for potential future use/reference -->
<!-- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quantize.min.js" defer></script> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Styles remain the same as before - truncated for brevity */
.dropzone { border: 3px dashed rgba(59, 130, 246, 0.5); transition: all 0.3s ease; }
.dropzone.active { border-color: rgba(59, 130, 246, 1); background-color: rgba(59, 130, 246, 0.1); }
.canvas-container { position: relative; overflow: hidden; user-select: none; /* Prevent text selection during drag */ }
.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); }
.comparison-slider::-webkit-resizer { display: none; /* Hide default resizer */ }
.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); }
.progress-bar { height: 5px; transition: width 0.1s ease-out; }
.svg-preview-bg { /* Renamed from .svg-preview to avoid conflict */
border: 1px solid #e5e7eb;
background-image: url('data:image/svg+xml;utf8,<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10" fill="%23f3f4f6"/><rect x="10" y="10" width="10" height="10" fill="%23f3f4f6"/></svg>');
background-size: 20px 20px;
}
.tooltip { position: relative; display: inline-block; }
.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; }
.tooltip:hover .tooltip-text { visibility: visible; opacity: 1; }
/* Loading spinner */
.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; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Ensure SVG scales correctly within its container */
#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 */ }
#original-image { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; margin: auto; }
/* Style for the SVG code preview */
#svg-code-container { background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.375rem; max-height: 200px; overflow: auto; padding: 0.5rem; }
#svg-code-el { font-family: monospace; font-size: 0.75rem; color: #1f2937; white-space: pre-wrap; word-break: break-all; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-800 mb-2">Advanced Photo to SVG Converter</h1>
<p class="text-gray-600">Transform photos into multi-color vector graphics using Potrace</p>
</div>
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Upload Section -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">1. Upload Image</h2>
<div class="flex space-x-2">
<button id="sample-btn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm hover:bg-blue-200 transition">
<i class="fas fa-image mr-1"></i> Sample
</button>
<button id="reset-btn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm hover:bg-gray-200 transition">
<i class="fas fa-redo mr-1"></i> Reset
</button>
</div>
</div>
<div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer">
<div class="flex flex-col items-center justify-center space-y-3">
<i class="fas fa-cloud-upload-alt text-4xl text-blue-500"></i>
<h3 class="text-lg font-medium text-gray-700">Drag & drop your photo here</h3>
<p class="text-sm text-gray-500">or click to browse files</p>
<input type="file" id="file-input" class="hidden" accept="image/png, image/jpeg, image/webp, image/bmp">
<button id="browse-btn" class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
Select Image
</button>
</div>
<div id="file-info" class="mt-3 text-sm text-gray-600"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-gray-700 mb-2">2. Conversion Settings</h4>
<div class="space-y-4">
<div>
<label for="color-select" class="block text-sm text-gray-600 mb-1 flex justify-between items-center">
Color Count
<span class="tooltip">
<i class="fas fa-info-circle text-gray-400"></i>
<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>
</span>
</label>
<select id="color-select" class="w-full p-2 border border-gray-300 rounded-md text-sm">
<option value="8">8 colors (Fastest)</option>
<option value="16" selected>16 colors</option>
<option value="32">32 colors</option>
<option value="64">64 colors</option>
<option value="128">128 colors (Slowest)</option>
</select>
</div>
<div>
<label for="detail-slider" class="block text-sm text-gray-600 mb-1 flex justify-between items-center">
Detail Preservation <span id="detail-value" class="font-mono text-xs bg-gray-200 px-1 rounded"></span>
<span class="tooltip">
<i class="fas fa-info-circle text-gray-400"></i>
<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>
</span>
</label>
<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">
</div>
<div class="flex items-center">
<input id="smooth-checkbox" type="checkbox" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="smooth-checkbox" class="ml-2 block text-sm text-gray-700">Optimize curves (slower, smoother)</label>
<span class="tooltip ml-2">
<i class="fas fa-info-circle text-gray-400"></i>
<span class="tooltip-text">Apply curve optimization (Potrace 'opticurve'). Makes curves smoother but increases processing time. Recommended for less jagged results.</span>
</span>
</div>
</div>
</div>
<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>
<i class="fas fa-magic"></i>
<span>Convert to SVG</span>
</button>
<div id="progress-container" class="hidden mt-2">
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span id="progress-label">Processing...</span>
<span id="progress-percent">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="progress-bar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Preview Section -->
<div class="space-y-4">
<h2 class="text-xl font-semibold text-gray-800">3. Preview & Download</h2>
<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 -->
<p class="text-gray-500">Upload an image and click Convert<br>to see the preview here.</p>
</div>
<div id="loading-indicator" class="hidden text-center py-10">
<div class="loader"></div>
<p class="text-gray-600 mt-2">Converting image, please wait...</p>
<p class="text-xs text-gray-500">(This can take a while for large images or many colors)</p>
</div>
<!-- Container for both Original and SVG -->
<div id="comparison-container" class="canvas-container hidden relative bg-gray-100 rounded-lg overflow-hidden svg-preview-bg" style="height: 300px;">
<!-- Original Image Layer -->
<div class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<img id="original-image" src="" alt="Original">
</div>
<!-- SVG Layer with Slider -->
<div class="comparison-slider absolute top-0 left-0 w-1/2 h-full overflow-hidden">
<div id="svg-output" class="absolute top-0 left-0 w-full h-full bg-white flex items-center justify-center">
<!-- SVG content will be injected here -->
</div>
<div class="slider-handle"></div>
</div>
</div>
<div id="download-section" class="hidden bg-blue-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-blue-800 mb-2">Conversion Complete!</h4>
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
<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">
<i class="fas fa-download"></i>
<span>Download SVG</span>
</button>
<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">
<i class="far fa-copy"></i>
<span>Copy SVG Code</span>
</button>
</div>
</div>
<div id="stats-section" class="hidden bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<p class="text-xs text-gray-500">Original Size</p>
<p id="original-size-el" class="font-medium text-sm">-</p>
</div>
<div>
<p class="text-xs text-gray-500">SVG Size</p>
<p id="svg-size-el" class="font-medium text-sm">-</p>
</div>
<div>
<p class="text-xs text-gray-500">Reduction</p>
<p id="reduction-el" class="font-medium text-sm">-</p>
</div>
</div>
</div>
<div id="svg-preview-section" class="hidden bg-white border border-gray-200 rounded-lg mt-4">
<div class="p-4">
<h3 class="text-lg font-semibold text-gray-700 mb-2">Generated SVG Code</h3>
<div id="svg-code-container">
<pre id="svg-code-el"></pre> <!-- Use pre for formatting -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- DeepSite Footer -->
<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>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM elements (using more descriptive names)
const fileInput = document.getElementById('file-input');
const dropzone = document.getElementById('dropzone');
const browseBtn = document.getElementById('browse-btn');
const fileInfo = document.getElementById('file-info');
const convertBtn = document.getElementById('convert-btn');
const previewPlaceholder = document.getElementById('preview-placeholder');
const loadingIndicator = document.getElementById('loading-indicator');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressPercent = document.getElementById('progress-percent');
const progressLabel = document.getElementById('progress-label');
const comparisonContainer = document.getElementById('comparison-container');
const originalImage = document.getElementById('original-image');
const svgOutputContainer = document.getElementById('svg-output');
const downloadSection = document.getElementById('download-section');
const downloadSvgBtn = document.getElementById('download-svg-btn');
const copySvgBtn = document.getElementById('copy-svg-btn');
const statsSection = document.getElementById('stats-section');
const originalSizeEl = document.getElementById('original-size-el');
const svgSizeEl = document.getElementById('svg-size-el');
const reductionEl = document.getElementById('reduction-el');
const sampleBtn = document.getElementById('sample-btn');
const resetBtn = document.getElementById('reset-btn');
const svgPreviewSection = document.getElementById('svg-preview-section');
const svgCodeEl = document.getElementById('svg-code-el');
const detailSlider = document.getElementById('detail-slider');
const detailValue = document.getElementById('detail-value');
const colorSelect = document.getElementById('color-select');
const smoothCheckbox = document.getElementById('smooth-checkbox');
// Comparison Slider Elements
const comparisonSlider = comparisonContainer.querySelector('.comparison-slider');
const sliderHandle = comparisonContainer.querySelector('.slider-handle');
// State variables
let originalFile = null;
let originalImageDataUrl = null; // Store the Data URL for Potrace
let svgData = null;
let imageWidth = 0;
let imageHeight = 0;
let isDraggingSlider = false;
// --- Initialization ---
detailValue.textContent = detailSlider.value; // Initial display
// --- Event listeners ---
browseBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
dropzone.addEventListener('dragover', handleDragOver, false);
dropzone.addEventListener('dragleave', handleDragLeave, false);
dropzone.addEventListener('drop', handleDrop, false);
convertBtn.addEventListener('click', convertToSvg);
downloadSvgBtn.addEventListener('click', downloadSvgFile);
copySvgBtn.addEventListener('click', copySvgToClipboard);
sampleBtn.addEventListener('click', loadSampleImage);
resetBtn.addEventListener('click', resetConverter);
detailSlider.addEventListener('input', () => {
detailValue.textContent = detailSlider.value;
});
// Comparison Slider Logic
sliderHandle.addEventListener('mousedown', startSliderDrag);
document.addEventListener('mousemove', dragSlider);
document.addEventListener('mouseup', stopSliderDrag);
sliderHandle.addEventListener('touchstart', startSliderDrag, { passive: false }); // Use non-passive for preventDefault
document.addEventListener('touchmove', dragSlider, { passive: false }); // Use non-passive for preventDefault
document.addEventListener('touchend', stopSliderDrag);
// --- Core Functions ---
function handleFileSelect(e) {
const file = e.target.files?.[0];
if (!file) return;
processImageFile(file);
// Reset input value so selecting the same file again triggers 'change'
e.target.value = null;
}
function handleDragOver(e) {
e.preventDefault(); // Necessary to allow drop
e.stopPropagation();
dropzone.classList.add('active');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
dropzone.classList.remove('active');
}
function handleDrop(e) {
e.preventDefault(); // Prevent default browser behavior (opening file)
e.stopPropagation();
dropzone.classList.remove('active');
const file = e.dataTransfer?.files?.[0];
if (file) {
processImageFile(file);
} else {
console.warn("No file found in drop event.");
}
}
function processImageFile(file) {
if (!file.type.match('image/(png|jpeg|webp|bmp)')) {
alert('Unsupported file type. Please use PNG, JPG, WebP, or BMP.');
resetInput(); // Clear any invalid selection
return;
}
originalFile = file;
const reader = new FileReader();
// Show temporary loading state while reading file
fileInfo.textContent = `Loading ${file.name}...`;
convertBtn.disabled = true;
resetResultsUI(); // Clear previous results immediately
reader.onload = function(e) {
originalImageDataUrl = e.target.result; // Store Data URL
const img = new Image();
img.onload = () => {
imageWidth = img.width;
imageHeight = img.height;
// Set original image source for comparison view
originalImage.src = originalImageDataUrl;
// Adjust container height dynamically or keep fixed? Keeping fixed for now.
// comparisonContainer.style.height = `${Math.min(300, imageHeight)}px`; // Example dynamic height adjustment
// Update UI
fileInfo.textContent = `Selected: ${file.name} (${imageWidth}x${imageHeight})`;
originalSizeEl.textContent = formatFileSize(file.size);
convertBtn.disabled = false; // Enable conversion
previewPlaceholder.classList.add('hidden'); // Hide placeholder
comparisonContainer.classList.remove('hidden'); // Show initial comparison view (original only)
comparisonSlider.style.width = '0%'; // Start slider showing only original
svgOutputContainer.innerHTML = ''; // Clear any old SVG output visually
};
img.onerror = () => {
alert('Could not load image dimensions. The file might be corrupted.');
resetInput();
};
img.src = originalImageDataUrl; // Load image to get dimensions
};
reader.onerror = () => {
alert('Error reading file.');
resetInput();
};
reader.readAsDataURL(file); // Read file as Data URL
}
async function convertToSvg() {
if (!originalFile || !originalImageDataUrl) {
alert('Please select an image first');
return;
}
// --- UI Updates: Start Conversion ---
convertBtn.disabled = true;
loadingIndicator.classList.remove('hidden');
progressContainer.classList.remove('hidden');
previewPlaceholder.classList.add('hidden'); // Ensure placeholder is hidden
comparisonContainer.classList.add('hidden'); // Hide comparison during processing
downloadSection.classList.add('hidden');
statsSection.classList.add('hidden');
svgPreviewSection.classList.add('hidden');
updateProgress(0, "Initializing...");
svgData = null; // Reset SVG data state
// --- Get Settings ---
const numColors = parseInt(colorSelect.value);
const turdSize = parseInt(detailSlider.value);
const optimizeCurves = smoothCheckbox.checked;
try {
// --- Check & Load Potrace WASM ---
// THIS IS THE CRITICAL CHECK
if (typeof PotraceWasm === 'undefined') {
// If this error happens, the script from the CDN likely failed to load or execute.
console.error("PotraceWasm global object is not defined. Script load failed?");
throw new Error("PotraceWasm library failed to load. Check browser console and network connection.");
}
updateProgress(5, "Loading converter module...");
// PotraceWasm.load() might return a promise, ensure it's awaited.
// It handles loading the actual WASM binary.
await PotraceWasm.load();
updateProgress(10, "Processing image data...");
// --- Prepare parameters for potrace-wasm ---
const params = {
posterize: true,
steps: numColors,
turdPolicy: PotraceWasm.TURD_SMOOTH, // Common policy for smoothing speckles
turdSize: turdSize, // Pixels: 0 keeps everything, >0 removes smaller areas
alphaMax: optimizeCurves ? 1.0 : 0, // Corner smoothing threshold (0 = sharp corners)
optCurve: optimizeCurves, // Enable Bezier curve optimization
optTolerance: optimizeCurves ? 0.2 : 0, // How much curve optimization can deviate
turnPolicy: PotraceWasm.TURN_MINORITY, // How to resolve ambiguities at path turns
// background: '#ffffff', // Optional: Set explicit background (usually transparent is fine)
// fillStrategy: PotraceWasm.FILL_REMOVE_LAST, // Experiment if needed
// rangeDistribution: PotraceWasm.RANGE_AUTO, // Experiment if needed
};
updateProgress(20, `Tracing ${numColors} colors (turdSize=${turdSize})...`);
// --- Perform Conversion ---
// Use the stored Data URL
const result = await PotraceWasm.trace(originalImageDataUrl, params);
updateProgress(90, "Cleaning & preparing SVG...");
// Store and slightly clean the SVG data
svgData = result.replace(/<!--[\s\S]*?-->/g, '').trim(); // Remove comments and trim whitespace
// --- Display Results ---
displayResults(svgData);
updateProgress(100, "Complete");
} catch (error) {
console.error('SVG Conversion Error:', error); // Log the full error object
const errorMsg = (error instanceof Error) ? error.message : String(error);
alert(`Conversion failed: ${errorMsg}`);
updateProgress(0, "Error");
// Ensure loading/progress indicators reflect the error state
loadingIndicator.classList.add('hidden');
progressContainer.classList.remove('hidden'); // Keep progress bar visible showing error state
previewPlaceholder.classList.remove('hidden'); // Show placeholder again on error
comparisonContainer.classList.add('hidden'); // Ensure comparison view is hidden on error
} finally {
// --- UI Updates: End Conversion ---
// Re-enable button only if an image is still loaded
convertBtn.disabled = !originalFile;
loadingIndicator.classList.add('hidden');
// Hide progress bar only on success after a delay
if (svgData) {
setTimeout(() => {
progressContainer.classList.add('hidden');
}, 1500);
} else {
// Don't automatically hide progress bar on error
}
}
}
function displayResults(generatedSvgData) {
// Inject SVG content into the output container
svgOutputContainer.innerHTML = generatedSvgData;
// Make the comparison view visible and hide placeholders/loaders
comparisonContainer.classList.remove('hidden');
previewPlaceholder.classList.add('hidden');
loadingIndicator.classList.add('hidden');
comparisonSlider.style.width = '50%'; // Reset slider to midpoint
// Show download & stats sections
downloadSection.classList.remove('hidden');
statsSection.classList.remove('hidden');
// Calculate and display stats
const svgSizeBytes = new Blob([generatedSvgData]).size;
svgSizeEl.textContent = formatFileSize(svgSizeBytes);
if (originalFile && originalFile.size > 0) {
const reductionPercent = Math.max(0, (1 - svgSizeBytes / originalFile.size) * 100).toFixed(1);
reductionEl.textContent = `${reductionPercent}%`;
} else {
reductionEl.textContent = '-';
}
// Display SVG code preview
svgCodeEl.textContent = generatedSvgData; // Use textContent for security in <pre>
svgPreviewSection.classList.remove('hidden');
}
function updateProgress(percent, label = "Processing...") {
const p = Math.max(0, Math.min(100, Math.round(percent)));
progressBar.style.width = `${p}%`;
progressPercent.textContent = `${p}%`;
progressLabel.textContent = label;
}
function downloadSvgFile() {
if (!svgData) {
alert("No SVG data available to download.");
return;
}
try {
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (originalFile?.name || 'converted-image').replace(/\.[^/.]+$/, '') + '.svg';
document.body.appendChild(a); // Required for Firefox
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error("Download failed:", error);
alert("Could not initiate download. Please try copying the code.");
}
}
function copySvgToClipboard() {
if (!svgData) {
alert("No SVG data available to copy.");
return;
}
navigator.clipboard.writeText(svgData)
.then(() => {
const originalText = copySvgBtn.querySelector('span').textContent;
const icon = copySvgBtn.querySelector('i');
const originalIconClass = icon.className;
copySvgBtn.querySelector('span').textContent = 'Copied!';
icon.className = 'fas fa-check text-green-500'; // Change icon to checkmark
copySvgBtn.classList.add('bg-green-100');
setTimeout(() => {
copySvgBtn.querySelector('span').textContent = originalText;
icon.className = originalIconClass; // Restore original icon
copySvgBtn.classList.remove('bg-green-100');
}, 2000);
})
.catch(err => {
console.error('Failed to copy SVG: ', err);
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.');
});
}
function loadSampleImage() {
resetConverter(); // Reset first
// Using a smaller image for quicker sample loading/processing by default
const sampleImageUrl = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80'; // Example: Shoe
// const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Example: Landscape
// Show immediate feedback
loadingIndicator.classList.remove('hidden');
previewPlaceholder.classList.add('hidden');
comparisonContainer.classList.add('hidden'); // Hide comparison view during load
fileInfo.textContent = "Loading sample image...";
convertBtn.disabled = true;
fetch(sampleImageUrl)
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.blob();
})
.then(blob => {
// Guess file extension from mime type if possible
let imageType = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/jpeg';
const extension = imageType.split('/')[1] || 'jpg';
const fileName = `sample-image.${extension}`;
const file = new File([blob], fileName, { type: imageType });
processImageFile(file); // Process the fetched image blob as a file
})
.catch(error => {
console.error('Error loading sample image:', error);
alert(`Failed to load sample image: ${error.message}. Please try uploading manually.`);
resetConverter(); // Reset fully on error
})
.finally(() => {
loadingIndicator.classList.add('hidden'); // Hide loading indicator regardless of outcome
});
}
// --- Reset Functions ---
function resetInput() {
fileInput.value = ''; // Clear file input
originalFile = null;
originalImageDataUrl = null;
imageWidth = 0;
imageHeight = 0;
convertBtn.disabled = true; // Disable convert button
fileInfo.textContent = ''; // Clear file info text
originalImage.src = ''; // Clear original image preview
}
function resetResultsUI() {
svgData = null;
svgOutputContainer.innerHTML = ''; // Clear SVG preview
comparisonContainer.classList.add('hidden'); // Hide comparison slider view
previewPlaceholder.classList.remove('hidden'); // Show the initial placeholder
downloadSection.classList.add('hidden');
statsSection.classList.add('hidden');
svgPreviewSection.classList.add('hidden');
originalSizeEl.textContent = '-'; // Reset stats
svgSizeEl.textContent = '-';
reductionEl.textContent = '-';
svgCodeEl.textContent = ''; // Clear SVG code view
progressContainer.classList.add('hidden'); // Hide progress bar
updateProgress(0); // Reset progress values
loadingIndicator.classList.add('hidden'); // Hide loader
}
function resetConverter() {
resetInput(); // Clear input-related things
resetResultsUI(); // Clear output/result related things
// Reset form controls to default values
detailSlider.value = 2;
detailValue.textContent = '2';
colorSelect.value = '16'; // Default color selection
smoothCheckbox.checked = true;
console.log('Converter Reset');
}
// --- Utility Functions ---
function formatFileSize(bytes) {
if (bytes == null || typeof bytes !== 'number' || bytes < 0) return 'N/A';
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// --- Comparison Slider Functions ---
function startSliderDrag(e) {
// Prevent default only for touch to avoid scrolling page while dragging
if (e.type === 'touchstart') e.preventDefault();
isDraggingSlider = true;
comparisonContainer.style.cursor = 'ew-resize';
}
function dragSlider(e) {
if (!isDraggingSlider) return;
// Prevent default only for touch to avoid scrolling page while dragging
if (e.type === 'touchmove') e.preventDefault();
const rect = comparisonContainer.getBoundingClientRect();
// Use touch or mouse coordinates
const clientX = e.clientX ?? e.touches?.[0]?.clientX;
if (typeof clientX === 'undefined') return; // Exit if no coordinate data
let offsetX = clientX - rect.left;
// Clamp the offset to be within the container bounds
let newWidth = Math.max(0, Math.min(rect.width, offsetX));
let percentWidth = (newWidth / rect.width) * 100;
comparisonSlider.style.width = `${percentWidth}%`;
}
function stopSliderDrag() {
if (isDraggingSlider) {
isDraggingSlider = false;
comparisonContainer.style.cursor = 'default'; // Restore default cursor
}
}
}); // End DOMContentLoaded
</script>
</body>
</html>