|
- //#region variables
-
- let selected = null;
- let prevSelected = null;
- let selectedEntity = null;
- let prevSelectedEntity = null;
-
- let entityIndex = 0;
-
- let firsterror = true;
-
- let clicked = null;
- let movingInBounds = false;
- let dragging = false;
- let clickTimeout = null;
- let dragOffsetX = null;
- let dragOffsetY = null;
-
- let preloaded = new Set();
-
- let panning = false;
- let panReady = true;
- let panOffsetX = null;
- let panOffsetY = null;
-
- let shiftHeld = false;
- let altHeld = false;
-
- let entityX;
- let canvasWidth;
- let canvasHeight;
-
- let dragScale = 1;
- let dragScaleHandle = null;
-
- let dragEntityScale = 1;
- let dragEntityScaleHandle = null;
-
- let scrollDirection = 0;
- let scrollHandle = null;
-
- let zoomDirection = 0;
- let zoomHandle = null;
-
- let sizeDirection = 0;
- let sizeHandle = null;
-
- let worldSizeDirty = false;
-
- let rulerMode = false;
-
- let rulers = [];
-
- let currentRuler = undefined;
-
- let webkitCanvasBug = false;
-
- const tagDefs = {
- anthro: "Anthro",
- feral: "Feral",
- taur: "Taur",
- naga: "Naga",
- goo: "Goo",
- };
-
- const availableEntities = {};
-
- const availableEntitiesByName = {};
-
- const entities = {};
-
- let ratioInfo;
-
- //#endregion
-
- //#region units
-
- function dimsEqual(unit1, unit2) {
- a = unit1.dimensions;
- b = unit2.dimensions;
-
- if (a.length != b.length) {
- return false;
- }
-
- for (let i = 0; i < a.length && i < b.length; i++) {
- if (a[i] != b[i]) {
- return false;
- }
- }
-
- return true;
- }
-
- // Determines if a unit is one of a length, area, volume, mass, or energy
- function typeOfUnit(unit) {
- if (dimsEqual(unit, math.unit(1, "meters"))) {
- return "length"
- }
- if (dimsEqual(unit, math.unit(1, "meters^2"))) {
- return "area"
- }
- if (dimsEqual(unit, math.unit(1, "meters^3"))) {
- return "volume"
- }
- if (dimsEqual(unit, math.unit(1, "kilograms"))) {
- return "mass"
- }
- if (dimsEqual(unit, math.unit(1, "joules"))) {
- return "energy"
- }
-
- return null;
- }
-
- const unitPowers = {
- "length": 1,
- "area": 2,
- "volume": 3,
- "mass": 3,
- "energy": 3 * (3 / 4)
- };
-
- math.createUnit("morbillion", "352.9e12");
-
- math.createUnit({
- ShoeSizeMensUS: {
- prefixes: "long",
- definition: "0.3333333333333333333 inches",
- offset: 22,
- },
- ShoeSizeWomensUS: {
- prefixes: "long",
- definition: "0.3333333333333333333 inches",
- offset: 21,
- },
- ShoeSizeEU: {
- prefixes: "long",
- definition: "0.666666666667 cm",
- offset: -2,
- },
- ShoeSizeUK: {
- prefixes: "long",
- definition: "0.3333333333333333333 in",
- offset: 23,
- },
- RingSizeNA: {
- prefixes: "long",
- definition: "0.0327 inches",
- offset: 13.883792
- },
- RingSizeISO: {
- prefixes: "long",
- definition: "0.318309886 mm",
- offset: 0
- },
- RingSizeIndia: {
- prefixes: "long",
- definition: "0.318309886 mm",
- offset: 40
- }
- });
-
- math.createUnit("humans", {
- definition: "5.75 feet",
- });
-
- math.createUnit("stories", {
- definition: "12 feet",
- prefixes: "long",
- aliases: ["story", "floor", "floors", "storey", "storeys"]
- });
- math.createUnit("buses", {
- definition: "11.95 meters",
- prefixes: "long",
- aliases: ["bus"]
- });
- math.createUnit("marathons", {
- definition: "26.2 miles",
- prefixes: "long",
- aliases: ["marathon"]
- });
- math.createUnit("timezones", {
- definition: "1037.54167 miles",
- prefixes: "long",
- aliases: ["timezone"],
- });
- math.createUnit("nauticalMiles", {
- definition: "6080 feet",
- prefixes: "long",
- aliases: ["nauticalMile"],
- });
- math.createUnit("fathoms", {
- definition: "6 feet",
- prefixes: "long",
- aliases: ["fathom"],
- });
- math.createUnit("U", {
- definition: "1.75 inches",
- prefixes: "short",
- aliases: ["rackUnits"]
- });
- math.createUnit("earths", {
- definition: "12756km",
- prefixes: "long",
- aliases: ["earth", "earths", "Earth", "Earths"],
- });
- math.createUnit("lightseconds", {
- definition: "299792458 meters",
- prefixes: "long",
- aliases: ["lightsecond"]
- });
- math.createUnit("parsecs", {
- definition: "3.086e16 meters",
- prefixes: "long",
- aliases: ["parsec"]
- });
- math.createUnit("lightyears", {
- definition: "9.461e15 meters",
- prefixes: "long",
- aliases: ["lightyear"]
- });
- math.createUnit("AUs", {
- definition: "149597870700 meters",
- aliases: ["AU", "astronomicalUnits", "astronomicalUnit"]
- });
- math.createUnit("daltons", {
- definition: "1.66e-27 kg",
- prefixes: "long",
- aliases: ["dalton", "Daltons", "Dalton"]
- });
- math.createUnit("solarradii", {
- definition: "695990 km",
- prefixes: "long",
- aliases: ["solarRadii"]
- });
- math.createUnit("solarmasses", {
- definition: "2e30 kg",
- prefixes: "long",
- aliases: ["solarMasses"]
- });
- math.createUnit("galaxies", {
- definition: "105700 lightyears",
- prefixes: "long",
- aliases: ["galaxy"]
- });
- math.createUnit("universes", {
- definition: "93.016e9 lightyears",
- prefixes: "long",
- aliases: ["universe"]
- });
- math.createUnit("multiverses", {
- definition: "1e30 lightyears",
- prefixes: "long",
- aliases: ["multiverse"]
- });
-
- math.createUnit("pinHeads", {
- definition: "3.14159 mm^2",
- prefixes: "long",
- aliases: ["pinHead"]
- });
- math.createUnit("dinnerPlates", {
- definition: "95 inches^2",
- prefixes: "long",
- aliases: ["dinnerPlate"]
- });
- math.createUnit("suburbanHouses", {
- definition: "2000 feet^2",
- prefixes: "long",
- aliases: ["suburbanHouse"]
- });
- math.createUnit("footballFields", {
- definition: "57600 feet^2",
- prefixes: "long",
- aliases: ["footballField"]
- });
- math.createUnit("blocks", {
- definition: "20000 m^2",
- prefixes: "long",
- aliases: ["block"],
- });
-
- math.createUnit("peopleInRural", {
- definition: "0.02 miles^2",
- });
- math.createUnit("peopleInManhattan", {
- definition: "15 m^2",
- });
- math.createUnit("peopleInLooseCrowd", {
- definition: "1 m^2",
- });
- math.createUnit("peopleInCrowd", {
- definition: "0.3333333333333333 m^2",
- });
- math.createUnit("peopleInDenseCrowd", {
- definition: "0.2 m^2",
- });
-
- math.createUnit("people", {
- definition: "75 liters",
- prefixes: "long",
- aliases: ["prey", "preys"]
- });
- math.createUnit("shippingContainers", {
- definition: "1169 ft^3",
- prefixes: "long",
- aliases: ["shippingContainer"]
- });
- math.createUnit("olympicPools", {
- definition: "2500 m^3",
- prefixes: "long",
- aliases: ["olympicPool"]
- });
- math.createUnit("oceans", {
- definition: "700000000 km^3",
- prefixes: "long",
- aliases: ["ocean"]
- });
- math.createUnit("earthVolumes", {
- definition: "1.0867813e12 km^3",
- prefixes: "long",
- aliases: ["earthVolume"]
- });
- math.createUnit("universeVolumes", {
- definition: "4.2137775e+32 lightyears^3",
- prefixes: "long",
- aliases: ["universeVolume"]
- });
- math.createUnit("multiverseVolumes", {
- definition: "5.2359878e+89 lightyears^3",
- prefixes: "long",
- aliases: ["multiverseVolume"]
- });
- math.createUnit("peopleMass", {
- definition: "80 kg",
- prefixes: "long",
- aliases: ["peopleMasses"]
- });
- math.createUnit("cars", {
- definition: "1250kg",
- prefixes: "long",
- aliases: ["car"]
- });
- math.createUnit("busMasses", {
- definition: "15000kg",
- prefixes: "long",
- aliases: ["busMass"]
- });
- math.createUnit("earthMasses", {
- definition: "5.97e24 kg",
- prefixes: "long",
- aliases: ["earthMass"]
- });
-
- math.createUnit("kcal", {
- definition: "4184 joules",
- prefixes: "long",
- });
- math.createUnit("foodPounds", {
- definition: "867 kcal",
- prefixes: "long",
- });
- math.createUnit("foodKilograms", {
- definition: "1909 kcal",
- prefixes: "long",
- });
- math.createUnit("chickenNuggets", {
- definition: "42 kcal",
- prefixes: "long",
- });
- math.createUnit("peopleEaten", {
- definition: "125000 kcal",
- prefixes: "long",
- });
- math.createUnit("villagesEaten", {
- definition: "1000 peopleEaten",
- prefixes: "long",
- });
- math.createUnit("townsEaten", {
- definition: "10000 peopleEaten",
- prefixes: "long",
- });
- math.createUnit("citiesEaten", {
- definition: "100000 peopleEaten",
- prefixes: "long",
- });
- math.createUnit("metrosEaten", {
- definition: "1000000 peopleEaten",
- prefixes: "long",
- });
-
- math.createUnit("barns", {
- definition: "10e-28 m^2",
- prefixes: "long",
- aliases: ["barn"]
- });
-
- math.createUnit("points", {
- definition: "0.013888888888888888888888888 inches",
- prefixes: "long",
- aliases: ["point"]
- });
- math.createUnit("picas", {
- definition: "12 points",
- prefixes: "long",
- aliases: ["pica"]
- });
- math.createUnit("beardSeconds", {
- definition: "10 nanometers",
- prefixes: "long",
- aliases: ["beardSecond"]
- });
- math.createUnit("smoots", {
- definition: "5.5833333 feet",
- prefixes: "long",
- aliases: ["smoot"]
- });
- math.createUnit("furlongs", {
- definition: "660 feet",
- prefixes: "long",
- aliases: ["furlong"]
- });
- math.createUnit("nanoacres", {
- definition: "1e-9 acres",
- prefixes: "long",
- aliases: ["nanoacre"]
- });
- math.createUnit("barnMegaparsecs", {
- definition: "1 barn megaparsec",
- prefixes: "long",
- aliases: ["barnMegaparsec"]
- });
- math.createUnit("firkins", {
- definition: "90 lb",
- prefixes: "long",
- aliases: ["firkin"]
- });
- math.createUnit("donkeySeconds", {
- definition: "250 joules",
- prefixes: "long",
- aliases: ["donkeySecond"]
- });
- math.createUnit("HU", {
- definition: "0.75 inches",
- aliases: ["HUs", "hammerUnits"],
- });
-
- math.createUnit("sections", {
- definition: "640 acres",
- aliases: ["section"]
- });
- math.createUnit("townships", {
- definition: "36 sections",
- aliases: ["township", "surveytownships", "surveytownships"]
- });
-
- math.createUnit("hands", {
- definition: "4 inches",
- prefixes: "long",
- aliases: ["hand", "hands"]
- });
-
- math.createUnit("acreFeet", {
- definition: "1 acre * foot",
- prefixes: "long",
- });
-
- //#endregion
-
- const defaultUnits = {
- length: {
- metric: "meters",
- customary: "feet",
- relative: "stories",
- quirky: "smoots",
- human: "humans",
- },
- area: {
- metric: "meters^2",
- customary: "feet^2",
- relative: "footballFields",
- quirky: "nanoacres",
- human: "peopleInCrowd",
- },
- volume: {
- metric: "liters",
- customary: "gallons",
- relative: "olympicPools",
- volume: "barnMegaparsecs",
- human: "people",
- },
- mass: {
- metric: "kilograms",
- customary: "lbs",
- relative: "peopleMass",
- quirky: "firkins",
- human: "peopleMass",
- },
- energy: {
- metric: "kJ",
- customary: "kcal",
- relative: "chickenNuggets",
- quirky: "donkeySeconds",
- human: "peopleEaten",
- },
- };
-
- const unitChoices = {
- length: {
- metric: [
- "angstroms",
- "millimeters",
- "centimeters",
- "meters",
- "kilometers",
- ],
- customary: ["inches", "feet", "yards", "miles", "nauticalMiles"],
- relative: [
- "RingSizeNA",
- "RingSizeISO",
- "RingSizeIndia",
- "ShoeSizeEU",
- "ShoeSizeUK",
- "ShoeSizeMensUS",
- "ShoeSizeWomensUS",
- "hands",
- "stories",
- "buses",
- "marathons",
- "timezones",
- "earths",
- "lightseconds",
- "solarradii",
- "AUs",
- "lightyears",
- "parsecs",
- "galaxies",
- "universes",
- "multiverses",
- ],
- quirky: [
- "beardSeconds",
- "points",
- "picas",
- "smoots",
- "links",
- "rods",
- "chains",
- "furlongs",
- "HUs",
- "U",
- "fathoms",
- ],
- human: ["humans"],
- },
- area: {
- metric: ["cm^2", "meters^2", "kilometers^2"],
- customary: ["inches^2", "feet^2", "chains^2", "acres", "miles^2", "sections", "townships"],
- relative: [
- "pinHeads",
- "dinnerPlates",
- "suburbanHouses",
- "footballFields",
- "blocks",
- ],
- quirky: ["barns", "nanoacres"],
- human: [
- "peopleInRural",
- "peopleInManhattan",
- "peopleInLooseCrowd",
- "peopleInCrowd",
- "peopleInDenseCrowd",
- ],
- },
- volume: {
- metric: ["milliliters", "liters", "m^3"],
- customary: ["in^3", "floz", "teaspoons", "tablespoons", "cups", "pints", "quarts", "gallons", "acreFeet"],
- relative: [
- "oilbarrels",
- "shippingContainers",
- "olympicPools",
- "oceans",
- "earthVolumes",
- "universeVolumes",
- "multiverseVolumes",
- ],
- quirky: ["barnMegaparsecs"],
- human: ["people"],
- },
- mass: {
- metric: ["kilograms", "milligrams", "grams", "tonnes"],
- customary: ["grains", "lbs", "ounces", "tons"],
- relative: ["cars", "busMasses", "earthMasses", "solarmasses"],
- quirky: ["firkins"],
- human: ["peopleMass"],
- },
- energy: {
- metric: ["kJ", "foodKilograms"],
- customary: ["kcal", "foodPounds"],
- relative: ["chickenNuggets"],
- quirky: ["donkeySeconds"],
- human: [
- "peopleEaten",
- "villagesEaten",
- "townsEaten",
- "citiesEaten",
- "metrosEaten",
- ],
- },
- };
-
- const config = {
- height: math.unit(1500, "meters"),
- x: 0,
- y: 0,
- minLineSize: 100,
- maxLineSize: 150,
- autoFit: false,
- drawYAxis: true,
- drawXAxis: false,
- autoMass: "off",
- autoFoodIntake: false,
- autoPreyCapacity: "off",
- autoSwallowSize: "off"
- };
-
- //#region transforms
-
- function constrainRel(coords) {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- const worldHeight = config.height.toNumber("meters");
-
- if (altHeld) {
- return coords;
- }
- return {
- x: Math.min(
- Math.max(coords.x, -worldWidth / 2 + config.x),
- worldWidth / 2 + config.x
- ),
- y: Math.min(Math.max(coords.y, config.y), worldHeight + config.y),
- };
- }
-
- // not using constrainRel anymore
- function snapPos(coords) {
- return {
- x: coords.x,
- y:
- !config.groundSnap || altHeld
- ? coords.y
- : Math.abs(coords.y) < config.height.toNumber("meters") / 20
- ? 0
- : coords.y,
- };
- }
-
- function adjustAbs(coords, oldHeight, newHeight) {
- const ratio = math.divide(newHeight, oldHeight);
-
- const x = (coords.x - config.x) * ratio + config.x;
- const y = (coords.y - config.y) * ratio + config.y;
-
- return { x: x, y: y };
- }
-
- function pos2pix(coords) {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- const worldHeight = config.height.toNumber("meters");
-
- const x =
- ((coords.x - config.x) / worldWidth + 0.5) * (canvasWidth - 50) + 50;
- const y =
- (1 - (coords.y - config.y) / worldHeight) * (canvasHeight - 50) + 50;
-
- return { x: x, y: y };
- }
-
- function pix2pos(coords) {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- const worldHeight = config.height.toNumber("meters");
-
- const x =
- ((coords.x - 50) / (canvasWidth - 50) - 0.5) * worldWidth + config.x;
- const y =
- (1 - (coords.y - 50) / (canvasHeight - 50)) * worldHeight + config.y;
- return { x: x, y: y };
- }
-
- //#endregion
-
- //#region update
-
- function updateEntityElement(entity, element) {
- const position = pos2pix({ x: element.dataset.x, y: element.dataset.y });
- const view = entity.view;
- const form = entity.form;
-
- element.style.left = position.x + "px";
- element.style.top = position.y + "px";
- element.style.setProperty("--xpos", position.x + "px");
- element.style.setProperty(
- "--entity-height",
- "'" +
- entity.views[view].height
- .to(config.height.units[0].unit.name)
- .format({ precision: 2 }) +
- "'"
- );
- const pixels =
- math.divide(entity.views[view].height, config.height) *
- (canvasHeight - 50);
- const extra = entity.views[view].image.extra;
- const bottom = entity.views[view].image.bottom;
- const bonus = (extra ? extra : 1) * (1 / (1 - (bottom ? bottom : 0)));
- let height = pixels * bonus;
-
- // working around a Firefox issue here
-
- if (height > 17895698) {
- height = 0;
- }
-
- element.style.setProperty("--height", height + "px");
- element.style.setProperty("--extra", height - pixels + "px");
-
- if (entity.views[view].rename)
- element.querySelector(".entity-name").innerText =
- entity.name == "" ? "" : entity.views[view].name;
- else if (
- entity.forms !== undefined &&
- Object.keys(entity.forms).length > 0 &&
- entity.forms[form].rename
- )
- element.querySelector(".entity-name").innerText =
- entity.name == "" ? "" : entity.forms[form].name;
- else element.querySelector(".entity-name").innerText = entity.name;
-
- const bottomName = document.querySelector(
- "#bottom-name-" + element.dataset.key
- );
-
- bottomName.style.left = position.x + entityX + "px";
- bottomName.style.bottom = "0vh";
- bottomName.innerText = entity.name;
-
- const topName = document.querySelector("#top-name-" + element.dataset.key);
-
- topName.style.left = position.x + entityX + "px";
- topName.style.top = "20vh";
- topName.innerText = entity.name;
-
- if (
- entity.views[view].height.toNumber("meters") / 10 >
- config.height.toNumber("meters")
- ) {
- topName.classList.add("top-name-needed");
- } else {
- topName.classList.remove("top-name-needed");
- }
- updateInfo();
- }
-
- function updateInfo() {
- let text = "";
- if (config.showRatios) {
- if (
- selectedEntity !== null &&
- prevSelectedEntity !== null &&
- selectedEntity !== prevSelectedEntity
- ) {
- let first = selectedEntity.currentView.height;
- let second = prevSelectedEntity.currentView.height;
-
- if (first.toNumber("meters") < second.toNumber("meters")) {
- text +=
- selectedEntity.name +
- " is " +
- math.format(math.divide(second, first), { precision: 5 }) +
- " times smaller than " +
- prevSelectedEntity.name;
- } else {
- text +=
- selectedEntity.name +
- " is " +
- math.format(math.divide(first, second), { precision: 5 }) +
- " times taller than " +
- prevSelectedEntity.name;
- }
-
- text += "\n";
-
- let apparentHeight = math.multiply(
- math.divide(second, first),
- math.unit(6, "feet")
- );
-
- if (config.units === "metric") {
- apparentHeight = apparentHeight.to("meters");
- }
-
- text +=
- prevSelectedEntity.name +
- " looks " +
- math.format(apparentHeight, { precision: 3 }) +
- " tall to " +
- selectedEntity.name +
- "\n";
-
- if (
- selectedEntity.currentView.weight &&
- prevSelectedEntity.currentView.weight
- ) {
- const ratio = math.divide(
- selectedEntity.currentView.weight,
- prevSelectedEntity.currentView.weight
- );
-
- if (ratio > 1) {
- text +=
- selectedEntity.name +
- " is " +
- math.format(ratio, { precision: 2 }) +
- " times heavier than " +
- prevSelectedEntity.name +
- "\n";
- } else {
- text +=
- selectedEntity.name +
- " is " +
- math.format(1 / ratio, { precision: 2 }) +
- " times lighter than " +
- prevSelectedEntity.name +
- "\n";
- }
- }
- const capacity =
- selectedEntity.currentView.preyCapacity ??
- selectedEntity.currentView.capacity ??
- selectedEntity.currentView.volume;
-
- if (capacity && prevSelectedEntity.currentView.weight) {
- const containCount = math.divide(
- capacity,
- math.divide(
- prevSelectedEntity.currentView.weight,
- math.unit("80kg/people")
- )
- );
- if (containCount > 0.1) {
- text +=
- selectedEntity.name +
- " can fit " +
- math.format(containCount, { precision: 1 }) +
- " of " +
- prevSelectedEntity.name +
- " inside them" +
- "\n";
- }
- }
- const swallowSize =
- selectedEntity.currentView.swallowSize;
-
- if (swallowSize && prevSelectedEntity.currentView.weight) {
- const containCount = math.divide(
- swallowSize,
- math.divide(
- prevSelectedEntity.currentView.weight,
- math.unit("80kg/people")
- )
- );
- if (containCount > 0.1) {
- text +=
- selectedEntity.name +
- " can swallow " +
- math.format(containCount, { precision: 3 }) +
- " of " +
- prevSelectedEntity.name +
- " at once" +
- "\n";
- }
- }
-
- if (
- selectedEntity.currentView.energyIntake &&
- prevSelectedEntity.currentView.energyValue
- ) {
- const consumeCount = math.divide(
- selectedEntity.currentView.energyIntake,
- prevSelectedEntity.currentView.energyValue
- );
- if (consumeCount > 0.1) {
- text +=
- selectedEntity.name +
- " needs to eat " +
- math.format(consumeCount, { precision: 1 }) +
- " of " +
- prevSelectedEntity.name +
- " per day" +
- "\n";
- }
- }
-
- // todo needs a nice system for formatting this
-
- Object.entries(selectedEntity.currentView.attributes).forEach(
- ([key, attr]) => {
- if (key !== "height") {
- if (attr.type === "length") {
- const ratio = math.divide(
- selectedEntity.currentView[key],
- prevSelectedEntity.currentView.height
- );
-
- if (ratio > 1) {
- text +=
- selectedEntity.name +
- "'s " +
- attr.name +
- " is " +
- math.format(ratio, { precision: 2 }) +
- " times longer than " +
- prevSelectedEntity.name +
- " is tall\n";
- } else {
- text +=
- selectedEntity.name +
- "'s " +
- attr.name +
- " is " +
- math.format(1 / ratio, { precision: 2 }) +
- " times shorter than " +
- prevSelectedEntity.name +
- " is tall\n";
- }
- }
- }
- }
- );
- }
- }
-
- if (config.showHorizon) {
- if (selectedEntity !== null) {
- const y = document.querySelector("#entity-" + selectedEntity.index)
- .dataset.y;
- const R = math.unit(1.2756e7, "meters");
- const h = math.add(
- selectedEntity.currentView.height,
- math.unit(y, "meters")
- );
-
- const first = math.multiply(2, math.multiply(R, h));
- const second = math.multiply(h, h);
-
- const sightline = math
- .sqrt(math.add(first, second))
- .to(config.height.units[0].unit.name);
-
- sightline.fixPrefix = false;
- text +=
- selectedEntity.name +
- " could see for " +
- math.format(sightline, { precision: 3 }) +
- "\n";
- }
- }
-
- if (config.showRatios && config.showHorizon) {
- if (
- selectedEntity !== null &&
- prevSelectedEntity !== null &&
- selectedEntity !== prevSelectedEntity
- ) {
- const y1 = document.querySelector("#entity-" + selectedEntity.index)
- .dataset.y;
- const y2 = document.querySelector(
- "#entity-" + prevSelectedEntity.index
- ).dataset.y;
- const R = math.unit(1.2756e7, "meters");
- const R2 = math.subtract(
- math.subtract(R, prevSelectedEntity.currentView.height),
- math.unit(y2, "meters")
- );
- const h = math.add(
- selectedEntity.currentView.height,
- math.unit(y1, "meters")
- );
-
- const first = math.pow(math.add(R, h), 2);
- const second = math.pow(R2, 2);
-
- const sightline = math
- .sqrt(math.subtract(first, second))
- .to(config.height.units[0].unit.name);
-
- sightline.fixPrefix = false;
- text +=
- selectedEntity.name +
- " could see " +
- prevSelectedEntity.name +
- " from " +
- math.format(sightline, { precision: 3 }) +
- " away\n";
- }
- }
-
- ratioInfo.innerText = text;
- }
-
- function updateEntityProperties(element) {
- entity = entities[element.dataset.key]
-
- element.style.setProperty("--flipped", entity.flipped ? -1 : 1);
- element.style.setProperty(
- "--rotation",
- (entity.rotation * 180) / Math.PI +
- "deg"
- );
- element.style.setProperty("--brightness", entity.brightness);
- }
-
- function updateSizes(dirtyOnly = false) {
- updateInfo();
-
- if (config.lockYAxis) {
- config.y = -getVerticalOffset();
- }
-
- drawScales(dirtyOnly);
-
- let ordered = Object.entries(entities);
-
- ordered.sort((e1, e2) => {
- if (e1[1].priority != e2[1].priority) {
- return e2[1].priority - e1[1].priority;
- } else {
- return (
- e1[1].views[e1[1].view].height.value -
- e2[1].views[e2[1].view].height.value
- );
- }
- });
-
- let zIndex = ordered.length + 1;
- let groundSet = false;
- ordered.forEach((entity) => {
- if (!groundSet && entity[1].priority < 0) {
- document.querySelector("#ground").style.zIndex = zIndex;
- zIndex -= 1;
- groundSet = true;
- }
-
- const element = document.querySelector("#entity-" + entity[0]);
- element.style.zIndex = zIndex;
- if (!dirtyOnly || entity[1].dirty) {
- updateEntityElement(entity[1], element, zIndex);
- entity[1].dirty = false;
- }
- zIndex -= 1;
-
- });
-
- if (!groundSet) {
- document.querySelector("#ground").style.zIndex = zIndex;
- }
-
- document.querySelector("#ground").style.top =
- pos2pix({ x: 0, y: 0 }).y + "px";
-
- drawRulers();
- }
-
- //#endregion
-
- function pickUnit() {
- if (!config.autoUnits) {
- return;
- }
-
- let type = null;
- let category = null;
-
- const heightSelect = document.querySelector("#options-height-unit");
-
- currentUnit = heightSelect.value;
-
- Object.keys(unitChoices).forEach((unitType) => {
- Object.keys(unitChoices[unitType]).forEach((unitCategory) => {
- if (unitChoices[unitType][unitCategory].includes(currentUnit)) {
- type = unitType;
- category = unitCategory;
- }
- });
- });
-
- // This should only happen if the unit selector isn't set up yet.
- // It doesn't really matter what goes into it.
- if (type === null || category === null) {
- return "meters";
- }
-
- const choices = unitChoices[type][category].map((unit) => {
- let value = config.height.toNumber(unit);
- if (value < 1) {
- value = 1 / value / value;
- }
- return [unit, value];
- });
-
- heightSelect.value = choices.sort((a, b) => {
- return a[1] - b[1];
- })[0][0];
-
- selectNewUnit();
- }
-
- //#region drawing
-
- function cleanRulers() {
- rulers = rulers.filter(ruler => {
- if (!ruler.entityKey) {
- return true;
- } else {
- return entities[ruler.entityKey] !== undefined;
- }
- });
- }
-
- function drawRulers() {
- cleanRulers();
- const canvas = document.querySelector("#rulers");
-
- /** @type {CanvasRenderingContext2D} */
-
- const ctx = canvas.getContext("2d");
-
- const deviceScale = window.devicePixelRatio;
-
- ctx.canvas.width = Math.floor(canvas.clientWidth * deviceScale);
- ctx.canvas.height = Math.floor(canvas.clientHeight * deviceScale);
-
- ctx.scale(deviceScale, deviceScale);
-
- rulers.concat(currentRuler ? [currentRuler] : []).forEach((rulerDef) => {
- let x0 = rulerDef.x0;
- let y0 = rulerDef.y0;
- let x1 = rulerDef.x1;
- let y1 = rulerDef.y1;
- if (rulerDef.entityKey !== null) {
- const entity = entities[rulerDef.entityKey];
- const entityElement = document.querySelector(
- "#entity-" + rulerDef.entityKey
- );
- x0 *= entity.scale;
- y0 *= entity.scale;
- x1 *= entity.scale;
- y1 *= entity.scale;
- x0 += parseFloat(entityElement.dataset.x);
- x1 += parseFloat(entityElement.dataset.x);
- y0 += parseFloat(entityElement.dataset.y);
- y1 += parseFloat(entityElement.dataset.y);
- }
- ctx.save();
- ctx.beginPath();
- const start = pos2pix({ x: x0, y: y0 });
- const end = pos2pix({ x: x1, y: y1 });
- ctx.moveTo(start.x, start.y);
- ctx.lineTo(end.x, end.y);
- ctx.lineWidth = 5;
- ctx.strokeStyle = "#f8f";
- ctx.stroke();
- const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
- ctx.fillStyle = "#eeeeee";
- ctx.font = "normal 24pt coda";
- ctx.translate(center.x, center.y);
- let angle = Math.atan2(end.y - start.y, end.x - start.x);
-
- if (angle < -Math.PI / 2) {
- angle += Math.PI;
- }
- if (angle > Math.PI / 2) {
- angle -= Math.PI;
- }
- ctx.rotate(angle);
- const offsetX = Math.cos(angle + Math.PI / 2);
- const offsetY = Math.sin(angle + Math.PI / 2);
-
- const distance = Math.sqrt(Math.pow(y1 - y0, 2) + Math.pow(x1 - x0, 2));
- const distanceInUnits = math
- .unit(distance, "meters")
- .to(document.querySelector("#options-height-unit").value);
- const textSize = ctx.measureText(
- distanceInUnits.format({ precision: 3 })
- );
- ctx.fillText(
- distanceInUnits.format({ precision: 3 }),
- -offsetX * 10 - textSize.width / 2,
- -offsetY * 10
- );
- ctx.restore();
- });
- }
-
- function drawScales(ifDirty = false) {
- const canvas = document.querySelector("#display");
-
- /** @type {CanvasRenderingContext2D} */
-
- const ctx = canvas.getContext("2d");
-
- const deviceScale = window.devicePixelRatio;
-
- ctx.canvas.width = Math.floor(canvas.clientWidth * deviceScale);
- ctx.canvas.height = Math.floor(canvas.clientHeight * deviceScale);
-
- ctx.scale(deviceScale, deviceScale);
-
- ctx.beginPath();
- ctx.rect(
- 0,
- 0,
- ctx.canvas.width / deviceScale,
- ctx.canvas.height / deviceScale
- );
-
- switch (config.background) {
- case "black":
- ctx.fillStyle = "#000";
- break;
- case "dark":
- ctx.fillStyle = "#111";
- break;
- case "medium":
- ctx.fillStyle = "#333";
- break;
- case "light":
- ctx.fillStyle = "#555";
- break;
- }
- ctx.fill();
-
- if (config.drawYAxis || config.drawAltitudes !== "none") {
- drawVerticalScale(ifDirty);
- }
- if (config.drawXAxis) {
- drawHorizontalScale(ifDirty);
- }
- }
-
- function drawVerticalScale(ifDirty = false) {
- if (ifDirty && !worldSizeDirty) return;
- function drawTicks(
- /** @type {CanvasRenderingContext2D} */ ctx,
- pixelsPer,
- heightPer
- ) {
- let total = heightPer.clone();
- total.value = config.y;
- let y = ctx.canvas.clientHeight - 50;
-
- let offset = total.toNumber("meters") % heightPer.toNumber("meters");
-
- y += (offset / heightPer.toNumber("meters")) * pixelsPer;
- total = math.subtract(total, math.unit(offset, "meters"));
-
- for (; y >= 50; y -= pixelsPer) {
- drawTick(ctx, 50, y, total.format({ precision: 3 }));
- total = math.add(total, heightPer);
- }
- }
-
- function drawTick(
- /** @type {CanvasRenderingContext2D} */ ctx,
- x,
- y,
- label,
- flipped = false
- ) {
- const oldStroke = ctx.strokeStyle;
- const oldFill = ctx.fillStyle;
-
- x = Math.round(x);
- y = Math.round(y);
-
- ctx.beginPath();
- ctx.moveTo(x, y);
- ctx.lineTo(x + 20, y);
- ctx.strokeStyle = "#000000";
- ctx.stroke();
-
- ctx.beginPath();
- ctx.moveTo(x + 20, y);
- ctx.lineTo(ctx.canvas.clientWidth - 70, y);
- if (flipped) {
- ctx.strokeStyle = "#666666";
- } else {
- ctx.strokeStyle = "#aaaaaa";
- }
-
- ctx.stroke();
-
- ctx.beginPath();
- ctx.moveTo(ctx.canvas.clientWidth - 70, y);
- ctx.lineTo(ctx.canvas.clientWidth - 50, y);
- ctx.strokeStyle = "#000000";
- ctx.stroke();
-
- const oldFont = ctx.font;
- ctx.font = "normal 24pt coda";
- ctx.fillStyle = "#dddddd";
-
- ctx.beginPath();
- if (flipped) {
- ctx.textAlign = "end";
- ctx.fillText(label, ctx.canvas.clientWidth - 70, y + 35);
- } else {
- ctx.fillText(label, x + 20, y + 35);
- }
- ctx.textAlign = "start";
-
- ctx.font = oldFont;
- ctx.strokeStyle = oldStroke;
- ctx.fillStyle = oldFill;
- }
-
- function drawAltitudeLine(ctx, height, label) {
- const pixelScale =
- (ctx.canvas.clientHeight - 100) / config.height.toNumber("meters");
- const y =
- ctx.canvas.clientHeight -
- 50 -
- (height.toNumber("meters") - config.y) * pixelScale;
- const offsetY = y + getVerticalOffset() * pixelScale;
- if (offsetY < ctx.canvas.clientHeight - 100) {
- drawTick(ctx, 50, y, label, true);
- }
- }
-
- const canvas = document.querySelector("#display");
-
- /** @type {CanvasRenderingContext2D} */
-
- const ctx = canvas.getContext("2d");
-
- const pixelScale =
- (ctx.canvas.clientHeight - 100) / config.height.toNumber();
-
- let pixelsPer = pixelScale;
- heightPer = 1;
-
- if (pixelsPer < config.minLineSize) {
- const factor = math.ceil(config.minLineSize / pixelsPer);
- heightPer *= factor;
- pixelsPer *= factor;
- }
-
- if (pixelsPer > config.maxLineSize) {
- const factor = math.ceil(pixelsPer / config.maxLineSize);
- heightPer /= factor;
- pixelsPer /= factor;
- }
-
- if (heightPer == 0) {
- console.error(
- "The world size is invalid! Refusing to draw the scale..."
- );
- return;
- }
- heightPer = math.unit(
- heightPer,
- document.querySelector("#options-height-unit").value
- );
-
- ctx.beginPath();
- ctx.moveTo(50, 50);
- ctx.lineTo(50, ctx.canvas.clientHeight - 50);
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
- ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
- ctx.stroke();
-
- if (config.drawYAxis) {
- drawTicks(ctx, pixelsPer, heightPer);
- }
-
- if (config.drawAltitudes == "atmosphere" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(8, "km"), "Troposphere");
- drawAltitudeLine(ctx, math.unit(17.5, "km"), "Ozone Layer");
- drawAltitudeLine(ctx, math.unit(50, "km"), "Stratosphere");
- drawAltitudeLine(ctx, math.unit(85, "km"), "Mesosphere");
- drawAltitudeLine(ctx, math.unit(675, "km"), "Thermosphere");
- drawAltitudeLine(ctx, math.unit(10000, "km"), "Exosphere");
- }
-
- if (config.drawAltitudes == "orbits" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(7, "miles"), "Cruising Altitude");
- drawAltitudeLine(
- ctx,
- math.unit(100, "km"),
- "Edge of Space (Kármán line)"
- );
- drawAltitudeLine(ctx, math.unit(211.3, "miles"), "Space Station");
- drawAltitudeLine(ctx, math.unit(369.7, "miles"), "Hubble Telescope");
- drawAltitudeLine(ctx, math.unit(1500, "km"), "Low Earth Orbit");
- drawAltitudeLine(ctx, math.unit(20350, "km"), "GPS Satellites");
- drawAltitudeLine(ctx, math.unit(35786, "km"), "Geosynchronous Orbit");
- drawAltitudeLine(ctx, math.unit(238900, "miles"), "Lunar Orbit");
- drawAltitudeLine(ctx, math.unit(57.9e6, "km"), "Orbit of Mercury");
- drawAltitudeLine(ctx, math.unit(108.2e6, "km"), "Orbit of Venus");
- drawAltitudeLine(ctx, math.unit(1, "AU"), "Orbit of Earth");
- drawAltitudeLine(ctx, math.unit(227.9e6, "km"), "Orbit of Mars");
- drawAltitudeLine(ctx, math.unit(778.6e6, "km"), "Orbit of Jupiter");
- drawAltitudeLine(ctx, math.unit(1433.5e6, "km"), "Orbit of Saturn");
- drawAltitudeLine(ctx, math.unit(2872.5e6, "km"), "Orbit of Uranus");
- drawAltitudeLine(ctx, math.unit(4495.1e6, "km"), "Orbit of Neptune");
- drawAltitudeLine(ctx, math.unit(5906.4e6, "km"), "Orbit of Pluto");
- drawAltitudeLine(ctx, math.unit(2.7, "AU"), "Asteroid Belt");
- drawAltitudeLine(ctx, math.unit(123, "AU"), "Heliopause");
- drawAltitudeLine(ctx, math.unit(26e3, "lightyears"), "Orbit of Sol");
- }
-
- if (config.drawAltitudes == "weather" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(1000, "meters"), "Low-level Clouds");
- drawAltitudeLine(ctx, math.unit(3000, "meters"), "Mid-level Clouds");
- drawAltitudeLine(ctx, math.unit(10000, "meters"), "High-level Clouds");
- drawAltitudeLine(
- ctx,
- math.unit(20, "km"),
- "Polar Stratospheric Clouds"
- );
- drawAltitudeLine(ctx, math.unit(80, "km"), "Noctilucent Clouds");
- drawAltitudeLine(ctx, math.unit(100, "km"), "Aurora");
- }
-
- if (config.drawAltitudes == "water" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(12100, "feet"), "Average Ocean Depth");
- drawAltitudeLine(ctx, math.unit(8376, "meters"), "Milkwaukee Deep");
- drawAltitudeLine(ctx, math.unit(10984, "meters"), "Challenger Deep");
- drawAltitudeLine(ctx, math.unit(5550, "meters"), "Molloy Deep");
- drawAltitudeLine(ctx, math.unit(7290, "meters"), "Sunda Deep");
- drawAltitudeLine(ctx, math.unit(592, "meters"), "Crater Lake");
- drawAltitudeLine(ctx, math.unit(7.5, "meters"), "Littoral Zone");
- drawAltitudeLine(ctx, math.unit(140, "meters"), "Continental Shelf");
- }
-
- if (config.drawAltitudes == "geology" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(35, "km"), "Crust");
- drawAltitudeLine(ctx, math.unit(670, "km"), "Upper Mantle");
- drawAltitudeLine(ctx, math.unit(2890, "km"), "Lower Mantle");
- drawAltitudeLine(ctx, math.unit(5150, "km"), "Outer Core");
- drawAltitudeLine(ctx, math.unit(6370, "km"), "Inner Core");
- }
-
- if (
- config.drawAltitudes == "thicknesses" ||
- config.drawAltitudes == "all"
- ) {
- drawAltitudeLine(ctx, math.unit(0.335, "nm"), "Monolayer Graphene");
- drawAltitudeLine(ctx, math.unit(3, "um"), "Spider Silk");
- drawAltitudeLine(ctx, math.unit(0.07, "mm"), "Human Hair");
- drawAltitudeLine(ctx, math.unit(0.1, "mm"), "Sheet of Paper");
- drawAltitudeLine(ctx, math.unit(0.5, "mm"), "Yarn");
- drawAltitudeLine(ctx, math.unit(0.0155, "inches"), "Thread");
- drawAltitudeLine(ctx, math.unit(0.1, "um"), "Gold Leaf");
- drawAltitudeLine(ctx, math.unit(35, "um"), "PCB Trace");
- }
-
- if (config.drawAltitudes == "airspaces" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(18000, "feet"), "Class A");
- drawAltitudeLine(ctx, math.unit(14500, "feet"), "Class E");
- drawAltitudeLine(ctx, math.unit(10000, "feet"), "Class B");
- drawAltitudeLine(ctx, math.unit(4000, "feet"), "Class C");
- drawAltitudeLine(ctx, math.unit(2500, "feet"), "Class D");
- }
-
- if (config.drawAltitudes == "races" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(100, "meters"), "100m Dash");
- drawAltitudeLine(ctx, math.unit(26.2188 / 2, "miles"), "Half Marathon");
- drawAltitudeLine(ctx, math.unit(26.2188, "miles"), "Marathon");
- drawAltitudeLine(ctx, math.unit(161.734, "miles"), "Monaco Grand Prix");
- drawAltitudeLine(ctx, math.unit(500, "miles"), "Daytona 500");
- drawAltitudeLine(ctx, math.unit(2121.6, "miles"), "Tour de France");
- }
-
- if (
- config.drawAltitudes == "olympic-records" ||
- config.drawAltitudes == "all"
- ) {
- drawAltitudeLine(ctx, math.unit(2.39, "meters"), "High Jump");
- drawAltitudeLine(ctx, math.unit(6.03, "meters"), "Pole Vault");
- drawAltitudeLine(ctx, math.unit(8.9, "meters"), "Long Jump");
- drawAltitudeLine(ctx, math.unit(18.09, "meters"), "Triple Jump");
- drawAltitudeLine(ctx, math.unit(23.3, "meters"), "Shot Put");
- drawAltitudeLine(ctx, math.unit(72.3, "meters"), "Discus Throw");
- drawAltitudeLine(ctx, math.unit(84.8, "meters"), "Hammer Throw");
- drawAltitudeLine(ctx, math.unit(90.57, "meters"), "Javelin Throw");
- }
-
- if (config.drawAltitudes == "d&d-sizes" || config.drawAltitudes == "all") {
- drawAltitudeLine(ctx, math.unit(0.375, "feet"), "Fine");
- drawAltitudeLine(ctx, math.unit(0.75, "feet"), "Dimnutive");
- drawAltitudeLine(ctx, math.unit(1.5, "feet"), "Tiny");
- drawAltitudeLine(ctx, math.unit(3, "feet"), "Small");
- drawAltitudeLine(ctx, math.unit(6, "feet"), "Medium");
- drawAltitudeLine(ctx, math.unit(12, "feet"), "Large");
- drawAltitudeLine(ctx, math.unit(24, "feet"), "Huge");
- drawAltitudeLine(ctx, math.unit(48, "feet"), "Gargantuan");
- drawAltitudeLine(ctx, math.unit(96, "feet"), "Colossal");
- }
- }
-
- // this is a lot of copypizza...
-
- function drawHorizontalScale(ifDirty = false) {
- if (ifDirty && !worldSizeDirty) return;
- function drawTicks(
- /** @type {CanvasRenderingContext2D} */ ctx,
- pixelsPer,
- heightPer
- ) {
- let total = heightPer.clone();
- total.value = math.unit(-config.x, "meters").toNumber(config.unit);
-
- // further adjust it to put the current position in the center
-
- total.value -=
- ((heightPer.toNumber("meters") / pixelsPer) * (canvasWidth + 50)) /
- 2;
- let x = ctx.canvas.clientWidth - 50;
-
- let offset = total.toNumber("meters") % heightPer.toNumber("meters");
-
- x += (offset / heightPer.toNumber("meters")) * pixelsPer;
- total = math.subtract(total, math.unit(offset, "meters"));
-
- for (; x >= 50 - pixelsPer; x -= pixelsPer) {
- // negate it so that the left side is negative
- drawTick(
- ctx,
- x,
- 50,
- math.multiply(-1, total).format({ precision: 3 })
- );
- total = math.add(total, heightPer);
- }
- }
-
- function drawTick(
- /** @type {CanvasRenderingContext2D} */ ctx,
- x,
- y,
- label
- ) {
- ctx.save();
-
- x = Math.round(x);
- y = Math.round(y);
-
- ctx.beginPath();
- ctx.moveTo(x, y);
- ctx.lineTo(x, y + 20);
- ctx.strokeStyle = "#000000";
- ctx.stroke();
-
- ctx.beginPath();
- ctx.moveTo(x, y + 20);
- ctx.lineTo(x, ctx.canvas.clientHeight - 70);
- ctx.strokeStyle = "#aaaaaa";
- ctx.stroke();
-
- ctx.beginPath();
- ctx.moveTo(x, ctx.canvas.clientHeight - 70);
- ctx.lineTo(x, ctx.canvas.clientHeight - 50);
- ctx.strokeStyle = "#000000";
- ctx.stroke();
-
- const oldFont = ctx.font;
- ctx.font = "normal 24pt coda";
- ctx.fillStyle = "#dddddd";
-
- ctx.beginPath();
- ctx.fillText(label, x + 35, y + 20);
-
- ctx.restore();
- }
- const canvas = document.querySelector("#display");
-
- /** @type {CanvasRenderingContext2D} */
-
- const ctx = canvas.getContext("2d");
-
- let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
-
- heightPer = 1;
-
- if (pixelsPer < config.minLineSize * 2) {
- const factor = math.ceil((config.minLineSize * 2) / pixelsPer);
- heightPer *= factor;
- pixelsPer *= factor;
- }
-
- if (pixelsPer > config.maxLineSize * 2) {
- const factor = math.ceil(pixelsPer / 2 / config.maxLineSize);
- heightPer /= factor;
- pixelsPer /= factor;
- }
-
- if (heightPer == 0) {
- console.error(
- "The world size is invalid! Refusing to draw the scale..."
- );
- return;
- }
- heightPer = math.unit(
- heightPer,
- document.querySelector("#options-height-unit").value
- );
-
- ctx.beginPath();
- ctx.moveTo(0, 50);
- ctx.lineTo(ctx.canvas.clientWidth, 50);
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(0, ctx.canvas.clientHeight - 50);
- ctx.lineTo(ctx.canvas.clientWidth, ctx.canvas.clientHeight - 50);
- ctx.stroke();
-
- drawTicks(ctx, pixelsPer, heightPer);
- }
-
- //#endregion
-
- //#region entities
-
- // Entities are generated as needed, and we make a copy
- // every time - the resulting objects get mutated, after all.
- // But we also want to be able to read some information without
- // calling the constructor -- e.g. making a list of authors and
- // owners. So, this function is used to generate that information.
- // It is invoked like makeEntity so that it can be dropped in easily,
- // but returns an object that lets you construct many copies of an entity,
- // rather than creating a new entity.
- function createEntityMaker(info, views, sizes, forms) {
- const maker = {};
-
- maker.name = info.name;
- maker.info = info;
- maker.sizes = sizes;
-
- maker.constructor = () => makeEntity(info, views, sizes, forms);
- maker.authors = [];
- maker.owners = [];
- maker.nsfw = false;
-
- Object.values(views).forEach((view) => {
- const authors = authorsOf(view.image.source);
- if (authors) {
- authors.forEach((author) => {
- if (maker.authors.indexOf(author) == -1) {
- maker.authors.push(author);
- }
- });
- }
- const owners = ownersOf(view.image.source);
- if (owners) {
- owners.forEach((owner) => {
- if (maker.owners.indexOf(owner) == -1) {
- maker.owners.push(owner);
- }
- });
- }
-
- if (isNsfw(view.image.source)) {
- maker.nsfw = true;
- }
- });
-
- return maker;
- }
-
- // Sets up the getters for each attribute. This needs to be
- // re-run if we add new attributes to an entity, so it's
- // broken out from makeEntity.
-
- function defineAttributeGetters(view) {
- Object.entries(view.attributes).forEach(([key, val]) => {
-
- if (val.defaultUnit !== undefined) {
- view.units[key] = val.defaultUnit;
- }
-
- if (view[key] !== undefined) {
- return;
- }
-
- Object.defineProperty(view, key, {
- get: function () {
- return math.multiply(
- Math.pow(
- this.parent.scale,
- this.attributes[key].power
- ),
- this.attributes[key].base
- );
- },
- set: function (value) {
- const newScale = Math.pow(
- math.divide(value, this.attributes[key].base),
- 1 / this.attributes[key].power
- );
- this.parent.scale = newScale;
- },
- });
- });
- }
-
- // This function serializes and parses its arguments to avoid sharing
- // references to a common object. This allows for the objects to be
- // safely mutated.
-
- function makeEntity(info, views, sizes, forms = {}) {
- const entityTemplate = {
- name: info.name,
- identifier: info.name,
- scale: 1,
- rotation: 0,
- flipped: false,
- info: JSON.parse(JSON.stringify(info)),
- views: JSON.parse(JSON.stringify(views), math.reviver),
- sizes:
- sizes === undefined
- ? []
- : JSON.parse(JSON.stringify(sizes), math.reviver),
- forms: forms,
- init: function () {
- const entity = this;
-
- Object.entries(this.forms).forEach(([formKey, form]) => {
- if (form.default) {
- this.defaultForm = formKey;
- }
- });
-
- Object.entries(this.forms).forEach(([formKey, form]) => {
- if (this.defaultForm === undefined) {
- this.defaultForm = formKey;
- }
- });
-
- Object.entries(this.views).forEach(([viewKey, view]) => {
- view.parent = this;
- if (this.defaultView === undefined) {
- this.defaultView = viewKey;
- this.view = viewKey;
- this.form = view.form;
- }
-
- if (view.default) {
- if (forms === {} || this.defaultForm === view.form || this.defaultForm === undefined) {
- this.defaultView = viewKey;
- this.view = viewKey;
- this.form = view.form;
- }
- }
-
- // to remember the units the user last picked
- // also handles default unit overrides
-
- view.units = {};
-
- if (
- config.autoMass !== "off" &&
- view.attributes.weight === undefined
- ) {
- let base = undefined;
- switch (config.autoMass) {
- case "human":
- baseMass = math.unit(150, "lbs");
- baseHeight = math.unit(5.917, "feet");
- break;
- case "quadruped at shoulder":
- baseMass = math.unit(80, "lbs");
- baseHeight = math.unit(30, "inches");
- break;
- }
-
- const ratio = math.divide(
- view.attributes.height.base,
- baseHeight
- );
- view.attributes.weight = {
- name: "Mass",
- power: 3,
- type: "mass",
- base: math.multiply(baseMass, Math.pow(ratio, 3)),
- };
- }
-
- if (
- config.autoFoodIntake &&
- view.attributes.weight !== undefined &&
- view.attributes.energyIntake === undefined
- ) {
- view.attributes.energyIntake = {
- name: "Food Intake",
- power: (3 * 3) / 4,
- type: "energy",
- base: math.unit(
- 2000 *
- Math.pow(
- view.attributes.weight.base.toNumber(
- "lbs"
- ) / 150,
- 3 / 4
- ),
- "kcal"
- ),
- };
- }
-
- if (
- config.autoCaloricValue &&
- view.attributes.weight !== undefined &&
- view.attributes.energyWorth === undefined
- ) {
- view.attributes.energyValue = {
- name: "Caloric Value",
- power: 3,
- type: "energy",
- base: math.unit(
- 860 * view.attributes.weight.base.toNumber("lbs"),
- "kcal"
- ),
- };
- }
-
- if (
- config.autoPreyCapacity !== "off" &&
- view.attributes.weight !== undefined &&
- view.attributes.preyCapacity === undefined
- ) {
- view.attributes.preyCapacity = {
- name: "Prey Capacity",
- power: 3,
- type: "volume",
- base: math.unit(
- ((config.autoPreyCapacity == "same-size"
- ? 1
- : 0.05) *
- view.attributes.weight.base.toNumber("lbs")) /
- 150,
- "people"
- ),
- };
- }
-
- if (
- config.autoSwallowSize !== "off" &&
- view.attributes.swallowSize === undefined
- ) {
- let size;
- switch (config.autoSwallowSize) {
- case "casual": size = math.unit(20, "mL"); break;
- case "big-swallow": size = math.unit(50, "mL"); break;
- case "same-size-predator": size = math.unit(1, "people"); break;
- }
- view.attributes.swallowSize = {
- name: "Swallow Size",
- power: 3,
- type: "volume",
- base: math.multiply(size, math.pow(math.divide(view.attributes.height.base, math.unit(6, "feet")), 3))
- };
- }
-
- defineAttributeGetters(view);
- });
-
- this.sizes.forEach((size) => {
- if (size.default === true) {
- if (Object.keys(forms).length > 0) {
- if (this.defaultForm !== size.form && !size.allForms) {
- return;
- }
- }
- this.views[this.defaultView].height = size.height;
- this.size = size;
- }
- });
-
- if (this.size === undefined && this.sizes.length > 0) {
- this.views[this.defaultView].height = this.sizes[0].height;
- this.size = this.sizes[0];
- console.warn("No default size set for " + info.name);
- } else if (this.sizes.length == 0) {
- this.sizes = [
- {
- name: "Normal",
- height: this.views[this.defaultView].height,
- },
- ];
- this.size = this.sizes[0];
- }
-
- this.desc = {};
-
- Object.entries(this.info).forEach(([key, value]) => {
- Object.defineProperty(this.desc, key, {
- get: function () {
- let text = value.text;
-
- if (entity.views[entity.view].info) {
- if (entity.views[entity.view].info[key]) {
- text = combineInfo(
- text,
- entity.views[entity.view].info[key]
- );
- }
- }
-
- if (entity.size.info) {
- if (entity.size.info[key]) {
- text = combineInfo(text, entity.size.info[key]);
- }
- }
-
- return { title: value.title, text: text };
- },
- });
- });
-
- Object.defineProperty(this, "currentView", {
- get: function () {
- return entity.views[entity.view];
- },
- });
-
- this.formViews = {};
- this.formSizes = {};
-
- this.formSizesMatch = this.sizes.every(x => x.allForms);
-
-
-
- Object.entries(views).forEach(([key, value]) => {
- if (value.default) {
- this.formViews[value.form] = key;
- }
- });
-
- Object.entries(views).forEach(([key, value]) => {
- if (this.formViews[value.form] === undefined) {
- this.formViews[value.form] = key;
- }
- });
-
- this.sizes.forEach((size) => {
- if (size.default) {
- if (size.allForms) {
- Object.keys(forms).forEach(form => {
- if (!forms[form].ignoreAllFormSizes) {
- this.formSizes[form] = size;
- }
- });
- } else {
- this.formSizes[size.form] = size;
- }
- }
- });
-
- this.sizes.forEach((size) => {
- if (this.formSizes[size.form] === undefined) {
- this.formSizes[size.form] = size;
- }
- });
-
- Object.values(views).forEach((view) => {
- if (this.formSizes[view.form] === undefined) {
- this.formSizes[view.form] = {
- name: "Normal",
- height: view.attributes.height.base,
- default: true,
- form: view.form,
- };
- }
- });
-
- delete this.init;
- return this;
- },
- }.init();
-
- return entityTemplate;
- }
-
- //#endregion
-
- function combineInfo(existing, next) {
- switch (next.mode) {
- case "replace":
- return next.text;
- case "prepend":
- return next.text + existing;
- case "append":
- return existing + next.text;
- }
-
- return existing;
- }
-
- //#region interaction
-
- function clickDown(target, x, y) {
- clicked = target;
- movingInBounds = false;
- const rect = target.getBoundingClientRect();
- let entX = document.querySelector("#entities").getBoundingClientRect().x;
- let entY = document.querySelector("#entities").getBoundingClientRect().y;
- dragOffsetX = x - rect.left + entX;
- dragOffsetY = y - rect.top + entY;
-
- x = x - dragOffsetX;
- y = y - dragOffsetY;
- if (x >= 0 && x <= canvasWidth && y >= 0 && y <= canvasHeight) {
- movingInBounds = true;
- }
-
- clickTimeout = setTimeout(() => {
- dragging = true;
- }, 200);
- target.classList.add("no-transition");
- }
-
- // could we make this actually detect the menu area?
-
- function hoveringInDeleteArea(e) {
- return e.clientY < document.querySelector("#menubar").clientHeight;
- }
-
- function clickUp(e) {
- if (e.which != 1) {
- return;
- }
-
- clearTimeout(clickTimeout);
-
- if (clicked) {
- clicked.classList.remove("no-transition");
- if (dragging) {
- dragging = false;
-
- if (hoveringInDeleteArea(e)) {
- removeEntity(clicked);
- document
- .querySelector("#menubar")
- .classList.remove("hover-delete");
- }
- } else {
- select(clicked);
- }
- clicked = null;
- }
- }
-
- function deselect(e) {
- if (rulerMode) {
- return;
- }
- if (e !== undefined && e.which != 1) {
- return;
- }
-
- if (selected) {
- selected.classList.remove("selected");
- }
- if (prevSelected) {
- prevSelected.classList.remove("prevSelected");
- }
-
- document.getElementById("options-selected-entity-none").selected =
- "selected";
- document.getElementById("delete-entity").style.display = "none";
- clearAttribution();
-
- selected = null;
- clearViewList();
- clearEntityOptions();
- clearViewOptions();
-
- document.querySelector("#delete-entity").disabled = true;
-
- document.querySelector("#grow").disabled = true;
- document.querySelector("#shrink").disabled = true;
- document.querySelector("#fit").disabled = true;
- }
-
- function select(target) {
- if (prevSelected !== null) {
- prevSelected.classList.remove("prevSelected");
- }
-
- prevSelected = selected;
- prevSelectedEntity = selectedEntity;
- deselect();
- selected = target;
- selectedEntity = entities[target.dataset.key];
-
- updateInfo();
-
- document.getElementById(
- "options-selected-entity-" + target.dataset.key
- ).selected = "selected";
- document.getElementById("delete-entity").style.display = "";
-
- if (
- prevSelected !== null &&
- config.showRatios &&
- selected !== prevSelected
- ) {
- prevSelected.classList.add("prevSelected");
- }
- selected.classList.add("selected");
-
- displayAttribution(selectedEntity.views[selectedEntity.view].image.source);
-
- configFormList(selectedEntity, selectedEntity.form);
- configViewList(selectedEntity, selectedEntity.view);
- configEntityOptions(selectedEntity, selectedEntity.view);
- configViewOptions(selectedEntity, selectedEntity.view);
-
- document.querySelector("#delete-entity").disabled = false;
-
- document.querySelector("#grow").disabled = false;
- document.querySelector("#shrink").disabled = false;
- document.querySelector("#fit").disabled = false;
- }
-
- //#endregion
-
- //#region ui
-
- function configFormList(entity, selectedForm) {
- const label = document.querySelector("#options-label-form");
- const list = document.querySelector("#entity-form");
-
- list.innerHTML = "";
-
- if (selectedForm === undefined) {
- label.style.display = "none";
- list.style.display = "none";
- return;
- }
-
- label.style.display = "block";
- list.style.display = "block";
-
- Object.keys(entity.forms).forEach((form) => {
- const option = document.createElement("option");
- option.innerText = entity.forms[form].name;
- option.value = form;
-
- if (form === selectedForm) {
- option.selected = true;
- }
-
- list.appendChild(option);
- });
- }
-
- function configViewList(entity, selectedView) {
- const list = document.querySelector("#entity-view");
-
- list.innerHTML = "";
-
- list.style.display = "block";
-
- Object.keys(entity.views).forEach((view) => {
- if (Object.keys(entity.forms).length > 0) {
- if (entity.views[view].form !== undefined && entity.views[view].form !== entity.form) {
- return;
- }
- }
- const option = document.createElement("option");
- option.innerText = entity.views[view].name;
- option.value = view;
-
- if (isNsfw(entity.views[view].image.source)) {
- option.classList.add("nsfw");
- }
-
- if (view === selectedView) {
- option.selected = true;
-
- if (option.classList.contains("nsfw")) {
- list.classList.add("nsfw");
- } else {
- list.classList.remove("nsfw");
- }
- }
-
- list.appendChild(option);
- });
- }
-
- function clearViewList() {
- const list = document.querySelector("#entity-view");
-
- list.innerHTML = "";
- list.style.display = "none";
- }
-
- function updateWorldOptions(entity, view) {
- const heightInput = document.querySelector("#options-height-value");
- const heightSelect = document.querySelector("#options-height-unit");
-
- const converted = config.height.toNumber(heightSelect.value);
-
- setNumericInput(heightInput, converted);
- }
-
- function configEntityOptions(entity, view) {
- const holder = document.querySelector("#options-entity");
-
- document.querySelector("#entity-category-header").style.display = "block";
- document.querySelector("#entity-category").style.display = "block";
-
- holder.innerHTML = "";
-
- const scaleLabel = document.createElement("div");
- scaleLabel.classList.add("options-label");
- scaleLabel.innerText = "Scale";
-
- const scaleRow = document.createElement("div");
- scaleRow.classList.add("options-row");
-
- const scaleInput = document.createElement("input");
- scaleInput.classList.add("options-field-numeric");
- scaleInput.id = "options-entity-scale";
-
- scaleInput.addEventListener("change", (e) => {
- try {
- const newScale =
- e.target.value == 0 ? 1 : math.evaluate(e.target.value);
- if (typeof newScale !== "number") {
- toast("Invalid input: scale can't have any units!");
- return;
- }
- entity.scale = newScale;
- } catch {
- toast("Invalid input: could not parse " + e.target.value);
- }
-
- entity.dirty = true;
- if (config.autoFit) {
- fitWorld();
- } else {
- updateSizes(true);
- }
- updateEntityOptions(entity, entity.view);
- updateViewOptions(entity, entity.view);
- });
-
- scaleInput.addEventListener("keydown", (e) => {
- e.stopPropagation();
- });
-
- setNumericInput(scaleInput, entity.scale);
-
- scaleRow.appendChild(scaleInput);
- holder.appendChild(scaleLabel);
- holder.appendChild(scaleRow);
-
- const nameLabel = document.createElement("div");
- nameLabel.classList.add("options-label");
- nameLabel.innerText = "Name";
-
- const nameRow = document.createElement("div");
- nameRow.classList.add("options-row");
-
- const nameInput = document.createElement("input");
- nameInput.classList.add("options-field-text");
- nameInput.value = entity.name;
-
- nameInput.addEventListener("input", (e) => {
- entity.name = e.target.value;
- entity.dirty = true;
- updateSizes(true);
- });
-
- nameInput.addEventListener("keydown", (e) => {
- e.stopPropagation();
- });
-
- nameRow.appendChild(nameInput);
-
- holder.appendChild(nameLabel);
- holder.appendChild(nameRow);
-
- configSizeList(entity);
-
- document.querySelector("#options-order-display").innerText =
- entity.priority;
- document.querySelector("#options-brightness-display").innerText =
- entity.brightness;
- document.querySelector("#options-ordering").style.display = "flex";
- }
-
- function configSizeList(entity) {
- const defaultHolder = document.querySelector("#options-entity-defaults");
-
- defaultHolder.innerHTML = "";
-
- entity.sizes.forEach((defaultInfo) => {
- if (Object.keys(entity.forms).length > 0) {
- if (!defaultInfo.allForms && defaultInfo.form !== entity.form) {
- return;
- }
- if (defaultInfo.allForms && entity.forms[entity.form].ignoreAllFormSizes) {
- return;
- }
- }
- const button = document.createElement("button");
- button.classList.add("options-button");
-
- button.innerText = defaultInfo.name;
-
- button.addEventListener("click", (e) => {
- if (Object.keys(entity.forms).length > 0) {
- entity.views[entity.formViews[entity.form]].height =
- defaultInfo.height;
- } else {
- entity.views[entity.defaultView].height = defaultInfo.height;
- }
- entity.dirty = true;
- updateEntityOptions(entity, entity.view);
- updateViewOptions(entity, entity.view);
- if (!checkFitWorld()) {
- updateSizes(true);
- }
-
- if (config.autoFitSize) {
- let targets = {};
- targets[selected.dataset.key] = entities[selected.dataset.key];
- fitEntities(targets);
- }
- });
-
- defaultHolder.appendChild(button);
- });
- }
-
- function updateEntityOptions(entity, view) {
- const scaleInput = document.querySelector("#options-entity-scale");
- setNumericInput(scaleInput, entity.scale);
-
- document.querySelector("#options-order-display").innerText =
- entity.priority;
- document.querySelector("#options-brightness-display").innerText =
- entity.brightness;
- }
-
- function clearEntityOptions() {
- document.querySelector("#entity-category-header").style.display = "none";
- document.querySelector("#entity-category").style.display = "none";
- /*
- const holder = document.querySelector("#options-entity");
-
- holder.innerHTML = "";
-
- document.querySelector("#options-entity-defaults").innerHTML = "";
- document.querySelector("#options-ordering").style.display = "none";
- document.querySelector("#options-ordering").style.display = "none";*/
- }
-
- function configViewOptions(entity, view) {
- const holder = document.querySelector("#options-view");
-
- document.querySelector("#view-category-header").style.display = "block";
- document.querySelector("#view-category").style.display = "block";
-
- holder.innerHTML = "";
-
- Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
- if (val.editing) {
- const name = document.createElement("input");
- name.placeholder = "Name";
- name.value = val.name;
- holder.appendChild(name);
-
- holder.addEventListener("keydown", (e) => {
- e.stopPropagation();
- });
-
- const input = document.createElement("input");
- input.placeholder = "Measurement (e.g. '3 feet')";
- input.value = val.text;
- holder.appendChild(input);
-
- input.addEventListener("keydown", (e) => {
- e.stopPropagation();
- });
-
- const button = document.createElement("button");
- button.innerText = "Confirm";
- holder.appendChild(button);
-
- button.addEventListener("click", e => {
- let unit;
- try {
- unit = math.unit(input.value);
- } catch {
- toast("Invalid unit: " + input.value);
- return;
- }
-
- const unitType = typeOfUnit(unit);
-
- if (unitType === null) {
- toast("Unit must be one of length, area, volume, mass, or energy.");
- return;
- }
-
- const power = unitPowers[unitType];
-
- const baseValue = math.multiply(unit, math.pow(1 / entity.scale, power));
-
- entity.views[view].attributes[key] = {
- name: name.value,
- power: power,
- type: unitType,
- base: baseValue,
- custom: true
- };
-
- // since we might have changed unit types, we should
- // clear this.
- entity.currentView.units[key] = undefined;
-
- defineAttributeGetters(entity.views[view]);
- configViewOptions(entity, view);
- updateSizes();
- });
- } else {
- const label = document.createElement("div");
- label.classList.add("options-label");
- label.innerText = val.name;
-
- holder.appendChild(label);
- if (config.editDefaultAttributes || val.custom) {
- const editButton = document.createElement("button");
- editButton.classList.add("attribute-edit-button");
- editButton.innerText = "Edit Attribute";
- editButton.addEventListener("click", e => {
- entity.currentView.attributes[key] = {
- name: val.name,
- text: entity.currentView[key],
- editing: true
- }
- configViewOptions(entity, view);
- });
-
- holder.appendChild(editButton);
- }
-
- if (val.custom) {
- const deleteButton = document.createElement("button");
- deleteButton.classList.add("attribute-edit-button");
- deleteButton.innerText = "Delete Attribute";
- deleteButton.addEventListener("click", e => {
- delete entity.currentView.attributes[key];
- configViewOptions(entity, view);
- });
-
- holder.appendChild(deleteButton);
- }
-
- const row = document.createElement("div");
- row.classList.add("options-row");
-
- holder.appendChild(row);
-
- const input = document.createElement("input");
- input.classList.add("options-field-numeric");
- input.id = "options-view-" + key + "-input";
-
- const select = document.createElement("select");
- select.classList.add("options-field-unit");
- select.id = "options-view-" + key + "-select";
-
- Object.entries(unitChoices[val.type]).forEach(([group, entries]) => {
- const optGroup = document.createElement("optgroup");
- optGroup.label = group;
- select.appendChild(optGroup);
- entries.forEach((entry) => {
- const option = document.createElement("option");
- option.innerText = entry;
- if (entry == defaultUnits[val.type][config.units]) {
- option.selected = true;
- }
- select.appendChild(option);
- });
- });
-
- input.addEventListener("change", (e) => {
- const raw_value = input.value == 0 ? 1 : input.value;
- let value;
- try {
- value = math.evaluate(raw_value).toNumber(select.value);
- } catch {
- try {
- value = math.evaluate(input.value);
- if (typeof value !== "number") {
- toast(
- "Invalid input: " +
- value.format() +
- " can't convert to " +
- select.value
- );
- value = undefined;
- }
- } catch {
- toast("Invalid input: could not parse: " + input.value);
- value = undefined;
- }
- }
- if (value === undefined) {
- return;
- }
- input.value = value;
- entity.views[view][key] = math.unit(value, select.value);
- entity.dirty = true;
- if (config.autoFit) {
- fitWorld();
- } else {
- updateSizes(true);
- }
- updateEntityOptions(entity, view);
- updateViewOptions(entity, view, key);
- });
-
- input.addEventListener("keydown", (e) => {
- e.stopPropagation();
- });
-
- if (entity.currentView.units[key]) {
- select.value = entity.currentView.units[key];
- } else {
- entity.currentView.units[key] = select.value;
- }
-
- select.dataset.oldUnit = select.value;
-
- setNumericInput(input, entity.views[view][key].toNumber(select.value));
-
- // TODO does this ever cause a change in the world?
- select.addEventListener("input", (e) => {
- const value = input.value == 0 ? 1 : input.value;
- const oldUnit = select.dataset.oldUnit;
- entity.views[entity.view][key] = math
- .unit(value, oldUnit)
- .to(select.value);
- entity.dirty = true;
- setNumericInput(
- input,
- entity.views[entity.view][key].toNumber(select.value)
- );
-
- select.dataset.oldUnit = select.value;
- entity.views[view].units[key] = select.value;
-
- if (config.autoFit) {
- fitWorld();
- } else {
- updateSizes(true);
- }
-
- updateEntityOptions(entity, view);
- updateViewOptions(entity, view, key);
- });
-
- row.appendChild(input);
- row.appendChild(select);
- }
- });
-
- const customButton = document.createElement("button");
- customButton.innerText = "New Attribute";
- holder.appendChild(customButton);
-
- customButton.addEventListener("click", e => {
- entity.currentView.attributes["custom" + (Object.keys(entity.currentView.attributes).length + 1)] = {
- name: "",
- text: "",
- editing: true,
- }
- configViewOptions(entity, view);
- });
- }
-
- function updateViewOptions(entity, view, changed) {
- Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
- if (key != changed) {
- const input = document.querySelector(
- "#options-view-" + key + "-input"
- );
- const select = document.querySelector(
- "#options-view-" + key + "-select"
- );
-
- const currentUnit = select.value;
- const convertedAmount =
- entity.views[view][key].toNumber(currentUnit);
- setNumericInput(input, convertedAmount);
- }
- });
- }
-
- function setNumericInput(input, value, round = 6) {
- if (typeof value == "string") {
- value = parseFloat(value);
- }
- input.value = value.toPrecision(round);
- }
-
- //#endregion
-
- function getSortedEntities() {
- return Object.keys(entities).sort((a, b) => {
- const entA = entities[a];
- const entB = entities[b];
- const viewA = entA.view;
- const viewB = entB.view;
- const heightA = entA.views[viewA].height.to("meter").value;
- const heightB = entB.views[viewB].height.to("meter").value;
- return heightA - heightB;
- });
- }
-
- function clearViewOptions() {
- document.querySelector("#view-category-header").style.display = "none";
- document.querySelector("#view-category").style.display = "none";
- }
-
- // this is a crime against humanity, and also stolen from
- // stack overflow
- // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
-
- const testCanvas = document.createElement("canvas");
- testCanvas.id = "test-canvas";
-
- function rotate(point, angle) {
- return [
- point[0] * Math.cos(angle) - point[1] * Math.sin(angle),
- point[0] * Math.sin(angle) + point[1] * Math.cos(angle),
- ];
- }
-
- const testCtx = testCanvas.getContext("2d");
- function testClick(event) {
- const target = event.target;
-
- if (webkitCanvasBug) {
- return clickDown(target.parentElement, event.clientX, event.clientY);
- }
- testCtx.save();
-
- if (rulerMode) {
- return;
- }
-
- // Get click coordinates
-
- let w = target.width;
- let h = target.height;
- let ratioW = 1,
- ratioH = 1;
-
- // Limit the size of the canvas so that very large images don't cause problems)
- if (w > 1000) {
- ratioW = w / 1000;
- w /= ratioW;
- h /= ratioW;
- }
- if (h > 1000) {
- ratioH = h / 1000;
- w /= ratioH;
- h /= ratioH;
- }
-
- // todo remove some of this unused stuff
-
- const ratio = ratioW * ratioH;
- const entity = entities[target.parentElement.dataset.key];
-
- const angle = entity.rotation;
-
- var x = event.clientX - target.getBoundingClientRect().x,
- y = event.clientY - target.getBoundingClientRect().y,
- alpha;
-
- [xTarget, yTarget] = [x, y];
-
- [actualW, actualH] = [
- target.getBoundingClientRect().width,
- target.getBoundingClientRect().height,
- ];
-
- xTarget /= ratio;
- yTarget /= ratio;
-
- actualW /= ratio;
- actualH /= ratio;
-
- testCtx.canvas.width = actualW;
- testCtx.canvas.height = actualH;
-
- testCtx.save();
-
- // dear future me: Sorry :(
-
- testCtx.resetTransform();
-
- testCtx.translate(actualW / 2, actualH / 2);
- testCtx.rotate(angle);
- if (entity.flipped) {
- testCtx.scale(-1, 1);
- }
- testCtx.translate(-actualW / 2, -actualH / 2);
- testCtx.drawImage(target, actualW / 2 - w / 2, actualH / 2 - h / 2, w, h);
- testCtx.fillStyle = "red";
- testCtx.fillRect(actualW / 2, actualH / 2, 10, 10);
-
- testCtx.restore();
-
- testCtx.fillStyle = "red";
- alpha = testCtx.getImageData(xTarget, yTarget, 1, 1).data[3];
- testCtx.fillRect(xTarget, yTarget, 3, 3);
-
- // If the pixel is transparent,
- // retrieve the element underneath and trigger its click event
- if (alpha === 0) {
- const oldDisplay = target.style.display;
- target.style.display = "none";
- const newTarget = document.elementFromPoint(
- event.clientX,
- event.clientY
- );
- newTarget.dispatchEvent(
- new MouseEvent(event.type, {
- clientX: event.clientX,
- clientY: event.clientY,
- })
- );
- target.style.display = oldDisplay;
- } else {
- clickDown(target.parentElement, event.clientX, event.clientY);
- }
- testCtx.restore();
- }
-
- function arrangeEntities(order) {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- let sum = 0;
- order.forEach((key) => {
- const image = document.querySelector(
- "#entity-" + key + " > .entity-image"
- );
- const meters =
- entities[key].views[entities[key].view].height.toNumber("meters");
-
- let height = image.height;
- let width = image.width;
-
- if (height == 0) {
- height = 100;
- }
- if (width == 0) {
- width = height;
- }
-
- sum += (meters * width) / height;
- });
-
- let x = config.x - sum / 2;
-
- order.forEach((key) => {
- const image = document.querySelector(
- "#entity-" + key + " > .entity-image"
- );
- const meters =
- entities[key].views[entities[key].view].height.toNumber("meters");
-
- let height = image.height;
- let width = image.width;
-
- if (height == 0) {
- height = 100;
- }
- if (width == 0) {
- width = height;
- }
-
- x += (meters * width) / height / 2;
- document.querySelector("#entity-" + key).dataset.x = x;
- document.querySelector("#entity-" + key).dataset.y = config.y;
- x += (meters * width) / height / 2;
- });
-
- fitWorld();
- updateSizes();
- }
-
- function removeAllEntities() {
- Object.keys(entities).forEach((key) => {
- removeEntity(document.querySelector("#entity-" + key));
- });
- }
-
- function clearAttribution() {
- document.querySelector("#attribution-category-header").style.display =
- "none";
- document.querySelector("#options-attribution").style.display = "none";
- }
-
- function displayAttribution(file) {
- document.querySelector("#attribution-category-header").style.display =
- "block";
- document.querySelector("#options-attribution").style.display = "inline";
- const authors = authorsOfFull(file);
- const owners = ownersOfFull(file);
- const citations = citationsOf(file);
- const source = sourceOf(file);
-
- const authorHolder = document.querySelector("#options-attribution-authors");
- const ownerHolder = document.querySelector("#options-attribution-owners");
- const citationHolder = document.querySelector(
- "#options-attribution-citations"
- );
- const sourceHolder = document.querySelector("#options-attribution-source");
-
- if (authors === []) {
- const div = document.createElement("div");
- div.innerText = "Unknown";
- authorHolder.innerHTML = "";
- authorHolder.appendChild(div);
- } else if (authors === undefined) {
- const div = document.createElement("div");
- div.innerText = "Not yet entered";
- authorHolder.innerHTML = "";
- authorHolder.appendChild(div);
- } else {
- authorHolder.innerHTML = "";
-
- const list = document.createElement("ul");
- authorHolder.appendChild(list);
- authors.forEach((author) => {
- const authorEntry = document.createElement("li");
- if (author.url) {
- const link = document.createElement("a");
- link.href = author.url;
- link.innerText = author.name;
- link.rel = "noreferrer no opener";
- link.target = "_blank";
- authorEntry.appendChild(link);
- } else {
- const div = document.createElement("div");
- div.innerText = author.name;
- authorEntry.appendChild(div);
- }
- list.appendChild(authorEntry);
- });
- }
-
- if (owners === []) {
- const div = document.createElement("div");
- div.innerText = "Unknown";
- ownerHolder.innerHTML = "";
- ownerHolder.appendChild(div);
- } else if (owners === undefined) {
- const div = document.createElement("div");
- div.innerText = "Not yet entered";
- ownerHolder.innerHTML = "";
- ownerHolder.appendChild(div);
- } else {
- ownerHolder.innerHTML = "";
-
- const list = document.createElement("ul");
- ownerHolder.appendChild(list);
- owners.forEach((owner) => {
- const ownerEntry = document.createElement("li");
- if (owner.url) {
- const link = document.createElement("a");
- link.href = owner.url;
- link.innerText = owner.name;
- link.rel = "noreferrer no opener";
- link.target = "_blank";
- ownerEntry.appendChild(link);
- } else {
- const div = document.createElement("div");
- div.innerText = owner.name;
- ownerEntry.appendChild(div);
- }
- list.appendChild(ownerEntry);
- });
- }
-
- citationHolder.innerHTML = "";
-
- if (citations === [] || citations === undefined) {
- } else {
- citationHolder.innerHTML = "";
-
- const list = document.createElement("ul");
- citationHolder.appendChild(list);
- citations.forEach((citation) => {
- const citationEntry = document.createElement("li");
- const link = document.createElement("a");
- link.style.display = "block";
- link.href = citation;
- link.innerText = new URL(citation).host;
- link.rel = "noreferrer no opener";
- link.target = "_blank";
- citationEntry.appendChild(link);
- list.appendChild(citationEntry);
- });
- }
-
- if (source === null) {
- const div = document.createElement("div");
- div.innerText = "No link";
- sourceHolder.innerHTML = "";
- sourceHolder.appendChild(div);
- } else if (source === undefined) {
- const div = document.createElement("div");
- div.innerText = "Not yet entered";
- sourceHolder.innerHTML = "";
- sourceHolder.appendChild(div);
- } else {
- sourceHolder.innerHTML = "";
- const link = document.createElement("a");
- link.style.display = "block";
- link.href = source;
- link.innerText = new URL(source).host;
- link.rel = "noreferrer no opener";
- link.target = "_blank";
- sourceHolder.appendChild(link);
- }
- }
-
- function removeEntity(element) {
- if (selected == element) {
- deselect();
- }
- if (clicked == element) {
- clicked = null;
- }
-
- const option = document.querySelector(
- "#options-selected-entity-" + element.dataset.key
- );
- option.parentElement.removeChild(option);
-
- delete entities[element.dataset.key];
- const bottomName = document.querySelector(
- "#bottom-name-" + element.dataset.key
- );
- const topName = document.querySelector("#top-name-" + element.dataset.key);
- bottomName.parentElement.removeChild(bottomName);
- topName.parentElement.removeChild(topName);
- element.parentElement.removeChild(element);
-
- selectedEntity = null;
- prevSelectedEntity = null;
- updateInfo();
- }
-
- function checkEntity(entity) {
- Object.values(entity.views).forEach((view) => {
- if (authorsOf(view.image.source) === undefined) {
- console.warn("No authors: " + view.image.source);
- }
- });
- }
-
- function preloadViews(entity) {
- Object.values(entity.views).forEach((view) => {
- if (Object.keys(entity.forms).length > 0) {
- if (entity.form !== view.form) {
- return;
- }
- }
- if (!preloaded.has(view.image.source)) {
- let img = new Image();
- img.src = view.image.source;
- preloaded.add(view.image.source);
- }
- });
- }
-
- function displayEntity(
- entity,
- view,
- x,
- y,
- selectEntity = false,
- refresh = false
- ) {
- checkEntity(entity);
-
- // preload all of the entity's views
-
- preloadViews(entity);
-
- const box = document.createElement("div");
- box.classList.add("entity-box");
-
- const img = document.createElement("img");
- img.classList.add("entity-image");
- img.addEventListener("dragstart", (e) => {
- e.preventDefault();
- });
-
- const nameTag = document.createElement("div");
- nameTag.classList.add("entity-name");
- nameTag.innerText = entity.name;
- box.appendChild(img);
- box.appendChild(nameTag);
-
- const image = entity.views[view].image;
- img.src = image.source;
-
- if (image.bottom !== undefined) {
- img.style.setProperty("--offset", (-1 + image.bottom) * 100 + "%");
- } else {
- img.style.setProperty("--offset", -1 * 100 + "%");
- }
-
- box.dataset.x = x;
- box.dataset.y = y;
-
- img.addEventListener("mousedown", (e) => {
- if (e.which == 1) {
- testClick(e);
- if (clicked) {
- e.stopPropagation();
- }
- }
- });
- img.addEventListener("touchstart", (e) => {
- const fakeEvent = {
- target: e.target,
- clientX: e.touches[0].clientX,
- clientY: e.touches[0].clientY,
- which: 1,
- };
- testClick(fakeEvent);
- if (clicked) {
- e.stopPropagation();
- }
- });
-
- const heightBar = document.createElement("div");
- heightBar.classList.add("height-bar");
-
- box.appendChild(heightBar);
-
- box.id = "entity-" + entityIndex;
- box.dataset.key = entityIndex;
- entity.view = view;
-
- if (entity.priority === undefined) entity.priority = 0;
- if (entity.brightness === undefined) entity.brightness = 1;
-
- entities[entityIndex] = entity;
- entity.index = entityIndex;
-
- const world = document.querySelector("#entities");
- world.appendChild(box);
-
- const bottomName = document.createElement("div");
- bottomName.classList.add("bottom-name");
- bottomName.id = "bottom-name-" + entityIndex;
- bottomName.innerText = entity.name;
-
- bottomName.addEventListener("click", () => select(box));
-
- world.appendChild(bottomName);
-
- const topName = document.createElement("div");
- topName.classList.add("top-name");
- topName.id = "top-name-" + entityIndex;
- topName.innerText = entity.name;
-
- topName.addEventListener("click", () => select(box));
-
- world.appendChild(topName);
-
- const entityOption = document.createElement("option");
- entityOption.id = "options-selected-entity-" + entityIndex;
- entityOption.value = entityIndex;
- entityOption.innerText = entity.name;
-
- document
- .getElementById("options-selected-entity")
- .appendChild(entityOption);
- entityIndex += 1;
- if (config.autoFit) {
- fitWorld();
- }
-
- updateEntityProperties(box);
-
- if (selectEntity) select(box);
-
- entity.dirty = true;
-
- if (refresh && config.autoFitAdd) {
- let targets = {};
- targets[entityIndex - 1] = entity;
- fitEntities(targets);
- }
- if (refresh) updateSizes(true);
- }
-
- window.onblur = function () {
- altHeld = false;
- shiftHeld = false;
- };
-
- window.onfocus = function () {
- window.dispatchEvent(new Event("keydown"));
- };
-
- // thanks to https://developers.google.com/web/fundamentals/native-hardware/fullscreen
-
- function toggleFullScreen() {
- var doc = window.document;
- var docEl = doc.documentElement;
-
- var requestFullScreen =
- docEl.requestFullscreen ||
- docEl.mozRequestFullScreen ||
- docEl.webkitRequestFullScreen ||
- docEl.msRequestFullscreen;
- var cancelFullScreen =
- doc.exitFullscreen ||
- doc.mozCancelFullScreen ||
- doc.webkitExitFullscreen ||
- doc.msExitFullscreen;
-
- if (
- !doc.fullscreenElement &&
- !doc.mozFullScreenElement &&
- !doc.webkitFullscreenElement &&
- !doc.msFullscreenElement
- ) {
- requestFullScreen.call(docEl);
- } else {
- cancelFullScreen.call(doc);
- }
- }
-
- function handleResize(update = true) {
- entityX = document.querySelector("#entities").getBoundingClientRect().x;
- canvasWidth = document.querySelector("#display").clientWidth - 100;
- canvasHeight = document.querySelector("#display").clientHeight - 50;
-
- if (update)
- updateSizes();
- }
-
- function preparePopoutMenu() {
- const menubars = {
- "menu": document.querySelector("#menu-menu"),
- "scene": document.querySelector("#scene-menu")
- };
- [
- {
- name: "Show/hide sidebar",
- id: "menu-toggle-sidebar",
- icon: "fas fa-chevron-circle-down",
- rotates: true,
- type: "menu"
- },
- {
- name: "Fullscreen",
- id: "menu-fullscreen",
- icon: "fas fa-compress",
- type: "menu"
- },
- {
- name: "Clear",
- id: "menu-clear",
- icon: "fas fa-file",
- type: "scene"
- },
- {
- name: "Sort by height",
- id: "menu-order-height",
- icon: "fas fa-sort-numeric-up",
- type: "scene"
- },
- {
- name: "Permalink",
- id: "menu-permalink",
- icon: "fas fa-link",
- type: "scene"
- },
- {
- name: "Export to clipboard",
- id: "menu-export",
- icon: "fas fa-share",
- type: "scene"
- },
- {
- name: "Import from clipboard",
- id: "menu-import",
- icon: "fas fa-share",
- classes: ["flipped"],
- type: "scene"
- },
- {
- name: "Save Scene",
- id: "menu-save",
- icon: "fas fa-download",
- input: true,
- type: "scene"
- },
- {
- name: "Load Scene",
- id: "menu-load",
- icon: "fas fa-upload",
- select: true,
- type: "scene"
- },
- {
- name: "Delete Scene",
- id: "menu-delete",
- icon: "fas fa-trash",
- select: true,
- type: "scene"
- },
- {
- name: "Load Autosave",
- id: "menu-load-autosave",
- icon: "fas fa-redo",
- type: "scene"
- },
- {
- name: "Load Preset",
- id: "menu-preset",
- icon: "fas fa-play",
- select: true,
- type: "scene"
- },
- {
- name: "Add Image",
- id: "menu-add-image",
- icon: "fas fa-camera",
- type: "menu"
- },
- {
- name: "Clear Rulers",
- id: "menu-clear-rulers",
- icon: "fas fa-ruler",
- type: "menu"
- },
- ].forEach((entry) => {
- const buttonHolder = document.createElement("div");
- buttonHolder.classList.add("menu-button-holder");
- const button = document.createElement("button");
- button.id = entry.id;
- button.classList.add("menu-button");
- const icon = document.createElement("i");
- icon.classList.add(...entry.icon.split(" "));
-
- if (entry.rotates) {
- icon.classList.add("rotate-backward", "transitions");
- }
-
- if (entry.classes) {
- entry.classes.forEach((cls) => icon.classList.add(cls));
- }
-
- const actionText = document.createElement("span");
- actionText.innerText = entry.name;
- actionText.classList.add("menu-text");
-
- const srText = document.createElement("span");
- srText.classList.add("sr-only");
- srText.innerText = entry.name;
-
- button.appendChild(icon);
- button.appendChild(srText);
-
- buttonHolder.appendChild(button);
- buttonHolder.appendChild(actionText);
-
- if (entry.input) {
- const input = document.createElement("input");
- buttonHolder.appendChild(input);
- input.placeholder = "default";
- input.addEventListener("keyup", (e) => {
- if (e.key === "Enter") {
- const name =
- document.querySelector("#menu-save ~ input").value;
- if (/\S/.test(name)) {
- saveScene(name);
- }
- updateSaveInfo();
- e.preventDefault();
- }
- });
- }
-
- if (entry.select) {
- const select = document.createElement("select");
- buttonHolder.appendChild(select);
- }
-
- menubars[entry.type].appendChild(buttonHolder);
- });
-
- document
- .querySelector("#menu-toggle-sidebar")
- .addEventListener("click", (e) => {
- const sidebar = document.querySelector("#options");
- if (sidebar.classList.contains("hidden")) {
- sidebar.classList.remove("hidden");
- e.target.classList.remove("rotate-forward");
- e.target.classList.add("rotate-backward");
- } else {
- sidebar.classList.add("hidden");
- e.target.classList.add("rotate-forward");
- e.target.classList.remove("rotate-backward");
- }
- handleResize();
- });
-
- document
- .querySelector("#menu-fullscreen")
- .addEventListener("click", toggleFullScreen);
-
- const sceneChoices = document.querySelector("#menu-preset ~ select");
-
- Object.entries(scenes).forEach(([id, scene]) => {
- const option = document.createElement("option");
- option.innerText = id;
- option.value = id;
- sceneChoices.appendChild(option);
- });
-
- document.querySelector("#menu-preset").addEventListener("click", (e) => {
- const chosen = sceneChoices.value;
- removeAllEntities();
- scenes[chosen]();
- });
-
- document.querySelector("#menu-clear").addEventListener("click", (e) => {
- removeAllEntities();
- });
-
- document.querySelector("#delete-entity").disabled = true;
- document.querySelector("#delete-entity").addEventListener("click", (e) => {
- if (selected) {
- removeEntity(selected);
- selected = null;
- }
- });
-
- document
- .querySelector("#menu-order-height")
- .addEventListener("click", (e) => {
- const order = Object.keys(entities).sort((a, b) => {
- const entA = entities[a];
- const entB = entities[b];
- const viewA = entA.view;
- const viewB = entB.view;
- const heightA = entA.views[viewA].height.to("meter").value;
- const heightB = entB.views[viewB].height.to("meter").value;
- return heightA - heightB;
- });
-
- arrangeEntities(order);
- });
-
-
- document
- .querySelector("#options-world-fit")
- .addEventListener("click", () => fitWorld(true));
-
- document
- .querySelector("#options-reset-pos-x")
- .addEventListener("click", () => {
- config.x = 0;
- updateSizes();
- });
- document
- .querySelector("#options-reset-pos-y")
- .addEventListener("click", () => {
- config.y = 0;
- updateSizes();
- });
-
- document.querySelector("#menu-permalink").addEventListener("click", (e) => {
- linkScene();
- });
-
- document.querySelector("#menu-export").addEventListener("click", (e) => {
- copyScene();
- });
-
- document.querySelector("#menu-import").addEventListener("click", (e) => {
- pasteScene();
- });
-
- document.querySelector("#menu-save").addEventListener("click", (e) => {
- const name = document.querySelector("#menu-save ~ input").value;
- if (/\S/.test(name)) {
- saveScene(name);
- }
- updateSaveInfo();
- });
-
- document.querySelector("#menu-load").addEventListener("click", (e) => {
- const name = document.querySelector("#menu-load ~ select").value;
- if (/\S/.test(name)) {
- loadScene(name);
- }
- });
-
- document.querySelector("#menu-delete").addEventListener("click", (e) => {
- const name = document.querySelector("#menu-delete ~ select").value;
- if (/\S/.test(name)) {
- deleteScene(name);
- }
- });
- }
-
- function checkBodyClass(cls) {
- return document.body.classList.contains(cls);
- }
-
- function toggleBodyClass(cls, setting) {
- if (setting) {
- document.body.classList.add(cls);
- } else {
- document.body.classList.remove(cls);
- }
- }
-
- const backgroundColors = {
- none: "#00000000",
- black: "#000",
- dark: "#111",
- medium: "#333",
- light: "#555",
- };
-
- const groundPosChoices = [
- "very-high",
- "high",
- "medium",
- "low",
- "very-low",
- "bottom",
- ];
-
- const settingsCategories = [
- {
- id: "scales",
- name: "Scales"
- },
- {
- id: "controls",
- name: "Controls"
- },
- {
- id: "appearance",
- name: "Appearance"
- },
- {
- id: "info",
- name: "Info"
- },
- {
- id: "estimates",
- name: "Estimates"
- },
- ]
-
- const settingsData = {
- "show-vertical-scale": {
- name: "Vertical Scale",
- desc: "Draw vertical scale marks",
- category: "scales",
- type: "toggle",
- default: true,
- get value() {
- return config.drawYAxis;
- },
- set value(param) {
- config.drawYAxis = param;
- drawScales(false);
- },
- },
- "show-horizontal-scale": {
- name: "Horiziontal Scale",
- desc: "Draw horizontal scale marks",
- category: "scales",
- type: "toggle",
- default: false,
- get value() {
- return config.drawXAxis;
- },
- set value(param) {
- config.drawXAxis = param;
- drawScales(false);
- },
- },
- "show-altitudes": {
- name: "Altitudes",
- desc: "Draw interesting altitudes",
- category: "scales",
- type: "select",
- default: "none",
- disabled: "none",
- options: [
- "none",
- "all",
- "atmosphere",
- "orbits",
- "weather",
- "water",
- "geology",
- "thicknesses",
- "airspaces",
- "races",
- "olympic-records",
- "d&d-sizes",
- ],
- get value() {
- return config.drawAltitudes;
- },
- set value(param) {
- config.drawAltitudes = param;
- drawScales(false);
- },
- },
- "lock-y-axis": {
- name: "Lock Y-Axis",
- desc: "Keep the camera at ground-level",
- category: "controls",
- type: "toggle",
- default: true,
- get value() {
- return config.lockYAxis;
- },
- set value(param) {
- config.lockYAxis = param;
- updateScrollButtons();
- if (param) {
- updateSizes();
- }
- },
- },
- "ground-snap": {
- name: "Snap to Ground",
- desc: "Snap things to the ground",
- category: "controls",
- type: "toggle",
- default: true,
- get value() {
- return config.groundSnap;
- },
- set value(param) {
- config.groundSnap = param;
- },
- },
- "axis-spacing": {
- name: "Axis Spacing",
- desc: "How frequent the axis lines are",
- category: "scales",
- type: "select",
- default: "standard",
- options: ["dense", "standard", "sparse"],
- get value() {
- return config.axisSpacing;
- },
- set value(param) {
- config.axisSpacing = param;
- const factor = {
- dense: 0.5,
- standard: 1,
- sparse: 2,
- }[param];
- config.minLineSize = factor * 100;
- config.maxLineSize = factor * 150;
- updateSizes();
- },
- },
- "ground-type": {
- name: "Ground",
- desc: "What kind of ground to show, if any",
- category: "appearance",
- type: "select",
- default: "black",
- disabled: "none",
- options: ["none", "black", "dark", "medium", "light"],
- get value() {
- return config.groundKind;
- },
- set value(param) {
- config.groundKind = param;
- document
- .querySelector("#ground")
- .style.setProperty("--ground-color", backgroundColors[param]);
- },
- },
- "ground-pos": {
- name: "Ground Position",
- desc: "How high the ground is if the y-axis is locked",
- category: "appearance",
- type: "select",
- default: "very-low",
- options: groundPosChoices,
- get value() {
- return config.groundPos;
- },
- set value(param) {
- config.groundPos = param;
- updateScrollButtons();
- updateSizes();
- },
- },
- "background-brightness": {
- name: "Background Color",
- desc: "How bright the background is",
- category: "appearance",
- type: "select",
- default: "medium",
- options: ["black", "dark", "medium", "light"],
- get value() {
- return config.background;
- },
- set value(param) {
- config.background = param;
- drawScales();
- },
- },
- "auto-scale": {
- name: "Auto-Size World",
- desc: "Constantly zoom to fit the largest entity",
- category: "controls",
- type: "toggle",
- default: false,
- get value() {
- return config.autoFit;
- },
- set value(param) {
- config.autoFit = param;
- checkFitWorld();
- },
- },
- "auto-units": {
- name: "Auto-Select Units",
- desc: "Automatically switch units when zooming in and out",
- category: "controls",
- type: "toggle",
- default: false,
- get value() {
- return config.autoUnits;
- },
- set value(param) {
- config.autoUnits = param;
- },
- },
- "zoom-when-adding": {
- name: "Zoom On Add",
- desc: "Zoom to fit when you add a new entity",
- category: "controls",
- type: "toggle",
- default: false,
- get value() {
- return config.autoFitAdd;
- },
- set value(param) {
- config.autoFitAdd = param;
- },
- },
- "zoom-when-sizing": {
- name: "Zoom On Size",
- desc: "Zoom to fit when you select an entity's size",
- category: "controls",
- type: "toggle",
- default: false,
- get value() {
- return config.autoFitSize;
- },
- set value(param) {
- config.autoFitSize = param;
- },
- },
- "show-ratios": {
- name: "Show Ratios",
- desc: "Show the proportions between the current selection and the most recent selection.",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return config.showRatios;
- },
- set value(param) {
- config.showRatios = param;
- updateInfo();
- },
- },
- "show-horizon": {
- name: "Show Horizon",
- desc: "Show how far the horizon would be for the selected character",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return config.showHorizon;
- },
- set value(param) {
- config.showHorizon = param;
- updateInfo();
- },
- },
- "attach-rulers": {
- name: "Attach Rulers",
- desc: "Rulers will attach to the currently-selected entity, moving around with it.",
- category: "controls",
- type: "toggle",
- default: true,
- get value() {
- return config.rulersStick;
- },
- set value(param) {
- config.rulersStick = param;
- },
- },
- units: {
- name: "Default Units",
- desc: "Which kind of unit to use by default",
- category: "info",
- type: "select",
- default: "metric",
- options: ["metric", "customary", "relative", "quirky", "human"],
- get value() {
- return config.units;
- },
- set value(param) {
- config.units = param;
- updateSizes();
- },
- },
- names: {
- name: "Show Names",
- desc: "Display names over entities",
- category: "info",
- type: "toggle",
- default: true,
- get value() {
- return checkBodyClass("toggle-entity-name");
- },
- set value(param) {
- toggleBodyClass("toggle-entity-name", param);
- },
- },
- "bottom-names": {
- name: "Bottom Names",
- desc: "Display names at the bottom",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return checkBodyClass("toggle-bottom-name");
- },
- set value(param) {
- toggleBodyClass("toggle-bottom-name", param);
- },
- },
- "top-names": {
- name: "Show Arrows",
- desc: "Point to entities that are much larger than the current view",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return checkBodyClass("toggle-top-name");
- },
- set value(param) {
- toggleBodyClass("toggle-top-name", param);
- },
- },
- "height-bars": {
- name: "Height Bars",
- desc: "Draw dashed lines to the top of each entity",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return checkBodyClass("toggle-height-bars");
- },
- set value(param) {
- toggleBodyClass("toggle-height-bars", param);
- },
- },
- "flag-nsfw": {
- name: "Flag NSFW",
- desc: "Highlight NSFW things in red",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return checkBodyClass("flag-nsfw");
- },
- set value(param) {
- toggleBodyClass("flag-nsfw", param);
- },
- },
- "glowing-entities": {
- name: "Glowing Edges",
- desc: "Makes all entities glow",
- category: "appearance",
- type: "toggle",
- default: false,
- get value() {
- return checkBodyClass("toggle-entity-glow");
- },
- set value(param) {
- toggleBodyClass("toggle-entity-glow", param);
- },
- },
- "select-style": {
- name: "Selection Style",
- desc: "How to highlight selected entities (outlines are laggier)",
- category: "appearance",
- type: "select",
- default: "color",
- options: ["color", "outline"],
- get value() {
- if (checkBodyClass("highlight-color")) {
- return "color";
- } else {
- return "outline";
- }
- },
- set value(param) {
- toggleBodyClass("highlight-color", param === "color");
- toggleBodyClass("highlight-outline", param === "outline");
- },
- },
- smoothing: {
- name: "Smoothing",
- desc: "Smooth out movements and size changes. Disable for better performance.",
- category: "appearance",
- type: "toggle",
- default: true,
- get value() {
- return checkBodyClass("smoothing");
- },
- set value(param) {
- toggleBodyClass("smoothing", param);
- },
- },
- "auto-mass": {
- name: "Estimate Mass",
- desc: "Guess the mass of things that don't have one specified using the selected body type",
- category: "estimates",
- type: "select",
- default: "off",
- disabled: "off",
- options: ["off", "human", "quadruped at shoulder"],
- get value() {
- return config.autoMass;
- },
- set value(param) {
- config.autoMass = param;
- },
- },
- "auto-food-intake": {
- name: "Estimate Food Intake",
- desc: "Guess how much food creatures need, based on their mass -- 2000kcal per 150lbs",
- category: "estimates",
- type: "toggle",
- default: false,
- get value() {
- return config.autoFoodIntake;
- },
- set value(param) {
- config.autoFoodIntake = param;
- },
- },
- "auto-caloric-value": {
- name: "Estimate Caloric Value",
- desc: "Guess how much food a creature is worth -- 860kcal per pound",
- category: "estimates",
- type: "toggle",
- default: false,
- get value() {
- return config.autoCaloricValue;
- },
- set value(param) {
- config.autoCaloricValue = param;
- },
- },
- "auto-prey-capacity": {
- name: "Estimate Prey Capacity",
- desc: "Guess how much prey creatures can hold, based on their mass",
- category: "estimates",
- type: "select",
- default: "off",
- disabled: "off",
- options: ["off", "realistic", "same-size"],
- get value() {
- return config.autoPreyCapacity;
- },
- set value(param) {
- config.autoPreyCapacity = param;
- },
- },
- "auto-swallow-size": {
- name: "Estimate Swallow Size",
- desc: "Guess how much creatures can swallow at once, based on their height",
- category: "estimates",
- type: "select",
- default: "off",
- disabled: "off",
- options: ["off", "casual", "big-swallow", "same-size-predator"],
- get value() {
- return config.autoSwallowSize;
- },
- set value(param) {
- config.autoSwallowSize = param;
- },
- },
- "edit-default-attributes": {
- name: "Edit Default Attributes",
- desc: "Lets you edit non-custom attributes",
- category: "info",
- type: "toggle",
- default: false,
- get value() {
- return config.editDefaultAttributes
- },
- set value(param) {
- config.editDefaultAttributes = param;
- if (selected) {
- const entity = entities[selected.dataset.key]
- configViewOptions(entity, entity.view);
- }
- }
- }
- };
-
- function prepareSettings(userSettings) {
- const menubar = document.querySelector("#settings-menu");
-
- settingsCategories.forEach(category => {
- const categoryLabel = document.createElement("div");
- categoryLabel.classList.add("settings-category-label");
- categoryLabel.innerText = category.name;
- menubar.appendChild(categoryLabel);
- Object.entries(settingsData).forEach(([id, entry]) => {
-
- if (settingsCategories.every(category => category.id != entry.category)) {
- console.warn(id + " has a bogus category of " + entry.category);
- }
- if (entry.category != category.id) {
- return;
- }
- const holder = document.createElement("label");
- holder.classList.add("settings-holder");
-
- const input = document.createElement("input");
- input.id = "setting-" + id;
-
- const vertical = document.createElement("div");
- vertical.classList.add("settings-vertical");
-
- const name = document.createElement("label");
- name.innerText = entry.name;
- name.classList.add("settings-name");
- name.setAttribute("for", input.id);
-
- const desc = document.createElement("label");
- desc.innerText = entry.desc;
- desc.classList.add("settings-desc");
- desc.setAttribute("for", input.id);
-
- if (entry.type == "toggle") {
- input.type = "checkbox";
-
- input.checked =
- userSettings[id] === undefined
- ? entry.default
- : userSettings[id];
-
- holder.setAttribute("for", input.id);
-
- vertical.appendChild(name);
- vertical.appendChild(desc);
- holder.appendChild(vertical);
- holder.appendChild(input);
- menubar.appendChild(holder);
-
- const update = () => {
- if (input.checked) {
- holder.classList.add("enabled");
- holder.classList.remove("disabled");
- } else {
- holder.classList.remove("enabled");
- holder.classList.add("disabled");
- }
-
- entry.value = input.checked;
- };
-
- setTimeout(update);
-
- input.addEventListener("change", update);
- } else if (entry.type == "select") {
- // we don't use the input element we made!
-
- const select = document.createElement("select");
- select.id = "setting-" + id;
-
- entry.options.forEach((choice) => {
- const option = document.createElement("option");
- option.innerText = choice;
- select.appendChild(option);
- });
-
- select.value =
- userSettings[id] === undefined
- ? entry.default
- : userSettings[id];
-
- vertical.appendChild(name);
- vertical.appendChild(desc);
- holder.appendChild(vertical);
- holder.appendChild(select);
- menubar.appendChild(holder);
-
- const update = () => {
- entry.value = select.value;
-
- if (
- entry.disabled !== undefined &&
- entry.value !== entry.disabled
- ) {
- holder.classList.add("enabled");
- holder.classList.remove("disabled");
- } else {
- holder.classList.remove("enabled");
- holder.classList.add("disabled");
- }
- };
-
- update();
-
- select.addEventListener("change", update);
- }
- });
- });
- }
-
- function updateSaveInfo() {
- const saves = getSaves();
- const load = document.querySelector("#menu-load ~ select");
- load.innerHTML = "";
- saves.forEach((save) => {
- const option = document.createElement("option");
- option.innerText = save;
- option.value = save;
- load.appendChild(option);
- });
-
- const del = document.querySelector("#menu-delete ~ select");
- del.innerHTML = "";
- saves.forEach((save) => {
- const option = document.createElement("option");
- option.innerText = save;
- option.value = save;
- del.appendChild(option);
- });
- }
-
- function getSaves() {
- try {
- const results = [];
- Object.keys(localStorage).forEach((key) => {
- if (key.startsWith("macrovision-save-")) {
- results.push(key.replace("macrovision-save-", ""));
- }
- });
- return results;
- } catch (err) {
- alert(
- "Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error."
- );
- console.error(err);
- return false;
- }
- }
-
- function getUserSettings() {
- try {
- const settings = JSON.parse(localStorage.getItem("settings"));
- return settings === null ? {} : settings;
- } catch {
- return {};
- }
- }
-
- function exportUserSettings() {
- const settings = {};
- Object.entries(settingsData).forEach(([id, entry]) => {
- settings[id] = entry.value;
- });
-
- return settings;
- }
-
- function setUserSettings(settings) {
- try {
- localStorage.setItem("settings", JSON.stringify(settings));
- } catch {
- // :(
- }
- }
-
- function doYScroll() {
- const worldHeight = config.height.toNumber("meters");
- config.y += (scrollDirection * worldHeight) / 180;
- updateSizes();
- scrollDirection *= 1.05;
- }
- function doXScroll() {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- config.x += (scrollDirection * worldWidth) / 180;
- updateSizes();
- scrollDirection *= 1.05;
- }
-
- function doZoom() {
- const oldHeight = config.height;
-
- setWorldHeight(oldHeight, math.multiply(oldHeight, 1 + zoomDirection / 10));
- zoomDirection *= 1.05;
- }
-
- function doSize() {
- if (selected) {
- const entity = entities[selected.dataset.key];
- const oldHeight = entity.views[entity.view].height;
- entity.views[entity.view].height = math.multiply(
- oldHeight,
- sizeDirection < 0 ? -1 / sizeDirection : sizeDirection
- );
- entity.dirty = true;
- updateEntityOptions(entity, entity.view);
- updateViewOptions(entity, entity.view);
- updateSizes(true);
- sizeDirection *= 1.01;
-
- const ownHeight = entity.views[entity.view].height.toNumber("meters");
- let extra = entity.views[entity.view].image.extra;
- extra = extra === undefined ? 1 : extra;
- const worldHeight = config.height.toNumber("meters");
-
- if (ownHeight * extra > worldHeight) {
- setWorldHeight(
- config.height,
- math.multiply(entity.views[entity.view].height, extra)
- );
- } else if (ownHeight * extra * 10 < worldHeight) {
- setWorldHeight(
- config.height,
- math.multiply(entity.views[entity.view].height, extra * 10)
- );
- }
- }
- }
-
- function selectNewUnit() {
- const unitSelector = document.querySelector("#options-height-unit");
- checkFitWorld();
- const scaleInput = document.querySelector("#options-height-value");
- const newVal = math
- .unit(scaleInput.value, unitSelector.dataset.oldUnit)
- .toNumber(unitSelector.value);
- setNumericInput(scaleInput, newVal);
- updateWorldHeight();
- unitSelector.dataset.oldUnit = unitSelector.value;
- }
-
- // given a world position, return the position relative to the entity at normal scale
- function entityRelativePosition(pos, entityElement) {
- const entity = entities[entityElement.dataset.key];
- const x = parseFloat(entityElement.dataset.x);
- const y = parseFloat(entityElement.dataset.y);
-
- pos.x -= x;
- pos.y -= y;
-
- pos.x /= entity.scale;
- pos.y /= entity.scale;
-
- return pos;
- }
-
- function hidePopoutMenu(id) {
- document.querySelector(id).classList.remove("visible");
- }
-
- function showPopoutMenu(id, event) {
- document
- .querySelectorAll(".popout-menu")
- .forEach((menu) => menu.classList.remove("visible"));
- const popoutMenu = document.querySelector(id);
- const rect = event.currentTarget.getBoundingClientRect();
- popoutMenu.classList.add("visible");
-
- popoutMenu.style.left = rect.x + "px";
- popoutMenu.style.top = rect.y + rect.height + "px";
-
- let menuWidth = popoutMenu.getBoundingClientRect().width;
- let screenWidth = window.innerWidth;
-
- if (rect.x + menuWidth > screenWidth) {
- popoutMenu.style.left = Math.max(0, screenWidth - menuWidth) + "px";
- }
- }
-
- function setupPopoutMenu(type) {
- let buttonId = "#toggle-" + type;
- let menuId = "#" + type + "-menu";
- document.querySelector(buttonId).addEventListener("click", (e) => {
- const popoutMenu = document.querySelector(menuId);
- if (popoutMenu.classList.contains("visible")) {
- hidePopoutMenu(menuId);
- } else {
- showPopoutMenu(menuId, e);
- }
- e.stopPropagation();
- });
-
- document.querySelector(buttonId).addEventListener("touchend", (e) => {
- e.stopPropagation();
- });
-
- document.querySelector(menuId).addEventListener("click", (e) => {
- e.stopPropagation();
- });
-
- document.querySelector(menuId).addEventListener("touchend", (e) => {
- e.stopPropagation();
- });
-
- document.addEventListener("click", (e) => {
- document.querySelector(menuId).classList.remove("visible");
- });
-
- document.addEventListener("touchend", (e) => {
- document.querySelector(menuId).classList.remove("visible");
- });
- }
-
- function setupMenuButtons() {
- document
- .querySelector("#copy-screenshot")
- .addEventListener("click", (e) => {
- copyScreenshot();
- });
-
- document
- .querySelector("#save-screenshot")
- .addEventListener("click", (e) => {
- saveScreenshot();
- });
-
- document
- .querySelector("#open-screenshot")
- .addEventListener("click", (e) => {
- openScreenshot();
- });
-
- setupPopoutMenu("menu");
- setupPopoutMenu("scene");
- setupPopoutMenu("settings");
- setupPopoutMenu("filters");
- setupPopoutMenu("info");
- }
-
- function setupSidebar() {
- document
- .querySelector("#options-selected-entity")
- .addEventListener("input", (e) => {
- if (e.target.value == "None") {
- deselect();
- } else {
- select(document.querySelector("#entity-" + e.target.value));
- }
- });
-
-
- document
- .querySelector("#options-order-forward")
- .addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].priority += 1;
- }
- document.querySelector("#options-order-display").innerText =
- entities[selected.dataset.key].priority;
- updateSizes();
- });
-
- document
- .querySelector("#options-order-back")
- .addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].priority -= 1;
- }
- document.querySelector("#options-order-display").innerText =
- entities[selected.dataset.key].priority;
- updateSizes();
- });
-
- document
- .querySelector("#options-brightness-up")
- .addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].brightness += 0.5;
- updateEntityProperties(selected);
- }
- document.querySelector("#options-brightness-display").innerText =
- entities[selected.dataset.key].brightness;
- updateSizes();
- });
-
- document
- .querySelector("#options-brightness-down")
- .addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].brightness -= 0.5;
- updateEntityProperties(selected);
- }
- document.querySelector("#options-brightness-display").innerText =
- entities[selected.dataset.key].brightness;
- updateSizes();
- });
-
- document
- .querySelector("#options-rotate-left")
- .addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].rotation -= Math.PI / 4;
- updateEntityProperties(selected);
- }
- updateSizes();
- });
-
- document
- .querySelector("#options-rotate-right")
- .addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].rotation += Math.PI / 4;
- updateEntityProperties(selected);
- }
- updateSizes();
- });
-
- document.querySelector("#options-flip").addEventListener("click", (e) => {
- if (selected) {
- entities[selected.dataset.key].flipped = !entities[selected.dataset.key].flipped
- updateEntityProperties(selected);
- }
- updateSizes();
- });
-
- const formList = document.querySelector("#entity-form");
-
- formList.addEventListener("input", (e) => {
- const entity = entities[selected.dataset.key];
- entity.form = e.target.value;
-
- const oldView = entity.currentView;
-
- entity.view = entity.formViews[entity.form];
-
- // to set the size properly, even if we use a non-default view
- if (Object.keys(entity.forms).length > 0 && !entity.formSizesMatch)
- entity.views[entity.view].height =
- entity.formSizes[entity.form].height;
-
- let found = Object.entries(entity.views).find(([key, view]) => {
- return view.form === entity.form && view.name === oldView.name;
- });
-
- const newView = found ? found[0] : entity.formViews[entity.form];
-
- entity.view = newView;
- preloadViews(entity);
-
- configViewList(entity, entity.view);
-
- const image = entity.views[entity.view].image;
- selected.querySelector(".entity-image").src = image.source;
-
- configViewOptions(entity, entity.view);
-
- displayAttribution(image.source);
-
- if (image.bottom !== undefined) {
- selected
- .querySelector(".entity-image")
- .style.setProperty("--offset", (-1 + image.bottom) * 100 + "%");
- } else {
- selected
- .querySelector(".entity-image")
- .style.setProperty("--offset", -1 * 100 + "%");
- }
-
- if (config.autoFitSize) {
- let targets = {};
- targets[selected.dataset.key] = entities[selected.dataset.key];
- fitEntities(targets);
- }
-
- configSizeList(entity);
- updateSizes();
- updateEntityOptions(entities[selected.dataset.key], e.target.value);
- updateViewOptions(entities[selected.dataset.key], entity.view);
- });
-
- const viewList = document.querySelector("#entity-view");
-
- document.querySelector("#entity-view").addEventListener("input", (e) => {
- const entity = entities[selected.dataset.key];
- entity.view = e.target.value;
- preloadViews(entity);
-
- const image =
- entities[selected.dataset.key].views[e.target.value].image;
- selected.querySelector(".entity-image").src = image.source;
-
- configViewOptions(entity, entity.view);
-
- displayAttribution(image.source);
-
- if (image.bottom !== undefined) {
- selected
- .querySelector(".entity-image")
- .style.setProperty("--offset", (-1 + image.bottom) * 100 + "%");
- } else {
- selected
- .querySelector(".entity-image")
- .style.setProperty("--offset", -1 * 100 + "%");
- }
- updateSizes();
- updateEntityOptions(entities[selected.dataset.key], e.target.value);
- updateViewOptions(entities[selected.dataset.key], e.target.value);
- });
-
- document.querySelector("#entity-view").addEventListener("input", (e) => {
- if (
- viewList.options[viewList.selectedIndex].classList.contains("nsfw")
- ) {
- viewList.classList.add("nsfw");
- } else {
- viewList.classList.remove("nsfw");
- }
- });
- }
-
- function setupScrollButtons() {
-
- // TODO: write some generic logic for this lol
-
- document
- .querySelector("#scroll-left")
- .addEventListener("mousedown", (e) => {
- scrollDirection = -1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doXScroll, 1000 / 20);
- e.stopPropagation();
- });
-
- document
- .querySelector("#scroll-right")
- .addEventListener("mousedown", (e) => {
- scrollDirection = 1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doXScroll, 1000 / 20);
- e.stopPropagation();
- });
-
- document
- .querySelector("#scroll-left")
- .addEventListener("touchstart", (e) => {
- scrollDirection = -1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doXScroll, 1000 / 20);
- e.stopPropagation();
- });
-
- document
- .querySelector("#scroll-right")
- .addEventListener("touchstart", (e) => {
- scrollDirection = 1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doXScroll, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#scroll-up").addEventListener("mousedown", (e) => {
- if (config.lockYAxis) {
- moveGround(true);
- } else {
- scrollDirection = 1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doYScroll, 1000 / 20);
- e.stopPropagation();
- }
- });
-
- document
- .querySelector("#scroll-down")
- .addEventListener("mousedown", (e) => {
- if (config.lockYAxis) {
- moveGround(false);
- } else {
- scrollDirection = -1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doYScroll, 1000 / 20);
- e.stopPropagation();
- }
- });
-
- document.querySelector("#scroll-up").addEventListener("touchstart", (e) => {
- if (config.lockYAxis) {
- moveGround(true);
- } else {
- scrollDirection = 1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doYScroll, 1000 / 20);
- e.stopPropagation();
- }
- });
-
- document
- .querySelector("#scroll-down")
- .addEventListener("touchstart", (e) => {
- if (config.lockYAxis) {
- moveGround(false);
- } else {
- scrollDirection = -1;
- clearInterval(scrollHandle);
- scrollHandle = setInterval(doYScroll, 1000 / 20);
- e.stopPropagation();
- }
- });
-
- document.addEventListener("mouseup", (e) => {
- clearInterval(scrollHandle);
- scrollHandle = null;
- });
-
- document.addEventListener("touchend", (e) => {
- clearInterval(scrollHandle);
- scrollHandle = null;
- });
-
- document.querySelector("#zoom-in").addEventListener("mousedown", (e) => {
- zoomDirection = -1;
- clearInterval(zoomHandle);
- zoomHandle = setInterval(doZoom, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#zoom-out").addEventListener("mousedown", (e) => {
- zoomDirection = 1;
- clearInterval(zoomHandle);
- zoomHandle = setInterval(doZoom, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#zoom-in").addEventListener("touchstart", (e) => {
- zoomDirection = -1;
- clearInterval(zoomHandle);
- zoomHandle = setInterval(doZoom, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#zoom-out").addEventListener("touchstart", (e) => {
- zoomDirection = 1;
- clearInterval(zoomHandle);
- zoomHandle = setInterval(doZoom, 1000 / 20);
- e.stopPropagation();
- });
-
- document.addEventListener("mouseup", (e) => {
- clearInterval(zoomHandle);
- zoomHandle = null;
- });
-
- document.addEventListener("touchend", (e) => {
- clearInterval(zoomHandle);
- zoomHandle = null;
- });
-
- document.querySelector("#shrink").addEventListener("mousedown", (e) => {
- sizeDirection = -1;
- clearInterval(sizeHandle);
- sizeHandle = setInterval(doSize, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#grow").addEventListener("mousedown", (e) => {
- sizeDirection = 1;
- clearInterval(sizeHandle);
- sizeHandle = setInterval(doSize, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#shrink").addEventListener("touchstart", (e) => {
- sizeDirection = -1;
- clearInterval(sizeHandle);
- sizeHandle = setInterval(doSize, 1000 / 20);
- e.stopPropagation();
- });
-
- document.querySelector("#grow").addEventListener("touchstart", (e) => {
- sizeDirection = 1;
- clearInterval(sizeHandle);
- sizeHandle = setInterval(doSize, 1000 / 20);
- e.stopPropagation();
- });
-
- document.addEventListener("mouseup", (e) => {
- clearInterval(sizeHandle);
- sizeHandle = null;
- });
-
- document.addEventListener("touchend", (e) => {
- clearInterval(sizeHandle);
- sizeHandle = null;
- });
-
- document.querySelector("#ruler").addEventListener("click", (e) => {
- rulerMode = !rulerMode;
- if (rulerMode) {
- toast("Ready to draw a ruler mark");
- } else {
- toast("Cancelled ruler mode");
- }
- });
-
- document.querySelector("#ruler").addEventListener("mousedown", (e) => {
- e.stopPropagation();
- });
-
- document.querySelector("#ruler").addEventListener("touchstart", (e) => {
- e.stopPropagation();
- });
-
- document.querySelector("#fit").addEventListener("click", (e) => {
- if (selected) {
- let targets = {};
- targets[selected.dataset.key] = entities[selected.dataset.key];
- fitEntities(targets);
- }
- });
-
- document.querySelector("#fit").addEventListener("mousedown", (e) => {
- e.stopPropagation();
- });
-
- document.querySelector("#fit").addEventListener("touchstart", (e) => {
- e.stopPropagation();
- });
- }
-
- function prepareMenu() {
- preparePopoutMenu();
- updateSaveInfo();
- setupMenuButtons();
- setupSidebar();
- setupScrollButtons();
- }
-
- function prepareEvents() {
- window.addEventListener("unload", () => {
- saveScene("autosave");
- setUserSettings(exportUserSettings());
- });
-
-
-
- document.querySelector("#world").addEventListener("mousedown", (e) => {
- // only middle mouse clicks
- if (e.which == 2) {
- panning = true;
- panOffsetX = e.clientX;
- panOffsetY = e.clientY;
-
- Object.keys(entities).forEach((key) => {
- document
- .querySelector("#entity-" + key)
- .classList.add("no-transition");
- });
- }
- });
-
- document.addEventListener("mouseup", (e) => {
- if (e.which == 2) {
- panning = false;
- Object.keys(entities).forEach((key) => {
- document
- .querySelector("#entity-" + key)
- .classList.remove("no-transition");
- });
- }
- });
-
- document.querySelector("#world").addEventListener("touchstart", (e) => {
- if (!rulerMode) {
- panning = true;
- panOffsetX = e.touches[0].clientX;
- panOffsetY = e.touches[0].clientY;
- e.preventDefault();
- Object.keys(entities).forEach((key) => {
- document
- .querySelector("#entity-" + key)
- .classList.add("no-transition");
- });
- }
- });
-
- document.querySelector("#world").addEventListener("touchend", (e) => {
- panning = false;
- Object.keys(entities).forEach((key) => {
- document
- .querySelector("#entity-" + key)
- .classList.remove("no-transition");
- });
- });
- document.querySelector("#world").addEventListener("mousedown", (e) => {
- // only left mouse clicks
- if (e.which == 1 && rulerMode) {
- let entX = document
- .querySelector("#entities")
- .getBoundingClientRect().x;
- let entY = document
- .querySelector("#entities")
- .getBoundingClientRect().y;
- let pos = pix2pos({ x: e.clientX - entX, y: e.clientY - entY });
-
- if (config.rulersStick && selected) {
- pos = entityRelativePosition(pos, selected);
- }
- currentRuler = {
- x0: pos.x,
- y0: pos.y,
- x1: pos.y,
- y1: pos.y,
- entityKey: null,
- };
-
- if (config.rulersStick && selected) {
- currentRuler.entityKey = selected.dataset.key;
- }
- }
- });
- document.querySelector("#world").addEventListener("mouseup", (e) => {
- // only left mouse clicks
- if (e.which == 1 && currentRuler) {
- rulers.push(currentRuler);
- currentRuler = null;
- rulerMode = false;
- }
- });
-
- document.querySelector("#world").addEventListener("touchstart", (e) => {
- if (rulerMode) {
- let entX = document
- .querySelector("#entities")
- .getBoundingClientRect().x;
- let entY = document
- .querySelector("#entities")
- .getBoundingClientRect().y;
- let pos = pix2pos({
- x: e.touches[0].clientX - entX,
- y: e.touches[0].clientY - entY,
- });
-
- if (config.rulersStick && selected) {
- pos = entityRelativePosition(pos, selected);
- }
- currentRuler = {
- x0: pos.x,
- y0: pos.y,
- x1: pos.y,
- y1: pos.y,
- entityKey: null,
- };
- if (config.rulersStick && selected) {
- currentRuler.entityKey = selected.dataset.key;
- }
- }
- });
- document.querySelector("#world").addEventListener("touchend", (e) => {
- if (currentRuler) {
- rulers.push(currentRuler);
- currentRuler = null;
- rulerMode = false;
- }
- });
-
- document.querySelector("body").appendChild(testCtx.canvas);
-
- world.addEventListener("mousedown", (e) => deselect(e));
- world.addEventListener("touchstart", (e) =>
- deselect({
- which: 1,
- })
- );
- document.querySelector("#entities").addEventListener("mousedown", deselect);
- document.querySelector("#display").addEventListener("mousedown", deselect);
- document.addEventListener("mouseup", (e) => clickUp(e));
- document.addEventListener("touchend", (e) => {
- const fakeEvent = {
- target: e.target,
- clientX: e.changedTouches[0].clientX,
- clientY: e.changedTouches[0].clientY,
- which: 1,
- };
- clickUp(fakeEvent);
- });
-
- document.addEventListener("keydown", (e) => {
- if (e.key == "Delete" || e.key == "Backspace") {
- if (selected) {
- removeEntity(selected);
- selected = null;
- }
- }
- });
-
- document.addEventListener("keydown", (e) => {
- if (e.key == "Shift") {
- shiftHeld = true;
- e.preventDefault();
- } else if (e.key == "Alt") {
- altHeld = true;
- movingInBounds = false; // don't snap the object back in bounds when we let go
- e.preventDefault();
- }
- });
-
- document.addEventListener("keyup", (e) => {
- if (e.key == "Shift") {
- shiftHeld = false;
- e.preventDefault();
- } else if (e.key == "Alt") {
- altHeld = false;
- e.preventDefault();
- }
- });
-
- document.addEventListener("paste", (e) => {
- let index = 0;
- let item = null;
- let found = false;
-
- for (; index < e.clipboardData.items.length; index++) {
- item = e.clipboardData.items[index];
- if (item.type == "image/png") {
- found = true;
- break;
- }
- }
-
- if (!found) {
- return;
- }
-
- let url = null;
-
- const file = item.getAsFile();
-
- customEntityFromFile(file);
- });
-
- document.querySelector("#world").addEventListener("dragover", (e) => {
- e.preventDefault();
- });
-
- document.querySelector("#world").addEventListener("drop", (e) => {
- e.preventDefault();
- if (e.dataTransfer.files.length > 0) {
- let entX = document
- .querySelector("#entities")
- .getBoundingClientRect().x;
- let entY = document
- .querySelector("#entities")
- .getBoundingClientRect().y;
- let coords = pix2pos({ x: e.clientX - entX, y: e.clientY - entY });
- customEntityFromFile(e.dataTransfer.files[0], coords.x, coords.y);
- }
- });
- document.querySelector("#world").addEventListener("wheel", (e) => {
- let magnitude = Math.abs(e.deltaY / 100);
-
- if (shiftHeld) {
- // macs do horizontal scrolling with shift held
-
- let delta = e.deltaY;
-
- if (e.deltaY == 0) {
- magnitude = Math.abs(e.deltaX / 100);
- delta = e.deltaX;
- }
- if (selected) {
- let dir = delta > 0 ? 10 / 11 : 11 / 10;
- dir -= 1;
- dir *= magnitude;
- dir += 1;
- const entity = entities[selected.dataset.key];
- entity.views[entity.view].height = math.multiply(
- entity.views[entity.view].height,
- dir
- );
- entity.dirty = true;
- updateEntityOptions(entity, entity.view);
- updateViewOptions(entity, entity.view);
- updateSizes(true);
- } else {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) *
- canvasWidth;
- config.x += ((e.deltaY > 0 ? 1 : -1) * worldWidth) / 20;
- updateSizes();
- updateSizes();
- }
- } else {
- if (config.autoFit) {
- toastRateLimit(
- "Zoom is locked! Check Settings to disable.",
- "zoom-lock",
- 1000
- );
- } else {
- let dir = e.deltaY < 0 ? 10 / 11 : 11 / 10;
- dir -= 1;
- dir *= magnitude;
- dir += 1;
- const change =
- config.height.toNumber("meters") -
- math.multiply(config.height, dir).toNumber("meters");
- if (!config.lockYAxis) {
- config.y += change / 2;
- }
- setWorldHeight(
- config.height,
- math.multiply(config.height, dir)
- );
- updateWorldOptions();
- }
- }
- checkFitWorld();
- });
-
- document.addEventListener("mousemove", (e) => {
- if (currentRuler) {
- let entX = document
- .querySelector("#entities")
- .getBoundingClientRect().x;
- let entY = document
- .querySelector("#entities")
- .getBoundingClientRect().y;
- let position = pix2pos({
- x: e.clientX - entX,
- y: e.clientY - entY,
- });
-
- if (config.rulersStick && selected) {
- position = entityRelativePosition(position, selected);
- }
- currentRuler.x1 = position.x;
- currentRuler.y1 = position.y;
- }
- drawRulers();
- });
-
- document.addEventListener("touchmove", (e) => {
- if (currentRuler) {
- let entX = document
- .querySelector("#entities")
- .getBoundingClientRect().x;
- let entY = document
- .querySelector("#entities")
- .getBoundingClientRect().y;
- let position = pix2pos({
- x: e.touches[0].clientX - entX,
- y: e.touches[0].clientY - entY,
- });
- if (config.rulersStick && selected) {
- position = entityRelativePosition(position, selected);
- }
- currentRuler.x1 = position.x;
- currentRuler.y1 = position.y;
- }
- drawRulers();
- });
-
- document.addEventListener("mousemove", (e) => {
- if (clicked) {
- let position = pix2pos({
- x: e.clientX - dragOffsetX,
- y: e.clientY - dragOffsetY,
- });
-
- if (movingInBounds) {
- position = snapPos(position);
- } else {
- let x = e.clientX - dragOffsetX;
- let y = e.clientY - dragOffsetY;
- if (x >= 0 && x <= canvasWidth && y >= 0 && y <= canvasHeight) {
- movingInBounds = true;
- }
- }
- clicked.dataset.x = position.x;
- clicked.dataset.y = position.y;
- updateEntityElement(entities[clicked.dataset.key], clicked);
-
- if (hoveringInDeleteArea(e)) {
- document
- .querySelector("#menubar")
- .classList.add("hover-delete");
- } else {
- document
- .querySelector("#menubar")
- .classList.remove("hover-delete");
- }
- }
- if (panning && panReady) {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- const worldHeight = config.height.toNumber("meters");
-
- config.x -= ((e.clientX - panOffsetX) / canvasWidth) * worldWidth;
- config.y += ((e.clientY - panOffsetY) / canvasHeight) * worldHeight;
- panOffsetX = e.clientX;
- panOffsetY = e.clientY;
- updateSizes();
- panReady = false;
- setTimeout(() => (panReady = true), 1000 / 120);
- }
- });
-
- document.addEventListener(
- "touchmove",
- (e) => {
- if (clicked) {
- e.preventDefault();
- let x = e.touches[0].clientX;
- let y = e.touches[0].clientY;
-
- const position = snapPos(
- pix2pos({ x: x - dragOffsetX, y: y - dragOffsetY })
- );
- clicked.dataset.x = position.x;
- clicked.dataset.y = position.y;
- updateEntityElement(entities[clicked.dataset.key], clicked);
-
- // what a hack
- // I should centralize this 'fake event' creation...
- if (hoveringInDeleteArea({ clientY: y })) {
- document
- .querySelector("#menubar")
- .classList.add("hover-delete");
- } else {
- document
- .querySelector("#menubar")
- .classList.remove("hover-delete");
- }
- }
- if (panning && panReady) {
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) *
- canvasWidth;
- const worldHeight = config.height.toNumber("meters");
-
- config.x -=
- ((e.touches[0].clientX - panOffsetX) / canvasWidth) *
- worldWidth;
- config.y +=
- ((e.touches[0].clientY - panOffsetY) / canvasHeight) *
- worldHeight;
- panOffsetX = e.touches[0].clientX;
- panOffsetY = e.touches[0].clientY;
- updateSizes();
- panReady = false;
- setTimeout(() => (panReady = true), 1000 / 60);
- }
- },
- { passive: false }
- );
-
- document
- .querySelector("#search-box")
- .addEventListener("change", (e) => doSearch(e.target.value));
-
- document
- .querySelector("#search-box")
- .addEventListener("keydown", e => e.stopPropagation());
- }
-
- document.addEventListener("DOMContentLoaded", () => {
-
- prepareMenu();
- prepareEntities();
- prepareEvents();
-
- handleResize(false);
-
- document
- .querySelector("#options-height-value")
- .addEventListener("change", (e) => {
- updateWorldHeight();
- });
-
- document
- .querySelector("#options-height-value")
- .addEventListener("keydown", (e) => {
- e.stopPropagation();
- });
-
- const unitSelector = document.querySelector("#options-height-unit");
-
- Object.entries(unitChoices.length).forEach(([group, entries]) => {
- const optGroup = document.createElement("optgroup");
- optGroup.label = group;
- unitSelector.appendChild(optGroup);
-
- entries.forEach((entry) => {
- const option = document.createElement("option");
- option.innerText = entry;
-
- // we haven't loaded user settings yet, so we can't choose the unit just yet
-
- unitSelector.appendChild(option);
- });
- });
-
- unitSelector.addEventListener("input", selectNewUnit);
-
- param = window.location.hash;
-
- // we now use the fragment for links, but we should still support old stuff:
-
- if (param.length > 0) {
- param = param.substring(1);
- } else {
- param = new URL(window.location.href).searchParams.get("scene");
- }
-
- clearViewList();
-
- window.addEventListener("resize", handleResize);
-
- // TODO: further investigate why the tool initially starts out with wrong
- // values under certain circumstances (seems to be narrow aspect ratios -
- // maybe the menu bar is animating when it shouldn't)
-
- setTimeout(handleResize, 250);
- setTimeout(handleResize, 500);
- setTimeout(handleResize, 750);
- setTimeout(handleResize, 1000);
-
- document
- .querySelector("#menu-load-autosave")
- .addEventListener("click", (e) => {
- loadScene("autosave");
- });
-
- document.querySelector("#menu-add-image").addEventListener("click", (e) => {
- document.querySelector("#file-upload-picker").click();
- });
-
- document
- .querySelector("#file-upload-picker")
- .addEventListener("change", (e) => {
- if (e.target.files.length > 0) {
- for (let i = 0; i < e.target.files.length; i++) {
- customEntityFromFile(e.target.files[i]);
- }
- }
- });
-
- document
- .querySelector("#menu-clear-rulers")
- .addEventListener("click", (e) => {
- rulers = [];
- drawRulers();
- });
- clearEntityOptions();
- clearViewOptions();
- clearAttribution();
-
- // we do this last because configuring settings can cause things
- // to happen (e.g. auto-fit)
- prepareSettings(getUserSettings());
-
- // now that we have this loaded, we can set it
-
- unitSelector.dataset.oldUnit = defaultUnits.length[config.units];
-
- document.querySelector("#options-height-unit").value =
- defaultUnits.length[config.units];
-
- // ...and then update the world height by setting off an input event
-
- document
- .querySelector("#options-height-unit")
- .dispatchEvent(new Event("input", {}));
-
- if (param === null) {
- scenes["Empty"]();
- } else {
- try {
- const data = JSON.parse(b64DecodeUnicode(param), math.reviver);
- if (data.entities === undefined) {
- return;
- }
- if (data.world === undefined) {
- return;
- }
-
- importScene(data);
- } catch (err) {
- console.error(err);
- scenes["Empty"]();
-
- // probably wasn't valid data
- }
- }
-
- updateWorldHeight();
- // Webkit doesn't draw resized SVGs correctly. It will always draw them at their intrinsic size, I think
- // This checks for that.
- webkitBugTest.onload = () => {
- testCtx.canvas.width = 500;
- testCtx.canvas.height = 500;
- testCtx.clearRect(0, 0, 500, 500);
- testCtx.drawImage(webkitBugTest, 0, 0, 500, 500);
- webkitCanvasBug = testCtx.getImageData(250, 250, 1, 1).data[3] == 0;
-
- if (webkitCanvasBug) {
- toast(
- "Heads up: Safari can't select through gaps or take screenshots (check the console for info!)"
- );
- console.log(
- "Webkit messes up the process of drawing an SVG image to a canvas. This is important for both selecting things (it lets you click through a gap and hit something else) and for taking screenshots (since it needs to render them to a canvas). Sorry :("
- );
- }
- };
-
- updateFilter();
- });
-
- let searchText = "";
-
- function doSearch(value) {
- searchText = value;
- updateFilter();
- }
-
- function customEntityFromFile(file, x = 0.5, y = 0.5) {
- file.arrayBuffer().then((buf) => {
- arr = new Uint8Array(buf);
- blob = new Blob([arr], { type: file.type });
- url = window.URL.createObjectURL(blob);
- makeCustomEntity(url, x, y);
- });
- }
-
- function makeCustomEntity(url, x = 0.5, y = 0.5) {
- const maker = createEntityMaker(
- {
- name: "Custom Entity",
- },
- {
- custom: {
- attributes: {
- height: {
- name: "Height",
- power: 1,
- type: "length",
- base: math.unit(6, "feet"),
- },
- },
- image: {
- source: url,
- },
- name: "Image",
- info: {},
- rename: false,
- },
- },
- []
- );
-
- const entity = maker.constructor();
-
- entity.scale = config.height.toNumber("feet") / 20;
-
- entity.ephemeral = true;
- displayEntity(entity, "custom", x, y, true, true);
- }
-
- const filterDefs = {
- author: {
- id: "author",
- name: "Authors",
- extract: (maker) => (maker.authors ? maker.authors : []),
- render: (author) => attributionData.people[author].name,
- sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1]),
- },
- owner: {
- id: "owner",
- name: "Owners",
- extract: (maker) => (maker.owners ? maker.owners : []),
- render: (owner) => attributionData.people[owner].name,
- sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1]),
- },
- species: {
- id: "species",
- name: "Species",
- extract: (maker) =>
- maker.info && maker.info.species
- ? getSpeciesInfo(maker.info.species)
- : [],
- render: (species) => speciesData[species].name,
- sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1]),
- },
- tags: {
- id: "tags",
- name: "Tags",
- extract: (maker) =>
- maker.info && maker.info.tags ? maker.info.tags : [],
- render: (tag) => tagDefs[tag],
- sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1]),
- },
- size: {
- id: "size",
- name: "Normal Size",
- extract: (maker) =>
- maker.sizes && maker.sizes.length > 0
- ? Array.from(
- maker.sizes.reduce((result, size) => {
- if (result && !size.default) {
- return result;
- }
- let meters = size.height.toNumber("meters");
- if (meters < 1e-1) {
- return ["micro"];
- } else if (meters < 1e1) {
- return ["moderate"];
- } else {
- return ["macro"];
- }
- }, null)
- )
- : [],
- render: (tag) => {
- return {
- micro: "Micro",
- moderate: "Moderate",
- macro: "Macro",
- }[tag];
- },
- sort: (tag1, tag2) => {
- const order = {
- micro: 0,
- moderate: 1,
- macro: 2,
- };
-
- return order[tag1[0]] - order[tag2[0]];
- },
- },
- allSizes: {
- id: "allSizes",
- name: "Possible Size",
- extract: (maker) =>
- maker.sizes
- ? Array.from(
- maker.sizes.reduce((set, size) => {
- const height = size.height;
-
- let result = Object.entries(sizeCategories).reduce(
- (result, [name, value]) => {
- if (result) {
- return result;
- } else {
- if (math.compare(height, value) <= 0) {
- return name;
- }
- }
- },
- null
- );
-
- set.add(result ? result : "infinite");
-
- return set;
- }, new Set())
- )
- : [],
- render: (tag) => tag[0].toUpperCase() + tag.slice(1),
- sort: (tag1, tag2) => {
- const order = [
- "atomic",
- "microscopic",
- "tiny",
- "small",
- "moderate",
- "large",
- "macro",
- "megamacro",
- "planetary",
- "stellar",
- "galactic",
- "universal",
- "omniversal",
- "infinite",
- ];
-
- return order.indexOf(tag1[0]) - order.indexOf(tag2[0]);
- },
- },
- };
-
- const filterStates = {};
-
- const sizeCategories = {
- atomic: math.unit(100, "angstroms"),
- microscopic: math.unit(100, "micrometers"),
- tiny: math.unit(100, "millimeters"),
- small: math.unit(1, "meter"),
- moderate: math.unit(3, "meters"),
- large: math.unit(10, "meters"),
- macro: math.unit(300, "meters"),
- megamacro: math.unit(1000, "kilometers"),
- planetary: math.unit(10, "earths"),
- stellar: math.unit(10, "solarradii"),
- galactic: math.unit(10, "galaxies"),
- universal: math.unit(10, "universes"),
- omniversal: math.unit(10, "multiverses"),
- };
-
- function prepareEntities() {
- availableEntities["buildings"] = makeBuildings();
- availableEntities["characters"] = makeCharacters();
- availableEntities["clothing"] = makeClothing();
- availableEntities["creatures"] = makeCreatures();
- availableEntities["fiction"] = makeFiction();
- availableEntities["food"] = makeFood();
- availableEntities["furniture"] = makeFurniture();
- availableEntities["landmarks"] = makeLandmarks();
- availableEntities["naturals"] = makeNaturals();
- availableEntities["objects"] = makeObjects();
- availableEntities["plants"] = makePlants();
- availableEntities["pokemon"] = makePokemon();
- availableEntities["real-buildings"] = makeRealBuildings();
- availableEntities["real-terrain"] = makeRealTerrains();
- availableEntities["species"] = makeSpecies();
- availableEntities["vehicles"] = makeVehicles();
-
- availableEntities["species"].forEach((x) => {
- if (x.name == "Human") {
- availableEntities["food"].push(x);
- }
- });
-
- availableEntities["characters"].sort((x, y) => {
- return x.name.localeCompare(y.name);
- });
- availableEntities["species"].sort((x, y) => {
- return x.name.localeCompare(y.name);
- });
- availableEntities["objects"].sort((x, y) => {
- return x.name.localeCompare(y.name);
- });
- availableEntities["furniture"].sort((x, y) => {
- return x.name.localeCompare(y.name);
- });
- const holder = document.querySelector("#spawners");
- const filterMenu = document.querySelector("#filters-menu");
-
- const categorySelect = document.createElement("select");
- categorySelect.id = "category-picker";
-
- holder.appendChild(categorySelect);
-
- const filterSets = {};
-
- Object.values(filterDefs).forEach((filter) => {
- filterSets[filter.id] = new Set();
- filterStates[filter.id] = false;
- });
-
- Object.entries(availableEntities).forEach(([category, entityList]) => {
- const select = document.createElement("select");
- select.id = "create-entity-" + category;
- select.classList.add("entity-select");
- for (let i = 0; i < entityList.length; i++) {
- const entity = entityList[i];
- const option = document.createElement("option");
- option.value = i;
- option.innerText = entity.name;
- select.appendChild(option);
-
- if (entity.nsfw) {
- option.classList.add("nsfw");
- }
-
- Object.values(filterDefs).forEach((filter) => {
- filter.extract(entity).forEach((result) => {
- filterSets[filter.id].add(result);
- });
- });
-
- availableEntitiesByName[entity.name] = entity;
- }
-
- select.addEventListener("change", (e) => {
- if (
- select.options[select.selectedIndex]?.classList.contains("nsfw")
- ) {
- select.classList.add("nsfw");
- } else {
- select.classList.remove("nsfw");
- }
-
- // preload the entity's first image
-
- const entity = entityList[select.selectedIndex]?.constructor();
-
- if (entity) {
- let img = new Image();
- img.src = entity.currentView.image.source;
- }
- });
-
- const button = document.createElement("button");
- button.id = "create-entity-" + category + "-button";
- button.classList.add("entity-button");
-
- button.innerHTML = '<i class="far fa-plus-square"></i>';
-
- button.addEventListener("click", (e) => {
- if (entityList[select.value] == null) return;
-
- const newEntity = entityList[select.value].constructor();
- let yOffset = 0;
-
- if (config.lockYAxis) {
- yOffset = getVerticalOffset();
- } else {
- // Snap to the ground if it's visible.
- if (config.groundSnap && pos2pix({x: 0, y: 0}).y < canvasHeight + 50) {
- yOffset = -config.y;
- } else {
- yOffset = config.height.toNumber("meters") / 2;
- }
- }
- displayEntity(
- newEntity,
- newEntity.defaultView,
- config.x,
- config.y + yOffset,
- true,
- true
- );
- });
-
- const categoryOption = document.createElement("option");
- categoryOption.value = category;
- categoryOption.innerText = category;
-
- if (category == "characters") {
- categoryOption.selected = true;
- select.classList.add("category-visible");
- button.classList.add("category-visible");
- }
-
- categorySelect.appendChild(categoryOption);
- holder.appendChild(select);
- holder.appendChild(button);
- });
-
- Object.values(filterDefs).forEach((filter) => {
- const filterHolder = document.createElement("label");
- filterHolder.setAttribute("for", "filter-toggle-" + filter.id);
- filterHolder.classList.add("filter-holder");
-
- const filterToggle = document.createElement("input");
- filterToggle.type = "checkbox";
- filterToggle.id = "filter-toggle-" + filter.id;
- filterHolder.appendChild(filterToggle);
-
- filterToggle.addEventListener("input", e => {
- filterStates[filter.id] = filterToggle.checked
- if (filterToggle.checked) {
- filterHolder.classList.add("enabled");
- } else {
- filterHolder.classList.remove("enabled");
- }
- clearFilter();
- updateFilter();
- });
-
- const filterLabel = document.createElement("div");
- filterLabel.innerText = filter.name;
- filterHolder.appendChild(filterLabel);
-
- const filterNameSelect = document.createElement("select");
- filterNameSelect.classList.add("filter-select");
- filterNameSelect.id = "filter-" + filter.id;
- filterHolder.appendChild(filterNameSelect);
-
- filterMenu.appendChild(filterHolder);
-
- Array.from(filterSets[filter.id])
- .map((name) => [name, filter.render(name)])
- .sort(filterDefs[filter.id].sort)
- .forEach((name) => {
- const option = document.createElement("option");
- option.innerText = name[1];
- option.value = name[0];
- filterNameSelect.appendChild(option);
- });
-
- filterNameSelect.addEventListener("change", (e) => {
- updateFilter();
- });
- });
-
- const spawnButton = document.createElement("button");
- spawnButton.id = "spawn-all"
- spawnButton.addEventListener("click", e => {
- spawnAll();
- });
-
- filterMenu.appendChild(spawnButton);
-
- console.log(
- "Loaded " + Object.keys(availableEntitiesByName).length + " entities"
- );
-
- categorySelect.addEventListener("input", (e) => {
- const oldSelect = document.querySelector(
- ".entity-select.category-visible"
- );
- oldSelect.classList.remove("category-visible");
- const oldButton = document.querySelector(
- ".entity-button.category-visible"
- );
- oldButton.classList.remove("category-visible");
-
- const newSelect = document.querySelector(
- "#create-entity-" + e.target.value
- );
- newSelect.classList.add("category-visible");
- const newButton = document.querySelector(
- "#create-entity-" + e.target.value + "-button"
- );
- newButton.classList.add("category-visible");
-
- recomputeFilters();
- updateFilter();
- });
-
- recomputeFilters();
-
- ratioInfo = document.body.querySelector(".extra-info");
- }
-
- function spawnAll() {
- const makers = Array.from(
- document.querySelector(".entity-select.category-visible")
- ).filter((element) => !element.classList.contains("filtered"));
- const count = makers.length + 2;
- let index = 1;
-
- if (makers.length > 50) {
- if (!confirm("Really spawn " + makers.length + " things at once?")) {
- return;
- }
- }
-
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
-
- const spawned = makers.map((element) => {
- const category = document.querySelector("#category-picker").value;
- const maker = availableEntities[category][element.value];
- const entity = maker.constructor();
-
-
- if (config.lockYAxis) {
- yOffset = getVerticalOffset();
- } else {
- // Snap to the ground if it's visible.
- if (config.groundSnap && pos2pix({x: 0, y: 0}).y < canvasHeight + 50) {
- yOffset = -config.y;
- } else {
- yOffset = config.height.toNumber("meters") / 2;
- }
- }
-
- displayEntity(
- entity,
- entity.view,
- -worldWidth * 0.45 +
- config.x +
- (worldWidth * 0.9 * index) / (count - 1),
- config.y + yOffset
- );
- index += 1;
- return entityIndex - 1;
- });
- updateSizes(true);
-
- if (config.autoFitAdd) {
- let targets = {};
- spawned.forEach((key) => {
- targets[key] = entities[key];
- });
- fitEntities(targets);
- }
- }
-
- // Only display authors and owners if they appear
- // somewhere in the current entity list
- function recomputeFilters() {
- const category = document.querySelector("#category-picker").value;
-
- const filterSets = {};
-
- Object.values(filterDefs).forEach((filter) => {
- filterSets[filter.id] = new Set();
- });
-
- availableEntities[category].forEach((entity) => {
- Object.values(filterDefs).forEach((filter) => {
- filter.extract(entity).forEach((result) => {
- filterSets[filter.id].add(result);
- });
- });
- });
-
- Object.values(filterDefs).forEach((filter) => {
- filterStates[filter.id] = false;
- document.querySelector("#filter-toggle-" + filter.id).checked = false
- document.querySelector("#filter-toggle-" + filter.id).dispatchEvent(new Event("click"))
- // always show the "none" option
- let found = filter.id == "none";
- const filterSelect = document.querySelector("#filter-" + filter.id);
- const filterSelectHolder = filterSelect.parentElement;
- filterSelect.querySelectorAll("option").forEach((element) => {
- if (
- filterSets[filter.id].has(element.value) ||
- filter.id == "none"
- ) {
- element.classList.remove("filtered");
- element.disabled = false;
- found = true;
- } else {
- element.classList.add("filtered");
- element.disabled = true;
- }
- });
-
- if (found) {
- filterSelectHolder.style.display = "";
- } else {
- filterSelectHolder.style.display = "none";
- }
- });
- }
-
- function updateFilter() {
- const category = document.querySelector("#category-picker").value;
-
- const types = Object.values(filterDefs).filter(def => filterStates[def.id]).map(def => def.id)
-
- const keys = {
-
- }
-
- types.forEach(type => {
- const filterKeySelect = document.querySelector("#filter-" + type);
- keys[type] = filterKeySelect.value;
- })
-
- clearFilter();
-
- let current = document.querySelector(
- ".entity-select.category-visible"
- ).value;
-
- let replace = current == "";
- let first = null;
-
- let count = 0;
-
- const lowerSearchText = searchText !== "" ? searchText.toLowerCase() : null;
- document
- .querySelectorAll(".entity-select.category-visible > option")
- .forEach((element) => {
- let keep = true
-
- types.forEach(type => {
- if (
- !(filterDefs[type]
- .extract(availableEntities[category][element.value])
- .indexOf(keys[type]) >= 0)
- ) {
- keep = false;
- }
- })
-
- if (
- searchText != "" &&
- !availableEntities[category][element.value].name
- .toLowerCase()
- .includes(lowerSearchText)
- ) {
- keep = false;
- }
-
- if (!keep) {
- element.classList.add("filtered");
- element.disabled = true;
-
- if (current == element.value) {
- replace = true;
- }
- } else {
- count += 1;
- if (!first) {
- first = element.value;
- }
- }
- });
-
- const button = document.querySelector("#spawn-all")
-
- button.innerText = "Spawn " + count + " filtered " + (count == 1 ? "entity" : "entities") + ".";
-
- if (replace) {
- document.querySelector(".entity-select.category-visible").value = first;
- document
- .querySelector("#create-entity-" + category)
- .dispatchEvent(new Event("change"));
- }
- }
-
- function clearFilter() {
- document
- .querySelectorAll(".entity-select.category-visible > option")
- .forEach((element) => {
- element.classList.remove("filtered");
- element.disabled = false;
- });
- }
-
- function checkFitWorld() {
- if (config.autoFit) {
- fitWorld();
- return true;
- }
- return false;
- }
-
- function fitWorld(manual = false, factor = 1.1) {
- if (Object.keys(entities).length > 0) {
- fitEntities(entities, factor);
- }
- }
-
- function fitEntities(targetEntities, manual = false, factor = 1.1) {
- let minX = Infinity;
- let maxX = -Infinity;
- let minY = Infinity;
- let maxY = -Infinity;
-
- let count = 0;
-
- const worldWidth =
- (config.height.toNumber("meters") / canvasHeight) * canvasWidth;
- const worldHeight = config.height.toNumber("meters");
-
- Object.entries(targetEntities).forEach(([key, entity]) => {
- const view = entity.view;
-
- let extra = entity.views[view].image.extra;
- extra = extra === undefined ? 1 : extra;
-
- const image = document.querySelector(
- "#entity-" + key + " > .entity-image"
- );
- const x = parseFloat(
- document.querySelector("#entity-" + key).dataset.x
- );
-
- let width = image.width;
- let height = image.height;
-
- // only really relevant if the images haven't loaded in yet
- if (height == 0) {
- height = 100;
- }
- if (width == 0) {
- width = height;
- }
-
- const xBottom =
- x -
- (entity.views[view].height.toNumber("meters") * width) / height / 2;
- const xTop =
- x +
- (entity.views[view].height.toNumber("meters") * width) / height / 2;
-
- const y = parseFloat(
- document.querySelector("#entity-" + key).dataset.y
- );
- const yBottom = y;
- const yTop = entity.views[view].height.toNumber("meters") + yBottom;
-
- minX = Math.min(minX, xBottom);
- maxX = Math.max(maxX, xTop);
- minY = Math.min(minY, yBottom);
- maxY = Math.max(maxY, yTop);
-
- count += 1;
- });
-
- if (config.lockYAxis) {
- minY = 0;
- }
- let ySize = (maxY - minY) * factor;
- let xSize = (maxX - minX) * factor;
-
- if (xSize / ySize > worldWidth / worldHeight) {
- ySize *= xSize / ySize / (worldWidth / worldHeight);
- }
-
- config.x = (maxX + minX) / 2;
- config.y = minY;
-
- height = math.unit(ySize, "meter");
-
- setWorldHeight(config.height, math.multiply(height, factor));
- }
-
- function updateWorldHeight() {
- const unit = document.querySelector("#options-height-unit").value;
- const rawValue = document.querySelector("#options-height-value").value;
-
- var value;
- try {
- value = math.evaluate(rawValue);
- if (typeof value !== "number") {
- try {
- value = value.toNumber(unit);
- } catch {
- toast(
- "Invalid input: " +
- rawValue +
- " can't be converted to " +
- unit
- );
- }
- }
- } catch {
- toast("Invalid input: could not parse " + rawValue);
- return;
- }
-
- const newHeight = Math.max(1e-40, value);
- const oldHeight = config.height;
-
- setWorldHeight(oldHeight, math.unit(newHeight, unit), true);
- }
-
- function setWorldHeight(oldHeight, newHeight, keepUnit = false) {
- worldSizeDirty = true;
- config.height = newHeight.to(
- document.querySelector("#options-height-unit").value
- );
-
- const unit = document.querySelector("#options-height-unit").value;
- setNumericInput(
- document.querySelector("#options-height-value"),
- config.height.toNumber(unit)
- );
- Object.entries(entities).forEach(([key, entity]) => {
- const element = document.querySelector("#entity-" + key);
- let newPosition;
-
- if (altHeld) {
- newPosition = adjustAbs(
- { x: element.dataset.x, y: element.dataset.y },
- oldHeight,
- config.height
- );
- } else {
- newPosition = { x: element.dataset.x, y: element.dataset.y };
- }
-
- element.dataset.x = newPosition.x;
- element.dataset.y = newPosition.y;
- });
-
- if (!keepUnit) {
- pickUnit();
- }
-
- updateSizes();
- }
-
- function loadScene(name = "default") {
- if (name === "") {
- name = "default";
- }
- try {
- const data = JSON.parse(
- localStorage.getItem("macrovision-save-" + name), math.reviver
- );
- if (data === null) {
- console.error("Couldn't load " + name);
- return false;
- }
- importScene(data);
- toast("Loaded " + name);
- return true;
- } catch (err) {
- alert(
- "Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error."
- );
- console.error(err);
- return false;
- }
- }
-
- function saveScene(name = "default") {
- try {
- const string = JSON.stringify(exportScene());
- localStorage.setItem("macrovision-save-" + name, string);
- toast("Saved as " + name);
- } catch (err) {
- alert(
- "Something went wrong while saving (maybe I don't have localStorage permissions, or exporting failed). Check the F12 console for the error."
- );
- console.error(err);
- }
- }
-
- function deleteScene(name = "default") {
- if (confirm("Really delete the " + name + " scene?")) {
- try {
- localStorage.removeItem("macrovision-save-" + name);
- toast("Deleted " + name);
- } catch (err) {
- console.error(err);
- }
- }
- updateSaveInfo();
- }
-
- function exportScene() {
- const results = {};
-
- results.entities = [];
-
- Object.entries(entities)
- .filter(([key, entity]) => entity.ephemeral !== true)
- .forEach(([key, entity]) => {
- const element = document.querySelector("#entity-" + key);
- const entityData = {
- name: entity.identifier,
- customName: entity.name,
- scale: entity.scale,
- rotation: entity.rotation,
- flipped: entity.flipped,
- view: entity.view,
- form: entity.form,
- x: element.dataset.x,
- y: element.dataset.y,
- priority: entity.priority,
- brightness: entity.brightness,
- };
-
- entityData.views = {};
-
- Object.entries(entity.views).forEach(([viewId, viewData]) => {
- Object.entries(viewData.attributes).forEach(([attrId, attrData]) => {
- if (attrData.custom) {
- if (entityData.views[viewId] === undefined) {
- entityData.views[viewId] = {};
- }
- entityData.views[viewId][attrId] = attrData;
- }
- });
- });
-
- results.entities.push(entityData);
- });
-
- const unit = document.querySelector("#options-height-unit").value;
- results.world = {
- height: config.height.toNumber(unit),
- unit: unit,
- x: config.x,
- y: config.y,
- };
-
- results.version = migrationDefs.length;
-
- return results;
- }
-
- // btoa doesn't like anything that isn't ASCII
- // great
-
- // thanks to https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
- // for providing an alternative
-
- function b64EncodeUnicode(str) {
- // first we use encodeURIComponent to get percent-encoded UTF-8,
- // then we convert the percent encodings into raw bytes which
- // can be fed into btoa.
- return btoa(
- encodeURIComponent(str).replace(
- /%([0-9A-F]{2})/g,
- function toSolidBytes(match, p1) {
- return String.fromCharCode("0x" + p1);
- }
- )
- );
- }
-
- function b64DecodeUnicode(str) {
- // Going backwards: from bytestream, to percent-encoding, to original string.
- return decodeURIComponent(
- atob(str)
- .split("")
- .map(function (c) {
- return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
- })
- .join("")
- );
- }
-
- function linkScene() {
- loc = new URL(window.location);
- const link =
- loc.protocol +
- "//" +
- loc.host +
- loc.pathname +
- "#" +
- b64EncodeUnicode(JSON.stringify(exportScene()));
- window.history.replaceState(null, "Macrovision", link);
- try {
- navigator.clipboard.writeText(link);
- toast("Copied permalink to clipboard");
- } catch {
- toast("Couldn't copy permalink");
- }
- }
-
- function copyScene() {
- const results = exportScene();
-
- navigator.clipboard.writeText(JSON.stringify(results));
- }
-
- function pasteScene() {
- try {
- navigator.clipboard
- .readText()
- .then((text) => {
- const data = JSON.parse(text, math.reviver);
- if (data.entities === undefined) {
- return;
- }
- if (data.world === undefined) {
- return;
- }
-
- importScene(data);
- })
- .catch((err) => { toast("Something went wrong when importing: " + err), console.error(err) });
- } catch (err) {
- console.error(err);
-
- // probably wasn't valid data
- }
- }
-
- function findEntity(name) {
- return availableEntitiesByName[name];
- }
-
- const migrationDefs = [
- /*
- Migration: 0 -> 1
-
- Adds x and y coordinates for the camera
- */
-
- (data) => {
- data.world.x = 0;
- data.world.y = 0;
- },
- /*
- Migration: 1 -> 2
-
- Adds priority and brightness to each entity
- */
-
- (data) => {
- data.entities.forEach((entity) => {
- entity.priority = 0;
- entity.brightness = 1;
- });
- },
- /*
- Migration: 2 -> 3
-
- Custom names are exported
- */
-
- (data) => {
- data.entities.forEach((entity) => {
- entity.customName = entity.name;
- });
- },
- /*
- Migration: 3 -> 4
-
- Rotation is now stored
- */
-
- (data) => {
- data.entities.forEach((entity) => {
- entity.rotation = 0;
- });
- },
- /*
- Migration: 4 -> 5
-
- Flipping is now stored
- */
-
- (data) => {
- data.entities.forEach((entity) => {
- entity.flipped = false;
- });
- },
- /*
- Migration: 5 -> 6
-
- Entities can now have custom attributes
- */
-
- (data) => {
- data.entities.forEach((entity) => {
- entity.views = {};
- });
- }
- ];
-
- function migrateScene(data) {
- if (data.version === undefined) {
- alert(
- "This save was created before save versions were tracked. The scene may import incorrectly."
- );
- console.trace();
- data.version = 0;
- } else if (data.version < migrationDefs.length) {
- migrationDefs[data.version](data);
- data.version += 1;
- migrateScene(data);
- }
- }
-
- function importScene(data) {
- removeAllEntities();
-
- migrateScene(data);
-
- data.entities.forEach((entityInfo) => {
- const entity = findEntity(entityInfo.name).constructor();
- entity.name = entityInfo.customName;
- entity.scale = entityInfo.scale;
- entity.rotation = entityInfo.rotation;
- entity.flipped = entityInfo.flipped;
- entity.priority = entityInfo.priority;
- entity.brightness = entityInfo.brightness;
- entity.form = entityInfo.form;
-
- Object.entries(entityInfo.views).forEach(([viewId, viewData]) => {
- if (entityInfo.views[viewId] !== undefined) {
- Object.entries(entityInfo.views[viewId]).forEach(([attrId, attrData]) => {
- entity.views[viewId].attributes[attrId] = attrData;
- });
- }
- });
-
- Object.keys(entityInfo.views).forEach(key => defineAttributeGetters(entity.views[key]));
- displayEntity(entity, entityInfo.view, entityInfo.x, entityInfo.y);
- });
-
- config.height = math.unit(data.world.height, data.world.unit);
- config.x = data.world.x;
- config.y = data.world.y;
-
- const height = math
- .unit(data.world.height, data.world.unit)
- .toNumber(defaultUnits.length[config.units]);
-
- document.querySelector("#options-height-value").value = height;
- document.querySelector("#options-height-unit").dataset.oldUnit =
- defaultUnits.length[config.units];
- document.querySelector("#options-height-unit").value =
- defaultUnits.length[config.units];
-
- if (data.canvasWidth) {
- doHorizReposition(data.canvasWidth / canvasWidth);
- }
-
- updateSizes();
- }
-
- function renderGround(ctx) {
- if (config.groundKind !== "none") {
- ctx.fillStyle = backgroundColors[config.groundKind];
- ctx.fillRect(
- 0,
- pos2pix({ x: 0, y: 0 }).y,
- canvasWidth + 100,
- canvasHeight
- );
- }
- }
- function renderToCanvas() {
- const ctx = document.querySelector("#display").getContext("2d");
-
- let groundDrawn = false;
- Object.entries(entities)
- .sort((ent1, ent2) => {
- z1 = document.querySelector("#entity-" + ent1[0]).style.zIndex;
- z2 = document.querySelector("#entity-" + ent2[0]).style.zIndex;
- return z1 - z2;
- })
- .forEach(([id, entity]) => {
- if (entity.priority >= 0 && !groundDrawn) {
- renderGround(ctx);
- groundDrawn = true;
- }
- element = document.querySelector("#entity-" + id);
- img = element.querySelector("img");
-
- let x = parseFloat(element.dataset.x);
- let y = parseFloat(element.dataset.y);
-
- let coords = pos2pix({ x: x, y: y });
-
- let offset = img.style.getPropertyValue("--offset");
- offset = parseFloat(offset.substring(0, offset.length - 1));
-
- let xSize = img.width;
- let ySize = img.height;
- x = coords.x;
- y = coords.y + ySize / 2 + (ySize * offset) / 100;
- const oldFilter = ctx.filter;
- const brightness =
- getComputedStyle(element).getPropertyValue("--brightness");
- ctx.filter = `brightness(${brightness})`;
-
- ctx.save();
- ctx.resetTransform();
- ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
- ctx.translate(x, y);
- ctx.rotate(entity.rotation);
- ctx.scale(entity.flipped ? -1 : 1, 1);
- ctx.drawImage(img, -xSize / 2, -ySize / 2, xSize, ySize);
- ctx.restore();
-
- ctx.filter = oldFilter;
- });
-
- if (!groundDrawn) {
- renderGround(ctx);
- }
- ctx.save();
- ctx.resetTransform();
- ctx.drawImage(document.querySelector("#rulers"), 0, 0);
- ctx.restore();
- }
-
- function exportCanvas(callback) {
- /** @type {CanvasRenderingContext2D} */
- const ctx = document.querySelector("#display").getContext("2d");
- ctx.canvas.toBlob(callback);
- }
-
- function generateScreenshot(callback) {
- /** @type {CanvasRenderingContext2D} */
- const ctx = document.querySelector("#display").getContext("2d");
-
- renderToCanvas();
-
- ctx.resetTransform();
-
- ctx.fillStyle = "#999";
- ctx.font = "normal normal lighter 16pt coda";
- ctx.fillText("macrovision.crux.sexy", 10, 25);
-
- exportCanvas((blob) => {
- callback(blob);
- });
- }
-
- function copyScreenshot() {
- if (window.ClipboardItem === undefined) {
- alert(
- "Sorry, this browser doesn't yet support writing images to the clipboard."
- );
- return;
- }
- generateScreenshot((blob) => {
- navigator.clipboard
- .write([
- new ClipboardItem({
- "image/png": blob,
- }),
- ])
- .then((e) => toast("Copied to clipboard!"))
- .catch((e) => {
- console.error(e);
- toast(
- "Couldn't write to the clipboard. Make sure the screenshot completes before switching tabs. Also, currently busted in Safari :("
- );
- });
- });
- drawScales(false);
- }
-
- function saveScreenshot() {
- generateScreenshot((blob) => {
- const a = document.createElement("a");
- a.href = URL.createObjectURL(blob);
- a.setAttribute("download", "macrovision.png");
- a.click();
- });
- drawScales(false);
- }
-
- function openScreenshot() {
- generateScreenshot((blob) => {
- const a = document.createElement("a");
- a.href = URL.createObjectURL(blob);
- a.setAttribute("target", "_blank");
- a.click();
- });
- drawScales(false);
- }
-
- const rateLimits = {};
-
- function toast(msg) {
- let div = document.createElement("div");
- div.innerHTML = msg;
- div.classList.add("toast");
-
- document.body.appendChild(div);
-
- setTimeout(() => {
- document.body.removeChild(div);
- }, 5000);
- }
-
- function toastRateLimit(msg, key, delay) {
- if (!rateLimits[key]) {
- toast(msg);
- rateLimits[key] = setTimeout(() => {
- delete rateLimits[key];
- }, delay);
- }
- }
- let lastTime = undefined;
-
- function pan(fromX, fromY, fromHeight, toX, toY, toHeight, duration) {
- Object.keys(entities).forEach((key) => {
- document.querySelector("#entity-" + key).classList.add("no-transition");
- });
-
- config.x = fromX;
- config.y = fromY;
- config.height = math.unit(fromHeight, "meters");
- updateSizes();
-
- lastTime = undefined;
-
- requestAnimationFrame((timestamp) =>
- panTo(
- toX,
- toY,
- toHeight,
- (toX - fromX) / duration,
- (toY - fromY) / duration,
- (toHeight - fromHeight) / duration,
- timestamp,
- duration
- )
- );
- }
-
- function panTo(
- x,
- y,
- height,
- xSpeed,
- ySpeed,
- heightSpeed,
- timestamp,
- remaining
- ) {
- if (lastTime === undefined) {
- lastTime = timestamp;
- }
- dt = timestamp - lastTime;
- remaining -= dt;
- if (remaining < 0) {
- dt += remaining;
- }
- let newX = config.x + xSpeed * dt;
- let newY = config.y + ySpeed * dt;
- let newHeight = config.height.toNumber("meters") + heightSpeed * dt;
- if (remaining > 0) {
- requestAnimationFrame((timestamp) =>
- panTo(
- x,
- y,
- height,
- xSpeed,
- ySpeed,
- heightSpeed,
- timestamp,
- remaining
- )
- );
- } else {
- Object.keys(entities).forEach((key) => {
- document
- .querySelector("#entity-" + key)
- .classList.remove("no-transition");
- });
- }
- config.x = newX;
- config.y = newY;
- config.height = math.unit(newHeight, "meters");
- updateSizes();
- }
-
- function getVerticalOffset() {
- if (config.groundPos === "very-high") {
- return (config.height.toNumber("meters") / 12) * 5;
- } else if (config.groundPos === "high") {
- return (config.height.toNumber("meters") / 12) * 4;
- } else if (config.groundPos === "medium") {
- return (config.height.toNumber("meters") / 12) * 3;
- } else if (config.groundPos === "low") {
- return (config.height.toNumber("meters") / 12) * 2;
- } else if (config.groundPos === "very-low") {
- return config.height.toNumber("meters") / 12;
- } else {
- return 0;
- }
- }
-
- function moveGround(down) {
- const index = groundPosChoices.indexOf(config.groundPos);
-
- if (down) {
- if (index < groundPosChoices.length - 1) {
- config.groundPos = groundPosChoices[index + 1];
- }
- } else {
- if (index > 0) {
- config.groundPos = groundPosChoices[index - 1];
- }
- }
-
- updateScrollButtons();
- updateSizes();
- }
-
- function updateScrollButtons() {
- const up = document.querySelector("#scroll-up");
- const down = document.querySelector("#scroll-down");
-
- up.disabled = false;
- down.disabled = false;
-
- document.querySelector("#setting-ground-pos").value = config.groundPos;
-
- if (config.lockYAxis) {
- const index = groundPosChoices.indexOf(config.groundPos);
-
- if (index == 0) {
- down.disabled = true;
- }
- if (index == groundPosChoices.length - 1) {
- up.disabled = true;
- }
- }
- }
|