You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, blockSize = 8, breakProbability = 0.1, deadPixelProbability = 0.05, colorShiftProbability = 0.2, scanlineOpacity = 0.2, scanlineWidth = 1) {
// It's good practice to ensure the image is loaded, but for this problem,
// we'll assume originalImg is a loaded Image object as per typical problem constraints.
// If not, naturalWidth/Height might be 0.
// A more robust solution would await originalImg.onload if needed.
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) {
console.warn("Image Broken LCD Filter: Image dimensions are zero. Returning empty canvas.");
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1;
emptyCanvas.height = 1;
// Optionally draw a pixel to make it non-blank
const eCtx = emptyCanvas.getContext('2d');
if (eCtx) eCtx.fillRect(0,0,1,1);
return emptyCanvas;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imgWidth;
canvas.height = imgHeight;
// Use a temporary canvas to get image data without affecting the main canvas prematurely.
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imgWidth;
tempCanvas.height = imgHeight;
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) { // Fallback if context cannot be created
console.error("Image Broken LCD Filter: Cannot get 2D context for temp canvas.");
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight); // Draw original image
return canvas;
}
tempCtx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
// This can happen if the image is tainted (e.g., cross-origin without CORS)
console.error("Image Broken LCD Filter: Error getting image data (possibly tainted canvas):", e);
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight); // Draw original as fallback
// Optionally, add a text overlay indicating the issue if context is available on main canvas
if (ctx) {
ctx.fillStyle = "rgba(255,0,0,0.7)";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Error: Could not process image (cross-origin?)", imgWidth / 2, imgHeight / 2);
}
return canvas;
}
const data = imageData.data;
// Sanitize blockSize
blockSize = Math.max(1, Math.floor(blockSize));
for (let y = 0; y < imgHeight; y += blockSize) {
for (let x = 0; x < imgWidth; x += blockSize) {
const currentBlockWidth = Math.min(blockSize, imgWidth - x);
const currentBlockHeight = Math.min(blockSize, imgHeight - y);
let rSum = 0, gSum = 0, bSum = 0, aSum = 0;
let numPixels = 0;
// Calculate average color for the block
for (let blockY = 0; blockY < currentBlockHeight; blockY++) {
for (let blockX = 0; blockX < currentBlockWidth; blockX++) {
const sourceX = x + blockX;
const sourceY = y + blockY;
// Ensure we are within image bounds (primarily for the last blocks)
if (sourceX < imgWidth && sourceY < imgHeight) {
const index = (sourceY * imgWidth + sourceX) * 4;
rSum += data[index];
gSum += data[index + 1];
bSum += data[index + 2];
aSum += data[index + 3];
numPixels++;
}
}
}
let avgR = numPixels > 0 ? rSum / numPixels : 0;
let avgG = numPixels > 0 ? gSum / numPixels : 0;
let avgB = numPixels > 0 ? bSum / numPixels : 0;
let avgA = numPixels > 0 ? aSum / numPixels : 255; // Default to opaque if no pixels
let displayR = avgR;
let displayG = avgG;
let displayB = avgB;
let displayA = avgA;
// Apply "broken" effects sequentially (order matters)
if (Math.random() < deadPixelProbability) { // Chance of dead pixel
displayR = 0;
displayG = 0;
displayB = 0;
// Alpha for dead pixels: use avgA to respect original transparency.
// Alternatively, displayA = (avgA > 0) ? 255 : 0; for opaque black if not fully transparent.
} else if (Math.random() < colorShiftProbability) { // Chance of color shift (if not dead)
const shiftType = Math.floor(Math.random() * 3);
if (shiftType === 0) { // Swap R and B
[displayR, displayB] = [displayB, displayR];
} else if (shiftType === 1) { // Boost R, dim G and B slightly
displayR = Math.min(255, displayR * 1.5);
displayG = displayG * 0.6;
displayB = displayB * 0.8;
} else { // Boost G, dim R and B slightly
displayG = Math.min(255, displayG * 1.5);
displayR = displayR * 0.6;
displayB = displayB * 0.8;
}
} else if (Math.random() < breakProbability) { // Chance of general break (if not dead or color-shifted)
const breakStyle = Math.random();
if (breakStyle < 0.4) { // Random solid color block
displayR = Math.random() * 255;
displayG = Math.random() * 255;
displayB = Math.random() * 255;
} else if (breakStyle < 0.7) { // Desaturate or monochrome tint
const gray = avgR * 0.299 + avgG * 0.587 + avgB * 0.114;
if (Math.random() < 0.5) { // Desaturate towards gray
displayR = (avgR + gray) / 2;
displayG = (avgG + gray) / 2;
displayB = (avgB + gray) / 2;
} else { // Apply a monochrome tint (e.g., greenish)
displayR = gray * (0.3 + Math.random() * 0.4); // Random tint intensity R
displayG = gray * (0.7 + Math.random() * 0.5); // Random tint intensity G (more green)
displayB = gray * (0.3 + Math.random() * 0.4); // Random tint intensity B
}
} else { // Add noise to the average color
const noiseAmount = 70; // Max deviation for noise
displayR = avgR + (Math.random() - 0.5) * noiseAmount;
displayG = avgG + (Math.random() - 0.5) * noiseAmount;
displayB = avgB + (Math.random() - 0.5) * noiseAmount;
}
}
// Clamp final RGB values to 0-255 range
displayR = Math.max(0, Math.min(255, displayR));
displayG = Math.max(0, Math.min(255, displayG));
displayB = Math.max(0, Math.min(255, displayB));
ctx.fillStyle = `rgba(${Math.floor(displayR)}, ${Math.floor(displayG)}, ${Math.floor(displayB)}, ${displayA / 255})`;
ctx.fillRect(x, y, currentBlockWidth, currentBlockHeight);
}
}
// Draw scan lines on top
if (scanlineOpacity > 0 && scanlineWidth > 0 && blockSize > 1) { // Scanlines make sense if blocks are larger than 1px
ctx.fillStyle = `rgba(0, 0, 0, ${scanlineOpacity})`;
const actualScanlineWidth = Math.max(1, Math.floor(scanlineWidth));
// Horizontal scan lines (between blocks)
for (let i = blockSize; i < imgHeight; i += blockSize) {
ctx.fillRect(0, i - Math.floor(actualScanlineWidth / 2), imgWidth, actualScanlineWidth);
}
// Vertical scan lines (between blocks)
for (let j = blockSize; j < imgWidth; j += blockSize) {
ctx.fillRect(j - Math.floor(actualScanlineWidth / 2), 0, actualScanlineWidth, imgHeight);
}
}
return canvas;
}
Apply Changes