You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, stitchSize = 10, palette = "", stitchType = "v", backgroundColor = "#FFFFFF") {
// Ensure originalImg is a usable image object with dimensions
if (!originalImg || typeof originalImg.width !== 'number' || typeof originalImg.height !== 'number' || originalImg.width === 0 || originalImg.height === 0) {
const emptyCanvas = document.createElement('canvas');
// Attempt to use provided dimensions if available, otherwise default to 0x0
emptyCanvas.width = (originalImg && originalImg.width) || 0;
emptyCanvas.height = (originalImg && originalImg.height) || 0;
// Use a default valid background color if the provided one is invalid
let validBackgroundColor = "#FFFFFF";
if (typeof backgroundColor === 'string' && backgroundColor.match(/^#[0-9a-fA-F]{6}$/)) {
validBackgroundColor = backgroundColor;
}
if (emptyCanvas.width > 0 && emptyCanvas.height > 0) {
const emptyCtx = emptyCanvas.getContext('2d');
emptyCtx.fillStyle = validBackgroundColor;
emptyCtx.fillRect(0, 0, emptyCanvas.width, emptyCanvas.height);
}
return emptyCanvas;
}
// Parameter validation and normalization
stitchSize = Math.max(1, Number(stitchSize) || 10);
stitchType = ["v", "x", "square"].includes(String(stitchType)) ? String(stitchType) : "v";
if (typeof backgroundColor !== 'string' || !backgroundColor.match(/^#[0-9a-fA-F]{6}$/)) {
backgroundColor = "#FFFFFF"; // Default to white if invalid format
}
if (typeof palette !== 'string') {
palette = ""; // Default to empty string if not a string
}
const numStitchesX = Math.ceil(originalImg.width / stitchSize);
const numStitchesY = Math.ceil(originalImg.height / stitchSize);
// Output canvas should match original image dimensions
const outputCanvas = document.createElement('canvas');
outputCanvas.width = originalImg.width;
outputCanvas.height = originalImg.height;
const ctx = outputCanvas.getContext('2d');
// Fill background of the output canvas
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
// Intermediate canvas for downsampling (to get average colors for stitch blocks)
const avgColorCanvas = document.createElement('canvas');
avgColorCanvas.width = numStitchesX;
avgColorCanvas.height = numStitchesY;
const avgCtx = avgColorCanvas.getContext('2d', { willReadFrequently: true });
// Draw original image scaled down to average colors. Enable smoothing for better averaging.
avgCtx.imageSmoothingEnabled = true;
avgCtx.drawImage(originalImg, 0, 0, numStitchesX, numStitchesY);
// --- Helper functions ---
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
function rgbToHex(r, g, b) {
// Clamp values to 0-255 and round them
r = Math.max(0, Math.min(255, Math.round(r)));
g = Math.max(0, Math.min(255, Math.round(g)));
b = Math.max(0, Math.min(255, Math.round(b)));
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}
function colorDistance(rgb1, rgb2) {
return Math.sqrt(
Math.pow(rgb1.r - rgb2.r, 2) +
Math.pow(rgb1.g - rgb2.g, 2) +
Math.pow(rgb1.b - rgb2.b, 2)
);
}
function findClosestColor(targetRgb, paletteRgbs) {
if (!paletteRgbs || paletteRgbs.length === 0) {
return targetRgb; // Fallback: return original color if palette is empty/invalid
}
let closestColor = paletteRgbs[0];
let minDistance = colorDistance(targetRgb, closestColor);
for (let i = 1; i < paletteRgbs.length; i++) {
const distance = colorDistance(targetRgb, paletteRgbs[i]);
if (distance < minDistance) {
minDistance = distance;
closestColor = paletteRgbs[i];
}
}
return closestColor;
}
// --- End Helper functions ---
const parsedPalette = [];
if (palette.trim() !== "") {
const colors = palette.split(',');
for (const hex of colors) {
const cleanedHex = hex.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(cleanedHex)) {
parsedPalette.push(hexToRgb(cleanedHex));
}
}
}
// Set drawing styles for stitches
ctx.lineWidth = Math.max(1, Math.floor(stitchSize / 6)); // Line thickness relative to stitch size
ctx.lineCap = 'round'; // Makes line endings rounded, softer look
ctx.lineJoin = 'round'; // Makes line joins rounded
for (let i = 0; i < numStitchesX; i++) { // Stitch column index
for (let j = 0; j < numStitchesY; j++) { // Stitch row index
// Get average color for this stitch cell from the downsampled canvas
const pixelData = avgCtx.getImageData(i, j, 1, 1).data;
let r = pixelData[0];
let g = pixelData[1];
let b = pixelData[2];
const a = pixelData[3];
// If the average color of the block is significantly transparent (e.g., alpha < 50%),
// use the background color for the stitch. This ensures stitches are visible.
if (a < 128) {
const bgRgb = hexToRgb(backgroundColor); // backgroundColor is guaranteed valid hex by now
r = bgRgb.r;
g = bgRgb.g;
b = bgRgb.b;
}
let stitchColorHex;
if (parsedPalette.length > 0) {
const closestRgb = findClosestColor({ r, g, b }, parsedPalette);
stitchColorHex = rgbToHex(closestRgb.r, closestRgb.g, closestRgb.b);
} else {
stitchColorHex = rgbToHex(r, g, b);
}
ctx.strokeStyle = stitchColorHex;
ctx.fillStyle = stitchColorHex;
const currentX = i * stitchSize;
const currentY = j * stitchSize;
if (stitchType === "square") {
ctx.fillRect(currentX, currentY, stitchSize, stitchSize);
} else if (stitchType === "v") {
ctx.beginPath();
// A 'V' shape, points adjusted to be within the stitchSize block
// Top-left arm of V
ctx.moveTo(currentX + stitchSize * 0.25, currentY + stitchSize * 0.30);
// Bottom apex of V
ctx.lineTo(currentX + stitchSize * 0.50, currentY + stitchSize * 0.70);
// Top-right arm of V
ctx.lineTo(currentX + stitchSize * 0.75, currentY + stitchSize * 0.30);
ctx.stroke();
} else if (stitchType === "x") {
// An 'X' shape, offsetRatio determines margin from cell boundary
const offsetRatio = 0.20;
ctx.beginPath();
// Line 1: top-left to bottom-right
ctx.moveTo(currentX + stitchSize * offsetRatio, currentY + stitchSize * offsetRatio);
ctx.lineTo(currentX + stitchSize * (1 - offsetRatio), currentY + stitchSize * (1 - offsetRatio));
// Line 2: top-right to bottom-left
ctx.moveTo(currentX + stitchSize * (1 - offsetRatio), currentY + stitchSize * offsetRatio);
ctx.lineTo(currentX + stitchSize * offsetRatio, currentY + stitchSize * (1 - offsetRatio));
ctx.stroke();
}
}
}
return outputCanvas;
}
Apply Changes