bitmap-to-vector2 / index.html
pg0's picture
potrace url changed
7ddf9b6 verified
<!DOCTYPE html>
<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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
});
</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>