You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, edgeStrength = 1.0, posterizeLevels = 4, outlineColorStr = "0,0,0") {
const canvas = document.createElement('canvas');
// The { willReadFrequently: true } attribute can optimize repeated getImageData/putImageData calls.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Ensure originalImg is loaded and has dimensions.
// This function expects a fully loaded image object.
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image Graffiti Filter: Original image has zero dimensions. Ensure it's loaded.");
// Return an empty canvas or a canvas indicating an error.
// For simplicity, returning the (potentially 0x0) canvas.
// A more robust error handling might draw an error message on a fixed-size canvas.
return canvas;
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
// This can happen due to tainted canvas (e.g., cross-origin image without CORS)
console.error("Image Graffiti Filter: Could not get image data due to security or other error.", e);
// Fallback: Return a canvas with an error message.
// Clear canvas (in case drawImage succeeded but getImageData failed)
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(200, 200, 200, 0.8)"; // Light gray background
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.font = "16px Arial";
ctx.fillText("Error: Cannot process image.", canvas.width / 2, canvas.height / 2 - 10);
ctx.fillText("(Possibly CORS issue)", canvas.width / 2, canvas.height / 2 + 10);
return canvas;
}
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
// Parse and validate outline color string "r,g,b"
let [or, og, ob] = outlineColorStr.split(',').map(s => parseInt(s.trim(), 10));
if (isNaN(or) || or < 0 || or > 255 ||
isNaN(og) || og < 0 || og > 255 ||
isNaN(ob) || ob < 0 || ob > 255) {
[or, og, ob] = [0, 0, 0]; // Default to black if parsing fails or values are out of range
}
// Validate and set posterizeLevels
posterizeLevels = Math.max(2, Math.floor(posterizeLevels)); // Ensure at least 2 levels
const posterizeFactor = 255 / (posterizeLevels - 1);
// Create temporary arrays for intermediate pixel data (posterized colors and grayscale)
// Using Uint8ClampedArray is memory-efficient for storing values from 0-255.
const posterizedR = new Uint8ClampedArray(width * height);
const posterizedG = new Uint8ClampedArray(width * height);
const posterizedB = new Uint8ClampedArray(width * height);
const grayscale = new Uint8ClampedArray(width * height);
// 1. First Pass: Apply Posterization and Calculate Grayscale of the original image
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4; // Index for the original imageData.data array (RGBA)
const pixelArrIdx = y * width + x; // Index for our 1D single-channel arrays
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Alpha (data[i+3]) is handled in the final composition step
// Posterize each color channel
posterizedR[pixelArrIdx] = Math.round((r / 255) * (posterizeLevels - 1)) * posterizeFactor;
posterizedG[pixelArrIdx] = Math.round((g / 255) * (posterizeLevels - 1)) * posterizeFactor;
posterizedB[pixelArrIdx] = Math.round((b / 255) * (posterizeLevels - 1)) * posterizeFactor;
// Calculate grayscale value (luminance) of the original pixel for edge detection
grayscale[pixelArrIdx] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}
}
// Map edgeStrength (parameter, e.g., 0.0 to 2.0) to an edge detection threshold for Sobel.
// A higher edgeStrength means more sensitivity to edges (resulting in a lower numerical threshold).
const effectiveEdgeStrength = Math.min(Math.max(edgeStrength, 0), 2.5); // Clamp strength (0=no edges, 2.5=very sensitive)
// Threshold range: high value (less sensitive) to low value (more sensitive)
// Example: 0 -> 150, 1.0 -> 90, 2.0 -> 30, 2.5 -> 0 (all gradients are edges)
const edgeThreshold = Math.max(0, 150 - (effectiveEdgeStrength * 60));
const thresholdSq = edgeThreshold * edgeThreshold; // Compare squared magnitude to avoid Math.sqrt
// 2. Second Pass: Edge Detection (Sobel operator on grayscale) and Final Image Composition
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4; // Index for the outputImageData.data array (RGBA)
const pixelArrIdx = y * width + x; // Index for our 1D single-channel arrays
let isEdge = false;
// Apply Sobel operator for non-border pixels to avoid out-of-bounds access
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
// Get grayscale values from the 3x3 neighborhood
const p_tl = grayscale[(y - 1) * width + (x - 1)]; // top-left
const p_tc = grayscale[(y - 1) * width + x ]; // top-center
const p_tr = grayscale[(y - 1) * width + (x + 1)]; // top-right
const p_ml = grayscale[y * width + (x - 1)]; // middle-left
// current pixel: grayscale[pixelArrIdx] or grayscale[y * width + x]
const p_mr = grayscale[y * width + (x + 1)]; // middle-right
const p_bl = grayscale[(y + 1) * width + (x - 1)]; // bottom-left
const p_bc = grayscale[(y + 1) * width + x ]; // bottom-center
const p_br = grayscale[(y + 1) * width + (x + 1)]; // bottom-right
// Sobel Gx (horizontal gradient)
const Gx = (p_tr + 2 * p_mr + p_br) - (p_tl + 2 * p_ml + p_bl);
// Sobel Gy (vertical gradient)
const Gy = (p_bl + 2 * p_bc + p_br) - (p_tl + 2 * p_tc + p_tr);
const magnitudeSq = Gx * Gx + Gy * Gy; // Squared magnitude
if (magnitudeSq > thresholdSq) {
isEdge = true;
}
}
// Border pixels (where x=0, x=width-1, y=0, or y=height-1) will not be marked as edges
// by this Sobel implementation, they'll receive the posterized color.
if (isEdge) {
outputData[i] = or; // Outline Red
outputData[i + 1] = og; // Outline Green
outputData[i + 2] = ob; // Outline Blue
outputData[i + 3] = data[i + 3]; // Preserve original alpha
} else {
outputData[i] = posterizedR[pixelArrIdx];
outputData[i + 1] = posterizedG[pixelArrIdx];
outputData[i + 2] = posterizedB[pixelArrIdx];
outputData[i + 3] = data[i + 3]; // Preserve original alpha
}
}
}
// Write the processed pixel data back to the canvas
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes