Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Upload, Scissors, Eraser, Wand2, Download, Undo2, Redo2, Trash2, Square, Image as ImageIcon, ZoomIn, ZoomOut, Check, X, Move } from 'lucide-react'; const Button = ({ onClick, active, disabled, children, className = "", title }) => ( <button onClick={onClick} disabled={disabled} title={title} className={`p-2 rounded-lg flex items-center justify-center transition-all duration-200 ${ active ? 'bg-blue-600 text-white shadow-md' : disabled ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-100 hover:text-blue-600 border border-gray-200 shadow-sm' } ${className}`} > {children} </button> ); const Slider = ({ label, value, min, max, onChange, disabled }) => ( <div className="flex flex-col gap-1 w-full max-w-[200px]"> <div className="flex justify-between text-xs font-medium text-gray-500 uppercase tracking-wide"> <span>{label}</span> <span>{value}</span> </div> <input type="range" min={min} max={max} value={value} onChange={(e) => onChange(parseInt(e.target.value))} disabled={disabled} className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600 disabled:opacity-50" /> </div> ); export default function BrandStudio() { // --- State --- const [image, setImage] = useState(null); const [mode, setMode] = useState('upload'); // upload, edit, crop const [tool, setTool] = useState('move'); // move, erase, magic, restore const [brushSize, setBrushSize] = useState(20); const [tolerance, setTolerance] = useState(30); const [history, setHistory] = useState([]); const [historyStep, setHistoryStep] = useState(-1); const [isProcessing, setIsProcessing] = useState(false); // Crop State const [crop, setCrop] = useState({ x: 0, y: 0, size: 100 }); const [imgDimensions, setImgDimensions] = useState({ w: 0, h: 0 }); // Refs const canvasRef = useRef(null); const tempCanvasRef = useRef(null); // For brush cursors / crop overlays const containerRef = useRef(null); // --- Initialization & History --- // Initialize Canvas when entering Edit mode with a new Image useEffect(() => { if (mode === 'edit' && image && canvasRef.current && history.length === 0) { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); // Limit max size let w = image.width; let h = image.height; const MAX_SIZE = 1200; if (w > MAX_SIZE || h > MAX_SIZE) { const ratio = Math.min(MAX_SIZE / w, MAX_SIZE / h); w *= ratio; h *= ratio; } canvas.width = w; canvas.height = h; setImgDimensions({ w, h }); // Center crop init const minDim = Math.min(w, h); setCrop({ x: (w - minDim/2) / 2, y: (h - minDim/2) / 2, size: minDim / 2 }); ctx.drawImage(image, 0, 0, w, h); const initialData = ctx.getImageData(0, 0, w, h); setHistory([initialData]); setHistoryStep(0); } }, [mode, image, history.length]); const addToHistory = useCallback((imageData) => { const newHistory = history.slice(0, historyStep + 1); newHistory.push(imageData); // Limit history to 20 steps to save memory if (newHistory.length > 20) newHistory.shift(); setHistory(newHistory); setHistoryStep(newHistory.length - 1); }, [history, historyStep]); const handleUndo = () => { if (historyStep > 0) { const prevStep = historyStep - 1; setHistoryStep(prevStep); putImageData(history[prevStep]); } }; const handleRedo = () => { if (historyStep < history.length - 1) { const nextStep = historyStep + 1; setHistoryStep(nextStep); putImageData(history[nextStep]); } }; const putImageData = (imageData) => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); }; // --- File Handling --- const handleImageUpload = (e) => { const file = e.target.files?.[0]; if (!file) return; // Reset input value so same file can be selected again e.target.value = ''; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { // Just set state here. The useEffect above handles the drawing // once the canvas is actually rendered in 'edit' mode. setHistory([]); setHistoryStep(-1); setImage(img); setMode('edit'); }; img.src = event.target.result; }; reader.readAsDataURL(file); }; // --- Tools Logic --- // Flood Fill Algorithm for Magic Wand const magicWand = (startX, startY) => { setIsProcessing(true); // Use setTimeout to allow UI to render loading state setTimeout(() => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); const w = canvas.width; const h = canvas.height; const imageData = ctx.getImageData(0, 0, w, h); const data = imageData.data; // Helper to get index const getIdx = (x, y) => (y * w + x) * 4; const startIdx = getIdx(startX, startY); const startR = data[startIdx]; const startG = data[startIdx + 1]; const startB = data[startIdx + 2]; const startA = data[startIdx + 3]; // If clicking already transparent area, do nothing if (startA === 0) { setIsProcessing(false); return; } const pixelStack = [[startX, startY]]; const visited = new Uint8Array(w * h); // track visited to prevent loops const match = (pos) => { const r = data[pos]; const g = data[pos + 1]; const b = data[pos + 2]; const a = data[pos + 3]; // If already transparent, it's a "border" for us, but not a match to remove if (a === 0) return false; // Euclidian distance or simple sum diff const diff = Math.abs(r - startR) + Math.abs(g - startG) + Math.abs(b - startB); return diff <= tolerance * 3; // tolerance scaled roughly }; while (pixelStack.length) { const [cx, cy] = pixelStack.pop(); const idx = getIdx(cx, cy); if (visited[cy * w + cx]) continue; if (match(idx)) { // Erase pixel data[idx + 3] = 0; visited[cy * w + cx] = 1; // Check neighbors (4-way) if (cx > 0) pixelStack.push([cx - 1, cy]); if (cx < w - 1) pixelStack.push([cx + 1, cy]); if (cy > 0) pixelStack.push([cx, cy - 1]); if (cy < h - 1) pixelStack.push([cx, cy + 1]); } } ctx.putImageData(imageData, 0, 0); addToHistory(imageData); setIsProcessing(false); }, 10); }; // Canvas Interactions const [isDrawing, setIsDrawing] = useState(false); const lastPos = useRef(null); const getCanvasCoordinates = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; // Handle both mouse and touch const clientX = e.clientX || e.touches?.[0]?.clientX; const clientY = e.clientY || e.touches?.[0]?.clientY; return { x: Math.floor((clientX - rect.left) * scaleX), y: Math.floor((clientY - rect.top) * scaleY) }; }; const handlePointerDown = (e) => { if (mode !== 'edit') return; const { x, y } = getCanvasCoordinates(e); if (tool === 'magic') { magicWand(x, y); } else if (tool === 'erase' || tool === 'restore') { setIsDrawing(true); lastPos.current = { x, y }; draw(x, y); } }; const handlePointerMove = (e) => { if (mode !== 'edit') return; if (!isDrawing) return; // Prevent scrolling on touch e.preventDefault(); const { x, y } = getCanvasCoordinates(e); draw(x, y); lastPos.current = { x, y }; }; const handlePointerUp = () => { if (isDrawing) { setIsDrawing(false); lastPos.current = null; // Save state const ctx = canvasRef.current.getContext('2d'); addToHistory(ctx.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height)); } }; const draw = (x, y) => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = brushSize; if (tool === 'erase') { ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.moveTo(lastPos.current.x, lastPos.current.y); ctx.lineTo(x, y); ctx.stroke(); ctx.globalCompositeOperation = 'source-over'; // reset } else if (tool === 'restore') { // Restore is tricky in a single canvas 'destructive' setup. // We need the original image. // We can draw from the original image onto the current canvas using 'source-over' // at the specific coordinates. // Best way: Clipping mask on temp canvas? // Simple way: Draw the original image again but clipped to the brush stroke? // Optimized Restore: ctx.save(); ctx.beginPath(); ctx.arc(x, y, brushSize / 2, 0, Math.PI * 2); ctx.clip(); // Clear area first to avoid alpha stacking issues if semi-transparent ctx.clearRect(x - brushSize, y - brushSize, brushSize * 2, brushSize * 2); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); ctx.restore(); } }; // --- Cropping Logic --- const [isDraggingCrop, setIsDraggingCrop] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [activeHandle, setActiveHandle] = useState(null); // 'move', 'nw', 'ne', 'sw', 'se' const handleCropPointerDown = (e, handle) => { e.stopPropagation(); e.preventDefault(); setIsDraggingCrop(true); setActiveHandle(handle); const clientX = e.clientX || e.touches?.[0]?.clientX; const clientY = e.clientY || e.touches?.[0]?.clientY; setDragStart({ x: clientX, y: clientY }); }; const handleCropPointerMove = (e) => { if (!isDraggingCrop) return; const clientX = e.clientX || e.touches?.[0]?.clientX; const clientY = e.clientY || e.touches?.[0]?.clientY; // Calculate delta in canvas pixels // We need to map screen pixels to canvas pixels based on how it's displayed // This is complex because the canvas scales with CSS object-contain. // Let's rely on percentage or screen delta mapped to canvas ratio. const canvas = canvasRef.current; if(!canvas) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const dx = (clientX - dragStart.x) * scaleX; const dy = (clientY - dragStart.y) * scaleX; let newCrop = { ...crop }; if (activeHandle === 'move') { newCrop.x += dx; newCrop.y += dy; } else if (activeHandle === 'se') { // Resize maintaining aspect ratio (Square) // We'll use the larger of dx/dy to drive size change const change = Math.max(dx, dy); newCrop.size += change; } // Simplified: Just 'move' and 'resize' (bottom right handle) for now to save code space & bugs // Bounds Check if (newCrop.size < 50) newCrop.size = 50; if (newCrop.size > Math.min(canvas.width, canvas.height)) newCrop.size = Math.min(canvas.width, canvas.height); // Keep inside if (newCrop.x < 0) newCrop.x = 0; if (newCrop.y < 0) newCrop.y = 0; if (newCrop.x + newCrop.size > canvas.width) newCrop.x = canvas.width - newCrop.size; if (newCrop.y + newCrop.size > canvas.height) newCrop.y = canvas.height - newCrop.size; setCrop(newCrop); setDragStart({ x: clientX, y: clientY }); }; const applyCrop = () => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); // Get cropped data const croppedData = ctx.getImageData(crop.x, crop.y, crop.size, crop.size); // Resize canvas canvas.width = crop.size; canvas.height = crop.size; // Put data ctx.putImageData(croppedData, 0, 0); // Update state setImgDimensions({ w: crop.size, h: crop.size }); addToHistory(ctx.getImageData(0, 0, crop.size, crop.size)); // Reset Crop box to cover new full image slightly smaller setCrop({ x: 10, y: 10, size: crop.size - 20 }); setMode('edit'); setTool('move'); }; // --- Download --- const handleDownload = () => { const canvas = canvasRef.current; const link = document.createElement('a'); link.download = 'brand-image.png'; link.href = canvas.toDataURL('image/png'); link.click(); }; // --- Render Helpers --- // Calculate style for the crop overlay const getCropStyle = () => { if (!canvasRef.current) return {}; const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); // visible size // Ratio of visible size to actual pixel size const scale = rect.width / canvas.width; return { left: crop.x * scale, top: crop.y * scale, width: crop.size * scale, height: crop.size * scale, }; }; return ( <div className="min-h-screen bg-slate-50 text-slate-900 font-sans flex flex-col" onMouseUp={() => { handlePointerUp(); setIsDraggingCrop(false); }} onTouchEnd={() => { handlePointerUp(); setIsDraggingCrop(false); }} onMouseMove={mode === 'crop' ? handleCropPointerMove : null} onTouchMove={mode === 'crop' ? handleCropPointerMove : null} > {/* Header */} <header className="bg-white border-b px-6 py-4 flex items-center justify-between shadow-sm z-10"> <div className="flex items-center gap-2"> <div className="bg-blue-600 p-2 rounded-lg"> <Scissors className="text-white w-5 h-5" /> </div> <h1 className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-700 to-indigo-600">BrandStudio</h1> </div> {mode !== 'upload' && ( <div className="flex gap-2"> <Button onClick={handleUndo} disabled={historyStep <= 0} title="Undo"> <Undo2 className="w-5 h-5" /> </Button> <Button onClick={handleRedo} disabled={historyStep >= history.length - 1} title="Redo"> <Redo2 className="w-5 h-5" /> </Button> <div className="w-px bg-gray-200 mx-1"></div> <button onClick={handleDownload} className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors shadow-sm" > <Download className="w-4 h-4" /> Save PNG </button> </div> )} </header> {/* Main Workspace */} <main className="flex-1 flex overflow-hidden"> {mode === 'upload' ? ( <div className="flex-1 flex flex-col items-center justify-center p-8 animate-in fade-in duration-500"> <div className="max-w-md w-full text-center"> <div className="w-24 h-24 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-6"> <Upload className="w-10 h-10 text-blue-600" /> </div> <h2 className="text-3xl font-bold text-gray-900 mb-4">Upload your Image</h2> <p className="text-gray-500 mb-8"> Remove backgrounds and create square crops for your brand assets instantly. No signup required. </p> <label className="block w-full cursor-pointer group"> <input type="file" className="hidden" accept="image/*" onChange={handleImageUpload} /> <div className="border-2 border-dashed border-gray-300 rounded-xl p-12 flex flex-col items-center justify-center bg-white group-hover:border-blue-500 group-hover:bg-blue-50 transition-all"> <div className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium shadow-md group-hover:shadow-lg transition-all transform group-hover:-translate-y-1"> Choose File </div> <p className="mt-4 text-sm text-gray-400">or drop image here</p> </div> </label> </div> </div> ) : ( <> {/* Sidebar Tools */} <aside className="w-72 bg-white border-r flex flex-col z-10 shadow-lg"> <div className="p-4 space-y-6 overflow-y-auto"> {/* Mode Selector */} <div className="flex bg-gray-100 p-1 rounded-lg"> <button onClick={() => { setMode('edit'); setTool('move'); }} className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all ${mode === 'edit' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`} > <Eraser className="w-4 h-4" /> Clean </button> <button onClick={() => setMode('crop')} className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-all ${mode === 'crop' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`} > <Square className="w-4 h-4" /> Crop </button> </div> {mode === 'edit' && ( <div className="space-y-6 animate-in slide-in-from-left-4 duration-300"> <div className="space-y-3"> <h3 className="text-xs font-semibold text-gray-400 uppercase">Selection Tools</h3> <div className="grid grid-cols-2 gap-2"> <Button active={tool === 'magic'} onClick={() => setTool('magic')} className="flex-col gap-2 h-20" > <Wand2 className="w-6 h-6" /> <span className="text-xs">Magic Wand</span> </Button> <Button active={tool === 'erase'} onClick={() => setTool('erase')} className="flex-col gap-2 h-20" > <Eraser className="w-6 h-6" /> <span className="text-xs">Eraser</span> </Button> <Button active={tool === 'restore'} onClick={() => setTool('restore')} className="flex-col gap-2 h-20" > <Undo2 className="w-6 h-6 rotate-180" /> <span className="text-xs">Restore</span> </Button> <Button active={tool === 'move'} onClick={() => setTool('move')} className="flex-col gap-2 h-20" > <Move className="w-6 h-6" /> <span className="text-xs">Pan/Zoom</span> </Button> </div> </div> <div className="space-y-4 pt-4 border-t"> {tool === 'magic' && ( <div className="space-y-2"> <label className="text-sm font-medium text-gray-700">Tolerance</label> <Slider value={tolerance} min={1} max={100} onChange={setTolerance} label="Color Match" /> <p className="text-xs text-gray-500 mt-1"> Higher tolerance removes more colors similar to the one you click. </p> </div> )} {(tool === 'erase' || tool === 'restore') && ( <div className="space-y-2"> <label className="text-sm font-medium text-gray-700">Brush Size</label> <Slider value={brushSize} min={5} max={100} onChange={setBrushSize} label="Pixel Width" /> </div> )} {tool === 'move' && ( <div className="p-4 bg-blue-50 rounded-lg text-sm text-blue-800"> <p>Select a tool above to start removing the background.</p> </div> )} </div> </div> )} {mode === 'crop' && ( <div className="space-y-6 animate-in slide-in-from-left-4 duration-300"> <div className="p-4 bg-yellow-50 rounded-lg text-sm text-yellow-800 border border-yellow-100"> Drag the box on the image to select your square area. </div> <div className="flex flex-col gap-2"> <button onClick={applyCrop} className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium shadow-sm flex items-center justify-center gap-2" > <Check className="w-4 h-4" /> Apply Crop </button> <button onClick={() => setMode('edit')} className="w-full py-3 bg-white border hover:bg-gray-50 text-gray-700 rounded-lg font-medium flex items-center justify-center gap-2" > <X className="w-4 h-4" /> Cancel </button> </div> </div> )} </div> </aside> {/* Canvas Area */} <div className="flex-1 bg-slate-200 relative overflow-hidden flex items-center justify-center p-8" ref={containerRef}> {/* Checkerboard Background */} <div className="absolute inset-0 opacity-20 pointer-events-none" style={{ backgroundImage: 'linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%)', backgroundSize: '20px 20px', backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px' }} /> <div className="relative shadow-2xl"> <canvas ref={canvasRef} onMouseDown={handlePointerDown} onMouseMove={handlePointerMove} onTouchStart={handlePointerDown} onTouchMove={handlePointerMove} className={`max-w-full max-h-[80vh] bg-transparent ${ tool === 'move' ? 'cursor-default' : tool === 'magic' ? 'cursor-crosshair' : 'cursor-none' }`} style={{ touchAction: 'none' }} /> {/* Custom Cursor for Brush */} {mode === 'edit' && (tool === 'erase' || tool === 'restore') && !isDrawing && ( <div className="pointer-events-none fixed rounded-full border border-gray-800 bg-white/20 z-50 transform -translate-x-1/2 -translate-y-1/2" ref={(el) => { if (!el) return; const updatePos = (e) => { el.style.left = e.clientX + 'px'; el.style.top = e.clientY + 'px'; } window.addEventListener('mousemove', updatePos); return () => window.removeEventListener('mousemove', updatePos); }} style={{ width: brushSize, height: brushSize }} /> )} {/* Processing Indicator */} {isProcessing && ( <div className="absolute inset-0 flex items-center justify-center bg-black/20 z-50"> <div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"></div> </div> )} {/* Crop Overlay */} {mode === 'crop' && ( <div className="absolute inset-0 pointer-events-none z-20" style={{ width: '100%', height: '100%' }} > <div className="absolute inset-0 bg-black/50"> {/* The Clear Box */} <div style={getCropStyle()} className="absolute border-2 border-white shadow-[0_0_0_9999px_rgba(0,0,0,0.5)] cursor-move pointer-events-auto" onMouseDown={(e) => handleCropPointerDown(e, 'move')} onTouchStart={(e) => handleCropPointerDown(e, 'move')} > {/* Grid Lines */} <div className="absolute inset-0 opacity-30 pointer-events-none"> <div className="absolute left-1/3 top-0 bottom-0 w-px bg-white"></div> <div className="absolute right-1/3 top-0 bottom-0 w-px bg-white"></div> <div className="absolute top-1/3 left-0 right-0 h-px bg-white"></div> <div className="absolute bottom-1/3 left-0 right-0 h-px bg-white"></div> </div> {/* Resize Handle (Bottom Right) */} <div className="absolute -bottom-3 -right-3 w-6 h-6 bg-blue-500 border-2 border-white rounded-full cursor-se-resize pointer-events-auto hover:scale-110 transition-transform shadow-lg" onMouseDown={(e) => handleCropPointerDown(e, 'se')} onTouchStart={(e) => handleCropPointerDown(e, 'se')} /> </div> </div> </div> )} </div> </div> </> )} </main> </div> ); }
No revisions found. Save the file to create a backup.
Delete
Update App