You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
seedStr,
numOctavesStr,
persistenceStr,
lacunarityStr,
scaleStr,
waterLevelStr,
landLevelStr, // Renamed from forestLevel to represent general land above water
forestLevelStr, // Added to distinguish forest patches on land
mountainLevelStr,
snowLevelStr,
waterColorStr,
landColorStr,
forestColorStr,
mountainColorStr,
snowColorStr,
useImageAsMaskStr,
islandExponentStr
) {
// 0. Parameter parsing and validation
const defaultColors = {
water: [70, 107, 146], // Deep Blue-ish
land: [139, 179, 92], // Greenish
forest: [85, 139, 47], // Darker Green
mountain: [130, 119, 105],// Greyish Brown
snow: [245, 245, 245], // Off-white
};
function parseNumericParam(value, defaultValue, min = -Infinity, max = Infinity) {
let num = parseFloat(value);
if (isNaN(num)) num = defaultValue;
return Math.max(min, Math.min(max, num));
}
function parseIntegerParam(value, defaultValue, min = -Infinity, max = Infinity) {
let num = parseInt(value, 10);
if (isNaN(num)) num = defaultValue;
return Math.max(min, Math.min(max, num));
}
function parseColorToArray(colorInputStr, defaultArr) {
let str = String(colorInputStr === undefined ? "" : colorInputStr);
try {
const parts = str.split(',').map(s => {
const val = parseInt(s.trim(), 10);
if (isNaN(val) || val < 0 || val > 255) throw new Error("Invalid color component");
return val;
});
if (parts.length === 3) return parts;
return defaultArr; // Return default if not 3 parts
} catch (e) {
return defaultArr;
}
}
seedStr = String(seedStr === undefined ? String(Date.now()) : seedStr);
const numOctaves = parseIntegerParam(numOctavesStr, 5, 1, 10);
const persistence = parseNumericParam(persistenceStr, 0.5, 0.1, 1.0);
const lacunarity = parseNumericParam(lacunarityStr, 2.0, 1.1, 4.0);
const scale = parseNumericParam(scaleStr, 80, 10, 300); // Larger scale means larger features
const waterLevel = parseNumericParam(waterLevelStr, 0.35, 0.0, 1.0);
const landLevel = parseNumericParam(landLevelStr, 0.55, 0.0, 1.0); // General land
const forestLevel = parseNumericParam(forestLevelStr, 0.75, 0.0, 1.0); // Forests appear on land up to this elevation
const mountainLevel = parseNumericParam(mountainLevelStr, 0.8, 0.0, 1.0);
const snowLevel = parseNumericParam(snowLevelStr, 0.9, 0.0, 1.0);
const wc = parseColorToArray(waterColorStr, defaultColors.water);
const lc = parseColorToArray(landColorStr, defaultColors.land);
const fc = parseColorToArray(forestColorStr, defaultColors.forest);
const mc = parseColorToArray(mountainColorStr, defaultColors.mountain);
const sc = parseColorToArray(snowColorStr, defaultColors.snow);
useImageAsMaskStr = String(useImageAsMaskStr === undefined ? "false" : useImageAsMaskStr).toLowerCase();
let islandExponent = parseNumericParam(islandExponentStr, 1.5, 0, 5.0);
// 1. Canvas Setup
const MIN_DIM = 16;
const DEFAULT_DIM = 512;
let W = DEFAULT_DIM, H = DEFAULT_DIM;
if (originalImg && typeof originalImg.width === 'number' && originalImg.width > MIN_DIM &&
typeof originalImg.height === 'number' && originalImg.height > MIN_DIM) {
W = originalImg.width;
H = originalImg.height;
}
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) return document.createTextNode("Could not get 2D context.");
// 2. Seed & PRNG (mulberry32 - provides numbers in [0,1) )
let S = 0;
for (let i = 0; i < seedStr.length; i++) {
S = (S + seedStr.charCodeAt(i) * (i + 19)) & 0xFFFFFFFF; // Ensure S is a 32-bit int
}
if (S === 0) S = 1; // Avoid seed 0 if it creates issues with mulberry32 state
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;
}
}
const random = mulberry32(S);
// 3. Noise Precomputation (for value noise)
const VAL_GRID_SIDE_LEN = 256; // Size of the precomputed random grid
const randomValuesForNoise = new Array(VAL_GRID_SIDE_LEN * VAL_GRID_SIDE_LEN);
for (let i = 0; i < randomValuesForNoise.length; i++) {
randomValuesForNoise[i] = random();
}
// Helper: smoothstep
function smoothstep(t) { return t * t * (3 - 2 * t); }
// Helper: lerp
function lerp(a, b, t) { return a + t * (b - a); }
// Helper: Get random value for grid point (ix, iy)
function getValue(ix, iy, grid, sideLen) {
const x_ = (ix % sideLen + sideLen) % sideLen;
const y_ = (iy % sideLen + sideLen) % sideLen;
return grid[y_ * sideLen + x_];
}
// Helper: 2D Value Noise
function valueNoise2D(x, y, randGrid, gridSide, _getValue, _smoothstep, _lerp) {
const ix = Math.floor(x);
const iy = Math.floor(y);
const fx = x - ix;
const fy = y - iy;
const v00 = _getValue(ix, iy, randGrid, gridSide);
const v10 = _getValue(ix + 1, iy, randGrid, gridSide);
const v01 = _getValue(ix, iy + 1, randGrid, gridSide);
const v11 = _getValue(ix + 1, iy + 1, randGrid, gridSide);
const sx = _smoothstep(fx);
const sy = _smoothstep(fy);
const nx0 = _lerp(v00, v10, sx);
const nx1 = _lerp(v01, v11, sx);
return _lerp(nx0, nx1, sy);
}
// Helper: Fractal Brownian Motion (FBM)
function fbm(x, y, octaves, persist, lacun, noiseScale, randGrid, gridSide) {
let total = 0;
let frequency = 1;
let amplitude = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += valueNoise2D(x * frequency / noiseScale, y * frequency / noiseScale,
randGrid, gridSide, getValue, smoothstep, lerp) * amplitude;
maxValue += amplitude;
amplitude *= persist;
frequency *= lacun;
}
return maxValue === 0 ? 0 : total / maxValue; // Normalize
}
// 4. Image Mask Setup (if useImageAsMaskStr === "true")
let imgMaskData = null;
if (useImageAsMaskStr === "true" && originalImg && originalImg.width > 1 && originalImg.height > 1) {
const maskCanvas = document.createElement('canvas');
maskCanvas.width = W;
maskCanvas.height = H;
const maskCtx = maskCanvas.getContext('2d', { willReadFrequently: true });
if (maskCtx) {
try {
maskCtx.drawImage(originalImg, 0, 0, W, H); // Scale originalImg to fit W, H
imgMaskData = maskCtx.getImageData(0, 0, W, H);
} catch (e) {
console.warn("Could not use image as mask (tainted canvas or other error):", e);
imgMaskData = null; // Ensure it's nullified on error
}
}
}
// 5. Main generation loop
const imageData = ctx.createImageData(W, H);
const data = imageData.data;
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const noiseX = px;
const noiseY = py;
let elevation = fbm(noiseX, noiseY, numOctaves, persistence, lacunarity, scale, randomValuesForNoise, VAL_GRID_SIDE_LEN);
// Apply island effect
if (islandExponent > 0) {
const dx = (px / W - 0.5) * 2; // -1 to 1
const dy = (py / H - 0.5) * 2; // -1 to 1
// Using squared distance to avoid sqrt, then raise to exponent/2.
// Or simpler, use Math.sqrt for clarity, performance isn't bottlenecked here.
let distFromCenter = Math.sqrt(dx*dx + dy*dy) / Math.sqrt(2); // Normalized approx 0-1
distFromCenter = Math.min(1, distFromCenter); // Clamp to 1
let falloff = Math.pow(distFromCenter, islandExponent);
elevation *= (1.0 - falloff);
}
// Apply image mask
if (imgMaskData) {
const maskIdx = (py * W + px) * 4;
const alpha = imgMaskData.data[maskIdx + 3];
if (alpha < 128) { // If transparent in mask
elevation = Math.min(elevation, waterLevel * 0.95); // Force to be water
}
}
let r, g, b;
if (elevation < waterLevel) { [r,g,b] = wc; }
else if (elevation < landLevel) { [r,g,b] = lc; } // Basic land
else if (elevation < forestLevel) { // Forests typically on mid-level land
// Check if current elevation is also suitable for forests in general land definition
if (elevation >= landLevel) { [r,g,b] = fc; }
else { [r,g,b] = lc; } // if forest bands are oddly configured
}
else if (elevation < mountainLevel) { [r,g,b] = mc; }
else { [r,g,b] = sc; }
const idx = (py * W + px) * 4;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
data[idx + 3] = 255; // Alpha
}
}
// 6. Put ImageData to canvas
ctx.putImageData(imageData, 0, 0);
// 7. Return canvas
return canvas;
}
Apply Changes