2023-10-20 02:54:39 +02:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
|
|
<title>Minetest Biome Point Visualizer</title>
|
|
|
|
<style>
|
|
|
|
canvas {
|
|
|
|
border: 1px solid black;
|
|
|
|
}
|
|
|
|
</style>
|
2023-10-20 10:43:04 +02:00
|
|
|
<!-- Voronoi diagram API -->
|
2023-10-20 02:54:39 +02:00
|
|
|
<script src="./rhill-voronoi-core.js"></script>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Minetest Biome Point Visualizer</h1>
|
2023-10-20 10:43:04 +02:00
|
|
|
<div>
|
|
|
|
<!-- TODO: Add proper canvas browser fallback by listing the voronoi points -->
|
2023-10-20 12:34:37 +02:00
|
|
|
<canvas id="voronoiCanvas" width="500" height="500">
|
2023-10-20 02:54:39 +02:00
|
|
|
A voronoi diagram is supposed to be here but for some reason it cannot be displayed.
|
|
|
|
</canvas>
|
2023-10-20 10:43:04 +02:00
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<form id="biomeForm">
|
2023-10-20 11:24:38 +02:00
|
|
|
<label for="biomeSelector">Biomes:<br>
|
2023-10-20 12:34:37 +02:00
|
|
|
<select id="biomeSelector" name="biomeList" size="8"></select>
|
2023-10-20 10:43:04 +02:00
|
|
|
</label>
|
|
|
|
<br>
|
2023-10-20 11:24:38 +02:00
|
|
|
<button id="addBiomeButton" type="button">Add</button>
|
|
|
|
<button id="removeBiomeButton" type="button">Remove</button>
|
2023-10-20 10:43:04 +02:00
|
|
|
<br>
|
2023-10-20 11:24:38 +02:00
|
|
|
<div id="biomeEditElements">
|
|
|
|
<label for="inputHeat">Heat:
|
|
|
|
<input id="inputHeat" type="number" value="50">
|
2023-10-20 10:43:04 +02:00
|
|
|
</label>
|
2023-10-20 11:24:38 +02:00
|
|
|
<label for="inputHumidity">Humidity:
|
|
|
|
<input id="inputHumidity" type="number" value="50">
|
2023-10-20 10:43:04 +02:00
|
|
|
</label>
|
|
|
|
|
|
|
|
<br>
|
|
|
|
|
2023-10-20 11:24:38 +02:00
|
|
|
<label for="minY">Min. Y:
|
|
|
|
<input id="minY" type="number" value="-31000">
|
2023-10-20 10:43:04 +02:00
|
|
|
</label>
|
2023-10-20 11:24:38 +02:00
|
|
|
<label for="maxY">Max. Y:
|
|
|
|
<input id="maxY" type="number" value="31000">
|
2023-10-20 10:43:04 +02:00
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
<div>
|
|
|
|
|
|
|
|
|
2023-10-20 02:54:39 +02:00
|
|
|
<script>
|
|
|
|
"use strict";
|
|
|
|
let biomePoints = [
|
2023-10-20 12:34:37 +02:00
|
|
|
{heat:50, humidity:50},
|
2023-10-20 02:54:39 +02:00
|
|
|
];
|
2023-10-20 10:43:04 +02:00
|
|
|
|
|
|
|
// FOREST-16 palette, plus black
|
|
|
|
const pointColor = "#913636";
|
2023-10-20 13:05:22 +02:00
|
|
|
const pointColorSelected = "#e19696";
|
2023-10-20 10:43:04 +02:00
|
|
|
const edgeColor = "#0f2c2e";
|
|
|
|
const gridColor = "#00000040";
|
|
|
|
const axisColor = "#000000";
|
|
|
|
const cellColors = [
|
|
|
|
"#64988e",
|
|
|
|
"#3d7085",
|
|
|
|
"#345644",
|
|
|
|
"#6b7f5c",
|
|
|
|
"#b0b17c",
|
|
|
|
"#e1c584",
|
|
|
|
"#c89660",
|
|
|
|
"#ad5f52",
|
|
|
|
"#692f11",
|
|
|
|
"#89542f",
|
|
|
|
"#796e63",
|
|
|
|
"#a17d5e",
|
|
|
|
"#b4a18f",
|
|
|
|
"#ecddba",
|
|
|
|
];
|
|
|
|
|
|
|
|
// define an array and randomize it
|
|
|
|
function shuffleArray(values) {
|
|
|
|
let index = values.length, randomIndex;
|
|
|
|
// While there remain elements to shuffle
|
|
|
|
while (index != 0) {
|
|
|
|
// Pick a remaining element.
|
|
|
|
randomIndex = Math.floor(Math.random() * index);
|
|
|
|
index--; // And swap it with the current element.
|
|
|
|
[values[index], values[randomIndex]] = [values[randomIndex], values[index]];
|
|
|
|
}
|
|
|
|
return values;
|
|
|
|
}
|
2023-10-20 02:54:39 +02:00
|
|
|
|
|
|
|
function biomePointToVoronoiPoint(point) {
|
|
|
|
let newPoint = { x: point.heat, y: point.humidity }
|
|
|
|
return newPoint;
|
|
|
|
}
|
|
|
|
function voronoiPointToBiomePoint(point) {
|
|
|
|
let newPoint = { heat: point.x, humidity: point.y }
|
|
|
|
return newPoint;
|
|
|
|
}
|
|
|
|
|
|
|
|
function putPoint(context, point) {
|
2023-10-20 13:05:22 +02:00
|
|
|
context.beginPath();
|
2023-10-20 02:54:39 +02:00
|
|
|
context.moveTo(0, 0);
|
|
|
|
context.arc(point.heat, point.humidity, 2, 0, Math.PI * 2);
|
|
|
|
context.fill();
|
2023-10-20 13:05:22 +02:00
|
|
|
context.closePath();
|
2023-10-20 02:54:39 +02:00
|
|
|
};
|
|
|
|
|
2023-10-20 11:24:38 +02:00
|
|
|
const LIMIT_MIN = -37.5
|
|
|
|
const LIMIT_MAX = 137.5
|
2023-10-20 12:34:37 +02:00
|
|
|
const DRAW_MIN = LIMIT_MIN - 10
|
|
|
|
const DRAW_MAX = LIMIT_MAX + 10
|
2023-10-20 02:54:39 +02:00
|
|
|
const GRID_STEP = 10
|
|
|
|
|
|
|
|
function putGrid(context) {
|
|
|
|
context.lineWidth = 0.5;
|
2023-10-20 10:43:04 +02:00
|
|
|
context.strokeStyle = gridColor;
|
2023-10-20 11:24:38 +02:00
|
|
|
for (let x=0; x<=LIMIT_MAX; x+=GRID_STEP) {
|
2023-10-20 02:54:39 +02:00
|
|
|
context.beginPath();
|
2023-10-20 11:24:38 +02:00
|
|
|
context.moveTo(x, LIMIT_MIN);
|
|
|
|
context.lineTo(x, LIMIT_MAX);
|
2023-10-20 02:54:39 +02:00
|
|
|
context.stroke();
|
|
|
|
}
|
2023-10-20 11:24:38 +02:00
|
|
|
for (let x=-GRID_STEP; x>=LIMIT_MIN; x-=GRID_STEP) {
|
2023-10-20 02:54:39 +02:00
|
|
|
context.beginPath();
|
2023-10-20 11:24:38 +02:00
|
|
|
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);
|
2023-10-20 02:54:39 +02:00
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function putAxes(context) {
|
|
|
|
context.lineWidth = 0.75;
|
|
|
|
// heat axis (horizontal)
|
|
|
|
context.beginPath();
|
2023-10-20 11:24:38 +02:00
|
|
|
context.moveTo(LIMIT_MIN,0)
|
|
|
|
context.lineTo(LIMIT_MAX,0)
|
2023-10-20 10:43:04 +02:00
|
|
|
context.closePath();
|
2023-10-20 02:54:39 +02:00
|
|
|
context.stroke();
|
|
|
|
|
|
|
|
// humidity axis ()
|
|
|
|
context.beginPath();
|
2023-10-20 11:24:38 +02:00
|
|
|
context.moveTo(0,LIMIT_MIN)
|
|
|
|
context.lineTo(0,LIMIT_MAX)
|
2023-10-20 10:43:04 +02:00
|
|
|
context.closePath();
|
2023-10-20 02:54:39 +02:00
|
|
|
context.stroke();
|
|
|
|
}
|
|
|
|
|
2023-10-20 13:27:31 +02:00
|
|
|
// Cache diagram object for performance boost
|
|
|
|
let cachedVoronoiDiagram = null;
|
2023-10-20 13:05:22 +02:00
|
|
|
|
2023-10-20 13:27:31 +02:00
|
|
|
function getVoronoiDiagram(recalculate) {
|
|
|
|
if ((cachedVoronoiDiagram === null) || recalculate) {
|
|
|
|
let pad = 10;
|
|
|
|
let vbbox = {xl: LIMIT_MIN-pad, xr: LIMIT_MAX+pad, yt: LIMIT_MIN-pad, yb: LIMIT_MAX+pad};
|
|
|
|
let sites = []
|
|
|
|
for (let p of biomePoints) {
|
|
|
|
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;
|
2023-10-20 02:54:39 +02:00
|
|
|
}
|
2023-10-20 12:34:37 +02:00
|
|
|
}
|
2023-10-20 10:43:04 +02:00
|
|
|
|
2023-10-20 13:05:22 +02:00
|
|
|
function getDrawContext() {
|
|
|
|
let canvas = document.getElementById("voronoiCanvas");
|
|
|
|
// TODO: Check for getContext support of browser
|
|
|
|
return canvas.getContext("2d");
|
|
|
|
}
|
|
|
|
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 13:27:31 +02:00
|
|
|
function draw(recalculate) {
|
2023-10-20 12:34:37 +02:00
|
|
|
let context = getDrawContext();
|
|
|
|
context.fillStyle = "#FFFFFF";
|
|
|
|
context.fillRect(DRAW_MIN, DRAW_MIN, DRAW_MAX-DRAW_MIN, DRAW_MAX-DRAW_MIN);
|
2023-10-20 13:27:31 +02:00
|
|
|
let diagram = getVoronoiDiagram(recalculate);
|
2023-10-20 12:34:37 +02:00
|
|
|
//let colors = shuffleArray(cellColors);
|
|
|
|
let colors = cellColors;
|
2023-10-20 10:43:04 +02:00
|
|
|
for (let c=0; c<diagram.cells.length; c++) {
|
|
|
|
let cell = diagram.cells[c]
|
2023-10-20 12:34:37 +02:00
|
|
|
let ccol = c % colors.length;
|
|
|
|
context.fillStyle = colors[ccol];
|
2023-10-20 10:43:04 +02:00
|
|
|
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 12:34:37 +02:00
|
|
|
if (biomePoints.length === 1 && diagram.cells.length === 1) {
|
|
|
|
context.fillStyle = colors[0];
|
|
|
|
context.fillRect(DRAW_MIN, DRAW_MIN, DRAW_MAX-DRAW_MIN, DRAW_MAX-DRAW_MIN);
|
|
|
|
}
|
2023-10-20 10:43:04 +02:00
|
|
|
|
2023-10-20 11:24:38 +02:00
|
|
|
//putAxes(context);
|
2023-10-20 10:43:04 +02:00
|
|
|
putGrid(context);
|
|
|
|
|
|
|
|
context.strokeStyle = edgeColor;
|
2023-10-20 13:05:22 +02:00
|
|
|
context.lineWidth = 2;
|
2023-10-20 02:54:39 +02:00
|
|
|
for (let e=0; e<diagram.edges.length; e++) {
|
|
|
|
let edge = diagram.edges[e];
|
|
|
|
context.beginPath();
|
|
|
|
context.moveTo(edge.va.x, edge.va.y);
|
|
|
|
context.lineTo(edge.vb.x, edge.vb.y);
|
2023-10-20 10:43:04 +02:00
|
|
|
context.closePath();
|
2023-10-20 02:54:39 +02:00
|
|
|
context.stroke();
|
|
|
|
}
|
2023-10-20 10:43:04 +02:00
|
|
|
|
2023-10-20 13:05:22 +02:00
|
|
|
for (let p=0; p<biomePoints.length; p++) {
|
|
|
|
if (p === biomeSelector.selectedIndex) {
|
|
|
|
context.fillStyle = pointColorSelected;
|
|
|
|
} else {
|
|
|
|
context.fillStyle = pointColor;
|
|
|
|
}
|
|
|
|
putPoint(context, biomePoints[p]);
|
2023-10-20 10:43:04 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 02:54:39 +02:00
|
|
|
}
|
2023-10-20 12:34:37 +02:00
|
|
|
window.addEventListener("load", drawInit);
|
2023-10-20 02:54:39 +02:00
|
|
|
window.addEventListener("load", draw);
|
2023-10-20 12:34:37 +02:00
|
|
|
window.addEventListener("load", rewriteBiomeSelector);
|
|
|
|
|
|
|
|
function rewriteBiomeSelector() {
|
|
|
|
biomeSelector.innerHTML = "";
|
|
|
|
for (let b=0; b<biomePoints.length; b++) {
|
|
|
|
let num = b+1;
|
|
|
|
let newElem = document.createElement("option");
|
|
|
|
newElem.value = num;
|
|
|
|
let newElemText = document.createTextNode("Biome #"+num);
|
|
|
|
newElem.append(newElemText);
|
|
|
|
biomeSelector.append(newElem);
|
|
|
|
}
|
|
|
|
}
|
2023-10-20 11:24:38 +02:00
|
|
|
|
2023-10-20 13:05:22 +02:00
|
|
|
biomeSelector.onchange = function() {
|
2023-10-20 13:27:31 +02:00
|
|
|
draw(false);
|
2023-10-20 13:05:22 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 11:24:38 +02:00
|
|
|
addBiomeButton.onclick = function() {
|
2023-10-20 12:34:37 +02:00
|
|
|
let he = Math.round(Math.random()*100);
|
|
|
|
let hu = Math.round(Math.random()*100);
|
|
|
|
biomePoints.push({heat:he, humidity:hu});
|
|
|
|
let num = biomePoints.length
|
|
|
|
|
|
|
|
let newElem = document.createElement("option");
|
|
|
|
newElem.value = "" + num;
|
|
|
|
let newElemText = document.createTextNode("Biome #"+num);
|
|
|
|
newElem.append(newElemText);
|
|
|
|
biomeSelector.append(newElem);
|
|
|
|
newElem.selected = "selected";
|
|
|
|
|
2023-10-20 13:27:31 +02:00
|
|
|
draw(true);
|
2023-10-20 11:24:38 +02:00
|
|
|
}
|
|
|
|
removeBiomeButton.onclick = function() {
|
2023-10-20 12:34:37 +02:00
|
|
|
if (biomeSelector.selectedOptions.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-20 13:05:22 +02:00
|
|
|
let firstIndex = null;
|
2023-10-20 12:34:37 +02:00
|
|
|
for (let o=0; o<biomeSelector.selectedOptions.length; o++) {
|
|
|
|
let opt = biomeSelector.selectedOptions[o]
|
|
|
|
let index = opt.index
|
2023-10-20 13:05:22 +02:00
|
|
|
if (firstIndex === null) {
|
2023-10-20 12:34:37 +02:00
|
|
|
firstIndex = index;
|
|
|
|
}
|
|
|
|
biomePoints.splice(index, 1);
|
|
|
|
opt.remove();
|
|
|
|
}
|
2023-10-20 13:05:22 +02:00
|
|
|
if (firstIndex !== null && biomePoints.length > 0) {
|
|
|
|
let newIndex = firstIndex-1;
|
|
|
|
if (newIndex < 0) {
|
|
|
|
newIndex = 0;
|
|
|
|
}
|
2023-10-20 12:34:37 +02:00
|
|
|
biomeSelector.options[newIndex].selected = "selected";
|
|
|
|
}
|
|
|
|
|
2023-10-20 13:27:31 +02:00
|
|
|
draw(true);
|
2023-10-20 11:24:38 +02:00
|
|
|
}
|
|
|
|
|
2023-10-20 02:54:39 +02:00
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|