tosvg / index.html
thelip's picture
Update index.html
749355d verified
raw
history blame
35.5 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>
<!-- Use the Potrace library that supports color tracing directly -->
<script src="https://unpkg.com/[email protected]/dist/potrace-wasm.js"></script>
<!-- Quantize is still useful for limiting the palette beforehand if desired, but potrace-wasm handles colors -->
<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; /* 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 { 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; }
/* 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 */
#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">
<!-- 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.</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>
<!-- Removed Edge Threshold as it's less relevant for multi-color layered tracing -->
<!-- Removed Quality/Optimize settings as potrace-wasm handles this differently -->
<!-- Removed Background setting for simplicity, Potrace handles background -->
</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="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">
<!-- SVG will be loaded 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" 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>
<!-- 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
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'); // Changed to container div
const downloadSection = document.getElementById('download-section');
const downloadSvgBtn = document.getElementById('download-svg'); // Renamed for clarity
const copySvgBtn = document.getElementById('copy-svg'); // Renamed for clarity
const statsSection = document.getElementById('stats-section');
const originalSizeEl = document.getElementById('original-size'); // Renamed for clarity
const svgSizeEl = document.getElementById('svg-size'); // Renamed for clarity
const reductionEl = document.getElementById('reduction'); // Renamed for clarity
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'); // Renamed for clarity
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;
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 });
document.addEventListener('touchmove', dragSlider, { passive: false });
document.addEventListener('touchend', stopSliderDrag);
// --- Core Functions ---
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;
// Set aspect ratio for container (optional, but helps layout)
comparisonContainer.style.aspectRatio = `${imageWidth} / ${imageHeight}`;
fileInfo.textContent = `Selected: ${file.name} (${imageWidth}x${imageHeight})`;
originalSizeEl.textContent = formatFileSize(file.size);
convertBtn.disabled = false;
resetResultsUI(); // Clear previous results if a new image is loaded
previewPlaceholder.classList.add('hidden'); // Hide placeholder
comparisonContainer.classList.remove('hidden'); // Show container with original
comparisonSlider.style.width = '50%'; // Reset slider position
svgOutputContainer.innerHTML = ''; // Clear old SVG output
};
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;
}
// --- UI Updates: Start Conversion ---
convertBtn.disabled = true;
loadingIndicator.classList.remove('hidden');
progressContainer.classList.remove('hidden');
comparisonContainer.classList.add('hidden'); // Hide comparison during processing
downloadSection.classList.add('hidden');
statsSection.classList.add('hidden');
svgPreviewSection.classList.add('hidden');
updateProgress(0, "Initializing...");
// --- Get Settings ---
const numColors = parseInt(colorSelect.value);
const turdSize = parseInt(detailSlider.value); // Potrace 'turdsize' parameter
const optimizeCurves = smoothCheckbox.checked; // Potrace 'opticurve' parameter
try {
// --- Load Potrace WASM ---
// Check if already loaded - PotraceWasm loads globally
if (typeof PotraceWasm === 'undefined' || !PotraceWasm.ready) {
updateProgress(5, "Loading converter...");
await PotraceWasm.load(); // Load the WASM module
}
updateProgress(10, "Processing image data...");
// --- Prepare parameters for potrace-wasm ---
const params = {
// General
// background: '#FFFFFF', // potrace-wasm often infers or makes transparent
// color: '#000000', // Not used for posterize
// threshold: 128, // Not used for posterize
// Posterization (Color Tracing)
posterize: true, // Enable color tracing
steps: numColors, // Number of color layers
// stepFunction: PotraceWasm.STEP_QUANTIZE, // default is QUANTIZE
// Path decomposition and smoothing
turdPolicy: PotraceWasm.TURD_SMOOTH, // Or MINORITY, ZERO etc.
turdSize: turdSize, // Suppress specks smaller than this size (pixels)
alphaMax: optimizeCurves ? 1.0 : 0, // Adjusts smoothness (corner threshold) - 1.0 is smoother
optCurve: optimizeCurves, // Enable/disable curve optimization
optTolerance: optimizeCurves ? 0.2 : 0, // Optimization tolerance when optcurve is true
// Turn policy (how to handle corners)
turnPolicy: PotraceWasm.TURN_MINORITY, // Or BLACK, WHITE, LEFT, RIGHT
};
updateProgress(20, "Tracing image (this may take time)...");
// --- Perform Conversion using potrace-wasm ---
// PotraceWasm.trace accepts ImageData, Canvas, Image, or URL
const result = await PotraceWasm.trace(originalImageDataUrl, params);
updateProgress(90, "Generating SVG...");
svgData = result; // The result is the SVG string directly
// Basic cleanup (optional, potrace-wasm output is often clean)
svgData = svgData.replace(/<!--[\s\S]*?-->/g, ''); // Remove comments
// --- Display Results ---
displayResults(svgData);
updateProgress(100, "Complete");
} catch (error) {
console.error('SVG Conversion Error:', error);
alert(`Conversion failed: ${error.message || error}`);
updateProgress(0, "Error");
} finally {
// --- UI Updates: End Conversion ---
convertBtn.disabled = false;
loadingIndicator.classList.add('hidden');
setTimeout(() => { // Hide progress bar after a short delay
progressContainer.classList.add('hidden');
}, 1000);
}
}
function displayResults(generatedSvgData) {
// Set SVG content in the preview container
svgOutputContainer.innerHTML = generatedSvgData;
// Ensure comparison view is visible
comparisonContainer.classList.remove('hidden');
previewPlaceholder.classList.add('hidden');
loadingIndicator.classList.add('hidden');
// Show download options
downloadSection.classList.remove('hidden');
// Calculate and show 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 = '-';
}
statsSection.classList.remove('hidden');
// Show SVG code preview
svgCodeEl.textContent = generatedSvgData; // Use textContent for security
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(); // Reset first
const sampleImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=80'; // Smaller sample for faster processing
loadingIndicator.classList.remove('hidden'); // Show temporary loading
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 => {
// Ensure blob type is correct, default to jpeg if necessary
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(); // Reset fully on error
})
.finally(() => {
loadingIndicator.classList.add('hidden'); // Hide temp loading
});
}
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();
// Reset form values to default
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];
}
// --- Comparison Slider Functions ---
function startSliderDrag(e) {
e.preventDefault(); // Prevent text selection, etc.
isDraggingSlider = true;
comparisonContainer.style.cursor = 'ew-resize'; // Indicate dragging
}
function dragSlider(e) {
if (!isDraggingSlider) return;
e.preventDefault();
const rect = comparisonContainer.getBoundingClientRect();
// Handle both mouse and touch events
const clientX = e.clientX ?? e.touches?.[0]?.clientX;
if (typeof clientX === 'undefined') return; // Exit if no coordinate
let offsetX = clientX - rect.left;
let newWidth = Math.max(0, Math.min(rect.width, offsetX)); // Clamp between 0 and container width
let percentWidth = (newWidth / rect.width) * 100;
comparisonSlider.style.width = `${percentWidth}%`;
}
function stopSliderDrag() {
if (isDraggingSlider) {
isDraggingSlider = false;
comparisonContainer.style.cursor = 'default'; // Restore default cursor
}
}
});
</script>
</body>
</html>