Please bookmark this page to avoid losing your image tool!

Image To PlayStation 1 Spyro Style Texture Converter

(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, maxTextureSize = "256", ditherStrength = "48", saturationBoost = "1.3") {
    // Parse parameters
    const maxSize = parseInt(maxTextureSize, 10);
    const strength = parseFloat(ditherStrength);
    const boost = parseFloat(saturationBoost);

    // Keep aspect ratio but cap at maxSize for the true PS1 texture resolution
    let w = originalImg.width;
    let h = originalImg.height;

    if (w > h) {
        if (w > maxSize) {
            h = Math.floor(h * (maxSize / w));
            w = maxSize;
        }
    } else {
        if (h > maxSize) {
            w = Math.floor(w * (maxSize / h));
            h = maxSize;
        }
    }

    // Step 1: Create a low-res canvas to simulate texture
    const lowResCanvas = document.createElement('canvas');
    lowResCanvas.width = w;
    lowResCanvas.height = h;
    const ctx = lowResCanvas.getContext('2d');
    ctx.drawImage(originalImg, 0, 0, w, h);

    const imgData = ctx.getImageData(0, 0, w, h);
    const data = imgData.data;

    // Step 2: Apply vibrant saturation boost (Spyro style textures are highly saturated)
    for (let i = 0; i < data.length; i += 4) {
        let r = data[i], g = data[i+1], b = data[i+2];
        let lum = 0.299 * r + 0.587 * g + 0.114 * b;
        data[i]   = Math.min(255, Math.max(0, lum + (r - lum) * boost));
        data[i+1] = Math.min(255, Math.max(0, lum + (g - lum) * boost));
        data[i+2] = Math.min(255, Math.max(0, lum + (b - lum) * boost));
    }

    // Step 3: Extract a 16-color palette using K-Means++
    const k = 16;
    let pixels = [];
    // Subsample the image for faster clustering
    let step = Math.max(4, Math.floor(data.length / (2000 * 4)) * 4);
    for (let i = 0; i < data.length; i += step) {
        if (data[i+3] > 0) { // Ignore fully transparency for color selection
            pixels.push([data[i], data[i+1], data[i+2]]);
        }
    }
    if (pixels.length === 0) pixels.push([0, 0, 0]);

    const distSq = (c1, c2) => (c1[0]-c2[0])**2 + (c1[1]-c2[1])**2 + (c1[2]-c2[2])**2;

    // K-Means++ centroid initialization
    let centroids = [pixels[Math.floor(Math.random() * pixels.length)]];
    for (let i = 1; i < k; i++) {
        let maxDist = -1;
        let bestPixel = pixels[0];
        for (let p of pixels) {
            let minDist = Infinity;
            for (let c of centroids) {
                let d = distSq(p, c);
                if (d < minDist) minDist = d;
            }
            if (minDist > maxDist) {
                maxDist = minDist;
                bestPixel = p;
            }
        }
        centroids.push(bestPixel);
    }

    // K-Means iterations to refine 16 colors
    for (let iter = 0; iter < 10; iter++) {
        let clusters = Array.from({length: k}, () => []);
        let sums = Array.from({length: k}, () => [0,0,0]);

        for (let p of pixels) {
            let minDist = Infinity;
            let minIdx = 0;
            for (let i = 0; i < k; i++) {
                let d = distSq(p, centroids[i]);
                if (d < minDist) { minDist = d; minIdx = i; }
            }
            clusters[minIdx].push(p);
            sums[minIdx][0] += p[0];
            sums[minIdx][1] += p[1];
            sums[minIdx][2] += p[2];
        }

        let moved = false;
        for (let i = 0; i < k; i++) {
            if (clusters[i].length > 0) {
                let newC = [
                    sums[i][0] / clusters[i].length,
                    sums[i][1] / clusters[i].length,
                    sums[i][2] / clusters[i].length
                ];
                if (distSq(newC, centroids[i]) > 1) moved = true;
                centroids[i] = newC;
            } else {
                centroids[i] = pixels[Math.floor(Math.random() * pixels.length)];
                moved = true;
            }
        }
        if (!moved) break; // Convergence
    }

    // Step 4: Apply 4x4 Bayer Dithering targeting our custom 16 colors
    // Standard 4x4 matrix normalized to -0.5 .. ~0.4375 range
    const bayer = [
        [ -0.5,     0,      -0.375,  0.125  ],
        [  0.25,   -0.25,    0.375, -0.125  ],
        [ -0.3125,  0.1875, -0.4375,  0.0625 ],
        [  0.4375, -0.0625,  0.3125, -0.1875 ]
    ];

    for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
            let idx = (y * w + x) * 4;
            if (data[idx+3] === 0) continue; // Skip transparency bounds

            // Perturb the original color with the Bayer matrix
            let offset = bayer[y % 4][x % 4] * strength;
            let r = data[idx] + offset;
            let g = data[idx+1] + offset;
            let b = data[idx+2] + offset;

            // Find closest palette color
            let minDist = Infinity;
            let closest = centroids[0];
            for (let c of centroids) {
                let d = distSq([r, g, b], c);
                if (d < minDist) { 
                    minDist = d; 
                    closest = c; 
                }
            }

            // Assign dithered 16-indexed color
            data[idx]   = closest[0];
            data[idx+1] = closest[1];
            data[idx+2] = closest[2];
            
            // PS1 typically used 1-bit alpha (cut-out) for textures
            data[idx+3] = data[idx+3] > 128 ? 255 : 0;
        }
    }

    // Apply modified pixels to low-res canvas
    ctx.putImageData(imgData, 0, 0);

    // Step 5: Upscale nearest-neighbor back to original image size for chunky aesthetics
    const finalCanvas = document.createElement('canvas');
    finalCanvas.width = originalImg.width;
    finalCanvas.height = originalImg.height;
    const fnCtx = finalCanvas.getContext('2d');
    
    fnCtx.imageSmoothingEnabled = false; // Nearest-neighbor scaling
    fnCtx.drawImage(lowResCanvas, 0, 0, finalCanvas.width, finalCanvas.height);

    return finalCanvas;
}

Free Image Tool Creator

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

Description

This tool transforms modern images into low-resolution textures reminiscent of the PlayStation 1 era, specifically mimicking the vibrant and dithered aesthetic seen in classic games like Spyro the Dragon. It achieves this by downscaling the image, boosting color saturation, reducing the color palette to 16 distinct colors using K-Means clustering, and applying Bayer dithering to create a retro pixelated look. This tool is ideal for game developers looking to create nostalgic assets, digital artists working on lo-fi aesthetics, or hobbyists wanting to give their photos a vintage 3D gaming makeover.

Leave a Reply

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