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.
 
 
 

1570 lignes
47 KiB

  1. let selected = null;
  2. let selectedEntity = null;
  3. let entityIndex = 0;
  4. let clicked = null;
  5. let dragging = false;
  6. let clickTimeout = null;
  7. let dragOffsetX = null;
  8. let dragOffsetY = null;
  9. let shiftHeld = false;
  10. let altHeld = false;
  11. let entityX;
  12. let canvasWidth;
  13. let canvasHeight;
  14. let dragScale = 1;
  15. let dragScaleHandle = null;
  16. let dragEntityScale = 1;
  17. let dragEntityScaleHandle = null;
  18. const unitChoices = {
  19. length: [
  20. "meters",
  21. "angstroms",
  22. "millimeters",
  23. "centimeters",
  24. "kilometers",
  25. "inches",
  26. "feet",
  27. "stories",
  28. "miles",
  29. "AUs",
  30. "lightyears",
  31. "parsecs",
  32. ],
  33. area: [
  34. "meters^2",
  35. "cm^2",
  36. "kilometers^2",
  37. "acres",
  38. "miles^2"
  39. ],
  40. mass: [
  41. "kilograms",
  42. "milligrams",
  43. "grams",
  44. "tonnes",
  45. "lbs",
  46. "ounces",
  47. "tons"
  48. ]
  49. }
  50. const config = {
  51. height: math.unit(1500, "meters"),
  52. minLineSize: 100,
  53. maxLineSize: 150,
  54. autoFit: false,
  55. autoFitMode: "max"
  56. }
  57. const availableEntities = {
  58. }
  59. const availableEntitiesByName = {
  60. }
  61. const entities = {
  62. }
  63. function constrainRel(coords) {
  64. if (altHeld) {
  65. return coords;
  66. }
  67. return {
  68. x: Math.min(Math.max(coords.x, 0), 1),
  69. y: Math.min(Math.max(coords.y, 0), 1)
  70. }
  71. }
  72. function snapRel(coords) {
  73. return constrainRel({
  74. x: coords.x,
  75. y: altHeld ? coords.y : (Math.abs(coords.y - 1) < 0.05 ? 1 : coords.y)
  76. });
  77. }
  78. function adjustAbs(coords, oldHeight, newHeight) {
  79. const ratio = math.divide(oldHeight, newHeight);
  80. return { x: 0.5 + (coords.x - 0.5) * math.divide(oldHeight, newHeight), y: 1 + (coords.y - 1) * math.divide(oldHeight, newHeight) };
  81. }
  82. function rel2abs(coords) {
  83. return { x: coords.x * canvasWidth + 50, y: coords.y * canvasHeight };
  84. }
  85. function abs2rel(coords) {
  86. return { x: (coords.x - 50) / canvasWidth, y: coords.y / canvasHeight };
  87. }
  88. function updateEntityElement(entity, element) {
  89. const position = rel2abs({ x: element.dataset.x, y: element.dataset.y });
  90. const view = entity.view;
  91. element.style.left = position.x + "px";
  92. element.style.top = position.y + "px";
  93. element.style.setProperty("--xpos", position.x + "px");
  94. element.style.setProperty("--entity-height", "'" + entity.views[view].height.to(config.height.units[0].unit.name).format({precision: 2}) + "'");
  95. const pixels = math.divide(entity.views[view].height, config.height) * (canvasHeight - 50);
  96. const extra = entity.views[view].image.extra;
  97. const bottom = entity.views[view].image.bottom;
  98. const bonus = (extra ? extra : 1) * (1 / (1 - (bottom ? bottom : 0)));
  99. element.style.setProperty("--height", pixels * bonus + "px");
  100. element.style.setProperty("--extra", pixels * bonus - pixels + "px");
  101. if (entity.views[view].rename)
  102. element.querySelector(".entity-name").innerText = entity.name == "" ? "" : entity.views[view].name;
  103. else
  104. element.querySelector(".entity-name").innerText = entity.name;
  105. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  106. bottomName.style.left = position.x + entityX + "px";
  107. bottomName.style.top = "95vh";
  108. bottomName.innerText = entity.name;
  109. }
  110. function updateSizes(dirtyOnly = false) {
  111. drawScale();
  112. let ordered = Object.entries(entities);
  113. ordered.sort((e1, e2) => {
  114. if (e1[1].priority != e2[1].priority) {
  115. return e2[1].priority - e1[1].priority;
  116. } else {
  117. return e1[1].views[e1[1].view].height.value - e2[1].views[e2[1].view].height.value
  118. }
  119. });
  120. let zIndex = ordered.length;
  121. ordered.forEach(entity => {
  122. const element = document.querySelector("#entity-" + entity[0]);
  123. element.style.zIndex = zIndex;
  124. if (!dirtyOnly || entity[1].dirty) {
  125. updateEntityElement(entity[1], element, zIndex);
  126. entity[1].dirty = false;
  127. }
  128. zIndex -= 1;
  129. });
  130. }
  131. function drawScale() {
  132. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  133. let total = heightPer.clone();
  134. total.value = 0;
  135. for (let y = ctx.canvas.clientHeight - 50; y >= 50; y -= pixelsPer) {
  136. drawTick(ctx, 50, y, total);
  137. total = math.add(total, heightPer);
  138. }
  139. }
  140. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, value) {
  141. const oldStroke = ctx.strokeStyle;
  142. const oldFill = ctx.fillStyle;
  143. ctx.beginPath();
  144. ctx.moveTo(x, y);
  145. ctx.lineTo(x + 20, y);
  146. ctx.strokeStyle = "#000000";
  147. ctx.stroke();
  148. ctx.beginPath();
  149. ctx.moveTo(x + 20, y);
  150. ctx.lineTo(ctx.canvas.clientWidth - 70, y);
  151. ctx.strokeStyle = "#aaaaaa";
  152. ctx.stroke();
  153. ctx.beginPath();
  154. ctx.moveTo(ctx.canvas.clientWidth - 70, y);
  155. ctx.lineTo(ctx.canvas.clientWidth - 50, y);
  156. ctx.strokeStyle = "#000000";
  157. ctx.stroke();
  158. const oldFont = ctx.font;
  159. ctx.font = 'normal 24pt coda';
  160. ctx.fillStyle = "#dddddd";
  161. ctx.beginPath();
  162. ctx.fillText(value.format({ precision: 3 }), x + 20, y + 35);
  163. ctx.font = oldFont;
  164. ctx.strokeStyle = oldStroke;
  165. ctx.fillStyle = oldFill;
  166. }
  167. const canvas = document.querySelector("#display");
  168. /** @type {CanvasRenderingContext2D} */
  169. const ctx = canvas.getContext("2d");
  170. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
  171. heightPer = 1;
  172. if (pixelsPer < config.minLineSize) {
  173. const factor = math.ceil(config.minLineSize / pixelsPer);
  174. heightPer *= factor;
  175. pixelsPer *= factor;
  176. }
  177. if (pixelsPer > config.maxLineSize) {
  178. const factor = math.ceil(pixelsPer / config.maxLineSize);
  179. heightPer /= factor;
  180. pixelsPer /= factor;
  181. }
  182. heightPer = math.unit(heightPer, config.height.units[0].unit.name)
  183. ctx.clearRect(0, 0, canvas.width, canvas.height);
  184. ctx.scale(1, 1);
  185. ctx.canvas.width = canvas.clientWidth;
  186. ctx.canvas.height = canvas.clientHeight;
  187. ctx.beginPath();
  188. ctx.moveTo(50, 50);
  189. ctx.lineTo(50, ctx.canvas.clientHeight - 50);
  190. ctx.stroke();
  191. ctx.beginPath();
  192. ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
  193. ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
  194. ctx.stroke();
  195. drawTicks(ctx, pixelsPer, heightPer);
  196. }
  197. function makeEntity(info, views, sizes) {
  198. const entityTemplate = {
  199. name: info.name,
  200. identifier: info.name,
  201. scale: 1,
  202. info: info,
  203. views: views,
  204. sizes: sizes === undefined ? [] : sizes,
  205. init: function () {
  206. const entity = this;
  207. Object.entries(this.views).forEach(([viewKey, view]) => {
  208. view.parent = this;
  209. if (this.defaultView === undefined) {
  210. this.defaultView = viewKey;
  211. this.view = viewKey;
  212. }
  213. Object.entries(view.attributes).forEach(([key, val]) => {
  214. Object.defineProperty(
  215. view,
  216. key,
  217. {
  218. get: function () {
  219. return math.multiply(Math.pow(this.parent.scale, this.attributes[key].power), this.attributes[key].base);
  220. },
  221. set: function (value) {
  222. const newScale = Math.pow(math.divide(value, this.attributes[key].base), 1 / this.attributes[key].power);
  223. this.parent.scale = newScale;
  224. }
  225. }
  226. )
  227. });
  228. });
  229. this.sizes.forEach(size => {
  230. if (size.default === true) {
  231. this.views[this.defaultView].height = size.height;
  232. this.size = size;
  233. }
  234. });
  235. if (this.size === undefined && this.sizes.length > 0) {
  236. this.views[this.defaultView].height = this.sizes[0].height;
  237. this.size = this.sizes[0];
  238. console.warn("No default size set for " + info.name);
  239. } else if (this.sizes.length == 0) {
  240. this.sizes = [
  241. {
  242. name: "Normal",
  243. height: this.views[this.defaultView].height
  244. }
  245. ];
  246. this.size = this.sizes[0];
  247. }
  248. this.desc = {};
  249. Object.entries(this.info).forEach(([key, value]) => {
  250. Object.defineProperty(
  251. this.desc,
  252. key,
  253. {
  254. get: function () {
  255. let text = value.text;
  256. if (entity.views[entity.view].info) {
  257. if (entity.views[entity.view].info[key]) {
  258. text = combineInfo(text, entity.views[entity.view].info[key]);
  259. }
  260. }
  261. if (entity.size.info) {
  262. if (entity.size.info[key]) {
  263. text = combineInfo(text, entity.size.info[key]);
  264. }
  265. }
  266. return { title: value.title, text: text };
  267. }
  268. }
  269. )
  270. });
  271. delete this.init;
  272. return this;
  273. }
  274. }.init();
  275. return entityTemplate;
  276. }
  277. function combineInfo(existing, next) {
  278. switch (next.mode) {
  279. case "replace":
  280. return next.text;
  281. case "prepend":
  282. return next.text + existing;
  283. case "append":
  284. return existing + next.text;
  285. }
  286. return existing;
  287. }
  288. function clickDown(target, x, y) {
  289. clicked = target;
  290. const rect = target.getBoundingClientRect();
  291. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  292. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  293. dragOffsetX = x - rect.left + entX;
  294. dragOffsetY = y - rect.top + entY;
  295. clickTimeout = setTimeout(() => { dragging = true }, 200)
  296. target.classList.add("no-transition");
  297. }
  298. // could we make this actually detect the menu area?
  299. function hoveringInDeleteArea(e) {
  300. return e.clientY < document.body.clientHeight / 10;
  301. }
  302. function clickUp(e) {
  303. clearTimeout(clickTimeout);
  304. if (clicked) {
  305. if (dragging) {
  306. dragging = false;
  307. if (hoveringInDeleteArea(e)) {
  308. removeEntity(clicked);
  309. document.querySelector("#menubar").classList.remove("hover-delete");
  310. }
  311. } else {
  312. select(clicked);
  313. }
  314. clicked.classList.remove("no-transition");
  315. clicked = null;
  316. }
  317. }
  318. function deselect() {
  319. if (selected) {
  320. selected.classList.remove("selected");
  321. }
  322. clearAttribution();
  323. selected = null;
  324. clearViewList();
  325. clearEntityOptions();
  326. clearViewOptions();
  327. }
  328. function select(target) {
  329. deselect();
  330. selected = target;
  331. selectedEntity = entities[target.dataset.key];
  332. selected.classList.add("selected");
  333. displayAttribution(selectedEntity.views[selectedEntity.view].image.source);
  334. configViewList(selectedEntity, selectedEntity.view);
  335. configEntityOptions(selectedEntity, selectedEntity.view);
  336. configViewOptions(selectedEntity, selectedEntity.view);
  337. }
  338. function configViewList(entity, selectedView) {
  339. const list = document.querySelector("#entity-view");
  340. list.innerHTML = "";
  341. list.style.display = "block";
  342. Object.keys(entity.views).forEach(view => {
  343. const option = document.createElement("option");
  344. option.innerText = entity.views[view].name;
  345. option.value = view;
  346. if (view === selectedView) {
  347. option.selected = true;
  348. }
  349. list.appendChild(option);
  350. });
  351. }
  352. function clearViewList() {
  353. const list = document.querySelector("#entity-view");
  354. list.innerHTML = "";
  355. list.style.display = "none";
  356. }
  357. function updateWorldOptions(entity, view) {
  358. const heightInput = document.querySelector("#options-height-value");
  359. const heightSelect = document.querySelector("#options-height-unit");
  360. const converted = config.height.toNumber(heightSelect.value);
  361. heightInput.value = math.round(converted, 3);
  362. }
  363. function configEntityOptions(entity, view) {
  364. const holder = document.querySelector("#options-entity");
  365. holder.innerHTML = "";
  366. const scaleLabel = document.createElement("div");
  367. scaleLabel.classList.add("options-label");
  368. scaleLabel.innerText = "Scale";
  369. const scaleRow = document.createElement("div");
  370. scaleRow.classList.add("options-row");
  371. const scaleInput = document.createElement("input");
  372. scaleInput.classList.add("options-field-numeric");
  373. scaleInput.id = "options-entity-scale";
  374. scaleInput.addEventListener("input", e => {
  375. entity.scale = e.target.value == 0 ? 1 : e.target.value;
  376. entity.dirty = true;
  377. if (config.autoFit) {
  378. fitWorld();
  379. } else {
  380. updateSizes(true);
  381. }
  382. updateEntityOptions(entity, view);
  383. updateViewOptions(entity, view);
  384. });
  385. scaleInput.setAttribute("min", 1);
  386. scaleInput.setAttribute("type", "number");
  387. scaleInput.value = entity.scale;
  388. scaleRow.appendChild(scaleInput);
  389. holder.appendChild(scaleLabel);
  390. holder.appendChild(scaleRow);
  391. const nameLabel = document.createElement("div");
  392. nameLabel.classList.add("options-label");
  393. nameLabel.innerText = "Name";
  394. const nameRow = document.createElement("div");
  395. nameRow.classList.add("options-row");
  396. const nameInput = document.createElement("input");
  397. nameInput.classList.add("options-field-text");
  398. nameInput.value = entity.name;
  399. nameInput.addEventListener("input", e => {
  400. entity.name = e.target.value;
  401. entity.dirty = true;
  402. updateSizes(true);
  403. })
  404. nameRow.appendChild(nameInput);
  405. holder.appendChild(nameLabel);
  406. holder.appendChild(nameRow);
  407. const defaultHolder = document.querySelector("#options-entity-defaults");
  408. defaultHolder.innerHTML = "";
  409. entity.sizes.forEach(defaultInfo => {
  410. const button = document.createElement("button");
  411. button.classList.add("options-button");
  412. button.innerText = defaultInfo.name;
  413. button.addEventListener("click", e => {
  414. entity.views[entity.defaultView].height = defaultInfo.height;
  415. entity.dirty = true;
  416. updateEntityOptions(entity, view);
  417. updateViewOptions(entity, view);
  418. if (!checkFitWorld()){
  419. updateSizes(true);
  420. }
  421. });
  422. defaultHolder.appendChild(button);
  423. });
  424. document.querySelector("#options-order-display").innerText = entity.priority;
  425. document.querySelector("#options-ordering").style.display = "flex";
  426. }
  427. function updateEntityOptions(entity, view) {
  428. const scaleInput = document.querySelector("#options-entity-scale");
  429. scaleInput.value = entity.scale;
  430. document.querySelector("#options-order-display").innerText = entity.priority;
  431. }
  432. function clearEntityOptions() {
  433. const holder = document.querySelector("#options-entity");
  434. holder.innerHTML = "";
  435. document.querySelector("#options-entity-defaults").innerHTML = "";
  436. document.querySelector("#options-ordering").style.display = "none";
  437. }
  438. function configViewOptions(entity, view) {
  439. const holder = document.querySelector("#options-view");
  440. holder.innerHTML = "";
  441. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  442. const label = document.createElement("div");
  443. label.classList.add("options-label");
  444. label.innerText = val.name;
  445. holder.appendChild(label);
  446. const row = document.createElement("div");
  447. row.classList.add("options-row");
  448. holder.appendChild(row);
  449. const input = document.createElement("input");
  450. input.classList.add("options-field-numeric");
  451. input.id = "options-view-" + key + "-input";
  452. input.setAttribute("type", "number");
  453. input.setAttribute("min", 1);
  454. input.value = entity.views[view][key].value;
  455. const select = document.createElement("select");
  456. select.id = "options-view-" + key + "-select"
  457. unitChoices[val.type].forEach(name => {
  458. const option = document.createElement("option");
  459. option.innerText = name;
  460. select.appendChild(option);
  461. });
  462. input.addEventListener("input", e => {
  463. const value = input.value == 0 ? 1 : input.value;
  464. entity.views[view][key] = math.unit(value, select.value);
  465. entity.dirty = true;
  466. if (config.autoFit) {
  467. fitWorld();
  468. } else {
  469. updateSizes(true);
  470. }
  471. updateEntityOptions(entity, view);
  472. updateViewOptions(entity, view, key);
  473. });
  474. select.setAttribute("oldUnit", select.value);
  475. // TODO does this ever cause a change in the world?
  476. select.addEventListener("input", e => {
  477. const value = input.value == 0 ? 1 : input.value;
  478. const oldUnit = select.getAttribute("oldUnit");
  479. entity.views[view][key] = math.unit(value, oldUnit).to(select.value);
  480. entity.dirty = true;
  481. input.value = entity.views[view][key].toNumber(select.value);
  482. select.setAttribute("oldUnit", select.value);
  483. if (config.autoFit) {
  484. fitWorld();
  485. } else {
  486. updateSizes(true);
  487. }
  488. updateEntityOptions(entity, view);
  489. updateViewOptions(entity, view, key);
  490. });
  491. row.appendChild(input);
  492. row.appendChild(select);
  493. });
  494. }
  495. function updateViewOptions(entity, view, changed) {
  496. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  497. if (key != changed) {
  498. const input = document.querySelector("#options-view-" + key + "-input");
  499. const select = document.querySelector("#options-view-" + key + "-select");
  500. const currentUnit = select.value;
  501. const convertedAmount = entity.views[view][key].toNumber(currentUnit);
  502. input.value = math.round(convertedAmount, 5);
  503. }
  504. });
  505. }
  506. function getSortedEntities() {
  507. return Object.keys(entities).sort((a, b) => {
  508. const entA = entities[a];
  509. const entB = entities[b];
  510. const viewA = entA.view;
  511. const viewB = entB.view;
  512. const heightA = entA.views[viewA].height.to("meter").value;
  513. const heightB = entB.views[viewB].height.to("meter").value;
  514. return heightA - heightB;
  515. });
  516. }
  517. function clearViewOptions() {
  518. const holder = document.querySelector("#options-view");
  519. holder.innerHTML = "";
  520. }
  521. // this is a crime against humanity, and also stolen from
  522. // stack overflow
  523. // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
  524. const testCanvas = document.createElement("canvas");
  525. testCanvas.id = "test-canvas";
  526. const testCtx = testCanvas.getContext("2d");
  527. function testClick(event) {
  528. // oh my god I can't believe I'm doing this
  529. const target = event.target;
  530. if (navigator.userAgent.indexOf("Firefox") != -1) {
  531. clickDown(target.parentElement, event.clientX, event.clientY);
  532. return;
  533. }
  534. // Get click coordinates
  535. let w = target.width;
  536. let h = target.height;
  537. let ratioW = 1, ratioH = 1;
  538. // Limit the size of the canvas so that very large images don't cause problems)
  539. if (w > 1000) {
  540. ratioW = w / 1000;
  541. w /= ratioW;
  542. h /= ratioW;
  543. }
  544. if (h > 1000) {
  545. ratioH = h / 1000;
  546. w /= ratioH;
  547. h /= ratioH;
  548. }
  549. const ratio = ratioW * ratioH;
  550. var x = event.clientX - target.getBoundingClientRect().x,
  551. y = event.clientY - target.getBoundingClientRect().y,
  552. alpha;
  553. testCtx.canvas.width = w;
  554. testCtx.canvas.height = h;
  555. // Draw image to canvas
  556. // and read Alpha channel value
  557. testCtx.drawImage(target, 0, 0, w, h);
  558. alpha = testCtx.getImageData(Math.floor(x / ratio), Math.floor(y / ratio), 1, 1).data[3]; // [0]R [1]G [2]B [3]A
  559. // If pixel is transparent,
  560. // retrieve the element underneath and trigger it's click event
  561. if (alpha === 0) {
  562. const oldDisplay = target.style.display;
  563. target.style.display = "none";
  564. const newTarget = document.elementFromPoint(event.clientX, event.clientY);
  565. newTarget.dispatchEvent(new MouseEvent(event.type, {
  566. "clientX": event.clientX,
  567. "clientY": event.clientY
  568. }));
  569. target.style.display = oldDisplay;
  570. } else {
  571. clickDown(target.parentElement, event.clientX, event.clientY);
  572. }
  573. }
  574. function arrangeEntities(order) {
  575. let x = 0.1;
  576. order.forEach(key => {
  577. document.querySelector("#entity-" + key).dataset.x = x;
  578. x += 0.8 / (order.length - 1);
  579. });
  580. updateSizes();
  581. }
  582. function removeAllEntities() {
  583. Object.keys(entities).forEach(key => {
  584. removeEntity(document.querySelector("#entity-" + key));
  585. });
  586. }
  587. function clearAttribution() {
  588. document.querySelector("#options-attribution").style.display = "none";
  589. }
  590. function displayAttribution(file) {
  591. document.querySelector("#options-attribution").style.display = "inline";
  592. const authors = authorsOfFull(file);
  593. const owners = ownersOfFull(file);
  594. const source = sourceOf(file);
  595. const authorHolder = document.querySelector("#options-attribution-authors");
  596. const ownerHolder = document.querySelector("#options-attribution-owners");
  597. const sourceHolder = document.querySelector("#options-attribution-source");
  598. if (authors === []) {
  599. const div = document.createElement("div");
  600. div.innerText = "Unknown";
  601. authorHolder.innerHTML = "";
  602. authorHolder.appendChild(div);
  603. console.warn("No authors: " + file);
  604. } else if (authors === undefined) {
  605. const div = document.createElement("div");
  606. div.innerText = "Not yet entered";
  607. authorHolder.innerHTML = "";
  608. authorHolder.appendChild(div);
  609. console.warn("No authors: " + file);
  610. } else {
  611. authorHolder.innerHTML = "";
  612. const list = document.createElement("ul");
  613. authorHolder.appendChild(list);
  614. authors.forEach(author => {
  615. const authorEntry = document.createElement("li");
  616. if (author.url) {
  617. const link = document.createElement("a");
  618. link.href = author.url;
  619. link.innerText = author.name;
  620. authorEntry.appendChild(link);
  621. } else {
  622. const div = document.createElement("div");
  623. div.innerText = author.name;
  624. authorEntry.appendChild(div);
  625. }
  626. list.appendChild(authorEntry);
  627. });
  628. }
  629. if (owners === []) {
  630. const div = document.createElement("div");
  631. div.innerText = "Unknown";
  632. ownerHolder.innerHTML = "";
  633. ownerHolder.appendChild(div);
  634. } else if (owners === undefined) {
  635. const div = document.createElement("div");
  636. div.innerText = "Not yet entered";
  637. ownerHolder.innerHTML = "";
  638. ownerHolder.appendChild(div);
  639. console.warn("No owners: " + file);
  640. } else {
  641. ownerHolder.innerHTML = "";
  642. const list = document.createElement("ul");
  643. ownerHolder.appendChild(list);
  644. owners.forEach(owner => {
  645. const ownerEntry = document.createElement("li");
  646. if (owner.url) {
  647. const link = document.createElement("a");
  648. link.href = owner.url;
  649. link.innerText = owner.name;
  650. ownerEntry.appendChild(link);
  651. } else {
  652. const div = document.createElement("div");
  653. div.innerText = owner.name;
  654. ownerEntry.appendChild(div);
  655. }
  656. list.appendChild(ownerEntry);
  657. });
  658. }
  659. if (source === null) {
  660. const div = document.createElement("div");
  661. div.innerText = "No link";
  662. sourceHolder.innerHTML = "";
  663. sourceHolder.appendChild(div);
  664. } else if (source === undefined) {
  665. const div = document.createElement("div");
  666. div.innerText = "Not yet entered";
  667. sourceHolder.innerHTML = "";
  668. sourceHolder.appendChild(div);
  669. } else {
  670. sourceHolder.innerHTML = "";
  671. const link = document.createElement("a");
  672. link.style.display = "block";
  673. link.href = source;
  674. link.innerText = new URL(source).host;
  675. sourceHolder.appendChild(link);
  676. }
  677. }
  678. function removeEntity(element) {
  679. if (selected == element) {
  680. deselect();
  681. }
  682. delete entities[element.dataset.key];
  683. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  684. bottomName.parentElement.removeChild(bottomName);
  685. element.parentElement.removeChild(element);
  686. }
  687. function displayEntity(entity, view, x, y, selectEntity=false) {
  688. const box = document.createElement("div");
  689. box.classList.add("entity-box");
  690. const img = document.createElement("img");
  691. img.classList.add("entity-image");
  692. img.addEventListener("dragstart", e => {
  693. e.preventDefault();
  694. });
  695. const nameTag = document.createElement("div");
  696. nameTag.classList.add("entity-name");
  697. nameTag.innerText = entity.name;
  698. box.appendChild(img);
  699. box.appendChild(nameTag);
  700. const image = entity.views[view].image;
  701. img.src = image.source;
  702. displayAttribution(image.source);
  703. if (image.bottom !== undefined) {
  704. img.style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  705. } else {
  706. img.style.setProperty("--offset", ((-1) * 100) + "%")
  707. }
  708. box.dataset.x = x;
  709. box.dataset.y = y;
  710. img.addEventListener("mousedown", e => { testClick(e); e.stopPropagation() });
  711. img.addEventListener("touchstart", e => {
  712. const fakeEvent = {
  713. target: e.target,
  714. clientX: e.touches[0].clientX,
  715. clientY: e.touches[0].clientY
  716. };
  717. testClick(fakeEvent);
  718. });
  719. const heightBar = document.createElement("div");
  720. heightBar.classList.add("height-bar");
  721. box.appendChild(heightBar);
  722. box.id = "entity-" + entityIndex;
  723. box.dataset.key = entityIndex;
  724. entity.view = view;
  725. entity.priority = 0;
  726. entities[entityIndex] = entity;
  727. entity.index = entityIndex;
  728. const world = document.querySelector("#entities");
  729. world.appendChild(box);
  730. const bottomName = document.createElement("div");
  731. bottomName.classList.add("bottom-name");
  732. bottomName.id = "bottom-name-" + entityIndex;
  733. bottomName.innerText = entity.name;
  734. bottomName.addEventListener("click", () => select(box));
  735. world.appendChild(bottomName);
  736. entityIndex += 1;
  737. if (config.autoFit) {
  738. fitWorld();
  739. }
  740. if (selectEntity)
  741. select(box);
  742. entity.dirty = true;
  743. updateSizes(true);
  744. }
  745. window.onblur = function () {
  746. altHeld = false;
  747. shiftHeld = false;
  748. }
  749. window.onfocus = function () {
  750. window.dispatchEvent(new Event("keydown"));
  751. }
  752. function doSliderScale() {
  753. setWorldHeight(config.height, math.multiply(config.height, (9 + sliderScale) / 10));
  754. }
  755. function doSliderEntityScale() {
  756. if (selected) {
  757. const entity = entities[selected.dataset.key];
  758. entity.scale *= (9 + sliderEntityScale) / 10;
  759. entity.dirty = true;
  760. updateSizes(true);
  761. updateEntityOptions(entity, entity.view);
  762. updateViewOptions(entity, entity.view);
  763. }
  764. }
  765. document.addEventListener("DOMContentLoaded", () => {
  766. prepareEntities();
  767. document.querySelector("#options-world-show-names").addEventListener("input", e => {
  768. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-entity-name");
  769. });
  770. document.querySelector("#options-world-show-bottom-names").addEventListener("input", e => {
  771. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-bottom-name");
  772. });
  773. document.querySelector("#options-world-show-height-bars").addEventListener("input", e => {
  774. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-height-bars");
  775. });
  776. document.querySelector("#options-world-show-entity-glow").addEventListener("input", e => {
  777. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-entity-glow");
  778. });
  779. document.querySelector("#options-order-forward").addEventListener("click", e => {
  780. if (selected) {
  781. entities[selected.dataset.key].priority += 1;
  782. }
  783. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  784. updateSizes();
  785. });
  786. document.querySelector("#options-order-back").addEventListener("click", e => {
  787. if (selected) {
  788. entities[selected.dataset.key].priority -= 1;
  789. }
  790. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  791. updateSizes();
  792. });
  793. document.querySelector("#slider-scale").addEventListener("mousedown", e => {
  794. dragScaleHandle = setInterval(doSliderScale, 50);
  795. e.stopPropagation();
  796. });
  797. document.querySelector("#slider-scale").addEventListener("touchstart", e => {
  798. dragScaleHandle = setInterval(doSliderScale, 50);
  799. e.stopPropagation();
  800. });
  801. document.querySelector("#slider-scale").addEventListener("input", e => {
  802. const val = Number(e.target.value);
  803. if (val < 1) {
  804. sliderScale = (val + 1) / 2;
  805. } else {
  806. sliderScale = val;
  807. }
  808. });
  809. document.querySelector("#slider-scale").addEventListener("change", e => {
  810. clearInterval(dragScaleHandle);
  811. dragScaleHandle = null;
  812. e.target.value = 1;
  813. });
  814. document.querySelector("#slider-entity-scale").addEventListener("mousedown", e => {
  815. dragEntityScaleHandle = setInterval(doSliderEntityScale, 50);
  816. e.stopPropagation();
  817. });
  818. document.querySelector("#slider-entity-scale").addEventListener("touchstart", e => {
  819. dragEntityScaleHandle = setInterval(doSliderEntityScale, 50);
  820. e.stopPropagation();
  821. });
  822. document.querySelector("#slider-entity-scale").addEventListener("input", e => {
  823. const val = Number(e.target.value);
  824. if (val < 1) {
  825. sliderEntityScale = (val + 1) / 2;
  826. } else {
  827. sliderEntityScale = val;
  828. }
  829. });
  830. document.querySelector("#slider-entity-scale").addEventListener("change", e => {
  831. clearInterval(dragEntityScaleHandle);
  832. dragEntityScaleHandle = null;
  833. e.target.value = 1;
  834. });
  835. const sceneChoices = document.querySelector("#scene-choices");
  836. Object.entries(scenes).forEach(([id, scene]) => {
  837. const option = document.createElement("option");
  838. option.innerText = id;
  839. option.value = id;
  840. sceneChoices.appendChild(option);
  841. });
  842. document.querySelector("#load-scene").addEventListener("click", e => {
  843. const chosen = sceneChoices.value;
  844. removeAllEntities();
  845. scenes[chosen]();
  846. });
  847. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  848. canvasWidth = document.querySelector("#display").clientWidth - 100;
  849. canvasHeight = document.querySelector("#display").clientHeight - 50;
  850. document.querySelector("#open-help").addEventListener("click", e => {
  851. document.querySelector("#help").classList.add("visible");
  852. });
  853. document.querySelector("#close-help").addEventListener("click", e => {
  854. document.querySelector("#help").classList.remove("visible");
  855. });
  856. const unitSelector = document.querySelector("#options-height-unit");
  857. unitChoices.length.forEach(lengthOption => {
  858. const option = document.createElement("option");
  859. option.innerText = lengthOption;
  860. option.value = lengthOption;
  861. if (lengthOption === "meters") {
  862. option.selected = true;
  863. }
  864. unitSelector.appendChild(option);
  865. });
  866. param = new URL(window.location.href).searchParams.get("scene");
  867. if (param === null)
  868. scenes["Default"]();
  869. else {
  870. try {
  871. const data = JSON.parse(b64DecodeUnicode(param));
  872. if (data.entities === undefined) {
  873. return;
  874. }
  875. if (data.world === undefined) {
  876. return;
  877. }
  878. importScene(data);
  879. } catch (err) {
  880. console.error(err);
  881. scenes["Default"]();
  882. // probably wasn't valid data
  883. }
  884. }
  885. document.querySelector("#world").addEventListener("wheel", e => {
  886. if (shiftHeld) {
  887. const dir = e.deltaY > 0 ? 0.9 : 1.1;
  888. if (selected) {
  889. const entity = entities[selected.dataset.key];
  890. entity.views[entity.view].height = math.multiply(entity.views[entity.view].height, dir);
  891. entity.dirty = true;
  892. updateEntityOptions(entity, entity.view);
  893. updateViewOptions(entity, entity.view);
  894. updateSizes(true);
  895. }
  896. } else {
  897. const dir = e.deltaY < 0 ? 0.9 : 1.1;
  898. setWorldHeight(config.height, math.multiply(config.height, dir));
  899. updateWorldOptions();
  900. }
  901. checkFitWorld();
  902. })
  903. document.querySelector("body").appendChild(testCtx.canvas);
  904. updateSizes();
  905. document.querySelector("#options-height-value").addEventListener("input", e => {
  906. updateWorldHeight();
  907. })
  908. unitSelector.addEventListener("input", e => {
  909. checkFitWorld();
  910. updateWorldHeight();
  911. })
  912. world.addEventListener("mousedown", e => deselect());
  913. document.querySelector("#display").addEventListener("mousedown", deselect);
  914. document.addEventListener("mouseup", e => clickUp(e));
  915. document.addEventListener("touchend", e => {
  916. const fakeEvent = {
  917. target: e.target,
  918. clientX: e.changedTouches[0].clientX,
  919. clientY: e.changedTouches[0].clientY
  920. };
  921. clickUp(fakeEvent);
  922. });
  923. document.querySelector("#entity-view").addEventListener("input", e => {
  924. const entity = entities[selected.dataset.key];
  925. entity.view = e.target.value;
  926. const image = entities[selected.dataset.key].views[e.target.value].image;
  927. selected.querySelector(".entity-image").src = image.source;
  928. displayAttribution(image.source);
  929. if (image.bottom !== undefined) {
  930. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  931. } else {
  932. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1) * 100) + "%")
  933. }
  934. updateSizes();
  935. updateEntityOptions(entities[selected.dataset.key], e.target.value);
  936. updateViewOptions(entities[selected.dataset.key], e.target.value);
  937. });
  938. clearViewList();
  939. document.querySelector("#menu-clear").addEventListener("click", e => {
  940. removeAllEntities();
  941. });
  942. document.querySelector("#menu-order-height").addEventListener("click", e => {
  943. const order = Object.keys(entities).sort((a, b) => {
  944. const entA = entities[a];
  945. const entB = entities[b];
  946. const viewA = entA.view;
  947. const viewB = entB.view;
  948. const heightA = entA.views[viewA].height.to("meter").value;
  949. const heightB = entB.views[viewB].height.to("meter").value;
  950. return heightA - heightB;
  951. });
  952. arrangeEntities(order);
  953. });
  954. document.querySelector("#options-world-fit").addEventListener("click", () => fitWorld(true));
  955. document.querySelector("#options-world-autofit").addEventListener("input", e => {
  956. config.autoFit = e.target.checked;
  957. if (config.autoFit) {
  958. fitWorld();
  959. }
  960. });
  961. document.querySelector("#options-world-autofit-mode").addEventListener("input", e => {
  962. config.autoFitMode = e.target.value;
  963. if (config.autoFit) {
  964. fitWorld();
  965. }
  966. })
  967. document.addEventListener("keydown", e => {
  968. if (e.key == "Delete") {
  969. if (selected) {
  970. removeEntity(selected);
  971. selected = null;
  972. }
  973. }
  974. })
  975. document.addEventListener("keydown", e => {
  976. if (e.key == "Shift") {
  977. shiftHeld = true;
  978. e.preventDefault();
  979. } else if (e.key == "Alt") {
  980. altHeld = true;
  981. e.preventDefault();
  982. }
  983. });
  984. document.addEventListener("keyup", e => {
  985. if (e.key == "Shift") {
  986. shiftHeld = false;
  987. e.preventDefault();
  988. } else if (e.key == "Alt") {
  989. altHeld = false;
  990. e.preventDefault();
  991. }
  992. });
  993. document.addEventListener("paste", e => {
  994. try {
  995. const data = JSON.parse(e.clipboardData.getData("text"));
  996. if (data.entities === undefined) {
  997. return;
  998. }
  999. if (data.world === undefined) {
  1000. return;
  1001. }
  1002. importScene(data);
  1003. } catch (err) {
  1004. console.error(err);
  1005. // probably wasn't valid data
  1006. }
  1007. });
  1008. document.querySelector("#menu-permalink").addEventListener("click", e => {
  1009. linkScene();
  1010. });
  1011. document.querySelector("#menu-export").addEventListener("click", e => {
  1012. copyScene();
  1013. });
  1014. document.querySelector("#menu-save").addEventListener("click", e => {
  1015. saveScene();
  1016. });
  1017. document.querySelector("#menu-load").addEventListener("click", e => {
  1018. loadScene();
  1019. });
  1020. });
  1021. function prepareEntities() {
  1022. availableEntities["buildings"] = makeBuildings();
  1023. availableEntities["landmarks"] = makeLandmarks();
  1024. availableEntities["characters"] = makeCharacters();
  1025. availableEntities["objects"] = makeObjects();
  1026. availableEntities["food"] = makeFood();
  1027. availableEntities["naturals"] = makeNaturals();
  1028. availableEntities["vehicles"] = makeVehicles();
  1029. availableEntities["cities"] = makeCities();
  1030. availableEntities["pokemon"] = makePokemon();
  1031. availableEntities["characters"].sort((x, y) => {
  1032. return x.name.toLowerCase() < y.name.toLowerCase() ? -1 : 1
  1033. });
  1034. const holder = document.querySelector("#spawners");
  1035. const categorySelect = document.createElement("select");
  1036. categorySelect.id = "category-picker";
  1037. holder.appendChild(categorySelect);
  1038. Object.entries(availableEntities).forEach(([category, entityList]) => {
  1039. const select = document.createElement("select");
  1040. select.id = "create-entity-" + category;
  1041. for (let i = 0; i < entityList.length; i++) {
  1042. const entity = entityList[i];
  1043. const option = document.createElement("option");
  1044. option.value = i;
  1045. option.innerText = entity.name;
  1046. select.appendChild(option);
  1047. availableEntitiesByName[entity.name] = entity;
  1048. };
  1049. const button = document.createElement("button");
  1050. button.id = "create-entity-" + category + "-button";
  1051. button.innerText = "Create";
  1052. button.addEventListener("click", e => {
  1053. const newEntity = entityList[select.value].constructor()
  1054. displayEntity(newEntity, newEntity.defaultView, 0.5, 1, true);
  1055. });
  1056. const categoryOption = document.createElement("option");
  1057. categoryOption.value = category
  1058. categoryOption.innerText = category;
  1059. if (category == "characters") {
  1060. categoryOption.selected = true;
  1061. select.classList.add("category-visible");
  1062. button.classList.add("category-visible");
  1063. }
  1064. categorySelect.appendChild(categoryOption);
  1065. holder.appendChild(select);
  1066. holder.appendChild(button);
  1067. });
  1068. categorySelect.addEventListener("input", e => {
  1069. const oldSelect = document.querySelector("select.category-visible");
  1070. oldSelect.classList.remove("category-visible");
  1071. const oldButton = document.querySelector("button.category-visible");
  1072. oldButton.classList.remove("category-visible");
  1073. const newSelect = document.querySelector("#create-entity-" + e.target.value);
  1074. newSelect.classList.add("category-visible");
  1075. const newButton = document.querySelector("#create-entity-" + e.target.value + "-button");
  1076. newButton.classList.add("category-visible");
  1077. });
  1078. }
  1079. window.addEventListener("resize", () => {
  1080. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  1081. canvasWidth = document.querySelector("#display").clientWidth - 100;
  1082. canvasHeight = document.querySelector("#display").clientHeight - 50;
  1083. updateSizes();
  1084. })
  1085. document.addEventListener("mousemove", (e) => {
  1086. if (clicked) {
  1087. const position = snapRel(abs2rel({ x: e.clientX - dragOffsetX, y: e.clientY - dragOffsetY }));
  1088. clicked.dataset.x = position.x;
  1089. clicked.dataset.y = position.y;
  1090. updateEntityElement(entities[clicked.dataset.key], clicked);
  1091. if (hoveringInDeleteArea(e)) {
  1092. document.querySelector("#menubar").classList.add("hover-delete");
  1093. } else {
  1094. document.querySelector("#menubar").classList.remove("hover-delete");
  1095. }
  1096. }
  1097. });
  1098. document.addEventListener("touchmove", (e) => {
  1099. if (clicked) {
  1100. e.preventDefault();
  1101. let x = e.touches[0].clientX;
  1102. let y = e.touches[0].clientY;
  1103. const position = snapRel(abs2rel({ x: x - dragOffsetX, y: y - dragOffsetY }));
  1104. clicked.dataset.x = position.x;
  1105. clicked.dataset.y = position.y;
  1106. updateEntityElement(entities[clicked.dataset.key], clicked);
  1107. // what a hack
  1108. // I should centralize this 'fake event' creation...
  1109. if (hoveringInDeleteArea({ clientY: y })) {
  1110. document.querySelector("#menubar").classList.add("hover-delete");
  1111. } else {
  1112. document.querySelector("#menubar").classList.remove("hover-delete");
  1113. }
  1114. }
  1115. }, { passive: false });
  1116. function checkFitWorld() {
  1117. if (config.autoFit) {
  1118. fitWorld();
  1119. return true;
  1120. }
  1121. return false;
  1122. }
  1123. const fitModes = {
  1124. "max": {
  1125. start: 0,
  1126. binop: math.max,
  1127. final: (total, count) => total
  1128. },
  1129. "arithmetic mean": {
  1130. start: 0,
  1131. binop: math.add,
  1132. final: (total, count) => total / count
  1133. },
  1134. "geometric mean": {
  1135. start: 1,
  1136. binop: math.multiply,
  1137. final: (total, count) => math.pow(total, 1 / count)
  1138. }
  1139. }
  1140. function fitWorld(manual=false, factor=1.1) {
  1141. const fitMode = fitModes[config.autoFitMode]
  1142. let max = fitMode.start
  1143. let count = 0;
  1144. Object.entries(entities).forEach(([key, entity]) => {
  1145. const view = entity.view;
  1146. let extra = entity.views[view].image.extra;
  1147. extra = extra === undefined ? 1 : extra;
  1148. max = fitMode.binop(max, math.multiply(extra, entity.views[view].height.toNumber("meter")));
  1149. count += 1;
  1150. });
  1151. max = fitMode.final(max, count)
  1152. max = math.unit(max, "meter")
  1153. if (manual)
  1154. altHeld = true;
  1155. setWorldHeight(config.height, math.multiply(max, factor));
  1156. if (manual)
  1157. altHeld = false;
  1158. }
  1159. function updateWorldHeight() {
  1160. const unit = document.querySelector("#options-height-unit").value;
  1161. const value = Math.max(0.000000001, document.querySelector("#options-height-value").value);
  1162. const oldHeight = config.height;
  1163. setWorldHeight(oldHeight, math.unit(value, unit));
  1164. }
  1165. function setWorldHeight(oldHeight, newHeight) {
  1166. config.height = newHeight.to(document.querySelector("#options-height-unit").value)
  1167. const unit = document.querySelector("#options-height-unit").value;
  1168. document.querySelector("#options-height-value").value = config.height.toNumber(unit);
  1169. Object.entries(entities).forEach(([key, entity]) => {
  1170. const element = document.querySelector("#entity-" + key);
  1171. let newPosition;
  1172. if (!altHeld) {
  1173. newPosition = adjustAbs({ x: element.dataset.x, y: element.dataset.y }, oldHeight, config.height);
  1174. } else {
  1175. newPosition = { x: element.dataset.x, y: element.dataset.y };
  1176. }
  1177. element.dataset.x = newPosition.x;
  1178. element.dataset.y = newPosition.y;
  1179. });
  1180. updateSizes();
  1181. }
  1182. function loadScene() {
  1183. try {
  1184. const data = JSON.parse(localStorage.getItem("macrovision-save"));
  1185. importScene(data);
  1186. } catch (err) {
  1187. alert("Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error.")
  1188. console.error(err);
  1189. }
  1190. }
  1191. function saveScene() {
  1192. try {
  1193. const string = JSON.stringify(exportScene());
  1194. localStorage.setItem("macrovision-save", string);
  1195. } catch (err) {
  1196. alert("Something went wrong while saving (maybe I don't have localStorage permissions, or exporting failed). Check the F12 console for the error.")
  1197. console.error(err);
  1198. }
  1199. }
  1200. function exportScene() {
  1201. const results = {};
  1202. results.entities = [];
  1203. Object.entries(entities).forEach(([key, entity]) => {
  1204. const element = document.querySelector("#entity-" + key);
  1205. results.entities.push({
  1206. name: entity.identifier,
  1207. scale: entity.scale,
  1208. view: entity.view,
  1209. x: element.dataset.x,
  1210. y: element.dataset.y
  1211. });
  1212. });
  1213. const unit = document.querySelector("#options-height-unit").value;
  1214. results.world = {
  1215. height: config.height.toNumber(unit),
  1216. unit: unit
  1217. }
  1218. return results;
  1219. }
  1220. // btoa doesn't like anything that isn't ASCII
  1221. // great
  1222. // thanks to https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
  1223. // for providing an alternative
  1224. function b64EncodeUnicode(str) {
  1225. // first we use encodeURIComponent to get percent-encoded UTF-8,
  1226. // then we convert the percent encodings into raw bytes which
  1227. // can be fed into btoa.
  1228. return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
  1229. function toSolidBytes(match, p1) {
  1230. return String.fromCharCode('0x' + p1);
  1231. }));
  1232. }
  1233. function b64DecodeUnicode(str) {
  1234. // Going backwards: from bytestream, to percent-encoding, to original string.
  1235. return decodeURIComponent(atob(str).split('').map(function(c) {
  1236. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  1237. }).join(''));
  1238. }
  1239. function linkScene() {
  1240. loc = new URL(window.location);
  1241. window.location = loc.protocol + "//" + loc.host + loc.pathname + "?scene=" + b64EncodeUnicode(JSON.stringify(exportScene()));
  1242. }
  1243. function copyScene() {
  1244. const results = exportScene();
  1245. navigator.clipboard.writeText(JSON.stringify(results))
  1246. alert("Scene copied to clipboard. Paste text into the page to load the scene.");
  1247. }
  1248. // TODO - don't just search through every single entity
  1249. // probably just have a way to do lookups directly
  1250. function findEntity(name) {
  1251. return availableEntitiesByName[name];
  1252. }
  1253. function importScene(data) {
  1254. removeAllEntities();
  1255. data.entities.forEach(entityInfo => {
  1256. const entity = findEntity(entityInfo.name).constructor();
  1257. entity.scale = entityInfo.scale
  1258. displayEntity(entity, entityInfo.view, entityInfo.x, entityInfo.y);
  1259. });
  1260. config.height = math.unit(data.world.height, data.world.unit);
  1261. document.querySelector("#options-height-unit").value = data.world.unit;
  1262. updateSizes();
  1263. }