Spaces:
Build error
Build error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Smart Inpaint Studio - Privacy & Object Removal</title> | |
| <!-- Importing Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- Importing FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #4F46E5; | |
| --primary-hover: #4338ca; | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --border: #334155; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| --radius: 12px; | |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| header { | |
| background-color: rgba(30, 41, 59, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-weight: 700; | |
| font-size: 1.25rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| color: var(--text-main); | |
| } | |
| .logo i { | |
| color: var(--primary); | |
| } | |
| .anycoder-link { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 320px 1fr; | |
| gap: 2rem; | |
| padding: 2rem; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Sidebar Controls */ | |
| aside { | |
| background-color: var(--bg-card); | |
| border-radius: var(--radius); | |
| padding: 1.5rem; | |
| border: 1px solid var(--border); | |
| height: fit-content; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| box-shadow: var(--shadow); | |
| } | |
| h2 { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| label { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| font-weight: 500; | |
| } | |
| /* Custom File Upload */ | |
| .file-upload { | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| background-color: rgba(255,255,255,0.02); | |
| text-align: center; | |
| } | |
| .file-upload:hover { | |
| border-color: var(--primary); | |
| background-color: rgba(79, 70, 229, 0.05); | |
| } | |
| .file-upload input { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| .file-upload i { | |
| font-size: 2rem; | |
| color: var(--text-muted); | |
| margin-bottom: 0.5rem; | |
| } | |
| .file-upload p { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| } | |
| /* Brush Size Slider */ | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 6px; | |
| background: var(--border); | |
| border-radius: 3px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| background: var(--primary); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: transform 0.1s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.1); | |
| } | |
| .value-display { | |
| font-size: 0.875rem; | |
| color: var(--text-main); | |
| float: right; | |
| } | |
| /* Mode Selection */ | |
| .mode-options { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 0.5rem; | |
| } | |
| .mode-btn { | |
| background-color: rgba(255,255,255,0.05); | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| padding: 0.75rem; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 0.875rem; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.25rem; | |
| } | |
| .mode-btn:hover { | |
| background-color: rgba(255,255,255,0.1); | |
| } | |
| .mode-btn.active { | |
| background-color: var(--primary); | |
| border-color: var(--primary); | |
| color: white; | |
| } | |
| /* Action Buttons */ | |
| .btn { | |
| padding: 0.875rem; | |
| border-radius: 8px; | |
| border: none; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| font-size: 0.95rem; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-hover); | |
| } | |
| .btn-secondary { | |
| background-color: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| } | |
| .btn-secondary:hover { | |
| background-color: rgba(255,255,255,0.05); | |
| border-color: var(--text-muted); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* Canvas Area */ | |
| .canvas-container { | |
| background-color: #000; | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| position: relative; | |
| min-height: 500px; | |
| box-shadow: inset 0 0 20px rgba(0,0,0,0.5); | |
| } | |
| /* We stack canvases: Bottom = Image, Top = Drawing/Mask */ | |
| canvas { | |
| max-width: 100%; | |
| max-height: 80vh; | |
| cursor: crosshair; | |
| position: absolute; | |
| } | |
| #imageCanvas { | |
| z-index: 1; | |
| } | |
| #maskCanvas { | |
| z-index: 2; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| color: var(--text-muted); | |
| z-index: 0; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| opacity: 0.5; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| background-color: var(--bg-card); | |
| border-left: 4px solid var(--success); | |
| padding: 1rem 1.5rem; | |
| border-radius: 8px; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| transform: translateY(150%); | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .toast.show { | |
| transform: translateY(0); | |
| } | |
| .toast i { | |
| color: var(--success); | |
| } | |
| .loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(15, 23, 42, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s; | |
| } | |
| .loading-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 4px solid rgba(255,255,255,0.1); | |
| border-left-color: var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-bottom: 1rem; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> | |
| <span>Smart Inpaint Studio</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square" style="font-size: 0.7em;"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Sidebar Controls --> | |
| <aside> | |
| <div class="control-group"> | |
| <h2><i class="fa-solid fa-upload"></i> Source Image</h2> | |
| <label class="file-upload"> | |
| <input type="file" id="imageUpload" accept="image/*"> | |
| <i class="fa-solid fa-image"></i> | |
| <p>Click to upload or drag & drop</p> | |
| </label> | |
| </div> | |
| <div class="control-group"> | |
| <h2><i class="fa-solid fa-paintbrush"></i> Brush Settings</h2> | |
| <label>Brush Size <span id="brushSizeVal" class="value-display">30px</span></label> | |
| <input type="range" id="brushSize" min="5" max="100" value="30"> | |
| </div> | |
| <div class="control-group"> | |
| <h2><i class="fa-solid fa-layer-group"></i> Fill Mode</h2> | |
| <div class="mode-options"> | |
| <div class="mode-btn active" data-mode="blur"> | |
| <i class="fa-solid fa-droplet"></i> | |
| <span>Smart Blur</span> | |
| </div> | |
| <div class="mode-btn" data-mode="pixelate"> | |
| <i class="fa-solid fa-border-all"></i> | |
| <span>Pixelate</span> | |
| </div> | |
| <div class="mode-btn" data-mode="remove"> | |
| <i class="fa-solid fa-eraser"></i> | |
| <span>Object Fill</span> | |
| </div> | |
| <div class="mode-btn" data-mode="color"> | |
| <i class="fa-solid fa-fill-drip"></i> | |
| <span>Solid Color</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-group" style="margin-top: auto;"> | |
| <button id="processBtn" class="btn btn-primary" disabled> | |
| <i class="fa-solid fa-bolt"></i> Apply Inpainting | |
| </button> | |
| <button id="resetBtn" class="btn btn-secondary" disabled> | |
| <i class="fa-solid fa-rotate-left"></i> Reset Mask | |
| </button> | |
| <button id="downloadBtn" class="btn btn-secondary" disabled> | |
| <i class="fa-solid fa-download"></i> Download Result | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Canvas Area --> | |
| <div class="canvas-container" id="canvasWrapper"> | |
| <div class="empty-state" id="emptyState"> | |
| <i class="fa-regular fa-image"></i> | |
| <p>Upload an image to start editing</p> | |
| </div> | |
| <canvas id="imageCanvas"></canvas> | |
| <canvas id="maskCanvas"></canvas> | |
| <div class="loading-overlay" id="loader"> | |
| <div class="spinner"></div> | |
| <p>Processing pixels...</p> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="toast" id="toast"> | |
| <i class="fa-solid fa-circle-check"></i> | |
| <span id="toastMsg">Action completed successfully</span> | |
| </div> | |
| <script> | |
| /** | |
| * Smart Inpaint Studio | |
| * Handles image upload, canvas masking, and client-side pixel manipulation. | |
| */ | |
| const imageUpload = document.getElementById('imageUpload'); | |
| const brushSizeInput = document.getElementById('brushSize'); | |
| const brushSizeVal = document.getElementById('brushSizeVal'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const canvasWrapper = document.getElementById('canvasWrapper'); | |
| const imageCanvas = document.getElementById('imageCanvas'); | |
| const maskCanvas = document.getElementById('maskCanvas'); | |
| const ctxImg = imageCanvas.getContext('2d'); | |
| const ctxMask = maskCanvas.getContext('2d'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const loader = document.getElementById('loader'); | |
| const modeBtns = document.querySelectorAll('.mode-btn'); | |
| // State | |
| let isDrawing = false; | |
| let img = new Image(); | |
| let currentMode = 'blur'; // blur, pixelate, remove, color | |
| let hasImage = false; | |
| let maskData = null; // Store mask data for processing | |
| // Initialize | |
| function init() { | |
| resizeCanvases(); | |
| window.addEventListener('resize', resizeCanvases); | |
| } | |
| function resizeCanvases() { | |
| // Canvases are sized based on the image, not the window, | |
| // but CSS handles the display size. | |
| if (hasImage) { | |
| fitImageToScreen(); | |
| } | |
| } | |
| // --- Event Listeners --- | |
| imageUpload.addEventListener('change', handleImageUpload); | |
| brushSizeInput.addEventListener('input', (e) => { | |
| brushSizeVal.textContent = `${e.target.value}px`; | |
| }); | |
| // Mode Selection | |
| modeBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| modeBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| currentMode = btn.dataset.mode; | |
| }); | |
| }); | |
| // Canvas Drawing Events | |
| maskCanvas.addEventListener('mousedown', startDrawing); | |
| maskCanvas.addEventListener('mousemove', draw); | |
| maskCanvas.addEventListener('mouseup', stopDrawing); | |
| maskCanvas.addEventListener('mouseleave', stopDrawing); | |
| // Touch support | |
| maskCanvas.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); // Prevent scrolling | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousedown', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| maskCanvas.dispatchEvent(mouseEvent); | |
| }, { passive: false }); | |
| maskCanvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousemove', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| maskCanvas.dispatchEvent(mouseEvent); | |
| }, { passive: false }); | |
| maskCanvas.addEventListener('touchend', () => { | |
| const mouseEvent = new MouseEvent('mouseup', {}); | |
| maskCanvas.dispatchEvent(mouseEvent); | |
| }); | |
| processBtn.addEventListener('click', processInpainting); | |
| resetBtn.addEventListener('click', resetMask); | |
| downloadBtn.addEventListener('click', downloadImage); | |
| // --- Core Functions --- | |
| function handleImageUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| img = new Image(); | |
| img.onload = () => { | |
| hasImage = true; | |
| emptyState.style.display = 'none'; | |
| imageCanvas.style.display = 'block'; | |
| maskCanvas.style.display = 'block'; | |
| // Set canvas dimensions to match image | |
| imageCanvas.width = img.width; | |
| imageCanvas.height = img.height; | |
| maskCanvas.width = img.width; | |
| maskCanvas.height = img.height; | |
| // Draw image | |
| ctxImg.drawImage(img, 0, 0); | |
| // Clear mask | |
| ctxMask.clearRect(0, 0, maskCanvas.width, maskCanvas.height); | |
| // Enable buttons | |
| processBtn.disabled = false; | |
| resetBtn.disabled = false; | |
| downloadBtn.disabled = true; // Disable download until processed | |
| fitImageToScreen(); | |
| showToast('Image uploaded successfully'); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function fitImageToScreen() { | |
| // CSS handles the visual scaling (max-width: 100%), | |
| // we just need to ensure the coordinate mapping is correct. | |
| } | |
| function getMousePos(canvas, evt) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = canvas.width / rect.width; | |
| const scaleY = canvas.height / rect.height; | |
| return { | |
| x: (evt.clientX - rect.left) * scaleX, | |
| y: (evt.clientY - rect.top) * scaleY | |
| }; | |
| } | |
| function startDrawing(e) { | |
| if (!hasImage) return; | |
| isDrawing = true; | |
| draw(e); | |
| } | |
| function draw(e) { | |
| if (!isDrawing) return; | |
| const pos = getMousePos(maskCanvas, e); | |
| const size = parseInt(brushSizeInput.value); | |
| ctxMask.lineCap = 'round'; | |
| ctxMask.lineJoin = 'round'; | |
| ctxMask.lineWidth = size; | |
| ctxMask.strokeStyle = 'rgba(255, 0, 0, 0.5)'; // Semi-transparent red for mask | |
| ctxMask.lineTo(pos.x, pos.y); | |
| ctxMask.stroke(); | |
| ctxMask.beginPath(); | |
| ctxMask.moveTo(pos.x, pos.y); | |
| } | |
| function stopDrawing() { | |
| if (isDrawing) { | |
| isDrawing = false; | |
| ctxMask.beginPath(); | |
| } | |
| } | |
| function resetMask() { | |
| ctxMask.clearRect(0, 0, maskCanvas.width, maskCanvas.height); | |
| // Restore original image if we processed it | |
| if (hasImage) { | |
| ctxImg.drawImage(img, 0, 0); | |
| downloadBtn.disabled = true; | |
| } | |
| } | |
| // --- The "Inpainting" Logic (Client-Side Simulation) --- | |
| function processInpainting() { | |
| if (!hasImage) return; | |
| // Show loading | |
| loader.classList.add('active'); | |
| // Use setTimeout to allow UI to update (show loader) before heavy calculation | |
| setTimeout(() => { | |
| const width = imageCanvas.width; | |
| const height = imageCanvas.height; | |
| const imgData = ctxImg.getImageData(0, 0, width, height); | |
| const maskImageData = ctxMask.getImageData(0, 0, width, height); | |
| const data = imgData.data; | |
| const maskData = maskImageData.data; | |
| // Create a temporary array to store modified pixels so we don't read pixels we just wrote | |
| const outputBuffer = new Uint8ClampedArray(data); | |
| // Identify masked pixels | |
| // In our mask canvas, we drew with alpha > 0. | |
| // We check the Alpha channel of the mask canvas (index 3 + 4n) | |
| const maskedPixels = []; | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const index = (y * width + x) * 4; | |
| // If mask alpha is significant | |
| if (maskData[index + 3] > 0) { | |
| maskedPixels.push({x, y}); | |
| } | |
| } | |
| } | |
| if (maskedPixels.length === 0) { | |
| loader.classList.remove('active'); | |
| showToast('No mask detected. Please paint over an area.'); | |
| return; | |
| } | |
| // Apply effect based on mode | |
| if (currentMode === 'blur') { | |
| applySmartBlur(outputBuffer, width, height, maskedPixels, maskData); | |
| } else if (currentMode === 'pixelate') { | |
| applyPixelate(outputBuffer, width, height, maskedPixels); | |
| } else if (currentMode === 'color') { | |
| applySolidColor(outputBuffer, width, height, maskedPixels, [0, 0, 0]); // Black fill | |
| } else if (currentMode === 'remove') { | |
| applyTextureFill(outputBuffer, width, height, maskedPixels, maskData); | |
| } | |
| // Put data back | |
| const finalImageData = new ImageData(outputBuffer, width, height); | |
| ctxImg.putImageData(finalImageData, 0, 0); | |
| // Clear mask for visual clarity | |
| ctxMask.clearRect(0, 0, width, height); | |
| loader.classList.remove('active'); | |
| downloadBtn.disabled = false; | |
| showToast('Inpainting applied!'); | |
| }, 100); | |
| } | |
| // Algorithm 1: Smart Blur (Averages surrounding non-masked pixels) | |
| function applySmartBlur(buffer, width, height, pixels, maskData) { | |
| const radius = 10; | |
| const data = buffer; | |
| pixels.forEach(p => { | |
| const idx = (p.y * width + p.x) * 4; | |
| let rSum = 0, gSum = 0, bSum = 0, count = 0; | |
| // Look at neighbors | |
| for (let dy = -radius; dy <= radius; dy++) { | |
| for (let dx = -radius; dx <= radius; dx++) { | |
| const ny = p.y + dy; | |
| const nx = p.x + dx; | |
| if (ny >= 0 && ny < height && nx >= 0 && nx < width) { | |
| const nIdx = (ny * width + nx) * 4; | |
| // Only sample from pixels that are NOT masked (mask alpha is 0) | |
| // This creates the "inpainting" effect by pulling from edges inward | |
| if (maskData[nIdx + 3] === 0) { | |
| rSum += data[nIdx]; | |
| gSum += data[nIdx + 1]; | |
| bSum += data[nIdx + 2]; | |
| count++; | |
| } | |
| } | |
| } | |
| } | |
| if (count > 0) { | |
| data[idx] = rSum / count; | |
| data[idx + 1] = gSum / count; | |
| data[idx + 2] = bSum / count; | |
| } | |
| }); | |
| } | |
| // Algorithm 2: Pixelate | |
| function applyPixelate(buffer, width, height, pixels) { | |
| const size = 15; // Pixel block size | |
| const data = buffer; | |
| // Group pixels into blocks | |
| const blocks = {}; | |
| pixels.forEach(p => { | |
| const bx = Math.floor(p.x / size); | |
| const by = Math.floor(p.y / size); | |
| const key = `${bx},${by}`; | |
| if (!blocks[key]) blocks[key] = []; | |
| blocks[key].push(p); | |
| }); | |
| // Process each block | |
| Object.values(blocks).forEach(blockPixels => { | |
| // Find center of block | |
| const cx = blockPixels[0].x; | |
| const cy = blockPixels[0].y; | |
| // Get color from center (approximate) | |
| const centerIdx = (cy * width + cx) * 4; | |
| const r = data[centerIdx]; | |
| const g = data[centerIdx + 1]; | |
| const b = data[centerIdx + 2]; | |
| // Fill block | |
| blockPixels.forEach(p => { | |
| const idx = (p.y * width + p.x) * 4; | |
| data[idx] = r; | |
| data[idx + 1] = g; | |
| data[idx + 2] = b; | |
| }); | |
| }); | |
| } | |
| // Algorithm 3: Texture Fill (Simple Patch Match simulation) | |
| function applyTextureFill(buffer, width, height, pixels, maskData) { | |
| // For simplicity in this demo, we just pull pixels from the left side of the image | |
| // or mirrored position to simulate "filling" space. | |
| const data = buffer; | |
| pixels.forEach(p => { | |
| const idx = (p.y * width + p.x) * 4; | |
| // Try to find a valid neighbor to the left | |
| let sourceX = p.x - 1; | |
| let valid = false; | |
| // Search leftwards | |
| while(sourceX >= 0) { | |
| const sIdx = (p.y * width + sourceX) * 4; | |
| if (maskData[sIdx + 3] === 0) { | |
| // Found a non-masked pixel, copy it | |
| data[idx] = data[sIdx]; | |
| data[idx+1] = data[sIdx+1]; | |
| data[idx+2] = data[sIdx+2]; | |
| valid = true; | |
| break; | |
| } | |
| sourceX--; | |
| } | |
| // If no left pixel, try top | |
| if (!valid) { | |
| let sourceY = p.y - 1; | |
| while(sourceY >= 0) { | |
| const sIdx = (sourceY * width + p.x) * 4; | |
| if (maskData[sIdx + 3] === 0) { | |
| data[idx] = data[sIdx]; | |
| data[idx+1] = data[sIdx+1]; | |
| data[idx+2] = data[sIdx+2]; | |
| break; | |
| } | |
| sourceY--; | |
| } | |
| } | |
| }); | |
| } | |
| function applySolidColor(buffer, width, height, pixels, color) { | |
| pixels.forEach(p => { | |
| const idx = (p.y * width + p.x) * 4; | |
| buffer[idx] = color[0]; // R | |
| buffer[idx + 1] = color[1]; // G | |
| buffer[idx + 2] = color[2]; // B | |
| }); | |
| } | |
| function downloadImage() { | |
| const link = document.createElement('a'); | |
| link.download = 'inpaint-result.png'; | |
| link.href = imageCanvas.toDataURL(); | |
| link.click(); | |
| } | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| const msg = document.getElementById('toastMsg'); | |
| msg.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Initialize on load | |
| init(); | |
| </script> | |
| </body> | |
| </html> |