2260 lines
66 KiB
JavaScript
2260 lines
66 KiB
JavaScript
"use strict";
|
||
|
||
// Default values for the min. and max. Y of biomes
|
||
const MIN_Y_DEFAULT = -31000
|
||
const MAX_Y_DEFAULT = 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)"
|
||
|
||
// 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 = "#ccccff";
|
||
const CELL_COLOR_V6_TUNDRA = "#aaaaff";
|
||
|
||
// 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
|
||
|
||
/* 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;
|
||
|
||
// Y altitude at which the diagram is currently viewed at
|
||
let viewY = 0;
|
||
|
||
let v6_flag_snowbiomes = true;
|
||
let v6_flag_jungles = true;
|
||
let v6_freq_desert = 0.5;
|
||
|
||
// 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", heat: midpoint_heat, humidity: midpoint_humidity, min_y: MIN_Y_DEFAULT, max_y: MAX_Y_DEFAULT});
|
||
|
||
// 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 Y value of the viewed altitude
|
||
function getViewY() {
|
||
return viewY;
|
||
}
|
||
// 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 = (limit_heat_max - limit_heat_min) * (100/175);
|
||
let 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";
|
||
context.fillText(Math.round(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";
|
||
context.fillText(Math.round(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 we’d 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 Y limits fall out of the
|
||
given y value */
|
||
function getRenderedPoints(y) {
|
||
let points = [];
|
||
for (let p=0; p<biomePoints.length; p++) {
|
||
let point = biomePoints[p];
|
||
if (y >= point.min_y && y <= point.max_y) {
|
||
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];
|
||
}
|
||
|
||
function checkDrawValid(context) {
|
||
if (!context) {
|
||
if (!voronoiCanvas.hidden) {
|
||
voronoiCanvas.hidden = true;
|
||
coordinateDisplay.hidden = true;
|
||
altitudeDisplay.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;
|
||
}
|
||
|
||
let showDiagramMessage = function(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);
|
||
updateAltitudeText();
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
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 y = getViewY();
|
||
|
||
// shorter function name (for "convert")
|
||
let conv = biomeCoordsToCanvasPixelCoords
|
||
|
||
if (!checkDrawValid(context)) {
|
||
return false;
|
||
}
|
||
clear(context);
|
||
|
||
let points = getRenderedPoints(y);
|
||
// 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 in this Y altitude.");
|
||
}
|
||
drawError = true;
|
||
putResizeCorner(context);
|
||
return false;
|
||
}
|
||
|
||
updateAltitudeText();
|
||
|
||
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;
|
||
let y = getViewY();
|
||
|
||
|
||
if (!checkDrawValid(context)) {
|
||
return false;
|
||
}
|
||
|
||
clear(context);
|
||
|
||
|
||
if (v6_flag_snowbiomes) {
|
||
|
||
let cx, cy;
|
||
|
||
// Calculate biome intersection coordinates
|
||
|
||
let cx_snow, cx_hot, cx_snow2, cy_snow2, cx_hot2, cy_hot2;
|
||
let _; // unused variable
|
||
|
||
// Temperate <-> Taiga/Tundra
|
||
[cx_snow, _] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_SNOW, 0);
|
||
// Temperate <-> Desert/Jungle
|
||
[cx_hot, _] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_HOT, 0);
|
||
// Taiga <-> Tundra <-> Temperate
|
||
[cx_snow2, cy_snow2] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_SNOW, MGV6_FREQ_TAIGA);
|
||
// Desert <-> Jungle <-> Temperate
|
||
[cx_hot2, cy_hot2] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_HOT, MGV6_FREQ_JUNGLE);
|
||
|
||
if (showCellColors) {
|
||
// Render biome areas
|
||
context.fillStyle = CELL_COLOR_V6_NORMAL;
|
||
// 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_snow2);
|
||
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();
|
||
|
||
// Temperate <-> Taiga/Tundra
|
||
context.moveTo(cx_snow, 0);
|
||
context.lineTo(cx_snow, h);
|
||
// Temperate <-> Desert/Jungle
|
||
context.moveTo(cx_hot, 0);
|
||
context.lineTo(cx_hot, h);
|
||
|
||
// Taiga <-> Tundra
|
||
context.moveTo(cx_snow2, cy_snow2);
|
||
context.lineTo(0, cy_snow2);
|
||
|
||
// Desert <-> Jungle
|
||
context.moveTo(cx_hot2, cy_hot2);
|
||
context.lineTo(w, cy_hot2);
|
||
|
||
context.closePath();
|
||
context.stroke();
|
||
} else {
|
||
// TODO;
|
||
}
|
||
|
||
if (showGrid) {
|
||
putGrid(context, GRID_STEP_V6);
|
||
}
|
||
if (showAxes) {
|
||
putAxes(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;
|
||
inputMinY.disabled = state;
|
||
inputMaxY.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 = " ";
|
||
}
|
||
}
|
||
if (biomeSelector.selectedIndex !== -1) {
|
||
let selected = biomeSelector.options[biomeSelector.selectedIndex];
|
||
let point = biomePoints[biomeSelector.selectedIndex];
|
||
inputHeat.value = point.heat;
|
||
inputHumidity.value = point.humidity;
|
||
inputMinY.value = point.min_y;
|
||
inputMaxY.value = point.max_y;
|
||
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 = "●"; // Unicode: BLACK CIRCLE
|
||
} else {
|
||
// Blank out non-selected color
|
||
elem.innerHTML = " ";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
|
||
/* 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 points = getRenderedPoints(getViewY());
|
||
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) {
|
||
let selectedPoint = null;
|
||
let points = getRenderedPoints(getViewY());
|
||
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 = newHeat;
|
||
selectedPoint.humidity = 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 altitude (Y height)
|
||
the diagram currently applies */
|
||
function updateAltitudeText() {
|
||
altitudeDisplay.innerHTML = "showing diagram for altitude Y=<span class='statAltitude'>"+getViewY()+"</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 = " ";
|
||
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 = " ";
|
||
}
|
||
}
|
||
|
||
/* 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) {
|
||
// 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;
|
||
}
|
||
|
||
/* 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 = " ";
|
||
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) {
|
||
// 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) {
|
||
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);
|
||
addBiome({name: generateBiomeName(lastBiomeID), heat:he, humidity: hu, min_y:MIN_Y_DEFAULT, max_y:MAX_Y_DEFAULT});
|
||
}
|
||
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 = " ";
|
||
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;
|
||
inputMinY.value = point.min_y;
|
||
inputMaxY.value = point.max_y;
|
||
}
|
||
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_y: MIN_Y_DEFAULT, max_y: MAX_Y_DEFAULT});
|
||
}
|
||
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);
|
||
}
|
||
|
||
inputHeat.oninput = function() {
|
||
handleBiomeNumberInput("heat", this);
|
||
}
|
||
inputHumidity.oninput = function() {
|
||
handleBiomeNumberInput("humidity", this);
|
||
}
|
||
inputMinY.oninput = function() {
|
||
handleBiomeNumberInput("min_y", this);
|
||
}
|
||
inputMaxY.oninput = function() {
|
||
handleBiomeNumberInput("max_y", this);
|
||
}
|
||
inputBiomeName.oninput = function() {
|
||
onChangeBiomeValueWidget("name", this.value);
|
||
}
|
||
|
||
/* Diagram view settings events */
|
||
|
||
inputViewY.oninput = function() {
|
||
let y = +this.value;
|
||
if (y === null) {
|
||
return;
|
||
}
|
||
viewY = Math.floor(y);
|
||
draw(true);
|
||
updateAltitudeText();
|
||
}
|
||
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);
|
||
}
|
||
|
||
/* Noise parameters events */
|
||
|
||
function updateNoiseParam(noiseName, noiseValueName, element) {
|
||
if (element.value === "") {
|
||
return;
|
||
}
|
||
let val = +element.value;
|
||
noises[noiseName][noiseValueName] = val;
|
||
clear();
|
||
updateAreaVars();
|
||
draw(true);
|
||
}
|
||
|
||
inputNoiseHeatScale.oninput = function() {
|
||
updateNoiseParam("heat", "scale", this);
|
||
}
|
||
inputNoiseHeatOffset.oninput = function() {
|
||
updateNoiseParam("heat", "offset", this);
|
||
}
|
||
inputNoiseHeatPersistence.oninput = function() {
|
||
updateNoiseParam("heat", "persistence", this);
|
||
}
|
||
inputNoiseHeatOctaves.oninput = function() {
|
||
updateNoiseParam("heat", "octaves", this);
|
||
}
|
||
inputNoiseHumidityScale.oninput = function() {
|
||
updateNoiseParam("humidity", "scale", this);
|
||
}
|
||
inputNoiseHumidityOffset.oninput = function() {
|
||
updateNoiseParam("humidity", "offset", this);
|
||
}
|
||
inputNoiseHumidityPersistence.oninput = function() {
|
||
updateNoiseParam("humidity", "persistence", this);
|
||
}
|
||
inputNoiseHumidityOctaves.oninput = function() {
|
||
updateNoiseParam("humidity", "octaves", this);
|
||
}
|
||
|
||
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 += "minetest.register_biome({\n";
|
||
str += ` name = \"${escapedName}\",\n`;
|
||
str += ` heat_point = ${biome.heat},\n`;
|
||
str += ` humidity_point = ${biome.humidity},\n`;
|
||
str += ` y_min = ${biome.min_y},\n`;
|
||
str += ` y_max = ${biome.max_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.y_min = biome.min_y;
|
||
jsonPoint.y_max = biome.max_y;
|
||
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 = "MiBPoV 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.innerText = "Exported biomes (as Amidst for Minetest 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 === "y_min" ||
|
||
key === "y_max")) {
|
||
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 didn’t return an object but it didn’t 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: "y_min", type: "number" },
|
||
{ fieldName: "y_max", type: "number" },
|
||
]
|
||
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 gotType = typeof parsedPoint[field];
|
||
if (gotType === "undefined") {
|
||
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 newPoint = {
|
||
id: lastBiomeID,
|
||
name: parsedPoint.name,
|
||
heat: parsedPoint.heat_point,
|
||
humidity: parsedPoint.humidity_point,
|
||
min_y: parsedPoint.y_min,
|
||
max_y: parsedPoint.y_max,
|
||
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;
|
||
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";
|
||
updateAreaVars();
|
||
updateWidgetStates();
|
||
draw(true);
|
||
}
|
||
v6ModeButton.onclick = function() {
|
||
biomeMode = "v6";
|
||
biomeConfigContainerOuter.hidden = true;
|
||
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";
|
||
updateAreaVars();
|
||
updateWidgetStates();
|
||
draw();
|
||
}
|
||
|
||
/* Events for collapsing/extending config section with the arrow thingie */
|
||
|
||
biomeConfigHeaderLink.onclick = function() {
|
||
toggleConfigSectionDisplay(this, biomeConfigContainer);
|
||
}
|
||
viewConfigHeaderLink.onclick = function() {
|
||
toggleConfigSectionDisplay(this, viewConfigContainer);
|
||
}
|
||
noiseConfigHeaderLink.onclick = function() {
|
||
toggleConfigSectionDisplay(this, noiseConfigContainer);
|
||
}
|
||
importHeaderLink.onclick = function() {
|
||
toggleConfigSectionDisplay(this, importContainer);
|
||
}
|
||
exportHeaderLink.onclick = function() {
|
||
toggleConfigSectionDisplay(this, exportContainer);
|
||
}
|
||
|
||
/* Load events */
|
||
|
||
window.addEventListener("load", initBiomeColorSelectors);
|
||
window.addEventListener("load", checkboxVarsInit);
|
||
window.addEventListener("load", function() {
|
||
draw(true);
|
||
})
|
||
window.addEventListener("load", repopulateBiomeSelector);
|
||
window.addEventListener("load", updateWidgetStates);
|
||
window.addEventListener("load", updateAltitudeText);
|
||
window.addEventListener("load", unhideContent);
|
||
window.addEventListener("load", fitCanvasInBody);
|