MKT / script.js
JSWAI's picture
Create script.js
0a4d106 verified
Raw
History Blame Contribute Delete
12.1 kB
/**
* 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);
});
});
```