You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, thresholdStr = "128", cellSizeStr = "5", liveColor = "black", deadColor = "white", invertStr = "false") {
// 0. Initial Image Sanity Check
// Check if originalImg is valid and has dimensions.
if (!originalImg || typeof originalImg.width === 'undefined' || originalImg.width === 0 || originalImg.height === 0) {
console.error("processImage: Invalid image provided or image not loaded.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 250; // A fixed size for this specific error
errorCanvas.height = 100;
const ctx = errorCanvas.getContext('2d');
ctx.fillStyle = '#EEEEEE'; // Light gray background
ctx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '14px Arial';
ctx.fillText("Invalid or Empty Image Provided.", errorCanvas.width / 2, errorCanvas.height / 2);
return errorCanvas;
}
// 1. Parameter Parsing and Validation
let threshold = parseInt(thresholdStr, 10);
if (isNaN(threshold) || threshold < 0 || threshold > 255) {
threshold = 128; // Default threshold if parsing fails or out of range
}
let cellSize = parseInt(cellSizeStr, 10);
if (isNaN(cellSize) || cellSize < 1) {
cellSize = 5; // Default cell size if parsing fails or less than 1
}
// liveColor and deadColor parameters are strings, defaults provided in signature.
let invert = false; // Default inversion state
if (typeof invertStr === 'string') {
const lowerInv = invertStr.toLowerCase();
invert = lowerInv === 'true' || lowerInv === '1';
} else if (typeof invertStr === 'number') { // Handle if a number (0 or 1) is passed
invert = invertStr === 1;
}
// Note: Default string 'false' for invertStr correctly results in invert = false.
// 2. Prepare Input Canvas to Access PixelData
const inputCanvas = document.createElement('canvas');
inputCanvas.width = originalImg.width;
inputCanvas.height = originalImg.height;
// Add willReadFrequently hint for potential performance optimization
const inputCtx = inputCanvas.getContext('2d', { willReadFrequently: true });
if (!inputCtx) {
// This should rarely happen in modern browsers for reasonable canvas sizes
console.error("processImage: Could not get 2D context for input canvas.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 250; errorCanvas.height = 100;
const ctx = errorCanvas.getContext('2d');
ctx.fillStyle = '#EEEEEE'; ctx.fillRect(0, 0, 250, 100);
ctx.fillStyle = 'red'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = '14px Arial'; ctx.fillText("Canvas Context Error (Input)", 125, 50);
return errorCanvas;
}
inputCtx.drawImage(originalImg, 0, 0);
let imageData;
try {
imageData = inputCtx.getImageData(0, 0, originalImg.width, originalImg.height);
} catch (e) {
// This error often occurs due to CORS restrictions on cross-origin images
console.error("processImage: Could not get image data. This can happen with cross-origin images if not CORS-enabled.", e);
const errorCanvas = document.createElement('canvas');
// Make error canvas potentially large enough to display message, using original image size as reference
errorCanvas.width = Math.max(350, originalImg.width);
errorCanvas.height = Math.max(150, originalImg.height);
const ctx = errorCanvas.getContext('2d');
ctx.fillStyle = '#EEEEEE';
ctx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 13px Arial';
const lines = [
"Error: Could not access image pixels.",
"This is common with cross-origin images.",
"Ensure image is from the same domain",
"or that the server provides CORS headers."
];
const lineHeight = 18;
let textY = errorCanvas.height / 2 - (lines.length - 1) * lineHeight / 2;
for (const line of lines) {
ctx.fillText(line, errorCanvas.width / 2, textY);
textY += lineHeight;
}
return errorCanvas;
}
const data = imageData.data; // Pixel data array (R,G,B,A, R,G,B,A, ...)
// 3. Calculate Grid Dimensions for the Game of Life Pattern
const gridWidth = Math.floor(originalImg.width / cellSize);
const gridHeight = Math.floor(originalImg.height / cellSize);
if (gridWidth === 0 || gridHeight === 0) {
// This happens if image is smaller than cellSize in either dimension
console.warn("processImage: Image is too small for the given cell size, or cell size is too large.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = Math.max(300, originalImg.width);
errorCanvas.height = Math.max(120, originalImg.height);
const ctx = errorCanvas.getContext('2d');
ctx.fillStyle = '#EEEEEE';
ctx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
ctx.fillStyle = 'darkorange'; // Warning color
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 14px Arial';
ctx.fillText('Image too small for selected cell size.', errorCanvas.width / 2, errorCanvas.height / 2 - 10);
ctx.font = '12px Arial';
ctx.fillText(`(Image: ${originalImg.width}x${originalImg.height}px, Cell: ${cellSize}x${cellSize}px)`, errorCanvas.width / 2, errorCanvas.height / 2 + 12);
return errorCanvas;
}
// 4. Initialize Game of Life Grid: Determine Live/Dead State for Each Cell
const initialGrid = []; // 2D array to store cell states (0 for dead, 1 for live)
for (let gy = 0; gy < gridHeight; gy++) { // Iterate through grid rows
initialGrid[gy] = [];
for (let gx = 0; gx < gridWidth; gx++) { // Iterate through grid columns
let sumLuminance = 0;
let pixelCount = 0;
// Define the image region corresponding to the current grid cell
const startX = gx * cellSize;
const startY = gy * cellSize;
// Ensure sampling bounds do not exceed original image dimensions
const endX = Math.min(startX + cellSize, originalImg.width);
const endY = Math.min(startY + cellSize, originalImg.height);
// Calculate average luminance for the pixels in this cell's region
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const idx = (y * originalImg.width + x) * 4; // Index for R value in imageData.data
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
// Standard luminance formula (perceived brightness)
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
sumLuminance += luminance;
pixelCount++;
}
}
// Average luminance for the cell; default to 0 if no pixels (should not happen with cellSize >= 1)
const avgLuminance = (pixelCount > 0) ? (sumLuminance / pixelCount) : 0;
let isLiveCell;
if (invert) { // If inverted, darker pixels (lower luminance) become live
isLiveCell = avgLuminance < threshold;
} else { // Otherwise, brighter pixels (higher luminance) become live
isLiveCell = avgLuminance > threshold;
}
initialGrid[gy][gx] = isLiveCell ? 1 : 0; // Store 1 for live, 0 for dead
}
}
// 5. Create Output Canvas and Draw the Resulting Game of Life Pattern
const outputCanvas = document.createElement('canvas');
outputCanvas.width = gridWidth * cellSize; // Final canvas width based on grid
outputCanvas.height = gridHeight * cellSize; // Final canvas height based on grid
const outputCtx = outputCanvas.getContext('2d');
if (!outputCtx) {
console.error("processImage: Could not get 2D context for output canvas.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 250; errorCanvas.height = 100;
const ctx = errorCanvas.getContext('2d');
ctx.fillStyle = '#EEEEEE'; ctx.fillRect(0, 0, 250, 100);
ctx.fillStyle = 'red'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = '14px Arial'; ctx.fillText("Canvas Context Error (Output)", 125, 50);
return errorCanvas;
}
// Efficiently draw by first filling with deadColor, then drawing live cells.
outputCtx.fillStyle = deadColor;
outputCtx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
for (let gy = 0; gy < gridHeight; gy++) {
for (let gx = 0; gx < gridWidth; gx++) {
if (initialGrid[gy][gx] === 1) { // If cell is live
outputCtx.fillStyle = liveColor;
outputCtx.fillRect(gx * cellSize, gy * cellSize, cellSize, cellSize);
}
}
}
return outputCanvas; // Return the canvas element with the pattern
}
Apply Changes