You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, cellSize = 8, dotScale = 1.0, dotColor = "black", backgroundColor = "white", levels = 5) {
// 1. Parameter validation/sanitization
cellSize = Math.max(1, Math.floor(cellSize));
dotScale = Math.max(0.01, dotScale); // Allow very small scales
levels = Math.max(1, Math.floor(levels));
const imgWidth = originalImg.naturalWidth || originalImg.width || 0;
const imgHeight = originalImg.naturalHeight || originalImg.height || 0;
// Handle cases where image dimensions might be 0 (e.g., image not loaded properly)
if (imgWidth === 0 || imgHeight === 0) {
console.warn("Image has zero width or height. Cannot process.");
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = Math.max(10, imgWidth); // Ensure minimum size if one dim is 0
emptyCanvas.height = Math.max(10, imgHeight);
const emptyCtx = emptyCanvas.getContext('2d');
if (emptyCanvas.width <=10 && emptyCanvas.height <= 10){ // Only draw placeholder if truly tiny
emptyCtx.fillStyle = 'lightgray';
emptyCtx.fillRect(0, 0, emptyCanvas.width, emptyCanvas.height);
emptyCtx.strokeStyle = 'red';
emptyCtx.lineWidth = 1;
emptyCtx.beginPath();
emptyCtx.moveTo(0, 0); emptyCtx.lineTo(emptyCanvas.width, emptyCanvas.height);
emptyCtx.moveTo(emptyCanvas.width, 0); emptyCtx.lineTo(0, emptyCanvas.height);
emptyCtx.stroke(); // Draw an X
}
return emptyCanvas;
}
// 2. Create input canvas to get pixel data from originalImg
const inputCanvas = document.createElement('canvas');
inputCanvas.width = imgWidth;
inputCanvas.height = imgHeight;
const inputCtx = inputCanvas.getContext('2d');
inputCtx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
let imageData;
try {
imageData = inputCtx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
console.error("Error getting imageData:", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = imgWidth;
errorCanvas.height = imgHeight;
const errorCtx = errorCanvas.getContext('2d');
errorCtx.fillStyle = '#F0F0F0'; // Light gray background
errorCtx.fillRect(0, 0, imgWidth, imgHeight);
errorCtx.fillStyle = '#D32F2F'; // Darker red for text
errorCtx.textAlign = 'center';
errorCtx.textBaseline = 'middle';
const primaryMessage = "Error: Could not process image.";
const secondaryMessage = "(Possibly a CORS issue or tainted canvas)";
// Dynamic font sizing for messages
let PADDING = imgWidth * 0.1; // 10% padding for text
let textRegionWidth = imgWidth - PADDING;
let primaryFontSize = Math.max(12, Math.min(textRegionWidth / (primaryMessage.length * 0.55), imgHeight / 6));
errorCtx.font = `bold ${primaryFontSize}px Arial, sans-serif`;
errorCtx.fillText(primaryMessage, imgWidth / 2, imgHeight / 2 - primaryFontSize * 0.7);
let secondaryFontSize = Math.max(10, primaryFontSize * 0.7);
errorCtx.font = `${secondaryFontSize}px Arial, sans-serif`;
errorCtx.fillText(secondaryMessage, imgWidth / 2, imgHeight / 2 + secondaryFontSize * 0.8);
return errorCanvas;
}
const pixels = imageData.data;
// 3. Create output canvas
const outputCanvas = document.createElement('canvas');
outputCanvas.width = imgWidth;
outputCanvas.height = imgHeight;
const outputCtx = outputCanvas.getContext('2d');
// 4. Fill background
outputCtx.fillStyle = backgroundColor;
outputCtx.fillRect(0, 0, imgWidth, imgHeight);
// 5. Halftone processing logic
const maxPossibleDotRadius = cellSize / 2; // Max radius if dot fills its cell part
for (let y = 0; y < imgHeight; y += cellSize) {
for (let x = 0; x < imgWidth; x += cellSize) {
let sumIntensity = 0;
let numPixelsInCell = 0;
// Calculate average intensity for the current cell
for (let subY = 0; subY < cellSize; subY++) {
for (let subX = 0; subX < cellSize; subX++) {
const currentPixelX = x + subX;
const currentPixelY = y + subY;
if (currentPixelX < imgWidth && currentPixelY < imgHeight) {
const pixelIndex = (currentPixelY * imgWidth + currentPixelX) * 4;
const r = pixels[pixelIndex];
const g = pixels[pixelIndex + 1];
const b = pixels[pixelIndex + 2];
// Standard luminance calculation for grayscale
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
sumIntensity += gray;
numPixelsInCell++;
}
}
}
if (numPixelsInCell === 0) continue;
const avgIntensity = sumIntensity / numPixelsInCell; // Range: 0 (black) to 255 (white)
// Convert to dotIntensity: 0 for white areas (small/no dot), 1 for black areas (large dot)
const dotIntensity = 1 - (avgIntensity / 255);
let radiusProportionForLevel;
if (levels === 1) {
// For 1 level, it's a binary decision: dot or no dot based on a threshold (0.5).
radiusProportionForLevel = (dotIntensity >= 0.5) ? 1 : 0;
} else {
// Quantize dotIntensity into one of 'levels' discrete steps.
// levelIndex will be from 0 (for brightest/smallest dot) to levels-1 (for darkest/largest dot).
const levelIndex = Math.round(dotIntensity * (levels - 1));
radiusProportionForLevel = levelIndex / (levels - 1);
}
const dotRadius = radiusProportionForLevel * maxPossibleDotRadius * dotScale;
if (dotRadius >= 0.1) { // Only draw if dot is somewhat visible (0.1px radius threshold)
const centerX = x + cellSize / 2;
const centerY = y + cellSize / 2;
outputCtx.beginPath();
outputCtx.arc(centerX, centerY, dotRadius, 0, 2 * Math.PI, false);
outputCtx.fillStyle = dotColor;
outputCtx.fill();
}
}
}
// 6. Return the output canvas
return outputCanvas;
}
Apply Changes