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.
 
 
 

2436 lignes
73 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. let scrollDirection = 0;
  19. let scrollHandle = null;
  20. let zoomDirection = 0;
  21. let zoomHandle = null;
  22. let sizeDirection = 0;
  23. let sizeHandle = null;
  24. let worldSizeDirty = false;
  25. math.createUnit("humans", {
  26. definition: "5.75 feet"
  27. });
  28. math.createUnit("story", {
  29. definition: "12 feet",
  30. prefixes: "long"
  31. });
  32. math.createUnit("stories", {
  33. definition: "12 feet",
  34. prefixes: "long"
  35. });
  36. math.createUnit("earths", {
  37. definition: "12756km",
  38. prefixes: "long"
  39. });
  40. math.createUnit("parsec", {
  41. definition: "3.086e16 meters",
  42. prefixes: "long"
  43. })
  44. math.createUnit("parsecs", {
  45. definition: "3.086e16 meters",
  46. prefixes: "long"
  47. })
  48. math.createUnit("lightyears", {
  49. definition: "9.461e15 meters",
  50. prefixes: "long"
  51. })
  52. math.createUnit("AU", {
  53. definition: "149597870700 meters"
  54. })
  55. math.createUnit("AUs", {
  56. definition: "149597870700 meters"
  57. })
  58. math.createUnit("dalton", {
  59. definition: "1.66e-27 kg",
  60. prefixes: "long"
  61. });
  62. math.createUnit("daltons", {
  63. definition: "1.66e-27 kg",
  64. prefixes: "long"
  65. });
  66. math.createUnit("solarradii", {
  67. definition: "695990 km",
  68. prefixes: "long"
  69. });
  70. math.createUnit("solarmasses", {
  71. definition: "2e30 kg",
  72. prefixes: "long"
  73. });
  74. math.createUnit("galaxy", {
  75. definition: "105700 lightyears",
  76. prefixes: "long"
  77. });
  78. math.createUnit("galaxies", {
  79. definition: "105700 lightyears",
  80. prefixes: "long"
  81. });
  82. math.createUnit("universe", {
  83. definition: "93.016e9 lightyears",
  84. prefixes: "long"
  85. });
  86. math.createUnit("universes", {
  87. definition: "93.016e9 lightyears",
  88. prefixes: "long"
  89. });
  90. math.createUnit("multiverse", {
  91. definition: "1e30 lightyears",
  92. prefixes: "long"
  93. });
  94. math.createUnit("multiverses", {
  95. definition: "1e30 lightyears",
  96. prefixes: "long"
  97. });
  98. const unitChoices = {
  99. length: [
  100. "meters",
  101. "angstroms",
  102. "millimeters",
  103. "centimeters",
  104. "kilometers",
  105. "inches",
  106. "feet",
  107. "humans",
  108. "stories",
  109. "miles",
  110. "earths",
  111. "solarradii",
  112. "AUs",
  113. "lightyears",
  114. "parsecs",
  115. "galaxies",
  116. "universes",
  117. "multiverses"
  118. ],
  119. area: [
  120. "meters^2",
  121. "cm^2",
  122. "kilometers^2",
  123. "acres",
  124. "miles^2"
  125. ],
  126. mass: [
  127. "kilograms",
  128. "milligrams",
  129. "grams",
  130. "tonnes",
  131. "lbs",
  132. "ounces",
  133. "tons"
  134. ]
  135. }
  136. const config = {
  137. height: math.unit(1500, "meters"),
  138. minLineSize: 100,
  139. maxLineSize: 150,
  140. autoFit: false,
  141. autoFitMode: "max"
  142. }
  143. const availableEntities = {
  144. }
  145. const availableEntitiesByName = {
  146. }
  147. const entities = {
  148. }
  149. function constrainRel(coords) {
  150. if (altHeld) {
  151. return coords;
  152. }
  153. return {
  154. x: Math.min(Math.max(coords.x, 0), 1),
  155. y: Math.min(Math.max(coords.y, 0), 1)
  156. }
  157. }
  158. function snapRel(coords) {
  159. return constrainRel({
  160. x: coords.x,
  161. y: altHeld ? coords.y : (Math.abs(coords.y - 1) < 0.05 ? 1 : coords.y)
  162. });
  163. }
  164. function adjustAbs(coords, oldHeight, newHeight) {
  165. const ratio = math.divide(oldHeight, newHeight);
  166. return { x: 0.5 + (coords.x - 0.5) * math.divide(oldHeight, newHeight), y: 1 + (coords.y - 1) * math.divide(oldHeight, newHeight) };
  167. }
  168. function rel2abs(coords) {
  169. return { x: coords.x * canvasWidth + 50, y: coords.y * canvasHeight };
  170. }
  171. function abs2rel(coords) {
  172. return { x: (coords.x - 50) / canvasWidth, y: coords.y / canvasHeight };
  173. }
  174. function updateEntityElement(entity, element) {
  175. const position = rel2abs({ x: element.dataset.x, y: element.dataset.y });
  176. const view = entity.view;
  177. element.style.left = position.x + "px";
  178. element.style.top = position.y + "px";
  179. element.style.setProperty("--xpos", position.x + "px");
  180. element.style.setProperty("--entity-height", "'" + entity.views[view].height.to(config.height.units[0].unit.name).format({ precision: 2 }) + "'");
  181. const pixels = math.divide(entity.views[view].height, config.height) * (canvasHeight - 50);
  182. const extra = entity.views[view].image.extra;
  183. const bottom = entity.views[view].image.bottom;
  184. const bonus = (extra ? extra : 1) * (1 / (1 - (bottom ? bottom : 0)));
  185. element.style.setProperty("--height", pixels * bonus + "px");
  186. element.style.setProperty("--extra", pixels * bonus - pixels + "px");
  187. if (entity.views[view].rename)
  188. element.querySelector(".entity-name").innerText = entity.name == "" ? "" : entity.views[view].name;
  189. else
  190. element.querySelector(".entity-name").innerText = entity.name;
  191. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  192. bottomName.style.left = position.x + entityX + "px";
  193. bottomName.style.bottom = "0vh";
  194. bottomName.innerText = entity.name;
  195. const topName = document.querySelector("#top-name-" + element.dataset.key);
  196. topName.style.left = position.x + entityX + "px";
  197. topName.style.top = "20vh";
  198. topName.innerText = entity.name;
  199. if (entity.views[view].height.toNumber("meters") / 10 > config.height.toNumber("meters")) {
  200. topName.classList.add("top-name-needed");
  201. } else {
  202. topName.classList.remove("top-name-needed");
  203. }
  204. }
  205. function updateSizes(dirtyOnly = false) {
  206. drawScale(dirtyOnly);
  207. let ordered = Object.entries(entities);
  208. ordered.sort((e1, e2) => {
  209. if (e1[1].priority != e2[1].priority) {
  210. return e2[1].priority - e1[1].priority;
  211. } else {
  212. return e1[1].views[e1[1].view].height.value - e2[1].views[e2[1].view].height.value
  213. }
  214. });
  215. let zIndex = ordered.length;
  216. ordered.forEach(entity => {
  217. const element = document.querySelector("#entity-" + entity[0]);
  218. element.style.zIndex = zIndex;
  219. if (!dirtyOnly || entity[1].dirty) {
  220. updateEntityElement(entity[1], element, zIndex);
  221. entity[1].dirty = false;
  222. }
  223. zIndex -= 1;
  224. });
  225. }
  226. function drawScale(ifDirty = false) {
  227. if (ifDirty && !worldSizeDirty)
  228. return;
  229. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  230. let total = heightPer.clone();
  231. total.value = 0;
  232. for (let y = ctx.canvas.clientHeight - 50; y >= 50; y -= pixelsPer) {
  233. drawTick(ctx, 50, y, total);
  234. total = math.add(total, heightPer);
  235. }
  236. }
  237. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, value) {
  238. const oldStroke = ctx.strokeStyle;
  239. const oldFill = ctx.fillStyle;
  240. ctx.beginPath();
  241. ctx.moveTo(x, y);
  242. ctx.lineTo(x + 20, y);
  243. ctx.strokeStyle = "#000000";
  244. ctx.stroke();
  245. ctx.beginPath();
  246. ctx.moveTo(x + 20, y);
  247. ctx.lineTo(ctx.canvas.clientWidth - 70, y);
  248. ctx.strokeStyle = "#aaaaaa";
  249. ctx.stroke();
  250. ctx.beginPath();
  251. ctx.moveTo(ctx.canvas.clientWidth - 70, y);
  252. ctx.lineTo(ctx.canvas.clientWidth - 50, y);
  253. ctx.strokeStyle = "#000000";
  254. ctx.stroke();
  255. const oldFont = ctx.font;
  256. ctx.font = 'normal 24pt coda';
  257. ctx.fillStyle = "#dddddd";
  258. ctx.beginPath();
  259. ctx.fillText(value.format({ precision: 3 }), x + 20, y + 35);
  260. ctx.font = oldFont;
  261. ctx.strokeStyle = oldStroke;
  262. ctx.fillStyle = oldFill;
  263. }
  264. const canvas = document.querySelector("#display");
  265. /** @type {CanvasRenderingContext2D} */
  266. const ctx = canvas.getContext("2d");
  267. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
  268. heightPer = 1;
  269. if (pixelsPer < config.minLineSize) {
  270. const factor = math.ceil(config.minLineSize / pixelsPer);
  271. heightPer *= factor;
  272. pixelsPer *= factor;
  273. }
  274. if (pixelsPer > config.maxLineSize) {
  275. const factor = math.ceil(pixelsPer / config.maxLineSize);
  276. heightPer /= factor;
  277. pixelsPer /= factor;
  278. }
  279. heightPer = math.unit(heightPer, config.height.units[0].unit.name)
  280. ctx.scale(1, 1);
  281. ctx.canvas.width = canvas.clientWidth;
  282. ctx.canvas.height = canvas.clientHeight;
  283. ctx.beginPath();
  284. ctx.rect(0, 0, canvas.width, canvas.height);
  285. ctx.fillStyle = "#333";
  286. ctx.fill();
  287. ctx.beginPath();
  288. ctx.moveTo(50, 50);
  289. ctx.lineTo(50, ctx.canvas.clientHeight - 50);
  290. ctx.stroke();
  291. ctx.beginPath();
  292. ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
  293. ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
  294. ctx.stroke();
  295. drawTicks(ctx, pixelsPer, heightPer);
  296. }
  297. // Entities are generated as needed, and we make a copy
  298. // every time - the resulting objects get mutated, after all.
  299. // But we also want to be able to read some information without
  300. // calling the constructor -- e.g. making a list of authors and
  301. // owners. So, this function is used to generate that information.
  302. // It is invoked like makeEntity so that it can be dropped in easily,
  303. // but returns an object that lets you construct many copies of an entity,
  304. // rather than creating a new entity.
  305. function createEntityMaker(info, views, sizes) {
  306. const maker = {};
  307. maker.name = info.name;
  308. maker.constructor = () => makeEntity(info, views, sizes);
  309. maker.authors = [];
  310. maker.owners = [];
  311. maker.nsfw = false;
  312. Object.values(views).forEach(view => {
  313. const authors = authorsOf(view.image.source);
  314. if (authors) {
  315. authors.forEach(author => {
  316. if (maker.authors.indexOf(author) == -1) {
  317. maker.authors.push(author);
  318. }
  319. });
  320. }
  321. const owners = ownersOf(view.image.source);
  322. if (owners) {
  323. owners.forEach(owner => {
  324. if (maker.owners.indexOf(owner) == -1) {
  325. maker.owners.push(owner);
  326. }
  327. });
  328. }
  329. if (isNsfw(view.image.source)) {
  330. maker.nsfw = true;
  331. }
  332. });
  333. return maker;
  334. }
  335. // This function serializes and parses its arguments to avoid sharing
  336. // references to a common object. This allows for the objects to be
  337. // safely mutated.
  338. function makeEntity(info, views, sizes) {
  339. const entityTemplate = {
  340. name: info.name,
  341. identifier: info.name,
  342. scale: 1,
  343. info: JSON.parse(JSON.stringify(info)),
  344. views: JSON.parse(JSON.stringify(views), math.reviver),
  345. sizes: sizes === undefined ? [] : JSON.parse(JSON.stringify(sizes), math.reviver),
  346. init: function () {
  347. const entity = this;
  348. Object.entries(this.views).forEach(([viewKey, view]) => {
  349. view.parent = this;
  350. if (this.defaultView === undefined) {
  351. this.defaultView = viewKey;
  352. this.view = viewKey;
  353. }
  354. Object.entries(view.attributes).forEach(([key, val]) => {
  355. Object.defineProperty(
  356. view,
  357. key,
  358. {
  359. get: function () {
  360. return math.multiply(Math.pow(this.parent.scale, this.attributes[key].power), this.attributes[key].base);
  361. },
  362. set: function (value) {
  363. const newScale = Math.pow(math.divide(value, this.attributes[key].base), 1 / this.attributes[key].power);
  364. this.parent.scale = newScale;
  365. }
  366. }
  367. )
  368. });
  369. });
  370. this.sizes.forEach(size => {
  371. if (size.default === true) {
  372. this.views[this.defaultView].height = size.height;
  373. this.size = size;
  374. }
  375. });
  376. if (this.size === undefined && this.sizes.length > 0) {
  377. this.views[this.defaultView].height = this.sizes[0].height;
  378. this.size = this.sizes[0];
  379. console.warn("No default size set for " + info.name);
  380. } else if (this.sizes.length == 0) {
  381. this.sizes = [
  382. {
  383. name: "Normal",
  384. height: this.views[this.defaultView].height
  385. }
  386. ];
  387. this.size = this.sizes[0];
  388. }
  389. this.desc = {};
  390. Object.entries(this.info).forEach(([key, value]) => {
  391. Object.defineProperty(
  392. this.desc,
  393. key,
  394. {
  395. get: function () {
  396. let text = value.text;
  397. if (entity.views[entity.view].info) {
  398. if (entity.views[entity.view].info[key]) {
  399. text = combineInfo(text, entity.views[entity.view].info[key]);
  400. }
  401. }
  402. if (entity.size.info) {
  403. if (entity.size.info[key]) {
  404. text = combineInfo(text, entity.size.info[key]);
  405. }
  406. }
  407. return { title: value.title, text: text };
  408. }
  409. }
  410. )
  411. });
  412. delete this.init;
  413. return this;
  414. }
  415. }.init();
  416. return entityTemplate;
  417. }
  418. function combineInfo(existing, next) {
  419. switch (next.mode) {
  420. case "replace":
  421. return next.text;
  422. case "prepend":
  423. return next.text + existing;
  424. case "append":
  425. return existing + next.text;
  426. }
  427. return existing;
  428. }
  429. function clickDown(target, x, y) {
  430. clicked = target;
  431. const rect = target.getBoundingClientRect();
  432. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  433. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  434. dragOffsetX = x - rect.left + entX;
  435. dragOffsetY = y - rect.top + entY;
  436. clickTimeout = setTimeout(() => { dragging = true }, 200)
  437. target.classList.add("no-transition");
  438. }
  439. // could we make this actually detect the menu area?
  440. function hoveringInDeleteArea(e) {
  441. return e.clientY < document.body.clientHeight / 10;
  442. }
  443. function clickUp(e) {
  444. clearTimeout(clickTimeout);
  445. if (clicked) {
  446. if (dragging) {
  447. dragging = false;
  448. if (hoveringInDeleteArea(e)) {
  449. removeEntity(clicked);
  450. document.querySelector("#menubar").classList.remove("hover-delete");
  451. }
  452. } else {
  453. select(clicked);
  454. }
  455. clicked.classList.remove("no-transition");
  456. clicked = null;
  457. }
  458. }
  459. function deselect() {
  460. if (selected) {
  461. selected.classList.remove("selected");
  462. }
  463. document.getElementById("options-selected-entity-none").selected = "selected";
  464. clearAttribution();
  465. selected = null;
  466. clearViewList();
  467. clearEntityOptions();
  468. clearViewOptions();
  469. document.querySelector("#delete-entity").disabled = true;
  470. document.querySelector("#grow").disabled = true;
  471. document.querySelector("#shrink").disabled = true;
  472. document.querySelector("#fit").disabled = true;
  473. }
  474. function select(target) {
  475. deselect();
  476. selected = target;
  477. selectedEntity = entities[target.dataset.key];
  478. document.getElementById("options-selected-entity-" + target.dataset.key).selected = "selected";
  479. selected.classList.add("selected");
  480. displayAttribution(selectedEntity.views[selectedEntity.view].image.source);
  481. configViewList(selectedEntity, selectedEntity.view);
  482. configEntityOptions(selectedEntity, selectedEntity.view);
  483. configViewOptions(selectedEntity, selectedEntity.view);
  484. document.querySelector("#delete-entity").disabled = false;
  485. document.querySelector("#grow").disabled = false;
  486. document.querySelector("#shrink").disabled = false;
  487. document.querySelector("#fit").disabled = false;
  488. }
  489. function configViewList(entity, selectedView) {
  490. const list = document.querySelector("#entity-view");
  491. list.innerHTML = "";
  492. list.style.display = "block";
  493. Object.keys(entity.views).forEach(view => {
  494. const option = document.createElement("option");
  495. option.innerText = entity.views[view].name;
  496. option.value = view;
  497. if (isNsfw(entity.views[view].image.source)) {
  498. option.classList.add("nsfw")
  499. }
  500. if (view === selectedView) {
  501. option.selected = true;
  502. if (option.classList.contains("nsfw")) {
  503. list.classList.add("nsfw");
  504. } else {
  505. list.classList.remove("nsfw");
  506. }
  507. }
  508. list.appendChild(option);
  509. });
  510. list.addEventListener("change", e => {
  511. if (list.options[list.selectedIndex].classList.contains("nsfw")) {
  512. list.classList.add("nsfw");
  513. } else {
  514. list.classList.remove("nsfw");
  515. }
  516. })
  517. }
  518. function clearViewList() {
  519. const list = document.querySelector("#entity-view");
  520. list.innerHTML = "";
  521. list.style.display = "none";
  522. }
  523. function updateWorldOptions(entity, view) {
  524. const heightInput = document.querySelector("#options-height-value");
  525. const heightSelect = document.querySelector("#options-height-unit");
  526. const converted = config.height.toNumber(heightSelect.value);
  527. setNumericInput(heightInput, converted);
  528. }
  529. function configEntityOptions(entity, view) {
  530. const holder = document.querySelector("#options-entity");
  531. document.querySelector("#entity-category-header").style.display = "block";
  532. document.querySelector("#entity-category").style.display = "block";
  533. holder.innerHTML = "";
  534. const scaleLabel = document.createElement("div");
  535. scaleLabel.classList.add("options-label");
  536. scaleLabel.innerText = "Scale";
  537. const scaleRow = document.createElement("div");
  538. scaleRow.classList.add("options-row");
  539. const scaleInput = document.createElement("input");
  540. scaleInput.classList.add("options-field-numeric");
  541. scaleInput.id = "options-entity-scale";
  542. scaleInput.addEventListener("change", e => {
  543. entity.scale = e.target.value == 0 ? 1 : e.target.value;
  544. entity.dirty = true;
  545. if (config.autoFit) {
  546. fitWorld();
  547. } else {
  548. updateSizes(true);
  549. }
  550. updateEntityOptions(entity, view);
  551. updateViewOptions(entity, view);
  552. });
  553. scaleInput.addEventListener("keydown", e => {
  554. e.stopPropagation();
  555. })
  556. scaleInput.setAttribute("min", 1);
  557. scaleInput.setAttribute("type", "number");
  558. setNumericInput(scaleInput, entity.scale);
  559. scaleRow.appendChild(scaleInput);
  560. holder.appendChild(scaleLabel);
  561. holder.appendChild(scaleRow);
  562. const nameLabel = document.createElement("div");
  563. nameLabel.classList.add("options-label");
  564. nameLabel.innerText = "Name";
  565. const nameRow = document.createElement("div");
  566. nameRow.classList.add("options-row");
  567. const nameInput = document.createElement("input");
  568. nameInput.classList.add("options-field-text");
  569. nameInput.value = entity.name;
  570. nameInput.addEventListener("input", e => {
  571. entity.name = e.target.value;
  572. entity.dirty = true;
  573. updateSizes(true);
  574. })
  575. nameInput.addEventListener("keydown", e => {
  576. e.stopPropagation();
  577. })
  578. nameRow.appendChild(nameInput);
  579. holder.appendChild(nameLabel);
  580. holder.appendChild(nameRow);
  581. const defaultHolder = document.querySelector("#options-entity-defaults");
  582. defaultHolder.innerHTML = "";
  583. entity.sizes.forEach(defaultInfo => {
  584. const button = document.createElement("button");
  585. button.classList.add("options-button");
  586. button.innerText = defaultInfo.name;
  587. button.addEventListener("click", e => {
  588. entity.views[entity.defaultView].height = defaultInfo.height;
  589. entity.dirty = true;
  590. updateEntityOptions(entity, entity.view);
  591. updateViewOptions(entity, entity.view);
  592. if (!checkFitWorld()) {
  593. updateSizes(true);
  594. }
  595. });
  596. defaultHolder.appendChild(button);
  597. });
  598. document.querySelector("#options-order-display").innerText = entity.priority;
  599. document.querySelector("#options-ordering").style.display = "flex";
  600. }
  601. function updateEntityOptions(entity, view) {
  602. const scaleInput = document.querySelector("#options-entity-scale");
  603. setNumericInput(scaleInput, entity.scale);
  604. document.querySelector("#options-order-display").innerText = entity.priority;
  605. }
  606. function clearEntityOptions() {
  607. document.querySelector("#entity-category-header").style.display = "none";
  608. document.querySelector("#entity-category").style.display = "none";
  609. /*
  610. const holder = document.querySelector("#options-entity");
  611. holder.innerHTML = "";
  612. document.querySelector("#options-entity-defaults").innerHTML = "";
  613. document.querySelector("#options-ordering").style.display = "none";
  614. document.querySelector("#options-ordering").style.display = "none";*/
  615. }
  616. function configViewOptions(entity, view) {
  617. const holder = document.querySelector("#options-view");
  618. document.querySelector("#view-category-header").style.display = "block";
  619. document.querySelector("#view-category").style.display = "block";
  620. holder.innerHTML = "";
  621. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  622. const label = document.createElement("div");
  623. label.classList.add("options-label");
  624. label.innerText = val.name;
  625. holder.appendChild(label);
  626. const row = document.createElement("div");
  627. row.classList.add("options-row");
  628. holder.appendChild(row);
  629. const input = document.createElement("input");
  630. input.classList.add("options-field-numeric");
  631. input.id = "options-view-" + key + "-input";
  632. input.setAttribute("type", "number");
  633. input.setAttribute("min", 1);
  634. setNumericInput(input, entity.views[view][key].value);
  635. const select = document.createElement("select");
  636. select.classList.add("options-field-unit");
  637. select.id = "options-view-" + key + "-select"
  638. unitChoices[val.type].forEach(name => {
  639. const option = document.createElement("option");
  640. option.innerText = name;
  641. select.appendChild(option);
  642. });
  643. input.addEventListener("change", e => {
  644. const value = input.value == 0 ? 1 : input.value;
  645. entity.views[view][key] = math.unit(value, select.value);
  646. entity.dirty = true;
  647. if (config.autoFit) {
  648. fitWorld();
  649. } else {
  650. updateSizes(true);
  651. }
  652. updateEntityOptions(entity, view);
  653. updateViewOptions(entity, view, key);
  654. });
  655. input.addEventListener("keydown", e => {
  656. e.stopPropagation();
  657. })
  658. select.setAttribute("oldUnit", select.value);
  659. // TODO does this ever cause a change in the world?
  660. select.addEventListener("input", e => {
  661. const value = input.value == 0 ? 1 : input.value;
  662. const oldUnit = select.getAttribute("oldUnit");
  663. entity.views[entity.view][key] = math.unit(value, oldUnit).to(select.value);
  664. entity.dirty = true;
  665. setNumericInput(input, entity.views[entity.view][key].toNumber(select.value));
  666. select.setAttribute("oldUnit", select.value);
  667. if (config.autoFit) {
  668. fitWorld();
  669. } else {
  670. updateSizes(true);
  671. }
  672. updateEntityOptions(entity, view);
  673. updateViewOptions(entity, view, key);
  674. });
  675. row.appendChild(input);
  676. row.appendChild(select);
  677. });
  678. }
  679. function updateViewOptions(entity, view, changed) {
  680. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  681. if (key != changed) {
  682. const input = document.querySelector("#options-view-" + key + "-input");
  683. const select = document.querySelector("#options-view-" + key + "-select");
  684. const currentUnit = select.value;
  685. const convertedAmount = entity.views[view][key].toNumber(currentUnit);
  686. setNumericInput(input, convertedAmount);
  687. }
  688. });
  689. }
  690. function setNumericInput(input, value, round = 3) {
  691. input.value = math.round(value, round);
  692. }
  693. function getSortedEntities() {
  694. return Object.keys(entities).sort((a, b) => {
  695. const entA = entities[a];
  696. const entB = entities[b];
  697. const viewA = entA.view;
  698. const viewB = entB.view;
  699. const heightA = entA.views[viewA].height.to("meter").value;
  700. const heightB = entB.views[viewB].height.to("meter").value;
  701. return heightA - heightB;
  702. });
  703. }
  704. function clearViewOptions() {
  705. document.querySelector("#view-category-header").style.display = "none";
  706. document.querySelector("#view-category").style.display = "none";
  707. }
  708. // this is a crime against humanity, and also stolen from
  709. // stack overflow
  710. // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
  711. const testCanvas = document.createElement("canvas");
  712. testCanvas.id = "test-canvas";
  713. const testCtx = testCanvas.getContext("2d");
  714. function testClick(event) {
  715. // oh my god I can't believe I'm doing this
  716. const target = event.target;
  717. if (navigator.userAgent.indexOf("Firefox") != -1) {
  718. clickDown(target.parentElement, event.clientX, event.clientY);
  719. return;
  720. }
  721. // Get click coordinates
  722. let w = target.width;
  723. let h = target.height;
  724. let ratioW = 1, ratioH = 1;
  725. // Limit the size of the canvas so that very large images don't cause problems)
  726. if (w > 1000) {
  727. ratioW = w / 1000;
  728. w /= ratioW;
  729. h /= ratioW;
  730. }
  731. if (h > 1000) {
  732. ratioH = h / 1000;
  733. w /= ratioH;
  734. h /= ratioH;
  735. }
  736. const ratio = ratioW * ratioH;
  737. var x = event.clientX - target.getBoundingClientRect().x,
  738. y = event.clientY - target.getBoundingClientRect().y,
  739. alpha;
  740. testCtx.canvas.width = w;
  741. testCtx.canvas.height = h;
  742. // Draw image to canvas
  743. // and read Alpha channel value
  744. testCtx.drawImage(target, 0, 0, w, h);
  745. alpha = testCtx.getImageData(Math.floor(x / ratio), Math.floor(y / ratio), 1, 1).data[3]; // [0]R [1]G [2]B [3]A
  746. // If pixel is transparent,
  747. // retrieve the element underneath and trigger its click event
  748. if (alpha === 0) {
  749. const oldDisplay = target.style.display;
  750. target.style.display = "none";
  751. const newTarget = document.elementFromPoint(event.clientX, event.clientY);
  752. newTarget.dispatchEvent(new MouseEvent(event.type, {
  753. "clientX": event.clientX,
  754. "clientY": event.clientY
  755. }));
  756. target.style.display = oldDisplay;
  757. } else {
  758. clickDown(target.parentElement, event.clientX, event.clientY);
  759. }
  760. }
  761. function arrangeEntities(order) {
  762. let x = 0.1;
  763. order.forEach(key => {
  764. document.querySelector("#entity-" + key).dataset.x = x;
  765. x += 0.8 / (order.length - 1);
  766. });
  767. updateSizes();
  768. }
  769. function removeAllEntities() {
  770. Object.keys(entities).forEach(key => {
  771. removeEntity(document.querySelector("#entity-" + key));
  772. });
  773. }
  774. function clearAttribution() {
  775. document.querySelector("#attribution-category-header").style.display = "none";
  776. document.querySelector("#options-attribution").style.display = "none";
  777. }
  778. function displayAttribution(file) {
  779. document.querySelector("#attribution-category-header").style.display = "block";
  780. document.querySelector("#options-attribution").style.display = "inline";
  781. const authors = authorsOfFull(file);
  782. const owners = ownersOfFull(file);
  783. const source = sourceOf(file);
  784. const authorHolder = document.querySelector("#options-attribution-authors");
  785. const ownerHolder = document.querySelector("#options-attribution-owners");
  786. const sourceHolder = document.querySelector("#options-attribution-source");
  787. if (authors === []) {
  788. const div = document.createElement("div");
  789. div.innerText = "Unknown";
  790. authorHolder.innerHTML = "";
  791. authorHolder.appendChild(div);
  792. } else if (authors === undefined) {
  793. const div = document.createElement("div");
  794. div.innerText = "Not yet entered";
  795. authorHolder.innerHTML = "";
  796. authorHolder.appendChild(div);
  797. } else {
  798. authorHolder.innerHTML = "";
  799. const list = document.createElement("ul");
  800. authorHolder.appendChild(list);
  801. authors.forEach(author => {
  802. const authorEntry = document.createElement("li");
  803. if (author.url) {
  804. const link = document.createElement("a");
  805. link.href = author.url;
  806. link.innerText = author.name;
  807. authorEntry.appendChild(link);
  808. } else {
  809. const div = document.createElement("div");
  810. div.innerText = author.name;
  811. authorEntry.appendChild(div);
  812. }
  813. list.appendChild(authorEntry);
  814. });
  815. }
  816. if (owners === []) {
  817. const div = document.createElement("div");
  818. div.innerText = "Unknown";
  819. ownerHolder.innerHTML = "";
  820. ownerHolder.appendChild(div);
  821. } else if (owners === undefined) {
  822. const div = document.createElement("div");
  823. div.innerText = "Not yet entered";
  824. ownerHolder.innerHTML = "";
  825. ownerHolder.appendChild(div);
  826. } else {
  827. ownerHolder.innerHTML = "";
  828. const list = document.createElement("ul");
  829. ownerHolder.appendChild(list);
  830. owners.forEach(owner => {
  831. const ownerEntry = document.createElement("li");
  832. if (owner.url) {
  833. const link = document.createElement("a");
  834. link.href = owner.url;
  835. link.innerText = owner.name;
  836. ownerEntry.appendChild(link);
  837. } else {
  838. const div = document.createElement("div");
  839. div.innerText = owner.name;
  840. ownerEntry.appendChild(div);
  841. }
  842. list.appendChild(ownerEntry);
  843. });
  844. }
  845. if (source === null) {
  846. const div = document.createElement("div");
  847. div.innerText = "No link";
  848. sourceHolder.innerHTML = "";
  849. sourceHolder.appendChild(div);
  850. } else if (source === undefined) {
  851. const div = document.createElement("div");
  852. div.innerText = "Not yet entered";
  853. sourceHolder.innerHTML = "";
  854. sourceHolder.appendChild(div);
  855. } else {
  856. sourceHolder.innerHTML = "";
  857. const link = document.createElement("a");
  858. link.style.display = "block";
  859. link.href = source;
  860. link.innerText = new URL(source).host;
  861. sourceHolder.appendChild(link);
  862. }
  863. }
  864. function removeEntity(element) {
  865. if (selected == element) {
  866. deselect();
  867. }
  868. const option = document.querySelector("#options-selected-entity-" + element.dataset.key);
  869. option.parentElement.removeChild(option);
  870. delete entities[element.dataset.key];
  871. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  872. const topName = document.querySelector("#top-name-" + element.dataset.key);
  873. bottomName.parentElement.removeChild(bottomName);
  874. topName.parentElement.removeChild(topName);
  875. element.parentElement.removeChild(element);
  876. }
  877. function checkEntity(entity) {
  878. Object.values(entity.views).forEach(view => {
  879. if (authorsOf(view.image.source) === undefined) {
  880. console.warn("No authors: " + view.image.source);
  881. }
  882. });
  883. }
  884. function displayEntity(entity, view, x, y, selectEntity = false, refresh = false) {
  885. checkEntity(entity);
  886. const box = document.createElement("div");
  887. box.classList.add("entity-box");
  888. const img = document.createElement("img");
  889. img.classList.add("entity-image");
  890. img.addEventListener("dragstart", e => {
  891. e.preventDefault();
  892. });
  893. const nameTag = document.createElement("div");
  894. nameTag.classList.add("entity-name");
  895. nameTag.innerText = entity.name;
  896. box.appendChild(img);
  897. box.appendChild(nameTag);
  898. const image = entity.views[view].image;
  899. img.src = image.source;
  900. if (image.bottom !== undefined) {
  901. img.style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  902. } else {
  903. img.style.setProperty("--offset", ((-1) * 100) + "%")
  904. }
  905. box.dataset.x = x;
  906. box.dataset.y = y;
  907. img.addEventListener("mousedown", e => { testClick(e); e.stopPropagation() });
  908. img.addEventListener("touchstart", e => {
  909. const fakeEvent = {
  910. target: e.target,
  911. clientX: e.touches[0].clientX,
  912. clientY: e.touches[0].clientY
  913. };
  914. testClick(fakeEvent);
  915. });
  916. const heightBar = document.createElement("div");
  917. heightBar.classList.add("height-bar");
  918. box.appendChild(heightBar);
  919. box.id = "entity-" + entityIndex;
  920. box.dataset.key = entityIndex;
  921. entity.view = view;
  922. entity.priority = 0;
  923. entities[entityIndex] = entity;
  924. entity.index = entityIndex;
  925. const world = document.querySelector("#entities");
  926. world.appendChild(box);
  927. const bottomName = document.createElement("div");
  928. bottomName.classList.add("bottom-name");
  929. bottomName.id = "bottom-name-" + entityIndex;
  930. bottomName.innerText = entity.name;
  931. bottomName.addEventListener("click", () => select(box));
  932. world.appendChild(bottomName);
  933. const topName = document.createElement("div");
  934. topName.classList.add("top-name");
  935. topName.id = "top-name-" + entityIndex;
  936. topName.innerText = entity.name;
  937. topName.addEventListener("click", () => select(box));
  938. world.appendChild(topName);
  939. const entityOption = document.createElement("option");
  940. entityOption.id = "options-selected-entity-" + entityIndex;
  941. entityOption.value = entityIndex;
  942. entityOption.innerText = entity.name;
  943. document.getElementById("options-selected-entity").appendChild(entityOption);
  944. entityIndex += 1;
  945. if (config.autoFit) {
  946. fitWorld();
  947. }
  948. if (selectEntity)
  949. select(box);
  950. entity.dirty = true;
  951. if (refresh)
  952. updateSizes(true);
  953. }
  954. window.onblur = function () {
  955. altHeld = false;
  956. shiftHeld = false;
  957. }
  958. window.onfocus = function () {
  959. window.dispatchEvent(new Event("keydown"));
  960. }
  961. // thanks to https://developers.google.com/web/fundamentals/native-hardware/fullscreen
  962. function toggleFullScreen() {
  963. var doc = window.document;
  964. var docEl = doc.documentElement;
  965. var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  966. var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
  967. if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
  968. requestFullScreen.call(docEl);
  969. }
  970. else {
  971. cancelFullScreen.call(doc);
  972. }
  973. }
  974. function handleResize() {
  975. const oldCanvasWidth = canvasWidth;
  976. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  977. canvasWidth = document.querySelector("#display").clientWidth - 100;
  978. canvasHeight = document.querySelector("#display").clientHeight - 50;
  979. const change = oldCanvasWidth / canvasWidth;
  980. doHorizReposition(change);
  981. updateSizes();
  982. }
  983. function doHorizReposition(change) {
  984. Object.keys(entities).forEach(key => {
  985. const element = document.querySelector("#entity-" + key);
  986. const x = element.dataset.x;
  987. element.dataset.x = (x - 0.5) * change + 0.5;
  988. });
  989. }
  990. function prepareMenu() {
  991. const menubar = document.querySelector("#popout-menu");
  992. [
  993. {
  994. name: "Show/hide sidebar",
  995. id: "menu-toggle-sidebar",
  996. icon: "fas fa-chevron-circle-down",
  997. rotates: true
  998. },
  999. {
  1000. name: "Fullscreen",
  1001. id: "menu-fullscreen",
  1002. icon: "fas fa-compress"
  1003. },
  1004. {
  1005. name: "Clear",
  1006. id: "menu-clear",
  1007. icon: "fas fa-file"
  1008. },
  1009. {
  1010. name: "Sort by height",
  1011. id: "menu-order-height",
  1012. icon: "fas fa-sort-numeric-up"
  1013. },
  1014. {
  1015. name: "Permalink",
  1016. id: "menu-permalink",
  1017. icon: "fas fa-link"
  1018. },
  1019. {
  1020. name: "Export to clipboard",
  1021. id: "menu-export",
  1022. icon: "fas fa-share"
  1023. },
  1024. {
  1025. name: "Import from clipboard",
  1026. id: "menu-import",
  1027. icon: "fas fa-share",
  1028. classes: ["flipped"]
  1029. },
  1030. {
  1031. name: "Save",
  1032. id: "menu-save",
  1033. icon: "fas fa-download"
  1034. },
  1035. {
  1036. name: "Load",
  1037. id: "menu-load",
  1038. icon: "fas fa-upload"
  1039. },
  1040. {
  1041. name: "Load Autosave",
  1042. id: "menu-load-autosave",
  1043. icon: "fas fa-redo"
  1044. },
  1045. {
  1046. name: "Add Image",
  1047. id: "menu-add-image",
  1048. icon: "fas fa-camera"
  1049. }
  1050. ].forEach(entry => {
  1051. const buttonHolder = document.createElement("div");
  1052. buttonHolder.classList.add("menu-button-holder");
  1053. const button = document.createElement("button");
  1054. button.id = entry.id;
  1055. button.classList.add("menu-button");
  1056. const icon = document.createElement("i");
  1057. icon.classList.add(...entry.icon.split(" "));
  1058. if (entry.rotates) {
  1059. icon.classList.add("rotate-backward", "transitions");
  1060. }
  1061. if (entry.classes) {
  1062. entry.classes.forEach(cls => icon.classList.add(cls));
  1063. }
  1064. const actionText = document.createElement("span");
  1065. actionText.innerText = entry.name;
  1066. actionText.classList.add("menu-text");
  1067. const srText = document.createElement("span");
  1068. srText.classList.add("sr-only");
  1069. srText.innerText = entry.name;
  1070. button.appendChild(icon);
  1071. button.appendChild(srText);
  1072. buttonHolder.appendChild(button);
  1073. buttonHolder.appendChild(actionText);
  1074. menubar.appendChild(buttonHolder);
  1075. });
  1076. if (checkHelpDate()) {
  1077. document.querySelector("#open-help").classList.add("highlighted");
  1078. }
  1079. }
  1080. const lastHelpChange = 1587847743294;
  1081. function checkHelpDate() {
  1082. try {
  1083. const old = localStorage.getItem("help-viewed");
  1084. if (old === null || old < lastHelpChange) {
  1085. return true;
  1086. }
  1087. return false;
  1088. } catch {
  1089. console.warn("Could not set the help-viewed date");
  1090. return false;
  1091. }
  1092. }
  1093. function setHelpDate() {
  1094. try {
  1095. localStorage.setItem("help-viewed", Date.now());
  1096. } catch {
  1097. console.warn("Could not set the help-viewed date");
  1098. }
  1099. }
  1100. function doScroll() {
  1101. document.querySelectorAll(".entity-box").forEach(element => {
  1102. element.dataset.x = parseFloat(element.dataset.x) + scrollDirection / 180;
  1103. });
  1104. updateSizes();
  1105. scrollDirection *= 1.05;
  1106. }
  1107. function doZoom() {
  1108. const oldHeight = config.height;
  1109. setWorldHeight(oldHeight, math.multiply(oldHeight, 1 + zoomDirection / 10));
  1110. zoomDirection *= 1.05;
  1111. }
  1112. function doSize() {
  1113. if (selected) {
  1114. const entity = entities[selected.dataset.key];
  1115. const oldHeight = entity.views[entity.view].height;
  1116. entity.views[entity.view].height = math.multiply(oldHeight, 1 + sizeDirection / 20);
  1117. entity.dirty = true;
  1118. updateEntityOptions(entity, entity.view);
  1119. updateViewOptions(entity, entity.view);
  1120. updateSizes(true);
  1121. sizeDirection *= 1.05;
  1122. const ownHeight = entity.views[entity.view].height.toNumber("meters");
  1123. const worldHeight = config.height.toNumber("meters");
  1124. console.log(ownHeight, worldHeight)
  1125. if (ownHeight > worldHeight) {
  1126. setWorldHeight(config.height, entity.views[entity.view].height)
  1127. } else if (ownHeight * 10 < worldHeight) {
  1128. setWorldHeight(config.height, math.multiply(entity.views[entity.view].height, 10));
  1129. }
  1130. }
  1131. }
  1132. function prepareHelp() {
  1133. const toc = document.querySelector("#table-of-contents");
  1134. const holder = document.querySelector("#help-contents-holder");
  1135. document.querySelectorAll("#help-contents h2").forEach(header => {
  1136. const li = document.createElement("li");
  1137. li.innerText = header.textContent;
  1138. li.addEventListener("click", e => {
  1139. holder.scrollTop = header.offsetTop;
  1140. });
  1141. toc.appendChild(li);
  1142. });
  1143. }
  1144. document.addEventListener("DOMContentLoaded", () => {
  1145. prepareMenu();
  1146. prepareEntities();
  1147. prepareHelp();
  1148. document.querySelector("#open-help").addEventListener("click", e => {
  1149. setHelpDate();
  1150. document.querySelector("#help-menu").classList.add("visible");
  1151. document.querySelector("#open-help").classList.remove("highlighted");
  1152. });
  1153. document.querySelector("#close-help").addEventListener("click", e => {
  1154. document.querySelector("#help-menu").classList.remove("visible");
  1155. });
  1156. document.querySelector("#copy-screenshot").addEventListener("click", e => {
  1157. copyScreenshot();
  1158. toast("Copied to clipboard!");
  1159. });
  1160. document.querySelector("#save-screenshot").addEventListener("click", e => {
  1161. saveScreenshot();
  1162. });
  1163. document.querySelector("#toggle-menu").addEventListener("click", e => {
  1164. const popoutMenu = document.querySelector("#popout-menu");
  1165. if (popoutMenu.classList.contains("visible")) {
  1166. popoutMenu.classList.remove("visible");
  1167. } else {
  1168. const rect = e.target.getBoundingClientRect();
  1169. popoutMenu.classList.add("visible");
  1170. popoutMenu.style.left = rect.x + rect.width + 10 + "px";
  1171. popoutMenu.style.top = rect.y + rect.height + 10 + "px";
  1172. }
  1173. e.stopPropagation();
  1174. });
  1175. document.querySelector("#popout-menu").addEventListener("click", e => {
  1176. e.stopPropagation();
  1177. });
  1178. document.addEventListener("click", e => {
  1179. document.querySelector("#popout-menu").classList.remove("visible");
  1180. });
  1181. window.addEventListener("unload", () => saveScene("autosave"));
  1182. document.querySelector("#options-selected-entity").addEventListener("input", e => {
  1183. if (e.target.value == "none") {
  1184. deselect()
  1185. } else {
  1186. select(document.querySelector("#entity-" + e.target.value));
  1187. }
  1188. });
  1189. document.querySelector("#menu-toggle-sidebar").addEventListener("click", e => {
  1190. const sidebar = document.querySelector("#options");
  1191. if (sidebar.classList.contains("hidden")) {
  1192. sidebar.classList.remove("hidden");
  1193. e.target.classList.remove("rotate-forward");
  1194. e.target.classList.add("rotate-backward");
  1195. } else {
  1196. sidebar.classList.add("hidden");
  1197. e.target.classList.add("rotate-forward");
  1198. e.target.classList.remove("rotate-backward");
  1199. }
  1200. handleResize();
  1201. });
  1202. document.querySelector("#menu-fullscreen").addEventListener("click", toggleFullScreen);
  1203. document.querySelector("#options-show-extra").addEventListener("input", e => {
  1204. document.body.classList[e.target.checked ? "add" : "remove"]("show-extra-options");
  1205. });
  1206. document.querySelector("#options-world-show-names").addEventListener("input", e => {
  1207. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-entity-name");
  1208. });
  1209. document.querySelector("#options-world-show-bottom-names").addEventListener("input", e => {
  1210. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-bottom-name");
  1211. });
  1212. document.querySelector("#options-world-show-top-names").addEventListener("input", e => {
  1213. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-top-name");
  1214. });
  1215. document.querySelector("#options-world-show-height-bars").addEventListener("input", e => {
  1216. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-height-bars");
  1217. });
  1218. document.querySelector("#options-world-show-entity-glow").addEventListener("input", e => {
  1219. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-entity-glow");
  1220. });
  1221. document.querySelector("#options-world-show-bottom-cover").addEventListener("input", e => {
  1222. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-bottom-cover");
  1223. });
  1224. document.querySelector("#options-world-show-scale").addEventListener("input", e => {
  1225. document.body.classList[e.target.checked ? "add" : "remove"]("toggle-scale");
  1226. });
  1227. document.querySelector("#options-order-forward").addEventListener("click", e => {
  1228. if (selected) {
  1229. entities[selected.dataset.key].priority += 1;
  1230. }
  1231. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  1232. updateSizes();
  1233. });
  1234. document.querySelector("#options-order-back").addEventListener("click", e => {
  1235. if (selected) {
  1236. entities[selected.dataset.key].priority -= 1;
  1237. }
  1238. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  1239. updateSizes();
  1240. });
  1241. const sceneChoices = document.querySelector("#scene-choices");
  1242. Object.entries(scenes).forEach(([id, scene]) => {
  1243. const option = document.createElement("option");
  1244. option.innerText = id;
  1245. option.value = id;
  1246. sceneChoices.appendChild(option);
  1247. });
  1248. document.querySelector("#load-scene").addEventListener("click", e => {
  1249. const chosen = sceneChoices.value;
  1250. removeAllEntities();
  1251. scenes[chosen]();
  1252. });
  1253. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  1254. canvasWidth = document.querySelector("#display").clientWidth - 100;
  1255. canvasHeight = document.querySelector("#display").clientHeight - 50;
  1256. document.querySelector("#options-height-value").addEventListener("change", e => {
  1257. updateWorldHeight();
  1258. })
  1259. document.querySelector("#options-height-value").addEventListener("keydown", e => {
  1260. e.stopPropagation();
  1261. })
  1262. const unitSelector = document.querySelector("#options-height-unit");
  1263. unitChoices.length.forEach(lengthOption => {
  1264. const option = document.createElement("option");
  1265. option.innerText = lengthOption;
  1266. option.value = lengthOption;
  1267. if (lengthOption === "meters") {
  1268. option.selected = true;
  1269. }
  1270. unitSelector.appendChild(option);
  1271. });
  1272. unitSelector.setAttribute("oldUnit", "meters");
  1273. unitSelector.addEventListener("input", e => {
  1274. checkFitWorld();
  1275. const scaleInput = document.querySelector("#options-height-value");
  1276. const newVal = math.unit(scaleInput.value, unitSelector.getAttribute("oldUnit")).toNumber(e.target.value);
  1277. setNumericInput(scaleInput, newVal);
  1278. updateWorldHeight();
  1279. unitSelector.setAttribute("oldUnit", unitSelector.value);
  1280. });
  1281. param = new URL(window.location.href).searchParams.get("scene");
  1282. if (param === null) {
  1283. scenes["Default"]();
  1284. }
  1285. else {
  1286. try {
  1287. const data = JSON.parse(b64DecodeUnicode(param));
  1288. if (data.entities === undefined) {
  1289. return;
  1290. }
  1291. if (data.world === undefined) {
  1292. return;
  1293. }
  1294. importScene(data);
  1295. } catch (err) {
  1296. console.error(err);
  1297. scenes["Default"]();
  1298. // probably wasn't valid data
  1299. }
  1300. }
  1301. document.querySelector("#world").addEventListener("wheel", e => {
  1302. if (shiftHeld) {
  1303. if (selected) {
  1304. const dir = e.deltaY > 0 ? 10 / 11 : 11 / 10;
  1305. const entity = entities[selected.dataset.key];
  1306. entity.views[entity.view].height = math.multiply(entity.views[entity.view].height, dir);
  1307. entity.dirty = true;
  1308. updateEntityOptions(entity, entity.view);
  1309. updateViewOptions(entity, entity.view);
  1310. updateSizes(true);
  1311. } else {
  1312. document.querySelectorAll(".entity-box").forEach(element => {
  1313. element.dataset.x = parseFloat(element.dataset.x) + (e.deltaY < 0 ? 0.1 : -0.1);
  1314. });
  1315. updateSizes();
  1316. }
  1317. } else {
  1318. const dir = e.deltaY < 0 ? 10 / 11 : 11 / 10;
  1319. setWorldHeight(config.height, math.multiply(config.height, dir));
  1320. updateWorldOptions();
  1321. }
  1322. checkFitWorld();
  1323. })
  1324. document.querySelector("body").appendChild(testCtx.canvas);
  1325. updateSizes();
  1326. world.addEventListener("mousedown", e => deselect());
  1327. document.querySelector("#entities").addEventListener("mousedown", deselect);
  1328. document.querySelector("#display").addEventListener("mousedown", deselect);
  1329. document.addEventListener("mouseup", e => clickUp(e));
  1330. document.addEventListener("touchend", e => {
  1331. const fakeEvent = {
  1332. target: e.target,
  1333. clientX: e.changedTouches[0].clientX,
  1334. clientY: e.changedTouches[0].clientY
  1335. };
  1336. clickUp(fakeEvent);
  1337. });
  1338. document.querySelector("#entity-view").addEventListener("input", e => {
  1339. const entity = entities[selected.dataset.key];
  1340. entity.view = e.target.value;
  1341. const image = entities[selected.dataset.key].views[e.target.value].image;
  1342. selected.querySelector(".entity-image").src = image.source;
  1343. displayAttribution(image.source);
  1344. if (image.bottom !== undefined) {
  1345. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  1346. } else {
  1347. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1) * 100) + "%")
  1348. }
  1349. updateSizes();
  1350. updateEntityOptions(entities[selected.dataset.key], e.target.value);
  1351. updateViewOptions(entities[selected.dataset.key], e.target.value);
  1352. });
  1353. clearViewList();
  1354. document.querySelector("#menu-clear").addEventListener("click", e => {
  1355. removeAllEntities();
  1356. });
  1357. document.querySelector("#delete-entity").disabled = true;
  1358. document.querySelector("#delete-entity").addEventListener("click", e => {
  1359. if (selected) {
  1360. removeEntity(selected);
  1361. selected = null;
  1362. }
  1363. });
  1364. document.querySelector("#menu-order-height").addEventListener("click", e => {
  1365. const order = Object.keys(entities).sort((a, b) => {
  1366. const entA = entities[a];
  1367. const entB = entities[b];
  1368. const viewA = entA.view;
  1369. const viewB = entB.view;
  1370. const heightA = entA.views[viewA].height.to("meter").value;
  1371. const heightB = entB.views[viewB].height.to("meter").value;
  1372. return heightA - heightB;
  1373. });
  1374. arrangeEntities(order);
  1375. });
  1376. // TODO: write some generic logic for this lol
  1377. document.querySelector("#scroll-left").addEventListener("mousedown", e => {
  1378. scrollDirection = 1;
  1379. clearInterval(scrollHandle);
  1380. scrollHandle = setInterval(doScroll, 1000 / 20);
  1381. e.stopPropagation();
  1382. });
  1383. document.querySelector("#scroll-right").addEventListener("mousedown", e => {
  1384. scrollDirection = -1;
  1385. clearInterval(scrollHandle);
  1386. scrollHandle = setInterval(doScroll, 1000 / 20);
  1387. e.stopPropagation();
  1388. });
  1389. document.querySelector("#scroll-left").addEventListener("touchstart", e => {
  1390. scrollDirection = 1;
  1391. clearInterval(scrollHandle);
  1392. scrollHandle = setInterval(doScroll, 1000 / 20);
  1393. e.stopPropagation();
  1394. });
  1395. document.querySelector("#scroll-right").addEventListener("touchstart", e => {
  1396. scrollDirection = -1;
  1397. clearInterval(scrollHandle);
  1398. scrollHandle = setInterval(doScroll, 1000 / 20);
  1399. e.stopPropagation();
  1400. });
  1401. document.addEventListener("mouseup", e => {
  1402. clearInterval(scrollHandle);
  1403. scrollHandle = null;
  1404. });
  1405. document.addEventListener("touchend", e => {
  1406. clearInterval(scrollHandle);
  1407. scrollHandle = null;
  1408. });
  1409. document.querySelector("#zoom-in").addEventListener("mousedown", e => {
  1410. zoomDirection = -1;
  1411. clearInterval(zoomHandle);
  1412. zoomHandle = setInterval(doZoom, 1000 / 20);
  1413. e.stopPropagation();
  1414. });
  1415. document.querySelector("#zoom-out").addEventListener("mousedown", e => {
  1416. zoomDirection = 1;
  1417. clearInterval(zoomHandle);
  1418. zoomHandle = setInterval(doZoom, 1000 / 20);
  1419. e.stopPropagation();
  1420. });
  1421. document.querySelector("#zoom-in").addEventListener("touchstart", e => {
  1422. zoomDirection = -1;
  1423. clearInterval(zoomHandle);
  1424. zoomHandle = setInterval(doZoom, 1000 / 20);
  1425. e.stopPropagation();
  1426. });
  1427. document.querySelector("#zoom-out").addEventListener("touchstart", e => {
  1428. zoomDirection = 1;
  1429. clearInterval(zoomHandle);
  1430. zoomHandle = setInterval(doZoom, 1000 / 20);
  1431. e.stopPropagation();
  1432. });
  1433. document.addEventListener("mouseup", e => {
  1434. clearInterval(zoomHandle);
  1435. zoomHandle = null;
  1436. });
  1437. document.addEventListener("touchend", e => {
  1438. clearInterval(zoomHandle);
  1439. zoomHandle = null;
  1440. });
  1441. document.querySelector("#shrink").addEventListener("mousedown", e => {
  1442. sizeDirection = -1;
  1443. clearInterval(sizeHandle);
  1444. sizeHandle = setInterval(doSize, 1000 / 20);
  1445. e.stopPropagation();
  1446. });
  1447. document.querySelector("#grow").addEventListener("mousedown", e => {
  1448. sizeDirection = 1;
  1449. clearInterval(sizeHandle);
  1450. sizeHandle = setInterval(doSize, 1000 / 20);
  1451. e.stopPropagation();
  1452. });
  1453. document.querySelector("#shrink").addEventListener("touchstart", e => {
  1454. sizeDirection = -1;
  1455. clearInterval(sizeHandle);
  1456. sizeHandle = setInterval(doSize, 1000 / 20);
  1457. e.stopPropagation();
  1458. });
  1459. document.querySelector("#grow").addEventListener("touchstart", e => {
  1460. sizeDirection = 1;
  1461. clearInterval(sizeHandle);
  1462. sizeHandle = setInterval(doSize, 1000 / 20);
  1463. e.stopPropagation();
  1464. });
  1465. document.addEventListener("mouseup", e => {
  1466. clearInterval(sizeHandle);
  1467. sizeHandle = null;
  1468. });
  1469. document.addEventListener("touchend", e => {
  1470. clearInterval(sizeHandle);
  1471. sizeHandle = null;
  1472. });
  1473. document.querySelector("#fit").addEventListener("click", e => {
  1474. const x = parseFloat(selected.dataset.x);
  1475. Object.keys(entities).forEach(id => {
  1476. const element = document.querySelector("#entity-" + id);
  1477. const newX = parseFloat(element.dataset.x) - x + 0.5;
  1478. element.dataset.x = newX;
  1479. });
  1480. const entity = entities[selected.dataset.key];
  1481. const height = math.multiply(entity.views[entity.view].height, 1.1);
  1482. setWorldHeight(config.height, height);
  1483. });
  1484. document.querySelector("#fit").addEventListener("mousedown", e => {
  1485. e.stopPropagation();
  1486. });
  1487. document.querySelector("#fit").addEventListener("touchstart", e => {
  1488. e.stopPropagation();
  1489. });
  1490. document.querySelector("#options-world-fit").addEventListener("click", () => fitWorld(true));
  1491. document.querySelector("#options-world-autofit").addEventListener("input", e => {
  1492. config.autoFit = e.target.checked;
  1493. if (config.autoFit) {
  1494. fitWorld();
  1495. }
  1496. });
  1497. document.addEventListener("keydown", e => {
  1498. if (e.key == "Delete") {
  1499. if (selected) {
  1500. removeEntity(selected);
  1501. selected = null;
  1502. }
  1503. }
  1504. })
  1505. document.addEventListener("keydown", e => {
  1506. if (e.key == "Shift") {
  1507. shiftHeld = true;
  1508. e.preventDefault();
  1509. } else if (e.key == "Alt") {
  1510. altHeld = true;
  1511. e.preventDefault();
  1512. }
  1513. });
  1514. document.addEventListener("keyup", e => {
  1515. if (e.key == "Shift") {
  1516. shiftHeld = false;
  1517. e.preventDefault();
  1518. } else if (e.key == "Alt") {
  1519. altHeld = false;
  1520. e.preventDefault();
  1521. }
  1522. });
  1523. window.addEventListener("resize", handleResize);
  1524. // TODO: further investigate why the tool initially starts out with wrong
  1525. // values under certain circumstances (seems to be narrow aspect ratios -
  1526. // maybe the menu bar is animating when it shouldn't)
  1527. setTimeout(handleResize, 250);
  1528. setTimeout(handleResize, 500);
  1529. setTimeout(handleResize, 750);
  1530. setTimeout(handleResize, 1000);
  1531. document.querySelector("#menu-permalink").addEventListener("click", e => {
  1532. linkScene();
  1533. });
  1534. document.querySelector("#menu-export").addEventListener("click", e => {
  1535. copyScene();
  1536. });
  1537. document.querySelector("#menu-import").addEventListener("click", e => {
  1538. pasteScene();
  1539. });
  1540. document.querySelector("#menu-save").addEventListener("click", e => {
  1541. saveScene();
  1542. });
  1543. document.querySelector("#menu-load").addEventListener("click", e => {
  1544. loadScene();
  1545. });
  1546. document.querySelector("#menu-load-autosave").addEventListener("click", e => {
  1547. loadScene("autosave");
  1548. });
  1549. document.querySelector("#menu-add-image").addEventListener("click", e => {
  1550. document.querySelector("#file-upload-picker").click();
  1551. });
  1552. document.querySelector("#file-upload-picker").addEventListener("change", e => {
  1553. if (e.target.files.length > 0) {
  1554. for (let i=0; i<e.target.files.length; i++) {
  1555. customEntityFromFile(e.target.files[i]);
  1556. }
  1557. }
  1558. })
  1559. document.addEventListener("paste", e => {
  1560. let index = 0;
  1561. let item = null;
  1562. let found = false;
  1563. for (; index < e.clipboardData.items.length; index++) {
  1564. item = e.clipboardData.items[index];
  1565. if (item.type == "image/png") {
  1566. found = true;
  1567. break;
  1568. }
  1569. }
  1570. if (!found) {
  1571. return;
  1572. }
  1573. console.log(item)
  1574. console.log(item.type)
  1575. let url = null;
  1576. const file = item.getAsFile();
  1577. customEntityFromFile(file);
  1578. });
  1579. document.querySelector("#world").addEventListener("dragover", e => {
  1580. e.preventDefault();
  1581. })
  1582. document.querySelector("#world").addEventListener("drop", e => {
  1583. e.preventDefault();
  1584. if (e.dataTransfer.files.length > 0) {
  1585. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  1586. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  1587. let coords = abs2rel({x: e.clientX-entX, y: e.clientY-entY});
  1588. customEntityFromFile(e.dataTransfer.files[0], coords.x, coords.y);
  1589. }
  1590. })
  1591. clearEntityOptions();
  1592. clearViewOptions();
  1593. clearAttribution();
  1594. });
  1595. function customEntityFromFile(file, x=0.5, y=0.5) {
  1596. file.arrayBuffer().then(buf => {
  1597. arr = new Uint8Array(buf);
  1598. blob = new Blob([arr], {type: file.type });
  1599. url = window.URL.createObjectURL(blob)
  1600. makeCustomEntity(url, x, y);
  1601. });
  1602. }
  1603. function makeCustomEntity(url, x=0.5, y=0.5) {
  1604. const maker = createEntityMaker(
  1605. {
  1606. name: "Custom Entity"
  1607. },
  1608. {
  1609. custom: {
  1610. attributes: {
  1611. height: {
  1612. name: "Height",
  1613. power: 1,
  1614. type: "length",
  1615. base: math.unit(6, "feet")
  1616. }
  1617. },
  1618. image: {
  1619. source: url
  1620. },
  1621. name: "Image",
  1622. info: {},
  1623. rename: false
  1624. }
  1625. },
  1626. []
  1627. );
  1628. const entity = maker.constructor();
  1629. entity.scale = config.height.toNumber("feet") / 20;
  1630. entity.ephemeral = true;
  1631. displayEntity(entity, "custom", x, y, true, true);
  1632. }
  1633. function prepareEntities() {
  1634. availableEntities["buildings"] = makeBuildings();
  1635. availableEntities["characters"] = makeCharacters();
  1636. availableEntities["cities"] = makeCities();
  1637. availableEntities["fiction"] = makeFiction();
  1638. availableEntities["food"] = makeFood();
  1639. availableEntities["landmarks"] = makeLandmarks();
  1640. availableEntities["naturals"] = makeNaturals();
  1641. availableEntities["objects"] = makeObjects();
  1642. availableEntities["pokemon"] = makePokemon();
  1643. availableEntities["species"] = makeSpecies();
  1644. availableEntities["vehicles"] = makeVehicles();
  1645. availableEntities["characters"].sort((x, y) => {
  1646. return x.name.toLowerCase() < y.name.toLowerCase() ? -1 : 1
  1647. });
  1648. const holder = document.querySelector("#spawners");
  1649. const categorySelect = document.createElement("select");
  1650. categorySelect.id = "category-picker";
  1651. holder.appendChild(categorySelect);
  1652. Object.entries(availableEntities).forEach(([category, entityList]) => {
  1653. const select = document.createElement("select");
  1654. select.id = "create-entity-" + category;
  1655. for (let i = 0; i < entityList.length; i++) {
  1656. const entity = entityList[i];
  1657. const option = document.createElement("option");
  1658. option.value = i;
  1659. option.innerText = entity.name;
  1660. select.appendChild(option);
  1661. if (entity.nsfw) {
  1662. option.classList.add("nsfw");
  1663. }
  1664. availableEntitiesByName[entity.name] = entity;
  1665. };
  1666. select.addEventListener("change", e => {
  1667. if (select.options[select.selectedIndex].classList.contains("nsfw")) {
  1668. select.classList.add("nsfw");
  1669. } else {
  1670. select.classList.remove("nsfw");
  1671. }
  1672. })
  1673. const button = document.createElement("button");
  1674. button.id = "create-entity-" + category + "-button";
  1675. button.innerHTML = "<i class=\"far fa-plus-square\"></i>";
  1676. button.addEventListener("click", e => {
  1677. const newEntity = entityList[select.value].constructor()
  1678. displayEntity(newEntity, newEntity.defaultView, 0.5, 1, true, true);
  1679. });
  1680. const categoryOption = document.createElement("option");
  1681. categoryOption.value = category
  1682. categoryOption.innerText = category;
  1683. if (category == "characters") {
  1684. categoryOption.selected = true;
  1685. select.classList.add("category-visible");
  1686. button.classList.add("category-visible");
  1687. }
  1688. categorySelect.appendChild(categoryOption);
  1689. holder.appendChild(select);
  1690. holder.appendChild(button);
  1691. });
  1692. console.log("Loaded " + Object.keys(availableEntitiesByName).length + " entities");
  1693. categorySelect.addEventListener("input", e => {
  1694. const oldSelect = document.querySelector("select.category-visible");
  1695. oldSelect.classList.remove("category-visible");
  1696. const oldButton = document.querySelector("button.category-visible");
  1697. oldButton.classList.remove("category-visible");
  1698. const newSelect = document.querySelector("#create-entity-" + e.target.value);
  1699. newSelect.classList.add("category-visible");
  1700. const newButton = document.querySelector("#create-entity-" + e.target.value + "-button");
  1701. newButton.classList.add("category-visible");
  1702. });
  1703. }
  1704. document.addEventListener("mousemove", (e) => {
  1705. if (clicked) {
  1706. const position = snapRel(abs2rel({ x: e.clientX - dragOffsetX, y: e.clientY - dragOffsetY }));
  1707. clicked.dataset.x = position.x;
  1708. clicked.dataset.y = position.y;
  1709. updateEntityElement(entities[clicked.dataset.key], clicked);
  1710. if (hoveringInDeleteArea(e)) {
  1711. document.querySelector("#menubar").classList.add("hover-delete");
  1712. } else {
  1713. document.querySelector("#menubar").classList.remove("hover-delete");
  1714. }
  1715. }
  1716. });
  1717. document.addEventListener("touchmove", (e) => {
  1718. if (clicked) {
  1719. e.preventDefault();
  1720. let x = e.touches[0].clientX;
  1721. let y = e.touches[0].clientY;
  1722. const position = snapRel(abs2rel({ x: x - dragOffsetX, y: y - dragOffsetY }));
  1723. clicked.dataset.x = position.x;
  1724. clicked.dataset.y = position.y;
  1725. updateEntityElement(entities[clicked.dataset.key], clicked);
  1726. // what a hack
  1727. // I should centralize this 'fake event' creation...
  1728. if (hoveringInDeleteArea({ clientY: y })) {
  1729. document.querySelector("#menubar").classList.add("hover-delete");
  1730. } else {
  1731. document.querySelector("#menubar").classList.remove("hover-delete");
  1732. }
  1733. }
  1734. }, { passive: false });
  1735. function checkFitWorld() {
  1736. if (config.autoFit) {
  1737. fitWorld();
  1738. return true;
  1739. }
  1740. return false;
  1741. }
  1742. const fitModes = {
  1743. "max": {
  1744. start: 0,
  1745. binop: Math.max,
  1746. final: (total, count) => total
  1747. },
  1748. "arithmetic mean": {
  1749. start: 0,
  1750. binop: math.add,
  1751. final: (total, count) => total / count
  1752. },
  1753. "geometric mean": {
  1754. start: 1,
  1755. binop: math.multiply,
  1756. final: (total, count) => math.pow(total, 1 / count)
  1757. }
  1758. }
  1759. function fitWorld(manual = false, factor = 1.1) {
  1760. const fitMode = fitModes[config.autoFitMode]
  1761. let max = fitMode.start
  1762. let count = 0;
  1763. Object.entries(entities).forEach(([key, entity]) => {
  1764. const view = entity.view;
  1765. let extra = entity.views[view].image.extra;
  1766. extra = extra === undefined ? 1 : extra;
  1767. max = fitMode.binop(max, math.multiply(extra, entity.views[view].height.toNumber("meter")));
  1768. count += 1;
  1769. });
  1770. max = fitMode.final(max, count)
  1771. max = math.unit(max, "meter")
  1772. if (manual)
  1773. altHeld = true;
  1774. setWorldHeight(config.height, math.multiply(max, factor));
  1775. if (manual)
  1776. altHeld = false;
  1777. }
  1778. function updateWorldHeight() {
  1779. const unit = document.querySelector("#options-height-unit").value;
  1780. const value = Math.max(0.000000001, document.querySelector("#options-height-value").value);
  1781. const oldHeight = config.height;
  1782. setWorldHeight(oldHeight, math.unit(value, unit));
  1783. }
  1784. function setWorldHeight(oldHeight, newHeight) {
  1785. worldSizeDirty = true;
  1786. config.height = newHeight.to(document.querySelector("#options-height-unit").value)
  1787. const unit = document.querySelector("#options-height-unit").value;
  1788. setNumericInput(document.querySelector("#options-height-value"), config.height.toNumber(unit));
  1789. Object.entries(entities).forEach(([key, entity]) => {
  1790. const element = document.querySelector("#entity-" + key);
  1791. let newPosition;
  1792. if (!altHeld) {
  1793. newPosition = adjustAbs({ x: element.dataset.x, y: element.dataset.y }, oldHeight, config.height);
  1794. } else {
  1795. newPosition = { x: element.dataset.x, y: element.dataset.y };
  1796. }
  1797. element.dataset.x = newPosition.x;
  1798. element.dataset.y = newPosition.y;
  1799. });
  1800. updateSizes();
  1801. }
  1802. function loadScene(name = "default") {
  1803. try {
  1804. const data = JSON.parse(localStorage.getItem("macrovision-save-" + name));
  1805. if (data === null) {
  1806. return false;
  1807. }
  1808. importScene(data);
  1809. return true;
  1810. } catch (err) {
  1811. alert("Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error.")
  1812. console.error(err);
  1813. return false;
  1814. }
  1815. }
  1816. function saveScene(name = "default") {
  1817. try {
  1818. const string = JSON.stringify(exportScene());
  1819. localStorage.setItem("macrovision-save-" + name, string);
  1820. } catch (err) {
  1821. alert("Something went wrong while saving (maybe I don't have localStorage permissions, or exporting failed). Check the F12 console for the error.")
  1822. console.error(err);
  1823. }
  1824. }
  1825. function deleteScene(name = "default") {
  1826. try {
  1827. localStorage.removeItem("macrovision-save-" + name)
  1828. } catch (err) {
  1829. console.error(err);
  1830. }
  1831. }
  1832. function exportScene() {
  1833. const results = {};
  1834. results.entities = [];
  1835. Object.entries(entities).filter(([key, entity]) => entity.ephemeral !== true).forEach(([key, entity]) => {
  1836. const element = document.querySelector("#entity-" + key);
  1837. results.entities.push({
  1838. name: entity.identifier,
  1839. scale: entity.scale,
  1840. view: entity.view,
  1841. x: element.dataset.x,
  1842. y: element.dataset.y
  1843. });
  1844. });
  1845. const unit = document.querySelector("#options-height-unit").value;
  1846. results.world = {
  1847. height: config.height.toNumber(unit),
  1848. unit: unit
  1849. }
  1850. results.canvasWidth = canvasWidth;
  1851. return results;
  1852. }
  1853. // btoa doesn't like anything that isn't ASCII
  1854. // great
  1855. // thanks to https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
  1856. // for providing an alternative
  1857. function b64EncodeUnicode(str) {
  1858. // first we use encodeURIComponent to get percent-encoded UTF-8,
  1859. // then we convert the percent encodings into raw bytes which
  1860. // can be fed into btoa.
  1861. return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
  1862. function toSolidBytes(match, p1) {
  1863. return String.fromCharCode('0x' + p1);
  1864. }));
  1865. }
  1866. function b64DecodeUnicode(str) {
  1867. // Going backwards: from bytestream, to percent-encoding, to original string.
  1868. return decodeURIComponent(atob(str).split('').map(function (c) {
  1869. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  1870. }).join(''));
  1871. }
  1872. function linkScene() {
  1873. loc = new URL(window.location);
  1874. window.location = loc.protocol + "//" + loc.host + loc.pathname + "?scene=" + b64EncodeUnicode(JSON.stringify(exportScene()));
  1875. }
  1876. function copyScene() {
  1877. const results = exportScene();
  1878. navigator.clipboard.writeText(JSON.stringify(results));
  1879. }
  1880. function pasteScene() {
  1881. try {
  1882. navigator.clipboard.readText().then(text => {
  1883. const data = JSON.parse(text);
  1884. if (data.entities === undefined) {
  1885. return;
  1886. }
  1887. if (data.world === undefined) {
  1888. return;
  1889. }
  1890. importScene(data);
  1891. }).catch(err => alert(err));
  1892. } catch (err) {
  1893. console.error(err);
  1894. // probably wasn't valid data
  1895. }
  1896. }
  1897. // TODO - don't just search through every single entity
  1898. // probably just have a way to do lookups directly
  1899. function findEntity(name) {
  1900. return availableEntitiesByName[name];
  1901. }
  1902. function importScene(data) {
  1903. removeAllEntities();
  1904. data.entities.forEach(entityInfo => {
  1905. const entity = findEntity(entityInfo.name).constructor();
  1906. entity.scale = entityInfo.scale
  1907. displayEntity(entity, entityInfo.view, entityInfo.x, entityInfo.y);
  1908. });
  1909. config.height = math.unit(data.world.height, data.world.unit);
  1910. document.querySelector("#options-height-unit").value = data.world.unit;
  1911. if (data.canvasWidth) {
  1912. doHorizReposition(data.canvasWidth / canvasWidth);
  1913. }
  1914. updateSizes();
  1915. }
  1916. function renderToCanvas() {
  1917. const ctx = document.querySelector("#display").getContext("2d");
  1918. Object.entries(entities).sort((ent1, ent2) => {
  1919. z1 = document.querySelector("#entity-" + ent1[0]).style.zIndex;
  1920. z2 = document.querySelector("#entity-" + ent2[0]).style.zIndex;
  1921. return z1 - z2;
  1922. }).forEach(([id, entity]) => {
  1923. element = document.querySelector("#entity-" + id);
  1924. img = element.querySelector("img");
  1925. let x = parseFloat(element.dataset.x);
  1926. let y = parseFloat(element.dataset.y);
  1927. let coords = rel2abs({x: x, y: y});
  1928. let offset = img.style.getPropertyValue("--offset");
  1929. offset = parseFloat(offset.substring(0, offset.length-1))
  1930. x = coords.x - img.getBoundingClientRect().width/2;
  1931. y = coords.y - img.getBoundingClientRect().height * (-offset/100);
  1932. let xSize = img.getBoundingClientRect().width;
  1933. let ySize = img.getBoundingClientRect().height;
  1934. ctx.drawImage(img, x, y, xSize, ySize);
  1935. });
  1936. }
  1937. function exportCanvas(callback) {
  1938. /** @type {CanvasRenderingContext2D} */
  1939. const ctx = document.querySelector("#display").getContext("2d");
  1940. const blob = ctx.canvas.toBlob(callback);
  1941. }
  1942. function generateScreenshot(callback) {
  1943. renderToCanvas();
  1944. /** @type {CanvasRenderingContext2D} */
  1945. const ctx = document.querySelector("#display").getContext("2d");
  1946. ctx.fillStyle = "#555";
  1947. ctx.font = "normal normal lighter 16pt coda";
  1948. ctx.fillText("macrovision.crux.sexy", 10, 25);
  1949. exportCanvas(blob => {
  1950. callback(blob);
  1951. });
  1952. }
  1953. function copyScreenshot() {
  1954. generateScreenshot(blob => {
  1955. navigator.clipboard.write([
  1956. new ClipboardItem({
  1957. "image/png": blob
  1958. })
  1959. ]);
  1960. });
  1961. drawScale(false);
  1962. }
  1963. function saveScreenshot() {
  1964. generateScreenshot(blob => {
  1965. const a = document.createElement("a");
  1966. a.href = URL.createObjectURL(blob);
  1967. a.setAttribute("download", "macrovision.png");
  1968. a.click();
  1969. });
  1970. drawScale(false);
  1971. }
  1972. function toast(msg) {
  1973. let div = document.createElement("div");
  1974. div.innerHTML = msg;
  1975. div.classList.add("toast");
  1976. document.body.appendChild(div);
  1977. setTimeout(() => {
  1978. document.body.removeChild(div);
  1979. }, 5000)
  1980. }