You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes