You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, inputCellSize = 10, outputCellSize = 20, dominoOrientation = "vertical", dotColor = "black", dominoBodyColor = "white", lineColor = "black", dominoLineThickness = 1, gap = 2, invertBrightnessToPips = 0) {
if (!originalImg || originalImg.width === 0 || originalImg.height === 0) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
// Optional: fill with a default color or leave transparent
// const ctx = canvas.getContext('2d');
// ctx.fillStyle = 'gray';
// ctx.fillRect(0,0,1,1);
console.error("Original image is not loaded or has zero dimensions.");
return canvas;
}
// Sanitize numerical parameters
inputCellSize = Math.max(1, Math.floor(inputCellSize));
outputCellSize = Math.max(1, Math.floor(outputCellSize));
gap = Math.max(0, Math.floor(gap));
dominoLineThickness = Math.max(1, Math.floor(dominoLineThickness));
// Create a temporary canvas to get image pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0);
const fullImageData = tempCtx.getImageData(0, 0, originalImg.width, originalImg.height);
// Helper function to get average grayscale value from a block of image data
const getAverageGray = (imageData, sx, sy, blockWidth, blockHeight) => {
let sumGray = 0;
let count = 0;
const data = imageData.data;
const imageActualWidth = imageData.width; // width of the imageData buffer
// Clamp block coordinates and dimensions to be within imageData bounds
const startY = Math.max(0, sy);
const endY = Math.min(imageData.height, sy + blockHeight);
const startX = Math.max(0, sx);
const endX = Math.min(imageActualWidth, sx + blockWidth);
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const i = (y * imageActualWidth + x) * 4; // Calculate index in the 1D data array
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Standard luminance calculation
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
sumGray += gray;
count++;
}
}
// If block is outside image or has no pixels, treat as black.
// The mapGrayToPips function will then handle inversion if specified.
if (count === 0) return 0;
return sumGray / count;
};
const maxPipsPerHalf = 6; // Standard dominoes go up to 6 pips per half
// Helper function to map a grayscale value (0-255) to a pip count (0-maxPipsPerHalf)
const mapGrayToPips = (gray) => {
const val = gray / 255; // Normalize gray value to 0 (black) - 1 (white)
const numBins = maxPipsPerHalf + 1; // e.g., 7 bins for 0-6 pips
let pips;
if (invertBrightnessToPips === 1) { // Lighter image area = more pips
pips = Math.floor(val * numBins);
} else { // Darker image area = more pips (default behavior)
pips = Math.floor((1 - val) * numBins);
}
// Clamp pips to ensure it's within [0, maxPipsPerHalf]
return Math.min(maxPipsPerHalf, Math.max(0, pips));
};
// Define dot patterns for 0-6 pips
// Coordinates are factors of cellSize (e.g., 0.5 means center)
const dotRadius = Math.max(0.5, outputCellSize / 14); // Ensure minimum radius for visibility
const dotPatterns = {
0: [],
1: [[0.5, 0.5]],
2: [[0.25, 0.25], [0.75, 0.75]], // Diagonal
3: [[0.25, 0.25], [0.5, 0.5], [0.75, 0.75]], // Diagonal + center
4: [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]], // Corners
5: [[0.25, 0.25], [0.75, 0.25], [0.5, 0.5], [0.25, 0.75], [0.75, 0.75]], // Corners + center
6: [ // Two columns of three dots
[0.25, 0.25], [0.75, 0.25],
[0.25, 0.5], [0.75, 0.5],
[0.25, 0.75], [0.75, 0.75]
]
};
// Helper function to draw dots for one half of a domino
const drawDominoHalf = (ctx, halfX, halfY, cellSize, pips, currentDotColor) => {
ctx.fillStyle = currentDotColor;
const pattern = dotPatterns[pips] || []; // Default to empty if pips out of range (should not happen with clamping)
pattern.forEach(([cxFactor, cyFactor]) => {
ctx.beginPath();
ctx.arc(halfX + cxFactor * cellSize, halfY + cyFactor * cellSize, dotRadius, 0, 2 * Math.PI);
ctx.fill();
});
};
let numDominoesX, numDominoesY;
let outputDominoActualWidth, outputDominoActualHeight; // Dimensions of a single drawn domino piece
const orientation = String(dominoOrientation).toLowerCase();
if (orientation === "vertical") {
numDominoesX = Math.floor(originalImg.width / inputCellSize);
numDominoesY = Math.floor(originalImg.height / (inputCellSize * 2));
outputDominoActualWidth = outputCellSize;
outputDominoActualHeight = outputCellSize * 2;
} else { // Assume "horizontal"
numDominoesX = Math.floor(originalImg.width / (inputCellSize * 2));
numDominoesY = Math.floor(originalImg.height / inputCellSize);
outputDominoActualWidth = outputCellSize * 2;
outputDominoActualHeight = outputCellSize;
}
if (numDominoesX <= 0 || numDominoesY <= 0) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
console.warn("Image is too small for the specified inputCellSize to create any dominoes.");
return canvas;
}
const outputCanvas = document.createElement('canvas');
outputCanvas.width = numDominoesX * (outputDominoActualWidth + gap) - (gap > 0 ? gap : 0);
outputCanvas.height = numDominoesY * (outputDominoActualHeight + gap) - (gap > 0 ? gap : 0);
const outputCtx = outputCanvas.getContext('2d');
// ---- Main loop to process image blocks and draw dominoes ----
for (let dy = 0; dy < numDominoesY; dy++) { // Iterate through domino rows
for (let dx = 0; dx < numDominoesX; dx++) { // Iterate through domino columns
let imgBlockX1, imgBlockY1, imgBlockX2, imgBlockY2; // Coordinates for the two halves in the source image
if (orientation === "vertical") {
imgBlockX1 = dx * inputCellSize;
imgBlockY1 = dy * 2 * inputCellSize;
imgBlockX2 = dx * inputCellSize;
imgBlockY2 = dy * 2 * inputCellSize + inputCellSize;
} else { // Horizontal
imgBlockX1 = dx * 2 * inputCellSize;
imgBlockY1 = dy * inputCellSize;
imgBlockX2 = dx * 2 * inputCellSize + inputCellSize;
imgBlockY2 = dy * inputCellSize;
}
// Get average gray values for the two halves
const gray1 = getAverageGray(fullImageData, imgBlockX1, imgBlockY1, inputCellSize, inputCellSize);
const gray2 = getAverageGray(fullImageData, imgBlockX2, imgBlockY2, inputCellSize, inputCellSize);
// Map gray values to pip counts
const pips1 = mapGrayToPips(gray1);
const pips2 = mapGrayToPips(gray2);
// Calculate draw position on the output canvas
const currentDrawX = dx * (outputDominoActualWidth + gap);
const currentDrawY = dy * (outputDominoActualHeight + gap);
// Draw domino body (background rectangle)
outputCtx.fillStyle = dominoBodyColor;
outputCtx.fillRect(currentDrawX, currentDrawY, outputDominoActualWidth, outputDominoActualHeight);
// Draw the two halves and the separating line
outputCtx.fillStyle = lineColor; // Set color for the line
if (orientation === "vertical") {
drawDominoHalf(outputCtx, currentDrawX, currentDrawY, outputCellSize, pips1, dotColor);
// Draw line centered between halves
const lineY = currentDrawY + outputCellSize - Math.ceil(dominoLineThickness / 2);
outputCtx.fillRect(currentDrawX, lineY, outputCellSize, dominoLineThickness);
drawDominoHalf(outputCtx, currentDrawX, currentDrawY + outputCellSize, outputCellSize, pips2, dotColor);
} else { // Horizontal
drawDominoHalf(outputCtx, currentDrawX, currentDrawY, outputCellSize, pips1, dotColor);
// Draw line centered between halves
const lineX = currentDrawX + outputCellSize - Math.ceil(dominoLineThickness / 2);
outputCtx.fillRect(lineX, currentDrawY, dominoLineThickness, outputCellSize);
drawDominoHalf(outputCtx, currentDrawX + outputCellSize, currentDrawY, outputCellSize, pips2, dotColor);
}
}
}
return outputCanvas;
}
Apply Changes