|
<!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> |
|
|
|
<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> |
|
|
|
body { font-family: sans-serif; } |
|
|
|
.dropzone { border: 3px dashed #cbd5e1; transition: all 0.3s ease; background-color: #f8fafc; } |
|
.dropzone.active { border-color: #3b82f6; background-color: #eff6ff; } |
|
|
|
.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-container { position: relative; overflow: hidden; user-select: none; background-color: #f3f4f6; aspect-ratio: 4 / 3; } |
|
.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; } |
|
|
|
.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-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 { 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; } |
|
|
|
#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"> |
|
|
|
|
|
<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> |
|
<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"> |
|
<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> |
|
|
|
|
|
<section class="space-y-6"> |
|
<h2 class="text-lg font-semibold text-gray-700 mb-2 border-b pb-1">Preview & Results</h2> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<div id="comparison-container" class="comparison-container hidden svg-bg-pattern rounded-lg overflow-hidden" style="min-height: 300px;"> |
|
|
|
<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> |
|
|
|
<div class="comparison-slider-wrapper"> |
|
<div id="svg-output-wrapper" class="absolute inset-0 bg-white flex items-center justify-center"> |
|
|
|
</div> |
|
<div class="comparison-handle"></div> |
|
</div> |
|
</div> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
</div> |
|
|
|
|
|
<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', () => { |
|
|
|
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'), |
|
}; |
|
|
|
|
|
let originalFile = null; |
|
let originalImageDataUrl = null; |
|
let svgResultData = null; |
|
let isSliderDragging = false; |
|
let originalImageDimensions = { width: 0, height: 0 }; |
|
|
|
|
|
|
|
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') => { |
|
|
|
console[type === 'error' ? 'error' : 'warn'](message); |
|
alert(`[${type.toUpperCase()}] ${message}`); |
|
}; |
|
|
|
|
|
const showLoadingState = (isLoading) => { |
|
elements.loadingIndicator.classList.toggle('hidden', !isLoading); |
|
elements.convertBtn.disabled = isLoading; |
|
|
|
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; |
|
}; |
|
|
|
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 = '#'; |
|
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; |
|
elements.comparisonSliderWrapper.style.width = '50%'; |
|
|
|
|
|
if (fullReset) { |
|
originalFile = null; |
|
originalImageDataUrl = null; |
|
svgResultData = null; |
|
originalImageDimensions = { width: 0, height: 0 }; |
|
elements.fileInput.value = ''; |
|
|
|
elements.colorCountSelect.value = '16'; |
|
elements.detailSlider.value = '2'; |
|
elements.detailValueLabel.textContent = '2'; |
|
elements.smoothCurvesCheckbox.checked = true; |
|
enableSettings(false); |
|
} else { |
|
|
|
svgResultData = null; |
|
enableSettings(true); |
|
} |
|
}; |
|
|
|
|
|
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); |
|
return; |
|
} |
|
|
|
resetUI(false); |
|
originalFile = file; |
|
|
|
elements.fileInfo.textContent = `Loading ${file.name}...`; |
|
enableSettings(false); |
|
|
|
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'); |
|
|
|
|
|
const aspectRatio = img.width / img.height; |
|
elements.comparisonContainer.style.aspectRatio = `${aspectRatio}`; |
|
|
|
|
|
elements.previewPlaceholder.classList.add('hidden'); |
|
|
|
enableSettings(true); |
|
}; |
|
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(); |
|
showLoadingState(true); |
|
updateProgress(0, "Initializing..."); |
|
|
|
const params = { |
|
steps: parseInt(elements.colorCountSelect.value, 10), |
|
turdSize: parseInt(elements.detailSlider.value, 10), |
|
optCurve: elements.smoothCurvesCheckbox.checked, |
|
|
|
posterize: true, |
|
turdPolicy: PotraceWasm.TURD_SMOOTH, |
|
alphaMax: elements.smoothCurvesCheckbox.checked ? 1.0 : 0, |
|
optTolerance: elements.smoothCurvesCheckbox.checked ? 0.2 : 0, |
|
turnPolicy: PotraceWasm.TURN_MINORITY, |
|
|
|
}; |
|
|
|
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(); |
|
|
|
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(); |
|
|
|
displayResults(); |
|
updateProgress(100, "Complete!"); |
|
setTimeout(() => elements.progressContainer.classList.add('hidden'), 1500); |
|
|
|
} catch (error) { |
|
console.error("SVG Conversion Error:", error); |
|
showAlert(`Conversion failed: ${error.message || error}`); |
|
resetResultsUI(true); |
|
elements.progressContainer.classList.remove('hidden'); |
|
updateProgress(0, "Error!"); |
|
} finally { |
|
showLoadingState(false); |
|
} |
|
}; |
|
|
|
const displayResults = () => { |
|
if (!svgResultData) return; |
|
|
|
|
|
elements.svgOutputWrapper.innerHTML = svgResultData; |
|
|
|
|
|
elements.comparisonContainer.classList.remove('hidden'); |
|
elements.resultsActions.classList.remove('hidden'); |
|
elements.statsSection.classList.remove('hidden'); |
|
elements.svgCodePreviewSection.classList.remove('hidden'); |
|
|
|
|
|
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); |
|
elements.reductionEl.classList.toggle('text-green-600', reductionPercent >= 0); |
|
} else { |
|
elements.reductionEl.textContent = '-'; |
|
} |
|
|
|
|
|
elements.svgCodeEl.textContent = svgResultData; |
|
|
|
|
|
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); |
|
showLoadingState(true); |
|
elements.fileInfo.textContent = "Fetching sample image..."; |
|
|
|
|
|
const sampleUrl = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; |
|
|
|
fetch(sampleUrl) |
|
.then(response => { |
|
if (!response.ok) throw new Error(`HTTP error ${response.status}`); |
|
return response.blob(); |
|
}) |
|
.then(blob => { |
|
const fileName = 'sample_image.jpg'; |
|
const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' }); |
|
showLoadingState(false); |
|
handleFileSelect(file); |
|
}) |
|
.catch(error => { |
|
showLoadingState(false); |
|
showAlert(`Failed to load sample image: ${error.message}`, 'error'); |
|
resetUI(true); |
|
}); |
|
}; |
|
|
|
|
|
const startDrag = (e) => { |
|
if (e.target !== elements.comparisonHandle) return; |
|
if (e.type === 'touchstart') e.preventDefault(); |
|
isSliderDragging = true; |
|
elements.comparisonContainer.style.cursor = 'ew-resize'; |
|
}; |
|
|
|
const drag = (e) => { |
|
if (!isSliderDragging) return; |
|
if (e.type === 'touchmove') e.preventDefault(); |
|
|
|
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'; |
|
} |
|
}; |
|
|
|
|
|
|
|
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; |
|
}); |
|
|
|
|
|
elements.comparisonContainer.addEventListener('mousedown', startDrag); |
|
document.addEventListener('mousemove', drag); |
|
document.addEventListener('mouseup', stopDrag); |
|
elements.comparisonContainer.addEventListener('touchstart', startDrag, { passive: false }); |
|
document.addEventListener('touchmove', drag, { passive: false }); |
|
document.addEventListener('touchend', stopDrag); |
|
|
|
|
|
resetUI(true); |
|
elements.copySvgBtnText.dataset.originalText = elements.copySvgBtnText.textContent; |
|
|
|
}); |
|
</script> |
|
</body> |
|
</html> |