Spaces:
Running
Running
Add 1 files
Browse files- index.html +279 -60
index.html
CHANGED
|
@@ -3,9 +3,10 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Image to SVG Vectorizer</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
| 9 |
<style>
|
| 10 |
.dropzone {
|
| 11 |
border: 2px dashed #cbd5e0;
|
|
@@ -58,16 +59,28 @@
|
|
| 58 |
.file-info {
|
| 59 |
display: none;
|
| 60 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</style>
|
| 62 |
</head>
|
| 63 |
<body class="bg-gray-50 min-h-screen">
|
| 64 |
<div class="container mx-auto px-4 py-8">
|
| 65 |
<header class="text-center mb-12">
|
| 66 |
<h1 class="text-4xl font-bold text-indigo-600 mb-2">
|
| 67 |
-
<i class="fas fa-vector-square mr-2"></i>Image to SVG Vectorizer
|
| 68 |
</h1>
|
| 69 |
<p class="text-gray-600 max-w-2xl mx-auto">
|
| 70 |
-
Convert
|
| 71 |
</p>
|
| 72 |
</header>
|
| 73 |
|
|
@@ -107,44 +120,75 @@
|
|
| 107 |
</div>
|
| 108 |
|
| 109 |
<div class="space-y-4">
|
| 110 |
-
<
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
<label for="colorPrecision" class="block text-sm font-medium text-gray-700 mb-1">Color Precision</label>
|
| 114 |
-
<select id="colorPrecision" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm">
|
| 115 |
-
<option value="1">Low (fewer colors)</option>
|
| 116 |
-
<option value="2" selected>Medium</option>
|
| 117 |
-
<option value="3">High (more colors)</option>
|
| 118 |
-
</select>
|
| 119 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
| 128 |
</div>
|
| 129 |
-
</div>
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
| 138 |
</div>
|
| 139 |
-
</div>
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
</div>
|
| 145 |
|
| 146 |
<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>
|
| 147 |
-
Convert to SVG
|
| 148 |
</button>
|
| 149 |
</div>
|
| 150 |
</div>
|
|
@@ -166,7 +210,7 @@
|
|
| 166 |
<div id="processingUI" class="hidden text-center py-12">
|
| 167 |
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
|
| 168 |
<h3 class="text-lg font-medium text-gray-700">Processing Image</h3>
|
| 169 |
-
<p class="text-gray-500 text-sm mt-1">
|
| 170 |
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-4">
|
| 171 |
<div id="progressBar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div>
|
| 172 |
</div>
|
|
@@ -177,6 +221,26 @@
|
|
| 177 |
<div id="svgPreview" class="mx-auto"></div>
|
| 178 |
</div>
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
<div>
|
| 181 |
<div class="flex items-center justify-between mb-2">
|
| 182 |
<h4 class="text-sm font-medium text-gray-700">SVG Code</h4>
|
|
@@ -200,7 +264,7 @@
|
|
| 200 |
</div>
|
| 201 |
|
| 202 |
<footer class="text-center mt-12 text-gray-500 text-sm">
|
| 203 |
-
<p>Image to SVG Vectorizer © 2023 |
|
| 204 |
</footer>
|
| 205 |
</div>
|
| 206 |
|
|
@@ -227,6 +291,41 @@
|
|
| 227 |
const downloadBtn = document.getElementById('downloadBtn');
|
| 228 |
const copyBtn = document.getElementById('copyBtn');
|
| 229 |
const copyCodeBtn = document.getElementById('copyCodeBtn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
// Prevent default drag behaviors
|
| 232 |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
@@ -292,6 +391,7 @@
|
|
| 292 |
// Handle selected files
|
| 293 |
function handleFiles(files) {
|
| 294 |
const file = files[0];
|
|
|
|
| 295 |
|
| 296 |
// Check if file is an image
|
| 297 |
if (!file.type.match('image.*')) {
|
|
@@ -299,9 +399,9 @@
|
|
| 299 |
return;
|
| 300 |
}
|
| 301 |
|
| 302 |
-
// Check file size (max
|
| 303 |
-
if (file.size >
|
| 304 |
-
showError('File size exceeds
|
| 305 |
return;
|
| 306 |
}
|
| 307 |
|
|
@@ -317,6 +417,15 @@
|
|
| 317 |
imagePreview.src = e.target.result;
|
| 318 |
previewContainer.classList.remove('hidden');
|
| 319 |
convertBtn.disabled = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
};
|
| 321 |
reader.readAsDataURL(file);
|
| 322 |
}
|
|
@@ -345,6 +454,7 @@
|
|
| 345 |
// Reset upload
|
| 346 |
function resetUpload() {
|
| 347 |
fileInput.value = '';
|
|
|
|
| 348 |
uploadUI.style.display = 'flex';
|
| 349 |
fileInfo.style.display = 'none';
|
| 350 |
previewContainer.classList.add('hidden');
|
|
@@ -353,49 +463,158 @@
|
|
| 353 |
resultContainer.classList.add('hidden');
|
| 354 |
}
|
| 355 |
|
| 356 |
-
// Process image
|
| 357 |
function processImage() {
|
| 358 |
// Show processing UI
|
| 359 |
processingUI.classList.remove('hidden');
|
| 360 |
emptyState.classList.add('hidden');
|
| 361 |
resultContainer.classList.add('hidden');
|
| 362 |
|
| 363 |
-
// Simulate
|
| 364 |
let progress = 0;
|
| 365 |
-
const
|
| 366 |
progress += Math.random() * 10;
|
| 367 |
-
if (progress >
|
| 368 |
progressBar.style.width = progress + '%';
|
|
|
|
| 369 |
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
}
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
}
|
| 376 |
|
| 377 |
// Show result
|
| 378 |
-
function showResult() {
|
| 379 |
processingUI.classList.add('hidden');
|
| 380 |
resultContainer.classList.remove('hidden');
|
| 381 |
emptyState.classList.add('hidden');
|
| 382 |
downloadBtn.classList.remove('hidden');
|
| 383 |
copyBtn.classList.remove('hidden');
|
| 384 |
|
| 385 |
-
//
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
svgCode.textContent = sampleSVG;
|
| 389 |
-
}
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
// Copy SVG to clipboard
|
|
@@ -429,7 +648,7 @@
|
|
| 429 |
const url = URL.createObjectURL(blob);
|
| 430 |
const a = document.createElement('a');
|
| 431 |
a.href = url;
|
| 432 |
-
a.download = fileName.textContent.replace(/\.[^/.]+$/, "") + '.svg';
|
| 433 |
document.body.appendChild(a);
|
| 434 |
a.click();
|
| 435 |
document.body.removeChild(a);
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Advanced Image to SVG Vectorizer</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/potrace@2.1.8/dist/potrace.min.js"></script>
|
| 10 |
<style>
|
| 11 |
.dropzone {
|
| 12 |
border: 2px dashed #cbd5e0;
|
|
|
|
| 59 |
.file-info {
|
| 60 |
display: none;
|
| 61 |
}
|
| 62 |
+
.settings-panel {
|
| 63 |
+
transition: all 0.3s ease;
|
| 64 |
+
}
|
| 65 |
+
.settings-panel.collapsed {
|
| 66 |
+
max-height: 0;
|
| 67 |
+
overflow: hidden;
|
| 68 |
+
opacity: 0;
|
| 69 |
+
}
|
| 70 |
+
.settings-panel.expanded {
|
| 71 |
+
max-height: 500px;
|
| 72 |
+
opacity: 1;
|
| 73 |
+
}
|
| 74 |
</style>
|
| 75 |
</head>
|
| 76 |
<body class="bg-gray-50 min-h-screen">
|
| 77 |
<div class="container mx-auto px-4 py-8">
|
| 78 |
<header class="text-center mb-12">
|
| 79 |
<h1 class="text-4xl font-bold text-indigo-600 mb-2">
|
| 80 |
+
<i class="fas fa-vector-square mr-2"></i>Advanced Image to SVG Vectorizer
|
| 81 |
</h1>
|
| 82 |
<p class="text-gray-600 max-w-2xl mx-auto">
|
| 83 |
+
Convert raster images to high-quality scalable vector graphics with advanced settings
|
| 84 |
</p>
|
| 85 |
</header>
|
| 86 |
|
|
|
|
| 120 |
</div>
|
| 121 |
|
| 122 |
<div class="space-y-4">
|
| 123 |
+
<div class="flex justify-between items-center cursor-pointer" id="settingsToggle">
|
| 124 |
+
<h3 class="text-lg font-medium text-gray-700">Vectorization Settings</h3>
|
| 125 |
+
<i class="fas fa-chevron-down text-gray-500 transition-transform duration-300" id="toggleIcon"></i>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
</div>
|
| 127 |
+
|
| 128 |
+
<div id="settingsPanel" class="settings-panel expanded space-y-4">
|
| 129 |
+
<div>
|
| 130 |
+
<label for="vectorMode" class="block text-sm font-medium text-gray-700 mb-1">Vectorization Mode</label>
|
| 131 |
+
<select id="vectorMode" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm">
|
| 132 |
+
<option value="posterize">Posterize (color blocks)</option>
|
| 133 |
+
<option value="grayscale">Grayscale</option>
|
| 134 |
+
<option value="blackwhite" selected>Black & White</option>
|
| 135 |
+
</select>
|
| 136 |
+
</div>
|
| 137 |
|
| 138 |
+
<div id="colorSettings">
|
| 139 |
+
<label for="colorCount" class="block text-sm font-medium text-gray-700 mb-1">Color Count</label>
|
| 140 |
+
<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">
|
| 141 |
+
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
| 142 |
+
<span>2</span>
|
| 143 |
+
<span>4</span>
|
| 144 |
+
<span>8</span>
|
| 145 |
+
<span>16</span>
|
| 146 |
+
</div>
|
| 147 |
</div>
|
|
|
|
| 148 |
|
| 149 |
+
<div>
|
| 150 |
+
<label for="threshold" class="block text-sm font-medium text-gray-700 mb-1">Threshold</label>
|
| 151 |
+
<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">
|
| 152 |
+
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
| 153 |
+
<span>Low</span>
|
| 154 |
+
<span>Medium</span>
|
| 155 |
+
<span>High</span>
|
| 156 |
+
</div>
|
| 157 |
</div>
|
|
|
|
| 158 |
|
| 159 |
+
<div>
|
| 160 |
+
<label for="smoothness" class="block text-sm font-medium text-gray-700 mb-1">Smoothness</label>
|
| 161 |
+
<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">
|
| 162 |
+
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
| 163 |
+
<span>Sharp</span>
|
| 164 |
+
<span>Balanced</span>
|
| 165 |
+
<span>Smooth</span>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div>
|
| 170 |
+
<label for="detailLevel" class="block text-sm font-medium text-gray-700 mb-1">Detail Level</label>
|
| 171 |
+
<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">
|
| 172 |
+
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
| 173 |
+
<span>Low</span>
|
| 174 |
+
<span>Medium</span>
|
| 175 |
+
<span>High</span>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<div class="flex items-center">
|
| 180 |
+
<input type="checkbox" id="transparentBg" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
| 181 |
+
<label for="transparentBg" class="ml-2 block text-sm text-gray-700">Transparent background</label>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<div class="flex items-center">
|
| 185 |
+
<input type="checkbox" id="optimizePaths" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" checked>
|
| 186 |
+
<label for="optimizePaths" class="ml-2 block text-sm text-gray-700">Optimize paths</label>
|
| 187 |
+
</div>
|
| 188 |
</div>
|
| 189 |
|
| 190 |
<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>
|
| 191 |
+
<i class="fas fa-magic mr-2"></i> Convert to SVG
|
| 192 |
</button>
|
| 193 |
</div>
|
| 194 |
</div>
|
|
|
|
| 210 |
<div id="processingUI" class="hidden text-center py-12">
|
| 211 |
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
|
| 212 |
<h3 class="text-lg font-medium text-gray-700">Processing Image</h3>
|
| 213 |
+
<p class="text-gray-500 text-sm mt-1">Vectorizing your image...</p>
|
| 214 |
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-4">
|
| 215 |
<div id="progressBar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div>
|
| 216 |
</div>
|
|
|
|
| 221 |
<div id="svgPreview" class="mx-auto"></div>
|
| 222 |
</div>
|
| 223 |
|
| 224 |
+
<div class="mb-4">
|
| 225 |
+
<div class="flex items-center justify-between mb-2">
|
| 226 |
+
<h4 class="text-sm font-medium text-gray-700">SVG Statistics</h4>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="grid grid-cols-3 gap-2 text-xs">
|
| 229 |
+
<div class="bg-gray-100 p-2 rounded">
|
| 230 |
+
<div class="text-gray-500">File Size</div>
|
| 231 |
+
<div id="svgSize" class="font-medium">-</div>
|
| 232 |
+
</div>
|
| 233 |
+
<div class="bg-gray-100 p-2 rounded">
|
| 234 |
+
<div class="text-gray-500">Path Count</div>
|
| 235 |
+
<div id="pathCount" class="font-medium">-</div>
|
| 236 |
+
</div>
|
| 237 |
+
<div class="bg-gray-100 p-2 rounded">
|
| 238 |
+
<div class="text-gray-500">Colors</div>
|
| 239 |
+
<div id="colorCountDisplay" class="font-medium">-</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
<div>
|
| 245 |
<div class="flex items-center justify-between mb-2">
|
| 246 |
<h4 class="text-sm font-medium text-gray-700">SVG Code</h4>
|
|
|
|
| 264 |
</div>
|
| 265 |
|
| 266 |
<footer class="text-center mt-12 text-gray-500 text-sm">
|
| 267 |
+
<p>Advanced Image to SVG Vectorizer © 2023 | Powered by Potrace algorithm</p>
|
| 268 |
</footer>
|
| 269 |
</div>
|
| 270 |
|
|
|
|
| 291 |
const downloadBtn = document.getElementById('downloadBtn');
|
| 292 |
const copyBtn = document.getElementById('copyBtn');
|
| 293 |
const copyCodeBtn = document.getElementById('copyCodeBtn');
|
| 294 |
+
const settingsToggle = document.getElementById('settingsToggle');
|
| 295 |
+
const settingsPanel = document.getElementById('settingsPanel');
|
| 296 |
+
const toggleIcon = document.getElementById('toggleIcon');
|
| 297 |
+
const vectorMode = document.getElementById('vectorMode');
|
| 298 |
+
const colorCount = document.getElementById('colorCount');
|
| 299 |
+
const threshold = document.getElementById('threshold');
|
| 300 |
+
const smoothness = document.getElementById('smoothness');
|
| 301 |
+
const detailLevel = document.getElementById('detailLevel');
|
| 302 |
+
const transparentBg = document.getElementById('transparentBg');
|
| 303 |
+
const optimizePaths = document.getElementById('optimizePaths');
|
| 304 |
+
const colorSettings = document.getElementById('colorSettings');
|
| 305 |
+
const svgSize = document.getElementById('svgSize');
|
| 306 |
+
const pathCount = document.getElementById('pathCount');
|
| 307 |
+
const colorCountDisplay = document.getElementById('colorCountDisplay');
|
| 308 |
+
|
| 309 |
+
// Current file data
|
| 310 |
+
let currentFile = null;
|
| 311 |
+
let canvas = document.createElement('canvas');
|
| 312 |
+
let ctx = canvas.getContext('2d');
|
| 313 |
+
|
| 314 |
+
// Toggle settings panel
|
| 315 |
+
settingsToggle.addEventListener('click', () => {
|
| 316 |
+
settingsPanel.classList.toggle('expanded');
|
| 317 |
+
settingsPanel.classList.toggle('collapsed');
|
| 318 |
+
toggleIcon.classList.toggle('rotate-180');
|
| 319 |
+
});
|
| 320 |
+
|
| 321 |
+
// Toggle color settings based on vector mode
|
| 322 |
+
vectorMode.addEventListener('change', () => {
|
| 323 |
+
if (vectorMode.value === 'posterize') {
|
| 324 |
+
colorSettings.style.display = 'block';
|
| 325 |
+
} else {
|
| 326 |
+
colorSettings.style.display = 'none';
|
| 327 |
+
}
|
| 328 |
+
});
|
| 329 |
|
| 330 |
// Prevent default drag behaviors
|
| 331 |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
|
|
| 391 |
// Handle selected files
|
| 392 |
function handleFiles(files) {
|
| 393 |
const file = files[0];
|
| 394 |
+
currentFile = file;
|
| 395 |
|
| 396 |
// Check if file is an image
|
| 397 |
if (!file.type.match('image.*')) {
|
|
|
|
| 399 |
return;
|
| 400 |
}
|
| 401 |
|
| 402 |
+
// Check file size (max 10MB)
|
| 403 |
+
if (file.size > 10 * 1024 * 1024) {
|
| 404 |
+
showError('File size exceeds 10MB limit');
|
| 405 |
return;
|
| 406 |
}
|
| 407 |
|
|
|
|
| 417 |
imagePreview.src = e.target.result;
|
| 418 |
previewContainer.classList.remove('hidden');
|
| 419 |
convertBtn.disabled = false;
|
| 420 |
+
|
| 421 |
+
// Load image to canvas for processing
|
| 422 |
+
const img = new Image();
|
| 423 |
+
img.onload = function() {
|
| 424 |
+
canvas.width = img.width;
|
| 425 |
+
canvas.height = img.height;
|
| 426 |
+
ctx.drawImage(img, 0, 0);
|
| 427 |
+
};
|
| 428 |
+
img.src = e.target.result;
|
| 429 |
};
|
| 430 |
reader.readAsDataURL(file);
|
| 431 |
}
|
|
|
|
| 454 |
// Reset upload
|
| 455 |
function resetUpload() {
|
| 456 |
fileInput.value = '';
|
| 457 |
+
currentFile = null;
|
| 458 |
uploadUI.style.display = 'flex';
|
| 459 |
fileInfo.style.display = 'none';
|
| 460 |
previewContainer.classList.add('hidden');
|
|
|
|
| 463 |
resultContainer.classList.add('hidden');
|
| 464 |
}
|
| 465 |
|
| 466 |
+
// Process image with vectorization
|
| 467 |
function processImage() {
|
| 468 |
// Show processing UI
|
| 469 |
processingUI.classList.remove('hidden');
|
| 470 |
emptyState.classList.add('hidden');
|
| 471 |
resultContainer.classList.add('hidden');
|
| 472 |
|
| 473 |
+
// Simulate progress
|
| 474 |
let progress = 0;
|
| 475 |
+
const progressInterval = setInterval(() => {
|
| 476 |
progress += Math.random() * 10;
|
| 477 |
+
if (progress > 90) progress = 90;
|
| 478 |
progressBar.style.width = progress + '%';
|
| 479 |
+
}, 200);
|
| 480 |
|
| 481 |
+
// Process image after a short delay to allow UI to update
|
| 482 |
+
setTimeout(() => {
|
| 483 |
+
// Get settings from UI
|
| 484 |
+
const settings = {
|
| 485 |
+
mode: vectorMode.value,
|
| 486 |
+
colorCount: parseInt(colorCount.value),
|
| 487 |
+
threshold: parseInt(threshold.value) / 100,
|
| 488 |
+
smoothness: parseInt(smoothness.value),
|
| 489 |
+
detail: parseInt(detailLevel.value),
|
| 490 |
+
transparent: transparentBg.checked,
|
| 491 |
+
optimize: optimizePaths.checked
|
| 492 |
+
};
|
| 493 |
+
|
| 494 |
+
// Process the image based on selected mode
|
| 495 |
+
let processedImageData;
|
| 496 |
+
if (settings.mode === 'posterize') {
|
| 497 |
+
processedImageData = posterizeImage(canvas, settings.colorCount);
|
| 498 |
+
} else if (settings.mode === 'grayscale') {
|
| 499 |
+
processedImageData = convertToGrayscale(canvas);
|
| 500 |
+
} else { // blackwhite
|
| 501 |
+
processedImageData = applyThreshold(canvas, settings.threshold);
|
| 502 |
}
|
| 503 |
+
|
| 504 |
+
// Create temporary canvas with processed image
|
| 505 |
+
const tempCanvas = document.createElement('canvas');
|
| 506 |
+
tempCanvas.width = canvas.width;
|
| 507 |
+
tempCanvas.height = canvas.height;
|
| 508 |
+
const tempCtx = tempCanvas.getContext('2d');
|
| 509 |
+
tempCtx.putImageData(processedImageData, 0, 0);
|
| 510 |
+
|
| 511 |
+
// Use Potrace to vectorize the image
|
| 512 |
+
const imageDataURL = tempCanvas.toDataURL('image/png');
|
| 513 |
+
|
| 514 |
+
// Update progress
|
| 515 |
+
progressBar.style.width = '95%';
|
| 516 |
+
|
| 517 |
+
// Vectorize using Potrace
|
| 518 |
+
potrace.loadImageFromUrl(imageDataURL);
|
| 519 |
+
potrace.setParameters({
|
| 520 |
+
turdsize: Math.max(1, 10 - settings.detail), // Fewer details with higher values
|
| 521 |
+
optcurve: settings.optimize,
|
| 522 |
+
alphamax: settings.smoothness * 1.4, // Controls corner threshold
|
| 523 |
+
opttolerance: settings.smoothness * 0.5 // Optimization tolerance
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
potrace.process(() => {
|
| 527 |
+
clearInterval(progressInterval);
|
| 528 |
+
progressBar.style.width = '100%';
|
| 529 |
+
|
| 530 |
+
// Get SVG data
|
| 531 |
+
const svg = potrace.getSVG(1, settings.transparent ? 'none' : '#ffffff');
|
| 532 |
+
|
| 533 |
+
// Show result after a short delay
|
| 534 |
+
setTimeout(() => {
|
| 535 |
+
showResult(svg);
|
| 536 |
+
}, 300);
|
| 537 |
+
});
|
| 538 |
+
}, 500);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
// Image processing functions
|
| 542 |
+
function posterizeImage(canvas, levels) {
|
| 543 |
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
| 544 |
+
const data = imageData.data;
|
| 545 |
+
|
| 546 |
+
// Calculate the step for each channel
|
| 547 |
+
const step = 255 / (levels - 1);
|
| 548 |
+
|
| 549 |
+
for (let i = 0; i < data.length; i += 4) {
|
| 550 |
+
// Posterize each channel
|
| 551 |
+
data[i] = Math.round(data[i] / step) * step; // R
|
| 552 |
+
data[i + 1] = Math.round(data[i + 1] / step) * step; // G
|
| 553 |
+
data[i + 2] = Math.round(data[i + 2] / step) * step; // B
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
return imageData;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
function convertToGrayscale(canvas) {
|
| 560 |
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
| 561 |
+
const data = imageData.data;
|
| 562 |
+
|
| 563 |
+
for (let i = 0; i < data.length; i += 4) {
|
| 564 |
+
// Convert to grayscale using luminance
|
| 565 |
+
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
| 566 |
+
data[i] = data[i + 1] = data[i + 2] = gray;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
return imageData;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
function applyThreshold(canvas, thresholdValue) {
|
| 573 |
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
| 574 |
+
const data = imageData.data;
|
| 575 |
+
|
| 576 |
+
for (let i = 0; i < data.length; i += 4) {
|
| 577 |
+
// Convert to grayscale first
|
| 578 |
+
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
| 579 |
+
// Apply threshold
|
| 580 |
+
const value = gray > (thresholdValue * 255) ? 255 : 0;
|
| 581 |
+
data[i] = data[i + 1] = data[i + 2] = value;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
return imageData;
|
| 585 |
}
|
| 586 |
|
| 587 |
// Show result
|
| 588 |
+
function showResult(svgData) {
|
| 589 |
processingUI.classList.add('hidden');
|
| 590 |
resultContainer.classList.remove('hidden');
|
| 591 |
emptyState.classList.add('hidden');
|
| 592 |
downloadBtn.classList.remove('hidden');
|
| 593 |
copyBtn.classList.remove('hidden');
|
| 594 |
|
| 595 |
+
// Display SVG
|
| 596 |
+
svgPreview.innerHTML = svgData;
|
| 597 |
+
svgCode.textContent = svgData;
|
|
|
|
|
|
|
| 598 |
|
| 599 |
+
// Calculate and display statistics
|
| 600 |
+
const svgSizeBytes = new Blob([svgData]).size;
|
| 601 |
+
svgSize.textContent = formatFileSize(svgSizeBytes);
|
| 602 |
+
|
| 603 |
+
// Count paths in SVG
|
| 604 |
+
const pathMatches = svgData.match(/<path/g);
|
| 605 |
+
const pathCountValue = pathMatches ? pathMatches.length : 0;
|
| 606 |
+
pathCount.textContent = pathCountValue;
|
| 607 |
+
|
| 608 |
+
// Count colors in SVG
|
| 609 |
+
const colorMatches = svgData.match(/fill="([^"]+)"/g);
|
| 610 |
+
let uniqueColors = new Set();
|
| 611 |
+
if (colorMatches) {
|
| 612 |
+
colorMatches.forEach(match => {
|
| 613 |
+
const color = match.match(/fill="([^"]+)"/)[1];
|
| 614 |
+
uniqueColors.add(color.toLowerCase());
|
| 615 |
+
});
|
| 616 |
+
}
|
| 617 |
+
colorCountDisplay.textContent = uniqueColors.size;
|
| 618 |
}
|
| 619 |
|
| 620 |
// Copy SVG to clipboard
|
|
|
|
| 648 |
const url = URL.createObjectURL(blob);
|
| 649 |
const a = document.createElement('a');
|
| 650 |
a.href = url;
|
| 651 |
+
a.download = (fileName.textContent.replace(/\.[^/.]+$/, "") || 'vectorized') + '.svg';
|
| 652 |
document.body.appendChild(a);
|
| 653 |
a.click();
|
| 654 |
document.body.removeChild(a);
|