1030 lines
29 KiB
JavaScript
1030 lines
29 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
|
|
|
|
// Distance from the point's center in which a point can
|
|
// be selected by clicking on it
|
|
const POINT_SELECT_DISTANCE = 25
|
|
|
|
// Symbol for storing the biome ID in site objects
|
|
// for the Voronoi script
|
|
const biomeIDSymbol = Symbol("Biome ID");
|
|
|
|
// Colors
|
|
const pointColor = "#913636";
|
|
const pointColorSelected = "#e19696";
|
|
const edgeColor = "#0f2c2e";
|
|
const gridColor = "#00000040";
|
|
const clearColor = "#ecddba";
|
|
const cellColorNeutral = "#888888";
|
|
const cellColors = [
|
|
"#64988e",
|
|
"#3d7085",
|
|
"#345644",
|
|
"#6b7f5c",
|
|
"#868750",
|
|
"#a7822c",
|
|
"#a06e38",
|
|
"#ad5f52",
|
|
"#692f11",
|
|
"#89542f",
|
|
"#796e63",
|
|
"#a17d5e",
|
|
"#5a3f20",
|
|
"#836299",
|
|
];
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
let noises = {
|
|
heat: {
|
|
offset: NOISE_OFFSET_DEFAULT,
|
|
scale: NOISE_SCALE_DEFAULT,
|
|
octaves: NOISE_OCTAVES_DEFAULT,
|
|
persistence: NOISE_PERSISTENCE_DEFAULT,
|
|
absvalue: NOISE_ABSVALUE_DEFAULT,
|
|
},
|
|
humidity: {
|
|
offset: NOISE_OFFSET_DEFAULT,
|
|
scale: NOISE_SCALE_DEFAULT,
|
|
octaves: NOISE_OCTAVES_DEFAULT,
|
|
persistence: NOISE_PERSISTENCE_DEFAULT,
|
|
absvalue: NOISE_ABSVALUE_DEFAULT,
|
|
},
|
|
|
|
};
|
|
|
|
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]
|
|
}
|
|
|
|
// 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.log("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;
|
|
|
|
// Set to true if the draw canvas currently shows an error message
|
|
let drawError = false;
|
|
|
|
let lastBiomeID = 0;
|
|
let biomePoints = [];
|
|
function addBiome(biomeDef) {
|
|
biomeDef.id = lastBiomeID;
|
|
biomePoints.push(biomeDef);
|
|
lastBiomeID++;
|
|
}
|
|
|
|
// Add a default biome at the midpoint
|
|
addBiome({name: generateBiomeName(midpoint_heat, midpoint_humidity), heat:midpoint_heat, humidity:midpoint_humidity, min_y: MIN_Y_DEFAULT, max_y: MAX_Y_DEFAULT})
|
|
|
|
function getViewY() {
|
|
if (!inputViewY) {
|
|
return 0;
|
|
}
|
|
return inputViewY.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 biome name for displaying it, based on heat and humidity values.
|
|
function generateBiomeName(heat, humidity) {
|
|
return "("+heat+","+humidity+")";
|
|
}
|
|
|
|
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;
|
|
}
|
|
function voronoiPointToBiomePoint(point) {
|
|
let newPoint = { heat: point.x, humidity: point.y, id: point[biomeIDSymbol] }
|
|
return newPoint;
|
|
}
|
|
|
|
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"
|
|
context.fillText(point.name, x, y);
|
|
}
|
|
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();
|
|
// top left corner
|
|
if (he < limit_heat_min && hu < limit_humidity_min) {
|
|
context.moveTo(limit_x_min, limit_x_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();
|
|
// top 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();
|
|
// bottom 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();
|
|
// top 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();
|
|
// bottom 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();
|
|
}
|
|
};
|
|
|
|
function putGrid(context) {
|
|
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);
|
|
context.lineWidth = 2;
|
|
context.strokeStyle = gridColor;
|
|
for (let he=0; he<=limit_heat_max; he+=GRID_STEP) {
|
|
let [x, _] = biomeCoordsToCanvasPixelCoords(he, 0);
|
|
context.beginPath();
|
|
context.moveTo(x, limit_y_min);
|
|
context.lineTo(x, limit_y_max);
|
|
context.stroke();
|
|
}
|
|
for (let he=-GRID_STEP; he>=limit_heat_min; he-=GRID_STEP) {
|
|
let [x, _] = biomeCoordsToCanvasPixelCoords(he, 0);
|
|
context.beginPath();
|
|
context.moveTo(x, limit_y_min);
|
|
context.lineTo(x, limit_y_max);
|
|
context.stroke();
|
|
}
|
|
for (let hu=0; hu<=limit_humidity_max; hu+=GRID_STEP) {
|
|
let [_, y] = biomeCoordsToCanvasPixelCoords(0, hu);
|
|
context.beginPath();
|
|
context.moveTo(limit_x_min, y);
|
|
context.lineTo(limit_x_max, y);
|
|
context.stroke();
|
|
}
|
|
for (let hu=-GRID_STEP; hu>=limit_humidity_min; hu-=GRID_STEP) {
|
|
let [_, y] = biomeCoordsToCanvasPixelCoords(0, hu);
|
|
context.beginPath();
|
|
context.moveTo(limit_x_min, y);
|
|
context.lineTo(limit_x_max, y);
|
|
context.stroke();
|
|
}
|
|
}
|
|
|
|
// Cache diagram object for performance boost
|
|
let cachedVoronoiDiagram = null;
|
|
|
|
function getVoronoiDiagram(points, recalculate) {
|
|
if ((cachedVoronoiDiagram === null) || recalculate) {
|
|
let vbbox = {xl: limit_heat_min, xr: limit_heat_max, yt: limit_humidity_min, yb: limit_humidity_max};
|
|
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);
|
|
}
|
|
diagram = voronoi.compute(sites, vbbox);
|
|
cachedVoronoiDiagram = diagram;
|
|
return diagram;
|
|
} else {
|
|
return cachedVoronoiDiagram;
|
|
}
|
|
}
|
|
|
|
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 = clearColor;
|
|
context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, voronoiCanvas.width+DRAW_OFFSET, voronoiCanvas.height+DRAW_OFFSET);
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 draw(y, recalculate) {
|
|
let context = getDrawContext();
|
|
let w = voronoiCanvas.width;
|
|
let h = voronoiCanvas.height;
|
|
|
|
// shorter function name (for "convert")
|
|
let conv = biomeCoordsToCanvasPixelCoords
|
|
|
|
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.";
|
|
}
|
|
return false;
|
|
}
|
|
clear(context);
|
|
|
|
// 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)) {
|
|
context.textAlign = "center";
|
|
context.fillStyle = "black";
|
|
context.textBaseline = "middle";
|
|
context.font = "200% sans-serif";
|
|
let msg = "Value range is too small.";
|
|
context.fillText(msg, w/2, h/2);
|
|
drawError = true;
|
|
updateAltitudeText();
|
|
return true;
|
|
}
|
|
|
|
let points = getRenderedPoints(y);
|
|
// Render a special message if there are no biomes
|
|
if (points.length === 0) {
|
|
context.textAlign = "center";
|
|
context.fillStyle = "black";
|
|
context.textBaseline = "middle";
|
|
context.font = "200% sans-serif";
|
|
let msg;
|
|
if (biomePoints.length === 0) {
|
|
msg = "No biomes.";
|
|
} else {
|
|
msg = "No biomes in this Y altitude.";
|
|
}
|
|
context.fillText(msg, w/2, h/2);
|
|
drawError = true;
|
|
updateAltitudeText();
|
|
return true;
|
|
}
|
|
drawError = false;
|
|
updateAltitudeText();
|
|
|
|
let diagram = getVoronoiDiagram(points, recalculate);
|
|
|
|
// Render cell colors
|
|
if (showCellColors) {
|
|
let colors = cellColors;
|
|
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 ccol = biomeID % cellColors.length;
|
|
context.fillStyle = cellColors[ccol];
|
|
|
|
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();
|
|
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
|
|
context.fillStyle = colors[points[0].id % colors.length];
|
|
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 = cellColorNeutral;
|
|
context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, w+DRAW_OFFSET, h+DRAW_OFFSET);
|
|
}
|
|
|
|
if (points.length > 0) {
|
|
if (showGrid) {
|
|
putGrid(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 = edgeColor;
|
|
}
|
|
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 = pointColorSelected;
|
|
} else {
|
|
context.fillStyle = pointColor;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function rewriteBiomeSelector() {
|
|
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 newElemText = document.createTextNode(biomePoints[b].name);
|
|
newElem.append(newElemText);
|
|
biomeSelector.append(newElem);
|
|
}
|
|
}
|
|
|
|
function updateWidgetStates() {
|
|
if (biomePoints.length === 0 || biomeSelector.selectedIndex === -1) {
|
|
removeBiomeButton.disabled = "disabled";
|
|
inputHeat.disabled = "disabled";
|
|
inputHumidity.disabled = "disabled";
|
|
inputMinY.disabled = "disabled";
|
|
inputMaxY.disabled = "disabled";
|
|
} else {
|
|
removeBiomeButton.disabled = "";
|
|
inputHeat.disabled = "";
|
|
inputHumidity.disabled = "";
|
|
inputMinY.disabled = "";
|
|
inputMaxY.disabled = "";
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
biomeSelector.onchange = function() {
|
|
draw(getViewY(), 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();
|
|
}
|
|
|
|
function onChangeBiomeValueWidget(pointField, value) {
|
|
if (value === null) {
|
|
return;
|
|
}
|
|
if (biomeSelector.selectedIndex === -1) {
|
|
return;
|
|
}
|
|
let selected = biomeSelector.options[biomeSelector.selectedIndex];
|
|
if (selected === null) {
|
|
return;
|
|
}
|
|
let point = biomePoints[biomeSelector.selectedIndex];
|
|
point[pointField] = value;
|
|
point.name = generateBiomeName(point.heat, point.humidity);
|
|
selected.innerText = point.name;
|
|
draw(getViewY(), true);
|
|
}
|
|
|
|
inputHeat.oninput = function() {
|
|
onChangeBiomeValueWidget("heat", +this.value);
|
|
}
|
|
inputHumidity.oninput = function() {
|
|
onChangeBiomeValueWidget("humidity", +this.value);
|
|
}
|
|
inputMinY.oninput = function() {
|
|
onChangeBiomeValueWidget("min_y", +this.value);
|
|
}
|
|
inputMaxY.oninput = function() {
|
|
onChangeBiomeValueWidget("max_y", +this.value);
|
|
}
|
|
inputViewY.oninput = function() {
|
|
draw(getViewY(), true);
|
|
updateAltitudeText();
|
|
}
|
|
function updateAltitudeText() {
|
|
altitudeDisplay.innerHTML = "showing diagram for altitude Y=<span class='statAltitude'>"+inputViewY.value+"</span>";
|
|
}
|
|
|
|
addBiomeButton.onclick = function() {
|
|
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 newPoint = {
|
|
id: lastBiomeID,
|
|
name: generateBiomeName(he, hu),
|
|
heat: he,
|
|
humidity: hu,
|
|
min_y: MIN_Y_DEFAULT,
|
|
max_y: MAX_Y_DEFAULT,
|
|
};
|
|
biomePoints.push(newPoint);
|
|
let num = biomePoints.length
|
|
|
|
let newElem = document.createElement("option");
|
|
newElem.id = "biome_list_element_" + lastBiomeID;
|
|
newElem.value = "" + num;
|
|
|
|
let newElemText = document.createTextNode(newPoint.name);
|
|
newElem.append(newElemText);
|
|
biomeSelector.append(newElem);
|
|
newElem.selected = "selected";
|
|
|
|
draw(getViewY(), true);
|
|
updateWidgetStates();
|
|
|
|
lastBiomeID++;
|
|
}
|
|
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(getViewY(), true);
|
|
updateWidgetStates();
|
|
}
|
|
|
|
function selectPoint(point) {
|
|
for (let elem of biomeSelector.options) {
|
|
let strID = elem.id;
|
|
let elemID = null;
|
|
let biomeID = getBiomeIDFromHTMLElement(elem);
|
|
if (biomeID !== null) {
|
|
if (point.id === biomeID) {
|
|
if (elem.selected) {
|
|
return [true, true];
|
|
}
|
|
elem.selected = "selected";
|
|
draw(getViewY(), true);
|
|
updateWidgetStates();
|
|
return [true, false];
|
|
}
|
|
}
|
|
}
|
|
return [false, false];
|
|
}
|
|
function getDistance(x1, y1, x2, y2) {
|
|
return Math.sqrt((x2 - x1)**2 + (y2 - y1)**2);
|
|
}
|
|
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 = Math.round((x + limit_heat_min * w) / w);
|
|
// This also flips the Y axis
|
|
let humidity = limit_humidity_min + (limit_humidity_max - (Math.round((y + limit_humidity_min * h) / h)));
|
|
return [heat, humidity];
|
|
}
|
|
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];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
let mouseIsDown = false;
|
|
let dragDropPointID = null;
|
|
|
|
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;
|
|
selectedPoint.name = generateBiomeName(newHeat, newHumidity);
|
|
draw(getViewY(), true);
|
|
updateWidgetStates();
|
|
let [elemID, elem] = getSelectedBiomeIDAndElement();
|
|
if (elemID !== null && points[i].id === elemID) {
|
|
elem.innerText = selectedPoint.name;
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateCoordinateDisplay(pixelX, pixelY) {
|
|
// show coordinates
|
|
let [heat, humidity] = canvasPixelCoordsToBiomeCoords(pixelX, pixelY);
|
|
if (!drawError) {
|
|
let html = "cursor coordinates: heat=<span class='statHeat'>"+heat+"</span>; humidity=<span class='statHumidity'>"+humidity+"</span>";
|
|
coordinateDisplay.innerHTML = html;
|
|
} else {
|
|
coordinateDisplay.innerHTML = " ";
|
|
}
|
|
}
|
|
|
|
function updateDragDropCursorStatus() {
|
|
if (drawError || !showPoints) {
|
|
voronoiCanvas.style.cursor = "auto";
|
|
return
|
|
}
|
|
|
|
let nearest = getNearestPointFromCanvasPos(event.offsetX, event.offsetY, POINT_SELECT_DISTANCE);
|
|
if (nearest !== null) {
|
|
let [id, elem] = getSelectedBiomeIDAndElement();
|
|
if (id !== null && nearest.id === id) {
|
|
voronoiCanvas.style.cursor = "grab";
|
|
} else {
|
|
voronoiCanvas.style.cursor = "crosshair";
|
|
}
|
|
} else {
|
|
voronoiCanvas.style.cursor = "auto";
|
|
}
|
|
}
|
|
|
|
voronoiCanvas.onmousemove = function(event) {
|
|
// drag-n-drop
|
|
if (mouseIsDown) {
|
|
updatePointWhenDragged(dragDropPointID);
|
|
}
|
|
updateCoordinateDisplay(event.offsetX, event.offsetY);
|
|
updateDragDropCursorStatus();
|
|
}
|
|
voronoiCanvas.onmouseenter = function(event) {
|
|
updateCoordinateDisplay(event.offsetX, event.offsetY);
|
|
updateDragDropCursorStatus();
|
|
}
|
|
|
|
voronoiCanvas.onmousedown = function(event) {
|
|
// select point by clicking.
|
|
// initiate drag-n-drop if already selected.
|
|
mouseIsDown = true;
|
|
if (!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);
|
|
if (alreadySelected) {
|
|
dragDropPointID = nearest.id;
|
|
}
|
|
updateDragDropCursorStatus();
|
|
}
|
|
}
|
|
voronoiCanvas.onmouseup = function(event) {
|
|
// end drag-n-drop
|
|
updatePointWhenDragged(dragDropPointID);
|
|
mouseIsDown = false;
|
|
dragDropPointID = null;
|
|
}
|
|
voronoiCanvas.onmouseleave = function() {
|
|
// end drag-n-drop
|
|
mouseIsDown = false;
|
|
dragDropPointID = null;
|
|
coordinateDisplay.innerHTML = " ";
|
|
}
|
|
|
|
inputCheckboxNames.onchange = function() {
|
|
showNames = this.checked;
|
|
draw(getViewY(), true);
|
|
}
|
|
inputCheckboxPoints.onchange = function() {
|
|
showPoints = this.checked;
|
|
draw(getViewY(), true);
|
|
}
|
|
inputCheckboxCellColors.onchange = function() {
|
|
showCellColors = this.checked;
|
|
draw(getViewY(), true);
|
|
}
|
|
inputCheckboxGrid.onchange = function() {
|
|
showGrid = this.checked;
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHeatScale.oninput = function() {
|
|
noises.heat.scale = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHeatOffset.oninput = function() {
|
|
noises.heat.offset = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHeatPersistence.oninput = function() {
|
|
noises.heat.persistence = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHeatOctaves.oninput = function() {
|
|
noises.heat.octaves = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHumidityScale.oninput = function() {
|
|
noises.humidity.scale = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHumidityOffset.oninput = function() {
|
|
noises.humidity.offset = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHumidityPersistence.oninput = function() {
|
|
noises.humidity.persistence = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseHumidityOctaves.oninput = function() {
|
|
noises.humidity.octaves = +this.value;
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
inputNoiseReset.onclick = function() {
|
|
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;
|
|
inputNoiseHeatOffset.value = noises.heat.offset;
|
|
inputNoiseHeatScale.value = noises.heat.scale;
|
|
inputNoiseHeatOctaves.value = noises.heat.octaves;
|
|
inputNoiseHeatPersistence.value = noises.heat.persistence;
|
|
|
|
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;
|
|
inputNoiseHumidityOffset.value = noises.humidity.offset;
|
|
inputNoiseHumidityScale.value = noises.humidity.scale;
|
|
inputNoiseHumidityOctaves.value = noises.humidity.octaves;
|
|
inputNoiseHumidityPersistence.value = noises.humidity.persistence;
|
|
|
|
clear();
|
|
updateAreaVars();
|
|
draw(getViewY(), true);
|
|
}
|
|
|
|
function checkboxVarsInit() {
|
|
showNames = inputCheckboxNames.checked;
|
|
showPoints = inputCheckboxPoints.checked;
|
|
showCellColors = inputCheckboxCellColors.checked;
|
|
showGrid = inputCheckboxGrid.checked;
|
|
}
|
|
|
|
function toggleConfigSectionDisplay(headerLink, container) {
|
|
if (container.style.display !== "none") {
|
|
headerLink.innerText = "▶";
|
|
container.style.display = "none";
|
|
} else {
|
|
headerLink.innerText = "▼";
|
|
container.style.display = "block";
|
|
}
|
|
}
|
|
biomeConfigHeaderLink.onclick = function() {
|
|
toggleConfigSectionDisplay(this, biomeConfigContainer);
|
|
}
|
|
viewConfigHeaderLink.onclick = function() {
|
|
toggleConfigSectionDisplay(this, viewConfigContainer);
|
|
}
|
|
noiseConfigHeaderLink.onclick = function() {
|
|
toggleConfigSectionDisplay(this, noiseConfigContainer);
|
|
}
|
|
|
|
function unhideContent() {
|
|
mainContentContainer.hidden = false;
|
|
noscriptContainer.hidden = true;
|
|
}
|
|
|
|
window.addEventListener("load", checkboxVarsInit);
|
|
window.addEventListener("load", function() {
|
|
draw(getViewY(), true);
|
|
})
|
|
window.addEventListener("load", rewriteBiomeSelector);
|
|
window.addEventListener("load", updateWidgetStates);
|
|
window.addEventListener("load", updateAltitudeText);
|
|
window.addEventListener("load", unhideContent);
|