/** * AI Masking & Distortion Engine * Handles drawing the mask, calculating the "outside" area, * and applying the distortion effect. */ const AIParser = { // Configuration for the distortion settings: { noiseIntensity: 0.04, // How much "grain" to add (visible to AI, invisible to human) displacementScale: 4, // How much to warp the outside pixels blurAmount: 2, // Slight blur outside to blend edges noiseOpacity: 0.8 // Opacity of the noise layer }, // State canvas: null, ctx: null, imgElement: null, points: [], // Array of {x, y} in percentage (0-100) // DOM References container: null, canvasEl: null, svgOverlay: null, init: (imgElement, containerEl) => { AIParser.imgElement = imgElement; AIParser.container = containerEl; // Create a hidden canvas for processing the distortion AIParser.canvas = document.createElement('canvas'); AIParser.ctx = AIParser.canvas.getContext('2d'); // We use an SVG overlay for the visible mask lines AIParser.svgOverlay = document.createElementNS("http://www.w3.org/2000/svg", "svg"); AIParser.svgOverlay.style.position = 'absolute'; AIParser.svgOverlay.style.top = '0'; AIParser.svgOverlay.style.left = '0'; AIParser.svgOverlay.style.pointerEvents = 'none'; AIParser.svgOverlay.style.zIndex = '10'; containerEl.appendChild(AIParser.svgOverlay); // Wait for image to load before setting canvas size imgElement.onload = () => { AIParser.resizeCanvas(); }; // Fallback if image is already loaded if (imgElement.complete) AIParser.resizeCanvas(); }, resizeCanvas: () => { const img = AIParser.imgElement; const container = AIParser.container; // Set canvas size to match the container/display size, not necessarily natural size const rect = container.getBoundingClientRect(); AIParser.canvas.width = rect.width; AIParser.canvas.height = rect.height; // Redraw if points exist if (AIParser.points.length > 0) { AIParser.applyDistortion(); } else { AIParser.clearDistortion(); } }, // 1. Add a point to the mask addPoint: (xPercent, yPercent) => { AIParser.points.push({ x: xPercent, y: yPercent }); AIParser.drawSVG(); AIParser.applyDistortion(); }, // 2. Draw the visible yellow mask line (SVG) drawSVG: () => { const svg = AIParser.svgOverlay; svg.innerHTML = ''; // Clear previous if (AIParser.points.length === 0) return; // Create the path data let pathData = `M ${AIParser.points[0].x}% ${AIParser.points[0].y}%`; for (let i = 1; i < AIParser.points.length; i++) { pathData += ` L ${AIParser.points[i].x}% ${AIParser.points[i].y}%`; } pathData += ' Z'; // Create SVG Path Element const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", pathData); path.setAttribute("fill", "none"); path.setAttribute("stroke", "#FFD700"); // Yellow highlight path.setAttribute("stroke-width", "2"); path.setAttribute("stroke-dasharray", "5,5"); // Add a subtle glow for visibility const filter = document.createElementNS("http://www.w3.org/2000/svg", "filter"); filter.setAttribute("id", "glow"); const blur = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur"); blur.setAttribute("stdDeviation", "2"); blur.setAttribute("result", "coloredBlur"); const merge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge"); const mergeNode1 = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode"); mergeNode1.setAttribute("in", "coloredBlur"); const mergeNode2 = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode"); mergeNode2.setAttribute("in", "SourceGraphic"); merge.appendChild(mergeNode1); merge.appendChild(mergeNode2); filter.appendChild(blur); filter.appendChild(merge); svg.appendChild(filter); path.setAttribute("filter", "url(#glow)"); svg.appendChild(path); }, // 3. The Core: Distort the OUTSIDE area for AI applyDistortion: () => { if (AIParser.points.length < 3) return; const ctx = AIParser.ctx; const w = AIParser.canvas.width; const h = AIParser.canvas.height; // Clear canvas ctx.clearRect(0, 0, w, h); // A. Draw the original image cleanly ctx.drawImage(AIParser.imgElement, 0, 0, w, h); // B. Define the Mask Path ctx.beginPath(); const startX = (AIParser.points[0].x / 100) * w; const startY = (AIParser.points[0].y / 100) * h; ctx.moveTo(startX, startY); for (let i = 1; i < AIParser.points.length; i++) { const x = (AIParser.points[i].x / 100) * w; const y = (AIParser.points[i].y / 100) * h; ctx.lineTo(x, y); } ctx.closePath(); // C. Isolate the INSIDE (so we don't distort the subject) // We use 'destination-out' to clear the inside, or we can just draw // a giant rectangle with a hole (evenodd rule) over the outside. // Strategy: Draw a giant rectangle covering everything, // then subtract the mask shape using 'evenodd' fill rule. // 1. Save context and clear the mask area from the current image (make it transparent) ctx.save(); ctx.globalCompositeOperation = 'destination-out'; ctx.fill(); // Removes the inside of the mask ctx.restore(); // 2. Now the "Inside" is transparent. The "Outside" has the original image. // We want to distort the "Outside" (which is currently visible). // We will draw the image again over the whole canvas, but // apply a filter that only affects the outside? // Actually, it's easier to just draw the distorted version on top // where the original is. // Let's just draw a distorted version of the image, but masked // so only the OUTSIDE shows. // Create a temporary canvas for the distorted image const tempCanvas = document.createElement('canvas'); tempCanvas.width = w; tempCanvas.height = h; const tempCtx = tempCanvas.getContext('2d'); // Draw original to temp tempCtx.drawImage(AIParser.imgElement, 0, 0, w, h); // Apply Noise/Displacement to Temp Canvas AIParser.applyNoiseToContext(tempCtx, w, h); // Now, we need to composite the Temp Canvas (distorted) // ONLY onto the Outside area of the Main Canvas. // Set composite mode: Only draw where there is existing content (the outside) ctx.save(); ctx.globalCompositeOperation = 'source-over'; // We need to ensure we only draw on the "Outside". // Since we cleared the Inside with destination-out, // we can just draw the temp canvas. But we need to mask it // so we don't draw over the inside. // Actually, simpler approach: // 1. We have the original image on the Main Canvas, but the INSIDE is transparent. // 2. We draw the Distorted Image on the Main Canvas. // 3. The Inside remains transparent (showing original inside? No, we need original inside). // REVISED STRATEGY: // 1. Draw Original Image to Main Canvas. // 2. Create a "Mask" of the outside area (white inside mask, black outside? No). // Let's just draw the Distorted version over the Original, but // use a global composite that respects the current pixel alpha? // Let's go with the "Hole" method. // Clear the Main Canvas. ctx.clearRect(0, 0, w, h); // Draw Original Image ctx.drawImage(AIParser.imgElement, 0, 0, w, h); // Cut out the inside of the mask (make it transparent) ctx.globalCompositeOperation = 'destination-out'; ctx.beginPath(); ctx.moveTo(startX, startY); for (let i = 1; i < AIParser.points.length; i++) { ctx.lineTo((AIParser.points[i].x / 100) * w, (AIParser.points[i].y / 100) * h); } ctx.closePath(); ctx.fill(); // Now Main Canvas has: Inside=Transparent, Outside=Original // Draw the Distorted Image // We want to draw it where the Outside is. ctx.globalCompositeOperation = 'source-over'; ctx.drawImage(tempCanvas, 0, 0); // Restore context ctx.restore(); // Apply a final global blur to the outside to blend edges if needed // (Optional, but helps AI ignore edge artifacts) // ctx.filter = `blur(${AIParser.settings.blurAmount}px)`; // ctx.drawImage(AIParser.canvas, 0, 0); // ctx.filter = 'none'; // Update the DOM image to show this canvas // Note: In a real tool, you might not update the DOM img immediately // but show it as a "preview" layer. Here we update the source for preview. // AIParser.imgElement.src = AIParser.canvas.toDataURL(); }, // Helper: Adds noise and displacement to a context applyNoiseToContext: (ctx, w, h) => { const imageData = ctx.getImageData(0, 0, w, h); const data = imageData.data; const len = data.length; // Generate a noise map (simple random) const noiseMap = new Float32Array(w * h); for (let i = 0; i < w * h; i++) { noiseMap[i] = Math.random(); } for (let i = 0; i < len; i += 4) { const x = (i / 4) % w; const y = Math.floor((i / 4) / w); // Random displacement const noise = noiseMap[x + y * w]; const dx = (noise - 0.5) * AIParser.settings.displacementScale; const dy = (noise - 0.5) * AIParser.settings.displacementScale; // Sample color from offset position const srcX = Math.min(w - 1, Math.max(0, x + dx)); const srcY = Math.min(h - 1, Math.max(0, y + dy)); const srcIdx = (Math.floor(srcY) * w + Math.floor(srcX)) * 4; // Add noise grain const noiseVal = (Math.random() - 0.5) * 30 * AIParser.settings.noiseIntensity; data[i] = data[srcIdx] + noiseVal; // R data[i + 1] = data[srcIdx + 1] + noiseVal; // G data[i + 2] = data[srcIdx + 2] + noiseVal; // B // Alpha remains unchanged } ctx.putImageData(imageData, 0, 0); }, clearDistortion: () => { if (AIParser.imgElement) { // Reset to original AIParser.ctx.clearRect(0, 0, AIParser.canvas.width, AIParser.canvas.height); AIParser.ctx.drawImage(AIParser.imgElement, 0, 0); } } }; // --- Initialization Example --- document.addEventListener('DOMContentLoaded', () => { const img = document.getElementById('canvasBaseImg'); const wrapper = document.getElementById('canvasWrapper'); // Initialize the parser AIParser.init(img, wrapper); // Example: Add points on click wrapper.addEventListener('click', (e) => { if (e.target.tagName === 'path') return; // Ignore if clicking SVG paths const rect = wrapper.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * 100; const y = ((e.clientY - rect.top) / rect.height) * 100; AIParser.addPoint(x, y); }); }); ```