libpov_2025/libpov.js

2633 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
// Min. and max. possible coordinates of biomes
const MIN_X = -31000
const MAX_X = 31000
const MIN_Y = -31000
const MAX_Y = 31000
const MIN_Z = -31000
const MAX_Z = 31000
// Draw a grid line every GRID_STEP units
const GRID_STEP = 10
const GRID_STEP_V6 = 0.1
// Size of the resizing corner
const RESIZE_CORNER = 14;
// Minimum canvas side length (px)
const MIN_CANVAS_SIZE = 100;
// Minimum required distance of canvas from the right page side
const CANVAS_PAGE_MARGIN_RIGHT = 20
// Minimum and maximum value for heat and humidity
const MIN_HEAT_HUMIDITY_VALUE = -1e6
const MAX_HEAT_HUMIDITY_VALUE = 1e6
// Grid widths. We use lower grid widths
// as the grid becomes more crammed.
// There are 4 levels from 0 to 3.
// Level 3 is the full width, level 1 is the
// smallest with and level 0 renders no grid lines
// when the grid would become too crammed.
const GRID_WIDTH_LEVEL_3 = 2; // full width
const GRID_WIDTH_LEVEL_2 = 1; // reduced width
const GRID_WIDTH_LEVEL_1 = 0.5; // more reduced width
// the grid thesholds are the "grid lines to pixel"
// ratio. The more grid lines therere are per pixel,
// the lower the grid level.
// e.g. grid level 2 triggers if ratio is above
// GRID_THRESHOLD_LEVEL_2. Grid level 3 is used
// if no grid thresholds are triggered.
const GRID_THRESHOLD_LEVEL_2 = 0.08;
const GRID_THRESHOLD_LEVEL_1 = 0.16;
const GRID_THRESHOLD_LEVEL_0 = 0.24; // this will disable grid rendering
// Distance from the point's center in which a point can
// be selected by clicking on it
const POINT_SELECT_DISTANCE = 25
// Distance a point has to be dragged after a single click
// before drag-n-drop starts.
// Normally, the user can drag-n-drop a point
// when clickong and dragging it *after* it has already been
// selected; there's no distance requirement.
// But it is also possible to select the point (by clicking
// on it) and dragging it with the same mouse click. But for
// this method, the mouse had to move the distance below
// from the point coordinates (in pixels) before drag-n-drop
// activates.
const ONE_CLICK_DRAG_DROP_DISTANCE = 30
// Name to display if empty
const FALLBACK_NAME = "(no name)"
// Name of the default / initial biome
const DEFAULT_BIOME_NAME = "default"
// Default heat and humidity values
const DEFAULT_HEAT = 50
const DEFAULT_HUMIDITY = 50
// Symbol for storing the biome ID in site objects
// for the Voronoi script
const biomeIDSymbol = Symbol("Biome ID");
// Colors (mostly self-explanatory)
const POINT_COLOR = "#913636";
const POINT_COLOR_SELECTED = "#e19696";
const EDGE_COLOR = "#0f2c2e";
const GRID_COLOR = "#00000040";
const AXIS_COLOR = "#000000";
// Color to be used when the diagram is cleared
const CLEAR_COLOR = "#ecddba";
// list of possible cell colors
// note: These MUST be in "#xxxxxx" format
// for the hexColorToRGBColor function to work.
const CELL_COLORS = [
"#64988e",
"#3d7085",
"#345644",
"#6b7f5c",
"#868750",
"#a7822c",
"#a06e38",
"#ad5f52",
"#692f11",
"#89542f",
"#796e63",
"#a17d5e",
"#5a3f20",
"#836299",
];
const CELL_COLOR_NEUTRAL = "#888888";
// Mapgen v6 "cell" colors
const CELL_COLOR_V6_NORMAL = "#6b7f5c";
const CELL_COLOR_V6_JUNGLE = "#345644";
const CELL_COLOR_V6_DESERT = "#a7822c";
const CELL_COLOR_V6_TAIGA = "#548583";
const CELL_COLOR_V6_TUNDRA = "#6371b4";
// Mapgen v6 biome thresholds
const MGV6_FREQ_HOT = 0.4
const MGV6_FREQ_SNOW = -0.4
const MGV6_FREQ_TAIGA = 0.5
const MGV6_FREQ_JUNGLE = 0.5
// For mgv6_freq_desert setting
const MGV6_FREQ_DESERT_DEFAULT = 0.45
/* Status variables for the diagram calculations */
// The current biome mode; which type of biome system
// the program will use.
// * "modern": The modern biome system, as used by mapgen v7 and many more
// * "v6": The biome system of mapgen v6
let biomeMode = "modern";
// Min. and max. mathematically possible values for heat and humidity
let limit_heat_min, limit_heat_max;
let limit_humidity_min, limit_humidity_max;
// Draw area. Slightly larger than the value area to avoid
// ugly edge rendering problems
const DRAW_OFFSET = 10
let draw_heat_min, draw_heat_max;
let draw_humidity_min, draw_humidity_max;
// The point that is considered the middle of heat/humidity;
// mathematically this value is the most probable.
let midpoint_heat;
let midpoint_humidity;
// XYZ at which the diagram is currently viewed at
let viewX = 0;
let viewY = 0;
let viewZ = 0;
let v6_flag_snowbiomes = true;
let v6_flag_jungles = true;
let v6_freq_desert = 0.45;
// Biome noise settings
const NOISE_OFFSET_DEFAULT = 50;
const NOISE_SCALE_DEFAULT = 50;
const NOISE_PERSISTENCE_DEFAULT = 0.5;
const NOISE_OCTAVES_DEFAULT = 3;
const NOISE_ABSVALUE_DEFAULT = false;
const NOISE_V6_HEAT_OFFSET_DEFAULT = 0;
const NOISE_V6_HEAT_SCALE_DEFAULT = 1;
const NOISE_V6_HEAT_PERSISTENCE_DEFAULT = 0.5;
const NOISE_V6_HEAT_OCTAVES_DEFAULT = 3;
const NOISE_V6_HEAT_ABSVALUE_DEFAULT = false;
const NOISE_V6_HUMIDITY_OFFSET_DEFAULT = 0.5;
const NOISE_V6_HUMIDITY_SCALE_DEFAULT = 0.5;
const NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT = 0.5;
const NOISE_V6_HUMIDITY_OCTAVES_DEFAULT = 3;
const NOISE_V6_HUMIDITY_ABSVALUE_DEFAULT = false;
// Current noise values
let noises = {
heat_modern: {
offset: NOISE_OFFSET_DEFAULT,
scale: NOISE_SCALE_DEFAULT,
octaves: NOISE_OCTAVES_DEFAULT,
persistence: NOISE_PERSISTENCE_DEFAULT,
absvalue: NOISE_ABSVALUE_DEFAULT,
},
humidity_modern: {
offset: NOISE_OFFSET_DEFAULT,
scale: NOISE_SCALE_DEFAULT,
octaves: NOISE_OCTAVES_DEFAULT,
persistence: NOISE_PERSISTENCE_DEFAULT,
absvalue: NOISE_ABSVALUE_DEFAULT,
},
heat_v6: {
offset: NOISE_V6_HEAT_OFFSET_DEFAULT,
scale: NOISE_V6_HEAT_SCALE_DEFAULT,
octaves: NOISE_V6_HEAT_OCTAVES_DEFAULT,
persistence: NOISE_V6_HEAT_PERSISTENCE_DEFAULT,
absvalue: NOISE_V6_HEAT_ABSVALUE_DEFAULT,
},
humidity_v6: {
offset: NOISE_V6_HUMIDITY_OFFSET_DEFAULT,
scale: NOISE_V6_HUMIDITY_SCALE_DEFAULT,
octaves: NOISE_V6_HUMIDITY_OCTAVES_DEFAULT,
persistence: NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT,
absvalue: NOISE_V6_HUMIDITY_ABSVALUE_DEFAULT,
},
};
noises.heat = noises.heat_modern;
noises.humidity = noises.humidity_modern;
function updateAreaVarsFor(noiseType) {
let noise = noises[noiseType];
let is_absolute = noise.absvalue === true
// Calculate min. and max. possible values
// Octaves
let [o_min, o_max] = [0, 0]
for (let o=1; o<=noise.octaves; o++) {
let exp = o-1
// Calculate the two possible extreme values
// with the octave value being either at 1 or -1.
let limit1 = (1 * noise.persistence ** exp)
let limit2
if (!is_absolute) {
limit2 = (-1 * noise.persistence ** exp)
} else {
// If absvalue is set, one of the
// limits is always 0 because we
// can't get lower.
limit2 = 0
}
// To add to the maximum, pick the higher value
if (limit1 > limit2) {
o_max = o_max + limit1
} else {
o_max = o_max + limit2
}
// To add to the minimum, pick the LOWER value
if (limit1 > limit2) {
o_min = o_min + limit2
} else {
o_min = o_min + limit1
}
}
// Add offset and scale to min/max value (final step)
let min_value = noise.offset + noise.scale * o_min
let max_value = noise.offset + noise.scale * o_max
// Bring the 2 values in the correct order
// (min_value might be bigger for negative scale)
if (min_value > max_value) {
[min_value, max_value] = [max_value, min_value]
}
// v6 humidity is forcefully clamped at 0 and 1
if (biomeMode === "v6" && noiseType == "humidity") {
min_value = Math.max(0.0, min_value);
max_value = Math.min(1.0, max_value);
}
// Update globals
let limit_min = min_value;
let limit_max = max_value;
let draw_min = limit_min - DRAW_OFFSET
let draw_max = limit_max + DRAW_OFFSET
let midpoint = min_value + (max_value - min_value) / 2
if (noiseType === "heat") {
limit_heat_min = limit_min;
limit_heat_max = limit_max;
draw_heat_min = draw_min;
draw_heat_max = draw_max;
midpoint_heat = midpoint;
} else if (noiseType === "humidity") {
limit_humidity_min = limit_min;
limit_humidity_max = limit_max;
draw_humidity_min = draw_min;
draw_humidity_max = draw_max;
midpoint_humidity = midpoint;
} else {
console.error("updateAreaVars() called with wrong noise_type!")
}
}
function updateAreaVars() {
updateAreaVarsFor("heat");
updateAreaVarsFor("humidity");
// Update element
rangeDisplay.innerHTML = "heat range: <span class='statHeat'>["+(+limit_heat_min)+", "+(+limit_heat_max)+"]</span>; " +
"humidity range: <span class='statHumidity'>["+(+limit_humidity_min)+", "+(+limit_humidity_max)+"]</span>";
}
updateAreaVars();
// If true, point names are shown in diagram
let showNames = true;
// If true, points are shown in diagram
let showPoints = true;
// If true, cells are colorized in diagram
let showCellColors = true;
// If true, show the grid in the diagram
let showGrid = true;
// If true, show the heat/humidity axes
let showAxes = false;
// Set to true if the draw canvas currently shows an error message
let drawError = false;
// Current cursor position on the canvas
let canvas_cursor_x = null;
let canvas_cursor_y = null;
// The last ID assigned to a biome (0 = none assigned yet).
let lastBiomeID = 0;
let biomePoints = [];
// Add a biome to the biome list, does not update widgets.
// Returns a reference to the biome that was actually added.
function addBiomeRaw(biomeDef) {
biomeDef.id = lastBiomeID;
biomeDef.colorIndex = lastBiomeID % CELL_COLORS.length;
biomePoints.push(biomeDef);
// The biome ID is just a simple ascending number
lastBiomeID++;
return biomeDef;
}
// Add a default biome at the midpoint
addBiomeRaw({name: DEFAULT_BIOME_NAME, heat: midpoint_heat, humidity: midpoint_humidity, min_x: MIN_X, max_x: MAX_X, min_y: MIN_Y, max_y: MAX_Y, min_z: MIN_Z, max_z: MAX_Z});
// Add a new random biome to the biome list with the given biome definition.
// Then select it and update widgets
function addBiome(biomeDef) {
let he = Math.round(limit_heat_min + Math.random() * (limit_heat_max - limit_heat_min));
let hu = Math.round(limit_humidity_min + Math.random() * (limit_humidity_max - limit_humidity_min));
let newBiome = addBiomeRaw(biomeDef);
let newElem = document.createElement("option");
newElem.id = "biome_list_element_" + newBiome.id;
let newElemText = document.createTextNode(newBiome.name);
newElem.append(newElemText);
biomeSelector.append(newElem);
newElem.selected = "selected";
updateWidgetStates();
draw(true);
}
// Get the X, Y or Z value of the viewed coordinate
function getViewCoord(axis) {
if (axis === "x") {
return viewX;
} else if (axis === "y") {
return viewY;
} else if (axis === "z") {
return viewZ;
}
}
// Set the X, Y or Z value of the viewed coordinate
function setViewCoord(axis, value) {
if (axis === "x") {
viewX = value;
} else if (axis === "y") {
viewY = value;
} else if (axis === "z") {
viewZ = value;
}
}
// Returns the biome point by its given ID
// or null if it couldn't be found
function getBiomeByID(id) {
for(let b=0; b<biomePoints.length; b++) {
let biome = biomePoints[b];
if (biome.id === id) {
return biome;
}
}
return null;
}
// Returns a new biome name for displaying
function generateBiomeName(id) {
return String(id);
}
// Converts a biome point to a point object to be passed to the Voronoi API
function biomePointToVoronoiPoint(point) {
// Apart from x and y, we also add the biome ID to the Voronoi point
// so we can re-identify to which biome it belongs to when the
// object is returned from the Voronoi script output.
let newPoint = { x: point.heat, y: point.humidity, [biomeIDSymbol]: point.id }
return newPoint;
}
// Converts a point object for the Voronoi API to a biome point
function voronoiPointToBiomePoint(point) {
let newPoint = { heat: point.x, humidity: point.y, id: point[biomeIDSymbol] }
return newPoint;
}
/***** Draw functions *****/
/* Note: All of these need a draw context. */
/* Render the "resize corner", a couple of diagonal lines in the corner
indicating the canvas can be resized */
function putResizeCorner(context) {
if (canvas_cursor_x !== null) {
context.beginPath();
context.moveTo(voronoiCanvas.width, voronoiCanvas.height - RESIZE_CORNER);
context.lineTo(voronoiCanvas.width - RESIZE_CORNER, voronoiCanvas.height);
context.lineTo(voronoiCanvas.width, voronoiCanvas.height);
context.fillStyle = "#80808080";
context.closePath();
context.fill();
context.beginPath();
context.lineWidth = 1;
for (let c = RESIZE_CORNER; c>0; c-=4) {
context.moveTo(voronoiCanvas.width, voronoiCanvas.height - c);
context.lineTo(voronoiCanvas.width - c, voronoiCanvas.height);
}
context.strokeStyle = "#00000080";
context.closePath();
context.stroke();
}
}
/* Put the name of the given point on the draw context */
function putPointName(context, point) {
let he = point.heat
let hu = point.humidity
if (he < limit_heat_min || he > limit_heat_max || hu < limit_humidity_min || hu > limit_humidity_max) {
return;
}
let [x, y] = biomeCoordsToCanvasPixelCoords(he, hu);
let w = voronoiCanvas.width;
let h = voronoiCanvas.height;
if (x > w/2) {
context.textAlign = "right";
x = x-5;
} else {
context.textAlign = "left";
x = x+5;
}
if (y < h/2) {
context.textBaseline = "top";
} else {
context.textBaseline = "alphabetic";
}
context.font = "120% sans-serif"
let displayName = point.name;
if (displayName === "") {
displayName = FALLBACK_NAME;
}
context.fillText(displayName, x, y);
}
/* Put the given point on the draw context */
function putPoint(context, point) {
const ARROW_SIZE_SIDE = 7;
const ARROW_SIZE_CORNER = 9;
let he = point.heat
let hu = point.humidity
let [x, y] = biomeCoordsToCanvasPixelCoords(he, hu);
let w = voronoiCanvas.width;
let h = voronoiCanvas.height;
let [limit_x_min, limit_y_min] = biomeCoordsToCanvasPixelCoords(limit_heat_min, limit_humidity_min);
let [limit_x_max, limit_y_max] = biomeCoordsToCanvasPixelCoords(limit_heat_max, limit_humidity_max);
// Point is out of bounds: Draw an arrow at the border
if (he < limit_heat_min || he > limit_heat_max || hu < limit_humidity_min || hu > limit_humidity_max) {
context.beginPath();
// bottom left corner
if (he < limit_heat_min && hu < limit_humidity_min) {
context.moveTo(limit_x_min, limit_y_min);
context.lineTo(limit_x_min + ARROW_SIZE_CORNER, limit_y_min);
context.lineTo(limit_x_min, limit_y_min - ARROW_SIZE_CORNER);
context.closePath();
context.fill();
// bottom right corner
} else if (he > limit_heat_max && hu < limit_humidity_min) {
context.moveTo(limit_x_max, limit_y_min);
context.lineTo(limit_x_max - ARROW_SIZE_CORNER, limit_y_min);
context.lineTo(limit_x_max, limit_y_min - ARROW_SIZE_CORNER);
context.closePath();
context.fill();
// top left corner
} else if (he < limit_heat_min && hu > limit_humidity_max) {
context.moveTo(limit_x_min, limit_y_max);
context.lineTo(limit_x_min + ARROW_SIZE_CORNER, limit_y_max);
context.lineTo(limit_x_min, limit_y_max + ARROW_SIZE_CORNER);
context.closePath();
context.fill();
// top right corner
} else if (he > limit_heat_max && hu > limit_humidity_max) {
context.moveTo(limit_x_max, limit_y_max);
context.lineTo(limit_x_max - ARROW_SIZE_CORNER, limit_y_max);
context.lineTo(limit_x_max, limit_y_max + ARROW_SIZE_CORNER);
context.closePath();
context.fill();
// left side
} else if (he < limit_heat_min) {
context.moveTo(limit_x_min, y);
context.lineTo(limit_x_min + ARROW_SIZE_SIDE, y + ARROW_SIZE_SIDE);
context.lineTo(limit_x_min + ARROW_SIZE_SIDE, y - ARROW_SIZE_SIDE);
context.closePath();
context.fill();
// right side
} else if (he > limit_heat_max) {
context.moveTo(limit_x_max, y);
context.lineTo(limit_x_max - ARROW_SIZE_SIDE, y + ARROW_SIZE_SIDE);
context.lineTo(limit_x_max - ARROW_SIZE_SIDE, y - ARROW_SIZE_SIDE);
context.closePath();
context.fill();
// bottom side
} else if (hu < limit_humidity_min) {
context.moveTo(x, limit_y_min);
context.lineTo(x - ARROW_SIZE_SIDE, limit_y_min - ARROW_SIZE_SIDE);
context.lineTo(x + ARROW_SIZE_SIDE, limit_y_min - ARROW_SIZE_SIDE);
context.closePath();
context.fill();
// top side
} else if (hu > limit_humidity_max) {
context.moveTo(x, limit_y_max);
context.lineTo(x - ARROW_SIZE_SIDE, limit_y_max + ARROW_SIZE_SIDE);
context.lineTo(x + ARROW_SIZE_SIDE, limit_y_max + ARROW_SIZE_SIDE);
context.closePath();
context.fill();
}
// Point is in bounds: Draw a dot
} else {
context.beginPath();
context.moveTo(0, 0);
context.arc(x, y, 5, 0, Math.PI * 2);
context.closePath();
context.fill();
}
};
/* Put the grid on the draw context */
function putGrid(context, gridStep) {
let [limit_x_min, limit_y_min] = biomeCoordsToCanvasPixelCoords(limit_heat_min, limit_humidity_min);
let [limit_x_max, limit_y_max] = biomeCoordsToCanvasPixelCoords(limit_heat_max, limit_humidity_max);
limit_x_min = Math.max(0, limit_x_min);
limit_x_max = Math.min(voronoiCanvas.width, limit_x_max);
limit_y_min = Math.max(0, limit_y_min);
limit_y_max = Math.min(voronoiCanvas.height, limit_y_max);
// Calculate the "grid lines pixel ratio" to reduce the
// width of grid lines or even disable rendering them.
// A high ratio means that a LOT of grid lines would render
// on the canvas.
// This code will effectively trigger if the value range
// is very high.
let xGridLinesPerPixel = (limit_heat_max-limit_heat_min) / voronoiCanvas.width / gridStep;
let yGridLinesPerPixel = (limit_humidity_max-limit_humidity_min) / voronoiCanvas.height/ gridStep;
let xWidth = GRID_WIDTH_LEVEL_3;
let yWidth = GRID_WIDTH_LEVEL_3;
if (xGridLinesPerPixel > GRID_THRESHOLD_LEVEL_0) {
xWidth = null;
} else if (xGridLinesPerPixel > GRID_THRESHOLD_LEVEL_1) {
xWidth = GRID_WIDTH_LEVEL_1;
} else if (xGridLinesPerPixel > GRID_THRESHOLD_LEVEL_2) {
xWidth = GRID_WIDTH_LEVEL_2;
}
if (yGridLinesPerPixel > GRID_THRESHOLD_LEVEL_0) {
yWidth = null;
} else if (yGridLinesPerPixel > GRID_THRESHOLD_LEVEL_1) {
yWidth = GRID_WIDTH_LEVEL_1;
} else if (yGridLinesPerPixel > GRID_THRESHOLD_LEVEL_2) {
yWidth = GRID_WIDTH_LEVEL_2;
}
context.strokeStyle = GRID_COLOR;
// Fallback variable to break the loop if it draws way too many lines
let steps;
if (xWidth !== null) {
context.lineWidth = xWidth;
context.beginPath();
let x = -xWidth*2;
let [heat, _] = canvasPixelCoordsToBiomeCoords(x, 0);
heat = heat - (heat % gridStep);
steps = 0;
while (x < voronoiCanvas.width + xWidth*2) {
[x, _] = biomeCoordsToCanvasPixelCoords(heat, 0);
context.moveTo(x, limit_y_min);
context.lineTo(x, limit_y_max);
heat += gridStep;
steps++;
if (steps > 10000) {
console.error("Over 10000 grid lines on the X axis!");
break;
}
}
context.closePath();
context.stroke();
}
if (yWidth !== null) {
context.lineWidth = yWidth;
context.beginPath();
let y = -yWidth*2;
let [_, humidity] = canvasPixelCoordsToBiomeCoords(0, y);
humidity = humidity - (humidity % gridStep);
steps = 0;
while (y < voronoiCanvas.height + yWidth*2) {
[_, y] = biomeCoordsToCanvasPixelCoords(0, humidity);
context.moveTo(limit_x_min, y);
context.lineTo(limit_x_max, y);
humidity -= gridStep;
steps++;
if (steps > 10000) {
console.error("Over 10000 grid lines on the Y axis!");
break;
}
}
context.closePath();
context.stroke();
}
}
/* Put the labelled heat/humidity axes on the draw context */
function putAxes(context) {
// Size of arrows (px)
const AXIS_ARROW_SIZE = 8;
// Offset that arrows have from the border (px)
const ARROW_OFFSET = 1;
// Minimum distance (px) that axis must have from the border
const AXIS_BORDER_OFFSET = 10;
// Maximum distance (px) from certain borders at which the axis
// labels and ticks will be put on the other sides
const AXIS_LABEL_FLIP_OFFSET = 40;
context.lineWidth = 2;
context.strokeStyle = AXIS_COLOR;
let [x0, y0] = biomeCoordsToCanvasPixelCoords(0, 0);
let tick_heat, tick_humidity;
if (biomeMode === "v6") {
tick_heat = 1.0;
tick_humidity = 0.5;
} else {
tick_heat = (limit_heat_max - limit_heat_min) * (100/175);
tick_humidity = (limit_humidity_max - limit_humidity_min) * (100/175);
}
let [tx, ty] = biomeCoordsToCanvasPixelCoords(tick_heat, tick_humidity);
let w = voronoiCanvas.width;
let h = voronoiCanvas.height;
// If axis would go out of bounds, force them to
// be rendered at the side instead.
let other_side_x = false;
let other_side_y = false;
if (x0 <= AXIS_BORDER_OFFSET) {
x0 = AXIS_BORDER_OFFSET;
} else if (x0 >= w - AXIS_BORDER_OFFSET) {
x0 = w - AXIS_BORDER_OFFSET;
}
if (y0 <= AXIS_BORDER_OFFSET) {
y0 = AXIS_BORDER_OFFSET;
} else if (y0 >= h - AXIS_BORDER_OFFSET) {
y0 = h - AXIS_BORDER_OFFSET;
}
// Flip axis labels if coming close to certain sides
if (x0 <= AXIS_LABEL_FLIP_OFFSET) {
other_side_y = true;
}
if (y0 >= h - AXIS_LABEL_FLIP_OFFSET) {
other_side_x = true;
}
// horizontal axis
context.beginPath();
context.moveTo(0, y0);
context.lineTo(w, y0);
// tick
if (other_side_x) {
context.moveTo(tx, y0 - AXIS_ARROW_SIZE);
context.lineTo(tx, y0);
} else {
context.moveTo(tx, y0);
context.lineTo(tx, y0 + AXIS_ARROW_SIZE);
}
// arrow
context.moveTo(w-ARROW_OFFSET, y0);
context.lineTo(w-ARROW_OFFSET - AXIS_ARROW_SIZE, y0 - AXIS_ARROW_SIZE);
context.moveTo(w-ARROW_OFFSET, y0);
context.lineTo(w-ARROW_OFFSET - AXIS_ARROW_SIZE, y0 + AXIS_ARROW_SIZE);
context.stroke();
context.closePath();
// vertical axis
context.beginPath();
context.moveTo(x0, 0);
context.lineTo(x0, h);
// tick
if (other_side_y) {
context.moveTo(x0 + AXIS_ARROW_SIZE, ty);
context.lineTo(x0, ty);
} else {
context.moveTo(x0, ty);
context.lineTo(x0 - AXIS_ARROW_SIZE, ty);
}
// arrow
context.moveTo(x0, ARROW_OFFSET);
context.lineTo(x0 - AXIS_ARROW_SIZE, ARROW_OFFSET+AXIS_ARROW_SIZE);
context.moveTo(x0, ARROW_OFFSET);
context.lineTo(x0 + AXIS_ARROW_SIZE, ARROW_OFFSET+AXIS_ARROW_SIZE);
context.stroke();
context.closePath();
// axis+tick labels
context.fillStyle = "black";
// heat label
context.font = "100% sans-serif";
let lx, ly, ttx, tty;
if (other_side_x) {
context.textBaseline = "bottom";
context.textAlign = "right";
lx = w - AXIS_ARROW_SIZE*2;
ly = y0 - 4;
tty = y0 - AXIS_ARROW_SIZE;
} else {
context.textBaseline = "top";
context.textAlign = "right";
lx = w - AXIS_ARROW_SIZE*2;
ly = y0 + 4;
tty = y0 + AXIS_ARROW_SIZE;
}
context.fillText("heat", lx, ly);
context.textAlign = "center";
let str_tick_heat;
if (biomeMode === "v6") {
str_tick_heat = tick_heat.toFixed(1);
} else {
str_tick_heat = tick_heat.toFixed(0);
}
context.fillText(str_tick_heat, tx, tty);
// humidity label
context.font = "100% sans-serif";
context.save();
context.rotate(-Math.PI/2);
if (other_side_y) {
context.textBaseline = "top";
context.textAlign = "right";
lx = -AXIS_ARROW_SIZE*2;
ly = x0 + 4;
} else {
context.textBaseline = "bottom";
context.textAlign = "right";
lx = -AXIS_ARROW_SIZE*2;
ly = x0 - 4;
}
context.fillText("humidity", lx, ly);
context.restore();
if (other_side_y) {
context.textAlign = "left";
ttx = x0 + AXIS_ARROW_SIZE-2;
} else {
context.textAlign = "right";
ttx = x0 - AXIS_ARROW_SIZE-2;
}
context.textBaseline = "middle";
let str_tick_humidity;
if (biomeMode === "v6") {
str_tick_humidity = tick_humidity.toFixed(1);
} else {
str_tick_humidity = tick_humidity.toFixed(0);
}
context.fillText(str_tick_humidity, ttx, ty);
}
// Cache diagram object for performance boost
let cachedVoronoiDiagram = null;
/* Given the list of biome points, returns a Voronoi diagram object
(which may have been cached). If recalculate is true, a recalculation is forced. */
function getVoronoiDiagram(points, recalculate) {
if ((cachedVoronoiDiagram === null) || recalculate) {
// Calculate bounding box, defaults to heat/humidity limits ...
let vbbox = {xl: limit_heat_min, xr: limit_heat_max, yb: limit_humidity_max, yt: limit_humidity_min};
// ... unless a point is out of bounds,
// then we increase the bounding box size
// Calculate by how much to extend the
// bounding box for the given value.
let getBufferZone = function(value) {
// This essentially calculates the "order of magnitude"
// in base-2.
return 2**Math.floor(Math.log2(Math.abs(value)));
// The reason why we do this is due to floating-point arithmetic.
// If wed add/subtract a constant offset (like 1) from the value,
// the offset might disappear if the value is very large,
// due to floating point rounding, thus effectively adding/subtracting 0.
// This scaling makes sure we'll apply an offset that is
// "in the ballpark" of the origin value
}
for (let p of points) {
if (p.heat < vbbox.xl) {
vbbox.xl = p.heat - getBufferZone(p.heat);
} else if (p.heat > vbbox.xr) {
vbbox.xr = p.heat + getBufferZone(p.heat);
}
if (p.humidity < vbbox.yt) {
vbbox.yt = p.humidity - getBufferZone(p.humidity);
} else if (p.humidity > vbbox.yb) {
vbbox.yb = p.humidity + getBufferZone(p.humidity);
}
}
let sites = []
for (let p of points) {
sites.push(biomePointToVoronoiPoint(p));
}
let voronoi = new Voronoi();
let diagram = null;
if (cachedVoronoiDiagram && recalculate) {
diagram = cachedVoronoiDiagram;
// This should improve performance
voronoi.recycle(diagram);
}
try {
diagram = voronoi.compute(sites, vbbox);
} catch(err) {
diagram = null;
if (err instanceof Error) {
console.error("Error when calling voronoi.compute from Javascript-Voronoi library!\n"+
"* exception name: "+err.name+"\n"+
"* exception message: "+err.message+"\n"+
"* stack:\n"+err.stack);
} else {
console.error("Error when calling voronoi.compute from Javascript-Voronoi library!");
console.error(err);
throw err;
}
} finally {
cachedVoronoiDiagram = diagram;
return diagram;
}
} else {
return cachedVoronoiDiagram;
}
}
/* Returns the context object required to draw on the canvas */
function getDrawContext() {
let canvas = document.getElementById("voronoiCanvas");
if (canvas.getContext) {
return canvas.getContext("2d");
} else {
return null;
}
}
// Clear draw area
function clear(context) {
if (!context) {
context = getDrawContext();
if (!context) {
return false;
}
}
context.fillStyle = CLEAR_COLOR;
context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, voronoiCanvas.width+DRAW_OFFSET, voronoiCanvas.height+DRAW_OFFSET);
return true;
}
/* Returns all biome points except those whose XYZ limits fall out of the
given x, y and z values */
function getRenderedPoints(x, y, z) {
let points = [];
for (let p=0; p<biomePoints.length; p++) {
let point = biomePoints[p];
if (x >= point.min_x && x <= point.max_x && y >= point.min_y && y <= point.max_y && z >= point.min_z && z <= point.max_z) {
points.push(point);
}
}
return points;
}
/* Given a biome ID, returns the matching HTML element from the
biome list widget or null if none */
function getBiomeIDFromHTMLElement(elem) {
let strID = elem.id;
if (strID && strID.startsWith("biome_list_element_")) {
let slice = strID.slice(19);
if (slice) {
return +slice;
}
}
return null;
}
/* Returns both the ID of selected biome and the associated HTML element from
the biome list widget, or null if nothing is selected.
Result is returned in the form [id, htmlElement] or [null, null] */
function getSelectedBiomeIDAndElement() {
if (biomeSelector.selectedIndex === -1) {
return [null, null];
}
let elem = biomeSelector.options[biomeSelector.selectedIndex];
let biomeID = getBiomeIDFromHTMLElement(elem);
if (biomeID !== null) {
return [biomeID, elem];
}
return [null, null];
}
// Show a message in the center of the given draw
// context.
function showDiagramMessage(context, text) {
context.textAlign = "center";
context.fillStyle = "black";
context.textBaseline = "middle";
if (voronoiCanvas.width < 300) {
context.font = "100% sans-serif";
} else if (voronoiCanvas.width < 450) {
context.font = "150% sans-serif";
} else {
context.font = "200% sans-serif";
}
context.fillText(text, voronoiCanvas.width/2, voronoiCanvas.height/2);
updateWorldPositionText();
}
// Check if a diagram can be drawn on the draw context and
// if not, draws a message and return false.
// If everything is fine, return true.
function checkDrawValid(context) {
if (!context) {
// We don't even have a valid draw context!
// Write an error message in the error message element
if (!voronoiCanvas.hidden) {
voronoiCanvas.hidden = true;
coordinateDisplay.hidden = true;
worldPositionDisplay.hidden = true;
rangeDisplay.hidden = true;
configDiv.hidden = true;
errorMessage.innerText = "ERROR: Could not get the canvas context which means this tool won't work for you. Maybe your browser does not support the HTML canvas element properly.";
console.error("Could not get the canvas context!");
}
return false;
}
// Fail and render a special message if the value range is tiny
if ((limit_heat_max - limit_heat_min < 0.01) || (limit_humidity_max - limit_humidity_min < 0.01)) {
showDiagramMessage(context, "Value range is too small.");
drawError = true;
putResizeCorner(context);
return false;
}
// Fail and render a special message if the value range is huge
if ((limit_heat_max - limit_heat_min > MAX_HEAT_HUMIDITY_VALUE) || (limit_humidity_max - limit_humidity_min > MAX_HEAT_HUMIDITY_VALUE)) {
showDiagramMessage(context, "Value range is too large.");
drawError = true;
putResizeCorner(context);
return false;
}
// Fail and render a special message if value limit is out of permissible bounds
if ((limit_heat_max > MAX_HEAT_HUMIDITY_VALUE) || (limit_humidity_max > MAX_HEAT_HUMIDITY_VALUE)) {
showDiagramMessage(context, "Maximum value is too large.");
drawError = true;
putResizeCorner(context);
return false;
}
if ((limit_heat_min < MIN_HEAT_HUMIDITY_VALUE) || (limit_humidity_min < MIN_HEAT_HUMIDITY_VALUE)) {
showDiagramMessage(context, "Minimum value is too small.");
drawError = true;
putResizeCorner(context);
return false;
}
drawError = false;
return true;
}
/* Draws the diagram on the voronoiCanvas.
Will (re-)calculate the Voronoi diagram if recalculate is true;
otherwise it may re-use a previous diagram for performance reasons. */
function drawModern(recalculate) {
let context = getDrawContext();
let w = voronoiCanvas.width;
let h = voronoiCanvas.height;
let x = getViewCoord("x");
let y = getViewCoord("y");
let z = getViewCoord("z");
// shorter function name (for "convert")
let conv = biomeCoordsToCanvasPixelCoords
clear(context);
if (!checkDrawValid(context)) {
return false;
}
let points = getRenderedPoints(x, y, z);
// Fail and render a special message if there are no biomes
if (points.length === 0) {
if (biomePoints.length === 0) {
showDiagramMessage(context, "No biomes.");
} else {
showDiagramMessage(context, "No biomes at these coordinates.");
}
drawError = true;
putResizeCorner(context);
return false;
}
updateWorldPositionText();
let voronoiError = function() {
showDiagramMessage(context, "Error in Javascript-Voronoi!");
drawError = true;
putResizeCorner(context);
}
let diagram = getVoronoiDiagram(points, recalculate);
if (!diagram) {
voronoiError();
drawError = true;
return false;
}
drawError = false;
let createHalfedgesPath = function(context, cell) {
context.beginPath();
for (let h=0; h<cell.halfedges.length; h++) {
let halfedge = cell.halfedges[h]
let start = halfedge.getStartpoint()
let end = halfedge.getEndpoint()
let [cx1, cy1] = conv(start.x, start.y);
let [cx2, cy2] = conv(end.x, end.y);
if (h === 0) {
context.moveTo(cx1, cy1);
} else {
context.lineTo(cx1, cy1);
}
context.lineTo(cx2, cy2);
}
context.closePath();
}
// Render cell colors
if (showCellColors) {
let colors = CELL_COLORS;
/* Before we paint the cells,
we paint the cell (half)edges in their
cell color. Painting the cells alone
leaves a tiny little space between the
cells unpainted, so there is an ugly boundary
between space. Painting the edges before
painting the cells should ensure these ugly
boundaries are gone */
for (let c=0; c<diagram.cells.length; c++) {
let cell = diagram.cells[c];
// We use the biomeID to select a color
let biomeID = cell.site[biomeIDSymbol];
// This works because the biome ID is a number
let biome = getBiomeByID(biomeID);
if (biome.colorIndex !== null) {
context.strokeStyle = CELL_COLORS[biome.colorIndex];
} else {
let ccol = biomeID % CELL_COLORS.length;
context.strokeStyle = CELL_COLORS[ccol];
}
/* The line can be quite thick because
most of it will be painted over by the
actual cell themselves anyway */
context.lineWidth = 6;
createHalfedgesPath(context, cell);
context.stroke();
}
// Paint the cells
for (let c=0; c<diagram.cells.length; c++) {
let cell = diagram.cells[c];
// We use the biomeID to select a color
let biomeID = cell.site[biomeIDSymbol];
// This works because the biome ID is a number
let biome = getBiomeByID(biomeID);
if (biome.colorIndex !== null) {
context.fillStyle = CELL_COLORS[biome.colorIndex];
} else {
let ccol = biomeID % CELL_COLORS.length;
context.fillStyle = CELL_COLORS[ccol];
}
createHalfedgesPath(context, cell);
context.fill();
}
// If there's only 1 cell, we have to manually colorize it because
// the Voronoi script doesn't return that area in this special case.
if (points.length === 1 && diagram.cells.length === 1) {
// 1 cell means the whole area is filled
let cell = diagram.cells[0];
let biomeID = cell.site[biomeIDSymbol];
let biome = getBiomeByID(biomeID);
if (biome.colorIndex !== null) {
context.fillStyle = CELL_COLORS[biome.colorIndex];
} else {
let ccol = biomeID % CELL_COLORS.length;
context.fillStyle = CELL_COLORS[ccol];
}
context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, w+DRAW_OFFSET, h+DRAW_OFFSET);
}
} else {
// Use a "neutral" background color for the whole area if cell colors are disabled
context.fillStyle = CELL_COLOR_NEUTRAL;
context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, w+DRAW_OFFSET, h+DRAW_OFFSET);
}
if (points.length > 0) {
if (showGrid) {
putGrid(context, GRID_STEP);
}
if (showAxes) {
putAxes(context);
}
}
// Render Voronoi cell edges
context.lineWidth = 2.5;
for (let e=0; e<diagram.edges.length; e++) {
let edge = diagram.edges[e];
if (edge.rSite === null) {
context.strokeStyle = "transparent";
} else {
context.strokeStyle = EDGE_COLOR;
}
let [eax, eay] = conv(edge.va.x, edge.va.y);
let [ebx, eby] = conv(edge.vb.x, edge.vb.y);
context.beginPath();
context.moveTo(eax, eay);
context.lineTo(ebx, eby);
context.closePath();
context.stroke();
}
let [selElemID, _] = getSelectedBiomeIDAndElement();
// Render biome points
if (showPoints) {
for (let point of points) {
let pointID = point.id;
// Highlight selected point
if (selElemID !== null && pointID === selElemID) {
context.fillStyle = POINT_COLOR_SELECTED;
} else {
context.fillStyle = POINT_COLOR;
}
putPoint(context, point);
}
}
if (showNames) {
// Render biome point names
for (let point of points) {
let pointID = point.id;
// Highlight selected point
if (selElemID !== null && pointID === selElemID) {
context.fillStyle = "#FF8888FF";
} else {
context.fillStyle = "#FFFFFFAA";
}
putPointName(context, point);
}
}
putResizeCorner(context);
return true;
}
function drawV6() {
let context = getDrawContext();
let w = voronoiCanvas.width;
let h = voronoiCanvas.height;
clear(context);
if (!checkDrawValid(context)) {
return false;
}
if (true) {
let cx, cy;
// Calculate biome intersection coordinates
let cx_snow, cx_hot, cx_snow2, cy_snow2, cx_hot2, cy_hot2;
let _; // unused variable
let freq_jungle, freq_hot;
if (v6_flag_snowbiomes) {
freq_jungle = MGV6_FREQ_JUNGLE;
freq_hot = MGV6_FREQ_HOT;
// Temperate <-> Taiga/Tundra
[cx_snow, _] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_SNOW, 0);
// Temperate <-> Desert/Jungle
[cx_hot, _] = biomeCoordsToCanvasPixelCoords(freq_hot, 0);
} else {
// Temperate <-> Desert/Jungle
freq_hot = v6_freq_desert;
[cx_hot, _] = biomeCoordsToCanvasPixelCoords(freq_hot, 0);
freq_jungle = 0.75;
}
// Taiga <-> Tundra <-> Temperate
[cx_snow2, cy_snow2] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_SNOW, MGV6_FREQ_TAIGA);
// Desert <-> Jungle <-> Temperate
[cx_hot2, cy_hot2] = biomeCoordsToCanvasPixelCoords(freq_hot, freq_jungle);
if (showCellColors) {
// Render biome areas
context.fillStyle = CELL_COLOR_V6_NORMAL;
if (v6_flag_snowbiomes) {
// Temperate
context.beginPath();
context.moveTo(cx_snow, 0);
context.lineTo(cx_snow, h);
context.lineTo(cx_hot, h);
context.lineTo(cx_hot, 0);
context.closePath();
context.fill();
// Tundra
context.fillStyle = CELL_COLOR_V6_TUNDRA;
context.beginPath();
context.moveTo(0, 0);
context.lineTo(cx_snow2, 0);
context.lineTo(cx_snow2, cy_snow2);
context.lineTo(0, cy_snow2);
context.closePath();
context.fill();
// Taiga
context.fillStyle = CELL_COLOR_V6_TAIGA;
context.beginPath();
context.moveTo(cx_snow2, cy_snow2);
context.lineTo(cx_snow2, h);
context.lineTo(0, h);
context.lineTo(0, cy_snow2);
context.closePath();
context.fill();
// Jungle
context.fillStyle = CELL_COLOR_V6_JUNGLE;
context.beginPath();
context.moveTo(w, 0);
context.lineTo(cx_hot2, 0);
context.lineTo(cx_hot2, cy_hot2);
context.lineTo(w, cy_hot2);
context.closePath();
context.fill();
// Desert
context.fillStyle = CELL_COLOR_V6_DESERT;
context.beginPath();
context.moveTo(cx_hot2, cy_hot2);
context.lineTo(cx_hot2, h);
context.lineTo(w, h);
context.lineTo(w, cy_hot2);
context.closePath();
context.fill();
} else {
// Temperate
context.beginPath();
context.moveTo(0, 0);
context.lineTo(0, h);
context.lineTo(cx_hot, h);
context.lineTo(cx_hot, 0);
context.closePath();
context.fill();
if (v6_flag_jungles) {
// Jungle
context.fillStyle = CELL_COLOR_V6_JUNGLE;
context.beginPath();
context.moveTo(0, 0);
context.lineTo(w, 0);
context.lineTo(w, cy_hot2);
context.lineTo(0, cy_hot2);
context.closePath();
context.fill();
// Desert
context.fillStyle = CELL_COLOR_V6_DESERT;
context.beginPath();
context.moveTo(cx_hot2, cy_hot2);
context.lineTo(cx_hot2, h);
context.lineTo(w, h);
context.lineTo(w, cy_hot2);
context.closePath();
context.fill();
} else {
// Desert
context.fillStyle = CELL_COLOR_V6_DESERT;
context.beginPath();
context.moveTo(cx_hot2, 0);
context.lineTo(cx_hot2, h);
context.lineTo(w, h);
context.lineTo(w, 0);
context.closePath();
context.fill();
}
}
} else {
// Use a "neutral" background color for the whole area if cell colors are disabled
context.fillStyle = CELL_COLOR_NEUTRAL;
context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, w+DRAW_OFFSET, h+DRAW_OFFSET);
}
// Render biome borders
context.lineWidth = 2.5;
context.strokeStyle = EDGE_COLOR;
context.beginPath();
if (v6_flag_snowbiomes) {
// Temperate <-> Taiga/Tundra
context.moveTo(cx_snow, 0);
context.lineTo(cx_snow, h);
// Taiga <-> Tundra
context.moveTo(cx_snow2, cy_snow2);
context.lineTo(0, cy_snow2);
}
// Temperate <-> Desert/Jungle
if (v6_flag_snowbiomes || (!v6_flag_jungles)) {
context.moveTo(cx_hot, 0);
context.lineTo(cx_hot, h);
} else {
context.moveTo(cx_hot, cy_hot2);
context.lineTo(cx_hot, h);
}
// Desert <-> Jungle
if (v6_flag_snowbiomes) {
context.moveTo(cx_hot2, cy_hot2);
context.lineTo(w, cy_hot2);
} else if (v6_flag_jungles) {
context.moveTo(0, cy_hot2);
context.lineTo(w, cy_hot2);
}
context.closePath();
context.stroke();
if (showGrid) {
putGrid(context, GRID_STEP_V6);
}
if (showAxes) {
putAxes(context);
}
// Biome names
if (showNames) {
context.font = "120% sans-serif";
context.fillStyle = "#FFFFFFBB";
context.textAlign = "center";
context.textBaseline = "middle";
if (v6_flag_snowbiomes) {
context.fillText("Taiga", cx_snow/2, cy_snow2/2);
context.fillText("Tundra", cx_snow/2, cy_snow2+((h-cy_snow2)/2));
context.fillText("Jungle", cx_hot + (w-cx_hot)/2, cy_hot2/2);
} else if (v6_flag_jungles) {
context.fillText("Jungle", cx_hot + (w-cx_hot)/2, cy_hot2/2);
}
if (v6_flag_snowbiomes) {
context.fillText("Desert", cx_hot + (w-cx_hot)/2, cy_hot2+((h-cy_hot2)/2));
} else {
if (v6_flag_jungles) {
context.fillText("Desert", cx_hot + (w-cx_hot)/2, cy_hot2+((h-cy_hot2)/2));
} else {
context.fillText("Desert", cx_hot + (w-cx_hot)/2, h/2);
}
}
if (v6_flag_snowbiomes) {
context.fillText("Normal", cx_snow + (cx_hot-cx_snow)/2, h-h/4);
} else {
if (v6_flag_jungles) {
context.fillText("Normal", cx_hot/2, cy_hot2 + (h-cy_hot2)/2);
} else {
context.fillText("Normal", cx_hot/2, h/2);
}
}
}
putResizeCorner(context);
}
}
function draw(recalculate) {
if (biomeMode === "v6") {
drawV6(recalculate);
} else {
drawModern(recalculate);
}
}
/* Clears the biome list widget and (re-adds) the list elements
for the biomes from scratch. */
function repopulateBiomeSelector() {
biomeSelector.innerHTML = "";
for (let b=0; b<biomePoints.length; b++) {
let num = b+1;
let newElem = document.createElement("option");
newElem.value = num;
newElem.id = "biome_list_element_" + biomePoints[b].id;
let displayName = biomePoints[b].name;
if (displayName === "") {
displayName = FALLBACK_NAME;
}
let newElemText = document.createTextNode(displayName);
newElem.append(newElemText);
biomeSelector.append(newElem);
}
}
/* Update the status (like disabled or value) of all widgets that
affect the diagram, based on the internal data */
function updateWidgetStates() {
let state;
if (biomePoints.length === 0 || biomeSelector.selectedIndex === -1) {
state = "disabled";
} else {
state = "";
}
inputHeat.disabled = state;
inputHumidity.disabled = state;
inputMinX.disabled = state;
inputMaxX.disabled = state;
inputMinY.disabled = state;
inputMaxY.disabled = state;
inputMinZ.disabled = state;
inputMaxZ.disabled = state;
inputBiomeName.disabled = state;
removeBiomeButton.disabled = state;
for (let c=0; c<CELL_COLORS.length; c++) {
let elem = document.getElementById("inputBiomeColor"+c);
if (elem) {
elem.disabled = state;
elem.style.borderColor = "";
elem.innerHTML = "&nbsp;";
}
}
if (biomeSelector.selectedIndex !== -1) {
let selected = biomeSelector.options[biomeSelector.selectedIndex];
let point = biomePoints[biomeSelector.selectedIndex];
inputHeat.value = point.heat;
inputHumidity.value = point.humidity;
inputMinX.value = point.min_x;
inputMaxX.value = point.max_x;
inputMinY.value = point.min_y;
inputMaxY.value = point.max_y;
inputMinZ.value = point.min_z;
inputMaxZ.value = point.max_z;
inputBiomeName.value = point.name;
let colorIndex = point.colorIndex;
for (let c=0; c<CELL_COLORS.length; c++) {
let elem = document.getElementById("inputBiomeColor"+c);
if (elem) {
if (c === colorIndex) {
// Add a symbol for selected color
elem.innerHTML = "&#9679;"; // Unicode: BLACK CIRCLE
} else {
// Blank out non-selected color
elem.innerHTML = "&nbsp;";
}
}
}
}
inputNoiseHeatOffset.value = noises.heat.offset;
inputNoiseHeatScale.value = noises.heat.scale;
inputNoiseHeatOctaves.value = noises.heat.octaves;
inputNoiseHeatPersistence.value = noises.heat.persistence;
inputNoiseHumidityOffset.value = noises.humidity.offset;
inputNoiseHumidityScale.value = noises.humidity.scale;
inputNoiseHumidityOctaves.value = noises.humidity.octaves;
inputNoiseHumidityPersistence.value = noises.humidity.persistence;
if (v6_flag_snowbiomes) {
inputV6FreqDesert.disabled = true;
inputCheckboxV6Jungles.disabled = true;
} else {
inputV6FreqDesert.disabled = false;
inputCheckboxV6Jungles.disabled = false;
}
inputV6FreqDesert.value = v6_freq_desert;
}
/* To be called when a biome value like heat was changed.
Will update internal data, biome list and the diagram.
* pointField: Name of the field it affects (same as in biomePoints)
* value: The value to set */
function onChangeBiomeValueWidget(pointField, value) {
if (biomeSelector.selectedIndex === -1) {
return;
}
let selected = biomeSelector.options[biomeSelector.selectedIndex];
if (selected === null) {
return;
}
let point = biomePoints[biomeSelector.selectedIndex];
point[pointField] = value;
let displayName = point.name;
if (displayName === "") {
displayName = FALLBACK_NAME;
}
selected.innerText = displayName;
draw(true);
}
// Select the given point.
// point is a point from biomePoints.
// returns [selected, alreadySelected]
// where seleted is true if the point has been selected
// and alreadySelected is true if the point was selected before
function selectPoint(point) {
for (let elem of biomeSelector.options) {
let strID = elem.id;
let elemID = null;
let biomeID = getBiomeIDFromHTMLElement(elem);
if ((biomeID !== null) && (point.id === biomeID)) {
if (elem.selected === true) {
return [true, true];
}
elem.selected = "selected";
draw(false);
updateWidgetStates();
return [true, false];
}
}
return [false, false];
}
// Returns the distance between the two 2D points (x1, y1) and (x2, y2)
function getDistance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1)**2 + (y2 - y1)**2);
}
// Converts (x, y) canvas pixel coordinates to biome coordinates;
// returns [heat, humidity]
function canvasPixelCoordsToBiomeCoords(x, y) {
let w = (voronoiCanvas.width/(limit_heat_max-limit_heat_min));
let h = (voronoiCanvas.height/(limit_humidity_max-limit_humidity_min));
let heat = (x + limit_heat_min * w) / w;
// This also flips the Y axis
let humidity = limit_humidity_min + (limit_humidity_max - ((y + limit_humidity_min * h) / h));
return [heat, humidity];
}
// Converts heat and humidity coordinates to canvas pixel coordinates;
// returns [x, y]
function biomeCoordsToCanvasPixelCoords(heat, humidity) {
let w = (voronoiCanvas.width/(limit_heat_max-limit_heat_min));
let h = (voronoiCanvas.height/(limit_humidity_max-limit_humidity_min));
let pixelX = heat * w - limit_heat_min * w;
// This also flips the Y axis
let pixelY = voronoiCanvas.height - (humidity * h - limit_humidity_min * h);
return [pixelX, pixelY];
}
// Given a (x, y) pixel positition on the canvas, returns
// the point that is nearest to it or null if none.
// maxDist is the maximum allowed point distance,
// if it is exceeded, null is returned.
function getNearestPointFromCanvasPos(x, y, maxDist) {
let nearestPoint = null;
let nearestDist = null;
let vx = getViewCoord("x");
let vy = getViewCoord("y");
let vz = getViewCoord("z");
let points = getRenderedPoints(vx, vy, vz);
for (let i=0; i<points.length; i++) {
let point = points[i];
let [pixelX, pixelY] = biomeCoordsToCanvasPixelCoords(point.heat, point.humidity);
let dist = getDistance(x, y, pixelX, pixelY);
if (nearestPoint === null) {
nearestPoint = point;
nearestDist = dist;
} else if (dist < nearestDist) {
nearestPoint = point;
nearestDist = dist;
}
}
if (nearestDist < maxDist) {
return nearestPoint;
} else {
return null;
}
}
// Whether the mouse is currently pressed
let mouseIsDown = false;
// Whether the canvas is being resized
let resizing = false;
// Start coordinates of canvas resize or null if not resizing
let resizing_start_pos_x = null;
let resizing_start_pos_y = null;
// Start size of canvas when resizing or null if not resizing
let resizing_start_size_x = null;
let resizing_start_size_y = null;
// Coordinates of where the drag-n-drop started
// or is about to start
let dragDropStartPos = null;
// ID of point being currently dragged by the user
// or null if none.
let dragDropPointID = null;
// Current drag-n-drop state:
// 0 = no drag-n-drop
// 1 = preparing drag-n-drop right after selection
// 2 = drag-n-drop active
let dragDropState = 0;
/* Move and update the biome point while the user
is dragging it */
function updatePointWhenDragged(pointID) {
if (pointID !== null && !drawError && biomeMode === "modern") {
let selectedPoint = null;
let x = getViewCoord("x");
let y = getViewCoord("y");
let z = getViewCoord("z");
let points = getRenderedPoints(x, y, z);
for (let i=0; i<points.length; i++) {
if (points[i].id === dragDropPointID) {
selectedPoint = points[i];
let [newHeat, newHumidity] = canvasPixelCoordsToBiomeCoords(event.offsetX, event.offsetY);
selectedPoint.heat = Math.round(newHeat);
selectedPoint.humidity = Math.round(newHumidity);
draw(true);
updateWidgetStates();
let [elemID, elem] = getSelectedBiomeIDAndElement();
if (elemID !== null && points[i].id === elemID) {
let displayName = selectedPoint.name;
if (displayName === "") {
displayName = FALLBACK_NAME;
}
elem.innerText = displayName;
break;
}
return;
}
}
}
}
/* Updates the text showing the current coordinates
the diagram currently applies */
function updateWorldPositionText() {
let x = getViewCoord("x");
let y = getViewCoord("y");
let z = getViewCoord("z");
worldPositionDisplay.innerHTML = "showing biomes at world coordinates XYZ=<span class='statPosition'>("+x+","+y+","+z+")</span>";
}
/* Update the text that shows the biome coordinates
of the cursor when it's on the diagram */
function updateCoordinateDisplay(pixelX, pixelY) {
if (pixelX === null || pixelY === null) {
coordinateDisplay.innerHtml = "&nbsp;";
return;
}
// show coordinates
let [heat, humidity] = canvasPixelCoordsToBiomeCoords(pixelX, pixelY);
let heat_range = limit_heat_max - limit_heat_min;
let humidity_range = limit_humidity_max - limit_humidity_min;
let heat_precision = null;
if (heat_range >= 100) {
heat_precision = 0;
} else if (heat_range >= 10) {
heat_precision = 1;
} else if (heat_range >= 1) {
heat_precision = 2;
}
let humidity_precision = null;
if (humidity_range >= 100) {
humidity_precision = 0;
} else if (humidity_range >= 10) {
humidity_precision = 1;
} else if (humidity_range >= 1) {
humidity_precision = 2;
}
let heatStr, humidityStr;
if (heat_precision !== null) {
heatStr = heat.toFixed(heat_precision);
} else {
heatStr = +heat;
}
if (heat_precision !== null) {
humidityStr = humidity.toFixed(humidity_precision);
} else {
humidityStr = +humidity;
}
if (!drawError) {
let html = "cursor coordinates: heat=<span class='statHeat'>"+heatStr+"</span>; humidity=<span class='statHumidity'>"+humidityStr+"</span>";
coordinateDisplay.innerHTML = html;
} else {
coordinateDisplay.innerHTML = "&nbsp;";
}
}
/* Updates and changes the cursor type on the diagram
canvas depending on whether we can select, drag or do nothing
at the pointed position */
function updateCanvasCursorStatus(x, y) {
// Show resize cursor at the bottom right corner
if (resizing || (x > voronoiCanvas.width - RESIZE_CORNER && y > voronoiCanvas.height - RESIZE_CORNER)) {
voronoiCanvas.style.cursor = "nwse-resize";
return
}
if (drawError || !showPoints || biomeMode === "v6") {
// a special message is shown; use auto cursor
voronoiCanvas.style.cursor = "auto";
return
}
let nearest = getNearestPointFromCanvasPos(x, y, POINT_SELECT_DISTANCE);
if (nearest !== null) {
let [id, elem] = getSelectedBiomeIDAndElement();
if (id !== null && nearest.id === id) {
// This cursor indicates we can grab the point
voronoiCanvas.style.cursor = "grab";
} else {
// This cursor indicates we can select the point
voronoiCanvas.style.cursor = "crosshair";
}
} else {
// Default cursor when a click doesn't to anything
voronoiCanvas.style.cursor = "auto";
}
}
/* Initializes checkbox variables of the view settings */
function checkboxVarsInit() {
showNames = inputCheckboxNames.checked;
showPoints = inputCheckboxPoints.checked;
showCellColors = inputCheckboxCellColors.checked;
showGrid = inputCheckboxGrid.checked;
showAxes = inputCheckboxAxes.checked;
v6_flag_snowbiomes = inputCheckboxV6Snowbiomes.checked;
v6_flag_jungles = inputCheckboxV6Jungles.checked;
}
/* Collapses/Expands a config section */
function toggleConfigSectionDisplay(headerLink, container) {
if (container.style.display !== "none") {
headerLink.innerText = "▶";
container.style.display = "none";
} else {
headerLink.innerText = "▼";
container.style.display = "block";
}
}
/* Unhide the main content. Used to disable the noscript
handling because the website starts with the main content
container hidden so the noscript version of the page
isn't cluttered. */
function unhideContent() {
mainContentContainer.hidden = false;
/* Also hide the container holding the noscript error
message to avoid spacing issues */
noscriptContainer.hidden = true;
}
function initBiomeColorSelectors() {
for (let c=0; c<CELL_COLORS.length; c++) {
let button = document.createElement("button");
button.type = "button";
button.style.backgroundColor = CELL_COLORS[c];
button.style.width = "2em";
button.style.height = "2em";
button.innerHTML = "&nbsp;";
button.id = "inputBiomeColor"+c;
button.className = "biomeColorButton";
biomeColorSection.append(button);
button.onclick = function() {
onChangeBiomeValueWidget("colorIndex", c);
updateWidgetStates();
}
}
}
function fitCanvasInBody() {
// Get x,y position of canvas
let bodyRect = document.body.getBoundingClientRect();
let canvasRect = voronoiCanvas.getBoundingClientRect();
let cx = canvasRect.left - bodyRect.left;
let cy = canvasRect.top - bodyRect.top;
// Calculate new size
let rx = window.innerWidth - cx - CANVAS_PAGE_MARGIN_RIGHT;
let oldWidth = voronoiCanvas.width;
let newWidth = Math.max(MIN_CANVAS_SIZE, rx);
if (newWidth < oldWidth) {
// Resize
if (voronoiCanvas.height === voronoiCanvas.width) {
voronoiCanvas.height = newWidth;
}
voronoiCanvas.width = newWidth;
draw(false);
}
}
/***** EVENTS *****/
/* Body events */
window.onresize = function(event) {
fitCanvasInBody();
}
document.body.onmousemove = function(event) {
if (resizing) {
// Get x,y position of canvas
let bodyRect = document.body.getBoundingClientRect();
let canvasRect = voronoiCanvas.getBoundingClientRect();
let cx = canvasRect.left - bodyRect.left;
let cy = canvasRect.top - bodyRect.top;
// Calculate new size
let rx = event.pageX - resizing_start_pos_x - cx;
let ry = event.pageY - resizing_start_pos_y - cy;
// Limit the width
let maxX = (bodyRect.width - cx) - CANVAS_PAGE_MARGIN_RIGHT;
// Resize
voronoiCanvas.width = Math.min(maxX, Math.max(MIN_CANVAS_SIZE, resizing_start_size_x + rx));
// Holding down Shift preserves aspect ratio
if (event.shiftKey) {
voronoiCanvas.height = voronoiCanvas.width;
} else {
voronoiCanvas.height = Math.max(MIN_CANVAS_SIZE, resizing_start_size_y + ry);
}
draw(false);
return;
}
}
document.body.onmouseup = function(event) {
if (resizing) {
resizing = false;
updateCanvasCursorStatus(event.offsetX, event.offsetY);
}
}
document.body.onmouseleave = function(event) {
if (resizing) {
resizing = false;
updateCanvasCursorStatus(event.offsetX, event.offsetY);
}
}
/* Canvas events */
voronoiCanvas.onmousemove = function(event) {
if (resizing) {
updateCoordinateDisplay(event.offsetX, event.offsetY);
updateCanvasCursorStatus(event.offsetX, event.offsetY);
canvas_cursor_x = event.offsetX;
canvas_cursor_y = event.offsetY;
draw(false);
return
}
// update drag-n-drop state
if (dragDropState !== 2 && dragDropPointID !== null && mouseIsDown && dragDropStartPos !== null) {
let dist = getDistance(dragDropStartPos.x, dragDropStartPos.y, event.offsetX, event.offsetY)
if (dragDropState === 1 && dist >= 1) {
dragDropState = 2;
} else if ((dragDropState === 0 || dragDropState === 1) && dist > ONE_CLICK_DRAG_DROP_DISTANCE) {
dragDropState = 2;
}
}
// drag-n-drop
if (dragDropState === 2) {
updatePointWhenDragged(dragDropPointID);
}
updateCoordinateDisplay(event.offsetX, event.offsetY);
updateCanvasCursorStatus(event.offsetX, event.offsetY);
canvas_cursor_x = event.offsetX;
canvas_cursor_y = event.offsetY;
draw(false);
}
voronoiCanvas.onmouseenter = function(event) {
updateCoordinateDisplay(event.offsetX, event.offsetY);
updateCanvasCursorStatus(event.offsetX, event.offsetY);
canvas_cursor_x = event.offsetX;
canvas_cursor_y = event.offsetY;
draw(false);
}
voronoiCanvas.onmousedown = function(event) {
// select point by clicking.
// initiate drag-n-drop if already selected.
mouseIsDown = true;
// Resizing the canvas
if (event.offsetX > voronoiCanvas.width - RESIZE_CORNER && event.offsetY > voronoiCanvas.height - RESIZE_CORNER) {
resizing_start_pos_x = event.offsetX;
resizing_start_pos_y = event.offsetY;
resizing_start_size_x = +this.width;
resizing_start_size_y = +this.height;
resizing = true;
return;
}
if (drawError || !showPoints || biomeMode === "v6") {
// Points need to be shown for drag-n-drop to work
return;
}
let nearest = getNearestPointFromCanvasPos(event.offsetX, event.offsetY, POINT_SELECT_DISTANCE);
if (nearest !== null) {
let success, alreadySelected
[success, alreadySelected] = selectPoint(nearest);
dragDropPointID = nearest.id;
if (success) {
let [x, y] = biomeCoordsToCanvasPixelCoords(nearest.heat, nearest.humidity);
dragDropStartPos = { x: x, y: y };
}
if (alreadySelected) {
dragDropState = 1;
}
updateCanvasCursorStatus(event.offsetX, event.offsetY);
}
}
voronoiCanvas.ondblclick = function(event) {
// Add a biome at double-click position, if possible
if (drawError || dragDropState !== 0 || biomeMode === "v6") {
return;
}
// No-op if in selection range
let nearest = getNearestPointFromCanvasPos(event.offsetX, event.offsetY, POINT_SELECT_DISTANCE);
if (nearest !== null) {
return;
}
let [he, hu] = canvasPixelCoordsToBiomeCoords(event.offsetX, event.offsetY);
he = Math.round(he);
hu = Math.round(hu);
addBiome({name: generateBiomeName(lastBiomeID), heat:he, humidity: hu, min_x:MIN_X, max_x:MAX_Y, min_y:MIN_Y, max_y:MAX_Y, min_z:MIN_Z, max_z:MAX_Z});
}
voronoiCanvas.onmouseup = function(event) {
// end drag-n-drop
if (dragDropState === 2) {
updatePointWhenDragged(dragDropPointID);
}
mouseIsDown = false;
dragDropStartPos = null;
dragDropPointID = null;
dragDropState = 0;
}
voronoiCanvas.onmouseleave = function() {
// end drag-n-drop
mouseIsDown = false;
dragDropStartPos = null;
dragDropPointID = null;
dragDropState = 0;
canvas_cursor_x = null;
canvas_cursor_y = null;
coordinateDisplay.innerHTML = "&nbsp;";
draw(false);
}
/* Biome list events */
biomeSelector.onchange = function() {
draw(false);
if (biomeSelector.selectedIndex !== -1) {
let selected = biomeSelector.options[biomeSelector.selectedIndex];
let point = biomePoints[biomeSelector.selectedIndex];
inputHeat.value = point.heat;
inputHumidity.value = point.humidity;
inputMinX.value = point.min_x;
inputMaxX.value = point.max_x;
inputMinY.value = point.min_y;
inputMaxY.value = point.max_y;
inputMinZ.value = point.min_z;
inputMaxZ.value = point.max_z;
}
updateWidgetStates();
}
addBiomeButton.onclick = function() {
// Add a biome at a random position
let he_min = Math.max(MIN_HEAT_HUMIDITY_VALUE, limit_heat_min);
let he_max = Math.min(MAX_HEAT_HUMIDITY_VALUE, limit_heat_max);
let hu_min = Math.max(MIN_HEAT_HUMIDITY_VALUE, limit_humidity_min);
let hu_max = Math.min(MAX_HEAT_HUMIDITY_VALUE, limit_humidity_max);
let he = Math.round(he_min + Math.random() * (he_max - he_min));
let hu = Math.round(hu_min + Math.random() * (hu_max - hu_min));
addBiome({name: generateBiomeName(lastBiomeID), heat: he, humidity: hu, min_x:MIN_X, max_x:MAX_Y, min_y:MIN_Y, max_y:MAX_Y, min_z:MIN_Z, max_z:MAX_Z});
}
removeBiomeButton.onclick = function() {
if (biomeSelector.selectedOptions.length === 0) {
return;
}
let firstIndex = null;
for (let o=0; o<biomeSelector.selectedOptions.length; o++) {
let opt = biomeSelector.selectedOptions[o]
let index = opt.index
if (firstIndex === null) {
firstIndex = index;
}
biomePoints.splice(index, 1);
opt.remove();
}
if (firstIndex !== null && biomePoints.length > 0) {
let newIndex = firstIndex-1;
if (newIndex < 0) {
newIndex = 0;
}
biomeSelector.options[newIndex].selected = "selected";
}
draw(true);
updateWidgetStates();
}
/* Biome editing widgets events */
function handleBiomeNumberInput(biomeValueName, element) {
let val = +element.value;
if (element.value === "" || typeof val !== "number") {
return;
}
if (element.min !== "" && (val < +element.min || val > +element.max)) {
return;
}
onChangeBiomeValueWidget(biomeValueName, val);
}
function handleBiomeNumberBlur(biomeValueName, element, defaultValue) {
let val = +element.value;
if (element.value === "" || typeof val !== "number") {
val = defaultValue;
element.value = defaultValue;
}
onChangeBiomeValueWidget(biomeValueName, val);
}
inputHeat.oninput = function() {
handleBiomeNumberInput("heat", this);
}
inputHumidity.oninput = function() {
handleBiomeNumberInput("humidity", this);
}
inputMinX.oninput = function() {
handleBiomeNumberInput("min_x", this);
}
inputMaxX.oninput = function() {
handleBiomeNumberInput("max_x", this);
}
inputMinY.oninput = function() {
handleBiomeNumberInput("min_y", this);
}
inputMaxY.oninput = function() {
handleBiomeNumberInput("max_y", this);
}
inputMinZ.oninput = function() {
handleBiomeNumberInput("min_z", this);
}
inputMaxZ.oninput = function() {
handleBiomeNumberInput("max_z", this);
}
inputBiomeName.oninput = function() {
onChangeBiomeValueWidget("name", this.value);
}
inputHeat.onblur = function() {
handleBiomeNumberBlur("heat", this, 0);
}
inputHumidity.onblur = function() {
handleBiomeNumberBlur("humidity", this, 0);
}
inputMinX.onblur = function() {
handleBiomeNumberBlur("min_x", this, MIN_X);
}
inputMaxX.onblur = function() {
handleBiomeNumberBlur("max_x", this, MAX_X);
}
inputMinY.onblur = function() {
handleBiomeNumberBlur("min_y", this, MIN_Y);
}
inputMaxY.onblur = function() {
handleBiomeNumberBlur("max_y", this, MAX_Y);
}
inputMinZ.onblur = function() {
handleBiomeNumberBlur("min_z", this, MIN_Z);
}
inputMaxZ.onblur = function() {
handleBiomeNumberBlur("max_z", this, MAX_Z);
}
/* Diagram view settings events */
let h_inputViewXYZ_oninput = function(axis, value) {
if (value === null) {
return;
}
setViewCoord(axis, Math.floor(value));
draw(true);
updateWorldPositionText();
}
let h_inputViewXYZ_onblur = function(axis, value, ref) {
if (value === null || value === "") {
ref.value = 0;
setViewCoord("axis", 0);
draw(true);
updateWorldPositionText();
}
}
inputViewX.oninput = function() {
h_inputViewXYZ_oninput("x", this.value);
}
inputViewX.onblur = function() {
h_inputViewXYZ_onblur("x", this.value, this);
}
inputViewY.oninput = function() {
h_inputViewXYZ_oninput("y", this.value);
}
inputViewY.onblur = function() {
h_inputViewXYZ_onblur("y", this.value, this);
}
inputViewZ.oninput = function() {
h_inputViewXYZ_oninput("z", this.value);
}
inputViewZ.onblur = function() {
h_inputViewXYZ_onblur("z", this.value, this);
}
inputCheckboxNames.onchange = function() {
showNames = this.checked;
draw(false);
}
inputCheckboxPoints.onchange = function() {
showPoints = this.checked;
draw(false);
}
inputCheckboxCellColors.onchange = function() {
showCellColors = this.checked;
draw(false);
}
inputCheckboxGrid.onchange = function() {
showGrid = this.checked;
draw(false);
}
inputCheckboxAxes.onchange = function() {
showAxes = this.checked;
draw(false);
}
inputCheckboxV6Snowbiomes.onchange = function() {
v6_flag_snowbiomes = this.checked;
updateWidgetStates();
draw(false);
}
inputCheckboxV6Jungles.onchange = function() {
v6_flag_jungles = this.checked;
draw(false);
}
inputV6FreqDesert.oninput = function() {
let f = +this.value;
if (f === null) {
return;
}
v6_freq_desert = f;
draw(true);
}
inputV6FreqDesert.onblur = function() {
let f = +this.value;
if (f === null || this.value === "") {
v6_freq_desert = MGV6_FREQ_DESERT_DEFAULT;
this.value = v6_freq_desert;
draw(true);
}
}
/* Noise parameters events */
function updateNoiseParam(noiseName, noiseValueName, element) {
if (element.value === "") {
return;
}
let val = +element.value;
noises[noiseName][noiseValueName] = val;
clear();
updateAreaVars();
draw(true);
}
function blurNoiseParam(noiseName, noiseValueName, element, defaultValue) {
let val = +element.value;
if (element.value === "" || val === null) {
val = defaultValue;
element.value = val;
noises[noiseName][noiseValueName] = val;
clear();
updateAreaVars();
draw(true);
}
}
let noiseWidgetTable = [
{ elem: inputNoiseHeatScale, noise: "heat", noise_param: "scale", default_modern: NOISE_SCALE_DEFAULT, default_v6: NOISE_V6_HEAT_SCALE_DEFAULT },
{ elem: inputNoiseHeatOffset, noise: "heat", noise_param: "offset", default_modern: NOISE_OFFSET_DEFAULT, default_v6: NOISE_V6_HEAT_OFFSET_DEFAULT },
{ elem: inputNoiseHeatOctaves, noise: "heat", noise_param: "octaves", default_modern: NOISE_OCTAVES_DEFAULT, default_v6: NOISE_V6_HEAT_OCTAVES_DEFAULT },
{ elem: inputNoiseHeatPersistence, noise: "heat", noise_param: "persistence", default_modern: NOISE_PERSISTENCE_DEFAULT, default_v6: NOISE_V6_HEAT_PERSISTENCE_DEFAULT },
{ elem: inputNoiseHumidityScale, noise: "humidity", noise_param: "scale", default_modern: NOISE_SCALE_DEFAULT, default_v6: NOISE_V6_HUMIDITY_SCALE_DEFAULT },
{ elem: inputNoiseHumidityOffset, noise: "humidity", noise_param: "offset", default_modern: NOISE_OFFSET_DEFAULT, default_v6: NOISE_V6_HUMIDITY_OFFSET_DEFAULT },
{ elem: inputNoiseHumidityOctaves, noise: "humidity", noise_param: "octaves", default_modern: NOISE_OCTAVES_DEFAULT, default_v6: NOISE_V6_HUMIDITY_OCTAVES_DEFAULT },
{ elem: inputNoiseHumidityPersistence, noise: "humidity", noise_param: "persistence", default_modern: NOISE_PERSISTENCE_DEFAULT, default_v6: NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT },
]
for (let n of noiseWidgetTable) {
n.elem.oninput = function() {
updateNoiseParam(n.noise, n.noise_param, this);
}
n.elem.onblur = function() {
let noiseElem = n.noise + "_" + biomeMode;
let defaultValue = n[DEFAULT_BIOME_NAME + "_" + biomeMode];
blurNoiseParam(n.noise, n.noise_param, this, defaultValue);
}
}
inputNoiseReset.onclick = function() {
if (biomeMode === "v6") {
noises.heat.offset = NOISE_V6_HEAT_OFFSET_DEFAULT;
noises.heat.scale = NOISE_V6_HEAT_SCALE_DEFAULT;
noises.heat.octaves = NOISE_V6_HEAT_OCTAVES_DEFAULT;
noises.heat.persistence = NOISE_V6_HEAT_PERSISTENCE_DEFAULT;
noises.heat.absvalue = NOISE_V6_HEAT_ABSVALUE_DEFAULT;
noises.humidity.offset = NOISE_V6_HUMIDITY_OFFSET_DEFAULT;
noises.humidity.scale = NOISE_V6_HUMIDITY_SCALE_DEFAULT;
noises.humidity.octaves = NOISE_V6_HUMIDITY_OCTAVES_DEFAULT;
noises.humidity.persistence = NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT;
noises.humidity.absvalue = NOISE_V6_HUMIDITY_ABSVALUE_DEFAULT;
} else {
noises.heat.offset = NOISE_OFFSET_DEFAULT;
noises.heat.scale = NOISE_SCALE_DEFAULT;
noises.heat.octaves = NOISE_OCTAVES_DEFAULT;
noises.heat.persistence = NOISE_PERSISTENCE_DEFAULT;
noises.heat.absvalue = NOISE_ABSVALUE_DEFAULT;
noises.humidity.offset = NOISE_OFFSET_DEFAULT;
noises.humidity.scale = NOISE_SCALE_DEFAULT;
noises.humidity.octaves = NOISE_OCTAVES_DEFAULT;
noises.humidity.persistence = NOISE_PERSISTENCE_DEFAULT;
noises.humidity.absvalue = NOISE_ABSVALUE_DEFAULT;
}
inputNoiseHeatOffset.value = noises.heat.offset;
inputNoiseHeatScale.value = noises.heat.scale;
inputNoiseHeatOctaves.value = noises.heat.octaves;
inputNoiseHeatPersistence.value = noises.heat.persistence;
inputNoiseHumidityOffset.value = noises.humidity.offset;
inputNoiseHumidityScale.value = noises.humidity.scale;
inputNoiseHumidityOctaves.value = noises.humidity.octaves;
inputNoiseHumidityPersistence.value = noises.humidity.persistence;
clear();
updateAreaVars();
draw(true);
}
/* Export events */
inputExportLua.onclick = function() {
let str = "";
for (let b=0; b<biomePoints.length; b++) {
let biome = biomePoints[b];
// escape the name for Lua
let escapedName = biome.name;
// escape backslash
escapedName = escapedName.replace(/\\/g, '\\\\');
// escape quotation mark
escapedName = escapedName.replace(/"/g, '\\\"');
str += "core.register_biome({\n";
str += ` name = \"${escapedName}\",\n`;
str += ` heat_point = ${biome.heat},\n`;
str += ` humidity_point = ${biome.humidity},\n`;
if (biome.min_x !== MIN_X || biome.max_x !== MAX_X || biome.min_z !== MIN_Z || biome.max_z !== MAX_Z) {
str += ` max_pos { x = ${biome.max_x}, y = ${biome.max_y}, z = ${biome.max_z} },\n`;
str += ` min_pos { x = ${biome.min_x}, y = ${biome.min_y}, z = ${biome.min_z} },\n`;
} else if (biome.min_y !== MIN_Y || biome.max_y !== MAX_Y) {
str += ` max_y = ${biome.max_y},\n`;
str += ` min_y = ${biome.min_y},\n`;
}
str += "})\n";
}
exportSectionText.innerText = str;
if (str === "") {
exportSectionText.hidden = true;
exportLabel.innerText = "Export is empty.";
} else {
exportSectionText.hidden = false;
exportLabel.innerText = "Exported biomes (as Lua code):";
}
exportSectionOuter.hidden = false;
}
inputExportJSON.onclick = function() {
// Convert the biome points to a new table
let jsonPoints = [];
for (let b=0; b<biomePoints.length; b++) {
let biome = biomePoints[b];
let jsonPoint = {};
// Use different field names for the
// JSON export to match the Lua field
// names exactly.
jsonPoint.name = biome.name;
jsonPoint.heat_point = biome.heat;
jsonPoint.humidity_point = biome.humidity;
jsonPoint.x_min = biome.min_x;
jsonPoint.x_max = biome.max_x;
jsonPoint.y_min = biome.min_y;
jsonPoint.y_max = biome.max_y;
jsonPoint.z_min = biome.min_z;
jsonPoint.z_max = biome.max_z;
jsonPoint.color_index = biome.colorIndex;
jsonPoints.push(jsonPoint);
}
let str = JSON.stringify(jsonPoints, undefined, 4);
exportSectionText.innerText = str;
if (str === "") {
exportSectionText.hidden = true;
exportLabel.innerText = "Export is empty.";
} else {
exportSectionText.hidden = false;
exportLabel.innerText = "Exported biomes (as JSON):";
}
exportSectionOuter.hidden = false;
}
// Assuming that hexColor is a string of the form
// "#xxxxxx" (where x is a hexadecimal digit),
// returns an object of the form { r: 0, g: 0, b: 0 }
function hexColorToRGBColor(hexColor) {
if (typeof hexColor !== "string") {
return null;
}
let rh = hexColor.slice(1,3);
let gh = hexColor.slice(3,5);
let bh = hexColor.slice(5,7);
if (rh === "" || gh === "" || bh === "") {
return null;
}
let r = Number("0x"+rh)
let g = Number("0x"+gh)
let b = Number("0x"+bh)
if (typeof r !== "number" || typeof g !== "number" || typeof b !== "number") {
return null;
}
return { r:r, g:g, b:b }
}
inputExportAmidstForMinetest.onclick = function() {
let jsonOut = {};
jsonOut.name = "LiBPoV Export";
let jsonPoints = [];
for (let b=0; b<biomePoints.length; b++) {
let biome = biomePoints[b];
let jsonPoint = {};
jsonPoint.name = biome.name;
let color = hexColorToRGBColor(CELL_COLORS[biome.colorIndex]);
if (color !== null) {
jsonPoint.color = color;
} else {
jsonPoint.color = { r: 255, g: 255, b: 255 };
}
jsonPoint.y_min = biome.min_y;
jsonPoint.y_max = biome.max_y;
jsonPoint.heat_point = biome.heat;
jsonPoint.humidity_point = biome.humidity;
jsonPoints.push(jsonPoint);
}
jsonOut.biomeList = jsonPoints;
let str = JSON.stringify(jsonOut, undefined, 4);
exportSectionText.innerText = str;
if (str === "") {
exportSectionText.hidden = true;
exportLabel.innerText = "Export is empty.";
} else {
exportSectionText.hidden = false;
exportLabel.innerHTML = "Exported biomes (as <i>Amidst for Minetest</i> biome profile):";
}
exportSectionOuter.hidden = false;
}
inputExportClear.onclick = function() {
exportSectionOuter.hidden = true;
exportSectionText.innerText = "";
}
/* Import */
inputImportSubmit.onclick = function() {
let importMessage = function(message) {
importResultOuter.hidden = false;
importResultMessage.innerText = message;
}
let importStr = inputImport.value;
// Do the JSON parse
let reviver = function(key, value) {
if (key === "name" && ((typeof value) === "string")) {
return value;
} else if (((typeof value) === "number") && (
key === "humidity_point" ||
key === "heat_point" ||
key === "x_min" ||
key === "x_max" ||
key === "y_min" ||
key === "y_max" ||
key === "z_min" ||
key === "z_max" ||
key === "colorcode")) {
return value;
} else {
return value;
}
}
let parsedJSON;
try {
parsedJSON = JSON.parse(importStr, reviver);
} catch(err) {
if (err.name === "SyntaxError") {
let details = err.message;
if (!details) {
details = "<none given>";
}
importMessage("Import failed. Not a syntactically valid JSON object. Details: "+details);
} else {
importMessage("Import failed due to internal error of type '"+err.name+"': "+err.message);
console.error("Internal error while calling JSON.parse during import!\n"+
"* exception name: "+err.name+"\n"+
"* exception message: "+err.message+"\n"+
"* stack:\n"+err.stack);
}
return;
}
if (typeof parsedJSON !== "object") {
importMessage("Import failed. JSON.parse didnt return an object but it didnt throw an exception?!");
console.error("JSON.parse didn't return an object although it didn't throw an exception");
return;
}
// Populate the temporary newPoints that MAY
// set the biomePoints if successful
let newPoints = [];
lastBiomeID = 0;
let fieldsToCheck = [
{ fieldName: "name", type: "string" },
{ fieldName: "heat_point", type: "number" },
{ fieldName: "humidity_point", type: "number" },
{ fieldName: "x_min", type: "number", fieldDefault: MIN_X },
{ fieldName: "x_max", type: "number", fieldDefault: MAX_X },
{ fieldName: "y_min", type: "number", fieldDefault: MIN_Y },
{ fieldName: "y_max", type: "number", fieldDefault: MAX_Y },
{ fieldName: "z_min", type: "number", fieldDefault: MIN_Z },
{ fieldName: "z_max", type: "number", fieldDefault: MAX_Z },
{ fieldName: "color_index", type: "number", optional: true },
]
for (let p=0; p<parsedJSON.length; p++) {
let parsedPoint = parsedJSON[p];
// Type checking
for (let f=0; f<fieldsToCheck.length; f++) {
let field = fieldsToCheck[f].fieldName;
let wantType = fieldsToCheck[f].type;
let defaultValue = fieldsToCheck[f].fieldDefault;
let isOptional = fieldsToCheck[f].optional;
let gotType = typeof parsedPoint[field];
if (gotType === "undefined") {
if (isOptional) {
// skip
} else if (defaultValue !== undefined) {
parsedPoint[field] = defaultValue;
} else {
importMessage(`Import failed. attribute "${field}" of biome #${p} is undefined.`)
return;
}
} else if (gotType !== wantType) {
importMessage(`Import failed. attribute "${field}" of biome #${p} is of type "${gotType}" but "${wantType}" expected.`)
return;
}
}
let colorIndex = parsedPoint.color_index;
if (typeof colorIndex === "number") {
if (colorIndex < 0) {
colorIndex = undefined;
} else if (colorIndex > CELL_COLORS.length-1) {
colorIndex = undefined;
}
}
let newPoint = {
id: lastBiomeID,
name: parsedPoint.name,
heat: parsedPoint.heat_point,
humidity: parsedPoint.humidity_point,
min_x: parsedPoint.x_min,
max_x: parsedPoint.x_max,
min_y: parsedPoint.y_min,
max_y: parsedPoint.y_max,
min_z: parsedPoint.z_min,
max_z: parsedPoint.z_max,
colorIndex: colorIndex || lastBiomeID % CELL_COLORS.length,
};
lastBiomeID++;
newPoints.push(newPoint);
}
// Replace the biomes
biomePoints = newPoints;
repopulateBiomeSelector();
updateWidgetStates();
draw(true);
if (biomePoints.length === 1) {
importMessage("Import successful. 1 biome imported.");
} else {
importMessage(`Import successful. ${biomePoints.length} biomes imported.`);
}
}
/* Mode events */
modernModeButton.onclick = function() {
biomeMode = "modern";
biomeConfigContainerOuter.hidden = false;
biomeV6ConfigContainerOuter.hidden = true;
importContainerOuter.hidden = false;
exportContainerOuter.hidden = false;
inputCheckboxPoints.disabled = false;
noises.heat = noises.heat_modern;
noises.humidity = noises.humidity_modern;
noiseSettingNameHeat.innerText = "mg_biome_np_heat";
noiseSettingNameHumidity.innerText = "mg_biome_np_humidity";
modernModeButton.className = "activeBiomeModeButton";
v6ModeButton.className = "";
updateAreaVars();
updateWidgetStates();
draw(true);
}
v6ModeButton.onclick = function() {
biomeMode = "v6";
biomeConfigContainerOuter.hidden = true;
biomeV6ConfigContainerOuter.hidden = false;
importContainerOuter.hidden = true;
exportContainerOuter.hidden = true;
inputCheckboxPoints.disabled = true;
noises.heat = noises.heat_v6;
noises.humidity = noises.humidity_v6;
noiseSettingNameHeat.innerText = "mgv6_np_biome";
noiseSettingNameHumidity.innerText = "mgv6_np_humidity";
modernModeButton.className = "";
v6ModeButton.className = "activeBiomeModeButton";
updateAreaVars();
updateWidgetStates();
draw();
}
/* Events for collapsing/extending config section with the arrow thingie */
biomeConfigHeaderLink.onclick = function() {
toggleConfigSectionDisplay(this, biomeConfigContainer);
}
biomeV6ConfigHeaderLink.onclick = function() {
toggleConfigSectionDisplay(this, biomeV6ConfigContainer);
}
viewConfigHeaderLink.onclick = function() {
toggleConfigSectionDisplay(this, viewConfigContainer);
}
noiseConfigHeaderLink.onclick = function() {
toggleConfigSectionDisplay(this, noiseConfigContainer);
}
importHeaderLink.onclick = function() {
toggleConfigSectionDisplay(this, importContainer);
}
exportHeaderLink.onclick = function() {
toggleConfigSectionDisplay(this, exportContainer);
}
/* Prevent forms from submitting */
function disableFormSubmission() {
let elements = document.querySelectorAll("form");
for (let elem of elements) {
elem.onsubmit = function() {
return false;
}
}
}
function initViewCoords() {
viewX = inputViewX.value;
viewY = inputViewY.value;
viewZ = inputViewZ.value;
}
function resetBiomeInputs() {
inputMinX.value = MIN_X;
inputMaxX.value = MAX_X;
inputMinY.value = MIN_Y;
inputMaxY.value = MAX_Y;
inputMinZ.value = MIN_Z;
inputMaxZ.value = MAX_Z;
inputHeat.value = DEFAULT_HEAT;
inputHumidity.value = DEFAULT_HUMIDITY;
inputBiomeName.value = DEFAULT_BIOME_NAME;
}
/* Load events */
window.addEventListener("load", initBiomeColorSelectors);
window.addEventListener("load", initViewCoords);
window.addEventListener("load", resetBiomeInputs);
window.addEventListener("load", checkboxVarsInit);
window.addEventListener("load", function() {
draw(true);
})
window.addEventListener("load", repopulateBiomeSelector);
window.addEventListener("load", updateWidgetStates);
window.addEventListener("load", updateWorldPositionText);
window.addEventListener("load", unhideContent);
window.addEventListener("load", fitCanvasInBody);
window.addEventListener("load", disableFormSubmission);