You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, pixelSize = 8, paletteStr = "default16", outlineColorStr = "black", outlineThickness = 1) {
// --- Parameter Validation & Normalization ---
pixelSize = Math.max(1, Math.floor(pixelSize));
outlineThickness = Math.max(0, Math.floor(outlineThickness));
const ALPHA_THRESHOLD = 64; // Alpha values below this are treated as fully transparent
// --- Helper Functions ---
function hexToRgb(hex) {
hex = hex.replace(/^#/, '');
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
const bigint = parseInt(hex, 16);
if (isNaN(bigint)) return [0,0,0]; // Invalid hex
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return [r, g, b];
}
function colorDistanceSquared(rgb1, rgb2) {
const dr = rgb1[0] - rgb2[0];
const dg = rgb1[1] - rgb2[1];
const db = rgb1[2] - rgb2[2];
return dr * dr + dg * dg + db * db;
}
function parsePalette(pStr) {
const palette = [];
if (pStr === "default16") { // PICO-8 inspired palette
const defaultHexColors = [
"#000000", "#1D2B53", "#7E2553", "#008751",
"#AB5236", "#5F574F", "#C2C3C7", "#FFF1E8",
"#FF004D", "#FFA300", "#FFEC27", "#00E436",
"#29ADFF", "#83769C", "#FF77A8", "#FFCCAA"
];
defaultHexColors.forEach(hex => palette.push(hexToRgb(hex)));
} else if (pStr === "grayscale16") {
for (let i = 0; i < 16; i++) {
const shade = Math.round(i * (255 / 15));
palette.push([shade, shade, shade]);
}
} else {
const hexColors = pStr.split(',');
hexColors.forEach(hex => {
if (hex.trim()) palette.push(hexToRgb(hex.trim()));
});
}
if (palette.length === 0) { // Fallback if parsing fails or results in empty palette
palette.push([0,0,0]); // Ensure palette has at least one color (black)
palette.push([255,255,255]); // And white, for some contrast
}
return palette;
}
function findClosestColor(targetRgb, rgbPalette) {
let closestColor = rgbPalette[0];
let minDistance = colorDistanceSquared(targetRgb, closestColor);
for (let i = 1; i < rgbPalette.length; i++) {
const dist = colorDistanceSquared(targetRgb, rgbPalette[i]);
if (dist < minDistance) {
minDistance = dist;
closestColor = rgbPalette[i];
}
}
return closestColor;
}
// --- Handle Empty/Invalid Image ---
if (originalImg.width === 0 || originalImg.height === 0) {
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1;
emptyCanvas.height = 1;
return emptyCanvas;
}
// --- Canvas & Context Setup for Original Image ---
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0);
const smallWidth = Math.ceil(originalImg.width / pixelSize);
const smallHeight = Math.ceil(originalImg.height / pixelSize);
if (smallWidth === 0 || smallHeight === 0) {
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1; emptyCanvas.height = 1;
return emptyCanvas;
}
// --- 1. Pixelation (Average Color Calculation) ---
const pixelGrid = []; // Stores [avgR, avgG, avgB, avgA]
for (let y = 0; y < smallHeight; y++) {
for (let x = 0; x < smallWidth; x++) {
const startX = x * pixelSize;
const startY = y * pixelSize;
// Clamp block dimensions to image boundaries
const blockWidth = Math.min(pixelSize, originalImg.width - startX);
const blockHeight = Math.min(pixelSize, originalImg.height - startY);
if (blockWidth <= 0 || blockHeight <= 0) continue; // Should not happen with Math.ceil for smallW/H
const imageData = tempCtx.getImageData(startX, startY, blockWidth, blockHeight);
const data = imageData.data;
let rTotal = 0, gTotal = 0, bTotal = 0, aTotal = 0;
const numPixelsInBlock = blockWidth * blockHeight;
for (let i = 0; i < data.length; i += 4) {
rTotal += data[i];
gTotal += data[i + 1];
bTotal += data[i + 2];
aTotal += data[i + 3];
}
pixelGrid.push([
Math.round(rTotal / numPixelsInBlock),
Math.round(gTotal / numPixelsInBlock),
Math.round(bTotal / numPixelsInBlock),
Math.round(aTotal / numPixelsInBlock)
]);
}
}
// --- 2. Color Quantization ---
const activePalette = parsePalette(paletteStr);
const quantizedPixelGrid = pixelGrid.map(avgColor => {
const [avgR, avgG, avgB, avgA] = avgColor;
if (avgA < ALPHA_THRESHOLD) {
return [0, 0, 0, 0]; // Treat as fully transparent
}
const closestRgb = findClosestColor([avgR, avgG, avgB], activePalette);
return [closestRgb[0], closestRgb[1], closestRgb[2], avgA];
});
// --- 3. Output Canvas Setup ---
const outputCanvas = document.createElement('canvas');
outputCanvas.width = smallWidth * pixelSize;
outputCanvas.height = smallHeight * pixelSize;
const outputCtx = outputCanvas.getContext('2d');
outputCtx.imageSmoothingEnabled = false; // Crucial for sharp pixels
// --- 4. Draw Outline Pass (Dilation Method) ---
let performOutline = false;
if (outlineThickness > 0 && outlineColorStr) {
const lowerOutlineColorStr = outlineColorStr.toLowerCase();
if (lowerOutlineColorStr !== 'none' && lowerOutlineColorStr !== 'transparent') {
// Check if the color string resolves to fully transparent
const prevFillStyle = outputCtx.fillStyle; // Save current style
outputCtx.fillStyle = outlineColorStr;
if (outputCtx.fillStyle !== 'rgba(0, 0, 0, 0)') { // Canvas standard for fully transparent
performOutline = true;
}
outputCtx.fillStyle = prevFillStyle; // Restore (though it'll be set again if performOutline)
}
}
if (performOutline) {
outputCtx.fillStyle = outlineColorStr; // Set the outline color
for (let yCore = 0; yCore < smallHeight; yCore++) {
for (let xCore = 0; xCore < smallWidth; xCore++) {
const corePixelData = quantizedPixelGrid[yCore * smallWidth + xCore];
if (corePixelData && corePixelData[3] >= ALPHA_THRESHOLD) { // If this core pixel is solid
for (let dy = -outlineThickness; dy <= outlineThickness; dy++) {
for (let dx = -outlineThickness; dx <= outlineThickness; dx++) {
const outlinePixelXSmall = xCore + dx;
const outlinePixelYSmall = yCore + dy;
if (outlinePixelXSmall >= 0 && outlinePixelXSmall < smallWidth &&
outlinePixelYSmall >= 0 && outlinePixelYSmall < smallHeight) {
outputCtx.fillRect(
outlinePixelXSmall * pixelSize,
outlinePixelYSmall * pixelSize,
pixelSize,
pixelSize
);
}
}
}
}
}
}
}
// --- 5. Draw Foreground Pass ---
for (let y = 0; y < smallHeight; y++) {
for (let x = 0; x < smallWidth; x++) {
const pixelIndex = y * smallWidth + x;
if (pixelIndex >= quantizedPixelGrid.length) continue; // Safety for any off-by-one
const pixelData = quantizedPixelGrid[pixelIndex];
if (!pixelData) continue; // Should not happen
const [r, g, b, a] = pixelData;
if (a >= ALPHA_THRESHOLD) { // Only draw if not (or minimally) transparent
outputCtx.fillStyle = `rgba(${r},${g},${b},${a / 255})`;
outputCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
}
}
return outputCanvas;
}
Apply Changes