You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, dotRadius = 5, spacing = 15, paletteColorsStr = "#5C2D1E,#D9A05B,#FFFFFF,#2E2E2E,#A74A27,#E8B64B", jitter = 2, sizeVariation = 1, backgroundColor = "#000000") {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // willReadFrequently for getImageData performance
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
// Handle cases where image dimensions are not yet available or invalid
console.error("Image has zero width or height.");
return canvas; // Return empty canvas
}
// 1. Draw original image to a temporary canvas to get pixel data reliably
// This step is crucial because originalImg might be an HTMLImageElement
// not yet fully decoded or on a different canvas, and direct getImageData
// from it might be problematic or restricted.
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
tempCtx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Error getting image data: ", e);
// Potentially a CORS issue if image is from another domain and canvas is tainted
// Fallback: fill with background and return
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas;
}
const data = imageData.data;
// 2. Fill the main canvas with background color
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Helper: Hex to RGB
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Parse palette colors
const palette = paletteColorsStr.split(',')
.map(hex => hex.trim())
.map(hexToRgb)
.filter(Boolean); // Remove nulls from invalid hex codes
if (palette.length === 0) {
// Fallback minimal palette if provided string is empty or all invalid
palette.push({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 });
}
// Helper: Calculate color distance (squared Euclidean distance is fine for comparison)
function colorDistanceSquared(color1, color2) {
const dr = color1.r - color2.r;
const dg = color1.g - color2.g;
const db = color1.b - color2.b;
return dr * dr + dg * dg + db * db;
}
// Helper: Find closest palette color
function findClosestPaletteColor(rgbSample, paletteRgbArray) {
let closestColor = paletteRgbArray[0];
let minDistance = colorDistanceSquared(rgbSample, closestColor);
for (let i = 1; i < paletteRgbArray.length; i++) {
const distance = colorDistanceSquared(rgbSample, paletteRgbArray[i]);
if (distance < minDistance) {
minDistance = distance;
closestColor = paletteRgbArray[i];
}
}
return `rgb(${closestColor.r},${closestColor.g},${closestColor.b})`;
}
// Ensure numeric parameters and apply constraints
const numDotRadius = Math.max(0.5, Number(dotRadius)); // Min radius 0.5 for tiny dots
const numSpacing = Math.max(1, Number(spacing));
const numJitter = Math.max(0, Number(jitter));
const numSizeVariation = Math.max(0, Number(sizeVariation));
// 3. Draw dots
for (let y = 0; y < canvas.height; y += numSpacing) {
for (let x = 0; x < canvas.width; x += numSpacing) {
// Jitter position: add random offset within [-numJitter, +numJitter]
const currentX = x + (Math.random() * 2 - 1) * numJitter;
const currentY = y + (Math.random() * 2 - 1) * numJitter;
// Vary size: add random variation within [-numSizeVariation, +numSizeVariation]
let currentRadius = numDotRadius + (Math.random() * 2 - 1) * numSizeVariation;
currentRadius = Math.max(0.5, currentRadius); // Ensure radius is at least 0.5
// Get color from original image at grid point (x, y)
// Clamp coordinates to be within image bounds and convert to integer for pixel array access
const sampleX = Math.min(Math.max(0, Math.floor(x)), canvas.width - 1);
const sampleY = Math.min(Math.max(0, Math.floor(y)), canvas.height - 1);
const pixelIndex = (sampleY * canvas.width + sampleX) * 4;
const r = data[pixelIndex];
const g = data[pixelIndex + 1];
const b = data[pixelIndex + 2];
// const a = data[pixelIndex + 3]; // Alpha, could be used for density/opacity if desired
const sampledColor = { r, g, b };
const dotColor = findClosestPaletteColor(sampledColor, palette);
ctx.beginPath();
ctx.arc(currentX, currentY, currentRadius, 0, Math.PI * 2);
ctx.fillStyle = dotColor;
ctx.fill();
}
}
return canvas;
}
Apply Changes