syscycle's picture
Update src/App.tsx
0db57c0 verified
Raw
History Blame Contribute Delete
26.1 kB
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { UploadCloud, Download, Image as ImageIcon, Sparkles, Settings2, Loader2 } from 'lucide-react';
// @ts-ignore
import ImageTracer from 'imagetracerjs';
import { cn } from './lib/utils';
export default function App() {
const [originalImage, setOriginalImage] = useState<string | null>(null);
const [svgString, setSvgString] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
// Vectorization parameters
const [colorCount, setColorCount] = useState<number>(16);
// 1. Pre-Processing (Anti-Noise & Oversampling)
const [upscale, setUpscale] = useState<number>(1); // 1 to 4
const [preBlur, setPreBlur] = useState<number>(0); // 0 to 5px Gaussian Blur
const [pixelatedUpscale, setPixelatedUpscale] = useState<boolean>(false); // Use nearest-neighbor instead of smooth
// 2. Tracing & Curve Fitting
const [curveFitting, setCurveFitting] = useState<number>(1); // qtres
const [lineFitting, setLineFitting] = useState<number>(1); // ltres
const [pathOmit, setPathOmit] = useState<number>(8); // Remove small segments
const [cornerDetection, setCornerDetection] = useState<boolean>(true); // rightangleenhance
// 3. Rendering
const [strokeWidth, setStrokeWidth] = useState<number>(1); // 0 to 5
const [layering, setLayering] = useState<number>(0); // 0 (sequential) or 1 (parallel)
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const processImage = useCallback(() => {
if (!originalImage) return;
setIsProcessing(true);
// Give UI time to update processing state
setTimeout(() => {
try {
const options = {
numberofcolors: colorCount,
qtres: curveFitting,
ltres: lineFitting,
pathomit: pathOmit,
blurradius: 0, // Using canvas pre-blur instead for exact gaussian control
blurdelta: 20, // default
strokewidth: strokeWidth,
layering: layering,
linefilter: true,
rightangleenhance: cornerDetection,
colorsampling: 2,
roundcoords: 2
};
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = () => {
let scale = upscale;
// Safety: avoid browser crashes on huge images
const maxDim = Math.max(img.width, img.height);
if (maxDim * scale > 2500) {
scale = Math.max(1, 2500 / maxDim);
}
const canvas = document.createElement('canvas');
const finalWidth = Math.round(img.width * scale);
const finalHeight = Math.round(img.height * scale);
canvas.width = finalWidth;
canvas.height = finalHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
if (scale > 1) {
if (pixelatedUpscale) {
ctx.imageSmoothingEnabled = false;
} else {
// Smooth interpolation to blur the stair-stepped pixels of low-res images
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
}
}
// Makaledeki 1. Teknik: Gaussian Blur (Anti-Noise)
if (preBlur > 0) {
ctx.filter = `blur(${preBlur}px)`;
}
// Draw the source image onto the upscaled canvas
ctx.drawImage(img, 0, 0, finalWidth, finalHeight);
// Reset filter so we can read it safely
ctx.filter = 'none';
const imgData = ctx.getImageData(0, 0, finalWidth, finalHeight);
// Vectorize
const svgstr = ImageTracer.imagedataToSVG(imgData, options);
// Critical Fix: ImageTracer's `strokewidth` creates sharp "miter" spikes on curved boundaries.
// We force rounded stroke joins globally in the SVG to eliminate these spikes.
const fixedSvg = svgstr.replace('<svg ', '<svg stroke-linejoin="round" stroke-linecap="round" ');
setSvgString(fixedSvg);
}
setIsProcessing(false);
};
img.onerror = () => {
setIsProcessing(false);
console.error("Failed to load image");
};
img.src = originalImage;
} catch (error) {
console.error("Vectorization failed:", error);
alert("Failed to vectorize image.");
setIsProcessing(false);
}
}, 50);
}, [originalImage, colorCount, curveFitting, lineFitting, pathOmit, cornerDetection, strokeWidth, layering, upscale, preBlur, pixelatedUpscale]);
// Re-process when parameters change
useEffect(() => {
// Add a small debounce if needed, but for now just process
const timeout = setTimeout(() => {
processImage();
}, 300);
return () => clearTimeout(timeout);
}, [processImage]);
const handleFileUpload = (file: File) => {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result && typeof e.target.result === 'string') {
const imageUrl = e.target.result;
setOriginalImage(imageUrl);
}
};
reader.readAsDataURL(file);
};
const onDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const onDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
};
const onDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFileUpload(e.dataTransfer.files[0]);
}
};
const handleDownload = () => {
if (!svgString) return;
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'vectorized_image.svg';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const hasImage = !!originalImage;
return (
<div className="min-h-screen bg-[#09090B] text-[#E4E4E7] font-sans flex flex-col">
{/* Header */}
<header className="h-16 border-b border-white/10 flex items-center justify-between px-6 bg-[#09090B] sticky top-0 z-10">
<div className="flex items-center gap-3 text-white">
<div className="w-8 h-8 bg-indigo-600 rounded flex items-center justify-center font-bold text-white shadow-lg shadow-indigo-500/20">
<Sparkles className="w-4 h-4" />
</div>
<h1 className="font-semibold text-lg tracking-tight">Vectorify <span className="text-indigo-400 font-normal">Pro</span></h1>
</div>
<div className="text-sm font-medium text-gray-400 hidden sm:block">
Flawless Image Vectorization
</div>
</header>
<main className="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8 flex flex-col lg:flex-row gap-6">
{/* Main Content Area */}
<div className="flex-1 flex flex-col min-w-0">
{!hasImage ? (
<div
className={cn(
"flex-1 flex flex-col items-center justify-center border-2 border-dashed rounded-xl transition-all duration-200 bg-[#121214]",
isDragging ? "border-indigo-500 bg-indigo-500/10" : "border-white/10 hover:border-white/20"
)}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="p-8 text-center cursor-pointer flex flex-col items-center">
<div className="w-16 h-16 bg-indigo-500/20 rounded border border-indigo-500/30 flex items-center justify-center mb-6 shadow-[0_0_15px_rgba(99,102,241,0.2)]">
<UploadCloud className="w-8 h-8 text-indigo-400" />
</div>
<h2 className="text-xl font-bold text-gray-200 mb-2">Upload an Image</h2>
<p className="text-gray-400 mb-8 max-w-sm text-sm">
Drag and drop a PNG, JPG, or GIF here, or click to browse. We'll automatically convert it to a crisp SVG.
</p>
<button className="px-6 py-2.5 bg-indigo-600 text-white rounded-full font-bold text-sm shadow-lg shadow-indigo-600/20 hover:bg-indigo-500 transition w-full sm:w-auto">
Browse Files
</button>
</div>
</div>
) : (
<div className="flex-1 grid grid-rows-2 lg:grid-rows-1 lg:grid-cols-2 gap-4 lg:gap-6">
{/* Original Image Box */}
<div className="bg-[#18181B] rounded-xl border border-white/5 shadow-sm overflow-hidden flex flex-col">
<div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center justify-between">
<div className="flex items-center text-gray-400 font-bold text-[10px] uppercase tracking-widest">
<ImageIcon className="w-3.5 h-3.5 mr-2 text-gray-500" />
Original
</div>
<button
onClick={() => { setOriginalImage(null); setSvgString(null); }}
className="text-[10px] font-bold uppercase tracking-widest text-gray-400 hover:text-white bg-white/5 hover:bg-white/10 px-2 py-1 rounded border border-white/10 transition-colors"
>
Start Over
</button>
</div>
<div className="flex-1 p-4 flex items-center justify-center bg-[#121214] bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiIHZpZXdCb3g9IjAgMCA4IDgiPjxyZWN0IHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9IiMxYjFiMWUiLz48cGF0aCBkPSJNMCAwaDR2NEgwem00IDRoNHY0SDR6IiBmaWxsPSIjMjEyMTI2Ii8+PC9zdmc+')] shadow-inner relative">
<img src={originalImage} alt="Original uploaded image" className="max-w-full max-h-[500px] object-contain drop-shadow-2xl rounded" />
</div>
</div>
{/* Vectorized Result Box */}
<div className="bg-[#18181B] rounded-xl border border-white/5 shadow-sm overflow-hidden flex flex-col relative">
<div className="px-4 py-3 border-b border-white/5 bg-black/20 flex items-center justify-between">
<div className="flex items-center text-indigo-400 font-bold text-[10px] uppercase tracking-widest">
<Sparkles className="w-3.5 h-3.5 mr-2" />
Vectorized (SVG)
</div>
{isProcessing && (
<div className="flex items-center text-[10px] font-bold uppercase tracking-widest text-indigo-300">
<Loader2 className="w-3 h-3 mr-1.5 animate-spin" />
Processing...
</div>
)}
</div>
<div className="flex-1 p-4 flex items-center justify-center bg-[#121214] bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiIHZpZXdCb3g9IjAgMCA4IDgiPjxyZWN0IHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9IiMxYjFiMWUiLz48cGF0aCBkPSJNMCAwaDR2NEgwem00IDRoNHY0SDR6IiBmaWxsPSIjMjEyMTI2Ii8+PC9zdmc+')] shadow-inner relative">
{svgString ? (
<div
className="max-w-full max-h-[500px] flex items-center justify-center drop-shadow-2xl [&>svg]:w-full [&>svg]:max-h-[500px] [&>svg]:h-auto"
dangerouslySetInnerHTML={{ __html: svgString }}
/>
) : (
<div className="text-gray-600 flex flex-col items-center">
<ImageIcon className="w-8 h-8 mb-2 opacity-50" />
<span className="text-xs font-medium uppercase tracking-widest">Preview Area</span>
</div>
)}
</div>
{/* Overlay while processing */}
{isProcessing && (
<div className="absolute inset-0 top-11 bg-[#09090B]/60 backdrop-blur-sm flex items-center justify-center z-10">
<div className="bg-[#18181B] px-6 py-4 rounded border border-white/10 shadow-2xl flex flex-col items-center">
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin mb-3" />
<span className="text-[10px] font-bold uppercase tracking-widest text-indigo-400">Tracing paths...</span>
</div>
</div>
)}
</div>
</div>
)}
<input
type="file"
accept="image/*"
className="hidden"
ref={fileInputRef}
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
}}
/>
</div>
{/* Sidebar Settings (Disabled if no image) */}
<div className={cn(
"w-full lg:w-72 flex flex-col transition-opacity duration-300",
!hasImage ? "opacity-30 pointer-events-none" : "opacity-100"
)}>
<div className="bg-[#09090B] rounded-xl border border-white/10 p-6 flex flex-col h-full lg:h-auto sticky top-24">
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-white/5">
<Settings2 className="w-4 h-4 text-indigo-500" />
<h3 className="text-[10px] uppercase tracking-widest text-gray-400 font-bold">Vectorization Settings</h3>
</div>
<div className="mb-6 flex-1 space-y-5 overflow-y-auto pr-1">
{/* 1. Low Res Curve Fixes */}
<div className="p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-lg space-y-4">
<h4 className="text-[10px] uppercase tracking-widest text-indigo-400 font-bold mb-3">1. Görüntü Ön İşleme (Anti-Noise)</h4>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Oversampling (Upscale)</label>
<span className="text-xs font-mono text-indigo-400">{upscale}x</span>
</div>
<input
type="range" min="1" max="4" step="1" value={upscale}
onChange={(e) => setUpscale(parseInt(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
<p className="text-[9px] text-gray-400 mt-1">Teknik 7: İnce detayları yakalamak için çözünürlüğü artırır.</p>
</div>
<div>
<label className="flex items-center justify-between cursor-pointer group">
<span className="text-[10px] uppercase tracking-widest text-gray-400 group-hover:text-white transition">Pürüzlü Büyüt (Halo Önleyici)</span>
<div className="relative inline-block w-8 h-4 rounded-full bg-white/10 transition-colors group-hover:bg-white/20">
<input
type="checkbox"
className="peer absolute opacity-0 w-0 h-0"
checked={pixelatedUpscale}
onChange={(e) => setPixelatedUpscale(e.target.checked)}
/>
<span className="absolute left-[2px] top-[2px] w-3 h-3 rounded-full bg-gray-400 transition-all peer-checked:bg-indigo-400 peer-checked:translate-x-4"></span>
</div>
</label>
<p className="text-[9px] text-gray-400 mt-2 leading-relaxed">Etkinken, büyütme işleminde yumuşatma (anti-aliasing) yapmaz. <strong>Büyütme veya Blur işleminden dolayı oluşan renk katmanlarını (Halo/aura etkisi) önlemek için bunu açın veya Renk Sayısını (menünün altında) azaltın!</strong></p>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Gaussian Blur</label>
<span className="text-xs font-mono text-indigo-400">{preBlur}px</span>
</div>
<input
type="range" min="0" max="5" step="0.5" value={preBlur}
onChange={(e) => setPreBlur(parseFloat(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
<p className="text-[9px] text-gray-400 mt-1">Teknik 1: Pütürleri/stair-step pikselleri yumuşatıp hatları belli eder.</p>
</div>
</div>
{/* 2. Curve Fitting & Tracing */}
<div className="p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-lg space-y-4">
<h4 className="text-[10px] uppercase tracking-widest text-indigo-400 font-bold mb-3">2. Vector Tracing & Curve Fitting</h4>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Bezier Curve Fitting Toleransı</label>
<span className="text-xs font-mono text-indigo-400">{curveFitting}</span>
</div>
<input
type="range" min="0.1" max="10" step="0.1" value={curveFitting}
onChange={(e) => setCurveFitting(parseFloat(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
<p className="text-[9px] text-gray-400 mt-1">Teknik 3 & 4: Eğrileri bezier olarak uydurur. Yüksek değer zigzagları yok edip daha pürüzsüz sonuç verir.</p>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Düz Çizgi Toleransı</label>
<span className="text-xs font-mono text-indigo-400">{lineFitting}</span>
</div>
<input
type="range" min="0.1" max="10" step="0.1" value={lineFitting}
onChange={(e) => setLineFitting(parseFloat(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">Küçük Segmentleri Sil</label>
<span className="text-xs font-mono text-indigo-400">{pathOmit}</span>
</div>
<input
type="range" min="0" max="64" step="1" value={pathOmit}
onChange={(e) => setPathOmit(parseInt(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
<p className="text-[9px] text-gray-400 mt-1">Teknik 5: Yüksek frekanslı noise'ları ve gereksiz noktaları çöpe atar.</p>
</div>
<div>
<label className="flex items-center justify-between cursor-pointer group">
<span className="text-[10px] uppercase tracking-widest text-gray-400 group-hover:text-white transition">Köşe Koruma (Corner Detect)</span>
<div className="relative inline-block w-8 h-4 rounded-full bg-white/10 transition-colors group-hover:bg-white/20">
<input
type="checkbox"
className="peer absolute opacity-0 w-0 h-0"
checked={cornerDetection}
onChange={(e) => setCornerDetection(e.target.checked)}
/>
<span className="absolute left-[2px] top-[2px] w-3 h-3 rounded-full bg-gray-400 transition-all peer-checked:bg-indigo-400 peer-checked:translate-x-4"></span>
</div>
</label>
<p className="text-[9px] text-gray-400 mt-2 leading-relaxed">Teknik 6: Keskin köşeleri tespit edip muhafaza eder, kalan yerleri curve smoothing ile yumuşatır.</p>
</div>
</div>
<div className="h-px bg-white/5 w-full my-4"></div>
{/* Colors */}
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">
Colors
</label>
<span className="text-xs font-mono text-indigo-400">{colorCount}</span>
</div>
<input
type="range" min="2" max="64" value={colorCount}
onChange={(e) => setColorCount(parseInt(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
<p className="text-[9px] text-gray-600 mt-1">Fewer colors = simpler output.</p>
</div>
{/* Layering (Sequential vs Parallel) */}
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">
Layer Stacking
</label>
</div>
<div className="flex bg-white/5 rounded-md p-1 gap-1">
<button
onClick={() => setLayering(0)}
className={cn("flex-1 text-[10px] py-1.5 rounded transition", layering === 0 ? "bg-indigo-500/20 text-indigo-400 font-semibold" : "text-gray-500 hover:text-white")}
>
Overlapping (Fills Gaps)
</button>
<button
onClick={() => setLayering(1)}
className={cn("flex-1 text-[10px] py-1.5 rounded transition", layering === 1 ? "bg-indigo-500/20 text-indigo-400 font-semibold" : "text-gray-500 hover:text-white")}
>
Cutout (No Overlap)
</button>
</div>
</div>
{/* Stroke Width / Anti-Gap */}
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] uppercase tracking-widest text-gray-500 font-bold">
Stroke (Anti-Gap)
</label>
<span className="text-xs font-mono text-indigo-400">{strokeWidth}px</span>
</div>
<input
type="range" min="0" max="5" step="0.5" value={strokeWidth}
onChange={(e) => setStrokeWidth(parseFloat(e.target.value))}
className="w-full accent-indigo-500 opacity-80 hover:opacity-100 cursor-pointer"
/>
<p className="text-[9px] text-gray-600 mt-1">If using Cutout mode, slightly increase stroke to hide gaps.</p>
</div>
</div>
<div className="mt-auto pt-6 border-t border-white/5">
<button
disabled={!svgString || isProcessing}
onClick={handleDownload}
className="w-full bg-indigo-600 hover:bg-indigo-500 py-3 rounded-lg font-bold text-sm text-white shadow-lg shadow-indigo-600/20 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Download className="w-4 h-4" />
<span>Download Vectors</span>
</button>
</div>
</div>
</div>
</main>
</div>
);
}