Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Image to SVG Vectorizer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://kilobtye.github.io/potrace/potrace.js"></script> | |
| <style> | |
| .dropzone { | |
| border: 2px dashed #cbd5e0; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #4f46e5; | |
| background-color: #eef2ff; | |
| } | |
| .dropzone.error { | |
| border-color: #ef4444; | |
| background-color: #fee2e2; | |
| } | |
| .progress-bar { | |
| transition: width 0.3s ease; | |
| } | |
| #svgPreview { | |
| max-height: 400px; | |
| overflow: auto; | |
| background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%), | |
| linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #f0f0f0 75%), | |
| linear-gradient(-45deg, transparent 75%, #f0f0f0 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0px; | |
| } | |
| .tooltip { | |
| position: relative; | |
| } | |
| .tooltip-text { | |
| visibility: hidden; | |
| width: 200px; | |
| background-color: #333; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltip-text { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| .file-info { | |
| display: none; | |
| } | |
| .settings-panel { | |
| transition: all 0.3s ease; | |
| } | |
| .settings-panel.collapsed { | |
| max-height: 0; | |
| overflow: hidden; | |
| opacity: 0; | |
| } | |
| .settings-panel.expanded { | |
| max-height: 500px; | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="text-center mb-12"> | |
| <h1 class="text-4xl font-bold text-indigo-600 mb-2"> | |
| <i class="fas fa-vector-square mr-2"></i>Advanced Image to SVG Vectorizer | |
| </h1> | |
| <p class="text-gray-600 max-w-2xl mx-auto"> | |
| Convert raster images to high-quality scalable vector graphics with advanced settings | |
| </p> | |
| </header> | |
| <div class="bg-white rounded-xl shadow-lg overflow-hidden max-w-4xl mx-auto"> | |
| <div class="md:flex"> | |
| <!-- Left Panel - Upload and Settings --> | |
| <div class="p-6 md:w-1/2 border-r border-gray-200"> | |
| <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-6"> | |
| <div id="uploadUI" class="flex flex-col items-center justify-center"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-3"></i> | |
| <h3 class="text-lg font-medium text-gray-700 mb-1">Drag & Drop your image here</h3> | |
| <p class="text-gray-500 text-sm mb-3">or click to browse files</p> | |
| <input type="file" id="fileInput" class="hidden" accept="image/*"> | |
| <button id="browseBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm font-medium transition"> | |
| Select Image | |
| </button> | |
| </div> | |
| <div id="fileInfo" class="file-info flex items-center justify-center"> | |
| <div class="bg-gray-100 rounded-lg p-3 flex items-center"> | |
| <i class="fas fa-file-image text-indigo-500 text-xl mr-3"></i> | |
| <div> | |
| <p id="fileName" class="text-sm font-medium text-gray-7truncate max-w-xs"></p> | |
| <p id="fileSize" class="text-xs text-gray-500"></p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="previewContainer" class="hidden mb-6"> | |
| <h3 class="text-lg font-medium text-gray-700 mb-3">Image Preview</h3> | |
| <div class="relative"> | |
| <img id="imagePreview" class="max-h-48 w-auto mx-auto rounded-md shadow-sm border border-gray-200"> | |
| <button id="removeImage" class="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white rounded-full p-1"> | |
| <i class="fas fa-times text-xs"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center cursor-pointer" id="settingsToggle"> | |
| <h3 class="text-lg font-medium text-gray-700">Vectorization Settings</h3> | |
| <i class="fas fa-chevron-down text-gray-500 transition-transform duration-300" id="toggleIcon"></i> | |
| </div> | |
| <div id="settingsPanel" class="settings-panel expanded space-y-4"> | |
| <div> | |
| <label for="vectorMode" class="block text-sm font-medium text-gray-700 mb-1">Vectorization Mode</label> | |
| <select id="vectorMode" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"> | |
| <option value="posterize">Posterize (color blocks)</option> | |
| <option value="grayscale">Grayscale</option> | |
| <option value="blackwhite" selected>Black & White</option> | |
| </select> | |
| </div> | |
| <div id="colorSettings"> | |
| <label for="colorCount" class="block text-sm font-medium text-gray-700 mb-1">Color Count</label> | |
| <input type="range" id="colorCount" min="2" max="16" value="4" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>2</span> | |
| <span>4</span> | |
| <span>8</span> | |
| <span>16</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="threshold" class="block text-sm font-medium text-gray-700 mb-1">Threshold</label> | |
| <input type="range" id="threshold" min="0" max="100" value="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>Low</span> | |
| <span>Medium</span> | |
| <span>High</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="smoothness" class="block text-sm font-medium text-gray-700 mb-1">Smoothness</label> | |
| <input type="range" id="smoothness" min="0" max="10" value="4" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>Sharp</span> | |
| <span>Balanced</span> | |
| <span>Smooth</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="detailLevel" class="block text-sm font-medium text-gray-700 mb-1">Detail Level</label> | |
| <input type="range" id="detailLevel" min="1" max="10" value="5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>Low</span> | |
| <span>Medium</span> | |
| <span>High</span> | |
| </div> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="transparentBg" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> | |
| <label for="transparentBg" class="ml-2 block text-sm text-gray-7">Transparent background</label> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="optimizePaths" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" checked> | |
| <label for="optimizePaths" class="ml-2 block text-sm text-gray-7">Optimize paths</label> | |
| </div> | |
| </div> | |
| <button id="convertBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-md font-medium transition disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i class="fas fa-magic mr-2"></i> Convert to SVG | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Right Panel - Results --> | |
| <div class="p-6 md:w-1/2"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-800">SVG Output</h2> | |
| <div class="flex space-x-2"> | |
| <button id="downloadBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm font-medium hidden"> | |
| <i class="fas fa-download mr-1"></i> Download | |
| </button> | |
| <button id="copyBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-sm font-medium hidden"> | |
| <i class="fas fa-copy mr-1"></i> Copy | |
| </button> | |
| </div> | |
| </div> | |
| <div id="processingUI" class="hidden text-center py-12"> | |
| <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div> | |
| <h3 class="text-lg font-medium text-gray-700">Processing Image</h3> | |
| <p class="text-gray-500 text-sm mt-1">Vectorizing your image...</p> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5 mt-4"> | |
| <div id="progressBar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div id="resultContainer" class="hidden"> | |
| <div class="bg-gray-100 rounded-lg p-4 mb-4"> | |
| <div id="svgPreview" class="mx-auto"></div> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="text-sm font-medium text-gray-700">SVG Statistics</h4> | |
| </div> | |
| <div class="grid grid-cols-3 gap-2 text-xs"> | |
| <div class="bg-gray-100 p-2 rounded"> | |
| <div class="text-gray-500">File Size</div> | |
| <div id="svgSize" class="font-medium">-</div> | |
| </div> | |
| <div class="bg-gray-100 p-2 rounded"> | |
| <div class="text-gray-500">Path Count</div> | |
| <div id="pathCount" class="font-medium">-</div> | |
| </div> | |
| <div class="bg-gray-100 p-2 rounded"> | |
| <div class="text-gray-500">Colors</div> | |
| <div id="colorCountDisplay" class="font-medium">-</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="text-sm font-medium text-gray-700">SVG Code</h4> | |
| <button id="copyCodeBtn" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium"> | |
| <i class="fas fa-copy mr-1"></i> Copy Code | |
| </button> | |
| </div> | |
| <pre id="svgCode" class="bg-gray-800 text-gray-100 text-xs p-3 rounded-md overflow-auto max-h-40"></pre> | |
| </div> | |
| </div> | |
| <div id="emptyState" class="text-center py-12"> | |
| <div class="mx-auto w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mb-4"> | |
| <i class="fas fa-image text-indigo-500 text-xl"></i> | |
| </div> | |
| <h3 class="text-lg font-medium text-gray-700">No image uploaded</h3> | |
| <p class="text-gray-500 text-sm mt-1">Upload an image to convert it to SVG format</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer class="text-center mt-12 text-gray-500 text-sm"> | |
| <p>Advanced Image to SVG Vectorizer © 2023 | Powered by Potrace algorithm</p> | |
| </footer> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM elements | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const browseBtn = document.getElementById('browseBtn'); | |
| const uploadUI = document.getElementById('uploadUI'); | |
| const fileInfo = document.getElementById('fileInfo'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const previewContainer = document.getElementById('previewContainer'); | |
| const imagePreview = document.getElementById('imagePreview'); | |
| const removeImage = document.getElementById('removeImage'); | |
| const convertBtn = document.getElementById('convertBtn'); | |
| const processingUI = document.getElementById('processingUI'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const resultContainer = document.getElementById('resultContainer'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const svgPreview = document.getElementById('svgPreview'); | |
| const svgCode = document.getElementById('svgCode'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const copyBtn = document.getElementById('copyBtn'); | |
| const copyCodeBtn = document.getElementById('copyCodeBtn'); | |
| const settingsToggle = document.getElementById('settingsToggle'); | |
| const settingsPanel = document.getElementById('settingsPanel'); | |
| const toggleIcon = document.getElementById('toggleIcon'); | |
| const vectorMode = document.getElementById('vectorMode'); | |
| const colorCount = document.getElementById('colorCount'); | |
| const threshold = document.getElementById('threshold'); | |
| const smoothness = document.getElementById('smoothness'); | |
| const detailLevel = document.getElementById('detailLevel'); | |
| const transparentBg = document.getElementById('transparentBg'); | |
| const optimizePaths = document.getElementById('optimizePaths'); | |
| const colorSettings = document.getElementById('colorSettings'); | |
| const svgSize = document.getElementById('svgSize'); | |
| const pathCount = document.getElementById('pathCount'); | |
| const colorCountDisplay = document.getElementById('colorCountDisplay'); | |
| // Current file data | |
| let currentFile = null; | |
| let canvas = document.createElement('canvas'); | |
| let ctx = canvas.getContext('2d'); | |
| let progressInterval; | |
| // Toggle settings panel | |
| settingsToggle.addEventListener('click', () => { | |
| settingsPanel.classList.toggle('expanded'); | |
| settingsPanel.classList.toggle('collapsed'); | |
| toggleIcon.classList.toggle('rotate-180'); | |
| }); | |
| // Toggle color settings based on vector mode | |
| vectorMode.addEventListener('change', () => { | |
| if (vectorMode.value === 'posterize') { | |
| colorSettings.style.display = 'block'; | |
| } else { | |
| colorSettings.style.display = 'none'; | |
| } | |
| }); | |
| // Prevent default drag behaviors | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, preventDefaults, false); | |
| document.body.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| // Highlight dropzone when item is dragged over it | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| dropzone.classList.add('active'); | |
| } | |
| function unhighlight() { | |
| dropzone.classList.remove('active'); | |
| } | |
| // Handle dropped files | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| // Handle file selection via button | |
| browseBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', () => { | |
| if (fileInput.files.length) { | |
| handleFiles(fileInput.files); | |
| } | |
| }); | |
| // Remove image | |
| removeImage.addEventListener('click', resetUpload); | |
| // Convert button | |
| convertBtn.addEventListener('click', processImage); | |
| // Copy buttons | |
| copyBtn.addEventListener('click', copySVG); | |
| copyCodeBtn.addEventListener('click', copySVGCode); | |
| // Download button | |
| downloadBtn.addEventListener('click', downloadSVG); | |
| // Handle selected files | |
| function handleFiles(files) { | |
| const file = files[0]; | |
| currentFile = file; | |
| // Check if file is an image | |
| if (!file.type.match('image.*')) { | |
| showError('Please upload an image file (JPG, PNG, etc.)'); | |
| return; | |
| } | |
| // Check file size (max 10MB) | |
| if (file.size > 10 * 1024 * 1024) { | |
| showError('File size exceeds 10MB limit'); | |
| return; | |
| } | |
| // Display file info | |
| uploadUI.style.display = 'none'; | |
| fileInfo.style.display = 'flex'; | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| // Display image preview | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| imagePreview.src = e.target.result; | |
| previewContainer.classList.remove('hidden'); | |
| convertBtn.disabled = false; | |
| // Load image to canvas for processing | |
| const img = new Image(); | |
| img.onload = function() { | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| ctx.drawImage(img, 0, 0); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| // Format file size | |
| function formatFileSize(bytes) { | |
| if (bytes < 1024) return bytes + ' bytes'; | |
| else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; | |
| else return (bytes / 1048576).toFixed(1) + ' MB'; | |
| } | |
| // Show error message | |
| function showError(message) { | |
| dropzone.classList.add('error'); | |
| const errorElement = document.createElement('div'); | |
| errorElement.className = 'text-red-500 text-sm mt-2'; | |
| errorElement.textContent = message; | |
| dropzone.appendChild(errorElement); | |
| setTimeout(() => { | |
| dropzone.classList.remove('error'); | |
| dropzone.removeChild(errorElement); | |
| }, 3000); | |
| } | |
| // Reset upload | |
| function resetUpload() { | |
| fileInput.value = ''; | |
| currentFile = null; | |
| uploadUI.style.display = 'flex'; | |
| fileInfo.style.display = 'none'; | |
| previewContainer.classList.add('hidden'); | |
| convertBtn.disabled = true; | |
| emptyState.classList.remove('hidden'); | |
| resultContainer.classList.add('hidden'); | |
| // Clear any existing progress interval | |
| if (progressInterval) { | |
| clearInterval(progressInterval); | |
| } | |
| } | |
| // Process image with vectorization | |
| function processImage() { | |
| // Show processing UI | |
| processingUI.classList.remove('hidden'); | |
| emptyState.classList.add('hidden'); | |
| resultContainer.classList.add('hidden'); | |
| // Reset progress bar | |
| progressBar.style.width = '0%'; | |
| // Simulate progress with more controlled increments | |
| let progress = 0; | |
| progressInterval = setInterval(() => { | |
| // Calculate progress increment based on remaining distance to 100% | |
| const remaining = 100 - progress; | |
| const increment = Math.min( | |
| Math.max(1, Math.floor(remaining * 0.15)), // Minimum 1%, maximum 15% of remaining | |
| 10 // Absolute maximum increment per step | |
| ); | |
| progress += increment; | |
| // Ensure we don't go over 100% | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(progressInterval); | |
| } | |
| progressBar.style.width = progress + '%'; | |
| }, 200); | |
| // Process image after a short delay to allow UI to update | |
| setTimeout(() => { | |
| // Get settings from UI | |
| const settings = { | |
| mode: vectorMode.value, | |
| colorCount: parseInt(colorCount.value), | |
| threshold: parseInt(threshold.value) / 100, | |
| smoothness: parseInt(smoothness.value), | |
| detail: parseInt(detailLevel.value), | |
| transparent: transparentBg.checked, | |
| optimize: optimizePaths.checked | |
| }; | |
| // Process the image based on selected mode | |
| let processedImageData; | |
| if (settings.mode === 'posterize') { | |
| processedImageData = posterizeImage(canvas, settings.colorCount); | |
| } else if (settings.mode === 'grayscale') { | |
| processedImageData = convertToGrayscale(canvas); | |
| } else { // blackwhite | |
| processedImageData = applyThreshold(canvas, settings.threshold); | |
| } | |
| // Create temporary canvas with processed image | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCtx.putImageData(processedImageData, 0, 0); | |
| // Use Potrace to vectorize the image | |
| const imageDataURL = tempCanvas.toDataURL('image/png'); | |
| // Vectorize using Potrace | |
| potrace.loadImageFromUrl(imageDataURL); | |
| potrace.setParameters({ | |
| turdsize: Math.max(1, 10 - settings.detail), // Fewer details with higher values | |
| optcurve: settings.optimize, | |
| alphamax: settings.smoothness * 1.4, // Controls corner threshold | |
| opttolerance: settings.smoothness * 0.5 // Optimization tolerance | |
| }); | |
| potrace.process(() => { | |
| // Clear the progress interval if it's still running | |
| if (progressInterval) { | |
| clearInterval(progressInterval); | |
| } | |
| // Ensure progress bar shows 100% | |
| progressBar.style.width = '100%'; | |
| // Get SVG data | |
| const svg = potrace.getSVG(1, settings.transparent ? 'none' : '#ffffff'); | |
| // Show result after a small delay to allow progress bar to complete | |
| setTimeout(() => { | |
| showResult(svg); | |
| }, 100); | |
| }); | |
| }, 500); | |
| } | |
| // Image processing functions | |
| function posterizeImage(canvas, levels) { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| // Calculate the step for each channel | |
| const step = 255 / (levels - 1); | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Posterize each channel | |
| data[i] = Math.round(data[i] / step) * step; // R | |
| data[i + 1] = Math.round(data[i + 1] / step) * step; // G | |
| data[i + 2] = Math.round(data[i + 2] / step) * step; // B | |
| } | |
| return imageData; | |
| } | |
| function convertToGrayscale(canvas) { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Convert to grayscale using luminance | |
| const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; | |
| data[i] = data[i + 1] = data[i + 2] = gray; | |
| } | |
| return imageData; | |
| } | |
| function applyThreshold(canvas, thresholdValue) { | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Convert to grayscale first | |
| const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; | |
| // Apply threshold | |
| const value = gray > (thresholdValue * 255) ? 255 : 0; | |
| data[i] = data[i + 1] = data[i + 2] = value; | |
| } | |
| return imageData; | |
| } | |
| // Show result | |
| function showResult(svgData) { | |
| processingUI.classList.add('hidden'); | |
| resultContainer.classList.remove('hidden'); | |
| emptyState.classList.add('hidden'); | |
| downloadBtn.classList.remove('hidden'); | |
| copyBtn.classList.remove('hidden'); | |
| // Display SVG | |
| svgPreview.innerHTML = svgData; | |
| svgCode.textContent = svgData; | |
| // Calculate and display statistics | |
| const svgSizeBytes = new Blob([svgData]).size; | |
| svgSize.textContent = formatFileSize(svgSizeBytes); | |
| // Count paths in SVG | |
| const pathMatches = svgData.match(/<path/g); | |
| const pathCountValue = pathMatches ? pathMatches.length : 0; | |
| pathCount.textContent = pathCountValue; | |
| // Count colors in SVG | |
| const colorMatches = svgData.match(/fill="([^"]+)"/g); | |
| let uniqueColors = new Set(); | |
| if (colorMatches) { | |
| colorMatches.forEach(match => { | |
| const color = match.match(/fill="([^"]+)"/)[1]; | |
| uniqueColors.add(color.toLowerCase()); | |
| }); | |
| } | |
| colorCountDisplay.textContent = uniqueColors.size; | |
| } | |
| // Copy SVG to clipboard | |
| function copySVG() { | |
| const svg = svgPreview.innerHTML; | |
| navigator.clipboard.writeText(svg) | |
| .then(() => { | |
| showTooltip(copyBtn, 'SVG copied to clipboard!'); | |
| }) | |
| .catch(err => { | |
| console.error('Failed to copy SVG: ', err); | |
| }); | |
| } | |
| // Copy SVG code to clipboard | |
| function copySVGCode() { | |
| const code = svgCode.textContent; | |
| navigator.clipboard.writeText(code) | |
| .then(() => { | |
| showTooltip(copyCodeBtn, 'Code copied to clipboard!'); | |
| }) | |
| .catch(err => { | |
| console.error('Failed to copy code: ', err); | |
| }); | |
| } | |
| // Download SVG | |
| function downloadSVG() { | |
| const svg = svgPreview.innerHTML; | |
| const blob = new Blob([svg], {type: 'image/svg+xml'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = (fileName.textContent.replace(/\.[^/.]+$/, "") || 'vectorized') + '.svg'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Show tooltip | |
| function showTooltip(element, message) { | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'absolute bg-gray-800 text-white text-xs px-2 py-1 rounded -mt-8'; | |
| tooltip.textContent = message; | |
| element.appendChild(tooltip); | |
| setTimeout(() => { | |
| element.removeChild(tooltip); | |
| }, 2000); | |
| } | |
| }); | |
| </script> | |
| <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=pg0/vectorizer" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |