Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Bitmap to Vector Converter</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://kilobtye.github.io/potrace/potrace.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/svg.js@3.0.17/dist/svg.min.js"></script> | |
| <style> | |
| .dropzone { | |
| border: 2px dashed #ccc; | |
| transition: all 0.3s; | |
| } | |
| .dropzone.active { | |
| border-color: #4f46e5; | |
| background-color: #eef2ff; | |
| } | |
| #canvas-container { | |
| position: relative; | |
| } | |
| #preview-canvas, #vector-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| } | |
| .slider-thumb::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #4f46e5; | |
| cursor: pointer; | |
| } | |
| .slider-thumb::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #4f46e5; | |
| cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8 text-center"> | |
| <h1 class="text-4xl font-bold text-indigo-700 mb-2">Bitmap to Vector Converter</h1> | |
| <p class="text-gray-600">Upload an image to trace it into vector paths and export as SVG</p> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <!-- Left Panel - Upload and Controls --> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <div id="dropzone" class="dropzone rounded-lg p-8 mb-6 text-center cursor-pointer"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-indigo-500 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
| </svg> | |
| <p class="text-lg font-medium text-gray-700">Drag & drop your image here</p> | |
| <p class="text-sm text-gray-500 mt-1">or click to browse files</p> | |
| <input type="file" id="file-input" class="hidden" accept="image/*"> | |
| </div> | |
| </div> | |
| <div class="space-y-6"> | |
| <!-- Tracing Options --> | |
| <div> | |
| <h3 class="text-lg font-medium text-gray-900 mb-3">Tracing Options</h3> | |
| <div class="space-y-4"> | |
| <!-- Threshold Slider --> | |
| <div> | |
| <label for="threshold" class="block text-sm font-medium text-gray-700 mb-1">Threshold: <span id="threshold-value">128</span></label> | |
| <input type="range" id="threshold" min="0" max="255" value="128" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> | |
| </div> | |
| <!-- Turd Size --> | |
| <div> | |
| <label for="turdSize" class="block text-sm font-medium text-gray-700 mb-1">Noise Reduction: <span id="turdSize-value">2</span></label> | |
| <input type="range" id="turdSize" min="0" max="20" value="2" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> | |
| </div> | |
| <!-- Curve Optimization --> | |
| <div> | |
| <label for="curveOptimization" class="block text-sm font-medium text-gray-700 mb-1">Curve Optimization: <span id="curveOptimization-value">0.2</span></label> | |
| <input type="range" id="curveOptimization" min="0" max="1" step="0.01" value="0.2" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> | |
| </div> | |
| <!-- Color Mode --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Color Mode:</label> | |
| <div class="flex space-x-4"> | |
| <label class="inline-flex items-center"> | |
| <input type="radio" name="colorMode" value="black" checked class="h-4 w-4 text-indigo-600 focus:ring-indigo-500"> | |
| <span class="ml-2 text-gray-700">Black & White</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="radio" name="colorMode" value="color" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500"> | |
| <span class="ml-2 text-gray-700">Color</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="pt-4"> | |
| <button id="trace-btn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M4 5a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-1.586a1 1 0 01-.707-.293l-1.121-1.121A2 2 0 0011.172 3H8.828a2 2 0 00-1.414.586L6.293 4.707A1 1 0 015.586 5H4zm6 9a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /> | |
| </svg> | |
| Trace Image | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel - Preview and Results --> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-medium text-gray-900">Preview</h3> | |
| <div class="flex space-x-2"> | |
| <button id="zoom-in" class="p-2 rounded-md hover:bg-gray-100"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" /> | |
| </svg> | |
| </button> | |
| <button id="zoom-out" class="p-2 rounded-md hover:bg-gray-100"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z" clip-rule="evenodd" /> | |
| </svg> | |
| </button> | |
| <button id="reset-zoom" class="p-2 rounded-md hover:bg-gray-100"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="canvas-container" class="relative border border-gray-200 rounded-lg overflow-hidden bg-gray-100" style="height: 400px;"> | |
| <canvas id="preview-canvas" class="absolute top-0 left-0"></canvas> | |
| <svg id="vector-canvas" class="absolute top-0 left-0" width="100%" height="100%"></svg> | |
| <div id="empty-state" class="flex flex-col items-center justify-center h-full text-gray-400"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| <p class="text-lg">No image loaded</p> | |
| </div> | |
| </div> | |
| <div class="mt-6"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h3 class="text-lg font-medium text-gray-900">Vector Output</h3> | |
| <div class="flex space-x-2"> | |
| <button id="copy-svg" class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-3 rounded-md transition duration-200 flex items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"> | |
| <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" /> | |
| <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" /> | |
| </svg> | |
| Copy SVG | |
| </button> | |
| <button id="download-svg" class="text-sm bg-indigo-600 hover:bg-indigo-700 text-white py-1 px-3 rounded-md transition duration-200 flex items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> | |
| </svg> | |
| Download SVG | |
| </button> | |
| </div> | |
| </div> | |
| <div id="svg-code-container" class="bg-gray-50 rounded-lg p-3 h-40 overflow-auto font-mono text-sm text-gray-700"> | |
| <div class="text-gray-400 italic">SVG output will appear here after tracing</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const previewCanvas = document.getElementById('preview-canvas'); | |
| const vectorCanvas = document.getElementById('vector-canvas'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const traceBtn = document.getElementById('trace-btn'); | |
| const zoomInBtn = document.getElementById('zoom-in'); | |
| const zoomOutBtn = document.getElementById('zoom-out'); | |
| const resetZoomBtn = document.getElementById('reset-zoom'); | |
| const copySvgBtn = document.getElementById('copy-svg'); | |
| const downloadSvgBtn = document.getElementById('download-svg'); | |
| const svgCodeContainer = document.getElementById('svg-code-container'); | |
| // Sliders and their value displays | |
| const thresholdSlider = document.getElementById('threshold'); | |
| const turdSizeSlider = document.getElementById('turdSize'); | |
| const curveOptimizationSlider = document.getElementById('curveOptimization'); | |
| const thresholdValue = document.getElementById('threshold-value'); | |
| const turdSizeValue = document.getElementById('turdSize-value'); | |
| const curveOptimizationValue = document.getElementById('curveOptimization-value'); | |
| // Canvas context | |
| const ctx = previewCanvas.getContext('2d'); | |
| // State variables | |
| let currentImage = null; | |
| let scale = 1; | |
| let offsetX = 0; | |
| let offsetY = 0; | |
| // Initialize sliders | |
| thresholdSlider.addEventListener('input', () => { | |
| thresholdValue.textContent = thresholdSlider.value; | |
| }); | |
| turdSizeSlider.addEventListener('input', () => { | |
| turdSizeValue.textContent = turdSizeSlider.value; | |
| }); | |
| curveOptimizationSlider.addEventListener('input', () => { | |
| curveOptimizationValue.textContent = curveOptimizationSlider.value; | |
| }); | |
| // Drag and drop handling | |
| dropzone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['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'); | |
| } | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length) { | |
| handleFileSelect({ target: { files } }); | |
| } | |
| } | |
| function handleFileSelect(e) { | |
| const file = e.target.files[0]; | |
| if (!file.type.match('image.*')) { | |
| alert('Please select an image file'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| currentImage = img; | |
| displayImage(img); | |
| emptyState.style.display = 'none'; | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function displayImage(img) { | |
| const container = document.getElementById('canvas-container'); | |
| const containerWidth = container.clientWidth; | |
| const containerHeight = container.clientHeight; | |
| // Calculate dimensions to fit the container while maintaining aspect ratio | |
| let width = img.width; | |
| let height = img.height; | |
| if (width > containerWidth || height > containerHeight) { | |
| const ratio = Math.min(containerWidth / width, containerHeight / height); | |
| width = width * ratio; | |
| height = height * ratio; | |
| } | |
| // Set canvas dimensions | |
| previewCanvas.width = width; | |
| previewCanvas.height = height; | |
| vectorCanvas.setAttribute('width', width); | |
| vectorCanvas.setAttribute('height', height); | |
| // Draw image | |
| ctx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); | |
| ctx.drawImage(img, 0, 0, width, height); | |
| // Reset zoom and offset | |
| scale = 1; | |
| offsetX = 0; | |
| offsetY = 0; | |
| updateCanvasTransform(); | |
| } | |
| // Zoom controls | |
| zoomInBtn.addEventListener('click', () => { | |
| scale *= 1.2; | |
| updateCanvasTransform(); | |
| }); | |
| zoomOutBtn.addEventListener('click', () => { | |
| scale /= 1.2; | |
| updateCanvasTransform(); | |
| }); | |
| resetZoomBtn.addEventListener('click', () => { | |
| scale = 1; | |
| offsetX = 0; | |
| offsetY = 0; | |
| updateCanvasTransform(); | |
| }); | |
| function updateCanvasTransform() { | |
| previewCanvas.style.transform = `scale(${scale}) translate(${offsetX}px, ${offsetY}px)`; | |
| vectorCanvas.style.transform = `scale(${scale}) translate(${offsetX}px, ${offsetY}px)`; | |
| } | |
| // Trace image button | |
| traceBtn.addEventListener('click', traceImage); | |
| function traceImage() { | |
| if (!currentImage) { | |
| alert('Please upload an image first'); | |
| return; | |
| } | |
| traceBtn.disabled = true; | |
| traceBtn.innerHTML = ` | |
| <svg class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| Tracing... | |
| `; | |
| // Get parameters | |
| const threshold = parseInt(thresholdSlider.value); | |
| const turdSize = parseInt(turdSizeSlider.value); | |
| const curveOptimization = parseFloat(curveOptimizationSlider.value); | |
| const colorMode = document.querySelector('input[name="colorMode"]:checked').value; | |
| // Create a temporary canvas for processing | |
| const tempCanvas = document.createElement('canvas'); | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCanvas.width = currentImage.width; | |
| tempCanvas.height = currentImage.height; | |
| tempCtx.drawImage(currentImage, 0, 0); | |
| // Get image data | |
| const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); | |
| // Trace using Potrace | |
| const potrace = new Potrace(); | |
| if (colorMode === 'black') { | |
| potrace.setParameters({ | |
| threshold: threshold, | |
| turdSize: turdSize, | |
| curveOptimization: curveOptimization | |
| }); | |
| potrace.loadImageData(imageData, function() { | |
| const svg = potrace.getSVG(); | |
| displayVectorResult(svg); | |
| }); | |
| } else { | |
| // For color tracing, we'll need to process each color channel separately | |
| // This is a simplified approach - a production tool would need more sophisticated color quantization | |
| alert('Color tracing is more complex and would require additional libraries for best results. Using black and white for now.'); | |
| potrace.setParameters({ | |
| threshold: threshold, | |
| turdSize: turdSize, | |
| curveOptimization: curveOptimization | |
| }); | |
| potrace.loadImageData(imageData, function() { | |
| const svg = potrace.getSVG(); | |
| displayVectorResult(svg); | |
| }); | |
| } | |
| } | |
| function displayVectorResult(svg) { | |
| // Clear previous vector | |
| vectorCanvas.innerHTML = ''; | |
| // Parse the SVG string and add it to our vector canvas | |
| vectorCanvas.innerHTML = svg; | |
| // Display the SVG code | |
| svgCodeContainer.innerHTML = `<pre class="text-xs">${escapeHtml(svg)}</pre>`; | |
| // Reset trace button | |
| traceBtn.disabled = false; | |
| traceBtn.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M4 5a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-1.586a1 1 0 01-.707-.293l-1.121-1.121A2 2 0 0011.172 3H8.828a2 2 0 00-1.414.586L6.293 4.707A1 1 0 015.586 5H4zm6 9a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /> | |
| </svg> | |
| Trace Image | |
| `; | |
| } | |
| // Copy SVG button | |
| copySvgBtn.addEventListener('click', () => { | |
| if (vectorCanvas.innerHTML.includes('path')) { | |
| const svgText = vectorCanvas.innerHTML; | |
| navigator.clipboard.writeText(svgText).then(() => { | |
| const originalText = copySvgBtn.textContent; | |
| copySvgBtn.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /> | |
| </svg> | |
| Copied! | |
| `; | |
| setTimeout(() => { | |
| copySvgBtn.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"> | |
| <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" /> | |
| <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" /> | |
| </svg> | |
| ${originalText} | |
| `; | |
| }, 2000); | |
| }).catch(err => { | |
| console.error('Failed to copy: ', err); | |
| }); | |
| } else { | |
| alert('No vector data to copy. Please trace an image first.'); | |
| } | |
| }); | |
| // Download SVG button | |
| downloadSvgBtn.addEventListener('click', () => { | |
| if (vectorCanvas.innerHTML.includes('path')) { | |
| const svgText = vectorCanvas.innerHTML; | |
| const blob = new Blob([svgText], { type: 'image/svg+xml' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'vector-image.svg'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } else { | |
| alert('No vector data to download. Please trace an image first.'); | |
| } | |
| }); | |
| // Helper function to escape HTML | |
| function escapeHtml(unsafe) { | |
| return unsafe | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| }); | |
| </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/bitmap-to-vector2" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |