|
<!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> |
|
|
|
<script src="https://unpkg.com/[email protected]/dist/potrace-wasm.js"></script> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quantize.min.js"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.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; } |
|
.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; } |
|
.slider-handle { position: absolute; right: -6px; 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 { 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; } |
|
.tooltip:hover .tooltip-text { visibility: visible; opacity: 1; } |
|
|
|
.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); } } |
|
|
|
#svg-output svg { display: block; width: 100%; height: 100%; } |
|
</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 your photos into high-quality, realistic SVG vector graphics</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"> |
|
|
|
<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.</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 Level (Turd Size) <span id="detail-value" class="font-mono text-xs"></span> |
|
<span class="tooltip"> |
|
<i class="fas fa-info-circle text-gray-400"></i> |
|
<span class="tooltip-text">Controls smoothness vs detail. Higher values remove smaller 'speckles' (turds), potentially smoothing edges. Lower values keep more fine detail but can look noisy. Potrace 'turdsize' parameter.</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.</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> |
|
|
|
|
|
<div class="space-y-4"> |
|
<h2 class="text-xl font-semibold text-gray-800">3. Preview & Download</h2> |
|
|
|
<div id="preview-placeholder" class="bg-gray-100 rounded-lg flex items-center justify-center text-center p-4" style="min-height: 300px;"> |
|
<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> |
|
|
|
<div id="comparison-container" class="canvas-container hidden relative bg-gray-100 rounded-lg overflow-hidden" style="height: 300px; aspect-ratio: 4/3;"> |
|
<img id="original-image" class="absolute top-0 left-0 w-full h-full object-contain" src="" alt="Original"> |
|
<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 svg-preview"> |
|
|
|
</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" 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" 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" class="font-medium text-sm">-</p> |
|
</div> |
|
<div> |
|
<p class="text-xs text-gray-500">SVG Size</p> |
|
<p id="svg-size" class="font-medium text-sm">-</p> |
|
</div> |
|
<div> |
|
<p class="text-xs text-gray-500">Reduction</p> |
|
<p id="reduction" 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 class="svg-preview rounded-lg overflow-auto p-2 border border-gray-200" style="max-height: 200px;"> |
|
<pre id="svg-code" class="text-xs text-gray-800 whitespace-pre-wrap break-all"></pre> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
</div> |
|
|
|
|
|
<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() { |
|
|
|
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'); |
|
const copySvgBtn = document.getElementById('copy-svg'); |
|
const statsSection = document.getElementById('stats-section'); |
|
const originalSizeEl = document.getElementById('original-size'); |
|
const svgSizeEl = document.getElementById('svg-size'); |
|
const reductionEl = document.getElementById('reduction'); |
|
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'); |
|
const detailSlider = document.getElementById('detail-slider'); |
|
const detailValue = document.getElementById('detail-value'); |
|
const colorSelect = document.getElementById('color-select'); |
|
const smoothCheckbox = document.getElementById('smooth-checkbox'); |
|
|
|
|
|
const comparisonSlider = comparisonContainer.querySelector('.comparison-slider'); |
|
const sliderHandle = comparisonContainer.querySelector('.slider-handle'); |
|
|
|
|
|
let originalFile = null; |
|
let originalImageDataUrl = null; |
|
let svgData = null; |
|
let imageWidth = 0; |
|
let imageHeight = 0; |
|
let isDraggingSlider = false; |
|
|
|
|
|
detailValue.textContent = detailSlider.value; |
|
|
|
|
|
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; |
|
}); |
|
|
|
|
|
sliderHandle.addEventListener('mousedown', startSliderDrag); |
|
document.addEventListener('mousemove', dragSlider); |
|
document.addEventListener('mouseup', stopSliderDrag); |
|
sliderHandle.addEventListener('touchstart', startSliderDrag, { passive: false }); |
|
document.addEventListener('touchmove', dragSlider, { passive: false }); |
|
document.addEventListener('touchend', stopSliderDrag); |
|
|
|
|
|
|
|
|
|
function handleFileSelect(e) { |
|
const file = e.target.files?.[0]; |
|
if (!file) return; |
|
processImageFile(file); |
|
} |
|
|
|
function handleDragOver(e) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
dropzone.classList.add('active'); |
|
} |
|
|
|
function handleDragLeave(e) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
dropzone.classList.remove('active'); |
|
} |
|
|
|
function handleDrop(e) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
dropzone.classList.remove('active'); |
|
const file = e.dataTransfer?.files?.[0]; |
|
if (file && file.type.match('image.*')) { |
|
processImageFile(file); |
|
} else { |
|
alert('Please drop a valid image file (PNG, JPG, WebP, BMP).'); |
|
} |
|
} |
|
|
|
function processImageFile(file) { |
|
if (!file.type.match('image/(png|jpeg|webp|bmp)')) { |
|
alert('Unsupported file type. Please use PNG, JPG, WebP, or BMP.'); |
|
resetInput(); |
|
return; |
|
} |
|
|
|
originalFile = file; |
|
const reader = new FileReader(); |
|
|
|
reader.onload = function(e) { |
|
originalImageDataUrl = e.target.result; |
|
const img = new Image(); |
|
img.onload = () => { |
|
imageWidth = img.width; |
|
imageHeight = img.height; |
|
originalImage.src = originalImageDataUrl; |
|
|
|
comparisonContainer.style.aspectRatio = `${imageWidth} / ${imageHeight}`; |
|
|
|
fileInfo.textContent = `Selected: ${file.name} (${imageWidth}x${imageHeight})`; |
|
originalSizeEl.textContent = formatFileSize(file.size); |
|
convertBtn.disabled = false; |
|
resetResultsUI(); |
|
previewPlaceholder.classList.add('hidden'); |
|
comparisonContainer.classList.remove('hidden'); |
|
comparisonSlider.style.width = '50%'; |
|
svgOutputContainer.innerHTML = ''; |
|
}; |
|
img.onerror = () => { |
|
alert('Could not load image file.'); |
|
resetInput(); |
|
}; |
|
img.src = originalImageDataUrl; |
|
}; |
|
reader.onerror = () => { |
|
alert('Could not read file.'); |
|
resetInput(); |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
|
|
async function convertToSvg() { |
|
if (!originalFile || !originalImageDataUrl) { |
|
alert('Please select an image first'); |
|
return; |
|
} |
|
|
|
|
|
convertBtn.disabled = true; |
|
loadingIndicator.classList.remove('hidden'); |
|
progressContainer.classList.remove('hidden'); |
|
comparisonContainer.classList.add('hidden'); |
|
downloadSection.classList.add('hidden'); |
|
statsSection.classList.add('hidden'); |
|
svgPreviewSection.classList.add('hidden'); |
|
updateProgress(0, "Initializing..."); |
|
|
|
|
|
const numColors = parseInt(colorSelect.value); |
|
const turdSize = parseInt(detailSlider.value); |
|
const optimizeCurves = smoothCheckbox.checked; |
|
|
|
try { |
|
|
|
|
|
if (typeof PotraceWasm === 'undefined' || !PotraceWasm.ready) { |
|
updateProgress(5, "Loading converter..."); |
|
await PotraceWasm.load(); |
|
} |
|
|
|
updateProgress(10, "Processing image data..."); |
|
|
|
|
|
const params = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
posterize: true, |
|
steps: numColors, |
|
|
|
|
|
|
|
turdPolicy: PotraceWasm.TURD_SMOOTH, |
|
turdSize: turdSize, |
|
alphaMax: optimizeCurves ? 1.0 : 0, |
|
optCurve: optimizeCurves, |
|
optTolerance: optimizeCurves ? 0.2 : 0, |
|
|
|
|
|
turnPolicy: PotraceWasm.TURN_MINORITY, |
|
}; |
|
|
|
updateProgress(20, "Tracing image (this may take time)..."); |
|
|
|
|
|
|
|
const result = await PotraceWasm.trace(originalImageDataUrl, params); |
|
|
|
updateProgress(90, "Generating SVG..."); |
|
|
|
svgData = result; |
|
|
|
|
|
svgData = svgData.replace(/<!--[\s\S]*?-->/g, ''); |
|
|
|
|
|
displayResults(svgData); |
|
updateProgress(100, "Complete"); |
|
|
|
} catch (error) { |
|
console.error('SVG Conversion Error:', error); |
|
alert(`Conversion failed: ${error.message || error}`); |
|
updateProgress(0, "Error"); |
|
} finally { |
|
|
|
convertBtn.disabled = false; |
|
loadingIndicator.classList.add('hidden'); |
|
setTimeout(() => { |
|
progressContainer.classList.add('hidden'); |
|
}, 1000); |
|
} |
|
} |
|
|
|
function displayResults(generatedSvgData) { |
|
|
|
svgOutputContainer.innerHTML = generatedSvgData; |
|
|
|
|
|
comparisonContainer.classList.remove('hidden'); |
|
previewPlaceholder.classList.add('hidden'); |
|
loadingIndicator.classList.add('hidden'); |
|
|
|
|
|
downloadSection.classList.remove('hidden'); |
|
|
|
|
|
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 = '-'; |
|
} |
|
statsSection.classList.remove('hidden'); |
|
|
|
|
|
svgCodeEl.textContent = generatedSvgData; |
|
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) return; |
|
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 || 'image').replace(/\.[^/.]+$/, '') + '.svg'; |
|
document.body.appendChild(a); |
|
a.click(); |
|
document.body.removeChild(a); |
|
URL.revokeObjectURL(url); |
|
} |
|
|
|
function copySvgToClipboard() { |
|
if (!svgData) return; |
|
navigator.clipboard.writeText(svgData) |
|
.then(() => { |
|
const originalText = copySvgBtn.querySelector('span').textContent; |
|
copySvgBtn.querySelector('span').textContent = 'Copied!'; |
|
copySvgBtn.classList.add('bg-green-100'); |
|
setTimeout(() => { |
|
copySvgBtn.querySelector('span').textContent = originalText; |
|
copySvgBtn.classList.remove('bg-green-100'); |
|
}, 2000); |
|
}) |
|
.catch(err => { |
|
console.error('Failed to copy SVG: ', err); |
|
alert('Failed to copy SVG to clipboard. You might need to grant permission.'); |
|
}); |
|
} |
|
|
|
function loadSampleImage() { |
|
resetConverter(); |
|
const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; |
|
loadingIndicator.classList.remove('hidden'); |
|
previewPlaceholder.classList.add('hidden'); |
|
fileInfo.textContent = "Loading sample image..."; |
|
|
|
fetch(sampleImageUrl) |
|
.then(response => { |
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); |
|
return response.blob(); |
|
}) |
|
.then(blob => { |
|
|
|
let imageType = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/jpeg'; |
|
const fileName = 'sample-image.' + (imageType.split('/')[1] || 'jpg'); |
|
const file = new File([blob], fileName, { type: imageType }); |
|
processImageFile(file); |
|
}) |
|
.catch(error => { |
|
console.error('Error loading sample image:', error); |
|
alert('Failed to load sample image. Please check the URL or try uploading manually.'); |
|
resetConverter(); |
|
}) |
|
.finally(() => { |
|
loadingIndicator.classList.add('hidden'); |
|
}); |
|
} |
|
|
|
function resetInput() { |
|
fileInput.value = ''; |
|
originalFile = null; |
|
originalImageDataUrl = null; |
|
imageWidth = 0; |
|
imageHeight = 0; |
|
convertBtn.disabled = true; |
|
fileInfo.textContent = ''; |
|
} |
|
|
|
function resetResultsUI() { |
|
svgData = null; |
|
svgOutputContainer.innerHTML = ''; |
|
comparisonContainer.classList.add('hidden'); |
|
previewPlaceholder.classList.remove('hidden'); |
|
downloadSection.classList.add('hidden'); |
|
statsSection.classList.add('hidden'); |
|
svgPreviewSection.classList.add('hidden'); |
|
originalSizeEl.textContent = '-'; |
|
svgSizeEl.textContent = '-'; |
|
reductionEl.textContent = '-'; |
|
svgCodeEl.textContent = ''; |
|
progressContainer.classList.add('hidden'); |
|
updateProgress(0); |
|
} |
|
|
|
function resetConverter() { |
|
resetInput(); |
|
resetResultsUI(); |
|
|
|
|
|
detailSlider.value = 2; |
|
detailValue.textContent = '2'; |
|
colorSelect.value = '16'; |
|
smoothCheckbox.checked = true; |
|
|
|
console.log('Converter Reset'); |
|
} |
|
|
|
function formatFileSize(bytes) { |
|
if (bytes < 0 || typeof bytes !== 'number') return 'N/A'; |
|
if (bytes === 0) return '0 Bytes'; |
|
const k = 1024; |
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; |
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
} |
|
|
|
|
|
function startSliderDrag(e) { |
|
e.preventDefault(); |
|
isDraggingSlider = true; |
|
comparisonContainer.style.cursor = 'ew-resize'; |
|
} |
|
|
|
function dragSlider(e) { |
|
if (!isDraggingSlider) return; |
|
e.preventDefault(); |
|
|
|
const rect = comparisonContainer.getBoundingClientRect(); |
|
|
|
const clientX = e.clientX ?? e.touches?.[0]?.clientX; |
|
if (typeof clientX === 'undefined') return; |
|
|
|
let offsetX = clientX - rect.left; |
|
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'; |
|
} |
|
} |
|
|
|
}); |
|
</script> |
|
</body> |
|
</html> |