Please bookmark this page to avoid losing your image tool!

Image Aztec Codex Page Creator

(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.
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;
}

Free Image Tool Creator

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

Description

The Image Aztec Codex Page Creator is a web tool designed to convert images into a stylized format resembling ancient Aztec codices. This tool allows users to upload an image and apply various artistic effects, such as color quantization to a specified palette, the addition of a textured paper background, and outline effects to enhance the overall aesthetics. Ideal use cases include creating unique visual artwork, enhancing game graphics, or producing themed art for educational purposes, such as history projects or cultural presentations.

Leave a Reply

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