Please bookmark this page to avoid losing your image tool!

Fantasy Realm Map Image 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.
async function processImage(
    originalImg, // This parameter is ignored for this map creator tool
    mapWidth = 512,
    mapHeight = 512,
    seed = "random", // Can be any string, or "random" for a Math.random based seed
    noiseScale = 100, // Affects the "zoom" level of the terrain features
    octaves = 6, // Number of noise layers to combine for detail
    persistence = 0.5, // How much detail is added or removed at each octave
    lacunarity = 2.0, // How much the frequency changes for each octave
    waterLevel = 0.4, // Defines the cutoff for water (0.0 - 1.0)
    beachLevel = 0.45, // Defines the cutoff for beaches
    plainsLevel = 0.6, // Defines the cutoff for plains
    forestLevel = 0.75, // Defines the cutoff for forests
    snowPeakStartLevel = 0.9, // Defines where mountains start to get snow
    waterColor = "rgb(70,100,180)",
    beachColor = "rgb(238,218,180)",
    plainsColor = "rgb(130,180,90)",
    forestColor = "rgb(60,130,70)",
    mountainColor = "rgb(150,150,150)", // Rock color for mountains below snow
    snowColor = "rgb(240,245,250)",
    enableLightBevel = "false", // "true" to enable a simple 3D lighting effect
    lightBevelStrength = 20 // How strong the bevel lighting effect is (RGB offset)
) {

    // --- Parameter Parsing and Validation ---
    const p_mapWidth = parseInt(String(mapWidth), 10);
    const p_mapHeight = parseInt(String(mapHeight), 10);
    const p_seed = String(seed);
    const p_noiseScale = parseFloat(String(noiseScale));
    const p_octaves = parseInt(String(octaves), 10);
    const p_persistence = parseFloat(String(persistence));
    const p_lacunarity = parseFloat(String(lacunarity));
    const p_waterLevel = parseFloat(String(waterLevel));
    const p_beachLevel = parseFloat(String(beachLevel));
    const p_plainsLevel = parseFloat(String(plainsLevel));
    const p_forestLevel = parseFloat(String(forestLevel));
    const p_snowPeakStartLevel = parseFloat(String(snowPeakStartLevel));
    const p_applyBevel = String(enableLightBevel).toLowerCase() === 'true';
    const p_lightBevelStrength = parseInt(String(lightBevelStrength), 10);

    // --- Helper Functions ---
    function mulberry32(a) {
        return function() {
            a |= 0; a = a + 0x6D2B79F5 | 0;
            var t = Math.imul(a ^ a >>> 15, 1 | a);
            t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
            return ((t ^ t >>> 14) >>> 0) / 4294967296;
        }
    }

    function parseRgb(rgbStr) {
        if (typeof rgbStr !== 'string') { // Ensure it's a string before matching
             console.warn(`Invalid color input type: "${typeof rgbStr}". Using default black.`);
             return [0,0,0];
        }
        const match = rgbStr.match(/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/);
        if (match) {
            const r = parseInt(match[1],10), g = parseInt(match[2],10), b = parseInt(match[3],10);
            if (r > 255 || g > 255 || b > 255 || r < 0 || g < 0 || b < 0) {
                 console.warn(`Invalid RGB color value in "${rgbStr}". Clamping. Using default black.`);
                 return [0,0,0];
            }
            return [r,g,b];
        }
        console.warn(`Invalid RGB string format: "${rgbStr}". Expected "rgb(r,g,b)". Using default black.`);
        return [0,0,0];
    }
    
    function createErrorCanvas(message1, message2 = "") {
        const errCanvas = document.createElement('canvas');
        errCanvas.width = Math.max(350, message1.length * 7, message2.length * 7); // Auto-size width roughly
        errCanvas.height = message2 ? 60 : 40;
        const errCtx = errCanvas.getContext('2d');
        if (!errCtx) return document.createElement('div'); // Should not happen for 2D
        errCtx.fillStyle = 'rgb(200,70,70)';
        errCtx.fillRect(0,0,errCanvas.width, errCanvas.height);
        errCtx.fillStyle = 'white'; 
        errCtx.font = '12px Arial';
        errCtx.fillText(message1, 10, 20);
        if (message2) errCtx.fillText(message2, 10, 40);
        return errCanvas;
    }
    
    // Validate parsed numeric parameters
    const numericParams = { mapWidth:p_mapWidth, mapHeight:p_mapHeight, noiseScale:p_noiseScale, octaves:p_octaves, persistence:p_persistence, lacunarity:p_lacunarity, waterLevel:p_waterLevel, beachLevel:p_beachLevel, plainsLevel:p_plainsLevel, forestLevel:p_forestLevel, snowPeakStartLevel:p_snowPeakStartLevel, lightBevelStrength:p_lightBevelStrength };
    const originalValues = { mapWidth, mapHeight, seed, noiseScale, octaves, persistence, lacunarity, waterLevel, beachLevel, plainsLevel, forestLevel, snowPeakStartLevel, enableLightBevel, lightBevelStrength };

    for (const key in numericParams) {
        if (isNaN(numericParams[key])) {
            return createErrorCanvas(`Error: Invalid value for parameter '${key}'.`, `Received: "${originalValues[key]}". Not a valid number.`);
        }
    }
    if (p_mapWidth <= 0 || p_mapHeight <= 0) {
        return createErrorCanvas('Error: Map dimensions must be positive.', `Received: ${p_mapWidth}x${p_mapHeight}`);
    }
    if (p_noiseScale <= 0) return createErrorCanvas('Error: Noise scale must be positive.', `Received: ${p_noiseScale}`);
    if (p_octaves < 1) console.warn("Octaves < 1. Map diversity will be low.");


    // --- Dynamic Import of Simplex Noise Library ---
    if (typeof window.createNoise2D === 'undefined') {
        try {
            const SimplexNoiseModule = await import('https://cdn.jsdelivr.net/npm/simplex-noise@4.0.1/dist/esm/simplex-noise.js');
            window.createNoise2D = SimplexNoiseModule.createNoise2D;
        } catch (e) {
            console.error("Failed to load SimplexNoise library:", e);
            return createErrorCanvas('Error: Failed to load noise generation library.', 'Check internet connection or ad-blockers.');
        }
    }
    if (typeof window.createNoise2D === 'undefined') {
        return createErrorCanvas('Error: Noise library (createNoise2D) unavailable.');
    }
    
    // --- Canvas and PRNG Setup ---
    const canvas = document.createElement('canvas');
    canvas.width = p_mapWidth;
    canvas.height = p_mapHeight;
    const ctx = canvas.getContext('2d');
     if (!ctx) { // Should not happen for 2D context
        return createErrorCanvas('Error: Could not get 2D rendering context.');
    }


    let prng_func;
    if (p_seed === "random") {
        prng_func = Math.random;
    } else {
        let numSeed = 0;
        for (let i = 0; i < p_seed.length; i++) {
            numSeed = (numSeed << 5) - numSeed + p_seed.charCodeAt(i);
            numSeed |= 0; 
        }
        prng_func = mulberry32(numSeed);
    }
    const noise2D = window.createNoise2D(prng_func);

    // --- Heightmap Generation ---
    const heightMap = new Array(p_mapWidth);
    for (let i = 0; i < p_mapWidth; i++) {
        heightMap[i] = new Array(p_mapHeight);
    }

    let minActualNoise = Infinity;
    let maxActualNoise = -Infinity;

    for (let x = 0; x < p_mapWidth; x++) {
        for (let y = 0; y < p_mapHeight; y++) {
            let amplitude = 1;
            let frequency = 1;
            let noiseValue = 0;
            for (let o = 0; o < p_octaves; o++) {
                const sampleX = (x / p_noiseScale) * frequency;
                const sampleY = (y / p_noiseScale) * frequency;
                noiseValue += noise2D(sampleX, sampleY) * amplitude;
                amplitude *= p_persistence;
                frequency *= p_lacunarity;
            }
            heightMap[x][y] = noiseValue;
            minActualNoise = Math.min(minActualNoise, noiseValue);
            maxActualNoise = Math.max(maxActualNoise, noiseValue);
        }
    }

    const noiseRange = maxActualNoise - minActualNoise;
    const effectivelyFlat = noiseRange < 1e-9; 

    for (let x = 0; x < p_mapWidth; x++) {
        for (let y = 0; y < p_mapHeight; y++) {
            if (effectivelyFlat) {
                heightMap[x][y] = 0; 
            } else {
                heightMap[x][y] = (heightMap[x][y] - minActualNoise) / noiseRange;
            }
        }
    }
    
    // --- Rendering ---
    const imageData = ctx.createImageData(p_mapWidth, p_mapHeight);
    const data = imageData.data;

    const parsedColors = {
        water: parseRgb(waterColor),
        beach: parseRgb(beachColor),
        plains: parseRgb(plainsColor),
        forest: parseRgb(forestColor),
        mountain: parseRgb(mountainColor),
        snow: parseRgb(snowColor)
    };
    
    // Validate terrain level order
    if (!(p_waterLevel <= p_beachLevel && p_beachLevel <= p_plainsLevel && p_plainsLevel <= p_forestLevel && p_forestLevel <= p_snowPeakStartLevel && p_snowPeakStartLevel <= 1.0 && p_waterLevel >=0.0 )) {
       console.warn("Terrain level thresholds are not monotonically increasing or are out of 0-1 bounds. Map appearance may be unexpected.");
    }

    for (let y = 0; y < p_mapHeight; y++) {
        for (let x = 0; x < p_mapWidth; x++) {
            const elevation = heightMap[x][y];
            let baseRgb;

            if (elevation < p_waterLevel) baseRgb = parsedColors.water;
            else if (elevation < p_beachLevel) baseRgb = parsedColors.beach;
            else if (elevation < p_plainsLevel) baseRgb = parsedColors.plains;
            else if (elevation < p_forestLevel) baseRgb = parsedColors.forest;
            else if (elevation < p_snowPeakStartLevel) baseRgb = parsedColors.mountain;
            else baseRgb = parsedColors.snow;
            
            let r = baseRgb[0], g = baseRgb[1], b = baseRgb[2];

            if (p_applyBevel && x > 0 && y > 0) {
                const prevElevation = heightMap[x-1][y-1]; 
                const elevationDiffThreshold = 0.002; // Sensitivity for bevel
                
                if (elevation > prevElevation + elevationDiffThreshold) {
                    r = Math.min(255, baseRgb[0] + p_lightBevelStrength);
                    g = Math.min(255, baseRgb[1] + p_lightBevelStrength);
                    b = Math.min(255, baseRgb[2] + p_lightBevelStrength);
                } else if (elevation < prevElevation - elevationDiffThreshold) {
                    r = Math.max(0, baseRgb[0] - p_lightBevelStrength);
                    g = Math.max(0, baseRgb[1] - p_lightBevelStrength);
                    b = Math.max(0, baseRgb[2] - p_lightBevelStrength);
                }
            }
            
            const pixelIndex = (y * p_mapWidth + x) * 4;
            data[pixelIndex] = r;
            data[pixelIndex + 1] = g;
            data[pixelIndex + 2] = b;
            data[pixelIndex + 3] = 255; // Alpha
        }
    }
    ctx.putImageData(imageData, 0, 0);

    return canvas;
}

Free Image Tool Creator

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

Description

The Fantasy Realm Map Image Creator is a tool designed to generate detailed map images for fantasy worlds. Users can customize the map’s dimensions, terrain features, and visual styles using various parameters, including colors for different types of landscapes like water, forests, and mountains. The generated maps can be used for table-top role-playing games, video game development, or storytelling, providing a visual representation of fictional settings. This tool allows users to experiment with different settings to produce unique and visually appealing maps tailored to their creative needs.

Leave a Reply

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