You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, paletteColorsStr = "#BC503F,#961E28,#ECB450,#5E96B4,#3C825A,#282828,#F0F0E0", outlineThickness = 2, paperColorHex = "#E0D6C0", textureIntensity = 0.25) {
// Helper function to convert HEX to RGB
// Placed inside to keep the function self-contained as per typical interpretation of "single function" requests,
// or can be outside if preferred and context allows.
function _hexToRgb(hex) {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) {
console.warn(`Invalid hex color: ${hex}. Defaulting to black.`);
return { r: 0, g: 0, b: 0 };
}
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
};
}
// Helper function to find the closest color in a palette
function _findClosestColor(rgb, palette) {
let closest = palette[0];
let minDistance = Infinity;
if (!closest) { // Palette might be empty if input string was bad
return { r: rgb.r, g: rgb.g, b: rgb.b }; // Return original color
}
for (const color of palette) {
const distance = Math.sqrt(
Math.pow(rgb.r - color.r, 2) +
Math.pow(rgb.g - color.g, 2) +
Math.pow(rgb.b - color.b, 2)
);
if (distance < minDistance) {
minDistance = distance;
closest = color;
}
}
return closest;
}
const parsedPalette = paletteColorsStr.split(',')
.map(hex => hex.trim())
.filter(hex => hex.length > 0)
.map(_hexToRgb);
if (parsedPalette.length === 0) {
console.warn("Palette is empty. Using fallback colors.");
// Add a simple fallback palette if parsing failed critically
parsedPalette.push({r:0,g:0,b:0}, {r:255,g:255,b:255});
}
const outputCanvas = document.createElement('canvas');
const ctx = outputCanvas.getContext('2d');
// Ensure naturalWidth and naturalHeight are available
if (!originalImg.naturalWidth || !originalImg.naturalHeight) {
console.error("Image not fully loaded or invalid.");
// Return an empty or error indicator canvas
outputCanvas.width = 100;
outputCanvas.height = 50;
ctx.fillStyle = "red";
ctx.fillRect(0,0,100,50);
ctx.fillStyle = "white";
ctx.fillText("Error", 10, 30);
return outputCanvas;
}
outputCanvas.width = originalImg.naturalWidth;
outputCanvas.height = originalImg.naturalHeight;
// 1. Draw original image to a temporary canvas for processing quantizatio
const tempCanvas = document.createElement('canvas');
tempCanvas.width = outputCanvas.width;
tempCanvas.height = outputCanvas.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// 2. Quantize colors
for (let i = 0; i < data.length; i += 4) {
// Only process opaque pixels or pixels with some alpha
if (data[i + 3] > 0) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const closest = _findClosestColor({ r, g, b }, parsedPalette);
data[i] = closest.r;
data[i + 1] = closest.g;
data[i + 2] = closest.b;
// Alpha (data[i+3]) remains unchanged (original alpha)
}
}
tempCtx.putImageData(imageData, 0, 0); // tempCanvas now holds the color-quantized image with original alpha
// 3. Prepare output canvas with paper texture
ctx.fillStyle = paperColorHex;
ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
if (textureIntensity > 0) {
const paperData = ctx.getImageData(0, 0, outputCanvas.width, outputCanvas.height);
const paperPixels = paperData.data;
const basePaperRgb = _hexToRgb(paperColorHex);
const maxNoiseMagnitude = 50; // Max deviation in RGB component for textureIntensity = 1
for (let i = 0; i < paperPixels.length; i += 4) {
// paperPixels[i, i+1, i+2] are from paperColorHex base
const noiseFactor = (Math.random() - 0.5) * 2 * textureIntensity * maxNoiseMagnitude;
paperPixels[i] = Math.max(0, Math.min(255, basePaperRgb.r + Math.round(noiseFactor)));
paperPixels[i+1] = Math.max(0, Math.min(255, basePaperRgb.g + Math.round(noiseFactor)));
paperPixels[i+2] = Math.max(0, Math.min(255, basePaperRgb.b + Math.round(noiseFactor)));
// Alpha paperPixels[i+3] remains 255
}
ctx.putImageData(paperData, 0, 0);
}
// 4. Draw outlines (shadow effect for non-transparent parts of quantized image)
if (outlineThickness > 0) {
const silhouetteCanvas = document.createElement('canvas');
silhouetteCanvas.width = outputCanvas.width;
silhouetteCanvas.height = outputCanvas.height;
const silCtx = silhouetteCanvas.getContext('2d');
// Draw the quantized image (which has original alpha) onto silhouette canvas
silCtx.drawImage(tempCanvas, 0, 0);
const silImageData = silCtx.getImageData(0, 0, silhouetteCanvas.width, silhouetteCanvas.height);
const silData = silImageData.data;
for (let i = 0; i < silData.length; i += 4) {
// If pixel in quantized image was not fully transparent (alpha from tempCanvas)
if (silData[i+3] > 10) { // Use a small threshold to capture faint edges
silData[i] = 0; // R = Black
silData[i+1] = 0; // G = Black
silData[i+2] = 0; // B = Black
silData[i+3] = 255; // Alpha = Solid for outline
} else {
silData[i+3] = 0; // Make fully transparent if source was transparent/faint
}
}
silCtx.putImageData(silImageData, 0, 0); // silhouetteCanvas now has black shapes
// Draw silhouette offset multiple times for outline effect
// Using a fixed black for outline color, common in codices
ctx.fillStyle = "black"; // Not strictly needed as silhouette itself is black
const offsets = [
[-outlineThickness, -outlineThickness], [0, -outlineThickness], [outlineThickness, -outlineThickness],
[-outlineThickness, 0], [outlineThickness, 0],
[-outlineThickness, outlineThickness], [0, outlineThickness], [outlineThickness, outlineThickness]
];
for (const [dx, dy] of offsets) {
ctx.drawImage(silhouetteCanvas, dx, dy);
}
}
// 5. Draw the color-quantized image on top of the paper and outlines
ctx.drawImage(tempCanvas, 0, 0);
return outputCanvas;
}
Apply Changes