2023-10-20 13:31:23 +02:00
|
|
|
"use strict";
|
2023-10-20 14:03:45 +02:00
|
|
|
|
2023-10-20 14:39:49 +02:00
|
|
|
const MIN_Y_DEFAULT = -31000
|
|
|
|
const MAX_Y_DEFAULT = 31000
|
|
|
|
const LIMIT_MIN = -37.5
|
|
|
|
const LIMIT_MAX = 137.5
|
|
|
|
const DRAW_MIN = LIMIT_MIN - 10
|
|
|
|
const DRAW_MAX = LIMIT_MAX + 10
|
|
|
|
const GRID_STEP = 10
|
2023-10-21 11:35:22 +02:00
|
|
|
const MIDPOINT = 50
|
2023-10-20 14:39:49 +02:00
|
|
|
|
2023-10-21 02:18:29 +02:00
|
|
|
// Symbol for storing the biome ID in site objects
|
|
|
|
// for the Voronoi script
|
|
|
|
const biomeIDSymbol = Symbol("Biome ID");
|
|
|
|
|
2023-10-20 16:31:11 +02:00
|
|
|
let lastBiomeID = 0;
|
2023-10-20 18:38:31 +02:00
|
|
|
let biomePoints = [];
|
|
|
|
function addBiome(biomeDef) {
|
|
|
|
biomeDef.id = lastBiomeID;
|
|
|
|
biomePoints.push(biomeDef);
|
|
|
|
lastBiomeID++;
|
|
|
|
}
|
2023-10-20 14:03:45 +02:00
|
|
|
|
2023-10-21 11:35:22 +02:00
|
|
|
// Add a default biome at the midpoint
|
|
|
|
addBiome({name: generateBiomeName(MIDPOINT, MIDPOINT), heat:MIDPOINT, humidity:MIDPOINT, min_y: MIN_Y_DEFAULT, max_y: MAX_Y_DEFAULT})
|
2023-10-20 13:31:23 +02:00
|
|
|
|
2023-10-20 17:22:48 +02:00
|
|
|
function getViewY() {
|
|
|
|
if (!inputViewY) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return inputViewY.value;
|
|
|
|
}
|
|
|
|
|
2023-10-20 13:31:23 +02:00
|
|
|
const pointColor = "#913636";
|
|
|
|
const pointColorSelected = "#e19696";
|
|
|
|
const edgeColor = "#0f2c2e";
|
|
|
|
const gridColor = "#00000040";
|
|
|
|
const axisColor = "#000000";
|
2023-10-20 17:30:05 +02:00
|
|
|
const clearColor = "#ecddba";
|
2023-10-20 13:31:23 +02:00
|
|
|
const cellColors = [
|
|
|
|
"#64988e",
|
|
|
|
"#3d7085",
|
|
|
|
"#345644",
|
|
|
|
"#6b7f5c",
|
2023-10-21 12:08:24 +02:00
|
|
|
"#868750",
|
|
|
|
"#a7822c",
|
|
|
|
"#a06e38",
|
2023-10-20 13:31:23 +02:00
|
|
|
"#ad5f52",
|
|
|
|
"#692f11",
|
|
|
|
"#89542f",
|
|
|
|
"#796e63",
|
|
|
|
"#a17d5e",
|
2023-10-21 12:08:24 +02:00
|
|
|
"#5a3f20",
|
|
|
|
"#836299",
|
2023-10-20 13:31:23 +02:00
|
|
|
];
|
|
|
|
|
2023-10-20 16:31:11 +02:00
|
|
|
// 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;
|
|
|
|
}
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
2023-10-20 16:31:11 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a biome name for displaying it, based on heat and humidity values.
|
|
|
|
function generateBiomeName(heat, humidity) {
|
|
|
|
return "("+heat+","+humidity+")";
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function biomePointToVoronoiPoint(point) {
|
2023-10-21 02:18:29 +02:00
|
|
|
// 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 }
|
2023-10-20 13:31:23 +02:00
|
|
|
return newPoint;
|
|
|
|
}
|
|
|
|
function voronoiPointToBiomePoint(point) {
|
2023-10-21 02:18:29 +02:00
|
|
|
let newPoint = { heat: point.x, humidity: point.y, id: point[biomeIDSymbol] }
|
2023-10-20 13:31:23 +02:00
|
|
|
return newPoint;
|
|
|
|
}
|
|
|
|
|
2023-10-21 11:35:22 +02:00
|
|
|
function putPointName(context, point) {
|
|
|
|
let x = point.heat
|
|
|
|
let y = point.humidity - 1
|
|
|
|
if (x < LIMIT_MIN || x > LIMIT_MAX || y < LIMIT_MIN || y > LIMIT_MAX) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (x > MIDPOINT) {
|
|
|
|
context.textAlign = "right";
|
|
|
|
x = x-2;
|
|
|
|
} else {
|
|
|
|
context.textAlign = "left";
|
|
|
|
x = x+2;
|
|
|
|
}
|
2023-10-21 12:08:24 +02:00
|
|
|
if (y < MIDPOINT) {
|
|
|
|
context.textBaseline = "top";
|
|
|
|
} else {
|
|
|
|
context.textBaseline = "alphabetic";
|
|
|
|
}
|
2023-10-21 11:35:22 +02:00
|
|
|
context.font = "35% sans-serif"
|
|
|
|
context.fillText(point.name, x, y);
|
|
|
|
}
|
2023-10-20 13:31:23 +02:00
|
|
|
function putPoint(context, point) {
|
2023-10-20 15:23:44 +02:00
|
|
|
const ARROW_SIZE_SIDE = 2.25;
|
|
|
|
const ARROW_SIZE_CORNER = 2.5;
|
|
|
|
let x = point.heat
|
|
|
|
let y = point.humidity
|
|
|
|
// Point is out of bounds: Draw an arrow at the border
|
|
|
|
if (x < LIMIT_MIN || x > LIMIT_MAX || y < LIMIT_MIN || y > LIMIT_MAX) {
|
|
|
|
context.beginPath();
|
|
|
|
// top left corner
|
|
|
|
if (x < LIMIT_MIN && y < LIMIT_MIN) {
|
|
|
|
context.moveTo(LIMIT_MIN, LIMIT_MIN);
|
|
|
|
context.lineTo(LIMIT_MIN + ARROW_SIZE_CORNER, LIMIT_MIN);
|
|
|
|
context.lineTo(LIMIT_MIN, LIMIT_MIN + ARROW_SIZE_CORNER);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// top right corner
|
|
|
|
} else if (x > LIMIT_MAX && y < LIMIT_MIN) {
|
|
|
|
context.moveTo(LIMIT_MAX, LIMIT_MIN);
|
|
|
|
context.lineTo(LIMIT_MAX - ARROW_SIZE_CORNER, LIMIT_MIN);
|
|
|
|
context.lineTo(LIMIT_MAX, LIMIT_MIN + ARROW_SIZE_CORNER);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// bottom left corner
|
|
|
|
} else if (x < LIMIT_MIN && y > LIMIT_MAX) {
|
|
|
|
context.moveTo(LIMIT_MIN, LIMIT_MAX);
|
|
|
|
context.lineTo(LIMIT_MIN + ARROW_SIZE_CORNER, LIMIT_MAX);
|
|
|
|
context.lineTo(LIMIT_MIN, LIMIT_MAX - ARROW_SIZE_CORNER);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// top right corner
|
|
|
|
} else if (x > LIMIT_MAX && y > LIMIT_MAX) {
|
|
|
|
context.moveTo(LIMIT_MAX, LIMIT_MAX);
|
|
|
|
context.lineTo(LIMIT_MAX - ARROW_SIZE_CORNER, LIMIT_MAX);
|
|
|
|
context.lineTo(LIMIT_MAX, LIMIT_MAX - ARROW_SIZE_CORNER);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// left side
|
|
|
|
} else if (x < LIMIT_MIN) {
|
|
|
|
context.moveTo(LIMIT_MIN, y);
|
|
|
|
context.lineTo(LIMIT_MIN + ARROW_SIZE_SIDE, y + ARROW_SIZE_SIDE);
|
|
|
|
context.lineTo(LIMIT_MIN + ARROW_SIZE_SIDE, y - ARROW_SIZE_SIDE);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// right side
|
|
|
|
} else if (x > LIMIT_MAX) {
|
|
|
|
context.moveTo(LIMIT_MAX, y);
|
|
|
|
context.lineTo(LIMIT_MAX - ARROW_SIZE_SIDE, y + ARROW_SIZE_SIDE);
|
|
|
|
context.lineTo(LIMIT_MAX - ARROW_SIZE_SIDE, y - ARROW_SIZE_SIDE);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// top side
|
|
|
|
} else if (y < LIMIT_MIN) {
|
|
|
|
context.moveTo(x, LIMIT_MIN);
|
|
|
|
context.lineTo(x - ARROW_SIZE_SIDE, LIMIT_MIN + ARROW_SIZE_SIDE);
|
|
|
|
context.lineTo(x + ARROW_SIZE_SIDE, LIMIT_MIN + ARROW_SIZE_SIDE);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
// bottom side
|
|
|
|
} else if (y > LIMIT_MAX) {
|
|
|
|
context.moveTo(x, LIMIT_MAX);
|
|
|
|
context.lineTo(x - ARROW_SIZE_SIDE, LIMIT_MAX - ARROW_SIZE_SIDE);
|
|
|
|
context.lineTo(x + ARROW_SIZE_SIDE, LIMIT_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, 2, 0, Math.PI * 2);
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
}
|
2023-10-20 13:31:23 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
function putGrid(context) {
|
|
|
|
context.lineWidth = 0.5;
|
|
|
|
context.strokeStyle = gridColor;
|
|
|
|
for (let x=0; x<=LIMIT_MAX; x+=GRID_STEP) {
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(x, LIMIT_MIN);
|
|
|
|
context.lineTo(x, LIMIT_MAX);
|
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
for (let x=-GRID_STEP; x>=LIMIT_MIN; x-=GRID_STEP) {
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(x, LIMIT_MIN);
|
|
|
|
context.lineTo(x, LIMIT_MAX);
|
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
for (let y=0; y<=LIMIT_MAX; y+=GRID_STEP) {
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(LIMIT_MIN, y);
|
|
|
|
context.lineTo(LIMIT_MAX, y);
|
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
for (let y=-GRID_STEP; y>=LIMIT_MIN; y-=GRID_STEP) {
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(LIMIT_MIN, y);
|
|
|
|
context.lineTo(LIMIT_MAX, y);
|
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function putAxes(context) {
|
|
|
|
context.lineWidth = 0.75;
|
|
|
|
// heat axis (horizontal)
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(LIMIT_MIN,0)
|
|
|
|
context.lineTo(LIMIT_MAX,0)
|
|
|
|
context.closePath();
|
|
|
|
context.stroke();
|
|
|
|
|
|
|
|
// humidity axis ()
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(0,LIMIT_MIN)
|
|
|
|
context.lineTo(0,LIMIT_MAX)
|
|
|
|
context.closePath();
|
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache diagram object for performance boost
|
|
|
|
let cachedVoronoiDiagram = null;
|
|
|
|
|
2023-10-20 17:22:48 +02:00
|
|
|
function getVoronoiDiagram(points, recalculate) {
|
2023-10-20 13:31:23 +02:00
|
|
|
if ((cachedVoronoiDiagram === null) || recalculate) {
|
2023-10-20 15:32:14 +02:00
|
|
|
let vbbox = {xl: LIMIT_MIN, xr: LIMIT_MAX, yt: LIMIT_MIN, yb: LIMIT_MAX};
|
2023-10-20 13:31:23 +02:00
|
|
|
let sites = []
|
2023-10-20 17:22:48 +02:00
|
|
|
for (let p of points) {
|
2023-10-20 13:31:23 +02:00
|
|
|
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");
|
|
|
|
// TODO: Check for getContext support of browser
|
2023-10-20 16:31:11 +02:00
|
|
|
return canvas.getContext("2d");
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
|
|
|
function drawInit() {
|
|
|
|
let context = getDrawContext();
|
|
|
|
context.scale(voronoiCanvas.width/(LIMIT_MAX-LIMIT_MIN), voronoiCanvas.height/(LIMIT_MAX-LIMIT_MIN));
|
|
|
|
context.translate(-LIMIT_MIN, -LIMIT_MIN);
|
|
|
|
}
|
2023-10-20 22:03:15 +02:00
|
|
|
function getRenderedPoints(y) {
|
2023-10-20 17:22:48 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2023-10-20 22:03:15 +02:00
|
|
|
return points;
|
|
|
|
}
|
|
|
|
function draw(y, recalculate) {
|
|
|
|
let context = getDrawContext();
|
|
|
|
// Clear draw area
|
|
|
|
context.fillStyle = clearColor;
|
|
|
|
context.fillRect(DRAW_MIN, DRAW_MIN, DRAW_MAX-DRAW_MIN, DRAW_MAX-DRAW_MIN);
|
|
|
|
|
|
|
|
let points = getRenderedPoints(y);
|
2023-10-20 17:22:48 +02:00
|
|
|
|
|
|
|
let diagram = getVoronoiDiagram(points, recalculate);
|
2023-10-20 16:31:11 +02:00
|
|
|
|
|
|
|
// Render cell colors
|
2023-10-20 13:31:23 +02:00
|
|
|
let colors = cellColors;
|
|
|
|
for (let c=0; c<diagram.cells.length; c++) {
|
2023-10-21 02:18:29 +02:00
|
|
|
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 % colors.length;
|
|
|
|
|
2023-10-20 13:31:23 +02:00
|
|
|
context.fillStyle = colors[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()
|
|
|
|
if (h === 0) {
|
|
|
|
context.moveTo(start.x, start.y);
|
|
|
|
} else {
|
|
|
|
context.lineTo(start.x, start.y);
|
|
|
|
}
|
|
|
|
context.lineTo(end.x, end.y);
|
|
|
|
}
|
|
|
|
context.closePath();
|
|
|
|
context.fill();
|
|
|
|
}
|
2023-10-20 16:31:11 +02:00
|
|
|
// 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.
|
2023-10-20 17:22:48 +02:00
|
|
|
if (points.length === 1 && diagram.cells.length === 1) {
|
2023-10-20 16:31:11 +02:00
|
|
|
// 1 cell means the whole area is filled
|
2023-10-21 11:08:39 +02:00
|
|
|
context.fillStyle = colors[points[0].id % colors.length];
|
2023-10-20 13:31:23 +02:00
|
|
|
context.fillRect(DRAW_MIN, DRAW_MIN, DRAW_MAX-DRAW_MIN, DRAW_MAX-DRAW_MIN);
|
|
|
|
}
|
|
|
|
|
2023-10-20 17:24:06 +02:00
|
|
|
if (points.length > 0) {
|
|
|
|
//putAxes(context);
|
|
|
|
putGrid(context);
|
|
|
|
}
|
2023-10-20 13:31:23 +02:00
|
|
|
|
2023-10-20 16:31:11 +02:00
|
|
|
// Render Voronoi cell edges
|
2023-10-21 11:35:22 +02:00
|
|
|
context.lineWidth = 1;
|
2023-10-20 13:31:23 +02:00
|
|
|
for (let e=0; e<diagram.edges.length; e++) {
|
|
|
|
let edge = diagram.edges[e];
|
2023-10-20 15:32:14 +02:00
|
|
|
if (edge.rSite === null) {
|
|
|
|
context.strokeStyle = "transparent";
|
|
|
|
} else {
|
|
|
|
context.strokeStyle = edgeColor;
|
|
|
|
}
|
2023-10-20 13:31:23 +02:00
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(edge.va.x, edge.va.y);
|
|
|
|
context.lineTo(edge.vb.x, edge.vb.y);
|
|
|
|
context.closePath();
|
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
|
2023-10-20 20:11:00 +02:00
|
|
|
let selElemID = null;
|
|
|
|
if (biomeSelector.selectedIndex !== -1) {
|
2023-10-20 22:03:15 +02:00
|
|
|
let selElem = biomeSelector.options[biomeSelector.selectedIndex];
|
2023-10-20 20:11:00 +02:00
|
|
|
let strID = selElem.id;
|
|
|
|
if (strID.startsWith("biome_list_element_")) {
|
|
|
|
let slice = strID.slice(19);
|
|
|
|
if (slice) {
|
|
|
|
selElemID = +slice;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 16:31:11 +02:00
|
|
|
// Render biome points
|
2023-10-21 12:08:24 +02:00
|
|
|
for (let point of points) {
|
2023-10-20 20:11:00 +02:00
|
|
|
let pointID = point.id;
|
|
|
|
// Highlight selected point
|
|
|
|
if (selElemID !== null && pointID === selElemID) {
|
2023-10-20 13:31:23 +02:00
|
|
|
context.fillStyle = pointColorSelected;
|
|
|
|
} else {
|
|
|
|
context.fillStyle = pointColor;
|
|
|
|
}
|
2023-10-20 20:11:00 +02:00
|
|
|
putPoint(context, point);
|
2023-10-21 12:08:24 +02:00
|
|
|
}
|
|
|
|
// 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";
|
|
|
|
}
|
2023-10-21 11:35:22 +02:00
|
|
|
putPointName(context, point);
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 16:31:11 +02:00
|
|
|
// Render a special message if there are no biomes
|
2023-10-20 17:22:48 +02:00
|
|
|
if (points.length === 0) {
|
2023-10-20 14:03:45 +02:00
|
|
|
context.textAlign = "center";
|
|
|
|
context.fillStyle = "black";
|
|
|
|
context.textBaseline = "middle";
|
2023-10-20 17:28:07 +02:00
|
|
|
let msg;
|
|
|
|
if (biomePoints.length === 0) {
|
|
|
|
msg = "No biomes.";
|
|
|
|
} else {
|
|
|
|
msg = "No biomes in this Y layer.";
|
|
|
|
}
|
2023-10-21 11:35:22 +02:00
|
|
|
context.fillText(msg, MIDPOINT, MIDPOINT);
|
2023-10-20 14:03:45 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function rewriteBiomeSelector() {
|
|
|
|
biomeSelector.innerHTML = "";
|
|
|
|
for (let b=0; b<biomePoints.length; b++) {
|
|
|
|
let num = b+1;
|
|
|
|
let newElem = document.createElement("option");
|
|
|
|
newElem.value = num;
|
2023-10-20 20:11:00 +02:00
|
|
|
newElem.id = "biome_list_element_" + biomePoints[b].id;
|
2023-10-20 13:44:37 +02:00
|
|
|
let newElemText = document.createTextNode(biomePoints[b].name);
|
2023-10-20 13:31:23 +02:00
|
|
|
newElem.append(newElemText);
|
|
|
|
biomeSelector.append(newElem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 14:03:45 +02:00
|
|
|
function updateWidgetStates() {
|
|
|
|
if (biomePoints.length === 0 || biomeSelector.selectedIndex === -1) {
|
|
|
|
removeBiomeButton.disabled = "disabled";
|
|
|
|
inputHeat.disabled = "disabled";
|
|
|
|
inputHumidity.disabled = "disabled";
|
2023-10-20 16:12:20 +02:00
|
|
|
inputMinY.disabled = "disabled";
|
|
|
|
inputMaxY.disabled = "disabled";
|
2023-10-20 14:03:45 +02:00
|
|
|
} else {
|
|
|
|
removeBiomeButton.disabled = "";
|
|
|
|
inputHeat.disabled = "";
|
|
|
|
inputHumidity.disabled = "";
|
2023-10-20 16:12:20 +02:00
|
|
|
inputMinY.disabled = "";
|
|
|
|
inputMaxY.disabled = "";
|
2023-10-20 14:18:22 +02:00
|
|
|
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;
|
|
|
|
}
|
2023-10-20 14:03:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 13:31:23 +02:00
|
|
|
biomeSelector.onchange = function() {
|
2023-10-20 17:22:48 +02:00
|
|
|
draw(getViewY(), false);
|
2023-10-20 14:03:45 +02:00
|
|
|
if (biomeSelector.selectedIndex !== -1) {
|
|
|
|
let selected = biomeSelector.options[biomeSelector.selectedIndex];
|
|
|
|
let point = biomePoints[biomeSelector.selectedIndex];
|
|
|
|
inputHeat.value = point.heat;
|
|
|
|
inputHumidity.value = point.humidity;
|
2023-10-20 14:18:22 +02:00
|
|
|
inputMinY.value = point.min_y;
|
|
|
|
inputMaxY.value = point.max_y;
|
2023-10-20 14:03:45 +02:00
|
|
|
}
|
|
|
|
updateWidgetStates();
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 14:18:22 +02:00
|
|
|
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;
|
2023-10-20 17:22:48 +02:00
|
|
|
draw(getViewY(), true);
|
2023-10-20 14:18:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
inputHeat.onchange = function() {
|
|
|
|
onChangeBiomeValueWidget("heat", this.value);
|
|
|
|
}
|
|
|
|
inputHumidity.onchange = function() {
|
|
|
|
onChangeBiomeValueWidget("humidity", this.value);
|
|
|
|
}
|
2023-10-20 16:12:20 +02:00
|
|
|
inputMinY.onchange = function() {
|
|
|
|
onChangeBiomeValueWidget("min_y", this.value);
|
|
|
|
}
|
|
|
|
inputMaxY.onchange = function() {
|
|
|
|
onChangeBiomeValueWidget("max_y", this.value);
|
|
|
|
}
|
2023-10-20 17:22:48 +02:00
|
|
|
inputViewY.onchange = function() {
|
|
|
|
draw(getViewY(), true);
|
|
|
|
}
|
2023-10-20 14:18:22 +02:00
|
|
|
|
2023-10-20 13:31:23 +02:00
|
|
|
addBiomeButton.onclick = function() {
|
|
|
|
let he = Math.round(Math.random()*100);
|
|
|
|
let hu = Math.round(Math.random()*100);
|
2023-10-20 13:44:37 +02:00
|
|
|
let newPoint = {
|
2023-10-20 16:31:11 +02:00
|
|
|
id: lastBiomeID,
|
2023-10-20 13:44:37 +02:00
|
|
|
name: generateBiomeName(he, hu),
|
|
|
|
heat: he,
|
2023-10-20 14:03:45 +02:00
|
|
|
min_y: MIN_Y_DEFAULT,
|
|
|
|
max_y: MAX_Y_DEFAULT,
|
2023-10-20 13:44:37 +02:00
|
|
|
humidity: hu,
|
|
|
|
};
|
|
|
|
biomePoints.push(newPoint);
|
2023-10-20 13:31:23 +02:00
|
|
|
let num = biomePoints.length
|
|
|
|
|
|
|
|
let newElem = document.createElement("option");
|
2023-10-20 17:22:48 +02:00
|
|
|
newElem.id = "biome_list_element_" + lastBiomeID;
|
2023-10-20 13:31:23 +02:00
|
|
|
newElem.value = "" + num;
|
2023-10-20 16:31:11 +02:00
|
|
|
|
2023-10-20 13:44:37 +02:00
|
|
|
let newElemText = document.createTextNode(newPoint.name);
|
2023-10-20 13:31:23 +02:00
|
|
|
newElem.append(newElemText);
|
|
|
|
biomeSelector.append(newElem);
|
|
|
|
newElem.selected = "selected";
|
|
|
|
|
2023-10-20 17:22:48 +02:00
|
|
|
draw(getViewY(), true);
|
2023-10-20 14:03:45 +02:00
|
|
|
updateWidgetStates();
|
2023-10-20 16:31:11 +02:00
|
|
|
|
|
|
|
lastBiomeID++;
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
|
|
|
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";
|
|
|
|
}
|
|
|
|
|
2023-10-20 17:22:48 +02:00
|
|
|
draw(getViewY(), true);
|
2023-10-20 14:03:45 +02:00
|
|
|
updateWidgetStates();
|
2023-10-20 13:31:23 +02:00
|
|
|
}
|
2023-10-20 16:31:11 +02:00
|
|
|
|
2023-10-20 22:03:15 +02:00
|
|
|
function selectPoint(point) {
|
|
|
|
for (let elem of biomeSelector.options) {
|
|
|
|
let strID = elem.id;
|
|
|
|
let elemID = null;
|
|
|
|
if (strID.startsWith("biome_list_element_")) {
|
|
|
|
let slice = strID.slice(19);
|
|
|
|
if (slice) {
|
|
|
|
elemID = +slice;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (elemID !== null) {
|
|
|
|
if (point.id === elemID) {
|
2023-10-21 00:57:25 +02:00
|
|
|
if (elem.selected) {
|
|
|
|
console.log("Already selected!");
|
|
|
|
return [true, true];
|
|
|
|
}
|
2023-10-20 22:03:15 +02:00
|
|
|
elem.selected = "selected";
|
|
|
|
draw(getViewY(), true);
|
|
|
|
updateWidgetStates();
|
2023-10-21 00:57:25 +02:00
|
|
|
return [true, false];
|
2023-10-20 22:03:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-21 00:57:25 +02:00
|
|
|
return [false, false];
|
|
|
|
}
|
|
|
|
function getDistance(x1, y1, x2, y2) {
|
|
|
|
return Math.sqrt((x2 - x1)**2 + (y2 - y1)**2);
|
2023-10-20 22:03:15 +02:00
|
|
|
}
|
2023-10-21 01:55:49 +02:00
|
|
|
function canvasPixelCoordsToBiomeCoords(x, y) {
|
|
|
|
let w = (voronoiCanvas.width/(LIMIT_MAX-LIMIT_MIN));
|
|
|
|
let h = (voronoiCanvas.height/(LIMIT_MAX-LIMIT_MIN));
|
|
|
|
let heat = Math.round((x + LIMIT_MIN * w) / w);
|
|
|
|
let humidity = Math.round((y + LIMIT_MIN * w) / w);
|
|
|
|
return [heat, humidity];
|
|
|
|
}
|
|
|
|
function biomeCoordsToCanvasPixelCoords(heat, humidity) {
|
|
|
|
let w = (voronoiCanvas.width/(LIMIT_MAX-LIMIT_MIN));
|
|
|
|
let h = (voronoiCanvas.height/(LIMIT_MAX-LIMIT_MIN));
|
|
|
|
let pixelX = heat * w - LIMIT_MIN * w;
|
|
|
|
let pixelY = humidity * h - LIMIT_MIN * h;
|
|
|
|
return [pixelX, pixelY];
|
|
|
|
}
|
|
|
|
|
2023-10-20 22:03:15 +02:00
|
|
|
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];
|
2023-10-21 01:55:49 +02:00
|
|
|
let [pixelX, pixelY] = biomeCoordsToCanvasPixelCoords(point.heat, point.humidity);
|
|
|
|
let dist = getDistance(x, y, pixelX, pixelY);
|
2023-10-20 22:03:15 +02:00
|
|
|
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;
|
2023-10-21 00:57:25 +02:00
|
|
|
let dragDropPointID = null;
|
|
|
|
let dragDropPos = null;
|
2023-10-21 01:08:35 +02:00
|
|
|
|
|
|
|
function updatePointWhenDragged(pointID) {
|
|
|
|
if (pointID !== null) {
|
2023-10-21 00:57:25 +02:00
|
|
|
let selectedPoint = null;
|
|
|
|
let points = getRenderedPoints(getViewY());
|
|
|
|
for (let i=0; i<points.length; i++) {
|
|
|
|
if (points[i].id === dragDropPointID) {
|
|
|
|
selectedPoint = points[i];
|
2023-10-21 01:55:49 +02:00
|
|
|
let [newHeat, newHumidity] = canvasPixelCoordsToBiomeCoords(event.offsetX, event.offsetY);
|
2023-10-21 00:57:25 +02:00
|
|
|
selectedPoint.heat = newHeat;
|
|
|
|
selectedPoint.humidity = newHumidity;
|
2023-10-21 01:08:35 +02:00
|
|
|
selectedPoint.name = generateBiomeName(newHeat, newHumidity);
|
2023-10-21 00:57:25 +02:00
|
|
|
draw(getViewY(), true);
|
|
|
|
updateWidgetStates();
|
2023-10-21 01:08:35 +02:00
|
|
|
for (let elem of biomeSelector.options) {
|
|
|
|
let strID = elem.id;
|
|
|
|
let elemID = null;
|
|
|
|
if (strID.startsWith("biome_list_element_")) {
|
|
|
|
let slice = strID.slice(19);
|
|
|
|
if (slice) {
|
|
|
|
elemID = +slice;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (elemID !== null && points[i].id === elemID) {
|
|
|
|
elem.innerText = selectedPoint.name;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
2023-10-21 00:57:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-21 01:08:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
voronoiCanvas.onmousemove = function(event) {
|
2023-10-21 01:48:45 +02:00
|
|
|
// drag-n-drop
|
2023-10-21 01:08:35 +02:00
|
|
|
if (mouseIsDown) {
|
|
|
|
updatePointWhenDragged(dragDropPointID);
|
|
|
|
}
|
2023-10-21 01:48:45 +02:00
|
|
|
// show coordinates
|
2023-10-21 01:55:49 +02:00
|
|
|
let [heat, humidity] = canvasPixelCoordsToBiomeCoords(event.offsetX, event.offsetY);
|
|
|
|
let html = "heat="+heat+"; humidity="+humidity;
|
|
|
|
coordinateDisplay.innerHTML = html;
|
|
|
|
}
|
|
|
|
voronoiCanvas.onmouseenter = function(event) {
|
|
|
|
// show coordinates
|
|
|
|
let [heat, humidity] = canvasPixelCoordsToBiomeCoords(event.offsetX, event.offsetY);
|
2023-10-21 01:48:45 +02:00
|
|
|
let html = "heat="+heat+"; humidity="+humidity;
|
|
|
|
coordinateDisplay.innerHTML = html;
|
2023-10-21 01:08:35 +02:00
|
|
|
}
|
2023-10-21 01:55:49 +02:00
|
|
|
|
2023-10-21 01:08:35 +02:00
|
|
|
voronoiCanvas.onmousedown = function(event) {
|
2023-10-21 01:48:45 +02:00
|
|
|
// select point by clicking.
|
|
|
|
// initiate drag-n-drop if already selected.
|
2023-10-21 01:08:35 +02:00
|
|
|
mouseIsDown = true;
|
|
|
|
let nearest = getNearestPointFromCanvasPos(event.offsetX, event.offsetY, 25);
|
|
|
|
if (nearest !== null) {
|
|
|
|
let success, alreadySelected
|
|
|
|
[success, alreadySelected] = selectPoint(nearest);
|
|
|
|
if (alreadySelected) {
|
|
|
|
dragDropPointID = nearest.id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
voronoiCanvas.onmouseup = function(event) {
|
2023-10-21 01:48:45 +02:00
|
|
|
// end drag-n-drop
|
2023-10-21 01:08:35 +02:00
|
|
|
updatePointWhenDragged(dragDropPointID);
|
|
|
|
mouseIsDown = false;
|
2023-10-21 00:57:25 +02:00
|
|
|
dragDropPos = null;
|
|
|
|
dragDropPointID = null;
|
2023-10-20 22:03:15 +02:00
|
|
|
}
|
|
|
|
voronoiCanvas.onmouseleave = function() {
|
2023-10-21 01:48:45 +02:00
|
|
|
// end drag-n-drop
|
2023-10-20 22:03:15 +02:00
|
|
|
mouseIsDown = false;
|
2023-10-21 00:57:25 +02:00
|
|
|
dragDropPointID = null;
|
|
|
|
dragDropPos = null;
|
2023-10-21 01:48:45 +02:00
|
|
|
coordinateDisplay.innerHTML = " ";
|
2023-10-20 22:03:15 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 16:31:11 +02:00
|
|
|
|
|
|
|
window.addEventListener("load", drawInit);
|
2023-10-20 17:22:48 +02:00
|
|
|
window.addEventListener("load", function() {
|
|
|
|
draw(getViewY(), true);
|
|
|
|
})
|
2023-10-20 16:31:11 +02:00
|
|
|
window.addEventListener("load", rewriteBiomeSelector);
|
|
|
|
window.addEventListener("load", updateWidgetStates);
|