You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
fillPaletteStr = "A0522D,D2B48C,F5DEB3,8B4513", // Default: Sienna, Tan, Wheat, SaddleBrown
lineColorHex = "301A0A", // Default: Very Dark Brown (almost black)
lineThickness = 2,
cellSize = 24,
patternDensity = 0.8,
textureStrength = 0.05
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure canvas dimensions are from natural dimensions if available
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// --- Utility functions ---
function hexToRgb(hexString) {
let hex = String(hexString).trim();
if (hex.startsWith('#')) {
hex = hex.substring(1);
}
if (!/^[0-9A-Fa-f]{6}$/i.test(hex)) {
console.warn("Invalid hex color string:", hexString, "- using black as fallback.");
return { r: 0, g: 0, b: 0 };
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return { r, g, b };
}
function colorDistanceSquared(c1, c2) { // c1, c2 are {r,g,b} objects
const dr = c1.r - c2.r;
const dg = c1.g - c2.g;
const db = c1.b - c2.b;
return dr * dr + dg * dg + db * db; // Squared Euclidean distance for speed
}
// Parse fill palette colors
let fillPalette = String(fillPaletteStr).split(',')
.map(hex => hex.trim())
.filter(hex => hex.length > 0) // Ensure not empty string from " ,, "
.map(hex => hexToRgb(hex));
// Fallback if palette parsing fails or results in an empty palette
if (fillPalette.length === 0) {
console.warn("Fill palette is empty or invalid, using default fill colors.");
fillPalette.push(hexToRgb("A0522D"));
fillPalette.push(hexToRgb("D2B48C"));
}
const lineColorRgb = hexToRgb(lineColorHex);
// 1. Draw original image to canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// 2. Apply Color Quantization using the fillPalette
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i+3] === 0) continue; // Skip fully transparent pixels
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const originalPixelColor = { r, g, b };
let closestColor = fillPalette[0];
let minDistance = colorDistanceSquared(originalPixelColor, closestColor);
for (let j = 1; j < fillPalette.length; j++) {
const dist = colorDistanceSquared(originalPixelColor, fillPalette[j]);
if (dist < minDistance) {
minDistance = dist;
closestColor = fillPalette[j];
}
}
data[i] = closestColor.r;
data[i + 1] = closestColor.g;
data[i + 2] = closestColor.b;
}
ctx.putImageData(imageData, 0, 0); // Quantized image is now on canvas
// 3. Overlay Tapa Patterns
ctx.strokeStyle = `rgb(${lineColorRgb.r}, ${lineColorRgb.g}, ${lineColorRgb.b})`;
ctx.fillStyle = `rgb(${lineColorRgb.r}, ${lineColorRgb.g}, ${lineColorRgb.b})`; // For filled pattern elements like dots
ctx.lineWidth = Math.max(1, Number(lineThickness) || 1); // Ensure lineWidth is at least 1
const currentCellSize = Math.max(8, Number(cellSize) || 24); // Minimum cell size
const numCellsX = Math.ceil(canvas.width / currentCellSize);
const numCellsY = Math.ceil(canvas.height / currentCellSize);
const currentPatternDensity = Math.max(0, Math.min(1, Number(patternDensity) || 0.8));
for (let cy = 0; cy < numCellsY; cy++) {
for (let cx = 0; cx < numCellsX; cx++) {
if (Math.random() > currentPatternDensity) continue;
const x = cx * currentCellSize;
const y = cy * currentCellSize;
// Calculate average brightness of the cell from the quantized image data
let sumBrightness = 0;
let pixelsInCell = 0;
for (let offY = 0; offY < currentCellSize && y + offY < canvas.height; offY++) {
for (let offX = 0; offX < currentCellSize && x + offX < canvas.width; offX++) {
const Gx = x + offX;
const Gy = y + offY;
const idx = (Gy * canvas.width + Gx) * 4;
if (data[idx+3] > 0) { // Consider only opaque pixels
sumBrightness += (data[idx] + data[idx+1] + data[idx+2]) / 3;
pixelsInCell++;
}
}
}
const avgBrightness = pixelsInCell > 0 ? sumBrightness / pixelsInCell / 255 : 0.5; // Normalized 0-1
ctx.beginPath(); // Start a new path for each cell's pattern elements (except for fillRect)
const patternTypeRand = Math.random();
// Darker areas get more complex/dense patterns
if (avgBrightness < 0.35) {
if (patternTypeRand < 0.33) { // Cross-hatch
ctx.moveTo(x, y); ctx.lineTo(x + currentCellSize, y + currentCellSize);
ctx.moveTo(x + currentCellSize, y); ctx.lineTo(x, y + currentCellSize);
} else if (patternTypeRand < 0.66) { // Multiple Parallel Lines (horizontal)
for (let k = 1; k <= 3; k++) { // 3 lines
ctx.moveTo(x, Math.round(y + currentCellSize * k / 4));
ctx.lineTo(x + currentCellSize, Math.round(y + currentCellSize * k / 4));
}
} else { // Zig-zag / Chevrons
ctx.moveTo(x, Math.round(y + currentCellSize * 0.25));
ctx.lineTo(Math.round(x + currentCellSize * 0.5), Math.round(y + currentCellSize * 0.75));
ctx.lineTo(x + currentCellSize, Math.round(y + currentCellSize * 0.25));
}
} else if (avgBrightness < 0.65) { // Medium brightness areas
if (patternTypeRand < 0.5) { // Single Diagonal
if (Math.random() < 0.5) {ctx.moveTo(x, y); ctx.lineTo(x + currentCellSize, y + currentCellSize);} // \
else {ctx.moveTo(x + currentCellSize, y); ctx.lineTo(x, y + currentCellSize);} // /
} else { // Small Center Cross
const cX = Math.round(x + currentCellSize / 2); const cY = Math.round(y + currentCellSize / 2);
const armLength = Math.round(currentCellSize / 4);
ctx.moveTo(cX - armLength, cY); ctx.lineTo(cX + armLength, cY);
ctx.moveTo(cX, cY - armLength); ctx.lineTo(cX, cY + armLength);
}
} else { // Lighter areas (simplest patterns or dots)
if (patternTypeRand < 0.5) { // Draw a few dots
const numDots = Math.floor(Math.random() * 3) + 1; // 1 to 3 dots
const dotSize = Math.max(1, Math.floor(ctx.lineWidth / 2) || 1);
for(let k=0; k<numDots; k++) {
// fillRect draws immediately, does not add to current path.
ctx.fillRect(
Math.round(x + Math.random() * (currentCellSize - dotSize)),
Math.round(y + Math.random() * (currentCellSize - dotSize)),
dotSize, dotSize
);
}
} else { // Cell border/frame
ctx.rect( // Adds to current path
Math.round(x + ctx.lineWidth/2),
Math.round(y + ctx.lineWidth/2),
Math.round(currentCellSize - ctx.lineWidth),
Math.round(currentCellSize - ctx.lineWidth)
);
}
}
// Stroke the path created by moveTo/lineTo/rect. Dots (fillRect) are already drawn.
ctx.stroke();
}
}
// 4. Apply Texture (subtle noise)
const currentTextureStrength = Math.max(0, Math.min(1, Number(textureStrength) || 0.05));
if (currentTextureStrength > 0) {
const texturedImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const texData = texturedImageData.data;
const noiseMultiplier = 50; // Scales strength: 0.1 strength => noise range +/- 2.5 RGB
for (let i = 0; i < texData.length; i += 4) {
if (texData[i+3] === 0) continue; // Skip transparent
// Apply same noise value to R, G, B to affect lightness more than hue
const noiseVal = (Math.random() - 0.5) * currentTextureStrength * noiseMultiplier;
texData[i] = Math.max(0, Math.min(255, texData[i] + noiseVal));
texData[i + 1] = Math.max(0, Math.min(255, texData[i + 1] + noiseVal));
texData[i + 2] = Math.max(0, Math.min(255, texData[i + 2] + noiseVal));
}
ctx.putImageData(texturedImageData, 0, 0);
}
return canvas;
}
Apply Changes