less copy protection, more size visualization
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

5265 lignes
161 KiB

  1. let selected = null;
  2. let prevSelected = null;
  3. let selectedEntity = null;
  4. let prevSelectedEntity = null;
  5. let entityIndex = 0;
  6. let firsterror = true;
  7. let clicked = null;
  8. let movingInBounds = false;
  9. let dragging = false;
  10. let clickTimeout = null;
  11. let dragOffsetX = null;
  12. let dragOffsetY = null;
  13. let preloaded = new Set();
  14. let panning = false;
  15. let panReady = true;
  16. let panOffsetX = null;
  17. let panOffsetY = null;
  18. let shiftHeld = false;
  19. let altHeld = false;
  20. let entityX;
  21. let canvasWidth;
  22. let canvasHeight;
  23. let dragScale = 1;
  24. let dragScaleHandle = null;
  25. let dragEntityScale = 1;
  26. let dragEntityScaleHandle = null;
  27. let scrollDirection = 0;
  28. let scrollHandle = null;
  29. let zoomDirection = 0;
  30. let zoomHandle = null;
  31. let sizeDirection = 0;
  32. let sizeHandle = null;
  33. let worldSizeDirty = false;
  34. let rulerMode = false;
  35. let rulers = [];
  36. let currentRuler = undefined;
  37. const tagDefs = {
  38. "anthro": "Anthro",
  39. "feral": "Feral",
  40. "taur": "Taur",
  41. "naga": "Naga",
  42. "goo": "Goo"
  43. }
  44. math.createUnit("humans", {
  45. definition: "5.75 feet"
  46. });
  47. math.createUnit("story", {
  48. definition: "12 feet",
  49. prefixes: "long"
  50. });
  51. math.createUnit("stories", {
  52. definition: "12 feet",
  53. prefixes: "long"
  54. });
  55. math.createUnit("buses", {
  56. definition: "11.95 meters",
  57. prefixes: "long"
  58. });
  59. math.createUnit("marathons", {
  60. definition: "26.2 miles",
  61. prefixes: "long"
  62. });
  63. math.createUnit("timezones", {
  64. definition: "1037.54167 miles",
  65. prefixes: "long",
  66. aliases: ["timezone", "timezones"]
  67. });
  68. math.createUnit("nauticalMiles", {
  69. definition: "6080 feet",
  70. prefixes: "long",
  71. aliases: ["nauticalMile", "nauticalMiles"]
  72. });
  73. math.createUnit("fathoms", {
  74. definition: "6 feet",
  75. prefixes: "long",
  76. aliases: ["fathom", "fathoms"]
  77. });
  78. math.createUnit("earths", {
  79. definition: "12756km",
  80. prefixes: "long",
  81. aliases: ["earth", "earths", "Earth", "Earths"]
  82. });
  83. math.createUnit("lightsecond", {
  84. definition: "299792458 meters",
  85. prefixes: "long"
  86. });
  87. math.createUnit("lightseconds", {
  88. definition: "299792458 meters",
  89. prefixes: "long"
  90. });
  91. math.createUnit("parsec", {
  92. definition: "3.086e16 meters",
  93. prefixes: "long"
  94. })
  95. math.createUnit("parsecs", {
  96. definition: "3.086e16 meters",
  97. prefixes: "long"
  98. })
  99. math.createUnit("lightyears", {
  100. definition: "9.461e15 meters",
  101. prefixes: "long"
  102. })
  103. math.createUnit("AU", {
  104. definition: "149597870700 meters"
  105. })
  106. math.createUnit("AUs", {
  107. definition: "149597870700 meters"
  108. })
  109. math.createUnit("dalton", {
  110. definition: "1.66e-27 kg",
  111. prefixes: "long"
  112. });
  113. math.createUnit("daltons", {
  114. definition: "1.66e-27 kg",
  115. prefixes: "long"
  116. });
  117. math.createUnit("solarradii", {
  118. definition: "695990 km",
  119. prefixes: "long"
  120. });
  121. math.createUnit("solarmasses", {
  122. definition: "2e30 kg",
  123. prefixes: "long"
  124. });
  125. math.createUnit("galaxy", {
  126. definition: "105700 lightyears",
  127. prefixes: "long"
  128. });
  129. math.createUnit("galaxies", {
  130. definition: "105700 lightyears",
  131. prefixes: "long"
  132. });
  133. math.createUnit("universe", {
  134. definition: "93.016e9 lightyears",
  135. prefixes: "long"
  136. });
  137. math.createUnit("universes", {
  138. definition: "93.016e9 lightyears",
  139. prefixes: "long"
  140. });
  141. math.createUnit("multiverse", {
  142. definition: "1e30 lightyears",
  143. prefixes: "long"
  144. });
  145. math.createUnit("multiverses", {
  146. definition: "1e30 lightyears",
  147. prefixes: "long"
  148. });
  149. math.createUnit("footballFields", {
  150. definition: "57600 feet^2",
  151. prefixes: "long"
  152. });
  153. math.createUnit("people", {
  154. definition: "75 liters",
  155. prefixes: "long"
  156. });
  157. math.createUnit("shippingContainers", {
  158. definition: "1169 ft^3",
  159. prefixes: "long"
  160. });
  161. math.createUnit("olympicPools", {
  162. definition: "2500 m^3",
  163. prefixes: "long"
  164. });
  165. math.createUnit("oceans", {
  166. definition: "700000000 km^3",
  167. prefixes: "long"
  168. });
  169. math.createUnit("earthVolumes", {
  170. definition: "1.0867813e12 km^3",
  171. prefixes: "long"
  172. });
  173. math.createUnit("universeVolumes", {
  174. definition: "4.2137775e+32 lightyears^3",
  175. prefixes: "long"
  176. });
  177. math.createUnit("multiverseVolumes", {
  178. definition: "5.2359878e+89 lightyears^3",
  179. prefixes: "long"
  180. });
  181. math.createUnit("peopleMass", {
  182. definition: "80 kg",
  183. prefixes: "long"
  184. });
  185. math.createUnit("cars", {
  186. definition: "1250kg",
  187. prefixes: "long"
  188. });
  189. math.createUnit("busMasses", {
  190. definition: "15000kg",
  191. prefixes: "long"
  192. });
  193. math.createUnit("earthMass", {
  194. definition: "5.97e24 kg",
  195. prefixes: "long"
  196. });
  197. math.createUnit("kcal", {
  198. definition: "4184 joules",
  199. prefixes: "long"
  200. })
  201. math.createUnit("foodPounds", {
  202. definition: "867 kcal",
  203. prefixes: "long"
  204. })
  205. math.createUnit("foodKilograms", {
  206. definition: "1909 kcal",
  207. prefixes: "long"
  208. })
  209. math.createUnit("peopleEaten", {
  210. definition: "125000 kcal",
  211. prefixes: "long"
  212. })
  213. math.createUnit("barn", {
  214. definition: "10e-28 m^2",
  215. prefixes: "long"
  216. })
  217. math.createUnit("barns", {
  218. definition: "10e-28 m^2",
  219. prefixes: "long"
  220. })
  221. math.createUnit("points", {
  222. definition: "0.013888888888888888888888888 inches",
  223. prefixes: "long"
  224. })
  225. math.createUnit("beardSeconds", {
  226. definition: "10 nanometers",
  227. prefixes: "long"
  228. })
  229. math.createUnit("smoots", {
  230. definition: "5.5833333 feet",
  231. prefixes: "long"
  232. })
  233. math.createUnit("furlongs", {
  234. definition: "660 feet",
  235. prefixes: "long"
  236. })
  237. math.createUnit("nanoacres", {
  238. definition: "1e-9 acres",
  239. prefixes: "long"
  240. })
  241. math.createUnit("barnMegaparsecs", {
  242. definition: "1 barn megaparsec",
  243. prefixes: "long"
  244. })
  245. math.createUnit("firkins", {
  246. definition: "90 lb",
  247. prefixes: "long"
  248. })
  249. math.createUnit("donkeySeconds", {
  250. definition: "250 joules",
  251. prefixes: "long"
  252. })
  253. math.createUnit("HU", {
  254. definition: "0.75 inches",
  255. aliases: [
  256. "HUs",
  257. "hammerUnits"
  258. ]
  259. });
  260. const defaultUnits = {
  261. length: {
  262. metric: "meters",
  263. customary: "feet",
  264. relative: "stories",
  265. quirky: "smoots"
  266. },
  267. area: {
  268. metric: "meters^2",
  269. customary: "feet^2",
  270. relative: "footballFields",
  271. quirky: "nanoacres"
  272. },
  273. volume: {
  274. metric: "liters",
  275. customary: "gallons",
  276. relative: "olympicPools",
  277. volume: "barnMegaparsecs"
  278. },
  279. mass: {
  280. metric: "kilograms",
  281. customary: "lbs",
  282. relative: "peopleMass",
  283. quirky: "firkins"
  284. },
  285. energy: {
  286. metric: "kJ",
  287. customary: "kcal",
  288. relative: "peopleEaten",
  289. quirky: "donkeySeconds"
  290. }
  291. }
  292. const unitChoices = {
  293. length: {
  294. "metric": [
  295. "angstroms",
  296. "millimeters",
  297. "centimeters",
  298. "meters",
  299. "kilometers",
  300. ],
  301. "customary": [
  302. "inches",
  303. "feet",
  304. "yards",
  305. "miles",
  306. "nauticalMiles",
  307. ],
  308. "relative": [
  309. "humans",
  310. "stories",
  311. "buses",
  312. "marathons",
  313. "timezones",
  314. "earths",
  315. "lightseconds",
  316. "solarradii",
  317. "AUs",
  318. "lightyears",
  319. "parsecs",
  320. "galaxies",
  321. "universes",
  322. "multiverses"
  323. ],
  324. "quirky": [
  325. "beardSeconds",
  326. "points",
  327. "smoots",
  328. "furlongs",
  329. "HUs",
  330. "fathoms",
  331. ]
  332. },
  333. area: {
  334. "metric": [
  335. "cm^2",
  336. "meters^2",
  337. "kilometers^2",
  338. ],
  339. "customary": [
  340. "feet^2",
  341. "acres",
  342. "miles^2"
  343. ],
  344. "relative": [
  345. "footballFields"
  346. ],
  347. "quirky": [
  348. "barns",
  349. "nanoacres"
  350. ]
  351. },
  352. volume: {
  353. "metric": [
  354. "milliliters",
  355. "liters",
  356. "m^3",
  357. ],
  358. "customary": [
  359. "floz",
  360. "cups",
  361. "pints",
  362. "quarts",
  363. "gallons",
  364. ],
  365. "relative": [
  366. "people",
  367. "shippingContainers",
  368. "olympicPools",
  369. "oceans",
  370. "earthVolumes",
  371. "universeVolumes",
  372. "multiverseVolumes",
  373. ],
  374. "quirky": [
  375. "barnMegaparsecs"
  376. ]
  377. },
  378. mass: {
  379. "metric": [
  380. "kilograms",
  381. "milligrams",
  382. "grams",
  383. "tonnes",
  384. ],
  385. "customary": [
  386. "lbs",
  387. "ounces",
  388. "tons"
  389. ],
  390. "relative": [
  391. "peopleMass",
  392. "cars",
  393. "busMasses",
  394. "earthMass",
  395. "solarmasses"
  396. ],
  397. "quirky": [
  398. "firkins"
  399. ]
  400. },
  401. energy: {
  402. "metric": [
  403. "kJ",
  404. "foodKilograms"
  405. ],
  406. "customary": [
  407. "kcal",
  408. "foodPounds"
  409. ],
  410. "relative": [
  411. "peopleEaten"
  412. ],
  413. "quirky": [
  414. "donkeySeconds"
  415. ]
  416. }
  417. }
  418. const config = {
  419. height: math.unit(1500, "meters"),
  420. x: 0,
  421. y: 0,
  422. minLineSize: 100,
  423. maxLineSize: 150,
  424. autoFit: false,
  425. drawYAxis: true,
  426. drawXAxis: false,
  427. autoMass: "off",
  428. autoFoodIntake: false,
  429. autoPreyCapacity: false
  430. }
  431. const availableEntities = {
  432. }
  433. const availableEntitiesByName = {
  434. }
  435. const entities = {
  436. }
  437. function constrainRel(coords) {
  438. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  439. const worldHeight = config.height.toNumber("meters");
  440. if (altHeld) {
  441. return coords;
  442. }
  443. return {
  444. x: Math.min(Math.max(coords.x, -worldWidth / 2 + config.x), worldWidth / 2 + config.x),
  445. y: Math.min(Math.max(coords.y, config.y), worldHeight + config.y)
  446. }
  447. }
  448. // not using constrainRel anymore
  449. function snapPos(coords) {
  450. return {
  451. x: coords.x,
  452. y: (!config.lockYAxis || altHeld) ? coords.y : (Math.abs(coords.y) < config.height.toNumber("meters")/20 ? 0 : coords.y)
  453. };
  454. }
  455. function adjustAbs(coords, oldHeight, newHeight) {
  456. const ratio = math.divide(newHeight, oldHeight);
  457. const x = (coords.x - config.x) * ratio + config.x;
  458. const y = (coords.y - config.y) * ratio + config.y;
  459. return { x: x, y: y};
  460. }
  461. function pos2pix(coords) {
  462. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  463. const worldHeight = config.height.toNumber("meters");
  464. const x = ((coords.x - config.x) / worldWidth + 0.5) * (canvasWidth - 50) + 50;
  465. const y = (1 - (coords.y - config.y) / worldHeight) * (canvasHeight - 50) + 50;
  466. return { x: x, y: y };
  467. }
  468. function pix2pos(coords) {
  469. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  470. const worldHeight = config.height.toNumber("meters");
  471. const x = (((coords.x - 50) / (canvasWidth - 50)) - 0.5) * worldWidth + config.x;
  472. const y = (1 - ((coords.y - 50) / (canvasHeight - 50))) * worldHeight + config.y;
  473. return { x: x, y: y };
  474. }
  475. function updateEntityElement(entity, element) {
  476. const position = pos2pix({ x: element.dataset.x, y: element.dataset.y });
  477. const view = entity.view;
  478. element.style.left = position.x + "px";
  479. element.style.top = position.y + "px";
  480. element.style.setProperty("--xpos", position.x + "px");
  481. element.style.setProperty("--entity-height", "'" + entity.views[view].height.to(config.height.units[0].unit.name).format({ precision: 2 }) + "'");
  482. const pixels = math.divide(entity.views[view].height, config.height) * (canvasHeight - 50);
  483. const extra = entity.views[view].image.extra;
  484. const bottom = entity.views[view].image.bottom;
  485. const bonus = (extra ? extra : 1) * (1 / (1 - (bottom ? bottom : 0)));
  486. let height = pixels * bonus;
  487. // working around a Firefox bug here
  488. if (height > 17895698) {
  489. height = 0;
  490. }
  491. element.style.setProperty("--height", height + "px");
  492. element.style.setProperty("--extra", height - pixels + "px");
  493. element.style.setProperty("--brightness", entity.brightness);
  494. if (entity.views[view].rename)
  495. element.querySelector(".entity-name").innerText = entity.name == "" ? "" : entity.views[view].name;
  496. else
  497. element.querySelector(".entity-name").innerText = entity.name;
  498. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  499. bottomName.style.left = position.x + entityX + "px";
  500. bottomName.style.bottom = "0vh";
  501. bottomName.innerText = entity.name;
  502. const topName = document.querySelector("#top-name-" + element.dataset.key);
  503. topName.style.left = position.x + entityX + "px";
  504. topName.style.top = "20vh";
  505. topName.innerText = entity.name;
  506. if (entity.views[view].height.toNumber("meters") / 10 > config.height.toNumber("meters")) {
  507. topName.classList.add("top-name-needed");
  508. } else {
  509. topName.classList.remove("top-name-needed");
  510. }
  511. updateInfo();
  512. }
  513. let ratioInfo
  514. function updateInfo() {
  515. let text = ""
  516. if (config.showRatios) {
  517. if (selectedEntity !== null && prevSelectedEntity !== null && selectedEntity !== prevSelectedEntity) {
  518. let first = selectedEntity.currentView.height;
  519. let second = prevSelectedEntity.currentView.height;
  520. if (first.toNumber("meters") < second.toNumber("meters")) {
  521. text += selectedEntity.name + " is " + math.format(math.divide(second, first), { precision: 5 }) + " times smaller than " + prevSelectedEntity.name;
  522. } else {
  523. text += selectedEntity.name + " is " + math.format(math.divide(first, second), { precision: 5 })+ " times taller than " + prevSelectedEntity.name;
  524. }
  525. text += "\n";
  526. let apparentHeight = math.multiply(math.divide(second, first), math.unit(6, "feet"));
  527. if (config.units === "metric") {
  528. apparentHeight = apparentHeight.to("meters");
  529. }
  530. text += prevSelectedEntity.name + " looks " + math.format(apparentHeight, { precision: 3}) + " tall to " + selectedEntity.name + "\n";
  531. if (selectedEntity.currentView.capacity && prevSelectedEntity.currentView.weight) {
  532. const containCount = math.divide(selectedEntity.currentView.capacity, math.divide(prevSelectedEntity.currentView.weight, math.unit("80kg/people")));
  533. if (containCount > 0.1) {
  534. text += selectedEntity.name + " can fit " + math.format(containCount, { precision: 1 }) + " of " + prevSelectedEntity.name + " inside them" + "\n"
  535. }
  536. }
  537. }
  538. }
  539. if (config.showHorizon) {
  540. if (selectedEntity !== null) {
  541. const y = document.querySelector("#entity-" + selectedEntity.index).dataset.y;
  542. const R = math.unit(1.27560e+7, "meters")
  543. const h = math.add(selectedEntity.currentView.height, math.unit(y, "meters"))
  544. const first = math.multiply(2, math.multiply(R, h))
  545. const second = math.multiply(h, h)
  546. const sightline = math.sqrt(math.add(first, second)).to(config.height.units[0].unit.name)
  547. sightline.fixPrefix = false
  548. text += selectedEntity.name + " could see for " + math.format(sightline, { precision: 3 }) + "\n"
  549. }
  550. }
  551. if (config.showRatios && config.showHorizon) {
  552. if (selectedEntity !== null && prevSelectedEntity !== null && selectedEntity !== prevSelectedEntity) {
  553. const y1 = document.querySelector("#entity-" + selectedEntity.index).dataset.y;
  554. const y2 = document.querySelector("#entity-" + prevSelectedEntity.index).dataset.y;
  555. const R = math.unit(1.27560e+7, "meters")
  556. const R2 = math.subtract(math.subtract(R, prevSelectedEntity.currentView.height), math.unit(y2, "meters"))
  557. const h = math.add(selectedEntity.currentView.height, math.unit(y1, "meters"))
  558. const first = math.pow(math.add(R, h), 2)
  559. const second = math.pow(R2, 2)
  560. const sightline = math.sqrt(math.subtract(first, second)).to(config.height.units[0].unit.name)
  561. sightline.fixPrefix = false
  562. text += selectedEntity.name + " could see " + prevSelectedEntity.name + " from " + math.format(sightline, { precision: 3 }) + " away\n"
  563. }
  564. }
  565. ratioInfo.innerText = text;
  566. }
  567. function pickUnit() {
  568. if (!config.autoUnits) {
  569. return;
  570. }
  571. let type = null;
  572. let category = null;
  573. const heightSelect = document.querySelector("#options-height-unit");
  574. currentUnit = heightSelect.value;
  575. Object.keys(unitChoices).forEach(unitType => {
  576. Object.keys(unitChoices[unitType]).forEach(unitCategory => {
  577. if (unitChoices[unitType][unitCategory].includes(currentUnit)) {
  578. type = unitType;
  579. category = unitCategory;
  580. }
  581. })
  582. })
  583. // This should only happen if the unit selector isn't set up yet.
  584. // It doesn't really matter what goes into it.
  585. if (type === null || category === null) {
  586. return "meters"
  587. }
  588. const choices = unitChoices[type][category].map(unit => {
  589. let value = config.height.toNumber(unit);
  590. if (value < 1) {
  591. value = 1 / value / value;
  592. }
  593. return [unit, value]
  594. })
  595. heightSelect.value = choices.sort((a, b) => {
  596. return a[1] - b[1]
  597. })[0][0]
  598. selectNewUnit();
  599. }
  600. function updateSizes(dirtyOnly = false) {
  601. updateInfo();
  602. if (config.lockYAxis) {
  603. if (config.groundPos === "very-high") {
  604. config.y = -config.height.toNumber("meters") / 12 * 5;
  605. } else if (config.groundPos === "high") {
  606. config.y = -config.height.toNumber("meters") / 12 * 4;
  607. } else if (config.groundPos === "medium") {
  608. config.y = -config.height.toNumber("meters") / 12 * 3;
  609. } else if (config.groundPos === "low") {
  610. config.y = -config.height.toNumber("meters") / 12 * 2;
  611. } else if (config.groundPos === "very-low") {
  612. config.y = -config.height.toNumber("meters") / 12;
  613. } else {
  614. config.y = 0;
  615. }
  616. }
  617. drawScales(dirtyOnly);
  618. let ordered = Object.entries(entities);
  619. ordered.sort((e1, e2) => {
  620. if (e1[1].priority != e2[1].priority) {
  621. return e2[1].priority - e1[1].priority;
  622. } else {
  623. return e1[1].views[e1[1].view].height.value - e2[1].views[e2[1].view].height.value
  624. }
  625. });
  626. let zIndex = ordered.length;
  627. ordered.forEach(entity => {
  628. const element = document.querySelector("#entity-" + entity[0]);
  629. element.style.zIndex = zIndex;
  630. if (!dirtyOnly || entity[1].dirty) {
  631. updateEntityElement(entity[1], element, zIndex);
  632. entity[1].dirty = false;
  633. }
  634. zIndex -= 1;
  635. });
  636. document.querySelector("#ground").style.top = pos2pix({x: 0, y: 0}).y + "px";
  637. drawRulers();
  638. }
  639. function drawRulers() {
  640. const canvas = document.querySelector("#rulers");
  641. /** @type {CanvasRenderingContext2D} */
  642. const ctx = canvas.getContext("2d");
  643. const deviceScale = window.devicePixelRatio;
  644. ctx.canvas.width = Math.floor(canvas.clientWidth * deviceScale);
  645. ctx.canvas.height = Math.floor(canvas.clientHeight * deviceScale);
  646. ctx.scale(deviceScale, deviceScale);
  647. rulers.concat(currentRuler ? [currentRuler] : []).forEach(rulerDef => {
  648. let x0 = rulerDef.x0;
  649. let y0 = rulerDef.y0;
  650. let x1 = rulerDef.x1;
  651. let y1 = rulerDef.y1;
  652. if (rulerDef.entityKey !== null) {
  653. const entity = entities[rulerDef.entityKey]
  654. const entityElement = document.querySelector("#entity-" + rulerDef.entityKey)
  655. x0 *= entity.scale;
  656. y0 *= entity.scale;
  657. x1 *= entity.scale;
  658. y1 *= entity.scale;
  659. x0 += parseFloat(entityElement.dataset.x)
  660. x1 += parseFloat(entityElement.dataset.x)
  661. y0 += parseFloat(entityElement.dataset.y)
  662. y1 += parseFloat(entityElement.dataset.y)
  663. }
  664. ctx.save();
  665. ctx.beginPath();
  666. const start = pos2pix({x: x0, y: y0});
  667. const end = pos2pix({x: x1, y: y1});
  668. ctx.moveTo(start.x, start.y);
  669. ctx.lineTo(end.x, end.y);
  670. ctx.lineWidth = 5;
  671. ctx.strokeStyle = "#f8f";
  672. ctx.stroke();
  673. const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
  674. ctx.fillStyle = "#eeeeee";
  675. ctx.font = 'normal 24pt coda';
  676. ctx.translate(center.x, center.y);
  677. let angle = Math.atan2(end.y - start.y, end.x - start.x);
  678. if (angle < -Math.PI/2) {
  679. angle += Math.PI;
  680. }
  681. if (angle > Math.PI/2) {
  682. angle -= Math.PI;
  683. }
  684. ctx.rotate(angle);
  685. const offsetX = Math.cos(angle + Math.PI/2);
  686. const offsetY = Math.sin(angle + Math.PI/2);
  687. const distance = Math.sqrt(Math.pow(y1 - y0, 2) + Math.pow(x1 - x0, 2));
  688. const distanceInUnits = math.unit(distance, "meters").to(document.querySelector("#options-height-unit").value);
  689. const textSize = ctx.measureText(distanceInUnits.format({ precision: 3}));
  690. ctx.fillText(distanceInUnits.format({ precision: 3}), -offsetX * 10 - textSize.width / 2, -offsetY*10);
  691. ctx.restore();
  692. });
  693. }
  694. function drawScales(ifDirty = false) {
  695. const canvas = document.querySelector("#display");
  696. /** @type {CanvasRenderingContext2D} */
  697. const ctx = canvas.getContext("2d");
  698. const deviceScale = window.devicePixelRatio;
  699. ctx.canvas.width = Math.floor(canvas.clientWidth * deviceScale);
  700. ctx.canvas.height = Math.floor(canvas.clientHeight * deviceScale);
  701. ctx.scale(deviceScale, deviceScale);
  702. ctx.beginPath();
  703. ctx.rect(0, 0, ctx.canvas.width / deviceScale, ctx.canvas.height / deviceScale);
  704. switch(config.background){
  705. case "black":
  706. ctx.fillStyle = "#000";
  707. break;
  708. case "dark":
  709. ctx.fillStyle = "#111";
  710. break;
  711. case "medium":
  712. ctx.fillStyle = "#333";
  713. break;
  714. case "light":
  715. ctx.fillStyle = "#555";
  716. break;
  717. }
  718. ctx.fill();
  719. if (config.drawYAxis || config.drawAltitudes !== "none") {
  720. drawVerticalScale(ifDirty);
  721. }
  722. if (config.drawXAxis) {
  723. drawHorizontalScale(ifDirty);
  724. }
  725. }
  726. function drawVerticalScale(ifDirty = false) {
  727. if (ifDirty && !worldSizeDirty)
  728. return;
  729. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  730. let total = heightPer.clone();
  731. total.value = config.y;
  732. let y = ctx.canvas.clientHeight - 50;
  733. let offset = total.toNumber("meters") % heightPer.toNumber("meters");
  734. y += offset / heightPer.toNumber("meters") * pixelsPer;
  735. total = math.subtract(total, math.unit(offset, "meters"));
  736. for (; y >= 50; y -= pixelsPer) {
  737. drawTick(ctx, 50, y, total.format({ precision: 3 }));
  738. total = math.add(total, heightPer);
  739. }
  740. }
  741. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, label, flipped=false) {
  742. const oldStroke = ctx.strokeStyle;
  743. const oldFill = ctx.fillStyle;
  744. x = Math.round(x);
  745. y = Math.round(y);
  746. ctx.beginPath();
  747. ctx.moveTo(x, y);
  748. ctx.lineTo(x + 20, y);
  749. ctx.strokeStyle = "#000000";
  750. ctx.stroke();
  751. ctx.beginPath();
  752. ctx.moveTo(x + 20, y);
  753. ctx.lineTo(ctx.canvas.clientWidth - 70, y);
  754. if (flipped) {
  755. ctx.strokeStyle = "#666666";
  756. } else {
  757. ctx.strokeStyle = "#aaaaaa";
  758. }
  759. ctx.stroke();
  760. ctx.beginPath();
  761. ctx.moveTo(ctx.canvas.clientWidth - 70, y);
  762. ctx.lineTo(ctx.canvas.clientWidth - 50, y);
  763. ctx.strokeStyle = "#000000";
  764. ctx.stroke();
  765. const oldFont = ctx.font;
  766. ctx.font = 'normal 24pt coda';
  767. ctx.fillStyle = "#dddddd";
  768. ctx.beginPath();
  769. if (flipped) {
  770. ctx.textAlign = "end";
  771. ctx.fillText(label, ctx.canvas.clientWidth - 70, y + 35)
  772. } else {
  773. ctx.fillText(label, x + 20, y + 35);
  774. }
  775. ctx.textAlign = "start";
  776. ctx.font = oldFont;
  777. ctx.strokeStyle = oldStroke;
  778. ctx.fillStyle = oldFill;
  779. }
  780. function drawAltitudeLine(ctx, height, label) {
  781. const pixelScale = (ctx.canvas.clientHeight - 100) / config.height.toNumber("meters");
  782. const y = ctx.canvas.clientHeight - 50 - (height.toNumber("meters") - config.y) * pixelScale;
  783. if (y < ctx.canvas.clientHeight - 100) {
  784. drawTick(ctx, 50, y, label, true);
  785. }
  786. }
  787. const canvas = document.querySelector("#display");
  788. /** @type {CanvasRenderingContext2D} */
  789. const ctx = canvas.getContext("2d");
  790. const pixelScale = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
  791. let pixelsPer = pixelScale;
  792. heightPer = 1;
  793. if (pixelsPer < config.minLineSize) {
  794. const factor = math.ceil(config.minLineSize / pixelsPer);
  795. heightPer *= factor;
  796. pixelsPer *= factor;
  797. }
  798. if (pixelsPer > config.maxLineSize) {
  799. const factor = math.ceil(pixelsPer / config.maxLineSize);
  800. heightPer /= factor;
  801. pixelsPer /= factor;
  802. }
  803. if (heightPer == 0) {
  804. console.error("The world size is invalid! Refusing to draw the scale...");
  805. return;
  806. }
  807. heightPer = math.unit(heightPer, document.querySelector("#options-height-unit").value);
  808. ctx.beginPath();
  809. ctx.moveTo(50, 50);
  810. ctx.lineTo(50, ctx.canvas.clientHeight - 50);
  811. ctx.stroke();
  812. ctx.beginPath();
  813. ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
  814. ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
  815. ctx.stroke();
  816. if (config.drawYAxis) {
  817. drawTicks(ctx, pixelsPer, heightPer);
  818. }
  819. if (config.drawAltitudes == "atmosphere" || config.drawAltitudes == "all") {
  820. drawAltitudeLine(ctx, math.unit(8, "km"), "Troposphere");
  821. drawAltitudeLine(ctx, math.unit(17.5, "km"), "Ozone Layer");
  822. drawAltitudeLine(ctx, math.unit(50, "km"), "Stratosphere");
  823. drawAltitudeLine(ctx, math.unit(85, "km"), "Mesosphere");
  824. drawAltitudeLine(ctx, math.unit(675, "km"), "Thermosphere");
  825. drawAltitudeLine(ctx, math.unit(10000, "km"), "Exosphere");
  826. }
  827. if (config.drawAltitudes == "orbits" || config.drawAltitudes == "all") {
  828. drawAltitudeLine(ctx, math.unit(7, "miles"), "Cruising Altitude");
  829. drawAltitudeLine(ctx, math.unit(100, "km"), "Edge of Space (Kármán line)");
  830. drawAltitudeLine(ctx, math.unit(211.3, "miles"), "Space Station");
  831. drawAltitudeLine(ctx, math.unit(369.7, "miles"), "Hubble Telescope");
  832. drawAltitudeLine(ctx, math.unit(1500, "km"), "Low Earth Orbit");
  833. drawAltitudeLine(ctx, math.unit(20350, "km"), "GPS Satellites");
  834. drawAltitudeLine(ctx, math.unit(35786, "km"), "Geosynchronous Orbit");
  835. drawAltitudeLine(ctx, math.unit(238900, "miles"), "Lunar Orbit");
  836. drawAltitudeLine(ctx, math.unit(57.9e6, "km"), "Orbit of Mercury");
  837. drawAltitudeLine(ctx, math.unit(108.2e6, "km"), "Orbit of Venus");
  838. drawAltitudeLine(ctx, math.unit(1, "AU"), "Orbit of Earth");
  839. drawAltitudeLine(ctx, math.unit(227.9e6, "km"), "Orbit of Mars");
  840. drawAltitudeLine(ctx, math.unit(778.6e6, "km"), "Orbit of Jupiter");
  841. drawAltitudeLine(ctx, math.unit(1433.5e6, "km"), "Orbit of Saturn");
  842. drawAltitudeLine(ctx, math.unit(2872.5e6, "km"), "Orbit of Uranus");
  843. drawAltitudeLine(ctx, math.unit(4495.1e6, "km"), "Orbit of Neptune");
  844. drawAltitudeLine(ctx, math.unit(5906.4e6, "km"), "Orbit of Pluto");
  845. drawAltitudeLine(ctx, math.unit(2.7, "AU"), "Asteroid Belt");
  846. drawAltitudeLine(ctx, math.unit(123, "AU"), "Heliopause");
  847. drawAltitudeLine(ctx, math.unit(26e3, "lightyears"), "Orbit of Sol");
  848. }
  849. if (config.drawAltitudes == "weather" || config.drawAltitudes == "all") {
  850. drawAltitudeLine(ctx, math.unit(1000, "meters"), "Low-level Clouds");
  851. drawAltitudeLine(ctx, math.unit(3000, "meters"), "Mid-level Clouds");
  852. drawAltitudeLine(ctx, math.unit(10000, "meters"), "High-level Clouds");
  853. drawAltitudeLine(ctx, math.unit(20, "km"), "Polar Stratospheric Clouds");
  854. drawAltitudeLine(ctx, math.unit(80, "km"), "Noctilucent Clouds");
  855. drawAltitudeLine(ctx, math.unit(100, "km"), "Aurora");
  856. }
  857. if (config.drawAltitudes == "water" || config.drawAltitudes == "all") {
  858. drawAltitudeLine(ctx, math.unit(12100, "feet"), "Average Ocean Depth");
  859. drawAltitudeLine(ctx, math.unit(8376, "meters"), "Milkwaukee Deep");
  860. drawAltitudeLine(ctx, math.unit(10984, "meters"), "Challenger Deep");
  861. drawAltitudeLine(ctx, math.unit(5550, "meters"), "Molloy Deep");
  862. drawAltitudeLine(ctx, math.unit(7290, "meters"), "Sunda Deep");
  863. drawAltitudeLine(ctx, math.unit(592, "meters"), "Crater Lake");
  864. drawAltitudeLine(ctx, math.unit(7.5, "meters"), "Littoral Zone");
  865. drawAltitudeLine(ctx, math.unit(140, "meters"), "Continental Shelf");
  866. }
  867. if (config.drawAltitudes == "geology" || config.drawAltitudes == "all") {
  868. drawAltitudeLine(ctx, math.unit(35, "km"), "Crust");
  869. drawAltitudeLine(ctx, math.unit(670, "km"), "Upper Mantle");
  870. drawAltitudeLine(ctx, math.unit(2890, "km"), "Lower Mantle");
  871. drawAltitudeLine(ctx, math.unit(5150, "km"), "Outer Core");
  872. drawAltitudeLine(ctx, math.unit(6370, "km"), "Inner Core");
  873. }
  874. if (config.drawAltitudes == "thicknesses" || config.drawAltitudes == "all") {
  875. drawAltitudeLine(ctx, math.unit(0.335, "nm"), "Monolayer Graphene");
  876. drawAltitudeLine(ctx, math.unit(3, "um"), "Spider Silk");
  877. drawAltitudeLine(ctx, math.unit(0.07, "mm"), "Human Hair");
  878. drawAltitudeLine(ctx, math.unit(0.1, "mm"), "Sheet of Paper");
  879. drawAltitudeLine(ctx, math.unit(0.5, "mm"), "Yarn");
  880. drawAltitudeLine(ctx, math.unit(0.0155, "inches"), "Thread");
  881. drawAltitudeLine(ctx, math.unit(0.1, "um"), "Gold Leaf");
  882. drawAltitudeLine(ctx, math.unit(35, "um"), "PCB Trace");
  883. }
  884. if (config.drawAltitudes == "airspaces" || config.drawAltitudes == "all") {
  885. drawAltitudeLine(ctx, math.unit(18000, "feet"), "Class A");
  886. drawAltitudeLine(ctx, math.unit(14500, "feet"), "Class E");
  887. drawAltitudeLine(ctx, math.unit(10000, "feet"), "Class B");
  888. drawAltitudeLine(ctx, math.unit(4000, "feet"), "Class C");
  889. drawAltitudeLine(ctx, math.unit(2500, "feet"), "Class D");
  890. }
  891. if (config.drawAltitudes == "races" || config.drawAltitudes == "all") {
  892. drawAltitudeLine(ctx, math.unit(100, "meters"), "100m Dash");
  893. drawAltitudeLine(ctx, math.unit(26.2188/2, "miles"), "Half Marathon");
  894. drawAltitudeLine(ctx, math.unit(26.2188, "miles"), "Marathon");
  895. drawAltitudeLine(ctx, math.unit(161.734, "miles"), "Monaco Grand Prix");
  896. drawAltitudeLine(ctx, math.unit(500, "miles"), "Daytona 500");
  897. drawAltitudeLine(ctx, math.unit(2121.6, "miles"), "Tour de France");
  898. }
  899. if (config.drawAltitudes == "olympic-records" || config.drawAltitudes == "all") {
  900. drawAltitudeLine(ctx, math.unit(2.39, "meters"), "High Jump");
  901. drawAltitudeLine(ctx, math.unit(6.03, "meters"), "Pole Vault");
  902. drawAltitudeLine(ctx, math.unit(8.90, "meters"), "Long Jump");
  903. drawAltitudeLine(ctx, math.unit(18.09, "meters"), "Triple Jump");
  904. drawAltitudeLine(ctx, math.unit(23.30, "meters"), "Shot Put");
  905. drawAltitudeLine(ctx, math.unit(72.30, "meters"), "Discus Throw");
  906. drawAltitudeLine(ctx, math.unit(84.80, "meters"), "Hammer Throw");
  907. drawAltitudeLine(ctx, math.unit(90.57, "meters"), "Javelin Throw");
  908. }
  909. }
  910. // this is a lot of copypizza...
  911. function drawHorizontalScale(ifDirty = false) {
  912. if (ifDirty && !worldSizeDirty)
  913. return;
  914. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  915. let total = heightPer.clone();
  916. total.value = math.unit(-config.x, "meters").toNumber(config.unit);
  917. // further adjust it to put the current position in the center
  918. total.value -= heightPer.toNumber("meters") / pixelsPer * (canvasWidth + 50) / 2;
  919. let x = ctx.canvas.clientWidth - 50;
  920. let offset = total.toNumber("meters") % heightPer.toNumber("meters");
  921. x += offset / heightPer.toNumber("meters") * pixelsPer;
  922. total = math.subtract(total, math.unit(offset, "meters"));
  923. for (; x >= 50 - pixelsPer; x -= pixelsPer) {
  924. // negate it so that the left side is negative
  925. drawTick(ctx, x, 50, math.multiply(-1, total).format({ precision: 3 }));
  926. total = math.add(total, heightPer);
  927. }
  928. }
  929. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, label) {
  930. ctx.save()
  931. x = Math.round(x);
  932. y = Math.round(y);
  933. ctx.beginPath();
  934. ctx.moveTo(x, y);
  935. ctx.lineTo(x, y + 20);
  936. ctx.strokeStyle = "#000000";
  937. ctx.stroke();
  938. ctx.beginPath();
  939. ctx.moveTo(x, y + 20);
  940. ctx.lineTo(x, ctx.canvas.clientHeight - 70);
  941. ctx.strokeStyle = "#aaaaaa";
  942. ctx.stroke();
  943. ctx.beginPath();
  944. ctx.moveTo(x, ctx.canvas.clientHeight - 70);
  945. ctx.lineTo(x, ctx.canvas.clientHeight - 50);
  946. ctx.strokeStyle = "#000000";
  947. ctx.stroke();
  948. const oldFont = ctx.font;
  949. ctx.font = 'normal 24pt coda';
  950. ctx.fillStyle = "#dddddd";
  951. ctx.beginPath();
  952. ctx.fillText(label, x + 35, y + 20);
  953. ctx.restore()
  954. }
  955. const canvas = document.querySelector("#display");
  956. /** @type {CanvasRenderingContext2D} */
  957. const ctx = canvas.getContext("2d");
  958. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
  959. heightPer = 1;
  960. if (pixelsPer < config.minLineSize * 2) {
  961. const factor = math.ceil(config.minLineSize * 2/ pixelsPer);
  962. heightPer *= factor;
  963. pixelsPer *= factor;
  964. }
  965. if (pixelsPer > config.maxLineSize * 2) {
  966. const factor = math.ceil(pixelsPer / 2/ config.maxLineSize);
  967. heightPer /= factor;
  968. pixelsPer /= factor;
  969. }
  970. if (heightPer == 0) {
  971. console.error("The world size is invalid! Refusing to draw the scale...");
  972. return;
  973. }
  974. heightPer = math.unit(heightPer, document.querySelector("#options-height-unit").value);
  975. ctx.beginPath();
  976. ctx.moveTo(0, 50);
  977. ctx.lineTo(ctx.canvas.clientWidth, 50);
  978. ctx.stroke();
  979. ctx.beginPath();
  980. ctx.moveTo(0, ctx.canvas.clientHeight - 50);
  981. ctx.lineTo(ctx.canvas.clientWidth , ctx.canvas.clientHeight - 50);
  982. ctx.stroke();
  983. drawTicks(ctx, pixelsPer, heightPer);
  984. }
  985. // Entities are generated as needed, and we make a copy
  986. // every time - the resulting objects get mutated, after all.
  987. // But we also want to be able to read some information without
  988. // calling the constructor -- e.g. making a list of authors and
  989. // owners. So, this function is used to generate that information.
  990. // It is invoked like makeEntity so that it can be dropped in easily,
  991. // but returns an object that lets you construct many copies of an entity,
  992. // rather than creating a new entity.
  993. function createEntityMaker(info, views, sizes, forms) {
  994. const maker = {};
  995. maker.name = info.name;
  996. maker.info = info;
  997. maker.sizes = sizes;
  998. maker.constructor = () => makeEntity(info, views, sizes, forms);
  999. maker.authors = [];
  1000. maker.owners = [];
  1001. maker.nsfw = false;
  1002. Object.values(views).forEach(view => {
  1003. const authors = authorsOf(view.image.source);
  1004. if (authors) {
  1005. authors.forEach(author => {
  1006. if (maker.authors.indexOf(author) == -1) {
  1007. maker.authors.push(author);
  1008. }
  1009. });
  1010. }
  1011. const owners = ownersOf(view.image.source);
  1012. if (owners) {
  1013. owners.forEach(owner => {
  1014. if (maker.owners.indexOf(owner) == -1) {
  1015. maker.owners.push(owner);
  1016. }
  1017. });
  1018. }
  1019. if (isNsfw(view.image.source)) {
  1020. maker.nsfw = true;
  1021. }
  1022. });
  1023. return maker;
  1024. }
  1025. // This function serializes and parses its arguments to avoid sharing
  1026. // references to a common object. This allows for the objects to be
  1027. // safely mutated.
  1028. function makeEntity(info, views, sizes, forms = {}) {
  1029. const entityTemplate = {
  1030. name: info.name,
  1031. identifier: info.name,
  1032. scale: 1,
  1033. rotation: 0,
  1034. info: JSON.parse(JSON.stringify(info)),
  1035. views: JSON.parse(JSON.stringify(views), math.reviver),
  1036. sizes: sizes === undefined ? [] : JSON.parse(JSON.stringify(sizes), math.reviver),
  1037. forms: forms,
  1038. init: function () {
  1039. const entity = this;
  1040. Object.entries(this.forms).forEach(([formKey, form]) => {
  1041. if (form.default) {
  1042. this.defaultForm = formKey;
  1043. }
  1044. });
  1045. Object.entries(this.views).forEach(([viewKey, view]) => {
  1046. view.parent = this;
  1047. if (this.defaultView === undefined) {
  1048. this.defaultView = viewKey;
  1049. this.view = viewKey;
  1050. this.form = view.form;
  1051. }
  1052. if (view.default) {
  1053. if (forms === {} || this.defaultForm === view.form)
  1054. {
  1055. this.defaultView = viewKey;
  1056. this.view = viewKey;
  1057. this.form = view.form;
  1058. }
  1059. }
  1060. // to remember the units the user last picked
  1061. view.units = {};
  1062. if (config.autoMass !== "off" && view.attributes.weight === undefined) {
  1063. let base = undefined;
  1064. switch(config.autoMass) {
  1065. case "human":
  1066. baseMass = math.unit(150, "lbs")
  1067. baseHeight = math.unit(5.917, "feet")
  1068. break
  1069. case "quadruped at shoulder":
  1070. baseMass = math.unit(80, "lbs")
  1071. baseHeight = math.unit(30, "inches")
  1072. break
  1073. }
  1074. const ratio = math.divide(view.attributes.height.base, baseHeight)
  1075. view.attributes.weight = {
  1076. name: "Mass",
  1077. power: 3,
  1078. type: "mass",
  1079. base: math.multiply(baseMass, Math.pow(ratio, 3))
  1080. }
  1081. }
  1082. if (config.autoFoodIntake && view.attributes.weight !== undefined && view.attributes.energyNeed === undefined) {
  1083. view.attributes.energyIntake = {
  1084. name: "Food Intake",
  1085. power: 3,
  1086. type: "energy",
  1087. base: math.unit(2000 * view.attributes.weight.base.toNumber("lbs") / 150, "kcal")
  1088. }
  1089. }
  1090. if (config.autoCaloricValue && view.attributes.weight !== undefined && view.attributes.energyWorth === undefined) {
  1091. view.attributes.energyNeed = {
  1092. name: "Caloric Value",
  1093. power: 3,
  1094. type: "energy",
  1095. base: math.unit(860 * view.attributes.weight.base.toNumber("lbs"), "kcal")
  1096. }
  1097. }
  1098. if (config.autoPreyCapacity !== "none" && view.attributes.weight !== undefined && view.attributes.capacity === undefined) {
  1099. view.attributes.capacity = {
  1100. name: "Capacity",
  1101. power: 3,
  1102. type: "volume",
  1103. base: math.unit((config.autoPreyCapacity == "same-size" ? 1 : 0.05) * view.attributes.weight.base.toNumber("lbs") / 150, "people")
  1104. }
  1105. }
  1106. Object.entries(view.attributes).forEach(([key, val]) => {
  1107. Object.defineProperty(
  1108. view,
  1109. key,
  1110. {
  1111. get: function () {
  1112. return math.multiply(Math.pow(this.parent.scale, this.attributes[key].power), this.attributes[key].base);
  1113. },
  1114. set: function (value) {
  1115. const newScale = Math.pow(math.divide(value, this.attributes[key].base), 1 / this.attributes[key].power);
  1116. this.parent.scale = newScale;
  1117. }
  1118. }
  1119. );
  1120. });
  1121. });
  1122. this.sizes.forEach(size => {
  1123. if (size.default === true) {
  1124. if (Object.keys(forms).length > 0) {
  1125. if (this.defaultForm !== size.form) {
  1126. return;
  1127. }
  1128. }
  1129. this.views[this.defaultView].height = size.height;
  1130. this.size = size;
  1131. }
  1132. });
  1133. if (this.size === undefined && this.sizes.length > 0) {
  1134. this.views[this.defaultView].height = this.sizes[0].height;
  1135. this.size = this.sizes[0];
  1136. console.warn("No default size set for " + info.name);
  1137. } else if (this.sizes.length == 0) {
  1138. this.sizes = [
  1139. {
  1140. name: "Normal",
  1141. height: this.views[this.defaultView].height
  1142. }
  1143. ];
  1144. this.size = this.sizes[0];
  1145. }
  1146. this.desc = {};
  1147. Object.entries(this.info).forEach(([key, value]) => {
  1148. Object.defineProperty(
  1149. this.desc,
  1150. key,
  1151. {
  1152. get: function () {
  1153. let text = value.text;
  1154. if (entity.views[entity.view].info) {
  1155. if (entity.views[entity.view].info[key]) {
  1156. text = combineInfo(text, entity.views[entity.view].info[key]);
  1157. }
  1158. }
  1159. if (entity.size.info) {
  1160. if (entity.size.info[key]) {
  1161. text = combineInfo(text, entity.size.info[key]);
  1162. }
  1163. }
  1164. return { title: value.title, text: text };
  1165. }
  1166. }
  1167. )
  1168. });
  1169. Object.defineProperty(
  1170. this,
  1171. "currentView",
  1172. {
  1173. get: function() {
  1174. return entity.views[entity.view];
  1175. }
  1176. }
  1177. )
  1178. this.formViews = {};
  1179. this.formSizes = {}
  1180. Object.entries(views).forEach(([key, value]) => {
  1181. if (value.default) {
  1182. this.formViews[value.form] = key;
  1183. }
  1184. });
  1185. Object.entries(views).forEach(([key, value]) => {
  1186. if (this.formViews[value.form] === undefined) {
  1187. this.formViews[value.form] = key;
  1188. }
  1189. });
  1190. this.sizes.forEach(size => {
  1191. if (size.default) {
  1192. this.formSizes[size.form] = size;
  1193. }
  1194. })
  1195. this.sizes.forEach(size => {
  1196. if (this.formSizes[size.form] === undefined) {
  1197. this.formSizes[size.form] = size;
  1198. }
  1199. });
  1200. Object.values(views).forEach(view => {
  1201. if (this.formSizes[view.form] === undefined) {
  1202. this.formSizes[view.form] = { name: "Normal", height: view.attributes.height.base, default: true, form: view.form };
  1203. }
  1204. });
  1205. delete this.init;
  1206. return this;
  1207. }
  1208. }.init();
  1209. return entityTemplate;
  1210. }
  1211. function combineInfo(existing, next) {
  1212. switch (next.mode) {
  1213. case "replace":
  1214. return next.text;
  1215. case "prepend":
  1216. return next.text + existing;
  1217. case "append":
  1218. return existing + next.text;
  1219. }
  1220. return existing;
  1221. }
  1222. function clickDown(target, x, y) {
  1223. clicked = target;
  1224. movingInBounds = false;
  1225. const rect = target.getBoundingClientRect();
  1226. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  1227. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  1228. dragOffsetX = x - rect.left + entX;
  1229. dragOffsetY = y - rect.top + entY;
  1230. x = x - dragOffsetX;
  1231. y = y - dragOffsetY;
  1232. if (x >= 0 && x <= canvasWidth && y >= 0 && y <= canvasHeight) {
  1233. movingInBounds = true;
  1234. }
  1235. clickTimeout = setTimeout(() => { dragging = true }, 200)
  1236. target.classList.add("no-transition");
  1237. }
  1238. // could we make this actually detect the menu area?
  1239. function hoveringInDeleteArea(e) {
  1240. return e.clientY < document.querySelector("#menubar").clientHeight;
  1241. }
  1242. function clickUp(e) {
  1243. if (e.which != 1) {
  1244. return;
  1245. }
  1246. clearTimeout(clickTimeout);
  1247. if (clicked) {
  1248. clicked.classList.remove("no-transition");
  1249. if (dragging) {
  1250. dragging = false;
  1251. if (hoveringInDeleteArea(e)) {
  1252. removeEntity(clicked);
  1253. document.querySelector("#menubar").classList.remove("hover-delete");
  1254. }
  1255. } else {
  1256. select(clicked);
  1257. }
  1258. clicked = null;
  1259. }
  1260. }
  1261. function deselect(e) {
  1262. if (rulerMode) {
  1263. return;
  1264. }
  1265. if (e !== undefined && e.which != 1) {
  1266. return;
  1267. }
  1268. if (selected) {
  1269. selected.classList.remove("selected");
  1270. }
  1271. if (prevSelected) {
  1272. prevSelected.classList.remove("prevSelected");
  1273. }
  1274. document.getElementById("options-selected-entity-none").selected = "selected";
  1275. document.getElementById("delete-entity").style.display = "none";
  1276. clearAttribution();
  1277. selected = null;
  1278. clearViewList();
  1279. clearEntityOptions();
  1280. clearViewOptions();
  1281. document.querySelector("#delete-entity").disabled = true;
  1282. document.querySelector("#grow").disabled = true;
  1283. document.querySelector("#shrink").disabled = true;
  1284. document.querySelector("#fit").disabled = true;
  1285. }
  1286. function select(target) {
  1287. if (prevSelected !== null) {
  1288. prevSelected.classList.remove("prevSelected");
  1289. }
  1290. prevSelected = selected;
  1291. prevSelectedEntity = selectedEntity;
  1292. deselect();
  1293. selected = target;
  1294. selectedEntity = entities[target.dataset.key];
  1295. updateInfo();
  1296. document.getElementById("options-selected-entity-" + target.dataset.key).selected = "selected";
  1297. if (prevSelected !== null && config.showRatios && selected !== prevSelected) {
  1298. prevSelected.classList.add("prevSelected");
  1299. }
  1300. selected.classList.add("selected");
  1301. displayAttribution(selectedEntity.views[selectedEntity.view].image.source);
  1302. configFormList(selectedEntity, selectedEntity.form);
  1303. configViewList(selectedEntity, selectedEntity.view);
  1304. configEntityOptions(selectedEntity, selectedEntity.view);
  1305. configViewOptions(selectedEntity, selectedEntity.view);
  1306. document.querySelector("#delete-entity").disabled = false;
  1307. document.querySelector("#grow").disabled = false;
  1308. document.querySelector("#shrink").disabled = false;
  1309. document.querySelector("#fit").disabled = false;
  1310. }
  1311. function configFormList(entity, selectedForm) {
  1312. const label = document.querySelector("#options-label-form");
  1313. const list = document.querySelector("#entity-form");
  1314. list.innerHTML = "";
  1315. if (selectedForm === undefined) {
  1316. label.style.display = "none";
  1317. list.style.display = "none";
  1318. return;
  1319. }
  1320. label.style.display = "block";
  1321. list.style.display = "block";
  1322. Object.keys(entity.forms).forEach(form => {
  1323. const option = document.createElement("option");
  1324. option.innerText = entity.forms[form].name;
  1325. option.value = form;
  1326. if (form === selectedForm) {
  1327. option.selected = true;
  1328. }
  1329. list.appendChild(option);
  1330. });
  1331. }
  1332. function configViewList(entity, selectedView) {
  1333. const list = document.querySelector("#entity-view");
  1334. list.innerHTML = "";
  1335. list.style.display = "block";
  1336. Object.keys(entity.views).forEach(view => {
  1337. if (Object.keys(entity.forms).length > 0) {
  1338. if (entity.views[view].form !== entity.form) {
  1339. return;
  1340. }
  1341. }
  1342. const option = document.createElement("option");
  1343. option.innerText = entity.views[view].name;
  1344. option.value = view;
  1345. if (isNsfw(entity.views[view].image.source)) {
  1346. option.classList.add("nsfw")
  1347. }
  1348. if (view === selectedView) {
  1349. option.selected = true;
  1350. if (option.classList.contains("nsfw")) {
  1351. list.classList.add("nsfw");
  1352. } else {
  1353. list.classList.remove("nsfw");
  1354. }
  1355. }
  1356. list.appendChild(option);
  1357. });
  1358. }
  1359. function clearViewList() {
  1360. const list = document.querySelector("#entity-view");
  1361. list.innerHTML = "";
  1362. list.style.display = "none";
  1363. }
  1364. function updateWorldOptions(entity, view) {
  1365. const heightInput = document.querySelector("#options-height-value");
  1366. const heightSelect = document.querySelector("#options-height-unit");
  1367. const converted = config.height.toNumber(heightSelect.value);
  1368. setNumericInput(heightInput, converted);
  1369. }
  1370. function configEntityOptions(entity, view) {
  1371. const holder = document.querySelector("#options-entity");
  1372. document.querySelector("#entity-category-header").style.display = "block";
  1373. document.querySelector("#entity-category").style.display = "block";
  1374. holder.innerHTML = "";
  1375. const scaleLabel = document.createElement("div");
  1376. scaleLabel.classList.add("options-label");
  1377. scaleLabel.innerText = "Scale";
  1378. const scaleRow = document.createElement("div");
  1379. scaleRow.classList.add("options-row");
  1380. const scaleInput = document.createElement("input");
  1381. scaleInput.classList.add("options-field-numeric");
  1382. scaleInput.id = "options-entity-scale";
  1383. scaleInput.addEventListener("change", e => {
  1384. try {
  1385. const newScale = e.target.value == 0 ? 1 : math.evaluate(e.target.value);
  1386. if (typeof(newScale) !== "number") {
  1387. toast("Invalid input: scale can't have any units!")
  1388. return;
  1389. }
  1390. entity.scale = newScale;
  1391. } catch {
  1392. toast("Invalid input: could not parse " + e.target.value)
  1393. }
  1394. entity.dirty = true;
  1395. if (config.autoFit) {
  1396. fitWorld();
  1397. } else {
  1398. updateSizes(true);
  1399. }
  1400. updateEntityOptions(entity, entity.view);
  1401. updateViewOptions(entity, entity.view);
  1402. });
  1403. scaleInput.addEventListener("keydown", e => {
  1404. e.stopPropagation();
  1405. })
  1406. setNumericInput(scaleInput, entity.scale);
  1407. scaleRow.appendChild(scaleInput);
  1408. holder.appendChild(scaleLabel);
  1409. holder.appendChild(scaleRow);
  1410. const nameLabel = document.createElement("div");
  1411. nameLabel.classList.add("options-label");
  1412. nameLabel.innerText = "Name";
  1413. const nameRow = document.createElement("div");
  1414. nameRow.classList.add("options-row");
  1415. const nameInput = document.createElement("input");
  1416. nameInput.classList.add("options-field-text");
  1417. nameInput.value = entity.name;
  1418. nameInput.addEventListener("input", e => {
  1419. entity.name = e.target.value;
  1420. entity.dirty = true;
  1421. updateSizes(true);
  1422. })
  1423. nameInput.addEventListener("keydown", e => {
  1424. e.stopPropagation();
  1425. })
  1426. nameRow.appendChild(nameInput);
  1427. holder.appendChild(nameLabel);
  1428. holder.appendChild(nameRow);
  1429. configSizeList(entity);
  1430. document.querySelector("#options-order-display").innerText = entity.priority;
  1431. document.querySelector("#options-brightness-display").innerText = entity.brightness;
  1432. document.querySelector("#options-ordering").style.display = "flex";
  1433. }
  1434. function configSizeList(entity) {
  1435. const defaultHolder = document.querySelector("#options-entity-defaults");
  1436. defaultHolder.innerHTML = "";
  1437. entity.sizes.forEach(defaultInfo => {
  1438. if (Object.keys(entity.forms).length > 0) {
  1439. if (defaultInfo.form !== entity.form) {
  1440. return;
  1441. }
  1442. }
  1443. const button = document.createElement("button");
  1444. button.classList.add("options-button");
  1445. button.innerText = defaultInfo.name;
  1446. button.addEventListener("click", e => {
  1447. if (Object.keys(entity.forms).length > 0) {
  1448. entity.views[entity.formViews[entity.form]].height = defaultInfo.height;
  1449. } else {
  1450. entity.views[entity.defaultView].height = defaultInfo.height;
  1451. }
  1452. entity.dirty = true;
  1453. updateEntityOptions(entity, entity.view);
  1454. updateViewOptions(entity, entity.view);
  1455. if (!checkFitWorld()) {
  1456. updateSizes(true);
  1457. }
  1458. if (config.autoFitSize) {
  1459. let targets = {};
  1460. targets[selected.dataset.key] = entities[selected.dataset.key];
  1461. fitEntities(targets);
  1462. }
  1463. });
  1464. defaultHolder.appendChild(button);
  1465. });
  1466. }
  1467. function updateEntityOptions(entity, view) {
  1468. const scaleInput = document.querySelector("#options-entity-scale");
  1469. setNumericInput(scaleInput, entity.scale);
  1470. document.querySelector("#options-order-display").innerText = entity.priority;
  1471. document.querySelector("#options-brightness-display").innerText = entity.brightness;
  1472. }
  1473. function clearEntityOptions() {
  1474. document.querySelector("#entity-category-header").style.display = "none";
  1475. document.querySelector("#entity-category").style.display = "none";
  1476. /*
  1477. const holder = document.querySelector("#options-entity");
  1478. holder.innerHTML = "";
  1479. document.querySelector("#options-entity-defaults").innerHTML = "";
  1480. document.querySelector("#options-ordering").style.display = "none";
  1481. document.querySelector("#options-ordering").style.display = "none";*/
  1482. }
  1483. function configViewOptions(entity, view) {
  1484. const holder = document.querySelector("#options-view");
  1485. document.querySelector("#view-category-header").style.display = "block";
  1486. document.querySelector("#view-category").style.display = "block";
  1487. holder.innerHTML = "";
  1488. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  1489. const label = document.createElement("div");
  1490. label.classList.add("options-label");
  1491. label.innerText = val.name;
  1492. holder.appendChild(label);
  1493. const row = document.createElement("div");
  1494. row.classList.add("options-row");
  1495. holder.appendChild(row);
  1496. const input = document.createElement("input");
  1497. input.classList.add("options-field-numeric");
  1498. input.id = "options-view-" + key + "-input";
  1499. const select = document.createElement("select");
  1500. select.classList.add("options-field-unit");
  1501. select.id = "options-view-" + key + "-select"
  1502. Object.entries(unitChoices[val.type]).forEach(([group, entries]) => {
  1503. const optGroup = document.createElement("optgroup");
  1504. optGroup.label = group;
  1505. select.appendChild(optGroup);
  1506. entries.forEach(entry => {
  1507. const option = document.createElement("option");
  1508. option.innerText = entry;
  1509. if (entry == defaultUnits[val.type][config.units]) {
  1510. option.selected = true;
  1511. }
  1512. select.appendChild(option);
  1513. })
  1514. });
  1515. input.addEventListener("change", e => {
  1516. const raw_value = input.value == 0 ? 1 : input.value;
  1517. let value
  1518. try {
  1519. value = math.evaluate(raw_value).toNumber(select.value);
  1520. } catch {
  1521. try {
  1522. value = math.evaluate(input.value)
  1523. if (typeof(value) !== "number") {
  1524. toast("Invalid input: " + value.format() + " can't convert to " + select.value)
  1525. value = undefined
  1526. }
  1527. } catch {
  1528. toast("Invalid input: could not parse: " + input.value)
  1529. value = undefined
  1530. }
  1531. }
  1532. if (value === undefined) {
  1533. return;
  1534. }
  1535. input.value = value
  1536. entity.views[view][key] = math.unit(value, select.value);
  1537. entity.dirty = true;
  1538. if (config.autoFit) {
  1539. fitWorld();
  1540. } else {
  1541. updateSizes(true);
  1542. }
  1543. updateEntityOptions(entity, view);
  1544. updateViewOptions(entity, view, key);
  1545. });
  1546. input.addEventListener("keydown", e => {
  1547. e.stopPropagation();
  1548. })
  1549. if (entity.currentView.units[key]) {
  1550. select.value = entity.currentView.units[key];
  1551. } else {
  1552. entity.currentView.units[key] = select.value;
  1553. }
  1554. select.dataset.oldUnit = select.value;
  1555. setNumericInput(input, entity.views[view][key].toNumber(select.value));
  1556. // TODO does this ever cause a change in the world?
  1557. select.addEventListener("input", e => {
  1558. const value = input.value == 0 ? 1 : input.value;
  1559. const oldUnit = select.dataset.oldUnit;
  1560. entity.views[entity.view][key] = math.unit(value, oldUnit).to(select.value);
  1561. entity.dirty = true;
  1562. setNumericInput(input, entity.views[entity.view][key].toNumber(select.value));
  1563. select.dataset.oldUnit = select.value;
  1564. entity.views[view].units[key] = select.value;
  1565. if (config.autoFit) {
  1566. fitWorld();
  1567. } else {
  1568. updateSizes(true);
  1569. }
  1570. updateEntityOptions(entity, view);
  1571. updateViewOptions(entity, view, key);
  1572. });
  1573. row.appendChild(input);
  1574. row.appendChild(select);
  1575. });
  1576. }
  1577. function updateViewOptions(entity, view, changed) {
  1578. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  1579. if (key != changed) {
  1580. const input = document.querySelector("#options-view-" + key + "-input");
  1581. const select = document.querySelector("#options-view-" + key + "-select");
  1582. const currentUnit = select.value;
  1583. const convertedAmount = entity.views[view][key].toNumber(currentUnit);
  1584. setNumericInput(input, convertedAmount);
  1585. }
  1586. });
  1587. }
  1588. function setNumericInput(input, value, round = 6) {
  1589. if (typeof value == "string") {
  1590. value = parseFloat(value)
  1591. }
  1592. input.value = value.toPrecision(round);
  1593. }
  1594. function getSortedEntities() {
  1595. return Object.keys(entities).sort((a, b) => {
  1596. const entA = entities[a];
  1597. const entB = entities[b];
  1598. const viewA = entA.view;
  1599. const viewB = entB.view;
  1600. const heightA = entA.views[viewA].height.to("meter").value;
  1601. const heightB = entB.views[viewB].height.to("meter").value;
  1602. return heightA - heightB;
  1603. });
  1604. }
  1605. function clearViewOptions() {
  1606. document.querySelector("#view-category-header").style.display = "none";
  1607. document.querySelector("#view-category").style.display = "none";
  1608. }
  1609. // this is a crime against humanity, and also stolen from
  1610. // stack overflow
  1611. // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
  1612. const testCanvas = document.createElement("canvas");
  1613. testCanvas.id = "test-canvas";
  1614. function rotate(point, angle) {
  1615. return [
  1616. point[0] * Math.cos(angle) - point[1] * Math.sin(angle),
  1617. point[0] * Math.sin(angle) + point[1] * Math.cos(angle)
  1618. ];
  1619. }
  1620. const testCtx = testCanvas.getContext("2d");
  1621. function testClick(event) {
  1622. testCtx.save();
  1623. const target = event.target;
  1624. if (rulerMode) {
  1625. return;
  1626. }
  1627. // Get click coordinates
  1628. let w = target.width;
  1629. let h = target.height;
  1630. let ratioW = 1, ratioH = 1;
  1631. // Limit the size of the canvas so that very large images don't cause problems)
  1632. if (w > 1000) {
  1633. ratioW = w / 1000;
  1634. w /= ratioW;
  1635. h /= ratioW;
  1636. }
  1637. if (h > 1000) {
  1638. ratioH = h / 1000;
  1639. w /= ratioH;
  1640. h /= ratioH;
  1641. }
  1642. // todo remove some of this unused stuff
  1643. const ratio = ratioW * ratioH;
  1644. const entity = entities[target.parentElement.dataset.key];
  1645. const angle = entity.rotation;
  1646. var x = event.clientX - target.getBoundingClientRect().x,
  1647. y = event.clientY - target.getBoundingClientRect().y,
  1648. alpha;
  1649. [xTarget,yTarget] = [x,y];
  1650. [actualW, actualH] = [target.getBoundingClientRect().width, target.getBoundingClientRect().height];
  1651. xTarget /= ratio;
  1652. yTarget /= ratio;
  1653. actualW /= ratio;
  1654. actualH /= ratio;
  1655. testCtx.canvas.width = actualW;
  1656. testCtx.canvas.height = actualH;
  1657. testCtx.save();
  1658. // dear future me: Sorry :(
  1659. testCtx.resetTransform();
  1660. testCtx.translate(actualW/2, actualH/2);
  1661. testCtx.rotate(angle);
  1662. testCtx.translate(-actualW/2, -actualH/2);
  1663. testCtx.drawImage(target, (actualW/2 - w/2), (actualH/2 - h/2), w, h);
  1664. testCtx.fillStyle = "red";
  1665. testCtx.fillRect(actualW/2,actualH/2,10,10);
  1666. testCtx.restore();
  1667. testCtx.fillStyle = "red";
  1668. alpha = testCtx.getImageData(xTarget, yTarget, 1, 1).data[3];
  1669. testCtx.fillRect(xTarget, yTarget, 3, 3);
  1670. // If the pixel is transparent,
  1671. // retrieve the element underneath and trigger its click event
  1672. if (alpha === 0) {
  1673. const oldDisplay = target.style.display;
  1674. target.style.display = "none";
  1675. const newTarget = document.elementFromPoint(event.clientX, event.clientY);
  1676. newTarget.dispatchEvent(new MouseEvent(event.type, {
  1677. "clientX": event.clientX,
  1678. "clientY": event.clientY
  1679. }));
  1680. target.style.display = oldDisplay;
  1681. } else {
  1682. clickDown(target.parentElement, event.clientX, event.clientY);
  1683. }
  1684. testCtx.restore();
  1685. }
  1686. function arrangeEntities(order) {
  1687. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  1688. let sum = 0;
  1689. order.forEach(key => {
  1690. const image = document.querySelector("#entity-" + key + " > .entity-image");
  1691. const meters = entities[key].views[entities[key].view].height.toNumber("meters");
  1692. let height = image.height;
  1693. let width = image.width;
  1694. if (height == 0) {
  1695. height = 100;
  1696. }
  1697. if (width == 0) {
  1698. width = height;
  1699. }
  1700. sum += meters * width / height;
  1701. });
  1702. let x = config.x - sum / 2;
  1703. order.forEach(key => {
  1704. const image = document.querySelector("#entity-" + key + " > .entity-image");
  1705. const meters = entities[key].views[entities[key].view].height.toNumber("meters");
  1706. let height = image.height;
  1707. let width = image.width;
  1708. if (height == 0) {
  1709. height = 100;
  1710. }
  1711. if (width == 0) {
  1712. width = height;
  1713. }
  1714. x += meters * width / height / 2;
  1715. document.querySelector("#entity-" + key).dataset.x = x;
  1716. document.querySelector("#entity-" + key).dataset.y = config.y;
  1717. x += meters * width / height / 2;
  1718. })
  1719. fitWorld();
  1720. updateSizes();
  1721. }
  1722. function removeAllEntities() {
  1723. Object.keys(entities).forEach(key => {
  1724. removeEntity(document.querySelector("#entity-" + key));
  1725. });
  1726. }
  1727. function clearAttribution() {
  1728. document.querySelector("#attribution-category-header").style.display = "none";
  1729. document.querySelector("#options-attribution").style.display = "none";
  1730. }
  1731. function displayAttribution(file) {
  1732. document.querySelector("#attribution-category-header").style.display = "block";
  1733. document.querySelector("#options-attribution").style.display = "inline";
  1734. const authors = authorsOfFull(file);
  1735. const owners = ownersOfFull(file);
  1736. const citations = citationsOf(file);
  1737. const source = sourceOf(file);
  1738. const authorHolder = document.querySelector("#options-attribution-authors");
  1739. const ownerHolder = document.querySelector("#options-attribution-owners");
  1740. const citationHolder = document.querySelector("#options-attribution-citations");
  1741. const sourceHolder = document.querySelector("#options-attribution-source");
  1742. if (authors === []) {
  1743. const div = document.createElement("div");
  1744. div.innerText = "Unknown";
  1745. authorHolder.innerHTML = "";
  1746. authorHolder.appendChild(div);
  1747. } else if (authors === undefined) {
  1748. const div = document.createElement("div");
  1749. div.innerText = "Not yet entered";
  1750. authorHolder.innerHTML = "";
  1751. authorHolder.appendChild(div);
  1752. } else {
  1753. authorHolder.innerHTML = "";
  1754. const list = document.createElement("ul");
  1755. authorHolder.appendChild(list);
  1756. authors.forEach(author => {
  1757. const authorEntry = document.createElement("li");
  1758. if (author.url) {
  1759. const link = document.createElement("a");
  1760. link.href = author.url;
  1761. link.innerText = author.name;
  1762. link.rel = "noreferrer no opener";
  1763. link.target = "_blank";
  1764. authorEntry.appendChild(link);
  1765. } else {
  1766. const div = document.createElement("div");
  1767. div.innerText = author.name;
  1768. authorEntry.appendChild(div);
  1769. }
  1770. list.appendChild(authorEntry);
  1771. });
  1772. }
  1773. if (owners === []) {
  1774. const div = document.createElement("div");
  1775. div.innerText = "Unknown";
  1776. ownerHolder.innerHTML = "";
  1777. ownerHolder.appendChild(div);
  1778. } else if (owners === undefined) {
  1779. const div = document.createElement("div");
  1780. div.innerText = "Not yet entered";
  1781. ownerHolder.innerHTML = "";
  1782. ownerHolder.appendChild(div);
  1783. } else {
  1784. ownerHolder.innerHTML = "";
  1785. const list = document.createElement("ul");
  1786. ownerHolder.appendChild(list);
  1787. owners.forEach(owner => {
  1788. const ownerEntry = document.createElement("li");
  1789. if (owner.url) {
  1790. const link = document.createElement("a");
  1791. link.href = owner.url;
  1792. link.innerText = owner.name;
  1793. link.rel = "noreferrer no opener";
  1794. link.target = "_blank";
  1795. ownerEntry.appendChild(link);
  1796. } else {
  1797. const div = document.createElement("div");
  1798. div.innerText = owner.name;
  1799. ownerEntry.appendChild(div);
  1800. }
  1801. list.appendChild(ownerEntry);
  1802. });
  1803. }
  1804. if (citations === [] || citations === undefined) {
  1805. } else {
  1806. citationHolder.innerHTML = "";
  1807. const list = document.createElement("ul");
  1808. citationHolder.appendChild(list);
  1809. citations.forEach(citation => {
  1810. const citationEntry = document.createElement("li");
  1811. const link = document.createElement("a");
  1812. link.style.display = "block";
  1813. link.href = citation;
  1814. link.innerText = new URL(citation).host;
  1815. link.rel = "noreferrer no opener";
  1816. link.target = "_blank";
  1817. citationEntry.appendChild(link);
  1818. list.appendChild(citationEntry);
  1819. })
  1820. }
  1821. if (source === null) {
  1822. const div = document.createElement("div");
  1823. div.innerText = "No link";
  1824. sourceHolder.innerHTML = "";
  1825. sourceHolder.appendChild(div);
  1826. } else if (source === undefined) {
  1827. const div = document.createElement("div");
  1828. div.innerText = "Not yet entered";
  1829. sourceHolder.innerHTML = "";
  1830. sourceHolder.appendChild(div);
  1831. } else {
  1832. sourceHolder.innerHTML = "";
  1833. const link = document.createElement("a");
  1834. link.style.display = "block";
  1835. link.href = source;
  1836. link.innerText = new URL(source).host;
  1837. link.rel = "noreferrer no opener";
  1838. link.target = "_blank";
  1839. sourceHolder.appendChild(link);
  1840. }
  1841. }
  1842. function removeEntity(element) {
  1843. if (selected == element) {
  1844. deselect();
  1845. }
  1846. if (clicked == element) {
  1847. clicked = null;
  1848. }
  1849. const option = document.querySelector("#options-selected-entity-" + element.dataset.key);
  1850. option.parentElement.removeChild(option);
  1851. delete entities[element.dataset.key];
  1852. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  1853. const topName = document.querySelector("#top-name-" + element.dataset.key);
  1854. bottomName.parentElement.removeChild(bottomName);
  1855. topName.parentElement.removeChild(topName);
  1856. element.parentElement.removeChild(element);
  1857. selectedEntity = null;
  1858. prevSelectedEntity = null;
  1859. updateInfo();
  1860. }
  1861. function checkEntity(entity) {
  1862. Object.values(entity.views).forEach(view => {
  1863. if (authorsOf(view.image.source) === undefined) {
  1864. console.warn("No authors: " + view.image.source);
  1865. }
  1866. });
  1867. }
  1868. function preloadViews(entity) {
  1869. Object.values(entity.views).forEach(view => {
  1870. if (Object.keys(entity.forms).length > 0) {
  1871. if (entity.form !== view.form){
  1872. return;
  1873. }
  1874. }
  1875. if (!preloaded.has(view.image.source)) {
  1876. let img = new Image();
  1877. img.src = view.image.source;
  1878. preloaded.add(view.image.source);
  1879. }
  1880. });
  1881. }
  1882. function displayEntity(entity, view, x, y, selectEntity = false, refresh = false) {
  1883. checkEntity(entity);
  1884. // preload all of the entity's views
  1885. preloadViews(entity);
  1886. const box = document.createElement("div");
  1887. box.classList.add("entity-box");
  1888. const img = document.createElement("img");
  1889. img.classList.add("entity-image");
  1890. img.addEventListener("dragstart", e => {
  1891. e.preventDefault();
  1892. });
  1893. const nameTag = document.createElement("div");
  1894. nameTag.classList.add("entity-name");
  1895. nameTag.innerText = entity.name;
  1896. box.appendChild(img);
  1897. box.appendChild(nameTag);
  1898. const image = entity.views[view].image;
  1899. img.src = image.source;
  1900. if (image.bottom !== undefined) {
  1901. img.style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  1902. } else {
  1903. img.style.setProperty("--offset", ((-1) * 100) + "%")
  1904. }
  1905. img.style.setProperty("--rotation", (entity.rotation * 180 / Math.PI) + "deg")
  1906. box.dataset.x = x;
  1907. box.dataset.y = y;
  1908. img.addEventListener("mousedown", e => { if (e.which == 1) { testClick(e); if (clicked) { e.stopPropagation() } } });
  1909. img.addEventListener("touchstart", e => {
  1910. const fakeEvent = {
  1911. target: e.target,
  1912. clientX: e.touches[0].clientX,
  1913. clientY: e.touches[0].clientY,
  1914. which: 1
  1915. };
  1916. testClick(fakeEvent);
  1917. if (clicked) { e.stopPropagation() }
  1918. });
  1919. const heightBar = document.createElement("div");
  1920. heightBar.classList.add("height-bar");
  1921. box.appendChild(heightBar);
  1922. box.id = "entity-" + entityIndex;
  1923. box.dataset.key = entityIndex;
  1924. entity.view = view;
  1925. if (entity.priority === undefined)
  1926. entity.priority = 0;
  1927. if (entity.brightness === undefined)
  1928. entity.brightness = 1;
  1929. entities[entityIndex] = entity;
  1930. entity.index = entityIndex;
  1931. const world = document.querySelector("#entities");
  1932. world.appendChild(box);
  1933. const bottomName = document.createElement("div");
  1934. bottomName.classList.add("bottom-name");
  1935. bottomName.id = "bottom-name-" + entityIndex;
  1936. bottomName.innerText = entity.name;
  1937. bottomName.addEventListener("click", () => select(box));
  1938. world.appendChild(bottomName);
  1939. const topName = document.createElement("div");
  1940. topName.classList.add("top-name");
  1941. topName.id = "top-name-" + entityIndex;
  1942. topName.innerText = entity.name;
  1943. topName.addEventListener("click", () => select(box));
  1944. world.appendChild(topName);
  1945. const entityOption = document.createElement("option");
  1946. entityOption.id = "options-selected-entity-" + entityIndex;
  1947. entityOption.value = entityIndex;
  1948. entityOption.innerText = entity.name;
  1949. document.getElementById("options-selected-entity").appendChild(entityOption);
  1950. entityIndex += 1;
  1951. if (config.autoFit) {
  1952. fitWorld();
  1953. }
  1954. if (selectEntity)
  1955. select(box);
  1956. entity.dirty = true;
  1957. if (refresh && config.autoFitAdd) {
  1958. let targets = {};
  1959. targets[entityIndex - 1] = entity;
  1960. fitEntities(targets);
  1961. }
  1962. if (refresh)
  1963. updateSizes(true);
  1964. }
  1965. window.onblur = function () {
  1966. altHeld = false;
  1967. shiftHeld = false;
  1968. }
  1969. window.onfocus = function () {
  1970. window.dispatchEvent(new Event("keydown"));
  1971. }
  1972. // thanks to https://developers.google.com/web/fundamentals/native-hardware/fullscreen
  1973. function toggleFullScreen() {
  1974. var doc = window.document;
  1975. var docEl = doc.documentElement;
  1976. var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  1977. var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
  1978. if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
  1979. requestFullScreen.call(docEl);
  1980. }
  1981. else {
  1982. cancelFullScreen.call(doc);
  1983. }
  1984. }
  1985. function handleResize() {
  1986. const oldCanvasWidth = canvasWidth;
  1987. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  1988. canvasWidth = document.querySelector("#display").clientWidth - 100;
  1989. canvasHeight = document.querySelector("#display").clientHeight - 50;
  1990. const change = oldCanvasWidth / canvasWidth;
  1991. updateSizes();
  1992. }
  1993. function prepareSidebar() {
  1994. const menubar = document.querySelector("#sidebar-menu");
  1995. [
  1996. {
  1997. name: "Show/hide sidebar",
  1998. id: "menu-toggle-sidebar",
  1999. icon: "fas fa-chevron-circle-down",
  2000. rotates: true
  2001. },
  2002. {
  2003. name: "Fullscreen",
  2004. id: "menu-fullscreen",
  2005. icon: "fas fa-compress"
  2006. },
  2007. {
  2008. name: "Clear",
  2009. id: "menu-clear",
  2010. icon: "fas fa-file"
  2011. },
  2012. {
  2013. name: "Sort by height",
  2014. id: "menu-order-height",
  2015. icon: "fas fa-sort-numeric-up"
  2016. },
  2017. {
  2018. name: "Permalink",
  2019. id: "menu-permalink",
  2020. icon: "fas fa-link"
  2021. },
  2022. {
  2023. name: "Export to clipboard",
  2024. id: "menu-export",
  2025. icon: "fas fa-share"
  2026. },
  2027. {
  2028. name: "Import from clipboard",
  2029. id: "menu-import",
  2030. icon: "fas fa-share",
  2031. classes: ["flipped"]
  2032. },
  2033. {
  2034. name: "Save Scene",
  2035. id: "menu-save",
  2036. icon: "fas fa-download",
  2037. input: true
  2038. },
  2039. {
  2040. name: "Load Scene",
  2041. id: "menu-load",
  2042. icon: "fas fa-upload",
  2043. select: true
  2044. },
  2045. {
  2046. name: "Delete Scene",
  2047. id: "menu-delete",
  2048. icon: "fas fa-trash",
  2049. select: true
  2050. },
  2051. {
  2052. name: "Load Autosave",
  2053. id: "menu-load-autosave",
  2054. icon: "fas fa-redo"
  2055. },
  2056. {
  2057. name: "Load Preset",
  2058. id: "menu-preset",
  2059. icon: "fas fa-play",
  2060. select: true
  2061. },
  2062. {
  2063. name: "Add Image",
  2064. id: "menu-add-image",
  2065. icon: "fas fa-camera"
  2066. },
  2067. {
  2068. name: "Clear Rulers",
  2069. id: "menu-clear-rulers",
  2070. icon: "fas fa-ruler"
  2071. }
  2072. ].forEach(entry => {
  2073. const buttonHolder = document.createElement("div");
  2074. buttonHolder.classList.add("menu-button-holder");
  2075. const button = document.createElement("button");
  2076. button.id = entry.id;
  2077. button.classList.add("menu-button");
  2078. const icon = document.createElement("i");
  2079. icon.classList.add(...entry.icon.split(" "));
  2080. if (entry.rotates) {
  2081. icon.classList.add("rotate-backward", "transitions");
  2082. }
  2083. if (entry.classes) {
  2084. entry.classes.forEach(cls => icon.classList.add(cls));
  2085. }
  2086. const actionText = document.createElement("span");
  2087. actionText.innerText = entry.name;
  2088. actionText.classList.add("menu-text");
  2089. const srText = document.createElement("span");
  2090. srText.classList.add("sr-only");
  2091. srText.innerText = entry.name;
  2092. button.appendChild(icon);
  2093. button.appendChild(srText);
  2094. buttonHolder.appendChild(button);
  2095. buttonHolder.appendChild(actionText);
  2096. if (entry.input) {
  2097. const input = document.createElement("input");
  2098. buttonHolder.appendChild(input);
  2099. input.placeholder = "default";
  2100. input.addEventListener("keyup", e => {
  2101. if (e.key === "Enter") {
  2102. const name = document.querySelector("#menu-save ~ input").value;
  2103. if (/\S/.test(name)) {
  2104. saveScene(name);
  2105. }
  2106. updateSaveInfo();
  2107. e.preventDefault();
  2108. }
  2109. })
  2110. }
  2111. if (entry.select) {
  2112. const select = document.createElement("select");
  2113. buttonHolder.appendChild(select);
  2114. }
  2115. menubar.appendChild(buttonHolder);
  2116. });
  2117. }
  2118. function checkBodyClass(cls) {
  2119. return document.body.classList.contains(cls);
  2120. }
  2121. function toggleBodyClass(cls, setting) {
  2122. if (setting) {
  2123. document.body.classList.add(cls);
  2124. } else {
  2125. document.body.classList.remove(cls);
  2126. }
  2127. }
  2128. const backgroundColors = {
  2129. "none": "#00000000",
  2130. "black": "#000",
  2131. "dark": "#111",
  2132. "medium": "#333",
  2133. "light": "#555"
  2134. }
  2135. const settingsData = {
  2136. "show-vertical-scale": {
  2137. name: "Vertical Scale",
  2138. desc: "Draw vertical scale marks",
  2139. type: "toggle",
  2140. default: true,
  2141. get value() {
  2142. return config.drawYAxis;
  2143. },
  2144. set value(param) {
  2145. config.drawYAxis = param;
  2146. drawScales(false);
  2147. }
  2148. },
  2149. "show-horizontal-scale": {
  2150. name: "Horiziontal Scale",
  2151. desc: "Draw horizontal scale marks",
  2152. type: "toggle",
  2153. default: false,
  2154. get value() {
  2155. return config.drawXAxis;
  2156. },
  2157. set value(param) {
  2158. config.drawXAxis = param;
  2159. drawScales(false);
  2160. }
  2161. },
  2162. "show-altitudes": {
  2163. name: "Altitudes",
  2164. desc: "Draw interesting altitudes",
  2165. type: "select",
  2166. default: "none",
  2167. disabled: "none",
  2168. options: [
  2169. "none",
  2170. "all",
  2171. "atmosphere",
  2172. "orbits",
  2173. "weather",
  2174. "water",
  2175. "geology",
  2176. "thicknesses",
  2177. "airspaces",
  2178. "races",
  2179. "olympic-records",
  2180. ],
  2181. get value() {
  2182. return config.drawAltitudes;
  2183. },
  2184. set value(param) {
  2185. config.drawAltitudes = param;
  2186. drawScales(false);
  2187. }
  2188. },
  2189. "lock-y-axis": {
  2190. name: "Lock Y-Axis",
  2191. desc: "Keep the camera at ground-level",
  2192. type: "toggle",
  2193. default: true,
  2194. get value() {
  2195. return config.lockYAxis;
  2196. },
  2197. set value(param) {
  2198. config.lockYAxis = param;
  2199. if (param) {
  2200. config.y = 0;
  2201. updateSizes();
  2202. document.querySelector("#scroll-up").disabled = true;
  2203. document.querySelector("#scroll-down").disabled = true;
  2204. } else {
  2205. document.querySelector("#scroll-up").disabled = false;
  2206. document.querySelector("#scroll-down").disabled = false;
  2207. }
  2208. }
  2209. },
  2210. "axis-spacing": {
  2211. name: "Axis Spacing",
  2212. desc: "How frequent the axis lines are",
  2213. type: "select",
  2214. default: "standard",
  2215. options: [
  2216. "dense",
  2217. "standard",
  2218. "sparse"
  2219. ],
  2220. get value() {
  2221. return config.axisSpacing;
  2222. },
  2223. set value(param) {
  2224. config.axisSpacing = param;
  2225. const factor = {
  2226. "dense": 0.5,
  2227. "standard": 1,
  2228. "sparse": 2
  2229. }[param];
  2230. config.minLineSize = factor * 100;
  2231. config.maxLineSize = factor * 150;
  2232. updateSizes();
  2233. }
  2234. },
  2235. "ground-type": {
  2236. name: "Ground",
  2237. desc: "What kind of ground to show, if any",
  2238. type: "select",
  2239. default: "black",
  2240. disabled: "none",
  2241. options: [
  2242. "none",
  2243. "black",
  2244. "dark",
  2245. "medium",
  2246. "light",
  2247. ],
  2248. get value() {
  2249. return config.groundKind;
  2250. },
  2251. set value(param) {
  2252. config.groundKind = param;
  2253. document.querySelector("#ground").style.setProperty("--ground-color", backgroundColors[param])
  2254. }
  2255. },
  2256. "ground-pos": {
  2257. name: "Ground Position",
  2258. desc: "How high the ground is if the y-axis is locked",
  2259. type: "select",
  2260. default: "bottom",
  2261. options: [
  2262. "very-high",
  2263. "high",
  2264. "medium",
  2265. "low",
  2266. "very-low",
  2267. "bottom",
  2268. ],
  2269. get value() {
  2270. return config.groundPos;
  2271. },
  2272. set value(param) {
  2273. config.groundPos = param;
  2274. updateSizes();
  2275. }
  2276. },
  2277. "background-brightness": {
  2278. name: "Background Color",
  2279. desc: "How bright the background is",
  2280. type: "select",
  2281. default: "medium",
  2282. options: [
  2283. "black",
  2284. "dark",
  2285. "medium",
  2286. "light",
  2287. ],
  2288. get value() {
  2289. return config.background;
  2290. },
  2291. set value(param) {
  2292. config.background = param;
  2293. drawScales();
  2294. }
  2295. },
  2296. "auto-scale": {
  2297. name: "Auto-Size World",
  2298. desc: "Constantly zoom to fit the largest entity",
  2299. type: "toggle",
  2300. default: false,
  2301. get value() {
  2302. return config.autoFit;
  2303. },
  2304. set value(param) {
  2305. config.autoFit = param;
  2306. checkFitWorld();
  2307. }
  2308. },
  2309. "auto-units": {
  2310. name: "Auto-Select Units",
  2311. desc: "Automatically switch units when zooming in and out",
  2312. type: "toggle",
  2313. default: false,
  2314. get value() {
  2315. return config.autoUnits;
  2316. },
  2317. set value(param) {
  2318. config.autoUnits = param;
  2319. }
  2320. },
  2321. "zoom-when-adding": {
  2322. name: "Zoom On Add",
  2323. desc: "Zoom to fit when you add a new entity",
  2324. type: "toggle",
  2325. default: true,
  2326. get value() {
  2327. return config.autoFitAdd;
  2328. },
  2329. set value(param) {
  2330. config.autoFitAdd = param;
  2331. }
  2332. },
  2333. "zoom-when-sizing": {
  2334. name: "Zoom On Size",
  2335. desc: "Zoom to fit when you select an entity's size",
  2336. type: "toggle",
  2337. default: false,
  2338. get value() {
  2339. return config.autoFitSize;
  2340. },
  2341. set value(param) {
  2342. config.autoFitSize = param;
  2343. }
  2344. },
  2345. "show-ratios": {
  2346. name: "Show Ratios",
  2347. desc: "Show the proportions between the current selection and the most recent selection.",
  2348. type: "toggle",
  2349. default: false,
  2350. get value() {
  2351. return config.showRatios;
  2352. },
  2353. set value(param) {
  2354. config.showRatios = param;
  2355. updateInfo();
  2356. }
  2357. },
  2358. "show-horizon": {
  2359. name: "Show Horizon",
  2360. desc: "Show how far the horizon would be for the selected character",
  2361. type: "toggle",
  2362. default: false,
  2363. get value() {
  2364. return config.showHorizon;
  2365. },
  2366. set value(param) {
  2367. config.showHorizon = param;
  2368. updateInfo();
  2369. }
  2370. },
  2371. "attach-rulers": {
  2372. name: "Attach Rulers",
  2373. desc: "Rulers will attach to the currently-selected entity, moving around with it.",
  2374. type: "toggle",
  2375. default: true,
  2376. get value() {
  2377. return config.rulersStick;
  2378. },
  2379. set value(param) {
  2380. config.rulersStick = param;
  2381. }
  2382. },
  2383. "units": {
  2384. name: "Default Units",
  2385. desc: "Which kind of unit to use by default",
  2386. type: "select",
  2387. default: "metric",
  2388. options: [
  2389. "metric",
  2390. "customary",
  2391. "relative",
  2392. "quirky"
  2393. ],
  2394. get value() {
  2395. return config.units;
  2396. },
  2397. set value(param) {
  2398. config.units = param;
  2399. updateSizes();
  2400. }
  2401. },
  2402. "names": {
  2403. name: "Show Names",
  2404. desc: "Display names over entities",
  2405. type: "toggle",
  2406. default: true,
  2407. get value() {
  2408. return checkBodyClass("toggle-entity-name");
  2409. },
  2410. set value(param) {
  2411. toggleBodyClass("toggle-entity-name", param);
  2412. }
  2413. },
  2414. "bottom-names": {
  2415. name: "Bottom Names",
  2416. desc: "Display names at the bottom",
  2417. type: "toggle",
  2418. default: false,
  2419. get value() {
  2420. return checkBodyClass("toggle-bottom-name");
  2421. },
  2422. set value(param) {
  2423. toggleBodyClass("toggle-bottom-name", param);
  2424. }
  2425. },
  2426. "top-names": {
  2427. name: "Show Arrows",
  2428. desc: "Point to entities that are much larger than the current view",
  2429. type: "toggle",
  2430. default: false,
  2431. get value() {
  2432. return checkBodyClass("toggle-top-name");
  2433. },
  2434. set value(param) {
  2435. toggleBodyClass("toggle-top-name", param);
  2436. }
  2437. },
  2438. "height-bars": {
  2439. name: "Height Bars",
  2440. desc: "Draw dashed lines to the top of each entity",
  2441. type: "toggle",
  2442. default: false,
  2443. get value() {
  2444. return checkBodyClass("toggle-height-bars");
  2445. },
  2446. set value(param) {
  2447. toggleBodyClass("toggle-height-bars", param);
  2448. }
  2449. },
  2450. "flag-nsfw": {
  2451. name: "Flag NSFW",
  2452. desc: "Highlight NSFW things in red",
  2453. type: "toggle",
  2454. default: false,
  2455. get value() {
  2456. return checkBodyClass("flag-nsfw");
  2457. },
  2458. set value(param) {
  2459. toggleBodyClass("flag-nsfw", param);
  2460. }
  2461. },
  2462. "glowing-entities": {
  2463. name: "Glowing Edges",
  2464. desc: "Makes all entities glow",
  2465. type: "toggle",
  2466. default: false,
  2467. get value() {
  2468. return checkBodyClass("toggle-entity-glow");
  2469. },
  2470. set value(param) {
  2471. toggleBodyClass("toggle-entity-glow", param);
  2472. }
  2473. },
  2474. "smoothing": {
  2475. name: "Smoothing",
  2476. desc: "Smooth out movements and size changes. Disable for better performance.",
  2477. type: "toggle",
  2478. default: true,
  2479. get value() {
  2480. return checkBodyClass("smoothing");
  2481. },
  2482. set value(param) {
  2483. toggleBodyClass("smoothing", param);
  2484. }
  2485. },
  2486. "auto-mass": {
  2487. name: "Estimate Mass",
  2488. desc: "Guess the mass of things that don't have one specified using the selected body type",
  2489. type: "select",
  2490. default: "off",
  2491. disabled: "off",
  2492. options: [
  2493. "off",
  2494. "human",
  2495. "quadruped at shoulder",
  2496. ],
  2497. get value() {
  2498. return config.autoMass
  2499. },
  2500. set value(param) {
  2501. config.autoMass = param
  2502. }
  2503. },
  2504. "auto-food-intake": {
  2505. name: "Estimate Food Intake",
  2506. desc: "Guess how much food creatures need, based on their mass -- 2000kcal per 150lbs",
  2507. type: "toggle",
  2508. default: false,
  2509. get value() {
  2510. return config.autoFoodIntake
  2511. },
  2512. set value(param) {
  2513. config.autoFoodIntake = param
  2514. }
  2515. },
  2516. "auto-caloric-value": {
  2517. name: "Estimate Caloric Value",
  2518. desc: "Guess how much food a creature is worth -- 860kcal per pound",
  2519. type: "toggle",
  2520. default: false,
  2521. get value() {
  2522. return config.autoCaloricValue
  2523. },
  2524. set value(param) {
  2525. config.autoCaloricValue = param
  2526. }
  2527. },
  2528. "auto-prey-capacity": {
  2529. name: "Estimate Prey Capacity",
  2530. desc: "Guess how much prey creatures can hold, based on their mass",
  2531. type: "select",
  2532. default: "none",
  2533. options: [
  2534. "none",
  2535. "realistic",
  2536. "same-size"
  2537. ],
  2538. get value() {
  2539. return config.autoPreyCapacity;
  2540. },
  2541. set value(param) {
  2542. config.autoPreyCapacity = param;
  2543. }
  2544. },
  2545. }
  2546. function prepareSettings(userSettings) {
  2547. const menubar = document.querySelector("#settings-menu");
  2548. Object.entries(settingsData).forEach(([id, entry]) => {
  2549. const holder = document.createElement("label");
  2550. holder.classList.add("settings-holder");
  2551. const input = document.createElement("input");
  2552. input.id = "setting-" + id;
  2553. const vertical = document.createElement("div");
  2554. vertical.classList.add("settings-vertical");
  2555. const name = document.createElement("label");
  2556. name.innerText = entry.name;
  2557. name.classList.add("settings-name");
  2558. name.setAttribute("for", input.id);
  2559. const desc = document.createElement("label");
  2560. desc.innerText = entry.desc;
  2561. desc.classList.add("settings-desc");
  2562. desc.setAttribute("for", input.id);
  2563. if (entry.type == "toggle") {
  2564. input.type = "checkbox";
  2565. input.checked = userSettings[id] === undefined ? entry.default : userSettings[id];
  2566. holder.setAttribute("for", input.id);
  2567. vertical.appendChild(name);
  2568. vertical.appendChild(desc);
  2569. holder.appendChild(vertical);
  2570. holder.appendChild(input);
  2571. menubar.appendChild(holder);
  2572. const update = () => {
  2573. if (input.checked) {
  2574. holder.classList.add("enabled");
  2575. holder.classList.remove("disabled");
  2576. } else {
  2577. holder.classList.remove("enabled");
  2578. holder.classList.add("disabled");
  2579. }
  2580. entry.value = input.checked;
  2581. }
  2582. update();
  2583. input.addEventListener("change", update);
  2584. } else if (entry.type == "select") {
  2585. // we don't use the input element we made!
  2586. const select = document.createElement("select");
  2587. select.id = "setting-" + id;
  2588. entry.options.forEach(choice => {
  2589. const option = document.createElement("option");
  2590. option.innerText = choice;
  2591. select.appendChild(option);
  2592. })
  2593. select.value = userSettings[id] === undefined ? entry.default : userSettings[id];
  2594. vertical.appendChild(name);
  2595. vertical.appendChild(desc);
  2596. holder.appendChild(vertical);
  2597. holder.appendChild(select);
  2598. menubar.appendChild(holder);
  2599. const update = () => {
  2600. entry.value = select.value;
  2601. if (entry.disabled !== undefined && entry.value !== entry.disabled) {
  2602. holder.classList.add("enabled");
  2603. holder.classList.remove("disabled");
  2604. } else {
  2605. holder.classList.remove("enabled");
  2606. holder.classList.add("disabled");
  2607. }
  2608. }
  2609. update();
  2610. select.addEventListener("change", update);
  2611. }
  2612. })
  2613. }
  2614. function prepareMenu() {
  2615. prepareSidebar();
  2616. updateSaveInfo();
  2617. if (checkHelpDate()) {
  2618. document.querySelector("#open-help").classList.add("highlighted");
  2619. }
  2620. }
  2621. function updateSaveInfo() {
  2622. const saves = getSaves();
  2623. const load = document.querySelector("#menu-load ~ select");
  2624. load.innerHTML = "";
  2625. saves.forEach(save => {
  2626. const option = document.createElement("option");
  2627. option.innerText = save;
  2628. option.value = save;
  2629. load.appendChild(option);
  2630. });
  2631. const del = document.querySelector("#menu-delete ~ select");
  2632. del.innerHTML = "";
  2633. saves.forEach(save => {
  2634. const option = document.createElement("option");
  2635. option.innerText = save;
  2636. option.value = save;
  2637. del.appendChild(option);
  2638. });
  2639. }
  2640. function getSaves() {
  2641. try {
  2642. const results = [];
  2643. Object.keys(localStorage).forEach(key => {
  2644. if (key.startsWith("macrovision-save-")) {
  2645. results.push(key.replace("macrovision-save-", ""));
  2646. }
  2647. })
  2648. return results;
  2649. } catch (err) {
  2650. alert("Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error.")
  2651. console.error(err);
  2652. return false;
  2653. }
  2654. }
  2655. function getUserSettings() {
  2656. try {
  2657. const settings = JSON.parse(localStorage.getItem("settings"));
  2658. return settings === null ? {} : settings;
  2659. } catch {
  2660. return {};
  2661. }
  2662. }
  2663. function exportUserSettings() {
  2664. const settings = {};
  2665. Object.entries(settingsData).forEach(([id, entry]) => {
  2666. settings[id] = entry.value;
  2667. });
  2668. return settings;
  2669. }
  2670. function setUserSettings(settings) {
  2671. try {
  2672. localStorage.setItem("settings", JSON.stringify(settings));
  2673. } catch {
  2674. // :(
  2675. }
  2676. }
  2677. const lastHelpChange = 1601955834693;
  2678. function checkHelpDate() {
  2679. // disabling this for now
  2680. return false;
  2681. try {
  2682. const old = localStorage.getItem("help-viewed");
  2683. if (old === null || old < lastHelpChange) {
  2684. return true;
  2685. }
  2686. return false;
  2687. } catch {
  2688. console.warn("Could not set the help-viewed date");
  2689. return false;
  2690. }
  2691. }
  2692. function setHelpDate() {
  2693. try {
  2694. localStorage.setItem("help-viewed", Date.now());
  2695. } catch {
  2696. console.warn("Could not set the help-viewed date");
  2697. }
  2698. }
  2699. function doYScroll() {
  2700. const worldHeight = config.height.toNumber("meters");
  2701. config.y += scrollDirection * worldHeight / 180;
  2702. updateSizes();
  2703. scrollDirection *= 1.05;
  2704. }
  2705. function doXScroll() {
  2706. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  2707. config.x += scrollDirection * worldWidth / 180 ;
  2708. updateSizes();
  2709. scrollDirection *= 1.05;
  2710. }
  2711. function doZoom() {
  2712. const oldHeight = config.height;
  2713. setWorldHeight(oldHeight, math.multiply(oldHeight, 1 + zoomDirection / 10));
  2714. zoomDirection *= 1.05;
  2715. }
  2716. function doSize() {
  2717. if (selected) {
  2718. const entity = entities[selected.dataset.key];
  2719. const oldHeight = entity.views[entity.view].height;
  2720. entity.views[entity.view].height = math.multiply(oldHeight, sizeDirection < 0 ? -1/sizeDirection : sizeDirection);
  2721. entity.dirty = true;
  2722. updateEntityOptions(entity, entity.view);
  2723. updateViewOptions(entity, entity.view);
  2724. updateSizes(true);
  2725. sizeDirection *= 1.01;
  2726. const ownHeight = entity.views[entity.view].height.toNumber("meters");
  2727. let extra = entity.views[entity.view].image.extra;
  2728. extra = extra === undefined ? 1 : extra;
  2729. const worldHeight = config.height.toNumber("meters");
  2730. if (ownHeight * extra > worldHeight) {
  2731. setWorldHeight(config.height, math.multiply(entity.views[entity.view].height, extra));
  2732. } else if (ownHeight * extra * 10 < worldHeight) {
  2733. setWorldHeight(config.height, math.multiply(entity.views[entity.view].height, extra * 10));
  2734. }
  2735. }
  2736. }
  2737. function selectNewUnit() {
  2738. const unitSelector = document.querySelector("#options-height-unit");
  2739. checkFitWorld();
  2740. const scaleInput = document.querySelector("#options-height-value");
  2741. const newVal = math.unit(scaleInput.value, unitSelector.dataset.oldUnit).toNumber(unitSelector.value);
  2742. setNumericInput(scaleInput, newVal);
  2743. updateWorldHeight();
  2744. unitSelector.dataset.oldUnit = unitSelector.value;
  2745. }
  2746. // given a world position, return the position relative to the entity at normal scale
  2747. function entityRelativePosition(pos, entityElement) {
  2748. const entity = entities[entityElement.dataset.key]
  2749. const x = parseFloat(entityElement.dataset.x)
  2750. const y = parseFloat(entityElement.dataset.y)
  2751. pos.x -= x
  2752. pos.y -= y
  2753. pos.x /= entity.scale
  2754. pos.y /= entity.scale
  2755. return pos
  2756. }
  2757. document.addEventListener("DOMContentLoaded", () => {
  2758. prepareMenu();
  2759. prepareEntities();
  2760. document.querySelector("#open-help").addEventListener("click", e => {
  2761. setHelpDate();
  2762. document.querySelector("#open-help").classList.remove("highlighted");
  2763. window.open("https://www.notion.so/Macrovision-5c7f9377424743358ddf6db5671f439e", "_blank");
  2764. });
  2765. document.querySelector("#copy-screenshot").addEventListener("click", e => {
  2766. copyScreenshot();
  2767. });
  2768. document.querySelector("#save-screenshot").addEventListener("click", e => {
  2769. saveScreenshot();
  2770. });
  2771. document.querySelector("#open-screenshot").addEventListener("click", e => {
  2772. openScreenshot();
  2773. });
  2774. document.querySelector("#toggle-menu").addEventListener("click", e => {
  2775. const popoutMenu = document.querySelector("#sidebar-menu");
  2776. if (popoutMenu.classList.contains("visible")) {
  2777. popoutMenu.classList.remove("visible");
  2778. } else {
  2779. document.querySelectorAll(".popout-menu").forEach(menu => menu.classList.remove("visible"));
  2780. const rect = e.target.getBoundingClientRect();
  2781. popoutMenu.classList.add("visible");
  2782. popoutMenu.style.left = rect.x + rect.width + 10 + "px";
  2783. popoutMenu.style.top = rect.y + rect.height + 10 + "px";
  2784. let menuWidth = popoutMenu.getBoundingClientRect().width;
  2785. let screenWidth = window.innerWidth;
  2786. if (menuWidth * 1.5 > screenWidth) {
  2787. popoutMenu.style.left = 25 + "px";
  2788. }
  2789. }
  2790. e.stopPropagation();
  2791. });
  2792. document.querySelector("#sidebar-menu").addEventListener("click", e => {
  2793. e.stopPropagation();
  2794. });
  2795. document.addEventListener("click", e => {
  2796. document.querySelector("#sidebar-menu").classList.remove("visible");
  2797. });
  2798. document.querySelector("#toggle-settings").addEventListener("click", e => {
  2799. const popoutMenu = document.querySelector("#settings-menu");
  2800. if (popoutMenu.classList.contains("visible")) {
  2801. popoutMenu.classList.remove("visible");
  2802. } else {
  2803. document.querySelectorAll(".popout-menu").forEach(menu => menu.classList.remove("visible"));
  2804. const rect = e.target.getBoundingClientRect();
  2805. popoutMenu.classList.add("visible");
  2806. popoutMenu.style.left = rect.x + rect.width + 10 + "px";
  2807. popoutMenu.style.top = rect.y + rect.height + 10 + "px";
  2808. let menuWidth = popoutMenu.getBoundingClientRect().width;
  2809. let screenWidth = window.innerWidth;
  2810. if (menuWidth * 1.5 > screenWidth) {
  2811. popoutMenu.style.left = 25 + "px";
  2812. }
  2813. }
  2814. e.stopPropagation();
  2815. });
  2816. document.querySelector("#settings-menu").addEventListener("click", e => {
  2817. e.stopPropagation();
  2818. });
  2819. document.addEventListener("click", e => {
  2820. document.querySelector("#settings-menu").classList.remove("visible");
  2821. });
  2822. window.addEventListener("unload", () => {
  2823. saveScene("autosave");
  2824. setUserSettings(exportUserSettings());
  2825. });
  2826. document.querySelector("#options-selected-entity").addEventListener("input", e => {
  2827. if (e.target.value == "None") {
  2828. deselect()
  2829. } else {
  2830. select(document.querySelector("#entity-" + e.target.value));
  2831. }
  2832. });
  2833. document.querySelector("#menu-toggle-sidebar").addEventListener("click", e => {
  2834. const sidebar = document.querySelector("#options");
  2835. if (sidebar.classList.contains("hidden")) {
  2836. sidebar.classList.remove("hidden");
  2837. e.target.classList.remove("rotate-forward");
  2838. e.target.classList.add("rotate-backward");
  2839. } else {
  2840. sidebar.classList.add("hidden");
  2841. e.target.classList.add("rotate-forward");
  2842. e.target.classList.remove("rotate-backward");
  2843. }
  2844. handleResize();
  2845. });
  2846. document.querySelector("#menu-fullscreen").addEventListener("click", toggleFullScreen);
  2847. document.querySelector("#options-order-forward").addEventListener("click", e => {
  2848. if (selected) {
  2849. entities[selected.dataset.key].priority += 1;
  2850. }
  2851. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  2852. updateSizes();
  2853. });
  2854. document.querySelector("#options-order-back").addEventListener("click", e => {
  2855. if (selected) {
  2856. entities[selected.dataset.key].priority -= 1;
  2857. }
  2858. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  2859. updateSizes();
  2860. });
  2861. document.querySelector("#options-brightness-up").addEventListener("click", e => {
  2862. if (selected) {
  2863. entities[selected.dataset.key].brightness += 1;
  2864. }
  2865. document.querySelector("#options-brightness-display").innerText = entities[selected.dataset.key].brightness;
  2866. updateSizes();
  2867. });
  2868. document.querySelector("#options-brightness-down").addEventListener("click", e => {
  2869. if (selected) {
  2870. entities[selected.dataset.key].brightness -= 1;
  2871. }
  2872. document.querySelector("#options-brightness-display").innerText = entities[selected.dataset.key].brightness;
  2873. updateSizes();
  2874. });
  2875. document.querySelector("#options-rotate-left").addEventListener("click", e => {
  2876. if (selected) {
  2877. entities[selected.dataset.key].rotation -= Math.PI/4;
  2878. }
  2879. selected.querySelector("img").style.setProperty("--rotation", (entities[selected.dataset.key].rotation * 180 / Math.PI) + "deg")
  2880. updateSizes();
  2881. });
  2882. document.querySelector("#options-rotate-right").addEventListener("click", e => {
  2883. if (selected) {
  2884. entities[selected.dataset.key].rotation += Math.PI/4;
  2885. }
  2886. selected.querySelector("img").style.setProperty("--rotation", (entities[selected.dataset.key].rotation * 180 / Math.PI) + "deg")
  2887. updateSizes();
  2888. });
  2889. document.querySelector("#options-flip").addEventListener("click", e => {
  2890. if (selected) {
  2891. selected.querySelector(".entity-image").classList.toggle("flipped");
  2892. }
  2893. document.querySelector("#options-brightness-display").innerText = entities[selected.dataset.key].brightness;
  2894. updateSizes();
  2895. });
  2896. const sceneChoices = document.querySelector("#menu-preset ~ select");
  2897. Object.entries(scenes).forEach(([id, scene]) => {
  2898. const option = document.createElement("option");
  2899. option.innerText = id;
  2900. option.value = id;
  2901. sceneChoices.appendChild(option);
  2902. });
  2903. document.querySelector("#menu-preset").addEventListener("click", e => {
  2904. const chosen = sceneChoices.value;
  2905. removeAllEntities();
  2906. scenes[chosen]();
  2907. });
  2908. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  2909. canvasWidth = document.querySelector("#display").clientWidth - 100;
  2910. canvasHeight = document.querySelector("#display").clientHeight - 50;
  2911. document.querySelector("#options-height-value").addEventListener("change", e => {
  2912. updateWorldHeight();
  2913. })
  2914. document.querySelector("#options-height-value").addEventListener("keydown", e => {
  2915. e.stopPropagation();
  2916. })
  2917. const unitSelector = document.querySelector("#options-height-unit");
  2918. Object.entries(unitChoices.length).forEach(([group, entries]) => {
  2919. const optGroup = document.createElement("optgroup");
  2920. optGroup.label = group;
  2921. unitSelector.appendChild(optGroup);
  2922. entries.forEach(entry => {
  2923. const option = document.createElement("option");
  2924. option.innerText = entry;
  2925. // we haven't loaded user settings yet, so we can't choose the unit just yet
  2926. unitSelector.appendChild(option);
  2927. })
  2928. });
  2929. unitSelector.addEventListener("input", selectNewUnit);
  2930. param = window.location.hash;
  2931. // we now use the fragment for links, but we should still support old stuff:
  2932. if (param.length > 0) {
  2933. param = param.substring(1);
  2934. } else {
  2935. param = new URL(window.location.href).searchParams.get("scene");
  2936. }
  2937. document.querySelector("#world").addEventListener("mousedown", e => {
  2938. // only middle mouse clicks
  2939. if (e.which == 2) {
  2940. panning = true;
  2941. panOffsetX = e.clientX;
  2942. panOffsetY = e.clientY;
  2943. Object.keys(entities).forEach(key => {
  2944. document.querySelector("#entity-" + key).classList.add("no-transition");
  2945. });
  2946. }
  2947. });
  2948. document.addEventListener("mouseup", e => {
  2949. if (e.which == 2) {
  2950. panning = false;
  2951. Object.keys(entities).forEach(key => {
  2952. document.querySelector("#entity-" + key).classList.remove("no-transition");
  2953. });
  2954. }
  2955. });
  2956. document.querySelector("#world").addEventListener("touchstart", e => {
  2957. if (!rulerMode) {
  2958. panning = true;
  2959. panOffsetX = e.touches[0].clientX;
  2960. panOffsetY = e.touches[0].clientY;
  2961. e.preventDefault();
  2962. Object.keys(entities).forEach(key => {
  2963. document.querySelector("#entity-" + key).classList.add("no-transition");
  2964. });
  2965. }
  2966. });
  2967. document.querySelector("#world").addEventListener("touchend", e => {
  2968. panning = false;
  2969. Object.keys(entities).forEach(key => {
  2970. document.querySelector("#entity-" + key).classList.remove("no-transition");
  2971. });
  2972. });
  2973. document.querySelector("#world").addEventListener("mousedown", e => {
  2974. // only left mouse clicks
  2975. if (e.which == 1 && rulerMode) {
  2976. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  2977. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  2978. let pos = pix2pos({ x: e.clientX - entX, y: e.clientY - entY });
  2979. if (config.rulersStick && selected) {
  2980. pos = entityRelativePosition(pos, selected)
  2981. }
  2982. currentRuler = { x0: pos.x, y0: pos.y, x1: pos.y, y1: pos.y, entityKey: null };
  2983. if (config.rulersStick && selected) {
  2984. currentRuler.entityKey = selected.dataset.key
  2985. }
  2986. }
  2987. });
  2988. document.querySelector("#world").addEventListener("mouseup", e => {
  2989. // only left mouse clicks
  2990. if (e.which == 1 && currentRuler) {
  2991. rulers.push(currentRuler);
  2992. currentRuler = null;
  2993. rulerMode = false;
  2994. }
  2995. });
  2996. document.querySelector("#world").addEventListener("touchstart", e => {
  2997. if (rulerMode) {
  2998. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  2999. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  3000. let pos = pix2pos({ x: e.touches[0].clientX - entX, y: e.touches[0].clientY - entY });
  3001. if (config.rulersStick && selected) {
  3002. pos = entityRelativePosition(pos, selected)
  3003. }
  3004. currentRuler = { x0: pos.x, y0: pos.y, x1: pos.y, y1: pos.y, entityKey: null };
  3005. if (config.rulersStick && selected) {
  3006. currentRuler.entityKey = selected.dataset.key
  3007. }
  3008. }
  3009. });
  3010. document.querySelector("#world").addEventListener("touchend", e => {
  3011. if (currentRuler) {
  3012. rulers.push(currentRuler);
  3013. currentRuler = null;
  3014. rulerMode = false;
  3015. }
  3016. });
  3017. document.querySelector("body").appendChild(testCtx.canvas);
  3018. world.addEventListener("mousedown", e => deselect(e));
  3019. world.addEventListener("touchstart", e => deselect({
  3020. which: 1,
  3021. }));
  3022. document.querySelector("#entities").addEventListener("mousedown", deselect);
  3023. document.querySelector("#display").addEventListener("mousedown", deselect);
  3024. document.addEventListener("mouseup", e => clickUp(e));
  3025. document.addEventListener("touchend", e => {
  3026. const fakeEvent = {
  3027. target: e.target,
  3028. clientX: e.changedTouches[0].clientX,
  3029. clientY: e.changedTouches[0].clientY,
  3030. which: 1
  3031. };
  3032. clickUp(fakeEvent);
  3033. });
  3034. const formList = document.querySelector("#entity-form");
  3035. formList.addEventListener("input", e => {
  3036. const entity = entities[selected.dataset.key];
  3037. entity.form = e.target.value;
  3038. entity.view = entity.formViews[entity.form];
  3039. if (Object.keys(entity.forms).length > 0)
  3040. entity.views[entity.view].height = entity.formSizes[entity.form].height;
  3041. preloadViews(entity);
  3042. configViewList(entity, entity.view);
  3043. const image = entity.views[entity.view].image;
  3044. selected.querySelector(".entity-image").src = image.source;
  3045. configViewOptions(entity, entity.view);
  3046. displayAttribution(image.source);
  3047. if (image.bottom !== undefined) {
  3048. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  3049. } else {
  3050. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1) * 100) + "%")
  3051. }
  3052. if (config.autoFitSize) {
  3053. let targets = {};
  3054. targets[selected.dataset.key] = entities[selected.dataset.key];
  3055. fitEntities(targets);
  3056. }
  3057. configSizeList(entity);
  3058. updateSizes();
  3059. updateEntityOptions(entities[selected.dataset.key], e.target.value);
  3060. updateViewOptions(entities[selected.dataset.key], entity.view);
  3061. });
  3062. const viewList = document.querySelector("#entity-view");
  3063. document.querySelector("#entity-view").addEventListener("input", e => {
  3064. const entity = entities[selected.dataset.key];
  3065. entity.view = e.target.value;
  3066. preloadViews(entity);
  3067. const image = entities[selected.dataset.key].views[e.target.value].image;
  3068. selected.querySelector(".entity-image").src = image.source;
  3069. configViewOptions(entity, entity.view);
  3070. displayAttribution(image.source);
  3071. if (image.bottom !== undefined) {
  3072. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  3073. } else {
  3074. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1) * 100) + "%")
  3075. }
  3076. updateSizes();
  3077. updateEntityOptions(entities[selected.dataset.key], e.target.value);
  3078. updateViewOptions(entities[selected.dataset.key], e.target.value);
  3079. });
  3080. document.querySelector("#entity-view").addEventListener("input", e => {
  3081. if (viewList.options[viewList.selectedIndex].classList.contains("nsfw")) {
  3082. viewList.classList.add("nsfw");
  3083. } else {
  3084. viewList.classList.remove("nsfw");
  3085. }
  3086. })
  3087. clearViewList();
  3088. document.querySelector("#menu-clear").addEventListener("click", e => {
  3089. removeAllEntities();
  3090. });
  3091. document.querySelector("#delete-entity").disabled = true;
  3092. document.querySelector("#delete-entity").addEventListener("click", e => {
  3093. if (selected) {
  3094. removeEntity(selected);
  3095. selected = null;
  3096. }
  3097. });
  3098. document.querySelector("#menu-order-height").addEventListener("click", e => {
  3099. const order = Object.keys(entities).sort((a, b) => {
  3100. const entA = entities[a];
  3101. const entB = entities[b];
  3102. const viewA = entA.view;
  3103. const viewB = entB.view;
  3104. const heightA = entA.views[viewA].height.to("meter").value;
  3105. const heightB = entB.views[viewB].height.to("meter").value;
  3106. return heightA - heightB;
  3107. });
  3108. arrangeEntities(order);
  3109. });
  3110. // TODO: write some generic logic for this lol
  3111. document.querySelector("#scroll-left").addEventListener("mousedown", e => {
  3112. scrollDirection = -1;
  3113. clearInterval(scrollHandle);
  3114. scrollHandle = setInterval(doXScroll, 1000 / 20);
  3115. e.stopPropagation();
  3116. });
  3117. document.querySelector("#scroll-right").addEventListener("mousedown", e => {
  3118. scrollDirection = 1;
  3119. clearInterval(scrollHandle);
  3120. scrollHandle = setInterval(doXScroll, 1000 / 20);
  3121. e.stopPropagation();
  3122. });
  3123. document.querySelector("#scroll-left").addEventListener("touchstart", e => {
  3124. scrollDirection = -1;
  3125. clearInterval(scrollHandle);
  3126. scrollHandle = setInterval(doXScroll, 1000 / 20);
  3127. e.stopPropagation();
  3128. });
  3129. document.querySelector("#scroll-right").addEventListener("touchstart", e => {
  3130. scrollDirection = 1;
  3131. clearInterval(scrollHandle);
  3132. scrollHandle = setInterval(doXScroll, 1000 / 20);
  3133. e.stopPropagation();
  3134. });
  3135. document.querySelector("#scroll-up").addEventListener("mousedown", e => {
  3136. scrollDirection = 1;
  3137. clearInterval(scrollHandle);
  3138. scrollHandle = setInterval(doYScroll, 1000 / 20);
  3139. e.stopPropagation();
  3140. });
  3141. document.querySelector("#scroll-down").addEventListener("mousedown", e => {
  3142. scrollDirection = -1;
  3143. clearInterval(scrollHandle);
  3144. scrollHandle = setInterval(doYScroll, 1000 / 20);
  3145. e.stopPropagation();
  3146. });
  3147. document.querySelector("#scroll-up").addEventListener("touchstart", e => {
  3148. scrollDirection = 1;
  3149. clearInterval(scrollHandle);
  3150. scrollHandle = setInterval(doYScroll, 1000 / 20);
  3151. e.stopPropagation();
  3152. });
  3153. document.querySelector("#scroll-down").addEventListener("touchstart", e => {
  3154. scrollDirection = -1;
  3155. clearInterval(scrollHandle);
  3156. scrollHandle = setInterval(doYScroll, 1000 / 20);
  3157. e.stopPropagation();
  3158. });
  3159. document.addEventListener("mouseup", e => {
  3160. clearInterval(scrollHandle);
  3161. scrollHandle = null;
  3162. });
  3163. document.addEventListener("touchend", e => {
  3164. clearInterval(scrollHandle);
  3165. scrollHandle = null;
  3166. });
  3167. document.querySelector("#zoom-in").addEventListener("mousedown", e => {
  3168. zoomDirection = -1;
  3169. clearInterval(zoomHandle);
  3170. zoomHandle = setInterval(doZoom, 1000 / 20);
  3171. e.stopPropagation();
  3172. });
  3173. document.querySelector("#zoom-out").addEventListener("mousedown", e => {
  3174. zoomDirection = 1;
  3175. clearInterval(zoomHandle);
  3176. zoomHandle = setInterval(doZoom, 1000 / 20);
  3177. e.stopPropagation();
  3178. });
  3179. document.querySelector("#zoom-in").addEventListener("touchstart", e => {
  3180. zoomDirection = -1;
  3181. clearInterval(zoomHandle);
  3182. zoomHandle = setInterval(doZoom, 1000 / 20);
  3183. e.stopPropagation();
  3184. });
  3185. document.querySelector("#zoom-out").addEventListener("touchstart", e => {
  3186. zoomDirection = 1;
  3187. clearInterval(zoomHandle);
  3188. zoomHandle = setInterval(doZoom, 1000 / 20);
  3189. e.stopPropagation();
  3190. });
  3191. document.addEventListener("mouseup", e => {
  3192. clearInterval(zoomHandle);
  3193. zoomHandle = null;
  3194. });
  3195. document.addEventListener("touchend", e => {
  3196. clearInterval(zoomHandle);
  3197. zoomHandle = null;
  3198. });
  3199. document.querySelector("#shrink").addEventListener("mousedown", e => {
  3200. sizeDirection = -1;
  3201. clearInterval(sizeHandle);
  3202. sizeHandle = setInterval(doSize, 1000 / 20);
  3203. e.stopPropagation();
  3204. });
  3205. document.querySelector("#grow").addEventListener("mousedown", e => {
  3206. sizeDirection = 1;
  3207. clearInterval(sizeHandle);
  3208. sizeHandle = setInterval(doSize, 1000 / 20);
  3209. e.stopPropagation();
  3210. });
  3211. document.querySelector("#shrink").addEventListener("touchstart", e => {
  3212. sizeDirection = -1;
  3213. clearInterval(sizeHandle);
  3214. sizeHandle = setInterval(doSize, 1000 / 20);
  3215. e.stopPropagation();
  3216. });
  3217. document.querySelector("#grow").addEventListener("touchstart", e => {
  3218. sizeDirection = 1;
  3219. clearInterval(sizeHandle);
  3220. sizeHandle = setInterval(doSize, 1000 / 20);
  3221. e.stopPropagation();
  3222. });
  3223. document.addEventListener("mouseup", e => {
  3224. clearInterval(sizeHandle);
  3225. sizeHandle = null;
  3226. });
  3227. document.addEventListener("touchend", e => {
  3228. clearInterval(sizeHandle);
  3229. sizeHandle = null;
  3230. });
  3231. document.querySelector("#ruler").addEventListener("click", e => {
  3232. rulerMode = !rulerMode;
  3233. if (rulerMode) {
  3234. toast("Ready to draw a ruler mark");
  3235. } else {
  3236. toast("Cancelled ruler mode");
  3237. }
  3238. });
  3239. document.querySelector("#ruler").addEventListener("mousedown", e => {
  3240. e.stopPropagation();
  3241. });
  3242. document.querySelector("#ruler").addEventListener("touchstart", e => {
  3243. e.stopPropagation();
  3244. });
  3245. document.querySelector("#fit").addEventListener("click", e => {
  3246. if (selected) {
  3247. let targets = {};
  3248. targets[selected.dataset.key] = entities[selected.dataset.key];
  3249. fitEntities(targets);
  3250. }
  3251. });
  3252. document.querySelector("#fit").addEventListener("mousedown", e => {
  3253. e.stopPropagation();
  3254. });
  3255. document.querySelector("#fit").addEventListener("touchstart", e => {
  3256. e.stopPropagation();
  3257. });
  3258. document.querySelector("#options-world-fit").addEventListener("click", () => fitWorld(true));
  3259. document.querySelector("#options-reset-pos-x").addEventListener("click", () => { config.x = 0; updateSizes(); });
  3260. document.querySelector("#options-reset-pos-y").addEventListener("click", () => { config.y = 0; updateSizes(); });
  3261. document.addEventListener("keydown", e => {
  3262. if (e.key == "Delete") {
  3263. if (selected) {
  3264. removeEntity(selected);
  3265. selected = null;
  3266. }
  3267. }
  3268. })
  3269. document.addEventListener("keydown", e => {
  3270. if (e.key == "Shift") {
  3271. shiftHeld = true;
  3272. e.preventDefault();
  3273. } else if (e.key == "Alt") {
  3274. altHeld = true;
  3275. movingInBounds = false; // don't snap the object back in bounds when we let go
  3276. e.preventDefault();
  3277. }
  3278. });
  3279. document.addEventListener("keyup", e => {
  3280. if (e.key == "Shift") {
  3281. shiftHeld = false;
  3282. e.preventDefault();
  3283. } else if (e.key == "Alt") {
  3284. altHeld = false;
  3285. e.preventDefault();
  3286. }
  3287. });
  3288. window.addEventListener("resize", handleResize);
  3289. // TODO: further investigate why the tool initially starts out with wrong
  3290. // values under certain circumstances (seems to be narrow aspect ratios -
  3291. // maybe the menu bar is animating when it shouldn't)
  3292. setTimeout(handleResize, 250);
  3293. setTimeout(handleResize, 500);
  3294. setTimeout(handleResize, 750);
  3295. setTimeout(handleResize, 1000);
  3296. document.querySelector("#menu-permalink").addEventListener("click", e => {
  3297. linkScene();
  3298. });
  3299. document.querySelector("#menu-export").addEventListener("click", e => {
  3300. copyScene();
  3301. });
  3302. document.querySelector("#menu-import").addEventListener("click", e => {
  3303. pasteScene();
  3304. });
  3305. document.querySelector("#menu-save").addEventListener("click", e => {
  3306. const name = document.querySelector("#menu-save ~ input").value;
  3307. if (/\S/.test(name)) {
  3308. saveScene(name);
  3309. }
  3310. updateSaveInfo();
  3311. });
  3312. document.querySelector("#menu-load").addEventListener("click", e => {
  3313. const name = document.querySelector("#menu-load ~ select").value;
  3314. if (/\S/.test(name)) {
  3315. loadScene(name);
  3316. }
  3317. });
  3318. document.querySelector("#menu-delete").addEventListener("click", e => {
  3319. const name = document.querySelector("#menu-delete ~ select").value;
  3320. if (/\S/.test(name)) {
  3321. deleteScene(name);
  3322. }
  3323. });
  3324. document.querySelector("#menu-load-autosave").addEventListener("click", e => {
  3325. loadScene("autosave");
  3326. });
  3327. document.querySelector("#menu-add-image").addEventListener("click", e => {
  3328. document.querySelector("#file-upload-picker").click();
  3329. });
  3330. document.querySelector("#file-upload-picker").addEventListener("change", e => {
  3331. if (e.target.files.length > 0) {
  3332. for (let i=0; i<e.target.files.length; i++) {
  3333. customEntityFromFile(e.target.files[i]);
  3334. }
  3335. }
  3336. })
  3337. document.querySelector("#menu-clear-rulers").addEventListener("click", e => {
  3338. rulers = [];
  3339. drawRulers();
  3340. });
  3341. document.addEventListener("paste", e => {
  3342. let index = 0;
  3343. let item = null;
  3344. let found = false;
  3345. for (; index < e.clipboardData.items.length; index++) {
  3346. item = e.clipboardData.items[index];
  3347. if (item.type == "image/png") {
  3348. found = true;
  3349. break;
  3350. }
  3351. }
  3352. if (!found) {
  3353. return;
  3354. }
  3355. let url = null;
  3356. const file = item.getAsFile();
  3357. customEntityFromFile(file);
  3358. });
  3359. document.querySelector("#world").addEventListener("dragover", e => {
  3360. e.preventDefault();
  3361. })
  3362. document.querySelector("#world").addEventListener("drop", e => {
  3363. e.preventDefault();
  3364. if (e.dataTransfer.files.length > 0) {
  3365. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  3366. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  3367. let coords = pix2pos({x: e.clientX-entX, y: e.clientY-entY});
  3368. customEntityFromFile(e.dataTransfer.files[0], coords.x, coords.y);
  3369. }
  3370. })
  3371. clearEntityOptions();
  3372. clearViewOptions();
  3373. clearAttribution();
  3374. // we do this last because configuring settings can cause things
  3375. // to happen (e.g. auto-fit)
  3376. prepareSettings(getUserSettings());
  3377. // now that we have this loaded, we can set it
  3378. unitSelector.dataset.oldUnit = defaultUnits.length[config.units];
  3379. document.querySelector("#options-height-unit").value = defaultUnits.length[config.units];
  3380. // ...and then update the world height by setting off an input event
  3381. document.querySelector("#options-height-unit").dispatchEvent(new Event('input', {
  3382. }));
  3383. if (param === null) {
  3384. scenes["Empty"]();
  3385. }
  3386. else {
  3387. try {
  3388. const data = JSON.parse(b64DecodeUnicode(param));
  3389. if (data.entities === undefined) {
  3390. return;
  3391. }
  3392. if (data.world === undefined) {
  3393. return;
  3394. }
  3395. importScene(data);
  3396. } catch (err) {
  3397. console.error(err);
  3398. scenes["Empty"]();
  3399. // probably wasn't valid data
  3400. }
  3401. }
  3402. document.querySelector("#world").addEventListener("wheel", e => {
  3403. if (shiftHeld) {
  3404. if (selected) {
  3405. const dir = e.deltaY > 0 ? 10 / 11 : 11 / 10;
  3406. const entity = entities[selected.dataset.key];
  3407. entity.views[entity.view].height = math.multiply(entity.views[entity.view].height, dir);
  3408. entity.dirty = true;
  3409. updateEntityOptions(entity, entity.view);
  3410. updateViewOptions(entity, entity.view);
  3411. updateSizes(true);
  3412. } else {
  3413. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  3414. config.x += (e.deltaY > 0 ? 1 : -1) * worldWidth / 20 ;
  3415. updateSizes();
  3416. updateSizes();
  3417. }
  3418. } else {
  3419. if (config.autoFit) {
  3420. toastRateLimit("Zoom is locked! Check Settings to disable.", "zoom-lock", 1000);
  3421. } else {
  3422. const dir = e.deltaY < 0 ? 10 / 11 : 11 / 10;
  3423. const change = config.height.toNumber("meters") - math.multiply(config.height, dir).toNumber("meters");
  3424. if (!config.lockYAxis) {
  3425. config.y += change / 2;
  3426. }
  3427. setWorldHeight(config.height, math.multiply(config.height, dir));
  3428. updateWorldOptions();
  3429. }
  3430. }
  3431. checkFitWorld();
  3432. })
  3433. updateWorldHeight();
  3434. document.querySelector("#search-box").addEventListener("change", e => doSearch(e.target.value));
  3435. });
  3436. let searchText = "";
  3437. function doSearch(value) {
  3438. searchText = value;
  3439. updateFilter();
  3440. }
  3441. function customEntityFromFile(file, x=0.5, y=0.5) {
  3442. file.arrayBuffer().then(buf => {
  3443. arr = new Uint8Array(buf);
  3444. blob = new Blob([arr], {type: file.type });
  3445. url = window.URL.createObjectURL(blob)
  3446. makeCustomEntity(url, x, y);
  3447. });
  3448. }
  3449. function makeCustomEntity(url, x=0.5, y=0.5) {
  3450. const maker = createEntityMaker(
  3451. {
  3452. name: "Custom Entity"
  3453. },
  3454. {
  3455. custom: {
  3456. attributes: {
  3457. height: {
  3458. name: "Height",
  3459. power: 1,
  3460. type: "length",
  3461. base: math.unit(6, "feet")
  3462. }
  3463. },
  3464. image: {
  3465. source: url
  3466. },
  3467. name: "Image",
  3468. info: {},
  3469. rename: false
  3470. }
  3471. },
  3472. []
  3473. );
  3474. const entity = maker.constructor();
  3475. entity.scale = config.height.toNumber("feet") / 20;
  3476. entity.ephemeral = true;
  3477. displayEntity(entity, "custom", x, y, true, true);
  3478. }
  3479. const filterDefs = {
  3480. none: {
  3481. id: "none",
  3482. name: "No Filter",
  3483. extract: maker => [],
  3484. render: name => name,
  3485. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  3486. },
  3487. author: {
  3488. id: "author",
  3489. name: "Authors",
  3490. extract: maker => maker.authors ? maker.authors : [],
  3491. render: author => attributionData.people[author].name,
  3492. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  3493. },
  3494. owner: {
  3495. id: "owner",
  3496. name: "Owners",
  3497. extract: maker => maker.owners ? maker.owners : [],
  3498. render: owner => attributionData.people[owner].name,
  3499. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  3500. },
  3501. species: {
  3502. id: "species",
  3503. name: "Species",
  3504. extract: maker => maker.info && maker.info.species ? getSpeciesInfo(maker.info.species) : [],
  3505. render: species => speciesData[species].name,
  3506. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  3507. },
  3508. tags: {
  3509. id: "tags",
  3510. name: "Tags",
  3511. extract: maker => maker.info && maker.info.tags ? maker.info.tags : [],
  3512. render: tag => tagDefs[tag],
  3513. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  3514. },
  3515. size: {
  3516. id: "size",
  3517. name: "Normal Size",
  3518. extract: maker => maker.sizes && maker.sizes.length > 0 ? Array.from(maker.sizes.reduce((result, size) => {
  3519. if (result && !size.default) {
  3520. return result;
  3521. }
  3522. let meters = size.height.toNumber("meters");
  3523. if (meters < 1e-1) {
  3524. return ["micro"];
  3525. } else if (meters < 1e1) {
  3526. return ["moderate"];
  3527. } else {
  3528. return ["macro"];
  3529. }
  3530. }, null)) : [],
  3531. render: tag => { return {
  3532. "micro": "Micro",
  3533. "moderate": "Moderate",
  3534. "macro": "Macro"
  3535. }[tag]},
  3536. sort: (tag1, tag2) => {
  3537. const order = {
  3538. "micro": 0,
  3539. "moderate": 1,
  3540. "macro": 2
  3541. };
  3542. return order[tag1[0]] - order[tag2[0]];
  3543. }
  3544. },
  3545. allSizes: {
  3546. id: "allSizes",
  3547. name: "Possible Size",
  3548. extract: maker => maker.sizes ? Array.from(maker.sizes.reduce((set, size) => {
  3549. const height = size.height;
  3550. let result = Object.entries(sizeCategories).reduce((result, [name, value]) => {
  3551. if (result) {
  3552. return result;
  3553. } else {
  3554. if (math.compare(height, value) <= 0) {
  3555. return name;
  3556. }
  3557. }
  3558. }, null);
  3559. set.add(result ? result : "infinite");
  3560. return set;
  3561. }, new Set())) : [],
  3562. render: tag => tag[0].toUpperCase() + tag.slice(1),
  3563. sort: (tag1, tag2) => {
  3564. const order = [
  3565. "atomic", "microscopic", "tiny", "small", "moderate", "large", "macro", "megamacro", "planetary", "stellar",
  3566. "galactic", "universal", "omniversal", "infinite"
  3567. ]
  3568. return order.indexOf(tag1[0]) - order.indexOf(tag2[0]);
  3569. }
  3570. }
  3571. }
  3572. const sizeCategories = {
  3573. "atomic": math.unit(100, "angstroms"),
  3574. "microscopic": math.unit(100, "micrometers"),
  3575. "tiny": math.unit(100, "millimeters"),
  3576. "small": math.unit(1, "meter"),
  3577. "moderate": math.unit(3, "meters"),
  3578. "large": math.unit(10, "meters"),
  3579. "macro": math.unit(300, "meters"),
  3580. "megamacro": math.unit(1000, "kilometers"),
  3581. "planetary": math.unit(10, "earths"),
  3582. "stellar": math.unit(10, "solarradii"),
  3583. "galactic": math.unit(10, "galaxies"),
  3584. "universal": math.unit(10, "universes"),
  3585. "omniversal": math.unit(10, "multiverses")
  3586. };
  3587. function prepareEntities() {
  3588. availableEntities["buildings"] = makeBuildings();
  3589. availableEntities["characters"] = makeCharacters();
  3590. availableEntities["clothing"] = makeClothing();
  3591. availableEntities["creatures"] = makeCreatures();
  3592. availableEntities["fiction"] = makeFiction();
  3593. availableEntities["food"] = makeFood();
  3594. availableEntities["furniture"] = makeFurniture();
  3595. availableEntities["landmarks"] = makeLandmarks();
  3596. availableEntities["naturals"] = makeNaturals();
  3597. availableEntities["objects"] = makeObjects();
  3598. availableEntities["pokemon"] = makePokemon();
  3599. availableEntities["real-buildings"] = makeRealBuildings();
  3600. availableEntities["real-terrain"] = makeRealTerrains();
  3601. availableEntities["species"] = makeSpecies();
  3602. availableEntities["vehicles"] = makeVehicles();
  3603. availableEntities["species"].forEach(x => {
  3604. if (x.name == "Human") {
  3605. availableEntities["food"].push(x);
  3606. }
  3607. })
  3608. availableEntities["characters"].sort((x, y) => {
  3609. return x.name.localeCompare(y.name)
  3610. });
  3611. availableEntities["species"].sort((x, y) => {
  3612. return x.name.localeCompare(y.name)
  3613. });
  3614. availableEntities["objects"].sort((x, y) => {
  3615. return x.name.localeCompare(y.name)
  3616. });
  3617. const holder = document.querySelector("#spawners");
  3618. const filterHolder = document.querySelector("#filters");
  3619. const categorySelect = document.createElement("select");
  3620. categorySelect.id = "category-picker";
  3621. const filterSelect = document.createElement("select");
  3622. filterSelect.id = "filter-picker";
  3623. holder.appendChild(categorySelect);
  3624. filterHolder.appendChild(filterSelect);
  3625. const filterSets = {};
  3626. Object.values(filterDefs).forEach(filter => {
  3627. filterSets[filter.id] = new Set();
  3628. })
  3629. Object.entries(availableEntities).forEach(([category, entityList]) => {
  3630. const select = document.createElement("select");
  3631. select.id = "create-entity-" + category;
  3632. select.classList.add("entity-select");
  3633. for (let i = 0; i < entityList.length; i++) {
  3634. const entity = entityList[i];
  3635. const option = document.createElement("option");
  3636. option.value = i;
  3637. option.innerText = entity.name;
  3638. select.appendChild(option);
  3639. if (entity.nsfw) {
  3640. option.classList.add("nsfw");
  3641. }
  3642. Object.values(filterDefs).forEach(filter => {
  3643. filter.extract(entity).forEach(result => {
  3644. filterSets[filter.id].add(result);
  3645. });
  3646. });
  3647. availableEntitiesByName[entity.name] = entity;
  3648. };
  3649. select.addEventListener("change", e => {
  3650. if (select.options[select.selectedIndex]?.classList.contains("nsfw")) {
  3651. select.classList.add("nsfw");
  3652. } else {
  3653. select.classList.remove("nsfw");
  3654. }
  3655. // preload the entity's first image
  3656. const entity = entityList[select.selectedIndex]?.constructor();
  3657. if (entity)
  3658. {
  3659. let img = new Image();
  3660. img.src = entity.currentView.image.source;
  3661. }
  3662. })
  3663. const button = document.createElement("button");
  3664. button.id = "create-entity-" + category + "-button";
  3665. button.classList.add("entity-button");
  3666. button.innerHTML = "<i class=\"far fa-plus-square\"></i>";
  3667. button.addEventListener("click", e => {
  3668. if (entityList[select.value] == null)
  3669. return;
  3670. const newEntity = entityList[select.value].constructor()
  3671. let yOffset = 0;
  3672. if (config.lockYAxis) {
  3673. if (config.groundPos === "very-high") {
  3674. yOffset = config.height.toNumber("meters") / 12 * 5;
  3675. }
  3676. else if (config.groundPos === "high") {
  3677. yOffset = config.height.toNumber("meters") / 12 * 4;
  3678. }
  3679. else if (config.groundPos === "medium") {
  3680. yOffset = config.height.toNumber("meters") / 12 * 3;
  3681. }
  3682. else if (config.groundPos === "low") {
  3683. yOffset = config.height.toNumber("meters") / 12 * 2;
  3684. }
  3685. else if (config.groundPos === "very-low") {
  3686. yOffset = config.height.toNumber("meters") / 12 * 1;
  3687. }
  3688. else if (config.groundPos === "bottom") {
  3689. yOffset = 0;
  3690. }
  3691. } else {
  3692. yOffset = (config.lockYAxis ? 0 : config.height.toNumber("meters")/2);
  3693. }
  3694. displayEntity(newEntity, newEntity.defaultView, config.x, config.y + yOffset, true, true);
  3695. });
  3696. const categoryOption = document.createElement("option");
  3697. categoryOption.value = category
  3698. categoryOption.innerText = category;
  3699. if (category == "characters") {
  3700. categoryOption.selected = true;
  3701. select.classList.add("category-visible");
  3702. button.classList.add("category-visible");
  3703. }
  3704. categorySelect.appendChild(categoryOption);
  3705. holder.appendChild(select);
  3706. holder.appendChild(button);
  3707. });
  3708. Object.values(filterDefs).forEach(filter => {
  3709. const option = document.createElement("option");
  3710. option.innerText = filter.name;
  3711. option.value = filter.id;
  3712. filterSelect.appendChild(option);
  3713. const filterNameSelect = document.createElement("select");
  3714. filterNameSelect.classList.add("filter-select");
  3715. filterNameSelect.id = "filter-" + filter.id;
  3716. filterHolder.appendChild(filterNameSelect);
  3717. const button = document.createElement("button");
  3718. button.classList.add("filter-button");
  3719. button.id = "create-filtered-" + filter.id + "-button";
  3720. filterHolder.appendChild(button);
  3721. const counter = document.createElement("div");
  3722. counter.classList.add("button-counter");
  3723. counter.innerText = "10";
  3724. button.appendChild(counter);
  3725. const i = document.createElement("i");
  3726. i.classList.add("fas");
  3727. i.classList.add("fa-plus");
  3728. button.appendChild(i);
  3729. button.addEventListener("click", e => {
  3730. const makers = Array.from(document.querySelector(".entity-select.category-visible")).filter(element => !element.classList.contains("filtered"));
  3731. const count = makers.length + 2;
  3732. let index = 1;
  3733. if (makers.length > 50) {
  3734. if (!confirm("Really spawn " + makers.length + " things at once?")) {
  3735. return;
  3736. }
  3737. }
  3738. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  3739. const spawned = makers.map(element => {
  3740. const category = document.querySelector("#category-picker").value;
  3741. const maker = availableEntities[category][element.value];
  3742. const entity = maker.constructor()
  3743. displayEntity(entity, entity.view, -worldWidth * 0.45 + config.x + worldWidth * 0.9 * index / (count - 1), config.y);
  3744. index += 1;
  3745. return entityIndex - 1;
  3746. });
  3747. updateSizes(true);
  3748. if (config.autoFitAdd) {
  3749. let targets = {};
  3750. spawned.forEach(key => {
  3751. targets[key] = entities[key];
  3752. })
  3753. fitEntities(targets);
  3754. }
  3755. });
  3756. Array.from(filterSets[filter.id]).map(name => [name, filter.render(name)]).sort(filterDefs[filter.id].sort).forEach(name => {
  3757. const option = document.createElement("option");
  3758. option.innerText = name[1];
  3759. option.value = name[0];
  3760. filterNameSelect.appendChild(option);
  3761. });
  3762. filterNameSelect.addEventListener("change", e => {
  3763. updateFilter();
  3764. });
  3765. });
  3766. console.log("Loaded " + Object.keys(availableEntitiesByName).length + " entities");
  3767. categorySelect.addEventListener("input", e => {
  3768. const oldSelect = document.querySelector(".entity-select.category-visible");
  3769. oldSelect.classList.remove("category-visible");
  3770. const oldButton = document.querySelector(".entity-button.category-visible");
  3771. oldButton.classList.remove("category-visible");
  3772. const newSelect = document.querySelector("#create-entity-" + e.target.value);
  3773. newSelect.classList.add("category-visible");
  3774. const newButton = document.querySelector("#create-entity-" + e.target.value + "-button");
  3775. newButton.classList.add("category-visible");
  3776. recomputeFilters();
  3777. updateFilter();
  3778. });
  3779. recomputeFilters();
  3780. filterSelect.addEventListener("input", e => {
  3781. const oldSelect = document.querySelector(".filter-select.category-visible");
  3782. if (oldSelect)
  3783. oldSelect.classList.remove("category-visible");
  3784. const newSelect = document.querySelector("#filter-" + e.target.value);
  3785. if (newSelect && e.target.value != "none")
  3786. newSelect.classList.add("category-visible");
  3787. updateFilter();
  3788. });
  3789. ratioInfo = document.body.querySelector(".extra-info")
  3790. }
  3791. // Only display authors and owners if they appear
  3792. // somewhere in the current entity list
  3793. function recomputeFilters() {
  3794. const category = document.querySelector("#category-picker").value;
  3795. const filterSets = {};
  3796. Object.values(filterDefs).forEach(filter => {
  3797. filterSets[filter.id] = new Set();
  3798. });
  3799. document.querySelectorAll(".entity-select.category-visible > option").forEach(element => {
  3800. const entity = availableEntities[category][element.value];
  3801. Object.values(filterDefs).forEach(filter => {
  3802. filter.extract(entity).forEach(result => {
  3803. filterSets[filter.id].add(result);
  3804. });
  3805. });
  3806. });
  3807. Object.values(filterDefs).forEach(filter => {
  3808. // always show the "none" option
  3809. let found = filter.id == "none";
  3810. document.querySelectorAll("#filter-" + filter.id + " > option").forEach(element => {
  3811. if (filterSets[filter.id].has(element.value) || filter.id == "none") {
  3812. element.classList.remove("filtered");
  3813. element.disabled = false;
  3814. found = true;
  3815. } else {
  3816. element.classList.add("filtered");
  3817. element.disabled = true;
  3818. }
  3819. });
  3820. const filterOption = document.querySelector("#filter-picker > option[value='" + filter.id + "']");
  3821. if (found) {
  3822. filterOption.classList.remove("filtered");
  3823. filterOption.disabled = false;
  3824. } else {
  3825. filterOption.classList.add("filtered");
  3826. filterOption.disabled = true;
  3827. }
  3828. });
  3829. document.querySelector("#filter-picker").value = "none";
  3830. document.querySelector("#filter-picker").dispatchEvent(new Event("input"));
  3831. }
  3832. function updateFilter() {
  3833. const category = document.querySelector("#category-picker").value;
  3834. const type = document.querySelector("#filter-picker").value;
  3835. const filterKeySelect = document.querySelector(".filter-select.category-visible");
  3836. clearFilter();
  3837. const noFilter = !filterKeySelect;
  3838. let key;
  3839. let current = document.querySelector(".entity-select.category-visible").value;
  3840. if (!noFilter)
  3841. {
  3842. key = filterKeySelect.value;
  3843. current
  3844. }
  3845. let replace = current == "";
  3846. let first = null;
  3847. let count = 0;
  3848. const lowerSearchText = searchText !== "" ? searchText.toLowerCase() : null;
  3849. document.querySelectorAll(".entity-select.category-visible > option").forEach(element => {
  3850. let keep = noFilter;
  3851. if (!noFilter && filterDefs[type].extract(availableEntities[category][element.value]).indexOf(key) >= 0) {
  3852. keep = true;
  3853. }
  3854. if (searchText != "" && !availableEntities[category][element.value].name.toLowerCase().includes(lowerSearchText))
  3855. {
  3856. keep = false;
  3857. }
  3858. if (!keep) {
  3859. element.classList.add("filtered");
  3860. element.disabled = true;
  3861. if (current == element.value) {
  3862. replace = true;
  3863. }
  3864. } else {
  3865. count += 1;
  3866. if (!first) {
  3867. first = element.value;
  3868. }
  3869. }
  3870. });
  3871. const button = document.querySelector(".filter-select.category-visible + button");
  3872. if (button) {
  3873. button.querySelector(".button-counter").innerText = count;
  3874. }
  3875. if (replace) {
  3876. document.querySelector(".entity-select.category-visible").value = first;
  3877. document.querySelector("#create-entity-" + category).dispatchEvent(new Event("change"));
  3878. }
  3879. }
  3880. function clearFilter() {
  3881. document.querySelectorAll(".entity-select.category-visible > option").forEach(element => {
  3882. element.classList.remove("filtered");
  3883. element.disabled = false;
  3884. });
  3885. }
  3886. document.addEventListener("mousemove", (e) => {
  3887. if (currentRuler) {
  3888. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  3889. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  3890. let position = pix2pos({ x: e.clientX - entX, y: e.clientY - entY });
  3891. if (config.rulersStick && selected) {
  3892. position = entityRelativePosition(position, selected)
  3893. }
  3894. currentRuler.x1 = position.x;
  3895. currentRuler.y1 = position.y;
  3896. }
  3897. drawRulers();
  3898. });
  3899. document.addEventListener("touchmove", (e) => {
  3900. if (currentRuler) {
  3901. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  3902. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  3903. let position = pix2pos({ x: e.touches[0].clientX - entX, y: e.touches[0].clientY - entY });
  3904. if (config.rulersStick && selected) {
  3905. position = entityRelativePosition(position, selected)
  3906. }
  3907. currentRuler.x1 = position.x;
  3908. currentRuler.y1 = position.y;
  3909. }
  3910. drawRulers();
  3911. });
  3912. document.addEventListener("mousemove", (e) => {
  3913. if (clicked) {
  3914. let position = pix2pos({ x: e.clientX - dragOffsetX, y: e.clientY - dragOffsetY });
  3915. if (movingInBounds) {
  3916. position = snapPos(position);
  3917. } else {
  3918. let x = e.clientX - dragOffsetX;
  3919. let y = e.clientY - dragOffsetY;
  3920. if (x >= 0 && x <= canvasWidth && y >= 0 && y <= canvasHeight) {
  3921. movingInBounds = true;
  3922. }
  3923. }
  3924. clicked.dataset.x = position.x;
  3925. clicked.dataset.y = position.y;
  3926. updateEntityElement(entities[clicked.dataset.key], clicked);
  3927. if (hoveringInDeleteArea(e)) {
  3928. document.querySelector("#menubar").classList.add("hover-delete");
  3929. } else {
  3930. document.querySelector("#menubar").classList.remove("hover-delete");
  3931. }
  3932. }
  3933. if (panning && panReady) {
  3934. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  3935. const worldHeight = config.height.toNumber("meters");
  3936. config.x -= (e.clientX - panOffsetX) / canvasWidth * worldWidth;
  3937. config.y += (e.clientY - panOffsetY) / canvasHeight * worldHeight;
  3938. panOffsetX = e.clientX;
  3939. panOffsetY = e.clientY;
  3940. updateSizes();
  3941. panReady = false;
  3942. setTimeout(() => panReady=true, 1000/120);
  3943. }
  3944. });
  3945. document.addEventListener("touchmove", (e) => {
  3946. if (clicked) {
  3947. e.preventDefault();
  3948. let x = e.touches[0].clientX;
  3949. let y = e.touches[0].clientY;
  3950. const position = snapPos(pix2pos({ x: x - dragOffsetX, y: y - dragOffsetY }));
  3951. clicked.dataset.x = position.x;
  3952. clicked.dataset.y = position.y;
  3953. updateEntityElement(entities[clicked.dataset.key], clicked);
  3954. // what a hack
  3955. // I should centralize this 'fake event' creation...
  3956. if (hoveringInDeleteArea({ clientY: y })) {
  3957. document.querySelector("#menubar").classList.add("hover-delete");
  3958. } else {
  3959. document.querySelector("#menubar").classList.remove("hover-delete");
  3960. }
  3961. }
  3962. if (panning && panReady) {
  3963. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  3964. const worldHeight = config.height.toNumber("meters");
  3965. config.x -= (e.touches[0].clientX - panOffsetX) / canvasWidth * worldWidth;
  3966. config.y += (e.touches[0].clientY - panOffsetY) / canvasHeight * worldHeight;
  3967. panOffsetX = e.touches[0].clientX;
  3968. panOffsetY = e.touches[0].clientY;
  3969. updateSizes();
  3970. panReady = false;
  3971. setTimeout(() => panReady=true, 1000/60);
  3972. }
  3973. }, { passive: false });
  3974. function checkFitWorld() {
  3975. if (config.autoFit) {
  3976. fitWorld();
  3977. return true;
  3978. }
  3979. return false;
  3980. }
  3981. function fitWorld(manual = false, factor = 1.1) {
  3982. if (Object.keys(entities).length > 0) {
  3983. fitEntities(entities, factor);
  3984. }
  3985. }
  3986. function fitEntities(targetEntities, manual = false, factor = 1.1) {
  3987. let minX = Infinity;
  3988. let maxX = -Infinity;
  3989. let minY = Infinity;
  3990. let maxY = -Infinity;
  3991. let count = 0;
  3992. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  3993. const worldHeight = config.height.toNumber("meters");
  3994. Object.entries(targetEntities).forEach(([key, entity]) => {
  3995. const view = entity.view;
  3996. let extra = entity.views[view].image.extra;
  3997. extra = extra === undefined ? 1 : extra;
  3998. const image = document.querySelector("#entity-" + key + " > .entity-image");
  3999. const x = parseFloat(document.querySelector("#entity-" + key).dataset.x);
  4000. let width = image.width;
  4001. let height = image.height;
  4002. // only really relevant if the images haven't loaded in yet
  4003. if (height == 0) {
  4004. height = 100;
  4005. }
  4006. if (width == 0) {
  4007. width = height;
  4008. }
  4009. const xBottom = x - entity.views[view].height.toNumber("meters") * width / height / 2;
  4010. const xTop = x + entity.views[view].height.toNumber("meters") * width / height / 2;
  4011. const y = parseFloat(document.querySelector("#entity-" + key).dataset.y);
  4012. const yBottom = y;
  4013. const yTop = entity.views[view].height.toNumber("meters") + yBottom;
  4014. minX = Math.min(minX, xBottom);
  4015. maxX = Math.max(maxX, xTop);
  4016. minY = Math.min(minY, yBottom);
  4017. maxY = Math.max(maxY, yTop);
  4018. count += 1;
  4019. });
  4020. if (config.lockYAxis) {
  4021. minY = 0;
  4022. }
  4023. let ySize = (maxY - minY) * factor;
  4024. let xSize = (maxX - minX) * factor;
  4025. if (xSize / ySize > worldWidth / worldHeight) {
  4026. ySize *= ((xSize / ySize) / (worldWidth / worldHeight));
  4027. }
  4028. config.x = (maxX + minX) / 2;
  4029. config.y = minY;
  4030. height = math.unit(ySize, "meter")
  4031. setWorldHeight(config.height, math.multiply(height, factor));
  4032. }
  4033. function updateWorldHeight() {
  4034. const unit = document.querySelector("#options-height-unit").value;
  4035. const rawValue = document.querySelector("#options-height-value").value;
  4036. var value
  4037. try {
  4038. value = math.evaluate(rawValue)
  4039. if (typeof(value) !== "number") {
  4040. try {
  4041. value = value.toNumber(unit)
  4042. } catch {
  4043. toast("Invalid input: " + rawValue + " can't be converted to " + unit)
  4044. }
  4045. }
  4046. } catch {
  4047. toast("Invalid input: could not parse " + rawValue)
  4048. return;
  4049. }
  4050. const newHeight = Math.max(0.000000001, value);
  4051. const oldHeight = config.height;
  4052. setWorldHeight(oldHeight, math.unit(newHeight, unit), true);
  4053. }
  4054. function setWorldHeight(oldHeight, newHeight, keepUnit=false) {
  4055. worldSizeDirty = true;
  4056. config.height = newHeight.to(document.querySelector("#options-height-unit").value)
  4057. const unit = document.querySelector("#options-height-unit").value;
  4058. setNumericInput(document.querySelector("#options-height-value"), config.height.toNumber(unit));
  4059. Object.entries(entities).forEach(([key, entity]) => {
  4060. const element = document.querySelector("#entity-" + key);
  4061. let newPosition;
  4062. if (altHeld) {
  4063. newPosition = adjustAbs({ x: element.dataset.x, y: element.dataset.y }, oldHeight, config.height);
  4064. } else {
  4065. newPosition = { x: element.dataset.x, y: element.dataset.y };
  4066. }
  4067. element.dataset.x = newPosition.x;
  4068. element.dataset.y = newPosition.y;
  4069. });
  4070. if (!keepUnit) {
  4071. pickUnit()
  4072. }
  4073. updateSizes();
  4074. }
  4075. function loadScene(name = "default") {
  4076. if (name === "") {
  4077. name = "default"
  4078. }
  4079. try {
  4080. const data = JSON.parse(localStorage.getItem("macrovision-save-" + name));
  4081. if (data === null) {
  4082. console.error("Couldn't load " + name)
  4083. return false;
  4084. }
  4085. importScene(data);
  4086. toast("Loaded " + name);
  4087. return true;
  4088. } catch (err) {
  4089. alert("Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error.")
  4090. console.error(err);
  4091. return false;
  4092. }
  4093. }
  4094. function saveScene(name = "default") {
  4095. try {
  4096. const string = JSON.stringify(exportScene());
  4097. localStorage.setItem("macrovision-save-" + name, string);
  4098. toast("Saved as " + name);
  4099. } catch (err) {
  4100. alert("Something went wrong while saving (maybe I don't have localStorage permissions, or exporting failed). Check the F12 console for the error.")
  4101. console.error(err);
  4102. }
  4103. }
  4104. function deleteScene(name = "default") {
  4105. if (confirm("Really delete the " + name + " scene?")) {
  4106. try {
  4107. localStorage.removeItem("macrovision-save-" + name)
  4108. toast("Deleted " + name);
  4109. } catch (err) {
  4110. console.error(err);
  4111. }
  4112. }
  4113. updateSaveInfo();
  4114. }
  4115. function exportScene() {
  4116. const results = {};
  4117. results.entities = [];
  4118. Object.entries(entities).filter(([key, entity]) => entity.ephemeral !== true).forEach(([key, entity]) => {
  4119. const element = document.querySelector("#entity-" + key);
  4120. results.entities.push({
  4121. name: entity.identifier,
  4122. customName: entity.name,
  4123. scale: entity.scale,
  4124. rotation: entity.rotation,
  4125. view: entity.view,
  4126. form: entity.form,
  4127. x: element.dataset.x,
  4128. y: element.dataset.y,
  4129. priority: entity.priority,
  4130. brightness: entity.brightness
  4131. });
  4132. });
  4133. const unit = document.querySelector("#options-height-unit").value;
  4134. results.world = {
  4135. height: config.height.toNumber(unit),
  4136. unit: unit,
  4137. x: config.x,
  4138. y: config.y
  4139. }
  4140. results.version = migrationDefs.length;
  4141. return results;
  4142. }
  4143. // btoa doesn't like anything that isn't ASCII
  4144. // great
  4145. // thanks to https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
  4146. // for providing an alternative
  4147. function b64EncodeUnicode(str) {
  4148. // first we use encodeURIComponent to get percent-encoded UTF-8,
  4149. // then we convert the percent encodings into raw bytes which
  4150. // can be fed into btoa.
  4151. return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
  4152. function toSolidBytes(match, p1) {
  4153. return String.fromCharCode('0x' + p1);
  4154. }));
  4155. }
  4156. function b64DecodeUnicode(str) {
  4157. // Going backwards: from bytestream, to percent-encoding, to original string.
  4158. return decodeURIComponent(atob(str).split('').map(function (c) {
  4159. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  4160. }).join(''));
  4161. }
  4162. function linkScene() {
  4163. loc = new URL(window.location);
  4164. const link = loc.protocol + "//" + loc.host + loc.pathname + "#" + b64EncodeUnicode(JSON.stringify(exportScene()));
  4165. window.history.replaceState(null, "Macrovision", link);
  4166. try {
  4167. navigator.clipboard.writeText(link);
  4168. toast("Copied permalink to clipboard");
  4169. } catch {
  4170. toast("Couldn't copy permalink");
  4171. }
  4172. }
  4173. function copyScene() {
  4174. const results = exportScene();
  4175. navigator.clipboard.writeText(JSON.stringify(results));
  4176. }
  4177. function pasteScene() {
  4178. try {
  4179. navigator.clipboard.readText().then(text => {
  4180. const data = JSON.parse(text);
  4181. if (data.entities === undefined) {
  4182. return;
  4183. }
  4184. if (data.world === undefined) {
  4185. return;
  4186. }
  4187. importScene(data);
  4188. }).catch(err => alert(err));
  4189. } catch (err) {
  4190. console.error(err);
  4191. // probably wasn't valid data
  4192. }
  4193. }
  4194. // TODO - don't just search through every single entity
  4195. // probably just have a way to do lookups directly
  4196. function findEntity(name) {
  4197. return availableEntitiesByName[name];
  4198. }
  4199. const migrationDefs = [
  4200. /*
  4201. Migration: 0 -> 1
  4202. Adds x and y coordinates for the camera
  4203. */
  4204. data => {
  4205. data.world.x = 0;
  4206. data.world.y = 0;
  4207. },
  4208. /*
  4209. Migration: 1 -> 2
  4210. Adds priority and brightness to each entity
  4211. */
  4212. data => {
  4213. data.entities.forEach(entity => {
  4214. entity.priority = 0;
  4215. entity.brightness = 1;
  4216. });
  4217. },
  4218. /*
  4219. Migration: 2 -> 3
  4220. Custom names are exported
  4221. */
  4222. data => {
  4223. data.entities.forEach(entity => {
  4224. entity.customName = entity.name
  4225. });
  4226. },
  4227. /*
  4228. Migration: 3 -> 4
  4229. Rotation is now stored
  4230. */
  4231. data => {
  4232. data.entities.forEach(entity => {
  4233. entity.rotation = 0
  4234. });
  4235. }
  4236. ]
  4237. function migrateScene(data) {
  4238. if (data.version === undefined) {
  4239. alert("This save was created before save versions were tracked. The scene may import incorrectly.");
  4240. console.trace()
  4241. data.version = 0;
  4242. } else if (data.version < migrationDefs.length) {
  4243. migrationDefs[data.version](data);
  4244. data.version += 1;
  4245. migrateScene(data);
  4246. }
  4247. }
  4248. function importScene(data) {
  4249. removeAllEntities();
  4250. migrateScene(data);
  4251. data.entities.forEach(entityInfo => {
  4252. const entity = findEntity(entityInfo.name).constructor();
  4253. entity.name = entityInfo.customName;
  4254. entity.scale = entityInfo.scale;
  4255. entity.rotation = entityInfo.rotation;
  4256. entity.priority = entityInfo.priority;
  4257. entity.brightness = entityInfo.brightness;
  4258. entity.form = entityInfo.form;
  4259. displayEntity(entity, entityInfo.view, entityInfo.x, entityInfo.y);
  4260. });
  4261. config.height = math.unit(data.world.height, data.world.unit);
  4262. config.x = data.world.x;
  4263. config.y = data.world.y;
  4264. const height = math.unit(data.world.height, data.world.unit).toNumber(defaultUnits.length[config.units]);
  4265. document.querySelector("#options-height-value").value = height;
  4266. document.querySelector("#options-height-unit").dataset.oldUnit = defaultUnits.length[config.units];
  4267. document.querySelector("#options-height-unit").value = defaultUnits.length[config.units];
  4268. if (data.canvasWidth) {
  4269. doHorizReposition(data.canvasWidth / canvasWidth);
  4270. }
  4271. updateSizes();
  4272. }
  4273. function renderToCanvas() {
  4274. const ctx = document.querySelector("#display").getContext("2d");
  4275. Object.entries(entities).sort((ent1, ent2) => {
  4276. z1 = document.querySelector("#entity-" + ent1[0]).style.zIndex;
  4277. z2 = document.querySelector("#entity-" + ent2[0]).style.zIndex;
  4278. return z1 - z2;
  4279. }).forEach(([id, entity]) => {
  4280. element = document.querySelector("#entity-" + id);
  4281. img = element.querySelector("img");
  4282. let x = parseFloat(element.dataset.x);
  4283. let y = parseFloat(element.dataset.y);
  4284. let coords = pos2pix({x: x, y: y});
  4285. let offset = img.style.getPropertyValue("--offset");
  4286. offset = parseFloat(offset.substring(0, offset.length-1))
  4287. let xSize = img.width;
  4288. let ySize = img.height;
  4289. x = coords.x
  4290. y = coords.y + ySize/2 + ySize * offset / 100;
  4291. const oldFilter = ctx.filter
  4292. const brightness = getComputedStyle(element).getPropertyValue("--brightness")
  4293. ctx.filter = `brightness(${brightness})`;
  4294. ctx.save();
  4295. ctx.resetTransform();
  4296. ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
  4297. ctx.translate(x, y);
  4298. ctx.rotate(entity.rotation);
  4299. ctx.drawImage(img, -xSize/2, -ySize/2, xSize, ySize);
  4300. ctx.restore();
  4301. ctx.filter = oldFilter
  4302. });
  4303. ctx.drawImage(document.querySelector("#rulers"), 0, 0);
  4304. }
  4305. function exportCanvas(callback) {
  4306. /** @type {CanvasRenderingContext2D} */
  4307. const ctx = document.querySelector("#display").getContext("2d");
  4308. const blob = ctx.canvas.toBlob(callback);
  4309. }
  4310. function generateScreenshot(callback) {
  4311. /** @type {CanvasRenderingContext2D} */
  4312. const ctx = document.querySelector("#display").getContext("2d");
  4313. if (config.groundKind !== "none") {
  4314. ctx.fillStyle = backgroundColors[config.groundKind];
  4315. ctx.fillRect(0, pos2pix({x: 0, y: 0}).y, canvasWidth + 100, canvasHeight);
  4316. }
  4317. renderToCanvas();
  4318. ctx.resetTransform();
  4319. ctx.fillStyle = "#999";
  4320. ctx.font = "normal normal lighter 16pt coda";
  4321. ctx.fillText("macrovision.crux.sexy", 10, 25);
  4322. exportCanvas(blob => {
  4323. callback(blob);
  4324. });
  4325. }
  4326. function copyScreenshot() {
  4327. if (window.ClipboardItem === undefined) {
  4328. alert("Sorry, this browser doesn't yet support writing images to the clipboard.");
  4329. return;
  4330. }
  4331. generateScreenshot(blob => {
  4332. navigator.clipboard.write([
  4333. new ClipboardItem({
  4334. "image/png": blob
  4335. })
  4336. ]).then(e =>
  4337. toast("Copied to clipboard!"))
  4338. .catch(e => {
  4339. toast("Couldn't write to the clipboard. Make sure the screenshot completes before switching tabs.")
  4340. })
  4341. });
  4342. drawScales(false);
  4343. }
  4344. function saveScreenshot() {
  4345. generateScreenshot(blob => {
  4346. const a = document.createElement("a");
  4347. a.href = URL.createObjectURL(blob);
  4348. a.setAttribute("download", "macrovision.png");
  4349. a.click();
  4350. });
  4351. drawScales(false);
  4352. }
  4353. function openScreenshot() {
  4354. generateScreenshot(blob => {
  4355. const a = document.createElement("a");
  4356. a.href = URL.createObjectURL(blob);
  4357. a.setAttribute("target", "_blank");
  4358. a.click();
  4359. });
  4360. drawScales(false);
  4361. }
  4362. const rateLimits = {};
  4363. function toast(msg) {
  4364. let div = document.createElement("div");
  4365. div.innerHTML = msg;
  4366. div.classList.add("toast");
  4367. document.body.appendChild(div);
  4368. setTimeout(() => {
  4369. document.body.removeChild(div);
  4370. }, 5000)
  4371. }
  4372. function toastRateLimit(msg, key, delay) {
  4373. if (!rateLimits[key]) {
  4374. toast(msg);
  4375. rateLimits[key] = setTimeout(() => {
  4376. delete rateLimits[key]
  4377. }, delay);
  4378. }
  4379. }
  4380. let lastTime = undefined;
  4381. function pan(fromX, fromY, fromHeight, toX, toY, toHeight, duration) {
  4382. Object.keys(entities).forEach(key => {
  4383. document.querySelector("#entity-" + key).classList.add("no-transition");
  4384. });
  4385. config.x = fromX;
  4386. config.y = fromY;
  4387. config.height = math.unit(fromHeight, "meters")
  4388. updateSizes();
  4389. lastTime = undefined;
  4390. requestAnimationFrame((timestamp) => panTo(toX, toY, toHeight, (toX - fromX) / duration, (toY - fromY) / duration, (toHeight - fromHeight) / duration, timestamp, duration));
  4391. }
  4392. function panTo(x, y, height, xSpeed, ySpeed, heightSpeed, timestamp, remaining) {
  4393. if (lastTime === undefined) {
  4394. lastTime = timestamp;
  4395. }
  4396. dt = timestamp - lastTime;
  4397. remaining -= dt;
  4398. if (remaining < 0) {
  4399. dt += remaining
  4400. }
  4401. let newX = config.x + xSpeed * dt;
  4402. let newY = config.y + ySpeed * dt;
  4403. let newHeight = config.height.toNumber("meters") + heightSpeed * dt;
  4404. if (remaining > 0) {
  4405. requestAnimationFrame((timestamp) => panTo(x, y, height, xSpeed, ySpeed, heightSpeed, timestamp, remaining))
  4406. } else {
  4407. Object.keys(entities).forEach(key => {
  4408. document.querySelector("#entity-" + key).classList.remove("no-transition");
  4409. });
  4410. }
  4411. config.x = newX;
  4412. config.y = newY;
  4413. config.height = math.unit(newHeight, "meters");
  4414. updateSizes();
  4415. }