Please bookmark this page to avoid losing your image tool!

16-bit Game Character Portrait Creator Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The 16-bit Game Character Portrait Creator Tool allows users to transform images into stylized pixel art versions reminiscent of retro video game graphics. Users can adjust parameters such as pixel size and color palette to achieve the desired artistic effect. This tool is ideal for game developers looking to create character art, for artists wanting to experiment with pixel art styles, and for fans of retro gaming aesthetics who want to convert their images into nostalgic 16-bit art.

Leave a Reply

Your email address will not be published. Required fields are marked *