"use strict"; // Min. and max. possible coordinates of biomes const MIN_X = -31000 const MAX_X = 31000 const MIN_Y = -31000 const MAX_Y = 31000 const MIN_Z = -31000 const MAX_Z = 31000 // Draw a grid line every GRID_STEP units const GRID_STEP = 10 const GRID_STEP_V6 = 0.1 // Size of the resizing corner const RESIZE_CORNER = 14; // Minimum canvas side length (px) const MIN_CANVAS_SIZE = 100; // Minimum required distance of canvas from the right page side const CANVAS_PAGE_MARGIN_RIGHT = 20 // Minimum and maximum value for heat and humidity const MIN_HEAT_HUMIDITY_VALUE = -1e6 const MAX_HEAT_HUMIDITY_VALUE = 1e6 // Grid widths. We use lower grid widths // as the grid becomes more crammed. // There are 4 levels from 0 to 3. // Level 3 is the full width, level 1 is the // smallest with and level 0 renders no grid lines // when the grid would become too crammed. const GRID_WIDTH_LEVEL_3 = 2; // full width const GRID_WIDTH_LEVEL_2 = 1; // reduced width const GRID_WIDTH_LEVEL_1 = 0.5; // more reduced width // the grid thesholds are the "grid lines to pixel" // ratio. The more grid lines therere are per pixel, // the lower the grid level. // e.g. grid level 2 triggers if ratio is above // GRID_THRESHOLD_LEVEL_2. Grid level 3 is used // if no grid thresholds are triggered. const GRID_THRESHOLD_LEVEL_2 = 0.08; const GRID_THRESHOLD_LEVEL_1 = 0.16; const GRID_THRESHOLD_LEVEL_0 = 0.24; // this will disable grid rendering // Distance from the point's center in which a point can // be selected by clicking on it const POINT_SELECT_DISTANCE = 25 // Distance a point has to be dragged after a single click // before drag-n-drop starts. // Normally, the user can drag-n-drop a point // when clickong and dragging it *after* it has already been // selected; there's no distance requirement. // But it is also possible to select the point (by clicking // on it) and dragging it with the same mouse click. But for // this method, the mouse had to move the distance below // from the point coordinates (in pixels) before drag-n-drop // activates. const ONE_CLICK_DRAG_DROP_DISTANCE = 30 // Name to display if empty const FALLBACK_NAME = "(no name)" // Name of the default / initial biome const DEFAULT_BIOME_NAME = "default" // Default heat and humidity values const DEFAULT_HEAT = 50 const DEFAULT_HUMIDITY = 50 // Symbol for storing the biome ID in site objects // for the Voronoi script const biomeIDSymbol = Symbol("Biome ID"); // Colors (mostly self-explanatory) const POINT_COLOR = "#913636"; const POINT_COLOR_SELECTED = "#e19696"; const EDGE_COLOR = "#0f2c2e"; const GRID_COLOR = "#00000040"; const AXIS_COLOR = "#000000"; // Color to be used when the diagram is cleared const CLEAR_COLOR = "#ecddba"; // list of possible cell colors // note: These MUST be in "#xxxxxx" format // for the hexColorToRGBColor function to work. const CELL_COLORS = [ "#64988e", "#3d7085", "#345644", "#6b7f5c", "#868750", "#a7822c", "#a06e38", "#ad5f52", "#692f11", "#89542f", "#796e63", "#a17d5e", "#5a3f20", "#836299", ]; const CELL_COLOR_NEUTRAL = "#888888"; // Mapgen v6 "cell" colors const CELL_COLOR_V6_NORMAL = "#6b7f5c"; const CELL_COLOR_V6_JUNGLE = "#345644"; const CELL_COLOR_V6_DESERT = "#a7822c"; const CELL_COLOR_V6_TAIGA = "#548583"; const CELL_COLOR_V6_TUNDRA = "#6371b4"; // Mapgen v6 biome thresholds const MGV6_FREQ_HOT = 0.4 const MGV6_FREQ_SNOW = -0.4 const MGV6_FREQ_TAIGA = 0.5 const MGV6_FREQ_JUNGLE = 0.5 // For mgv6_freq_desert setting const MGV6_FREQ_DESERT_DEFAULT = 0.45 /* Status variables for the diagram calculations */ // The current biome mode; which type of biome system // the program will use. // * "modern": The modern biome system, as used by mapgen v7 and many more // * "v6": The biome system of mapgen v6 let biomeMode = "modern"; // Min. and max. mathematically possible values for heat and humidity let limit_heat_min, limit_heat_max; let limit_humidity_min, limit_humidity_max; // Draw area. Slightly larger than the value area to avoid // ugly edge rendering problems const DRAW_OFFSET = 10 let draw_heat_min, draw_heat_max; let draw_humidity_min, draw_humidity_max; // The point that is considered the middle of heat/humidity; // mathematically this value is the most probable. let midpoint_heat; let midpoint_humidity; // XYZ at which the diagram is currently viewed at let viewX = 0; let viewY = 0; let viewZ = 0; let v6_flag_snowbiomes = true; let v6_flag_jungles = true; let v6_freq_desert = 0.45; // Biome noise settings const NOISE_OFFSET_DEFAULT = 50; const NOISE_SCALE_DEFAULT = 50; const NOISE_PERSISTENCE_DEFAULT = 0.5; const NOISE_OCTAVES_DEFAULT = 3; const NOISE_ABSVALUE_DEFAULT = false; const NOISE_V6_HEAT_OFFSET_DEFAULT = 0; const NOISE_V6_HEAT_SCALE_DEFAULT = 1; const NOISE_V6_HEAT_PERSISTENCE_DEFAULT = 0.5; const NOISE_V6_HEAT_OCTAVES_DEFAULT = 3; const NOISE_V6_HEAT_ABSVALUE_DEFAULT = false; const NOISE_V6_HUMIDITY_OFFSET_DEFAULT = 0.5; const NOISE_V6_HUMIDITY_SCALE_DEFAULT = 0.5; const NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT = 0.5; const NOISE_V6_HUMIDITY_OCTAVES_DEFAULT = 3; const NOISE_V6_HUMIDITY_ABSVALUE_DEFAULT = false; // Current noise values let noises = { heat_modern: { offset: NOISE_OFFSET_DEFAULT, scale: NOISE_SCALE_DEFAULT, octaves: NOISE_OCTAVES_DEFAULT, persistence: NOISE_PERSISTENCE_DEFAULT, absvalue: NOISE_ABSVALUE_DEFAULT, }, humidity_modern: { offset: NOISE_OFFSET_DEFAULT, scale: NOISE_SCALE_DEFAULT, octaves: NOISE_OCTAVES_DEFAULT, persistence: NOISE_PERSISTENCE_DEFAULT, absvalue: NOISE_ABSVALUE_DEFAULT, }, heat_v6: { offset: NOISE_V6_HEAT_OFFSET_DEFAULT, scale: NOISE_V6_HEAT_SCALE_DEFAULT, octaves: NOISE_V6_HEAT_OCTAVES_DEFAULT, persistence: NOISE_V6_HEAT_PERSISTENCE_DEFAULT, absvalue: NOISE_V6_HEAT_ABSVALUE_DEFAULT, }, humidity_v6: { offset: NOISE_V6_HUMIDITY_OFFSET_DEFAULT, scale: NOISE_V6_HUMIDITY_SCALE_DEFAULT, octaves: NOISE_V6_HUMIDITY_OCTAVES_DEFAULT, persistence: NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT, absvalue: NOISE_V6_HUMIDITY_ABSVALUE_DEFAULT, }, }; noises.heat = noises.heat_modern; noises.humidity = noises.humidity_modern; function updateAreaVarsFor(noiseType) { let noise = noises[noiseType]; let is_absolute = noise.absvalue === true // Calculate min. and max. possible values // Octaves let [o_min, o_max] = [0, 0] for (let o=1; o<=noise.octaves; o++) { let exp = o-1 // Calculate the two possible extreme values // with the octave value being either at 1 or -1. let limit1 = (1 * noise.persistence ** exp) let limit2 if (!is_absolute) { limit2 = (-1 * noise.persistence ** exp) } else { // If absvalue is set, one of the // limits is always 0 because we // can't get lower. limit2 = 0 } // To add to the maximum, pick the higher value if (limit1 > limit2) { o_max = o_max + limit1 } else { o_max = o_max + limit2 } // To add to the minimum, pick the LOWER value if (limit1 > limit2) { o_min = o_min + limit2 } else { o_min = o_min + limit1 } } // Add offset and scale to min/max value (final step) let min_value = noise.offset + noise.scale * o_min let max_value = noise.offset + noise.scale * o_max // Bring the 2 values in the correct order // (min_value might be bigger for negative scale) if (min_value > max_value) { [min_value, max_value] = [max_value, min_value] } // v6 humidity is forcefully clamped at 0 and 1 if (biomeMode === "v6" && noiseType == "humidity") { min_value = Math.max(0.0, min_value); max_value = Math.min(1.0, max_value); } // Update globals let limit_min = min_value; let limit_max = max_value; let draw_min = limit_min - DRAW_OFFSET let draw_max = limit_max + DRAW_OFFSET let midpoint = min_value + (max_value - min_value) / 2 if (noiseType === "heat") { limit_heat_min = limit_min; limit_heat_max = limit_max; draw_heat_min = draw_min; draw_heat_max = draw_max; midpoint_heat = midpoint; } else if (noiseType === "humidity") { limit_humidity_min = limit_min; limit_humidity_max = limit_max; draw_humidity_min = draw_min; draw_humidity_max = draw_max; midpoint_humidity = midpoint; } else { console.error("updateAreaVars() called with wrong noise_type!") } } function updateAreaVars() { updateAreaVarsFor("heat"); updateAreaVarsFor("humidity"); // Update element rangeDisplay.innerHTML = "heat range: ["+(+limit_heat_min)+", "+(+limit_heat_max)+"]; " + "humidity range: ["+(+limit_humidity_min)+", "+(+limit_humidity_max)+"]"; } updateAreaVars(); // If true, point names are shown in diagram let showNames = true; // If true, points are shown in diagram let showPoints = true; // If true, cells are colorized in diagram let showCellColors = true; // If true, show the grid in the diagram let showGrid = true; // If true, show the heat/humidity axes let showAxes = false; // Set to true if the draw canvas currently shows an error message let drawError = false; // Current cursor position on the canvas let canvas_cursor_x = null; let canvas_cursor_y = null; // The last ID assigned to a biome (0 = none assigned yet). let lastBiomeID = 0; let biomePoints = []; // Add a biome to the biome list, does not update widgets. // Returns a reference to the biome that was actually added. function addBiomeRaw(biomeDef) { biomeDef.id = lastBiomeID; biomeDef.colorIndex = lastBiomeID % CELL_COLORS.length; biomePoints.push(biomeDef); // The biome ID is just a simple ascending number lastBiomeID++; return biomeDef; } // Add a default biome at the midpoint addBiomeRaw({name: DEFAULT_BIOME_NAME, heat: midpoint_heat, humidity: midpoint_humidity, min_x: MIN_X, max_x: MAX_X, min_y: MIN_Y, max_y: MAX_Y, min_z: MIN_Z, max_z: MAX_Z}); // Add a new random biome to the biome list with the given biome definition. // Then select it and update widgets function addBiome(biomeDef) { let he = Math.round(limit_heat_min + Math.random() * (limit_heat_max - limit_heat_min)); let hu = Math.round(limit_humidity_min + Math.random() * (limit_humidity_max - limit_humidity_min)); let newBiome = addBiomeRaw(biomeDef); let newElem = document.createElement("option"); newElem.id = "biome_list_element_" + newBiome.id; let newElemText = document.createTextNode(newBiome.name); newElem.append(newElemText); biomeSelector.append(newElem); newElem.selected = "selected"; updateWidgetStates(); draw(true); } // Get the X, Y or Z value of the viewed coordinate function getViewCoord(axis) { if (axis === "x") { return viewX; } else if (axis === "y") { return viewY; } else if (axis === "z") { return viewZ; } } // Set the X, Y or Z value of the viewed coordinate function setViewCoord(axis, value) { if (axis === "x") { viewX = value; } else if (axis === "y") { viewY = value; } else if (axis === "z") { viewZ = value; } } // Returns the biome point by its given ID // or null if it couldn't be found function getBiomeByID(id) { for(let b=0; b0; c-=4) { context.moveTo(voronoiCanvas.width, voronoiCanvas.height - c); context.lineTo(voronoiCanvas.width - c, voronoiCanvas.height); } context.strokeStyle = "#00000080"; context.closePath(); context.stroke(); } } /* Put the name of the given point on the draw context */ function putPointName(context, point) { let he = point.heat let hu = point.humidity if (he < limit_heat_min || he > limit_heat_max || hu < limit_humidity_min || hu > limit_humidity_max) { return; } let [x, y] = biomeCoordsToCanvasPixelCoords(he, hu); let w = voronoiCanvas.width; let h = voronoiCanvas.height; if (x > w/2) { context.textAlign = "right"; x = x-5; } else { context.textAlign = "left"; x = x+5; } if (y < h/2) { context.textBaseline = "top"; } else { context.textBaseline = "alphabetic"; } context.font = "120% sans-serif" let displayName = point.name; if (displayName === "") { displayName = FALLBACK_NAME; } context.fillText(displayName, x, y); } /* Put the given point on the draw context */ function putPoint(context, point) { const ARROW_SIZE_SIDE = 7; const ARROW_SIZE_CORNER = 9; let he = point.heat let hu = point.humidity let [x, y] = biomeCoordsToCanvasPixelCoords(he, hu); let w = voronoiCanvas.width; let h = voronoiCanvas.height; let [limit_x_min, limit_y_min] = biomeCoordsToCanvasPixelCoords(limit_heat_min, limit_humidity_min); let [limit_x_max, limit_y_max] = biomeCoordsToCanvasPixelCoords(limit_heat_max, limit_humidity_max); // Point is out of bounds: Draw an arrow at the border if (he < limit_heat_min || he > limit_heat_max || hu < limit_humidity_min || hu > limit_humidity_max) { context.beginPath(); // bottom left corner if (he < limit_heat_min && hu < limit_humidity_min) { context.moveTo(limit_x_min, limit_y_min); context.lineTo(limit_x_min + ARROW_SIZE_CORNER, limit_y_min); context.lineTo(limit_x_min, limit_y_min - ARROW_SIZE_CORNER); context.closePath(); context.fill(); // bottom right corner } else if (he > limit_heat_max && hu < limit_humidity_min) { context.moveTo(limit_x_max, limit_y_min); context.lineTo(limit_x_max - ARROW_SIZE_CORNER, limit_y_min); context.lineTo(limit_x_max, limit_y_min - ARROW_SIZE_CORNER); context.closePath(); context.fill(); // top left corner } else if (he < limit_heat_min && hu > limit_humidity_max) { context.moveTo(limit_x_min, limit_y_max); context.lineTo(limit_x_min + ARROW_SIZE_CORNER, limit_y_max); context.lineTo(limit_x_min, limit_y_max + ARROW_SIZE_CORNER); context.closePath(); context.fill(); // top right corner } else if (he > limit_heat_max && hu > limit_humidity_max) { context.moveTo(limit_x_max, limit_y_max); context.lineTo(limit_x_max - ARROW_SIZE_CORNER, limit_y_max); context.lineTo(limit_x_max, limit_y_max + ARROW_SIZE_CORNER); context.closePath(); context.fill(); // left side } else if (he < limit_heat_min) { context.moveTo(limit_x_min, y); context.lineTo(limit_x_min + ARROW_SIZE_SIDE, y + ARROW_SIZE_SIDE); context.lineTo(limit_x_min + ARROW_SIZE_SIDE, y - ARROW_SIZE_SIDE); context.closePath(); context.fill(); // right side } else if (he > limit_heat_max) { context.moveTo(limit_x_max, y); context.lineTo(limit_x_max - ARROW_SIZE_SIDE, y + ARROW_SIZE_SIDE); context.lineTo(limit_x_max - ARROW_SIZE_SIDE, y - ARROW_SIZE_SIDE); context.closePath(); context.fill(); // bottom side } else if (hu < limit_humidity_min) { context.moveTo(x, limit_y_min); context.lineTo(x - ARROW_SIZE_SIDE, limit_y_min - ARROW_SIZE_SIDE); context.lineTo(x + ARROW_SIZE_SIDE, limit_y_min - ARROW_SIZE_SIDE); context.closePath(); context.fill(); // top side } else if (hu > limit_humidity_max) { context.moveTo(x, limit_y_max); context.lineTo(x - ARROW_SIZE_SIDE, limit_y_max + ARROW_SIZE_SIDE); context.lineTo(x + ARROW_SIZE_SIDE, limit_y_max + ARROW_SIZE_SIDE); context.closePath(); context.fill(); } // Point is in bounds: Draw a dot } else { context.beginPath(); context.moveTo(0, 0); context.arc(x, y, 5, 0, Math.PI * 2); context.closePath(); context.fill(); } }; /* Put the grid on the draw context */ function putGrid(context, gridStep) { let [limit_x_min, limit_y_min] = biomeCoordsToCanvasPixelCoords(limit_heat_min, limit_humidity_min); let [limit_x_max, limit_y_max] = biomeCoordsToCanvasPixelCoords(limit_heat_max, limit_humidity_max); limit_x_min = Math.max(0, limit_x_min); limit_x_max = Math.min(voronoiCanvas.width, limit_x_max); limit_y_min = Math.max(0, limit_y_min); limit_y_max = Math.min(voronoiCanvas.height, limit_y_max); // Calculate the "grid lines pixel ratio" to reduce the // width of grid lines or even disable rendering them. // A high ratio means that a LOT of grid lines would render // on the canvas. // This code will effectively trigger if the value range // is very high. let xGridLinesPerPixel = (limit_heat_max-limit_heat_min) / voronoiCanvas.width / gridStep; let yGridLinesPerPixel = (limit_humidity_max-limit_humidity_min) / voronoiCanvas.height/ gridStep; let xWidth = GRID_WIDTH_LEVEL_3; let yWidth = GRID_WIDTH_LEVEL_3; if (xGridLinesPerPixel > GRID_THRESHOLD_LEVEL_0) { xWidth = null; } else if (xGridLinesPerPixel > GRID_THRESHOLD_LEVEL_1) { xWidth = GRID_WIDTH_LEVEL_1; } else if (xGridLinesPerPixel > GRID_THRESHOLD_LEVEL_2) { xWidth = GRID_WIDTH_LEVEL_2; } if (yGridLinesPerPixel > GRID_THRESHOLD_LEVEL_0) { yWidth = null; } else if (yGridLinesPerPixel > GRID_THRESHOLD_LEVEL_1) { yWidth = GRID_WIDTH_LEVEL_1; } else if (yGridLinesPerPixel > GRID_THRESHOLD_LEVEL_2) { yWidth = GRID_WIDTH_LEVEL_2; } context.strokeStyle = GRID_COLOR; // Fallback variable to break the loop if it draws way too many lines let steps; if (xWidth !== null) { context.lineWidth = xWidth; context.beginPath(); let x = -xWidth*2; let [heat, _] = canvasPixelCoordsToBiomeCoords(x, 0); heat = heat - (heat % gridStep); steps = 0; while (x < voronoiCanvas.width + xWidth*2) { [x, _] = biomeCoordsToCanvasPixelCoords(heat, 0); context.moveTo(x, limit_y_min); context.lineTo(x, limit_y_max); heat += gridStep; steps++; if (steps > 10000) { console.error("Over 10000 grid lines on the X axis!"); break; } } context.closePath(); context.stroke(); } if (yWidth !== null) { context.lineWidth = yWidth; context.beginPath(); let y = -yWidth*2; let [_, humidity] = canvasPixelCoordsToBiomeCoords(0, y); humidity = humidity - (humidity % gridStep); steps = 0; while (y < voronoiCanvas.height + yWidth*2) { [_, y] = biomeCoordsToCanvasPixelCoords(0, humidity); context.moveTo(limit_x_min, y); context.lineTo(limit_x_max, y); humidity -= gridStep; steps++; if (steps > 10000) { console.error("Over 10000 grid lines on the Y axis!"); break; } } context.closePath(); context.stroke(); } } /* Put the labelled heat/humidity axes on the draw context */ function putAxes(context) { // Size of arrows (px) const AXIS_ARROW_SIZE = 8; // Offset that arrows have from the border (px) const ARROW_OFFSET = 1; // Minimum distance (px) that axis must have from the border const AXIS_BORDER_OFFSET = 10; // Maximum distance (px) from certain borders at which the axis // labels and ticks will be put on the other sides const AXIS_LABEL_FLIP_OFFSET = 40; context.lineWidth = 2; context.strokeStyle = AXIS_COLOR; let [x0, y0] = biomeCoordsToCanvasPixelCoords(0, 0); let tick_heat, tick_humidity; if (biomeMode === "v6") { tick_heat = 1.0; tick_humidity = 0.5; } else { tick_heat = (limit_heat_max - limit_heat_min) * (100/175); tick_humidity = (limit_humidity_max - limit_humidity_min) * (100/175); } let [tx, ty] = biomeCoordsToCanvasPixelCoords(tick_heat, tick_humidity); let w = voronoiCanvas.width; let h = voronoiCanvas.height; // If axis would go out of bounds, force them to // be rendered at the side instead. let other_side_x = false; let other_side_y = false; if (x0 <= AXIS_BORDER_OFFSET) { x0 = AXIS_BORDER_OFFSET; } else if (x0 >= w - AXIS_BORDER_OFFSET) { x0 = w - AXIS_BORDER_OFFSET; } if (y0 <= AXIS_BORDER_OFFSET) { y0 = AXIS_BORDER_OFFSET; } else if (y0 >= h - AXIS_BORDER_OFFSET) { y0 = h - AXIS_BORDER_OFFSET; } // Flip axis labels if coming close to certain sides if (x0 <= AXIS_LABEL_FLIP_OFFSET) { other_side_y = true; } if (y0 >= h - AXIS_LABEL_FLIP_OFFSET) { other_side_x = true; } // horizontal axis context.beginPath(); context.moveTo(0, y0); context.lineTo(w, y0); // tick if (other_side_x) { context.moveTo(tx, y0 - AXIS_ARROW_SIZE); context.lineTo(tx, y0); } else { context.moveTo(tx, y0); context.lineTo(tx, y0 + AXIS_ARROW_SIZE); } // arrow context.moveTo(w-ARROW_OFFSET, y0); context.lineTo(w-ARROW_OFFSET - AXIS_ARROW_SIZE, y0 - AXIS_ARROW_SIZE); context.moveTo(w-ARROW_OFFSET, y0); context.lineTo(w-ARROW_OFFSET - AXIS_ARROW_SIZE, y0 + AXIS_ARROW_SIZE); context.stroke(); context.closePath(); // vertical axis context.beginPath(); context.moveTo(x0, 0); context.lineTo(x0, h); // tick if (other_side_y) { context.moveTo(x0 + AXIS_ARROW_SIZE, ty); context.lineTo(x0, ty); } else { context.moveTo(x0, ty); context.lineTo(x0 - AXIS_ARROW_SIZE, ty); } // arrow context.moveTo(x0, ARROW_OFFSET); context.lineTo(x0 - AXIS_ARROW_SIZE, ARROW_OFFSET+AXIS_ARROW_SIZE); context.moveTo(x0, ARROW_OFFSET); context.lineTo(x0 + AXIS_ARROW_SIZE, ARROW_OFFSET+AXIS_ARROW_SIZE); context.stroke(); context.closePath(); // axis+tick labels context.fillStyle = "black"; // heat label context.font = "100% sans-serif"; let lx, ly, ttx, tty; if (other_side_x) { context.textBaseline = "bottom"; context.textAlign = "right"; lx = w - AXIS_ARROW_SIZE*2; ly = y0 - 4; tty = y0 - AXIS_ARROW_SIZE; } else { context.textBaseline = "top"; context.textAlign = "right"; lx = w - AXIS_ARROW_SIZE*2; ly = y0 + 4; tty = y0 + AXIS_ARROW_SIZE; } context.fillText("heat", lx, ly); context.textAlign = "center"; let str_tick_heat; if (biomeMode === "v6") { str_tick_heat = tick_heat.toFixed(1); } else { str_tick_heat = tick_heat.toFixed(0); } context.fillText(str_tick_heat, tx, tty); // humidity label context.font = "100% sans-serif"; context.save(); context.rotate(-Math.PI/2); if (other_side_y) { context.textBaseline = "top"; context.textAlign = "right"; lx = -AXIS_ARROW_SIZE*2; ly = x0 + 4; } else { context.textBaseline = "bottom"; context.textAlign = "right"; lx = -AXIS_ARROW_SIZE*2; ly = x0 - 4; } context.fillText("humidity", lx, ly); context.restore(); if (other_side_y) { context.textAlign = "left"; ttx = x0 + AXIS_ARROW_SIZE-2; } else { context.textAlign = "right"; ttx = x0 - AXIS_ARROW_SIZE-2; } context.textBaseline = "middle"; let str_tick_humidity; if (biomeMode === "v6") { str_tick_humidity = tick_humidity.toFixed(1); } else { str_tick_humidity = tick_humidity.toFixed(0); } context.fillText(str_tick_humidity, ttx, ty); } // Cache diagram object for performance boost let cachedVoronoiDiagram = null; /* Given the list of biome points, returns a Voronoi diagram object (which may have been cached). If recalculate is true, a recalculation is forced. */ function getVoronoiDiagram(points, recalculate) { if ((cachedVoronoiDiagram === null) || recalculate) { // Calculate bounding box, defaults to heat/humidity limits ... let vbbox = {xl: limit_heat_min, xr: limit_heat_max, yb: limit_humidity_max, yt: limit_humidity_min}; // ... unless a point is out of bounds, // then we increase the bounding box size // Calculate by how much to extend the // bounding box for the given value. let getBufferZone = function(value) { // This essentially calculates the "order of magnitude" // in base-2. return 2**Math.floor(Math.log2(Math.abs(value))); // The reason why we do this is due to floating-point arithmetic. // If 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 XYZ limits fall out of the given x, y and z values */ function getRenderedPoints(x, y, z) { let points = []; for (let p=0; p= point.min_x && x <= point.max_x && y >= point.min_y && y <= point.max_y && z >= point.min_z && z <= point.max_z) { points.push(point); } } return points; } /* Given a biome ID, returns the matching HTML element from the biome list widget or null if none */ function getBiomeIDFromHTMLElement(elem) { let strID = elem.id; if (strID && strID.startsWith("biome_list_element_")) { let slice = strID.slice(19); if (slice) { return +slice; } } return null; } /* Returns both the ID of selected biome and the associated HTML element from the biome list widget, or null if nothing is selected. Result is returned in the form [id, htmlElement] or [null, null] */ function getSelectedBiomeIDAndElement() { if (biomeSelector.selectedIndex === -1) { return [null, null]; } let elem = biomeSelector.options[biomeSelector.selectedIndex]; let biomeID = getBiomeIDFromHTMLElement(elem); if (biomeID !== null) { return [biomeID, elem]; } return [null, null]; } // Show a message in the center of the given draw // context. function showDiagramMessage(context, text) { context.textAlign = "center"; context.fillStyle = "black"; context.textBaseline = "middle"; if (voronoiCanvas.width < 300) { context.font = "100% sans-serif"; } else if (voronoiCanvas.width < 450) { context.font = "150% sans-serif"; } else { context.font = "200% sans-serif"; } context.fillText(text, voronoiCanvas.width/2, voronoiCanvas.height/2); updateWorldPositionText(); } // Check if a diagram can be drawn on the draw context and // if not, draws a message and return false. // If everything is fine, return true. function checkDrawValid(context) { if (!context) { // We don't even have a valid draw context! // Write an error message in the error message element if (!voronoiCanvas.hidden) { voronoiCanvas.hidden = true; coordinateDisplay.hidden = true; worldPositionDisplay.hidden = true; rangeDisplay.hidden = true; configDiv.hidden = true; errorMessage.innerText = "ERROR: Could not get the canvas context which means this tool won't work for you. Maybe your browser does not support the HTML canvas element properly."; console.error("Could not get the canvas context!"); } return false; } // Fail and render a special message if the value range is tiny if ((limit_heat_max - limit_heat_min < 0.01) || (limit_humidity_max - limit_humidity_min < 0.01)) { showDiagramMessage(context, "Value range is too small."); drawError = true; putResizeCorner(context); return false; } // Fail and render a special message if the value range is huge if ((limit_heat_max - limit_heat_min > MAX_HEAT_HUMIDITY_VALUE) || (limit_humidity_max - limit_humidity_min > MAX_HEAT_HUMIDITY_VALUE)) { showDiagramMessage(context, "Value range is too large."); drawError = true; putResizeCorner(context); return false; } // Fail and render a special message if value limit is out of permissible bounds if ((limit_heat_max > MAX_HEAT_HUMIDITY_VALUE) || (limit_humidity_max > MAX_HEAT_HUMIDITY_VALUE)) { showDiagramMessage(context, "Maximum value is too large."); drawError = true; putResizeCorner(context); return false; } if ((limit_heat_min < MIN_HEAT_HUMIDITY_VALUE) || (limit_humidity_min < MIN_HEAT_HUMIDITY_VALUE)) { showDiagramMessage(context, "Minimum value is too small."); drawError = true; putResizeCorner(context); return false; } drawError = false; return true; } /* Draws the diagram on the voronoiCanvas. Will (re-)calculate the Voronoi diagram if recalculate is true; otherwise it may re-use a previous diagram for performance reasons. */ function drawModern(recalculate) { let context = getDrawContext(); let w = voronoiCanvas.width; let h = voronoiCanvas.height; let x = getViewCoord("x"); let y = getViewCoord("y"); let z = getViewCoord("z"); // shorter function name (for "convert") let conv = biomeCoordsToCanvasPixelCoords clear(context); if (!checkDrawValid(context)) { return false; } let points = getRenderedPoints(x, y, z); // Fail and render a special message if there are no biomes if (points.length === 0) { if (biomePoints.length === 0) { showDiagramMessage(context, "No biomes."); } else { showDiagramMessage(context, "No biomes at these coordinates."); } drawError = true; putResizeCorner(context); return false; } updateWorldPositionText(); let voronoiError = function() { showDiagramMessage(context, "Error in Javascript-Voronoi!"); drawError = true; putResizeCorner(context); } let diagram = getVoronoiDiagram(points, recalculate); if (!diagram) { voronoiError(); drawError = true; return false; } drawError = false; let createHalfedgesPath = function(context, cell) { context.beginPath(); for (let h=0; h 0) { if (showGrid) { putGrid(context, GRID_STEP); } if (showAxes) { putAxes(context); } } // Render Voronoi cell edges context.lineWidth = 2.5; for (let e=0; e Taiga/Tundra [cx_snow, _] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_SNOW, 0); // Temperate <-> Desert/Jungle [cx_hot, _] = biomeCoordsToCanvasPixelCoords(freq_hot, 0); } else { // Temperate <-> Desert/Jungle freq_hot = v6_freq_desert; [cx_hot, _] = biomeCoordsToCanvasPixelCoords(freq_hot, 0); freq_jungle = 0.75; } // Taiga <-> Tundra <-> Temperate [cx_snow2, cy_snow2] = biomeCoordsToCanvasPixelCoords(MGV6_FREQ_SNOW, MGV6_FREQ_TAIGA); // Desert <-> Jungle <-> Temperate [cx_hot2, cy_hot2] = biomeCoordsToCanvasPixelCoords(freq_hot, freq_jungle); if (showCellColors) { // Render biome areas context.fillStyle = CELL_COLOR_V6_NORMAL; if (v6_flag_snowbiomes) { // Temperate context.beginPath(); context.moveTo(cx_snow, 0); context.lineTo(cx_snow, h); context.lineTo(cx_hot, h); context.lineTo(cx_hot, 0); context.closePath(); context.fill(); // Tundra context.fillStyle = CELL_COLOR_V6_TUNDRA; context.beginPath(); context.moveTo(0, 0); context.lineTo(cx_snow2, 0); context.lineTo(cx_snow2, cy_snow2); context.lineTo(0, cy_snow2); context.closePath(); context.fill(); // Taiga context.fillStyle = CELL_COLOR_V6_TAIGA; context.beginPath(); context.moveTo(cx_snow2, cy_snow2); context.lineTo(cx_snow2, h); context.lineTo(0, h); context.lineTo(0, cy_snow2); context.closePath(); context.fill(); // Jungle context.fillStyle = CELL_COLOR_V6_JUNGLE; context.beginPath(); context.moveTo(w, 0); context.lineTo(cx_hot2, 0); context.lineTo(cx_hot2, cy_hot2); context.lineTo(w, cy_hot2); context.closePath(); context.fill(); // Desert context.fillStyle = CELL_COLOR_V6_DESERT; context.beginPath(); context.moveTo(cx_hot2, cy_hot2); context.lineTo(cx_hot2, h); context.lineTo(w, h); context.lineTo(w, cy_hot2); context.closePath(); context.fill(); } else { // Temperate context.beginPath(); context.moveTo(0, 0); context.lineTo(0, h); context.lineTo(cx_hot, h); context.lineTo(cx_hot, 0); context.closePath(); context.fill(); if (v6_flag_jungles) { // Jungle context.fillStyle = CELL_COLOR_V6_JUNGLE; context.beginPath(); context.moveTo(0, 0); context.lineTo(w, 0); context.lineTo(w, cy_hot2); context.lineTo(0, cy_hot2); context.closePath(); context.fill(); // Desert context.fillStyle = CELL_COLOR_V6_DESERT; context.beginPath(); context.moveTo(cx_hot2, cy_hot2); context.lineTo(cx_hot2, h); context.lineTo(w, h); context.lineTo(w, cy_hot2); context.closePath(); context.fill(); } else { // Desert context.fillStyle = CELL_COLOR_V6_DESERT; context.beginPath(); context.moveTo(cx_hot2, 0); context.lineTo(cx_hot2, h); context.lineTo(w, h); context.lineTo(w, 0); context.closePath(); context.fill(); } } } else { // Use a "neutral" background color for the whole area if cell colors are disabled context.fillStyle = CELL_COLOR_NEUTRAL; context.fillRect(-DRAW_OFFSET, -DRAW_OFFSET, w+DRAW_OFFSET, h+DRAW_OFFSET); } // Render biome borders context.lineWidth = 2.5; context.strokeStyle = EDGE_COLOR; context.beginPath(); if (v6_flag_snowbiomes) { // Temperate <-> Taiga/Tundra context.moveTo(cx_snow, 0); context.lineTo(cx_snow, h); // Taiga <-> Tundra context.moveTo(cx_snow2, cy_snow2); context.lineTo(0, cy_snow2); } // Temperate <-> Desert/Jungle if (v6_flag_snowbiomes || (!v6_flag_jungles)) { context.moveTo(cx_hot, 0); context.lineTo(cx_hot, h); } else { context.moveTo(cx_hot, cy_hot2); context.lineTo(cx_hot, h); } // Desert <-> Jungle if (v6_flag_snowbiomes) { context.moveTo(cx_hot2, cy_hot2); context.lineTo(w, cy_hot2); } else if (v6_flag_jungles) { context.moveTo(0, cy_hot2); context.lineTo(w, cy_hot2); } context.closePath(); context.stroke(); if (showGrid) { putGrid(context, GRID_STEP_V6); } if (showAxes) { putAxes(context); } // Biome names if (showNames) { context.font = "120% sans-serif"; context.fillStyle = "#FFFFFFBB"; context.textAlign = "center"; context.textBaseline = "middle"; if (v6_flag_snowbiomes) { context.fillText("Taiga", cx_snow/2, cy_snow2/2); context.fillText("Tundra", cx_snow/2, cy_snow2+((h-cy_snow2)/2)); context.fillText("Jungle", cx_hot + (w-cx_hot)/2, cy_hot2/2); } else if (v6_flag_jungles) { context.fillText("Jungle", cx_hot + (w-cx_hot)/2, cy_hot2/2); } if (v6_flag_snowbiomes) { context.fillText("Desert", cx_hot + (w-cx_hot)/2, cy_hot2+((h-cy_hot2)/2)); } else { if (v6_flag_jungles) { context.fillText("Desert", cx_hot + (w-cx_hot)/2, cy_hot2+((h-cy_hot2)/2)); } else { context.fillText("Desert", cx_hot + (w-cx_hot)/2, h/2); } } if (v6_flag_snowbiomes) { context.fillText("Normal", cx_snow + (cx_hot-cx_snow)/2, h-h/4); } else { if (v6_flag_jungles) { context.fillText("Normal", cx_hot/2, cy_hot2 + (h-cy_hot2)/2); } else { context.fillText("Normal", cx_hot/2, h/2); } } } putResizeCorner(context); } } function draw(recalculate) { if (biomeMode === "v6") { drawV6(recalculate); } else { drawModern(recalculate); } } /* Clears the biome list widget and (re-adds) the list elements for the biomes from scratch. */ function repopulateBiomeSelector() { biomeSelector.innerHTML = ""; for (let b=0; b"; } /* 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="+heatStr+"; humidity="+humidityStr+""; 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 || biomeMode === "v6") { // a special message is shown; use auto cursor voronoiCanvas.style.cursor = "auto"; return } let nearest = getNearestPointFromCanvasPos(x, y, POINT_SELECT_DISTANCE); if (nearest !== null) { let [id, elem] = getSelectedBiomeIDAndElement(); if (id !== null && nearest.id === id) { // This cursor indicates we can grab the point voronoiCanvas.style.cursor = "grab"; } else { // This cursor indicates we can select the point voronoiCanvas.style.cursor = "crosshair"; } } else { // Default cursor when a click doesn't to anything voronoiCanvas.style.cursor = "auto"; } } /* Initializes checkbox variables of the view settings */ function checkboxVarsInit() { showNames = inputCheckboxNames.checked; showPoints = inputCheckboxPoints.checked; showCellColors = inputCheckboxCellColors.checked; showGrid = inputCheckboxGrid.checked; showAxes = inputCheckboxAxes.checked; v6_flag_snowbiomes = inputCheckboxV6Snowbiomes.checked; v6_flag_jungles = inputCheckboxV6Jungles.checked; } /* Collapses/Expands a config section */ function toggleConfigSectionDisplay(headerLink, container) { if (container.style.display !== "none") { headerLink.innerText = "▶"; container.style.display = "none"; } else { headerLink.innerText = "▼"; container.style.display = "block"; } } /* Unhide the main content. Used to disable the noscript handling because the website starts with the main content container hidden so the noscript version of the page isn't cluttered. */ function unhideContent() { mainContentContainer.hidden = false; /* Also hide the container holding the noscript error message to avoid spacing issues */ noscriptContainer.hidden = true; } function initBiomeColorSelectors() { for (let c=0; c= 1) { dragDropState = 2; } else if ((dragDropState === 0 || dragDropState === 1) && dist > ONE_CLICK_DRAG_DROP_DISTANCE) { dragDropState = 2; } } // drag-n-drop if (dragDropState === 2) { updatePointWhenDragged(dragDropPointID); } updateCoordinateDisplay(event.offsetX, event.offsetY); updateCanvasCursorStatus(event.offsetX, event.offsetY); canvas_cursor_x = event.offsetX; canvas_cursor_y = event.offsetY; draw(false); } voronoiCanvas.onmouseenter = function(event) { updateCoordinateDisplay(event.offsetX, event.offsetY); updateCanvasCursorStatus(event.offsetX, event.offsetY); canvas_cursor_x = event.offsetX; canvas_cursor_y = event.offsetY; draw(false); } voronoiCanvas.onmousedown = function(event) { // select point by clicking. // initiate drag-n-drop if already selected. mouseIsDown = true; // Resizing the canvas if (event.offsetX > voronoiCanvas.width - RESIZE_CORNER && event.offsetY > voronoiCanvas.height - RESIZE_CORNER) { resizing_start_pos_x = event.offsetX; resizing_start_pos_y = event.offsetY; resizing_start_size_x = +this.width; resizing_start_size_y = +this.height; resizing = true; return; } if (drawError || !showPoints || biomeMode === "v6") { // Points need to be shown for drag-n-drop to work return; } let nearest = getNearestPointFromCanvasPos(event.offsetX, event.offsetY, POINT_SELECT_DISTANCE); if (nearest !== null) { let success, alreadySelected [success, alreadySelected] = selectPoint(nearest); dragDropPointID = nearest.id; if (success) { let [x, y] = biomeCoordsToCanvasPixelCoords(nearest.heat, nearest.humidity); dragDropStartPos = { x: x, y: y }; } if (alreadySelected) { dragDropState = 1; } updateCanvasCursorStatus(event.offsetX, event.offsetY); } } voronoiCanvas.ondblclick = function(event) { // Add a biome at double-click position, if possible if (drawError || dragDropState !== 0 || biomeMode === "v6") { return; } // No-op if in selection range let nearest = getNearestPointFromCanvasPos(event.offsetX, event.offsetY, POINT_SELECT_DISTANCE); if (nearest !== null) { return; } let [he, hu] = canvasPixelCoordsToBiomeCoords(event.offsetX, event.offsetY); he = Math.round(he); hu = Math.round(hu); addBiome({name: generateBiomeName(lastBiomeID), heat:he, humidity: hu, min_x:MIN_X, max_x:MAX_Y, min_y:MIN_Y, max_y:MAX_Y, min_z:MIN_Z, max_z:MAX_Z}); } voronoiCanvas.onmouseup = function(event) { // end drag-n-drop if (dragDropState === 2) { updatePointWhenDragged(dragDropPointID); } mouseIsDown = false; dragDropStartPos = null; dragDropPointID = null; dragDropState = 0; } voronoiCanvas.onmouseleave = function() { // end drag-n-drop mouseIsDown = false; dragDropStartPos = null; dragDropPointID = null; dragDropState = 0; canvas_cursor_x = null; canvas_cursor_y = null; coordinateDisplay.innerHTML = " "; draw(false); } /* Biome list events */ biomeSelector.onchange = function() { draw(false); if (biomeSelector.selectedIndex !== -1) { let selected = biomeSelector.options[biomeSelector.selectedIndex]; let point = biomePoints[biomeSelector.selectedIndex]; inputHeat.value = point.heat; inputHumidity.value = point.humidity; inputMinX.value = point.min_x; inputMaxX.value = point.max_x; inputMinY.value = point.min_y; inputMaxY.value = point.max_y; inputMinZ.value = point.min_z; inputMaxZ.value = point.max_z; } updateWidgetStates(); } addBiomeButton.onclick = function() { // Add a biome at a random position let he_min = Math.max(MIN_HEAT_HUMIDITY_VALUE, limit_heat_min); let he_max = Math.min(MAX_HEAT_HUMIDITY_VALUE, limit_heat_max); let hu_min = Math.max(MIN_HEAT_HUMIDITY_VALUE, limit_humidity_min); let hu_max = Math.min(MAX_HEAT_HUMIDITY_VALUE, limit_humidity_max); let he = Math.round(he_min + Math.random() * (he_max - he_min)); let hu = Math.round(hu_min + Math.random() * (hu_max - hu_min)); addBiome({name: generateBiomeName(lastBiomeID), heat: he, humidity: hu, min_x:MIN_X, max_x:MAX_Y, min_y:MIN_Y, max_y:MAX_Y, min_z:MIN_Z, max_z:MAX_Z}); } removeBiomeButton.onclick = function() { if (biomeSelector.selectedOptions.length === 0) { return; } let firstIndex = null; for (let o=0; o 0) { let newIndex = firstIndex-1; if (newIndex < 0) { newIndex = 0; } biomeSelector.options[newIndex].selected = "selected"; } draw(true); updateWidgetStates(); } /* Biome editing widgets events */ function handleBiomeNumberInput(biomeValueName, element) { let val = +element.value; if (element.value === "" || typeof val !== "number") { return; } if (element.min !== "" && (val < +element.min || val > +element.max)) { return; } onChangeBiomeValueWidget(biomeValueName, val); } function handleBiomeNumberBlur(biomeValueName, element, defaultValue) { let val = +element.value; if (element.value === "" || typeof val !== "number") { val = defaultValue; element.value = defaultValue; } onChangeBiomeValueWidget(biomeValueName, val); } inputHeat.oninput = function() { handleBiomeNumberInput("heat", this); } inputHumidity.oninput = function() { handleBiomeNumberInput("humidity", this); } inputMinX.oninput = function() { handleBiomeNumberInput("min_x", this); } inputMaxX.oninput = function() { handleBiomeNumberInput("max_x", this); } inputMinY.oninput = function() { handleBiomeNumberInput("min_y", this); } inputMaxY.oninput = function() { handleBiomeNumberInput("max_y", this); } inputMinZ.oninput = function() { handleBiomeNumberInput("min_z", this); } inputMaxZ.oninput = function() { handleBiomeNumberInput("max_z", this); } inputBiomeName.oninput = function() { onChangeBiomeValueWidget("name", this.value); } inputHeat.onblur = function() { handleBiomeNumberBlur("heat", this, 0); } inputHumidity.onblur = function() { handleBiomeNumberBlur("humidity", this, 0); } inputMinX.onblur = function() { handleBiomeNumberBlur("min_x", this, MIN_X); } inputMaxX.onblur = function() { handleBiomeNumberBlur("max_x", this, MAX_X); } inputMinY.onblur = function() { handleBiomeNumberBlur("min_y", this, MIN_Y); } inputMaxY.onblur = function() { handleBiomeNumberBlur("max_y", this, MAX_Y); } inputMinZ.onblur = function() { handleBiomeNumberBlur("min_z", this, MIN_Z); } inputMaxZ.onblur = function() { handleBiomeNumberBlur("max_z", this, MAX_Z); } /* Diagram view settings events */ let h_inputViewXYZ_oninput = function(axis, value) { if (value === null) { return; } setViewCoord(axis, Math.floor(value)); draw(true); updateWorldPositionText(); } let h_inputViewXYZ_onblur = function(axis, value, ref) { if (value === null || value === "") { ref.value = 0; setViewCoord("axis", 0); draw(true); updateWorldPositionText(); } } inputViewX.oninput = function() { h_inputViewXYZ_oninput("x", this.value); } inputViewX.onblur = function() { h_inputViewXYZ_onblur("x", this.value, this); } inputViewY.oninput = function() { h_inputViewXYZ_oninput("y", this.value); } inputViewY.onblur = function() { h_inputViewXYZ_onblur("y", this.value, this); } inputViewZ.oninput = function() { h_inputViewXYZ_oninput("z", this.value); } inputViewZ.onblur = function() { h_inputViewXYZ_onblur("z", this.value, this); } inputCheckboxNames.onchange = function() { showNames = this.checked; draw(false); } inputCheckboxPoints.onchange = function() { showPoints = this.checked; draw(false); } inputCheckboxCellColors.onchange = function() { showCellColors = this.checked; draw(false); } inputCheckboxGrid.onchange = function() { showGrid = this.checked; draw(false); } inputCheckboxAxes.onchange = function() { showAxes = this.checked; draw(false); } inputCheckboxV6Snowbiomes.onchange = function() { v6_flag_snowbiomes = this.checked; updateWidgetStates(); draw(false); } inputCheckboxV6Jungles.onchange = function() { v6_flag_jungles = this.checked; draw(false); } inputV6FreqDesert.oninput = function() { let f = +this.value; if (f === null) { return; } v6_freq_desert = f; draw(true); } inputV6FreqDesert.onblur = function() { let f = +this.value; if (f === null || this.value === "") { v6_freq_desert = MGV6_FREQ_DESERT_DEFAULT; this.value = v6_freq_desert; draw(true); } } /* Noise parameters events */ function updateNoiseParam(noiseName, noiseValueName, element) { if (element.value === "") { return; } let val = +element.value; noises[noiseName][noiseValueName] = val; clear(); updateAreaVars(); draw(true); } function blurNoiseParam(noiseName, noiseValueName, element, defaultValue) { let val = +element.value; if (element.value === "" || val === null) { val = defaultValue; element.value = val; noises[noiseName][noiseValueName] = val; clear(); updateAreaVars(); draw(true); } } function updateNoiseParamCheckbox(noiseName, noiseValueName, element) { let val = element.checked; noises[noiseName][noiseValueName] = val; clear(); updateAreaVars(); draw(true); } let noiseWidgetTable = [ { elem: inputNoiseHeatScale, type: "input", noise: "heat", noise_param: "scale", default_modern: NOISE_SCALE_DEFAULT, default_v6: NOISE_V6_HEAT_SCALE_DEFAULT }, { elem: inputNoiseHeatOffset, type: "input", noise: "heat", noise_param: "offset", default_modern: NOISE_OFFSET_DEFAULT, default_v6: NOISE_V6_HEAT_OFFSET_DEFAULT }, { elem: inputNoiseHeatOctaves, type: "input", noise: "heat", noise_param: "octaves", default_modern: NOISE_OCTAVES_DEFAULT, default_v6: NOISE_V6_HEAT_OCTAVES_DEFAULT }, { elem: inputNoiseHeatPersistence, type: "input", noise: "heat", noise_param: "persistence", default_modern: NOISE_PERSISTENCE_DEFAULT, default_v6: NOISE_V6_HEAT_PERSISTENCE_DEFAULT }, { elem: inputNoiseHeatAbsvalue, type: "checkbox", noise: "heat", noise_param: "absvalue", default_modern: NOISE_ABSVALUE_DEFAULT, default_v6: NOISE_V6_HEAT_ABSVALUE_DEFAULT }, { elem: inputNoiseHumidityScale, type: "input", noise: "humidity", noise_param: "scale", default_modern: NOISE_SCALE_DEFAULT, default_v6: NOISE_V6_HUMIDITY_SCALE_DEFAULT }, { elem: inputNoiseHumidityOffset, type: "input", noise: "humidity", noise_param: "offset", default_modern: NOISE_OFFSET_DEFAULT, default_v6: NOISE_V6_HUMIDITY_OFFSET_DEFAULT }, { elem: inputNoiseHumidityOctaves, type: "input", noise: "humidity", noise_param: "octaves", default_modern: NOISE_OCTAVES_DEFAULT, default_v6: NOISE_V6_HUMIDITY_OCTAVES_DEFAULT }, { elem: inputNoiseHumidityPersistence, type: "input", noise: "humidity", noise_param: "persistence", default_modern: NOISE_PERSISTENCE_DEFAULT, default_v6: NOISE_V6_HUMIDITY_PERSISTENCE_DEFAULT }, { elem: inputNoiseHumidityAbsvalue, type: "checkbox", noise: "humidity", noise_param: "absvalue", default_modern: NOISE_ABSVALUE_DEFAULT, default_v6: NOISE_V6_HUMIDITY_ABSVALUE_DEFAULT }, ] for (let n of noiseWidgetTable) { if (n.type === "input") { n.elem.oninput = function() { updateNoiseParam(n.noise, n.noise_param, this); } n.elem.onblur = function() { let noiseElem = n.noise + "_" + biomeMode; let defaultValue = n[DEFAULT_BIOME_NAME + "_" + biomeMode]; blurNoiseParam(n.noise, n.noise_param, this, defaultValue); } } else if (n.type === "checkbox") { n.elem.onchange = function() { updateNoiseParamCheckbox(n.noise, n.noise_param, 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; inputNoiseHeatAbsvalue.checked = noises.heat.absvalue; inputNoiseHumidityOffset.value = noises.humidity.offset; inputNoiseHumidityScale.value = noises.humidity.scale; inputNoiseHumidityOctaves.value = noises.humidity.octaves; inputNoiseHumidityPersistence.value = noises.humidity.persistence; inputNoiseHumidityAbsvalue.checked = noises.humidity.absvalue; clear(); updateAreaVars(); draw(true); } /* Export events */ inputExportLua.onclick = function() { let str = ""; for (let b=0; b CELL_COLORS.length-1) { colorIndex = undefined; } } let newPoint = { id: lastBiomeID, name: parsedPoint.name, heat: parsedPoint.heat_point, humidity: parsedPoint.humidity_point, min_x: parsedPoint.x_min, max_x: parsedPoint.x_max, min_y: parsedPoint.y_min, max_y: parsedPoint.y_max, min_z: parsedPoint.z_min, max_z: parsedPoint.z_max, colorIndex: colorIndex || lastBiomeID % CELL_COLORS.length, }; lastBiomeID++; newPoints.push(newPoint); } // Replace the biomes biomePoints = newPoints; repopulateBiomeSelector(); updateWidgetStates(); draw(true); if (biomePoints.length === 1) { importMessage("Import successful. 1 biome imported."); } else { importMessage(`Import successful. ${biomePoints.length} biomes imported.`); } } inputImportLibpovJSONSubmit.onclick = function() { importJSON("libpov"); } inputImportAmidstForMinetestSubmit.onclick = function() { importJSON("amidst_for_minetest"); } /* Mode events */ modernModeButton.onclick = function() { biomeMode = "modern"; biomeConfigContainerOuter.hidden = false; biomeV6ConfigContainerOuter.hidden = true; importContainerOuter.hidden = false; exportContainerOuter.hidden = false; inputCheckboxPoints.disabled = false; noises.heat = noises.heat_modern; noises.humidity = noises.humidity_modern; noiseSettingNameHeat.innerText = "mg_biome_np_heat"; noiseSettingNameHumidity.innerText = "mg_biome_np_humidity"; modernModeButton.className = "activeBiomeModeButton"; v6ModeButton.className = ""; updateAreaVars(); updateWidgetStates(); draw(true); } v6ModeButton.onclick = function() { biomeMode = "v6"; biomeConfigContainerOuter.hidden = true; biomeV6ConfigContainerOuter.hidden = false; importContainerOuter.hidden = true; exportContainerOuter.hidden = true; inputCheckboxPoints.disabled = true; noises.heat = noises.heat_v6; noises.humidity = noises.humidity_v6; noiseSettingNameHeat.innerText = "mgv6_np_biome"; noiseSettingNameHumidity.innerText = "mgv6_np_humidity"; modernModeButton.className = ""; v6ModeButton.className = "activeBiomeModeButton"; updateAreaVars(); updateWidgetStates(); draw(); } /* Events for collapsing/extending config section with the arrow thingie */ biomeConfigHeaderLink.onclick = function() { toggleConfigSectionDisplay(this, biomeConfigContainer); } biomeV6ConfigHeaderLink.onclick = function() { toggleConfigSectionDisplay(this, biomeV6ConfigContainer); } viewConfigHeaderLink.onclick = function() { toggleConfigSectionDisplay(this, viewConfigContainer); } noiseConfigHeaderLink.onclick = function() { toggleConfigSectionDisplay(this, noiseConfigContainer); } importHeaderLink.onclick = function() { toggleConfigSectionDisplay(this, importContainer); } exportHeaderLink.onclick = function() { toggleConfigSectionDisplay(this, exportContainer); } /* Prevent forms from submitting */ function disableFormSubmission() { let elements = document.querySelectorAll("form"); for (let elem of elements) { elem.onsubmit = function() { return false; } } } function initViewCoords() { viewX = inputViewX.value; viewY = inputViewY.value; viewZ = inputViewZ.value; } function resetBiomeInputs() { inputMinX.value = MIN_X; inputMaxX.value = MAX_X; inputMinY.value = MIN_Y; inputMaxY.value = MAX_Y; inputMinZ.value = MIN_Z; inputMaxZ.value = MAX_Z; inputHeat.value = DEFAULT_HEAT; inputHumidity.value = DEFAULT_HUMIDITY; inputBiomeName.value = DEFAULT_BIOME_NAME; } /* Load events */ window.addEventListener("load", initBiomeColorSelectors); window.addEventListener("load", initViewCoords); window.addEventListener("load", resetBiomeInputs); window.addEventListener("load", checkboxVarsInit); window.addEventListener("load", function() { draw(true); }) window.addEventListener("load", repopulateBiomeSelector); window.addEventListener("load", updateWidgetStates); window.addEventListener("load", updateWorldPositionText); window.addEventListener("load", unhideContent); window.addEventListener("load", fitCanvasInBody); window.addEventListener("load", disableFormSubmission);