tosvg / index.html
thelip's picture
Update index.html
fda99b1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Robust Photo to SVG Converter</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Use jsDelivr CDN for potentially better reliability -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/potrace-wasm.js" defer></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* General */
body { font-family: sans-serif; }
/* Dropzone */
.dropzone { border: 3px dashed #cbd5e1; transition: all 0.3s ease; background-color: #f8fafc; }
.dropzone.active { border-color: #3b82f6; background-color: #eff6ff; }
/* Loader */
.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; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Comparison Slider */
.comparison-container { position: relative; overflow: hidden; user-select: none; background-color: #f3f4f6; aspect-ratio: 4 / 3; /* Default aspect ratio */ }
.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); }
.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); }
.comparison-handle::before, .comparison-handle::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); width: 2px; height: 8px; background-color: white; border-radius: 1px; }
.comparison-handle::before { top: 10px; }
.comparison-handle::after { bottom: 10px; }
/* Ensure images/SVG scale nicely */
.comparison-container img,
.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; }
/* SVG Preview Background */
.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; }
/* Tooltip */
.tooltip { position: relative; display: inline-block; }
.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; }
.tooltip:hover .tooltip-text { visibility: visible; opacity: 1; }
/* Code Preview */
#svg-code-container { background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.375rem; max-height: 250px; overflow: auto; padding: 0.75rem; }
#svg-code-el { font-family: monospace; font-size: 0.8rem; color: #1f2937; white-space: pre-wrap; word-break: break-all; }
</style>
</head>
<body class="bg-gray-100 min-h-screen p-4 md:p-8">
<div class="container mx-auto max-w-6xl bg-white rounded-lg shadow-xl overflow-hidden">
<header class="bg-gray-800 text-white p-4 text-center">
<h1 class="text-2xl font-bold">Photo to SVG Converter</h1>
<p class="text-sm text-gray-300">Convert raster images to colorful vector graphics</p>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
<!-- Left Column: Upload & Settings -->
<section class="space-y-6">
<div>
<h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">1. Upload Image</h2>
<div id="dropzone" class="dropzone rounded-lg p-6 text-center cursor-pointer">
<div class="flex flex-col items-center justify-center space-y-2 text-gray-600">
<i class="fas fa-upload text-4xl text-blue-500"></i>
<p class="font-medium">Drag & drop image here</p>
<p class="text-sm">or</p>
<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">
Browse Files
</button>
<input type="file" id="file-input" class="hidden" accept="image/png, image/jpeg, image/webp, image/bmp">
</div>
</div>
<div id="file-info" class="mt-2 text-sm text-gray-600 h-5"></div> <!-- Placeholder for text -->
<div class="mt-2 flex justify-end space-x-2">
<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>
<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>
</div>
</div>
<div id="settings-block" class="space-y-4 opacity-50 pointer-events-none"> <!-- Disabled initially -->
<h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">2. Configure Conversion</h2>
<div>
<label for="color-count-select" class="block text-sm font-medium text-gray-700 mb-1 flex justify-between items-center">
Number of Colors
<span class="tooltip">
<i class="fas fa-info-circle text-gray-400 cursor-help"></i>
<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>
</span>
</label>
<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">
<option value="8">8 Colors (Fast, Abstract)</option>
<option value="16" selected>16 Colors</option>
<option value="32">32 Colors</option>
<option value="64">64 Colors (Detailed)</option>
<option value="128">128 Colors (Very Detailed, Slow)</option>
</select>
</div>
<div>
<label for="detail-slider" class="block text-sm font-medium text-gray-700 mb-1 flex justify-between items-center">
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>
<span class="tooltip">
<i class="fas fa-info-circle text-gray-400 cursor-help"></i>
<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>
</span>
</label>
<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">
</div>
<div class="flex items-center pt-2">
<input id="smooth-curves-checkbox" type="checkbox" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="smooth-curves-checkbox" class="ml-2 block text-sm font-medium text-gray-700">Smooth Curves (Recommended)</label>
<span class="tooltip ml-2">
<i class="fas fa-info-circle text-gray-400 cursor-help"></i>
<span class="tooltip-text">Optimizes the curves in the SVG paths for smoother results. Slightly increases processing time. (Potrace 'opticurve')</span>
</span>
</div>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">3. Generate SVG</h2>
<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>
<i class="fas fa-magic"></i>
<span>Convert to SVG</span>
</button>
<div id="progress-container" class="hidden mt-3">
<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">
<div id="progress-bar" class="bg-blue-600 h-2 rounded-full transition-width duration-150 ease-linear" style="width: 0%"></div>
</div>
</div>
</div>
</section>
<!-- Right Column: Preview & Results -->
<section class="space-y-6">
<h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">Preview & Results</h2>
<!-- Loading Indicator -->
<div id="loading-indicator" class="hidden text-center py-10">
<div class="loader"></div>
<p class="text-gray-600 mt-2 animate-pulse">Generating SVG, please wait...</p>
<p class="text-xs text-gray-500">(Complex images may take some time)</p>
</div>
<!-- Placeholder -->
<div id="preview-placeholder" class="svg-bg-pattern rounded-lg flex items-center justify-center text-center p-4" style="min-height: 300px;">
<p class="text-gray-500">Upload an image to begin</p>
</div>
<!-- Comparison Viewer -->
<div id="comparison-container" class="comparison-container hidden svg-bg-pattern rounded-lg overflow-hidden" style="min-height: 300px;">
<!-- Base Layer: Original Image -->
<div class="absolute inset-0 flex items-center justify-center">
<img id="original-image-preview" src="#" alt="Original" class="opacity-0 transition-opacity duration-300">
</div>
<!-- Top Layer: SVG Output with Slider -->
<div class="comparison-slider-wrapper">
<div id="svg-output-wrapper" class="absolute inset-0 bg-white flex items-center justify-center">
<!-- SVG will be injected here -->
</div>
<div class="comparison-handle"></div>
</div>
</div>
<!-- Download & Copy Actions -->
<div id="results-actions" class="hidden bg-green-50 p-4 rounded-lg border border-green-200">
<h3 class="text-sm font-medium text-green-800 mb-3">Success!</h3>
<div class="flex flex-col sm:flex-row gap-3">
<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">
<i class="fas fa-download"></i>
<span>Download SVG</span>
</button>
<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">
<i class="far fa-copy"></i>
<span data-original-text="Copy SVG Code">Copy SVG Code</span>
</button>
</div>
</div>
<!-- Stats Display -->
<div id="stats-section" class="hidden bg-gray-50 p-3 rounded-lg border border-gray-200">
<div class="grid grid-cols-3 gap-3 text-center">
<div>
<p class="text-xs text-gray-500">Original Size</p>
<p id="original-size-el" class="font-medium text-sm text-gray-800">-</p>
</div>
<div>
<p class="text-xs text-gray-500">SVG Size</p>
<p id="svg-size-el" class="font-medium text-sm text-gray-800">-</p>
</div>
<div>
<p class="text-xs text-gray-500">Reduction</p>
<p id="reduction-el" class="font-medium text-sm text-green-600">-</p>
</div>
</div>
</div>
<!-- SVG Code Preview -->
<div id="svg-code-preview-section" class="hidden">
<h3 class="text-md font-semibold text-gray-700 mb-2">Generated SVG Code</h3>
<div id="svg-code-container">
<pre id="svg-code-el"></pre>
</div>
</div>
</section>
</div> <!-- End Grid -->
</div> <!-- End Container -->
<!-- DeepSite Footer (Optional) -->
<p style="font-size: 11px; color: #555; text-align: center; margin-top: 1rem; padding-bottom: 0.5rem;">Powered by Potrace WASM</p>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References ---
const elements = {
dropzone: document.getElementById('dropzone'),
fileInput: document.getElementById('file-input'),
browseBtn: document.getElementById('browse-btn'),
fileInfo: document.getElementById('file-info'),
sampleBtn: document.getElementById('sample-btn'),
resetBtn: document.getElementById('reset-btn'),
settingsBlock: document.getElementById('settings-block'),
colorCountSelect: document.getElementById('color-count-select'),
detailSlider: document.getElementById('detail-slider'),
detailValueLabel: document.getElementById('detail-value-label'),
smoothCurvesCheckbox: document.getElementById('smooth-curves-checkbox'),
convertBtn: document.getElementById('convert-btn'),
progressContainer: document.getElementById('progress-container'),
progressLabel: document.getElementById('progress-label'),
progressPercent: document.getElementById('progress-percent'),
progressBar: document.getElementById('progress-bar'),
loadingIndicator: document.getElementById('loading-indicator'),
previewPlaceholder: document.getElementById('preview-placeholder'),
comparisonContainer: document.getElementById('comparison-container'),
originalImagePreview: document.getElementById('original-image-preview'),
svgOutputWrapper: document.getElementById('svg-output-wrapper'),
comparisonSliderWrapper: document.querySelector('.comparison-slider-wrapper'),
comparisonHandle: document.querySelector('.comparison-handle'),
resultsActions: document.getElementById('results-actions'),
downloadSvgBtn: document.getElementById('download-svg-btn'),
copySvgBtn: document.getElementById('copy-svg-btn'),
copySvgBtnText: document.querySelector('#copy-svg-btn span'),
statsSection: document.getElementById('stats-section'),
originalSizeEl: document.getElementById('original-size-el'),
svgSizeEl: document.getElementById('svg-size-el'),
reductionEl: document.getElementById('reduction-el'),
svgCodePreviewSection: document.getElementById('svg-code-preview-section'),
svgCodeEl: document.getElementById('svg-code-el'),
};
// --- State Variables ---
let originalFile = null;
let originalImageDataUrl = null;
let svgResultData = null;
let isSliderDragging = false;
let originalImageDimensions = { width: 0, height: 0 };
// --- Utility Functions ---
const 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]}`;
};
const updateProgress = (percent, label = "Processing...") => {
const p = Math.max(0, Math.min(100, Math.round(percent)));
elements.progressBar.style.width = `${p}%`;
elements.progressPercent.textContent = `${p}%`;
elements.progressLabel.textContent = label;
elements.progressContainer.classList.remove('hidden');
};
const showAlert = (message, type = 'error') => {
// Simple alert for now, could be replaced with a nicer modal/toast
console[type === 'error' ? 'error' : 'warn'](message);
alert(`[${type.toUpperCase()}] ${message}`);
};
// --- UI State Management ---
const showLoadingState = (isLoading) => {
elements.loadingIndicator.classList.toggle('hidden', !isLoading);
elements.convertBtn.disabled = isLoading;
// Hide preview areas during loading
if (isLoading) {
elements.previewPlaceholder.classList.add('hidden');
elements.comparisonContainer.classList.add('hidden');
elements.resultsActions.classList.add('hidden');
elements.statsSection.classList.add('hidden');
elements.svgCodePreviewSection.classList.add('hidden');
}
};
const enableSettings = (isEnabled) => {
elements.settingsBlock.classList.toggle('opacity-50', !isEnabled);
elements.settingsBlock.classList.toggle('pointer-events-none', !isEnabled);
elements.convertBtn.disabled = !isEnabled || !originalFile; // Also check if file exists
};
const resetUI = (fullReset = true) => {
console.log('Resetting UI...');
showLoadingState(false);
elements.progressContainer.classList.add('hidden');
elements.previewPlaceholder.classList.remove('hidden');
elements.comparisonContainer.classList.add('hidden');
elements.resultsActions.classList.add('hidden');
elements.statsSection.classList.add('hidden');
elements.svgCodePreviewSection.classList.add('hidden');
elements.fileInfo.textContent = '';
elements.originalImagePreview.src = '#'; // Clear image src
elements.originalImagePreview.classList.add('opacity-0');
elements.svgOutputWrapper.innerHTML = '';
elements.svgCodeEl.textContent = '';
elements.originalSizeEl.textContent = '-';
elements.svgSizeEl.textContent = '-';
elements.reductionEl.textContent = '-';
elements.copySvgBtnText.textContent = elements.copySvgBtnText.dataset.originalText; // Reset copy button text
elements.comparisonSliderWrapper.style.width = '50%'; // Reset slider visual
if (fullReset) {
originalFile = null;
originalImageDataUrl = null;
svgResultData = null;
originalImageDimensions = { width: 0, height: 0 };
elements.fileInput.value = ''; // Clear file input selection
// Reset settings to defaults
elements.colorCountSelect.value = '16';
elements.detailSlider.value = '2';
elements.detailValueLabel.textContent = '2';
elements.smoothCurvesCheckbox.checked = true;
enableSettings(false); // Disable settings block
} else {
// Partial reset (e.g., before conversion starts)
svgResultData = null;
enableSettings(true); // Keep settings enabled if file is loaded
}
};
// --- Event Handlers ---
const handleFileSelect = (file) => {
if (!file) return;
if (!file.type.match('image/(png|jpeg|webp|bmp)')) {
showAlert('Invalid file type. Please select a PNG, JPG, WebP, or BMP image.');
resetUI(true); // Full reset if invalid file
return;
}
resetUI(false); // Reset results, keep settings potentially enabled
originalFile = file;
elements.fileInfo.textContent = `Loading ${file.name}...`;
enableSettings(false); // Disable settings while reading
const reader = new FileReader();
reader.onload = (e) => {
originalImageDataUrl = e.target.result;
const img = new Image();
img.onload = () => {
originalImageDimensions = { width: img.width, height: img.height };
elements.fileInfo.textContent = `${originalFile.name} (${img.width}x${img.height})`;
elements.originalSizeEl.textContent = formatFileSize(originalFile.size);
elements.originalImagePreview.src = originalImageDataUrl;
elements.originalImagePreview.classList.remove('opacity-0');
// Adjust comparison container aspect ratio (optional but nice)
const aspectRatio = img.width / img.height;
elements.comparisonContainer.style.aspectRatio = `${aspectRatio}`;
elements.previewPlaceholder.classList.add('hidden');
// Don't show comparison container yet, wait for conversion attempt
enableSettings(true); // Enable settings and convert button
};
img.onerror = () => {
showAlert('Could not read image dimensions. File may be corrupt.');
resetUI(true);
};
img.src = originalImageDataUrl;
};
reader.onerror = () => {
showAlert('Failed to read the file.');
resetUI(true);
};
reader.readAsDataURL(file);
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
elements.dropzone.classList.remove('active');
const file = e.dataTransfer?.files?.[0];
if (file) {
handleFileSelect(file);
}
};
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
elements.dropzone.classList.add('active');
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
elements.dropzone.classList.remove('active');
};
const convertToSvg = async () => {
if (!originalFile || !originalImageDataUrl) {
showAlert('Please upload an image first.');
return;
}
resetResultsUI(); // Clear previous results before starting
showLoadingState(true);
updateProgress(0, "Initializing...");
const params = {
steps: parseInt(elements.colorCountSelect.value, 10),
turdSize: parseInt(elements.detailSlider.value, 10),
optCurve: elements.smoothCurvesCheckbox.checked,
// --- Standard Potrace Params (Refer to potrace-wasm docs for more) ---
posterize: true, // Essential for color tracing
turdPolicy: PotraceWasm.TURD_SMOOTH,
alphaMax: elements.smoothCurvesCheckbox.checked ? 1.0 : 0, // Adjusts smoothness (corner threshold)
optTolerance: elements.smoothCurvesCheckbox.checked ? 0.2 : 0, // Optimization tolerance
turnPolicy: PotraceWasm.TURN_MINORITY,
// background: '#FFFFFF', // Can set explicit background if needed
};
try {
if (typeof PotraceWasm === 'undefined') {
throw new Error("PotraceWasm library is not available. Check network connection or browser console.");
}
updateProgress(5, "Loading WASM module...");
await PotraceWasm.load(); // Ensure WASM binary is loaded & ready
updateProgress(15, `Tracing ${params.steps} colors...`);
console.log("Potrace Parameters:", params);
const svgString = await PotraceWasm.trace(originalImageDataUrl, params);
updateProgress(95, "Finalizing SVG...");
svgResultData = svgString.replace(/<!--[\s\S]*?-->/g, '').trim(); // Clean comments
displayResults();
updateProgress(100, "Complete!");
setTimeout(() => elements.progressContainer.classList.add('hidden'), 1500); // Hide progress after success
} catch (error) {
console.error("SVG Conversion Error:", error);
showAlert(`Conversion failed: ${error.message || error}`);
resetResultsUI(true); // Show placeholder again on error
elements.progressContainer.classList.remove('hidden'); // Keep progress bar visible
updateProgress(0, "Error!"); // Show error status
} finally {
showLoadingState(false); // Hide loader, re-enable button
}
};
const displayResults = () => {
if (!svgResultData) return;
// Inject SVG
elements.svgOutputWrapper.innerHTML = svgResultData;
// Show relevant sections
elements.comparisonContainer.classList.remove('hidden');
elements.resultsActions.classList.remove('hidden');
elements.statsSection.classList.remove('hidden');
elements.svgCodePreviewSection.classList.remove('hidden');
// Calculate and show stats
const svgSizeBytes = new Blob([svgResultData]).size;
elements.svgSizeEl.textContent = formatFileSize(svgSizeBytes);
if (originalFile && originalFile.size > 0) {
const reductionPercent = Math.max(0, (1 - svgSizeBytes / originalFile.size) * 100);
elements.reductionEl.textContent = `${reductionPercent.toFixed(1)}%`;
elements.reductionEl.classList.toggle('text-red-600', reductionPercent < 0); // Red if larger
elements.reductionEl.classList.toggle('text-green-600', reductionPercent >= 0);
} else {
elements.reductionEl.textContent = '-';
}
// Show SVG code
elements.svgCodeEl.textContent = svgResultData;
// Ensure comparison container is visible and placeholder is hidden
elements.previewPlaceholder.classList.add('hidden');
elements.loadingIndicator.classList.add('hidden');
};
const downloadSvg = () => {
if (!svgResultData) {
showAlert("No SVG generated yet.", "warning");
return;
}
try {
const blob = new Blob([svgResultData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${originalFile.name.replace(/\.[^/.]+$/, '')}_${elements.colorCountSelect.value}colors.svg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
showAlert("Failed to initiate download.", "error");
console.error("Download error:", error);
}
};
const copySvgCode = () => {
if (!svgResultData) {
showAlert("No SVG generated yet.", "warning");
return;
}
navigator.clipboard.writeText(svgResultData).then(() => {
const originalText = elements.copySvgBtnText.dataset.originalText;
elements.copySvgBtnText.textContent = 'Copied!';
elements.copySvgBtn.classList.add('bg-green-100');
setTimeout(() => {
elements.copySvgBtnText.textContent = originalText;
elements.copySvgBtn.classList.remove('bg-green-100');
}, 2000);
}).catch(err => {
showAlert('Failed to copy text. Please try manually.', 'error');
console.error('Clipboard copy error:', err);
});
};
const loadSample = () => {
console.log("Loading sample image...");
resetUI(true); // Full reset before loading sample
showLoadingState(true); // Show loader while fetching
elements.fileInfo.textContent = "Fetching sample image...";
// Sample image URL (consider using a smaller one for faster testing)
const sampleUrl = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Shoe example
fetch(sampleUrl)
.then(response => {
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
return response.blob();
})
.then(blob => {
const fileName = 'sample_image.jpg'; // Give it a name
const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
showLoadingState(false); // Hide loader before processing
handleFileSelect(file); // Process the fetched blob as a file
})
.catch(error => {
showLoadingState(false);
showAlert(`Failed to load sample image: ${error.message}`, 'error');
resetUI(true);
});
};
// --- Slider Logic ---
const startDrag = (e) => {
if (e.target !== elements.comparisonHandle) return;
if (e.type === 'touchstart') e.preventDefault(); // Prevent scroll on touch
isSliderDragging = true;
elements.comparisonContainer.style.cursor = 'ew-resize';
};
const drag = (e) => {
if (!isSliderDragging) return;
if (e.type === 'touchmove') e.preventDefault(); // Prevent scroll on touch
const rect = elements.comparisonContainer.getBoundingClientRect();
const clientX = e.clientX ?? e.touches?.[0]?.clientX;
if (typeof clientX === 'undefined') return;
let offsetX = clientX - rect.left;
let newWidthPercent = Math.max(0, Math.min(100, (offsetX / rect.width) * 100));
elements.comparisonSliderWrapper.style.width = `${newWidthPercent}%`;
};
const stopDrag = () => {
if (isSliderDragging) {
isSliderDragging = false;
elements.comparisonContainer.style.cursor = 'default';
}
};
// --- Event Listener Setup ---
elements.browseBtn.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', (e) => handleFileSelect(e.target.files[0]));
elements.dropzone.addEventListener('dragover', handleDragOver);
elements.dropzone.addEventListener('dragleave', handleDragLeave);
elements.dropzone.addEventListener('drop', handleDrop);
elements.convertBtn.addEventListener('click', convertToSvg);
elements.downloadSvgBtn.addEventListener('click', downloadSvg);
elements.copySvgBtn.addEventListener('click', copySvgCode);
elements.sampleBtn.addEventListener('click', loadSample);
elements.resetBtn.addEventListener('click', () => resetUI(true));
elements.detailSlider.addEventListener('input', (e) => {
elements.detailValueLabel.textContent = e.target.value;
});
// Slider Listeners
elements.comparisonContainer.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag); // Listen on document for wider drag area
document.addEventListener('mouseup', stopDrag);
elements.comparisonContainer.addEventListener('touchstart', startDrag, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', stopDrag);
// --- Initial Setup ---
resetUI(true); // Start with a clean slate
elements.copySvgBtnText.dataset.originalText = elements.copySvgBtnText.textContent; // Store original button text
}); // End DOMContentLoaded
</script>
</body>
</html>