You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, colorsStr = "FF5F00,0074D9", dotSize = 5, grainAmount = 15, offsetX = 1, offsetY = 1, blendMode = "multiply", backgroundColor = "#FFFFFF") {
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) {
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1;
emptyCanvas.height = 1;
const emptyCtx = emptyCanvas.getContext('2d');
if (emptyCtx) {
emptyCtx.fillStyle = backgroundColor;
emptyCtx.fillRect(0, 0, 1, 1);
}
return emptyCanvas;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imgWidth;
canvas.height = imgHeight;
// Fill background on the main canvas
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const parsedColors = colorsStr.split(',')
.map(c => c.trim())
.filter(c => c.length > 0)
.map(c => `#${c.replace('#', '')}`);
if (parsedColors.length === 0) {
// Fallback if colorsStr is empty or results in no valid colors
parsedColors.push("#FF5F00");
parsedColors.push("#0074D9");
}
// Create Grayscale version of the image for intensity mapping
const grayscaleCanvas = document.createElement('canvas');
grayscaleCanvas.width = canvas.width;
grayscaleCanvas.height = canvas.height;
// Add { willReadFrequently: true } for potential performance optimization
const gsCtx = grayscaleCanvas.getContext('2d', { willReadFrequently: true });
// Draw original image to grayscale canvas to get its pixel data including alpha
gsCtx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imgDataForGrayscale = gsCtx.getImageData(0, 0, canvas.width, canvas.height);
const gsPixels = imgDataForGrayscale.data;
for (let i = 0; i < gsPixels.length; i += 4) {
const r = gsPixels[i];
const g = gsPixels[i + 1];
const b = gsPixels[i + 2];
// Standard luminance calculation
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
gsPixels[i] = gsPixels[i + 1] = gsPixels[i + 2] = gray;
// Alpha (gsPixels[i+3]) is preserved from original image
}
gsCtx.putImageData(imgDataForGrayscale, 0, 0);
const actualDotSize = Math.max(1, Math.floor(dotSize)); // dotSize should be integer and >= 1
parsedColors.forEach((color, layerIndex) => {
const layerCanvas = document.createElement('canvas');
layerCanvas.width = canvas.width;
layerCanvas.height = canvas.height;
const layerCtx = layerCanvas.getContext('2d');
layerCtx.fillStyle = color;
// Halftone effect for this layer
for (let y = 0; y < canvas.height; y += actualDotSize) {
for (let x = 0; x < canvas.width; x += actualDotSize) {
// Adjust block size for edges of the image
const blockWidth = Math.min(actualDotSize, canvas.width - x);
const blockHeight = Math.min(actualDotSize, canvas.height - y);
if (blockWidth <= 0 || blockHeight <= 0) continue;
// Get average brightness from grayscaleCanvas for the current cell
const blockImageData = gsCtx.getImageData(x, y, blockWidth, blockHeight);
const blockPixels = blockImageData.data;
let totalGray = 0;
let numSignificantPixelsInBlock = 0; // Counts pixels that contribute to "ink"
for (let k = 0; k < blockPixels.length; k += 4) {
// Consider pixel if it's not fully transparent (or mostly transparent)
if (blockPixels[k+3] > 32) { // Alpha threshold (0-255)
totalGray += blockPixels[k]; // R, G, B are all gray
numSignificantPixelsInBlock++;
}
}
if (numSignificantPixelsInBlock === 0) continue; // Skip if block is effectively transparent
const avgGray = totalGray / numSignificantPixelsInBlock;
// normalizedIntensity: 0 for black (darkest), 1 for white (lightest).
const normalizedIntensity = avgGray / 255;
// dotRadiusFactor: 1 for black (full dot), 0 for white (no dot).
// This means darker areas of the original image get more ink.
const dotRadiusFactor = 1.0 - normalizedIntensity;
// Define a minimum radius for a dot to be visible, relative to cell size
const minRadiusThreshold = 0.1 * (actualDotSize / 2) ; // e.g. 10% of max radius
const dotRadius = (actualDotSize / 2) * dotRadiusFactor;
if (dotRadius > minRadiusThreshold) {
layerCtx.beginPath();
// Center dot in the middle of the potentially smaller block at edges
layerCtx.arc(x + blockWidth / 2, y + blockHeight / 2, Math.max(0, dotRadius), 0, 2 * Math.PI, false);
layerCtx.fill();
}
}
}
// Composite this layer onto the main canvas
if (layerIndex > 0) {
ctx.globalCompositeOperation = blendMode;
} else {
// First layer is drawn directly over the background
ctx.globalCompositeOperation = 'source-over';
}
const currentOffsetX = layerIndex * offsetX;
const currentOffsetY = layerIndex * offsetY;
ctx.drawImage(layerCanvas, currentOffsetX, currentOffsetY);
});
// Reset composite operation for grain application or other subsequent drawing
ctx.globalCompositeOperation = 'source-over';
// Add Grain (if grainAmount > 0)
const absGrainAmount = Math.abs(grainAmount);
if (absGrainAmount > 0 && absGrainAmount <= 255) {
const finalImgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const finalPixels = finalImgData.data;
for (let i = 0; i < finalPixels.length; i += 4) {
// Only apply grain to pixels that are not fully transparent
if (finalPixels[i+3] === 0) continue;
// Add monochrome noise
const noise = (Math.random() - 0.5) * absGrainAmount;
finalPixels[i] = Math.max(0, Math.min(255, finalPixels[i] + noise));
finalPixels[i+1] = Math.max(0, Math.min(255, finalPixels[i+1] + noise));
finalPixels[i+2] = Math.max(0, Math.min(255, finalPixels[i+2] + noise));
}
ctx.putImageData(finalImgData, 0, 0);
}
return canvas;
}
Apply Changes