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.
 
 
 

3707 lignes
111 KiB

  1. let selected = null;
  2. let selectedEntity = null;
  3. let entityIndex = 0;
  4. let clicked = null;
  5. let movingInBounds = false;
  6. let dragging = false;
  7. let clickTimeout = null;
  8. let dragOffsetX = null;
  9. let dragOffsetY = null;
  10. let panning = false;
  11. let panReady = true;
  12. let panOffsetX = null;
  13. let panOffsetY = null;
  14. let shiftHeld = false;
  15. let altHeld = false;
  16. let entityX;
  17. let canvasWidth;
  18. let canvasHeight;
  19. let dragScale = 1;
  20. let dragScaleHandle = null;
  21. let dragEntityScale = 1;
  22. let dragEntityScaleHandle = null;
  23. let scrollDirection = 0;
  24. let scrollHandle = null;
  25. let zoomDirection = 0;
  26. let zoomHandle = null;
  27. let sizeDirection = 0;
  28. let sizeHandle = null;
  29. let worldSizeDirty = false;
  30. const tagDefs = {
  31. "anthro": "Anthro",
  32. "feral": "Feral",
  33. "taur": "Taur",
  34. "naga": "Naga",
  35. "goo": "Goo"
  36. }
  37. math.createUnit("humans", {
  38. definition: "5.75 feet"
  39. });
  40. math.createUnit("story", {
  41. definition: "12 feet",
  42. prefixes: "long"
  43. });
  44. math.createUnit("stories", {
  45. definition: "12 feet",
  46. prefixes: "long"
  47. });
  48. math.createUnit("earths", {
  49. definition: "12756km",
  50. prefixes: "long"
  51. });
  52. math.createUnit("parsec", {
  53. definition: "3.086e16 meters",
  54. prefixes: "long"
  55. })
  56. math.createUnit("parsecs", {
  57. definition: "3.086e16 meters",
  58. prefixes: "long"
  59. })
  60. math.createUnit("lightyears", {
  61. definition: "9.461e15 meters",
  62. prefixes: "long"
  63. })
  64. math.createUnit("AU", {
  65. definition: "149597870700 meters"
  66. })
  67. math.createUnit("AUs", {
  68. definition: "149597870700 meters"
  69. })
  70. math.createUnit("dalton", {
  71. definition: "1.66e-27 kg",
  72. prefixes: "long"
  73. });
  74. math.createUnit("daltons", {
  75. definition: "1.66e-27 kg",
  76. prefixes: "long"
  77. });
  78. math.createUnit("solarradii", {
  79. definition: "695990 km",
  80. prefixes: "long"
  81. });
  82. math.createUnit("solarmasses", {
  83. definition: "2e30 kg",
  84. prefixes: "long"
  85. });
  86. math.createUnit("galaxy", {
  87. definition: "105700 lightyears",
  88. prefixes: "long"
  89. });
  90. math.createUnit("galaxies", {
  91. definition: "105700 lightyears",
  92. prefixes: "long"
  93. });
  94. math.createUnit("universe", {
  95. definition: "93.016e9 lightyears",
  96. prefixes: "long"
  97. });
  98. math.createUnit("universes", {
  99. definition: "93.016e9 lightyears",
  100. prefixes: "long"
  101. });
  102. math.createUnit("multiverse", {
  103. definition: "1e30 lightyears",
  104. prefixes: "long"
  105. });
  106. math.createUnit("multiverses", {
  107. definition: "1e30 lightyears",
  108. prefixes: "long"
  109. });
  110. math.createUnit("footballFields", {
  111. definition: "57600 feet^2",
  112. prefixes: "long"
  113. });
  114. math.createUnit("people", {
  115. definition: "75 liters",
  116. prefixes: "long"
  117. });
  118. math.createUnit("olympicPools", {
  119. definition: "2500 m^3",
  120. prefixes: "long"
  121. });
  122. math.createUnit("oceans", {
  123. definition: "700000000 km^3",
  124. prefixes: "long"
  125. });
  126. math.createUnit("earthVolumes", {
  127. definition: "1.0867813e12 km^3",
  128. prefixes: "long"
  129. });
  130. math.createUnit("universeVolumes", {
  131. definition: "4.2137775e+32 lightyears^3",
  132. prefixes: "long"
  133. });
  134. math.createUnit("multiverseVolumes", {
  135. definition: "5.2359878e+89 lightyears^3",
  136. prefixes: "long"
  137. });
  138. math.createUnit("peopleMass", {
  139. definition: "80 kg",
  140. prefixes: "long"
  141. });
  142. const defaultUnits = {
  143. length: {
  144. metric: "meters",
  145. customary: "feet",
  146. relative: "stories"
  147. },
  148. area: {
  149. metric: "meters^2",
  150. customary: "feet^2",
  151. relative: "footballFields"
  152. },
  153. volume: {
  154. metric: "liters",
  155. customary: "gallons",
  156. relative: "olympicPools"
  157. },
  158. mass: {
  159. metric: "kilograms",
  160. customary: "lbs",
  161. relative: "peopleMass"
  162. }
  163. }
  164. const unitChoices = {
  165. length: {
  166. "metric": [
  167. "angstroms",
  168. "millimeters",
  169. "centimeters",
  170. "meters",
  171. "kilometers",
  172. ],
  173. "customary": [
  174. "inches",
  175. "feet",
  176. "miles",
  177. ],
  178. "relative": [
  179. "humans",
  180. "stories",
  181. "earths",
  182. "solarradii",
  183. "AUs",
  184. "lightyears",
  185. "parsecs",
  186. "galaxies",
  187. "universes",
  188. "multiverses"
  189. ]
  190. },
  191. area: {
  192. "metric": [
  193. "cm^2",
  194. "meters^2",
  195. "kilometers^2",
  196. ],
  197. "customary": [
  198. "feet^2",
  199. "acres",
  200. "miles^2"
  201. ],
  202. "relative": [
  203. "footballFields"
  204. ]
  205. },
  206. volume: {
  207. "metric": [
  208. "milliliters",
  209. "liters",
  210. "m^3",
  211. ],
  212. "customary": [
  213. "floz",
  214. "cups",
  215. "pints",
  216. "quarts",
  217. "gallons",
  218. ],
  219. "relative": [
  220. "people",
  221. "olympicPools",
  222. "oceans",
  223. "earthVolumes",
  224. "universeVolumes",
  225. "multiverseVolumes",
  226. ]
  227. },
  228. mass: {
  229. "metric": [
  230. "kilograms",
  231. "milligrams",
  232. "grams",
  233. "tonnes",
  234. ],
  235. "customary": [
  236. "lbs",
  237. "ounces",
  238. "tons"
  239. ],
  240. "relative": [
  241. "peopleMass"
  242. ]
  243. }
  244. }
  245. const config = {
  246. height: math.unit(1500, "meters"),
  247. x: 0,
  248. y: 0,
  249. minLineSize: 100,
  250. maxLineSize: 150,
  251. autoFit: false,
  252. drawYAxis: true,
  253. drawXAxis: false
  254. }
  255. const availableEntities = {
  256. }
  257. const availableEntitiesByName = {
  258. }
  259. const entities = {
  260. }
  261. function constrainRel(coords) {
  262. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  263. const worldHeight = config.height.toNumber("meters");
  264. if (altHeld) {
  265. return coords;
  266. }
  267. return {
  268. x: Math.min(Math.max(coords.x, -worldWidth / 2 + config.x), worldWidth / 2 + config.x),
  269. y: Math.min(Math.max(coords.y, config.y), worldHeight + config.y)
  270. }
  271. }
  272. function snapPos(coords) {
  273. return constrainRel({
  274. x: coords.x,
  275. y: (!config.lockYAxis || altHeld) ? coords.y : (Math.abs(coords.y) < config.height.toNumber("meters")/20 ? 0 : coords.y)
  276. });
  277. }
  278. function adjustAbs(coords, oldHeight, newHeight) {
  279. const ratio = math.divide(newHeight, oldHeight);
  280. const x = coords.x * ratio;
  281. const y = coords.y * ratio;
  282. return { x: x, y: y};
  283. }
  284. function pos2pix(coords) {
  285. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  286. const worldHeight = config.height.toNumber("meters");
  287. const x = ((coords.x - config.x) / worldWidth + 0.5) * (canvasWidth - 50) + 50;
  288. const y = (1 - (coords.y - config.y) / worldHeight) * (canvasHeight - 50) + 50;
  289. return { x: x, y: y };
  290. }
  291. function pix2pos(coords) {
  292. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  293. const worldHeight = config.height.toNumber("meters");
  294. const x = (((coords.x - 50) / (canvasWidth - 50)) - 0.5) * worldWidth + config.x;
  295. const y = (1 - ((coords.y - 50) / (canvasHeight - 50))) * worldHeight + config.y;
  296. return { x: x, y: y };
  297. }
  298. function updateEntityElement(entity, element) {
  299. const position = pos2pix({ x: element.dataset.x, y: element.dataset.y });
  300. const view = entity.view;
  301. element.style.left = position.x + "px";
  302. element.style.top = position.y + "px";
  303. element.style.setProperty("--xpos", position.x + "px");
  304. element.style.setProperty("--entity-height", "'" + entity.views[view].height.to(config.height.units[0].unit.name).format({ precision: 2 }) + "'");
  305. const pixels = math.divide(entity.views[view].height, config.height) * (canvasHeight - 50);
  306. const extra = entity.views[view].image.extra;
  307. const bottom = entity.views[view].image.bottom;
  308. const bonus = (extra ? extra : 1) * (1 / (1 - (bottom ? bottom : 0)));
  309. element.style.setProperty("--height", pixels * bonus + "px");
  310. element.style.setProperty("--extra", pixels * bonus - pixels + "px");
  311. element.style.setProperty("--brightness", entity.brightness);
  312. if (entity.views[view].rename)
  313. element.querySelector(".entity-name").innerText = entity.name == "" ? "" : entity.views[view].name;
  314. else
  315. element.querySelector(".entity-name").innerText = entity.name;
  316. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  317. bottomName.style.left = position.x + entityX + "px";
  318. bottomName.style.bottom = "0vh";
  319. bottomName.innerText = entity.name;
  320. const topName = document.querySelector("#top-name-" + element.dataset.key);
  321. topName.style.left = position.x + entityX + "px";
  322. topName.style.top = "20vh";
  323. topName.innerText = entity.name;
  324. if (entity.views[view].height.toNumber("meters") / 10 > config.height.toNumber("meters")) {
  325. topName.classList.add("top-name-needed");
  326. } else {
  327. topName.classList.remove("top-name-needed");
  328. }
  329. }
  330. function updateSizes(dirtyOnly = false) {
  331. if (config.lockYAxis) {
  332. config.y = 0;
  333. }
  334. drawScales(dirtyOnly);
  335. let ordered = Object.entries(entities);
  336. ordered.sort((e1, e2) => {
  337. if (e1[1].priority != e2[1].priority) {
  338. return e2[1].priority - e1[1].priority;
  339. } else {
  340. return e1[1].views[e1[1].view].height.value - e2[1].views[e2[1].view].height.value
  341. }
  342. });
  343. let zIndex = ordered.length;
  344. ordered.forEach(entity => {
  345. const element = document.querySelector("#entity-" + entity[0]);
  346. element.style.zIndex = zIndex;
  347. if (!dirtyOnly || entity[1].dirty) {
  348. updateEntityElement(entity[1], element, zIndex);
  349. entity[1].dirty = false;
  350. }
  351. zIndex -= 1;
  352. });
  353. document.querySelector("#ground").style.top = pos2pix({x: 0, y: 0}).y + "px";
  354. }
  355. function drawScales(ifDirty = false) {
  356. const canvas = document.querySelector("#display");
  357. /** @type {CanvasRenderingContext2D} */
  358. const ctx = canvas.getContext("2d");
  359. ctx.scale(1, 1);
  360. ctx.canvas.width = canvas.clientWidth;
  361. ctx.canvas.height = canvas.clientHeight;
  362. ctx.beginPath();
  363. ctx.rect(0, 0, canvas.width, canvas.height);
  364. ctx.fillStyle = "#333";
  365. ctx.fill();
  366. if (config.drawYAxis) {
  367. drawVerticalScale(ifDirty);
  368. }
  369. if (config.drawXAxis) {
  370. drawHorizontalScale(ifDirty);
  371. }
  372. }
  373. function drawVerticalScale(ifDirty = false) {
  374. if (ifDirty && !worldSizeDirty)
  375. return;
  376. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  377. let total = heightPer.clone();
  378. total.value = config.y;
  379. let y = ctx.canvas.clientHeight - 50;
  380. let offset = total.toNumber("meters") % heightPer.toNumber("meters");
  381. y += offset / heightPer.toNumber("meters") * pixelsPer;
  382. total = math.subtract(total, math.unit(offset, "meters"));
  383. for (; y >= 50; y -= pixelsPer) {
  384. drawTick(ctx, 50, y, total);
  385. total = math.add(total, heightPer);
  386. }
  387. }
  388. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, value) {
  389. const oldStroke = ctx.strokeStyle;
  390. const oldFill = ctx.fillStyle;
  391. ctx.beginPath();
  392. ctx.moveTo(x, y);
  393. ctx.lineTo(x + 20, y);
  394. ctx.strokeStyle = "#000000";
  395. ctx.stroke();
  396. ctx.beginPath();
  397. ctx.moveTo(x + 20, y);
  398. ctx.lineTo(ctx.canvas.clientWidth - 70, y);
  399. ctx.strokeStyle = "#aaaaaa";
  400. ctx.stroke();
  401. ctx.beginPath();
  402. ctx.moveTo(ctx.canvas.clientWidth - 70, y);
  403. ctx.lineTo(ctx.canvas.clientWidth - 50, y);
  404. ctx.strokeStyle = "#000000";
  405. ctx.stroke();
  406. const oldFont = ctx.font;
  407. ctx.font = 'normal 24pt coda';
  408. ctx.fillStyle = "#dddddd";
  409. ctx.beginPath();
  410. ctx.fillText(value.format({ precision: 3 }), x + 20, y + 35);
  411. ctx.font = oldFont;
  412. ctx.strokeStyle = oldStroke;
  413. ctx.fillStyle = oldFill;
  414. }
  415. const canvas = document.querySelector("#display");
  416. /** @type {CanvasRenderingContext2D} */
  417. const ctx = canvas.getContext("2d");
  418. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
  419. heightPer = 1;
  420. if (pixelsPer < config.minLineSize) {
  421. const factor = math.ceil(config.minLineSize / pixelsPer);
  422. heightPer *= factor;
  423. pixelsPer *= factor;
  424. }
  425. if (pixelsPer > config.maxLineSize) {
  426. const factor = math.ceil(pixelsPer / config.maxLineSize);
  427. heightPer /= factor;
  428. pixelsPer /= factor;
  429. }
  430. if (heightPer == 0) {
  431. console.error("The world size is invalid! Refusing to draw the scale...");
  432. return;
  433. }
  434. heightPer = math.unit(heightPer, document.querySelector("#options-height-unit").value);
  435. ctx.beginPath();
  436. ctx.moveTo(50, 50);
  437. ctx.lineTo(50, ctx.canvas.clientHeight - 50);
  438. ctx.stroke();
  439. ctx.beginPath();
  440. ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
  441. ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
  442. ctx.stroke();
  443. drawTicks(ctx, pixelsPer, heightPer);
  444. }
  445. // this is a lot of copypizza...
  446. function drawHorizontalScale(ifDirty = false) {
  447. if (ifDirty && !worldSizeDirty)
  448. return;
  449. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  450. let total = heightPer.clone();
  451. total.value = math.unit(-config.x, "meters").toNumber(config.unit);
  452. // further adjust it to put the current position in the center
  453. total.value -= heightPer.toNumber("meters") / pixelsPer * (canvasWidth + 50) / 2;
  454. let x = ctx.canvas.clientWidth - 50;
  455. let offset = total.toNumber("meters") % heightPer.toNumber("meters");
  456. x += offset / heightPer.toNumber("meters") * pixelsPer;
  457. total = math.subtract(total, math.unit(offset, "meters"));
  458. for (; x >= 50 - pixelsPer; x -= pixelsPer) {
  459. // negate it so that the left side is negative
  460. drawTick(ctx, x, 50, math.multiply(-1, total));
  461. total = math.add(total, heightPer);
  462. }
  463. }
  464. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, value) {
  465. const oldStroke = ctx.strokeStyle;
  466. const oldFill = ctx.fillStyle;
  467. ctx.beginPath();
  468. ctx.moveTo(x, y);
  469. ctx.lineTo(x, y + 20);
  470. ctx.strokeStyle = "#000000";
  471. ctx.stroke();
  472. ctx.beginPath();
  473. ctx.moveTo(x, y + 20);
  474. ctx.lineTo(x, ctx.canvas.clientHeight - 70);
  475. ctx.strokeStyle = "#aaaaaa";
  476. ctx.stroke();
  477. ctx.beginPath();
  478. ctx.moveTo(x, ctx.canvas.clientHeight - 70);
  479. ctx.lineTo(x, ctx.canvas.clientHeight - 50);
  480. ctx.strokeStyle = "#000000";
  481. ctx.stroke();
  482. const oldFont = ctx.font;
  483. ctx.font = 'normal 24pt coda';
  484. ctx.fillStyle = "#dddddd";
  485. ctx.beginPath();
  486. ctx.fillText(value.format({ precision: 3 }), x + 35, y + 20);
  487. ctx.font = oldFont;
  488. ctx.strokeStyle = oldStroke;
  489. ctx.fillStyle = oldFill;
  490. }
  491. const canvas = document.querySelector("#display");
  492. /** @type {CanvasRenderingContext2D} */
  493. const ctx = canvas.getContext("2d");
  494. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.toNumber();
  495. heightPer = 1;
  496. if (pixelsPer < config.minLineSize * 2) {
  497. const factor = math.ceil(config.minLineSize * 2/ pixelsPer);
  498. heightPer *= factor;
  499. pixelsPer *= factor;
  500. }
  501. if (pixelsPer > config.maxLineSize * 2) {
  502. const factor = math.ceil(pixelsPer / 2/ config.maxLineSize);
  503. heightPer /= factor;
  504. pixelsPer /= factor;
  505. }
  506. if (heightPer == 0) {
  507. console.error("The world size is invalid! Refusing to draw the scale...");
  508. return;
  509. }
  510. heightPer = math.unit(heightPer, document.querySelector("#options-height-unit").value);
  511. ctx.beginPath();
  512. ctx.moveTo(0, 50);
  513. ctx.lineTo(ctx.canvas.clientWidth, 50);
  514. ctx.stroke();
  515. ctx.beginPath();
  516. ctx.moveTo(0, ctx.canvas.clientHeight - 50);
  517. ctx.lineTo(ctx.canvas.clientWidth , ctx.canvas.clientHeight - 50);
  518. ctx.stroke();
  519. drawTicks(ctx, pixelsPer, heightPer);
  520. }
  521. // Entities are generated as needed, and we make a copy
  522. // every time - the resulting objects get mutated, after all.
  523. // But we also want to be able to read some information without
  524. // calling the constructor -- e.g. making a list of authors and
  525. // owners. So, this function is used to generate that information.
  526. // It is invoked like makeEntity so that it can be dropped in easily,
  527. // but returns an object that lets you construct many copies of an entity,
  528. // rather than creating a new entity.
  529. function createEntityMaker(info, views, sizes) {
  530. const maker = {};
  531. maker.name = info.name;
  532. maker.info = info;
  533. maker.sizes = sizes;
  534. maker.constructor = () => makeEntity(info, views, sizes);
  535. maker.authors = [];
  536. maker.owners = [];
  537. maker.nsfw = false;
  538. Object.values(views).forEach(view => {
  539. const authors = authorsOf(view.image.source);
  540. if (authors) {
  541. authors.forEach(author => {
  542. if (maker.authors.indexOf(author) == -1) {
  543. maker.authors.push(author);
  544. }
  545. });
  546. }
  547. const owners = ownersOf(view.image.source);
  548. if (owners) {
  549. owners.forEach(owner => {
  550. if (maker.owners.indexOf(owner) == -1) {
  551. maker.owners.push(owner);
  552. }
  553. });
  554. }
  555. if (isNsfw(view.image.source)) {
  556. maker.nsfw = true;
  557. }
  558. });
  559. return maker;
  560. }
  561. // This function serializes and parses its arguments to avoid sharing
  562. // references to a common object. This allows for the objects to be
  563. // safely mutated.
  564. function makeEntity(info, views, sizes) {
  565. const entityTemplate = {
  566. name: info.name,
  567. identifier: info.name,
  568. scale: 1,
  569. info: JSON.parse(JSON.stringify(info)),
  570. views: JSON.parse(JSON.stringify(views), math.reviver),
  571. sizes: sizes === undefined ? [] : JSON.parse(JSON.stringify(sizes), math.reviver),
  572. init: function () {
  573. const entity = this;
  574. Object.entries(this.views).forEach(([viewKey, view]) => {
  575. view.parent = this;
  576. if (this.defaultView === undefined) {
  577. this.defaultView = viewKey;
  578. this.view = viewKey;
  579. }
  580. if (view.default) {
  581. this.defaultView = viewKey;
  582. this.view = viewKey;
  583. }
  584. // to remember the units the user last picked
  585. view.units = {};
  586. Object.entries(view.attributes).forEach(([key, val]) => {
  587. Object.defineProperty(
  588. view,
  589. key,
  590. {
  591. get: function () {
  592. return math.multiply(Math.pow(this.parent.scale, this.attributes[key].power), this.attributes[key].base);
  593. },
  594. set: function (value) {
  595. const newScale = Math.pow(math.divide(value, this.attributes[key].base), 1 / this.attributes[key].power);
  596. this.parent.scale = newScale;
  597. }
  598. }
  599. );
  600. });
  601. });
  602. this.sizes.forEach(size => {
  603. if (size.default === true) {
  604. this.views[this.defaultView].height = size.height;
  605. this.size = size;
  606. }
  607. });
  608. if (this.size === undefined && this.sizes.length > 0) {
  609. this.views[this.defaultView].height = this.sizes[0].height;
  610. this.size = this.sizes[0];
  611. console.warn("No default size set for " + info.name);
  612. } else if (this.sizes.length == 0) {
  613. this.sizes = [
  614. {
  615. name: "Normal",
  616. height: this.views[this.defaultView].height
  617. }
  618. ];
  619. this.size = this.sizes[0];
  620. }
  621. this.desc = {};
  622. Object.entries(this.info).forEach(([key, value]) => {
  623. Object.defineProperty(
  624. this.desc,
  625. key,
  626. {
  627. get: function () {
  628. let text = value.text;
  629. if (entity.views[entity.view].info) {
  630. if (entity.views[entity.view].info[key]) {
  631. text = combineInfo(text, entity.views[entity.view].info[key]);
  632. }
  633. }
  634. if (entity.size.info) {
  635. if (entity.size.info[key]) {
  636. text = combineInfo(text, entity.size.info[key]);
  637. }
  638. }
  639. return { title: value.title, text: text };
  640. }
  641. }
  642. )
  643. });
  644. Object.defineProperty(
  645. this,
  646. "currentView",
  647. {
  648. get: function() {
  649. return entity.views[entity.view];
  650. }
  651. }
  652. )
  653. delete this.init;
  654. return this;
  655. }
  656. }.init();
  657. return entityTemplate;
  658. }
  659. function combineInfo(existing, next) {
  660. switch (next.mode) {
  661. case "replace":
  662. return next.text;
  663. case "prepend":
  664. return next.text + existing;
  665. case "append":
  666. return existing + next.text;
  667. }
  668. return existing;
  669. }
  670. function clickDown(target, x, y) {
  671. clicked = target;
  672. movingInBounds = false;
  673. const rect = target.getBoundingClientRect();
  674. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  675. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  676. dragOffsetX = x - rect.left + entX;
  677. dragOffsetY = y - rect.top + entY;
  678. x = x - dragOffsetX;
  679. y = y - dragOffsetY;
  680. if (x >= 0 && x <= canvasWidth && y >= 0 && y <= canvasHeight) {
  681. movingInBounds = true;
  682. }
  683. clickTimeout = setTimeout(() => { dragging = true }, 200)
  684. target.classList.add("no-transition");
  685. }
  686. // could we make this actually detect the menu area?
  687. function hoveringInDeleteArea(e) {
  688. return e.clientY < document.body.clientHeight / 10;
  689. }
  690. function clickUp(e) {
  691. if (e.which != 1) {
  692. return;
  693. }
  694. clearTimeout(clickTimeout);
  695. if (clicked) {
  696. clicked.classList.remove("no-transition");
  697. if (dragging) {
  698. dragging = false;
  699. if (hoveringInDeleteArea(e)) {
  700. removeEntity(clicked);
  701. document.querySelector("#menubar").classList.remove("hover-delete");
  702. }
  703. } else {
  704. select(clicked);
  705. }
  706. clicked = null;
  707. }
  708. }
  709. function deselect(e) {
  710. if (e !== undefined && e.which != 1) {
  711. return;
  712. }
  713. if (selected) {
  714. selected.classList.remove("selected");
  715. }
  716. document.getElementById("options-selected-entity-none").selected = "selected";
  717. clearAttribution();
  718. selected = null;
  719. clearViewList();
  720. clearEntityOptions();
  721. clearViewOptions();
  722. document.querySelector("#delete-entity").disabled = true;
  723. document.querySelector("#grow").disabled = true;
  724. document.querySelector("#shrink").disabled = true;
  725. document.querySelector("#fit").disabled = true;
  726. }
  727. function select(target) {
  728. deselect();
  729. selected = target;
  730. selectedEntity = entities[target.dataset.key];
  731. document.getElementById("options-selected-entity-" + target.dataset.key).selected = "selected";
  732. selected.classList.add("selected");
  733. displayAttribution(selectedEntity.views[selectedEntity.view].image.source);
  734. configViewList(selectedEntity, selectedEntity.view);
  735. configEntityOptions(selectedEntity, selectedEntity.view);
  736. configViewOptions(selectedEntity, selectedEntity.view);
  737. document.querySelector("#delete-entity").disabled = false;
  738. document.querySelector("#grow").disabled = false;
  739. document.querySelector("#shrink").disabled = false;
  740. document.querySelector("#fit").disabled = false;
  741. }
  742. function configViewList(entity, selectedView) {
  743. const list = document.querySelector("#entity-view");
  744. list.innerHTML = "";
  745. list.style.display = "block";
  746. Object.keys(entity.views).forEach(view => {
  747. const option = document.createElement("option");
  748. option.innerText = entity.views[view].name;
  749. option.value = view;
  750. if (isNsfw(entity.views[view].image.source)) {
  751. option.classList.add("nsfw")
  752. }
  753. if (view === selectedView) {
  754. option.selected = true;
  755. if (option.classList.contains("nsfw")) {
  756. list.classList.add("nsfw");
  757. } else {
  758. list.classList.remove("nsfw");
  759. }
  760. }
  761. list.appendChild(option);
  762. });
  763. }
  764. function clearViewList() {
  765. const list = document.querySelector("#entity-view");
  766. list.innerHTML = "";
  767. list.style.display = "none";
  768. }
  769. function updateWorldOptions(entity, view) {
  770. const heightInput = document.querySelector("#options-height-value");
  771. const heightSelect = document.querySelector("#options-height-unit");
  772. const converted = config.height.toNumber(heightSelect.value);
  773. setNumericInput(heightInput, converted);
  774. }
  775. function configEntityOptions(entity, view) {
  776. const holder = document.querySelector("#options-entity");
  777. document.querySelector("#entity-category-header").style.display = "block";
  778. document.querySelector("#entity-category").style.display = "block";
  779. holder.innerHTML = "";
  780. const scaleLabel = document.createElement("div");
  781. scaleLabel.classList.add("options-label");
  782. scaleLabel.innerText = "Scale";
  783. const scaleRow = document.createElement("div");
  784. scaleRow.classList.add("options-row");
  785. const scaleInput = document.createElement("input");
  786. scaleInput.classList.add("options-field-numeric");
  787. scaleInput.id = "options-entity-scale";
  788. scaleInput.addEventListener("change", e => {
  789. entity.scale = e.target.value == 0 ? 1 : e.target.value;
  790. entity.dirty = true;
  791. if (config.autoFit) {
  792. fitWorld();
  793. } else {
  794. updateSizes(true);
  795. }
  796. updateEntityOptions(entity, view);
  797. updateViewOptions(entity, view);
  798. });
  799. scaleInput.addEventListener("keydown", e => {
  800. e.stopPropagation();
  801. })
  802. scaleInput.setAttribute("min", 1);
  803. scaleInput.setAttribute("type", "number");
  804. setNumericInput(scaleInput, entity.scale);
  805. scaleRow.appendChild(scaleInput);
  806. holder.appendChild(scaleLabel);
  807. holder.appendChild(scaleRow);
  808. const nameLabel = document.createElement("div");
  809. nameLabel.classList.add("options-label");
  810. nameLabel.innerText = "Name";
  811. const nameRow = document.createElement("div");
  812. nameRow.classList.add("options-row");
  813. const nameInput = document.createElement("input");
  814. nameInput.classList.add("options-field-text");
  815. nameInput.value = entity.name;
  816. nameInput.addEventListener("input", e => {
  817. entity.name = e.target.value;
  818. entity.dirty = true;
  819. updateSizes(true);
  820. })
  821. nameInput.addEventListener("keydown", e => {
  822. e.stopPropagation();
  823. })
  824. nameRow.appendChild(nameInput);
  825. holder.appendChild(nameLabel);
  826. holder.appendChild(nameRow);
  827. const defaultHolder = document.querySelector("#options-entity-defaults");
  828. defaultHolder.innerHTML = "";
  829. entity.sizes.forEach(defaultInfo => {
  830. const button = document.createElement("button");
  831. button.classList.add("options-button");
  832. button.innerText = defaultInfo.name;
  833. button.addEventListener("click", e => {
  834. entity.views[entity.defaultView].height = defaultInfo.height;
  835. entity.dirty = true;
  836. updateEntityOptions(entity, entity.view);
  837. updateViewOptions(entity, entity.view);
  838. if (!checkFitWorld()) {
  839. updateSizes(true);
  840. }
  841. if (config.autoFitSize) {
  842. let targets = {};
  843. targets[selected.dataset.key] = entities[selected.dataset.key];
  844. fitEntities(targets);
  845. }
  846. });
  847. defaultHolder.appendChild(button);
  848. });
  849. document.querySelector("#options-order-display").innerText = entity.priority;
  850. document.querySelector("#options-brightness-display").innerText = entity.brightness;
  851. document.querySelector("#options-ordering").style.display = "flex";
  852. }
  853. function updateEntityOptions(entity, view) {
  854. const scaleInput = document.querySelector("#options-entity-scale");
  855. setNumericInput(scaleInput, entity.scale);
  856. document.querySelector("#options-order-display").innerText = entity.priority;
  857. document.querySelector("#options-brightness-display").innerText = entity.brightness;
  858. }
  859. function clearEntityOptions() {
  860. document.querySelector("#entity-category-header").style.display = "none";
  861. document.querySelector("#entity-category").style.display = "none";
  862. /*
  863. const holder = document.querySelector("#options-entity");
  864. holder.innerHTML = "";
  865. document.querySelector("#options-entity-defaults").innerHTML = "";
  866. document.querySelector("#options-ordering").style.display = "none";
  867. document.querySelector("#options-ordering").style.display = "none";*/
  868. }
  869. function configViewOptions(entity, view) {
  870. const holder = document.querySelector("#options-view");
  871. document.querySelector("#view-category-header").style.display = "block";
  872. document.querySelector("#view-category").style.display = "block";
  873. holder.innerHTML = "";
  874. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  875. const label = document.createElement("div");
  876. label.classList.add("options-label");
  877. label.innerText = val.name;
  878. holder.appendChild(label);
  879. const row = document.createElement("div");
  880. row.classList.add("options-row");
  881. holder.appendChild(row);
  882. const input = document.createElement("input");
  883. input.classList.add("options-field-numeric");
  884. input.id = "options-view-" + key + "-input";
  885. input.setAttribute("type", "number");
  886. input.setAttribute("min", 1);
  887. const select = document.createElement("select");
  888. select.classList.add("options-field-unit");
  889. select.id = "options-view-" + key + "-select"
  890. Object.entries(unitChoices[val.type]).forEach(([group, entries]) => {
  891. const optGroup = document.createElement("optgroup");
  892. optGroup.label = group;
  893. select.appendChild(optGroup);
  894. entries.forEach(entry => {
  895. const option = document.createElement("option");
  896. option.innerText = entry;
  897. if (entry == defaultUnits[val.type][config.units]) {
  898. option.selected = true;
  899. }
  900. select.appendChild(option);
  901. })
  902. });
  903. input.addEventListener("change", e => {
  904. const value = input.value == 0 ? 1 : input.value;
  905. entity.views[view][key] = math.unit(value, select.value);
  906. entity.dirty = true;
  907. if (config.autoFit) {
  908. fitWorld();
  909. } else {
  910. updateSizes(true);
  911. }
  912. updateEntityOptions(entity, view);
  913. updateViewOptions(entity, view, key);
  914. });
  915. input.addEventListener("keydown", e => {
  916. e.stopPropagation();
  917. })
  918. if (entity.currentView.units[key]) {
  919. select.value = entity.currentView.units[key];
  920. } else {
  921. entity.currentView.units[key] = select.value;
  922. }
  923. select.setAttribute("oldUnit", select.value);
  924. setNumericInput(input, entity.views[view][key].toNumber(select.value));
  925. // TODO does this ever cause a change in the world?
  926. select.addEventListener("input", e => {
  927. const value = input.value == 0 ? 1 : input.value;
  928. const oldUnit = select.getAttribute("oldUnit");
  929. entity.views[entity.view][key] = math.unit(value, oldUnit).to(select.value);
  930. entity.dirty = true;
  931. setNumericInput(input, entity.views[entity.view][key].toNumber(select.value));
  932. select.setAttribute("oldUnit", select.value);
  933. entity.views[view].units[key] = select.value;
  934. if (config.autoFit) {
  935. fitWorld();
  936. } else {
  937. updateSizes(true);
  938. }
  939. updateEntityOptions(entity, view);
  940. updateViewOptions(entity, view, key);
  941. });
  942. row.appendChild(input);
  943. row.appendChild(select);
  944. });
  945. }
  946. function updateViewOptions(entity, view, changed) {
  947. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  948. if (key != changed) {
  949. const input = document.querySelector("#options-view-" + key + "-input");
  950. const select = document.querySelector("#options-view-" + key + "-select");
  951. const currentUnit = select.value;
  952. const convertedAmount = entity.views[view][key].toNumber(currentUnit);
  953. setNumericInput(input, convertedAmount);
  954. }
  955. });
  956. }
  957. function setNumericInput(input, value, round = 6) {
  958. if (typeof value == "string") {
  959. value = parseFloat(value)
  960. }
  961. input.value = value.toPrecision(round);
  962. }
  963. function getSortedEntities() {
  964. return Object.keys(entities).sort((a, b) => {
  965. const entA = entities[a];
  966. const entB = entities[b];
  967. const viewA = entA.view;
  968. const viewB = entB.view;
  969. const heightA = entA.views[viewA].height.to("meter").value;
  970. const heightB = entB.views[viewB].height.to("meter").value;
  971. return heightA - heightB;
  972. });
  973. }
  974. function clearViewOptions() {
  975. document.querySelector("#view-category-header").style.display = "none";
  976. document.querySelector("#view-category").style.display = "none";
  977. }
  978. // this is a crime against humanity, and also stolen from
  979. // stack overflow
  980. // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
  981. const testCanvas = document.createElement("canvas");
  982. testCanvas.id = "test-canvas";
  983. const testCtx = testCanvas.getContext("2d");
  984. function testClick(event) {
  985. // oh my god I can't believe I'm doing this
  986. const target = event.target;
  987. // Get click coordinates
  988. let w = target.width;
  989. let h = target.height;
  990. let ratioW = 1, ratioH = 1;
  991. // Limit the size of the canvas so that very large images don't cause problems)
  992. if (w > 1000) {
  993. ratioW = w / 1000;
  994. w /= ratioW;
  995. h /= ratioW;
  996. }
  997. if (h > 1000) {
  998. ratioH = h / 1000;
  999. w /= ratioH;
  1000. h /= ratioH;
  1001. }
  1002. const ratio = ratioW * ratioH;
  1003. var x = event.clientX - target.getBoundingClientRect().x,
  1004. y = event.clientY - target.getBoundingClientRect().y,
  1005. alpha;
  1006. testCtx.canvas.width = w;
  1007. testCtx.canvas.height = h;
  1008. // Draw image to canvas
  1009. // and read Alpha channel value
  1010. testCtx.drawImage(target, 0, 0, w, h);
  1011. alpha = testCtx.getImageData(Math.floor(x / ratio), Math.floor(y / ratio), 1, 1).data[3]; // [0]R [1]G [2]B [3]A
  1012. // If pixel is transparent,
  1013. // retrieve the element underneath and trigger its click event
  1014. if (alpha === 0) {
  1015. const oldDisplay = target.style.display;
  1016. target.style.display = "none";
  1017. const newTarget = document.elementFromPoint(event.clientX, event.clientY);
  1018. newTarget.dispatchEvent(new MouseEvent(event.type, {
  1019. "clientX": event.clientX,
  1020. "clientY": event.clientY
  1021. }));
  1022. target.style.display = oldDisplay;
  1023. } else {
  1024. clickDown(target.parentElement, event.clientX, event.clientY);
  1025. }
  1026. }
  1027. function arrangeEntities(order) {
  1028. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  1029. let sum = 0;
  1030. order.forEach(key => {
  1031. const image = document.querySelector("#entity-" + key + " > .entity-image");
  1032. const meters = entities[key].views[entities[key].view].height.toNumber("meters");
  1033. let height = image.height;
  1034. let width = image.width;
  1035. if (height == 0) {
  1036. height = 100;
  1037. }
  1038. if (width == 0) {
  1039. width = height;
  1040. }
  1041. sum += meters * width / height;
  1042. });
  1043. let x = config.x - sum / 2;
  1044. order.forEach(key => {
  1045. const image = document.querySelector("#entity-" + key + " > .entity-image");
  1046. const meters = entities[key].views[entities[key].view].height.toNumber("meters");
  1047. let height = image.height;
  1048. let width = image.width;
  1049. if (height == 0) {
  1050. height = 100;
  1051. }
  1052. if (width == 0) {
  1053. width = height;
  1054. }
  1055. x += meters * width / height / 2;
  1056. document.querySelector("#entity-" + key).dataset.x = x;
  1057. document.querySelector("#entity-" + key).dataset.y = config.y;
  1058. x += meters * width / height / 2;
  1059. })
  1060. fitWorld();
  1061. updateSizes();
  1062. }
  1063. function removeAllEntities() {
  1064. Object.keys(entities).forEach(key => {
  1065. removeEntity(document.querySelector("#entity-" + key));
  1066. });
  1067. }
  1068. function clearAttribution() {
  1069. document.querySelector("#attribution-category-header").style.display = "none";
  1070. document.querySelector("#options-attribution").style.display = "none";
  1071. }
  1072. function displayAttribution(file) {
  1073. document.querySelector("#attribution-category-header").style.display = "block";
  1074. document.querySelector("#options-attribution").style.display = "inline";
  1075. const authors = authorsOfFull(file);
  1076. const owners = ownersOfFull(file);
  1077. const source = sourceOf(file);
  1078. const authorHolder = document.querySelector("#options-attribution-authors");
  1079. const ownerHolder = document.querySelector("#options-attribution-owners");
  1080. const sourceHolder = document.querySelector("#options-attribution-source");
  1081. if (authors === []) {
  1082. const div = document.createElement("div");
  1083. div.innerText = "Unknown";
  1084. authorHolder.innerHTML = "";
  1085. authorHolder.appendChild(div);
  1086. } else if (authors === undefined) {
  1087. const div = document.createElement("div");
  1088. div.innerText = "Not yet entered";
  1089. authorHolder.innerHTML = "";
  1090. authorHolder.appendChild(div);
  1091. } else {
  1092. authorHolder.innerHTML = "";
  1093. const list = document.createElement("ul");
  1094. authorHolder.appendChild(list);
  1095. authors.forEach(author => {
  1096. const authorEntry = document.createElement("li");
  1097. if (author.url) {
  1098. const link = document.createElement("a");
  1099. link.href = author.url;
  1100. link.innerText = author.name;
  1101. authorEntry.appendChild(link);
  1102. } else {
  1103. const div = document.createElement("div");
  1104. div.innerText = author.name;
  1105. authorEntry.appendChild(div);
  1106. }
  1107. list.appendChild(authorEntry);
  1108. });
  1109. }
  1110. if (owners === []) {
  1111. const div = document.createElement("div");
  1112. div.innerText = "Unknown";
  1113. ownerHolder.innerHTML = "";
  1114. ownerHolder.appendChild(div);
  1115. } else if (owners === undefined) {
  1116. const div = document.createElement("div");
  1117. div.innerText = "Not yet entered";
  1118. ownerHolder.innerHTML = "";
  1119. ownerHolder.appendChild(div);
  1120. } else {
  1121. ownerHolder.innerHTML = "";
  1122. const list = document.createElement("ul");
  1123. ownerHolder.appendChild(list);
  1124. owners.forEach(owner => {
  1125. const ownerEntry = document.createElement("li");
  1126. if (owner.url) {
  1127. const link = document.createElement("a");
  1128. link.href = owner.url;
  1129. link.innerText = owner.name;
  1130. ownerEntry.appendChild(link);
  1131. } else {
  1132. const div = document.createElement("div");
  1133. div.innerText = owner.name;
  1134. ownerEntry.appendChild(div);
  1135. }
  1136. list.appendChild(ownerEntry);
  1137. });
  1138. }
  1139. if (source === null) {
  1140. const div = document.createElement("div");
  1141. div.innerText = "No link";
  1142. sourceHolder.innerHTML = "";
  1143. sourceHolder.appendChild(div);
  1144. } else if (source === undefined) {
  1145. const div = document.createElement("div");
  1146. div.innerText = "Not yet entered";
  1147. sourceHolder.innerHTML = "";
  1148. sourceHolder.appendChild(div);
  1149. } else {
  1150. sourceHolder.innerHTML = "";
  1151. const link = document.createElement("a");
  1152. link.style.display = "block";
  1153. link.href = source;
  1154. link.innerText = new URL(source).host;
  1155. sourceHolder.appendChild(link);
  1156. }
  1157. }
  1158. function removeEntity(element) {
  1159. if (selected == element) {
  1160. deselect();
  1161. }
  1162. if (clicked == element) {
  1163. clicked = null;
  1164. }
  1165. const option = document.querySelector("#options-selected-entity-" + element.dataset.key);
  1166. option.parentElement.removeChild(option);
  1167. delete entities[element.dataset.key];
  1168. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  1169. const topName = document.querySelector("#top-name-" + element.dataset.key);
  1170. bottomName.parentElement.removeChild(bottomName);
  1171. topName.parentElement.removeChild(topName);
  1172. element.parentElement.removeChild(element);
  1173. }
  1174. function checkEntity(entity) {
  1175. Object.values(entity.views).forEach(view => {
  1176. if (authorsOf(view.image.source) === undefined) {
  1177. console.warn("No authors: " + view.image.source);
  1178. }
  1179. });
  1180. }
  1181. function displayEntity(entity, view, x, y, selectEntity = false, refresh = false) {
  1182. checkEntity(entity);
  1183. const box = document.createElement("div");
  1184. box.classList.add("entity-box");
  1185. const img = document.createElement("img");
  1186. img.classList.add("entity-image");
  1187. img.addEventListener("dragstart", e => {
  1188. e.preventDefault();
  1189. });
  1190. const nameTag = document.createElement("div");
  1191. nameTag.classList.add("entity-name");
  1192. nameTag.innerText = entity.name;
  1193. box.appendChild(img);
  1194. box.appendChild(nameTag);
  1195. const image = entity.views[view].image;
  1196. img.src = image.source;
  1197. if (image.bottom !== undefined) {
  1198. img.style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  1199. } else {
  1200. img.style.setProperty("--offset", ((-1) * 100) + "%")
  1201. }
  1202. box.dataset.x = x;
  1203. box.dataset.y = y;
  1204. img.addEventListener("mousedown", e => { if (e.which == 1) { testClick(e); if (clicked) { e.stopPropagation() } } });
  1205. img.addEventListener("touchstart", e => {
  1206. const fakeEvent = {
  1207. target: e.target,
  1208. clientX: e.touches[0].clientX,
  1209. clientY: e.touches[0].clientY,
  1210. which: 1
  1211. };
  1212. testClick(fakeEvent);
  1213. if (clicked) { e.stopPropagation() }
  1214. });
  1215. const heightBar = document.createElement("div");
  1216. heightBar.classList.add("height-bar");
  1217. box.appendChild(heightBar);
  1218. box.id = "entity-" + entityIndex;
  1219. box.dataset.key = entityIndex;
  1220. entity.view = view;
  1221. if (entity.priority === undefined)
  1222. entity.priority = 0;
  1223. if (entity.brightness === undefined)
  1224. entity.brightness = 1;
  1225. entities[entityIndex] = entity;
  1226. entity.index = entityIndex;
  1227. const world = document.querySelector("#entities");
  1228. world.appendChild(box);
  1229. const bottomName = document.createElement("div");
  1230. bottomName.classList.add("bottom-name");
  1231. bottomName.id = "bottom-name-" + entityIndex;
  1232. bottomName.innerText = entity.name;
  1233. bottomName.addEventListener("click", () => select(box));
  1234. world.appendChild(bottomName);
  1235. const topName = document.createElement("div");
  1236. topName.classList.add("top-name");
  1237. topName.id = "top-name-" + entityIndex;
  1238. topName.innerText = entity.name;
  1239. topName.addEventListener("click", () => select(box));
  1240. world.appendChild(topName);
  1241. const entityOption = document.createElement("option");
  1242. entityOption.id = "options-selected-entity-" + entityIndex;
  1243. entityOption.value = entityIndex;
  1244. entityOption.innerText = entity.name;
  1245. document.getElementById("options-selected-entity").appendChild(entityOption);
  1246. entityIndex += 1;
  1247. if (config.autoFit) {
  1248. fitWorld();
  1249. }
  1250. if (selectEntity)
  1251. select(box);
  1252. entity.dirty = true;
  1253. if (refresh && config.autoFitAdd) {
  1254. let targets = {};
  1255. targets[entityIndex - 1] = entity;
  1256. fitEntities(targets);
  1257. }
  1258. if (refresh)
  1259. updateSizes(true);
  1260. }
  1261. window.onblur = function () {
  1262. altHeld = false;
  1263. shiftHeld = false;
  1264. }
  1265. window.onfocus = function () {
  1266. window.dispatchEvent(new Event("keydown"));
  1267. }
  1268. // thanks to https://developers.google.com/web/fundamentals/native-hardware/fullscreen
  1269. function toggleFullScreen() {
  1270. var doc = window.document;
  1271. var docEl = doc.documentElement;
  1272. var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  1273. var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
  1274. if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
  1275. requestFullScreen.call(docEl);
  1276. }
  1277. else {
  1278. cancelFullScreen.call(doc);
  1279. }
  1280. }
  1281. function handleResize() {
  1282. const oldCanvasWidth = canvasWidth;
  1283. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  1284. canvasWidth = document.querySelector("#display").clientWidth - 100;
  1285. canvasHeight = document.querySelector("#display").clientHeight - 50;
  1286. const change = oldCanvasWidth / canvasWidth;
  1287. updateSizes();
  1288. }
  1289. function prepareSidebar() {
  1290. const menubar = document.querySelector("#sidebar-menu");
  1291. [
  1292. {
  1293. name: "Show/hide sidebar",
  1294. id: "menu-toggle-sidebar",
  1295. icon: "fas fa-chevron-circle-down",
  1296. rotates: true
  1297. },
  1298. {
  1299. name: "Fullscreen",
  1300. id: "menu-fullscreen",
  1301. icon: "fas fa-compress"
  1302. },
  1303. {
  1304. name: "Clear",
  1305. id: "menu-clear",
  1306. icon: "fas fa-file"
  1307. },
  1308. {
  1309. name: "Sort by height",
  1310. id: "menu-order-height",
  1311. icon: "fas fa-sort-numeric-up"
  1312. },
  1313. {
  1314. name: "Permalink",
  1315. id: "menu-permalink",
  1316. icon: "fas fa-link"
  1317. },
  1318. {
  1319. name: "Export to clipboard",
  1320. id: "menu-export",
  1321. icon: "fas fa-share"
  1322. },
  1323. {
  1324. name: "Import from clipboard",
  1325. id: "menu-import",
  1326. icon: "fas fa-share",
  1327. classes: ["flipped"]
  1328. },
  1329. {
  1330. name: "Save",
  1331. id: "menu-save",
  1332. icon: "fas fa-download"
  1333. },
  1334. {
  1335. name: "Load",
  1336. id: "menu-load",
  1337. icon: "fas fa-upload"
  1338. },
  1339. {
  1340. name: "Load Autosave",
  1341. id: "menu-load-autosave",
  1342. icon: "fas fa-redo"
  1343. },
  1344. {
  1345. name: "Add Image",
  1346. id: "menu-add-image",
  1347. icon: "fas fa-camera"
  1348. }
  1349. ].forEach(entry => {
  1350. const buttonHolder = document.createElement("div");
  1351. buttonHolder.classList.add("menu-button-holder");
  1352. const button = document.createElement("button");
  1353. button.id = entry.id;
  1354. button.classList.add("menu-button");
  1355. const icon = document.createElement("i");
  1356. icon.classList.add(...entry.icon.split(" "));
  1357. if (entry.rotates) {
  1358. icon.classList.add("rotate-backward", "transitions");
  1359. }
  1360. if (entry.classes) {
  1361. entry.classes.forEach(cls => icon.classList.add(cls));
  1362. }
  1363. const actionText = document.createElement("span");
  1364. actionText.innerText = entry.name;
  1365. actionText.classList.add("menu-text");
  1366. const srText = document.createElement("span");
  1367. srText.classList.add("sr-only");
  1368. srText.innerText = entry.name;
  1369. button.appendChild(icon);
  1370. button.appendChild(srText);
  1371. buttonHolder.appendChild(button);
  1372. buttonHolder.appendChild(actionText);
  1373. menubar.appendChild(buttonHolder);
  1374. });
  1375. }
  1376. function checkBodyClass(cls) {
  1377. return document.body.classList.contains(cls);
  1378. }
  1379. function toggleBodyClass(cls, setting) {
  1380. if (setting) {
  1381. document.body.classList.add(cls);
  1382. } else {
  1383. document.body.classList.remove(cls);
  1384. }
  1385. }
  1386. const settingsData = {
  1387. "lock-y-axis": {
  1388. name: "Lock Y-Axis",
  1389. desc: "Keep the camera at ground-level",
  1390. type: "toggle",
  1391. default: true,
  1392. get value() {
  1393. return config.lockYAxis;
  1394. },
  1395. set value(param) {
  1396. config.lockYAxis = param;
  1397. if (param) {
  1398. config.y = 0;
  1399. updateSizes();
  1400. document.querySelector("#scroll-up").disabled = true;
  1401. document.querySelector("#scroll-down").disabled = true;
  1402. } else {
  1403. document.querySelector("#scroll-up").disabled = false;
  1404. document.querySelector("#scroll-down").disabled = false;
  1405. }
  1406. }
  1407. },
  1408. "auto-scale": {
  1409. name: "Auto-Size World",
  1410. desc: "Constantly zoom to fit the largest entity",
  1411. type: "toggle",
  1412. default: false,
  1413. get value() {
  1414. return config.autoFit;
  1415. },
  1416. set value(param) {
  1417. config.autoFit = param;
  1418. checkFitWorld();
  1419. }
  1420. },
  1421. "show-vertical-scale": {
  1422. name: "Show Vertical Scale",
  1423. desc: "Draw vertical scale marks",
  1424. type: "toggle",
  1425. default: true,
  1426. get value() {
  1427. return config.drawYAxis;
  1428. },
  1429. set value(param) {
  1430. config.drawYAxis = param;
  1431. drawScales(false);
  1432. }
  1433. },
  1434. "show-horizontal-scale": {
  1435. name: "Show Horiziontal Scale",
  1436. desc: "Draw horizontal scale marks",
  1437. type: "toggle",
  1438. default: false,
  1439. get value() {
  1440. return config.drawXAxis;
  1441. },
  1442. set value(param) {
  1443. config.drawXAxis = param;
  1444. drawScales(false);
  1445. }
  1446. },
  1447. "zoom-when-adding": {
  1448. name: "Zoom When Adding",
  1449. desc: "Zoom to fit when you add a new entity",
  1450. type: "toggle",
  1451. default: true,
  1452. get value() {
  1453. return config.autoFitAdd;
  1454. },
  1455. set value(param) {
  1456. config.autoFitAdd = param;
  1457. }
  1458. },
  1459. "zoom-when-sizing": {
  1460. name: "Zoom When Sizing",
  1461. desc: "Zoom to fit when you select an entity's size",
  1462. type: "toggle",
  1463. default: true,
  1464. get value() {
  1465. return config.autoFitSize;
  1466. },
  1467. set value(param) {
  1468. config.autoFitSize = param;
  1469. }
  1470. },
  1471. "units": {
  1472. name: "Default Units",
  1473. desc: "Which kind of unit to use by default",
  1474. type: "select",
  1475. default: "metric",
  1476. options: [
  1477. "metric",
  1478. "customary",
  1479. "relative"
  1480. ],
  1481. get value() {
  1482. return config.units;
  1483. },
  1484. set value(param) {
  1485. config.units = param;
  1486. }
  1487. },
  1488. "names": {
  1489. name: "Show Names",
  1490. desc: "Display names over entities",
  1491. type: "toggle",
  1492. default: true,
  1493. get value() {
  1494. return checkBodyClass("toggle-entity-name");
  1495. },
  1496. set value(param) {
  1497. toggleBodyClass("toggle-entity-name", param);
  1498. }
  1499. },
  1500. "bottom-names": {
  1501. name: "Bottom Names",
  1502. desc: "Display names at the bottom",
  1503. type: "toggle",
  1504. default: false,
  1505. get value() {
  1506. return checkBodyClass("toggle-bottom-name");
  1507. },
  1508. set value(param) {
  1509. toggleBodyClass("toggle-bottom-name", param);
  1510. }
  1511. },
  1512. "top-names": {
  1513. name: "Show Arrows",
  1514. desc: "Point to entities that are much larger than the current view",
  1515. type: "toggle",
  1516. default: false,
  1517. get value() {
  1518. return checkBodyClass("toggle-top-name");
  1519. },
  1520. set value(param) {
  1521. toggleBodyClass("toggle-top-name", param);
  1522. }
  1523. },
  1524. "height-bars": {
  1525. name: "Height Bars",
  1526. desc: "Draw dashed lines to the top of each entity",
  1527. type: "toggle",
  1528. default: false,
  1529. get value() {
  1530. return checkBodyClass("toggle-height-bars");
  1531. },
  1532. set value(param) {
  1533. toggleBodyClass("toggle-height-bars", param);
  1534. }
  1535. },
  1536. "glowing-entities": {
  1537. name: "Glowing Edges",
  1538. desc: "Makes all entities glow",
  1539. type: "toggle",
  1540. default: false,
  1541. get value() {
  1542. return checkBodyClass("toggle-entity-glow");
  1543. },
  1544. set value(param) {
  1545. toggleBodyClass("toggle-entity-glow", param);
  1546. }
  1547. },
  1548. "solid-ground": {
  1549. name: "Solid Ground",
  1550. desc: "Draw solid ground at the y=0 line",
  1551. type: "toggle",
  1552. default: false,
  1553. get value() {
  1554. return checkBodyClass("toggle-bottom-cover");
  1555. },
  1556. set value(param) {
  1557. toggleBodyClass("toggle-bottom-cover", param);
  1558. }
  1559. },
  1560. }
  1561. function getBoundingBox(entities, margin = 0.05) {
  1562. }
  1563. function prepareSettings(userSettings) {
  1564. const menubar = document.querySelector("#settings-menu");
  1565. Object.entries(settingsData).forEach(([id, entry]) => {
  1566. const holder = document.createElement("label");
  1567. holder.classList.add("settings-holder");
  1568. const input = document.createElement("input");
  1569. input.id = "setting-" + id;
  1570. const name = document.createElement("label");
  1571. name.innerText = entry.name;
  1572. name.classList.add("settings-name");
  1573. name.setAttribute("for", input.id);
  1574. const desc = document.createElement("label");
  1575. desc.innerText = entry.desc;
  1576. desc.classList.add("settings-desc");
  1577. desc.setAttribute("for", input.id);
  1578. if (entry.type == "toggle") {
  1579. input.type = "checkbox";
  1580. input.checked = userSettings[id] === undefined ? entry.default : userSettings[id];
  1581. holder.setAttribute("for", input.id);
  1582. holder.appendChild(name);
  1583. holder.appendChild(input);
  1584. holder.appendChild(desc);
  1585. menubar.appendChild(holder);
  1586. const update = () => {
  1587. if (input.checked) {
  1588. holder.classList.add("enabled");
  1589. holder.classList.remove("disabled");
  1590. } else {
  1591. holder.classList.remove("enabled");
  1592. holder.classList.add("disabled");
  1593. }
  1594. entry.value = input.checked;
  1595. }
  1596. update();
  1597. input.addEventListener("change", update);
  1598. } else if (entry.type == "select") {
  1599. // we don't use the input element we made!
  1600. const select = document.createElement("select");
  1601. select.id = "setting-" + id;
  1602. entry.options.forEach(choice => {
  1603. const option = document.createElement("option");
  1604. option.innerText = choice;
  1605. select.appendChild(option);
  1606. })
  1607. select.value = userSettings[id] === undefined ? entry.default : userSettings[id];
  1608. holder.appendChild(name);
  1609. holder.appendChild(select);
  1610. holder.appendChild(desc);
  1611. menubar.appendChild(holder);
  1612. holder.classList.add("enabled");
  1613. const update = () => {
  1614. entry.value = select.value;
  1615. }
  1616. update();
  1617. select.addEventListener("change", update);
  1618. }
  1619. })
  1620. }
  1621. function prepareMenu() {
  1622. prepareSidebar();
  1623. if (checkHelpDate()) {
  1624. document.querySelector("#open-help").classList.add("highlighted");
  1625. }
  1626. }
  1627. function getUserSettings() {
  1628. try {
  1629. const settings = JSON.parse(localStorage.getItem("settings"));
  1630. return settings === null ? {} : settings;
  1631. } catch {
  1632. return {};
  1633. }
  1634. }
  1635. function exportUserSettings() {
  1636. const settings = {};
  1637. Object.entries(settingsData).forEach(([id, entry]) => {
  1638. settings[id] = entry.value;
  1639. });
  1640. return settings;
  1641. }
  1642. function setUserSettings(settings) {
  1643. try {
  1644. localStorage.setItem("settings", JSON.stringify(settings));
  1645. } catch {
  1646. // :(
  1647. }
  1648. }
  1649. const lastHelpChange = 1587847743294;
  1650. function checkHelpDate() {
  1651. try {
  1652. const old = localStorage.getItem("help-viewed");
  1653. if (old === null || old < lastHelpChange) {
  1654. return true;
  1655. }
  1656. return false;
  1657. } catch {
  1658. console.warn("Could not set the help-viewed date");
  1659. return false;
  1660. }
  1661. }
  1662. function setHelpDate() {
  1663. try {
  1664. localStorage.setItem("help-viewed", Date.now());
  1665. } catch {
  1666. console.warn("Could not set the help-viewed date");
  1667. }
  1668. }
  1669. function doYScroll() {
  1670. const worldHeight = config.height.toNumber("meters");
  1671. config.y += scrollDirection * worldHeight / 180;
  1672. updateSizes();
  1673. scrollDirection *= 1.05;
  1674. }
  1675. function doXScroll() {
  1676. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  1677. config.x += scrollDirection * worldWidth / 180 ;
  1678. updateSizes();
  1679. scrollDirection *= 1.05;
  1680. }
  1681. function doZoom() {
  1682. const oldHeight = config.height;
  1683. setWorldHeight(oldHeight, math.multiply(oldHeight, 1 + zoomDirection / 10));
  1684. zoomDirection *= 1.05;
  1685. }
  1686. function doSize() {
  1687. if (selected) {
  1688. const entity = entities[selected.dataset.key];
  1689. const oldHeight = entity.views[entity.view].height;
  1690. entity.views[entity.view].height = math.multiply(oldHeight, sizeDirection < 0 ? -1/sizeDirection : sizeDirection);
  1691. entity.dirty = true;
  1692. updateEntityOptions(entity, entity.view);
  1693. updateViewOptions(entity, entity.view);
  1694. updateSizes(true);
  1695. sizeDirection *= 1.01;
  1696. const ownHeight = entity.views[entity.view].height.toNumber("meters");
  1697. const worldHeight = config.height.toNumber("meters");
  1698. if (ownHeight > worldHeight) {
  1699. setWorldHeight(config.height, entity.views[entity.view].height)
  1700. } else if (ownHeight * 10 < worldHeight) {
  1701. setWorldHeight(config.height, math.multiply(entity.views[entity.view].height, 10));
  1702. }
  1703. }
  1704. }
  1705. function prepareHelp() {
  1706. const toc = document.querySelector("#table-of-contents");
  1707. const holder = document.querySelector("#help-contents-holder");
  1708. document.querySelectorAll("#help-contents h2").forEach(header => {
  1709. const li = document.createElement("li");
  1710. li.innerText = header.textContent;
  1711. li.addEventListener("click", e => {
  1712. holder.scrollTop = header.offsetTop;
  1713. });
  1714. toc.appendChild(li);
  1715. });
  1716. }
  1717. document.addEventListener("DOMContentLoaded", () => {
  1718. prepareMenu();
  1719. prepareEntities();
  1720. prepareHelp();
  1721. document.querySelector("#open-help").addEventListener("click", e => {
  1722. setHelpDate();
  1723. document.querySelector("#help-menu").classList.add("visible");
  1724. document.querySelector("#open-help").classList.remove("highlighted");
  1725. });
  1726. document.querySelector("#close-help").addEventListener("click", e => {
  1727. document.querySelector("#help-menu").classList.remove("visible");
  1728. });
  1729. document.querySelector("#copy-screenshot").addEventListener("click", e => {
  1730. copyScreenshot();
  1731. toast("Copied to clipboard!");
  1732. });
  1733. document.querySelector("#save-screenshot").addEventListener("click", e => {
  1734. saveScreenshot();
  1735. });
  1736. document.querySelector("#open-screenshot").addEventListener("click", e => {
  1737. openScreenshot();
  1738. });
  1739. document.querySelector("#toggle-menu").addEventListener("click", e => {
  1740. const popoutMenu = document.querySelector("#sidebar-menu");
  1741. if (popoutMenu.classList.contains("visible")) {
  1742. popoutMenu.classList.remove("visible");
  1743. } else {
  1744. document.querySelectorAll(".popout-menu").forEach(menu => menu.classList.remove("visible"));
  1745. const rect = e.target.getBoundingClientRect();
  1746. popoutMenu.classList.add("visible");
  1747. popoutMenu.style.left = rect.x + rect.width + 10 + "px";
  1748. popoutMenu.style.top = rect.y + rect.height + 10 + "px";
  1749. }
  1750. e.stopPropagation();
  1751. });
  1752. document.querySelector("#sidebar-menu").addEventListener("click", e => {
  1753. e.stopPropagation();
  1754. });
  1755. document.addEventListener("click", e => {
  1756. document.querySelector("#sidebar-menu").classList.remove("visible");
  1757. });
  1758. document.querySelector("#toggle-settings").addEventListener("click", e => {
  1759. const popoutMenu = document.querySelector("#settings-menu");
  1760. if (popoutMenu.classList.contains("visible")) {
  1761. popoutMenu.classList.remove("visible");
  1762. } else {
  1763. document.querySelectorAll(".popout-menu").forEach(menu => menu.classList.remove("visible"));
  1764. const rect = e.target.getBoundingClientRect();
  1765. popoutMenu.classList.add("visible");
  1766. popoutMenu.style.left = rect.x + rect.width + 10 + "px";
  1767. popoutMenu.style.top = rect.y + rect.height + 10 + "px";
  1768. }
  1769. e.stopPropagation();
  1770. });
  1771. document.querySelector("#settings-menu").addEventListener("click", e => {
  1772. e.stopPropagation();
  1773. });
  1774. document.addEventListener("click", e => {
  1775. document.querySelector("#settings-menu").classList.remove("visible");
  1776. });
  1777. window.addEventListener("unload", () => {
  1778. saveScene("autosave");
  1779. setUserSettings(exportUserSettings());
  1780. });
  1781. document.querySelector("#options-selected-entity").addEventListener("input", e => {
  1782. if (e.target.value == "None") {
  1783. deselect()
  1784. } else {
  1785. select(document.querySelector("#entity-" + e.target.value));
  1786. }
  1787. });
  1788. document.querySelector("#menu-toggle-sidebar").addEventListener("click", e => {
  1789. const sidebar = document.querySelector("#options");
  1790. if (sidebar.classList.contains("hidden")) {
  1791. sidebar.classList.remove("hidden");
  1792. e.target.classList.remove("rotate-forward");
  1793. e.target.classList.add("rotate-backward");
  1794. } else {
  1795. sidebar.classList.add("hidden");
  1796. e.target.classList.add("rotate-forward");
  1797. e.target.classList.remove("rotate-backward");
  1798. }
  1799. handleResize();
  1800. });
  1801. document.querySelector("#menu-fullscreen").addEventListener("click", toggleFullScreen);
  1802. document.querySelector("#options-order-forward").addEventListener("click", e => {
  1803. if (selected) {
  1804. entities[selected.dataset.key].priority += 1;
  1805. }
  1806. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  1807. updateSizes();
  1808. });
  1809. document.querySelector("#options-order-back").addEventListener("click", e => {
  1810. if (selected) {
  1811. entities[selected.dataset.key].priority -= 1;
  1812. }
  1813. document.querySelector("#options-order-display").innerText = entities[selected.dataset.key].priority;
  1814. updateSizes();
  1815. });
  1816. document.querySelector("#options-brightness-up").addEventListener("click", e => {
  1817. if (selected) {
  1818. entities[selected.dataset.key].brightness += 1;
  1819. }
  1820. document.querySelector("#options-brightness-display").innerText = entities[selected.dataset.key].brightness;
  1821. updateSizes();
  1822. });
  1823. document.querySelector("#options-brightness-down").addEventListener("click", e => {
  1824. if (selected) {
  1825. entities[selected.dataset.key].brightness = Math.max(entities[selected.dataset.key].brightness -1, 0);
  1826. }
  1827. document.querySelector("#options-brightness-display").innerText = entities[selected.dataset.key].brightness;
  1828. updateSizes();
  1829. });
  1830. document.querySelector("#options-flip").addEventListener("click", e => {
  1831. if (selected) {
  1832. selected.querySelector(".entity-image").classList.toggle("flipped");
  1833. }
  1834. document.querySelector("#options-brightness-display").innerText = entities[selected.dataset.key].brightness;
  1835. updateSizes();
  1836. });
  1837. const sceneChoices = document.querySelector("#scene-choices");
  1838. Object.entries(scenes).forEach(([id, scene]) => {
  1839. const option = document.createElement("option");
  1840. option.innerText = id;
  1841. option.value = id;
  1842. sceneChoices.appendChild(option);
  1843. });
  1844. document.querySelector("#load-scene").addEventListener("click", e => {
  1845. const chosen = sceneChoices.value;
  1846. removeAllEntities();
  1847. scenes[chosen]();
  1848. });
  1849. entityX = document.querySelector("#entities").getBoundingClientRect().x;
  1850. canvasWidth = document.querySelector("#display").clientWidth - 100;
  1851. canvasHeight = document.querySelector("#display").clientHeight - 50;
  1852. document.querySelector("#options-height-value").addEventListener("change", e => {
  1853. updateWorldHeight();
  1854. })
  1855. document.querySelector("#options-height-value").addEventListener("keydown", e => {
  1856. e.stopPropagation();
  1857. })
  1858. const unitSelector = document.querySelector("#options-height-unit");
  1859. Object.entries(unitChoices.length).forEach(([group, entries]) => {
  1860. const optGroup = document.createElement("optgroup");
  1861. optGroup.label = group;
  1862. unitSelector.appendChild(optGroup);
  1863. entries.forEach(entry => {
  1864. const option = document.createElement("option");
  1865. option.innerText = entry;
  1866. // we haven't loaded user settings yet, so we can't choose the unit just yet
  1867. unitSelector.appendChild(option);
  1868. })
  1869. });
  1870. unitSelector.setAttribute("oldUnit", "meters");
  1871. unitSelector.addEventListener("input", e => {
  1872. checkFitWorld();
  1873. const scaleInput = document.querySelector("#options-height-value");
  1874. const newVal = math.unit(scaleInput.value, unitSelector.getAttribute("oldUnit")).toNumber(e.target.value);
  1875. setNumericInput(scaleInput, newVal);
  1876. updateWorldHeight();
  1877. unitSelector.setAttribute("oldUnit", unitSelector.value);
  1878. });
  1879. param = new URL(window.location.href).searchParams.get("scene");
  1880. if (param === null) {
  1881. scenes["Default"]();
  1882. }
  1883. else {
  1884. try {
  1885. const data = JSON.parse(b64DecodeUnicode(param));
  1886. if (data.entities === undefined) {
  1887. return;
  1888. }
  1889. if (data.world === undefined) {
  1890. return;
  1891. }
  1892. importScene(data);
  1893. } catch (err) {
  1894. console.error(err);
  1895. scenes["Default"]();
  1896. // probably wasn't valid data
  1897. }
  1898. }
  1899. document.querySelector("#world").addEventListener("wheel", e => {
  1900. if (shiftHeld) {
  1901. if (selected) {
  1902. const dir = e.deltaY > 0 ? 10 / 11 : 11 / 10;
  1903. const entity = entities[selected.dataset.key];
  1904. entity.views[entity.view].height = math.multiply(entity.views[entity.view].height, dir);
  1905. entity.dirty = true;
  1906. updateEntityOptions(entity, entity.view);
  1907. updateViewOptions(entity, entity.view);
  1908. updateSizes(true);
  1909. } else {
  1910. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  1911. config.x += (e.deltaY > 0 ? 1 : -1) * worldWidth / 20 ;
  1912. updateSizes();
  1913. updateSizes();
  1914. }
  1915. } else {
  1916. if (config.autoFit) {
  1917. toastRateLimit("Zoom is locked! Check Settings to disable.", "zoom-lock", 1000);
  1918. } else {
  1919. const dir = e.deltaY < 0 ? 10 / 11 : 11 / 10;
  1920. const change = config.height.toNumber("meters") - math.multiply(config.height, dir).toNumber("meters");
  1921. setWorldHeight(config.height, math.multiply(config.height, dir));
  1922. updateWorldOptions();
  1923. if (!config.lockYAxis) {
  1924. config.y += change / 2;
  1925. }
  1926. }
  1927. }
  1928. checkFitWorld();
  1929. })
  1930. document.querySelector("#world").addEventListener("mousedown", e => {
  1931. // only middle mouse clicks
  1932. if (e.which == 2) {
  1933. panning = true;
  1934. panOffsetX = e.clientX;
  1935. panOffsetY = e.clientY;
  1936. Object.keys(entities).forEach(key => {
  1937. document.querySelector("#entity-" + key).classList.add("no-transition");
  1938. });
  1939. }
  1940. });
  1941. document.addEventListener("mouseup", e => {
  1942. if (e.which == 2) {
  1943. panning = false;
  1944. Object.keys(entities).forEach(key => {
  1945. document.querySelector("#entity-" + key).classList.remove("no-transition");
  1946. });
  1947. }
  1948. });
  1949. document.querySelector("#world").addEventListener("touchstart", e => {
  1950. panning = true;
  1951. panOffsetX = e.touches[0].clientX;
  1952. panOffsetY = e.touches[0].clientY;
  1953. e.preventDefault();
  1954. Object.keys(entities).forEach(key => {
  1955. document.querySelector("#entity-" + key).classList.add("no-transition");
  1956. });
  1957. });
  1958. document.querySelector("#world").addEventListener("touchend", e => {
  1959. panning = false;
  1960. Object.keys(entities).forEach(key => {
  1961. document.querySelector("#entity-" + key).classList.remove("no-transition");
  1962. });
  1963. });
  1964. document.querySelector("body").appendChild(testCtx.canvas);
  1965. updateSizes();
  1966. world.addEventListener("mousedown", e => deselect(e));
  1967. world.addEventListener("touchstart", e => deselect({
  1968. which: 1,
  1969. }));
  1970. document.querySelector("#entities").addEventListener("mousedown", deselect);
  1971. document.querySelector("#display").addEventListener("mousedown", deselect);
  1972. document.addEventListener("mouseup", e => clickUp(e));
  1973. document.addEventListener("touchend", e => {
  1974. const fakeEvent = {
  1975. target: e.target,
  1976. clientX: e.changedTouches[0].clientX,
  1977. clientY: e.changedTouches[0].clientY,
  1978. which: 1
  1979. };
  1980. clickUp(fakeEvent);
  1981. });
  1982. const viewList = document.querySelector("#entity-view");
  1983. document.querySelector("#entity-view").addEventListener("input", e => {
  1984. const entity = entities[selected.dataset.key];
  1985. entity.view = e.target.value;
  1986. const image = entities[selected.dataset.key].views[e.target.value].image;
  1987. selected.querySelector(".entity-image").src = image.source;
  1988. configViewOptions(entity, entity.view);
  1989. displayAttribution(image.source);
  1990. if (image.bottom !== undefined) {
  1991. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  1992. } else {
  1993. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1) * 100) + "%")
  1994. }
  1995. updateSizes();
  1996. updateEntityOptions(entities[selected.dataset.key], e.target.value);
  1997. updateViewOptions(entities[selected.dataset.key], e.target.value);
  1998. });
  1999. document.querySelector("#entity-view").addEventListener("input", e => {
  2000. if (viewList.options[viewList.selectedIndex].classList.contains("nsfw")) {
  2001. viewList.classList.add("nsfw");
  2002. } else {
  2003. viewList.classList.remove("nsfw");
  2004. }
  2005. })
  2006. clearViewList();
  2007. document.querySelector("#menu-clear").addEventListener("click", e => {
  2008. removeAllEntities();
  2009. });
  2010. document.querySelector("#delete-entity").disabled = true;
  2011. document.querySelector("#delete-entity").addEventListener("click", e => {
  2012. if (selected) {
  2013. removeEntity(selected);
  2014. selected = null;
  2015. }
  2016. });
  2017. document.querySelector("#menu-order-height").addEventListener("click", e => {
  2018. const order = Object.keys(entities).sort((a, b) => {
  2019. const entA = entities[a];
  2020. const entB = entities[b];
  2021. const viewA = entA.view;
  2022. const viewB = entB.view;
  2023. const heightA = entA.views[viewA].height.to("meter").value;
  2024. const heightB = entB.views[viewB].height.to("meter").value;
  2025. return heightA - heightB;
  2026. });
  2027. arrangeEntities(order);
  2028. });
  2029. // TODO: write some generic logic for this lol
  2030. document.querySelector("#scroll-left").addEventListener("mousedown", e => {
  2031. scrollDirection = -1;
  2032. clearInterval(scrollHandle);
  2033. scrollHandle = setInterval(doXScroll, 1000 / 20);
  2034. e.stopPropagation();
  2035. });
  2036. document.querySelector("#scroll-right").addEventListener("mousedown", e => {
  2037. scrollDirection = 1;
  2038. clearInterval(scrollHandle);
  2039. scrollHandle = setInterval(doXScroll, 1000 / 20);
  2040. e.stopPropagation();
  2041. });
  2042. document.querySelector("#scroll-left").addEventListener("touchstart", e => {
  2043. scrollDirection = -1;
  2044. clearInterval(scrollHandle);
  2045. scrollHandle = setInterval(doXScroll, 1000 / 20);
  2046. e.stopPropagation();
  2047. });
  2048. document.querySelector("#scroll-right").addEventListener("touchstart", e => {
  2049. scrollDirection = 1;
  2050. clearInterval(scrollHandle);
  2051. scrollHandle = setInterval(doXScroll, 1000 / 20);
  2052. e.stopPropagation();
  2053. });
  2054. document.querySelector("#scroll-up").addEventListener("mousedown", e => {
  2055. scrollDirection = 1;
  2056. clearInterval(scrollHandle);
  2057. scrollHandle = setInterval(doYScroll, 1000 / 20);
  2058. e.stopPropagation();
  2059. });
  2060. document.querySelector("#scroll-down").addEventListener("mousedown", e => {
  2061. scrollDirection = -1;
  2062. clearInterval(scrollHandle);
  2063. scrollHandle = setInterval(doYScroll, 1000 / 20);
  2064. e.stopPropagation();
  2065. });
  2066. document.querySelector("#scroll-up").addEventListener("touchstart", e => {
  2067. scrollDirection = 1;
  2068. clearInterval(scrollHandle);
  2069. scrollHandle = setInterval(doYScroll, 1000 / 20);
  2070. e.stopPropagation();
  2071. });
  2072. document.querySelector("#scroll-down").addEventListener("touchstart", e => {
  2073. scrollDirection = -1;
  2074. clearInterval(scrollHandle);
  2075. scrollHandle = setInterval(doYScroll, 1000 / 20);
  2076. e.stopPropagation();
  2077. });
  2078. document.addEventListener("mouseup", e => {
  2079. clearInterval(scrollHandle);
  2080. scrollHandle = null;
  2081. });
  2082. document.addEventListener("touchend", e => {
  2083. clearInterval(scrollHandle);
  2084. scrollHandle = null;
  2085. });
  2086. document.querySelector("#zoom-in").addEventListener("mousedown", e => {
  2087. zoomDirection = -1;
  2088. clearInterval(zoomHandle);
  2089. zoomHandle = setInterval(doZoom, 1000 / 20);
  2090. e.stopPropagation();
  2091. });
  2092. document.querySelector("#zoom-out").addEventListener("mousedown", e => {
  2093. zoomDirection = 1;
  2094. clearInterval(zoomHandle);
  2095. zoomHandle = setInterval(doZoom, 1000 / 20);
  2096. e.stopPropagation();
  2097. });
  2098. document.querySelector("#zoom-in").addEventListener("touchstart", e => {
  2099. zoomDirection = -1;
  2100. clearInterval(zoomHandle);
  2101. zoomHandle = setInterval(doZoom, 1000 / 20);
  2102. e.stopPropagation();
  2103. });
  2104. document.querySelector("#zoom-out").addEventListener("touchstart", e => {
  2105. zoomDirection = 1;
  2106. clearInterval(zoomHandle);
  2107. zoomHandle = setInterval(doZoom, 1000 / 20);
  2108. e.stopPropagation();
  2109. });
  2110. document.addEventListener("mouseup", e => {
  2111. clearInterval(zoomHandle);
  2112. zoomHandle = null;
  2113. });
  2114. document.addEventListener("touchend", e => {
  2115. clearInterval(zoomHandle);
  2116. zoomHandle = null;
  2117. });
  2118. document.querySelector("#shrink").addEventListener("mousedown", e => {
  2119. sizeDirection = -1;
  2120. clearInterval(sizeHandle);
  2121. sizeHandle = setInterval(doSize, 1000 / 20);
  2122. e.stopPropagation();
  2123. });
  2124. document.querySelector("#grow").addEventListener("mousedown", e => {
  2125. sizeDirection = 1;
  2126. clearInterval(sizeHandle);
  2127. sizeHandle = setInterval(doSize, 1000 / 20);
  2128. e.stopPropagation();
  2129. });
  2130. document.querySelector("#shrink").addEventListener("touchstart", e => {
  2131. sizeDirection = -1;
  2132. clearInterval(sizeHandle);
  2133. sizeHandle = setInterval(doSize, 1000 / 20);
  2134. e.stopPropagation();
  2135. });
  2136. document.querySelector("#grow").addEventListener("touchstart", e => {
  2137. sizeDirection = 1;
  2138. clearInterval(sizeHandle);
  2139. sizeHandle = setInterval(doSize, 1000 / 20);
  2140. e.stopPropagation();
  2141. });
  2142. document.addEventListener("mouseup", e => {
  2143. clearInterval(sizeHandle);
  2144. sizeHandle = null;
  2145. });
  2146. document.addEventListener("touchend", e => {
  2147. clearInterval(sizeHandle);
  2148. sizeHandle = null;
  2149. });
  2150. document.querySelector("#fit").addEventListener("click", e => {
  2151. if (selected) {
  2152. let targets = {};
  2153. targets[selected.dataset.key] = entities[selected.dataset.key];
  2154. fitEntities(targets);
  2155. }
  2156. });
  2157. document.querySelector("#fit").addEventListener("mousedown", e => {
  2158. e.stopPropagation();
  2159. });
  2160. document.querySelector("#fit").addEventListener("touchstart", e => {
  2161. e.stopPropagation();
  2162. });
  2163. document.querySelector("#options-world-fit").addEventListener("click", () => fitWorld(true));
  2164. document.querySelector("#options-reset-pos-x").addEventListener("click", () => { config.x = 0; updateSizes(); });
  2165. document.querySelector("#options-reset-pos-y").addEventListener("click", () => { config.y = 0; updateSizes(); });
  2166. document.addEventListener("keydown", e => {
  2167. if (e.key == "Delete") {
  2168. if (selected) {
  2169. removeEntity(selected);
  2170. selected = null;
  2171. }
  2172. }
  2173. })
  2174. document.addEventListener("keydown", e => {
  2175. if (e.key == "Shift") {
  2176. shiftHeld = true;
  2177. e.preventDefault();
  2178. } else if (e.key == "Alt") {
  2179. altHeld = true;
  2180. movingInBounds = false; // don't snap the object back in bounds when we let go
  2181. e.preventDefault();
  2182. }
  2183. });
  2184. document.addEventListener("keyup", e => {
  2185. if (e.key == "Shift") {
  2186. shiftHeld = false;
  2187. e.preventDefault();
  2188. } else if (e.key == "Alt") {
  2189. altHeld = false;
  2190. e.preventDefault();
  2191. }
  2192. });
  2193. window.addEventListener("resize", handleResize);
  2194. // TODO: further investigate why the tool initially starts out with wrong
  2195. // values under certain circumstances (seems to be narrow aspect ratios -
  2196. // maybe the menu bar is animating when it shouldn't)
  2197. setTimeout(handleResize, 250);
  2198. setTimeout(handleResize, 500);
  2199. setTimeout(handleResize, 750);
  2200. setTimeout(handleResize, 1000);
  2201. document.querySelector("#menu-permalink").addEventListener("click", e => {
  2202. linkScene();
  2203. });
  2204. document.querySelector("#menu-export").addEventListener("click", e => {
  2205. copyScene();
  2206. });
  2207. document.querySelector("#menu-import").addEventListener("click", e => {
  2208. pasteScene();
  2209. });
  2210. document.querySelector("#menu-save").addEventListener("click", e => {
  2211. saveScene();
  2212. });
  2213. document.querySelector("#menu-load").addEventListener("click", e => {
  2214. loadScene();
  2215. });
  2216. document.querySelector("#menu-load-autosave").addEventListener("click", e => {
  2217. loadScene("autosave");
  2218. });
  2219. document.querySelector("#menu-add-image").addEventListener("click", e => {
  2220. document.querySelector("#file-upload-picker").click();
  2221. });
  2222. document.querySelector("#file-upload-picker").addEventListener("change", e => {
  2223. if (e.target.files.length > 0) {
  2224. for (let i=0; i<e.target.files.length; i++) {
  2225. customEntityFromFile(e.target.files[i]);
  2226. }
  2227. }
  2228. })
  2229. document.addEventListener("paste", e => {
  2230. let index = 0;
  2231. let item = null;
  2232. let found = false;
  2233. for (; index < e.clipboardData.items.length; index++) {
  2234. item = e.clipboardData.items[index];
  2235. if (item.type == "image/png") {
  2236. found = true;
  2237. break;
  2238. }
  2239. }
  2240. if (!found) {
  2241. return;
  2242. }
  2243. let url = null;
  2244. const file = item.getAsFile();
  2245. customEntityFromFile(file);
  2246. });
  2247. document.querySelector("#world").addEventListener("dragover", e => {
  2248. e.preventDefault();
  2249. })
  2250. document.querySelector("#world").addEventListener("drop", e => {
  2251. e.preventDefault();
  2252. if (e.dataTransfer.files.length > 0) {
  2253. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  2254. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  2255. let coords = pix2pos({x: e.clientX-entX, y: e.clientY-entY});
  2256. customEntityFromFile(e.dataTransfer.files[0], coords.x, coords.y);
  2257. }
  2258. })
  2259. clearEntityOptions();
  2260. clearViewOptions();
  2261. clearAttribution();
  2262. // we do this last because configuring settings can cause things
  2263. // to happen (e.g. auto-fit)
  2264. prepareSettings(getUserSettings());
  2265. // now that we have this loaded, we can set it
  2266. document.querySelector("#options-height-unit").setAttribute("oldUnit", defaultUnits.length[config.units]);
  2267. document.querySelector("#options-height-unit").value = defaultUnits.length[config.units];
  2268. // ...and then update the world height by setting off an input event
  2269. document.querySelector("#options-height-unit").dispatchEvent(new Event('input', {
  2270. }));
  2271. });
  2272. function customEntityFromFile(file, x=0.5, y=0.5) {
  2273. file.arrayBuffer().then(buf => {
  2274. arr = new Uint8Array(buf);
  2275. blob = new Blob([arr], {type: file.type });
  2276. url = window.URL.createObjectURL(blob)
  2277. makeCustomEntity(url, x, y);
  2278. });
  2279. }
  2280. function makeCustomEntity(url, x=0.5, y=0.5) {
  2281. const maker = createEntityMaker(
  2282. {
  2283. name: "Custom Entity"
  2284. },
  2285. {
  2286. custom: {
  2287. attributes: {
  2288. height: {
  2289. name: "Height",
  2290. power: 1,
  2291. type: "length",
  2292. base: math.unit(6, "feet")
  2293. }
  2294. },
  2295. image: {
  2296. source: url
  2297. },
  2298. name: "Image",
  2299. info: {},
  2300. rename: false
  2301. }
  2302. },
  2303. []
  2304. );
  2305. const entity = maker.constructor();
  2306. entity.scale = config.height.toNumber("feet") / 20;
  2307. entity.ephemeral = true;
  2308. displayEntity(entity, "custom", x, y, true, true);
  2309. }
  2310. const filterDefs = {
  2311. none: {
  2312. id: "none",
  2313. name: "No Filter",
  2314. extract: maker => [],
  2315. render: name => name,
  2316. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  2317. },
  2318. author: {
  2319. id: "author",
  2320. name: "Authors",
  2321. extract: maker => maker.authors ? maker.authors : [],
  2322. render: author => attributionData.people[author].name,
  2323. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  2324. },
  2325. owner: {
  2326. id: "owner",
  2327. name: "Owners",
  2328. extract: maker => maker.owners ? maker.owners : [],
  2329. render: owner => attributionData.people[owner].name,
  2330. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  2331. },
  2332. species: {
  2333. id: "species",
  2334. name: "Species",
  2335. extract: maker => maker.info && maker.info.species ? getSpeciesInfo(maker.info.species) : [],
  2336. render: species => speciesData[species].name,
  2337. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  2338. },
  2339. tags: {
  2340. id: "tags",
  2341. name: "Tags",
  2342. extract: maker => maker.info && maker.info.tags ? maker.info.tags : [],
  2343. render: tag => tagDefs[tag],
  2344. sort: (tag1, tag2) => tag1[1].localeCompare(tag2[1])
  2345. },
  2346. size: {
  2347. id: "size",
  2348. name: "Normal Size",
  2349. extract: maker => maker.sizes && maker.sizes.length > 0 ? Array.from(maker.sizes.reduce((result, size) => {
  2350. if (result && !size.default) {
  2351. return result;
  2352. }
  2353. let meters = size.height.toNumber("meters");
  2354. if (meters < 1e-1) {
  2355. return ["micro"];
  2356. } else if (meters < 1e1) {
  2357. return ["moderate"];
  2358. } else {
  2359. return ["macro"];
  2360. }
  2361. }, null)) : [],
  2362. render: tag => { return {
  2363. "micro": "Micro",
  2364. "moderate": "Moderate",
  2365. "macro": "Macro"
  2366. }[tag]},
  2367. sort: (tag1, tag2) => {
  2368. const order = {
  2369. "micro": 0,
  2370. "moderate": 1,
  2371. "macro": 2
  2372. };
  2373. return order[tag1[0]] - order[tag2[0]];
  2374. }
  2375. },
  2376. allSizes: {
  2377. id: "allSizes",
  2378. name: "Possible Size",
  2379. extract: maker => maker.sizes ? Array.from(maker.sizes.reduce((set, size) => {
  2380. const height = size.height;
  2381. let result = Object.entries(sizeCategories).reduce((result, [name, value]) => {
  2382. if (result) {
  2383. return result;
  2384. } else {
  2385. if (math.compare(height, value) <= 0) {
  2386. return name;
  2387. }
  2388. }
  2389. }, null);
  2390. set.add(result ? result : "infinite");
  2391. return set;
  2392. }, new Set())) : [],
  2393. render: tag => tag[0].toUpperCase() + tag.slice(1),
  2394. sort: (tag1, tag2) => {
  2395. const order = [
  2396. "atomic", "microscopic", "tiny", "small", "moderate", "large", "macro", "megamacro", "planetary", "stellar",
  2397. "galactic", "universal", "omniversal", "infinite"
  2398. ]
  2399. return order.indexOf(tag1[0]) - order.indexOf(tag2[0]);
  2400. }
  2401. }
  2402. }
  2403. const sizeCategories = {
  2404. "atomic": math.unit(100, "angstroms"),
  2405. "microscopic": math.unit(100, "micrometers"),
  2406. "tiny": math.unit(100, "millimeters"),
  2407. "small": math.unit(1, "meter"),
  2408. "moderate": math.unit(3, "meters"),
  2409. "large": math.unit(10, "meters"),
  2410. "macro": math.unit(300, "meters"),
  2411. "megamacro": math.unit(1000, "kilometers"),
  2412. "planetary": math.unit(10, "earths"),
  2413. "stellar": math.unit(10, "solarradii"),
  2414. "galactic": math.unit(10, "galaxies"),
  2415. "universal": math.unit(10, "universes"),
  2416. "omniversal": math.unit(10, "multiverses")
  2417. };
  2418. function prepareEntities() {
  2419. availableEntities["buildings"] = makeBuildings();
  2420. availableEntities["characters"] = makeCharacters();
  2421. availableEntities["cities"] = makeCities();
  2422. availableEntities["fiction"] = makeFiction();
  2423. availableEntities["food"] = makeFood();
  2424. availableEntities["landmarks"] = makeLandmarks();
  2425. availableEntities["naturals"] = makeNaturals();
  2426. availableEntities["objects"] = makeObjects();
  2427. availableEntities["dildos"] = makeDildos();
  2428. availableEntities["pokemon"] = makePokemon();
  2429. availableEntities["species"] = makeSpecies();
  2430. availableEntities["vehicles"] = makeVehicles();
  2431. availableEntities["characters"].sort((x, y) => {
  2432. return x.name.toLowerCase() < y.name.toLowerCase() ? -1 : 1
  2433. });
  2434. const holder = document.querySelector("#spawners");
  2435. const filterHolder = document.querySelector("#filters");
  2436. const categorySelect = document.createElement("select");
  2437. categorySelect.id = "category-picker";
  2438. const filterSelect = document.createElement("select");
  2439. filterSelect.id = "filter-picker";
  2440. holder.appendChild(categorySelect);
  2441. filterHolder.appendChild(filterSelect);
  2442. const filterSets = {};
  2443. Object.values(filterDefs).forEach(filter => {
  2444. filterSets[filter.id] = new Set();
  2445. })
  2446. Object.entries(availableEntities).forEach(([category, entityList]) => {
  2447. const select = document.createElement("select");
  2448. select.id = "create-entity-" + category;
  2449. select.classList.add("entity-select");
  2450. for (let i = 0; i < entityList.length; i++) {
  2451. const entity = entityList[i];
  2452. const option = document.createElement("option");
  2453. option.value = i;
  2454. option.innerText = entity.name;
  2455. select.appendChild(option);
  2456. if (entity.nsfw) {
  2457. option.classList.add("nsfw");
  2458. }
  2459. Object.values(filterDefs).forEach(filter => {
  2460. filter.extract(entity).forEach(result => {
  2461. filterSets[filter.id].add(result);
  2462. });
  2463. });
  2464. availableEntitiesByName[entity.name] = entity;
  2465. };
  2466. select.addEventListener("change", e => {
  2467. if (select.options[select.selectedIndex].classList.contains("nsfw")) {
  2468. select.classList.add("nsfw");
  2469. } else {
  2470. select.classList.remove("nsfw");
  2471. }
  2472. })
  2473. const button = document.createElement("button");
  2474. button.id = "create-entity-" + category + "-button";
  2475. button.classList.add("entity-button");
  2476. button.innerHTML = "<i class=\"far fa-plus-square\"></i>";
  2477. button.addEventListener("click", e => {
  2478. const newEntity = entityList[select.value].constructor()
  2479. displayEntity(newEntity, newEntity.defaultView, config.x, config.y + (config.lockYAxis ? 0 : config.height.toNumber("meters")/2), true, true);
  2480. });
  2481. const categoryOption = document.createElement("option");
  2482. categoryOption.value = category
  2483. categoryOption.innerText = category;
  2484. if (category == "characters") {
  2485. categoryOption.selected = true;
  2486. select.classList.add("category-visible");
  2487. button.classList.add("category-visible");
  2488. }
  2489. categorySelect.appendChild(categoryOption);
  2490. holder.appendChild(select);
  2491. holder.appendChild(button);
  2492. });
  2493. Object.values(filterDefs).forEach(filter => {
  2494. const option = document.createElement("option");
  2495. option.innerText = filter.name;
  2496. option.value = filter.id;
  2497. filterSelect.appendChild(option);
  2498. const filterNameSelect = document.createElement("select");
  2499. filterNameSelect.classList.add("filter-select");
  2500. filterNameSelect.id = "filter-" + filter.id;
  2501. filterHolder.appendChild(filterNameSelect);
  2502. const button = document.createElement("button");
  2503. button.classList.add("filter-button");
  2504. button.id = "create-filtered-" + filter.id + "-button";
  2505. filterHolder.appendChild(button);
  2506. const counter = document.createElement("div");
  2507. counter.classList.add("button-counter");
  2508. counter.innerText = "10";
  2509. button.appendChild(counter);
  2510. const i = document.createElement("i");
  2511. i.classList.add("fas");
  2512. i.classList.add("fa-plus");
  2513. button.appendChild(i);
  2514. button.addEventListener("click", e => {
  2515. const makers = Array.from(document.querySelector(".entity-select.category-visible")).filter(element => !element.classList.contains("filtered"));
  2516. const count = makers.length + 2;
  2517. let index = 1;
  2518. if (makers.length > 50) {
  2519. if (!confirm("Really spawn " + makers.length + " things at once?")) {
  2520. return;
  2521. }
  2522. }
  2523. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  2524. const spawned = makers.map(element => {
  2525. const category = document.querySelector("#category-picker").value;
  2526. const maker = availableEntities[category][element.value];
  2527. const entity = maker.constructor()
  2528. displayEntity(entity, entity.view, -worldWidth * 0.45 + config.x + worldWidth * 0.9 * index / (count - 1), config.y);
  2529. index += 1;
  2530. return entityIndex - 1;
  2531. });
  2532. updateSizes(true);
  2533. if (config.autoFitAdd) {
  2534. let targets = {};
  2535. spawned.forEach(key => {
  2536. targets[key] = entities[key];
  2537. })
  2538. fitEntities(targets);
  2539. }
  2540. });
  2541. Array.from(filterSets[filter.id]).map(name => [name, filter.render(name)]).sort(filterDefs[filter.id].sort).forEach(name => {
  2542. const option = document.createElement("option");
  2543. option.innerText = name[1];
  2544. option.value = name[0];
  2545. filterNameSelect.appendChild(option);
  2546. });
  2547. filterNameSelect.addEventListener("change", e => {
  2548. updateFilter();
  2549. });
  2550. });
  2551. console.log("Loaded " + Object.keys(availableEntitiesByName).length + " entities");
  2552. categorySelect.addEventListener("input", e => {
  2553. const oldSelect = document.querySelector(".entity-select.category-visible");
  2554. oldSelect.classList.remove("category-visible");
  2555. const oldButton = document.querySelector(".entity-button.category-visible");
  2556. oldButton.classList.remove("category-visible");
  2557. const newSelect = document.querySelector("#create-entity-" + e.target.value);
  2558. newSelect.classList.add("category-visible");
  2559. const newButton = document.querySelector("#create-entity-" + e.target.value + "-button");
  2560. newButton.classList.add("category-visible");
  2561. recomputeFilters();
  2562. updateFilter();
  2563. });
  2564. recomputeFilters();
  2565. filterSelect.addEventListener("input", e => {
  2566. const oldSelect = document.querySelector(".filter-select.category-visible");
  2567. if (oldSelect)
  2568. oldSelect.classList.remove("category-visible");
  2569. const newSelect = document.querySelector("#filter-" + e.target.value);
  2570. if (newSelect && e.target.value != "none")
  2571. newSelect.classList.add("category-visible");
  2572. updateFilter();
  2573. });
  2574. }
  2575. // Only display authors and owners if they appear
  2576. // somewhere in the current entity list
  2577. function recomputeFilters() {
  2578. const category = document.querySelector("#category-picker").value;
  2579. const filterSets = {};
  2580. Object.values(filterDefs).forEach(filter => {
  2581. filterSets[filter.id] = new Set();
  2582. });
  2583. document.querySelectorAll(".entity-select.category-visible > option").forEach(element => {
  2584. const entity = availableEntities[category][element.value];
  2585. Object.values(filterDefs).forEach(filter => {
  2586. filter.extract(entity).forEach(result => {
  2587. filterSets[filter.id].add(result);
  2588. });
  2589. });
  2590. });
  2591. Object.values(filterDefs).forEach(filter => {
  2592. // always show the "none" option
  2593. let found = filter.id == "none";
  2594. document.querySelectorAll("#filter-" + filter.id + " > option").forEach(element => {
  2595. if (filterSets[filter.id].has(element.value) || filter.id == "none") {
  2596. element.classList.remove("filtered");
  2597. element.disabled = false;
  2598. found = true;
  2599. } else {
  2600. element.classList.add("filtered");
  2601. element.disabled = true;
  2602. }
  2603. });
  2604. const filterOption = document.querySelector("#filter-picker > option[value='" + filter.id + "']");
  2605. if (found) {
  2606. filterOption.classList.remove("filtered");
  2607. filterOption.disabled = false;
  2608. } else {
  2609. filterOption.classList.add("filtered");
  2610. filterOption.disabled = true;
  2611. }
  2612. });
  2613. document.querySelector("#filter-picker").value = "none";
  2614. document.querySelector("#filter-picker").dispatchEvent(new Event("input"));
  2615. }
  2616. function updateFilter() {
  2617. const category = document.querySelector("#category-picker").value;
  2618. const type = document.querySelector("#filter-picker").value;
  2619. const filterKeySelect = document.querySelector(".filter-select.category-visible");
  2620. clearFilter();
  2621. if (!filterKeySelect) {
  2622. return;
  2623. }
  2624. const key = filterKeySelect.value;
  2625. let current = document.querySelector(".entity-select.category-visible").value;
  2626. let replace = false;
  2627. let first = null;
  2628. let count = 0;
  2629. document.querySelectorAll(".entity-select.category-visible > option").forEach(element => {
  2630. let keep = type == "none";
  2631. if (filterDefs[type].extract(availableEntities[category][element.value]).indexOf(key) >= 0) {
  2632. keep = true;
  2633. }
  2634. if (!keep) {
  2635. element.classList.add("filtered");
  2636. element.disabled = true;
  2637. if (current == element.value) {
  2638. replace = true;
  2639. }
  2640. } else {
  2641. count += 1;
  2642. if (!first) {
  2643. first = element.value;
  2644. }
  2645. }
  2646. });
  2647. const button = document.querySelector(".filter-select.category-visible + button");
  2648. if (button) {
  2649. button.querySelector(".button-counter").innerText = count;
  2650. }
  2651. if (replace) {
  2652. document.querySelector(".entity-select.category-visible").value = first;
  2653. document.querySelector("#create-entity-" + category).dispatchEvent(new Event("change"));
  2654. }
  2655. }
  2656. function clearFilter() {
  2657. document.querySelectorAll(".entity-select.category-visible > option").forEach(element => {
  2658. element.classList.remove("filtered");
  2659. element.disabled = false;
  2660. });
  2661. }
  2662. document.addEventListener("mousemove", (e) => {
  2663. if (clicked) {
  2664. let position = pix2pos({ x: e.clientX - dragOffsetX, y: e.clientY - dragOffsetY });
  2665. if (movingInBounds) {
  2666. position = snapPos(position);
  2667. } else {
  2668. let x = e.clientX - dragOffsetX;
  2669. let y = e.clientY - dragOffsetY;
  2670. if (x >= 0 && x <= canvasWidth && y >= 0 && y <= canvasHeight) {
  2671. movingInBounds = true;
  2672. }
  2673. }
  2674. clicked.dataset.x = position.x;
  2675. clicked.dataset.y = position.y;
  2676. updateEntityElement(entities[clicked.dataset.key], clicked);
  2677. if (hoveringInDeleteArea(e)) {
  2678. document.querySelector("#menubar").classList.add("hover-delete");
  2679. } else {
  2680. document.querySelector("#menubar").classList.remove("hover-delete");
  2681. }
  2682. }
  2683. if (panning && panReady) {
  2684. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  2685. const worldHeight = config.height.toNumber("meters");
  2686. config.x -= (e.clientX - panOffsetX) / canvasWidth * worldWidth;
  2687. config.y += (e.clientY - panOffsetY) / canvasHeight * worldHeight;
  2688. panOffsetX = e.clientX;
  2689. panOffsetY = e.clientY;
  2690. updateSizes();
  2691. panReady = false;
  2692. setTimeout(() => panReady=true, 1000/120);
  2693. }
  2694. });
  2695. document.addEventListener("touchmove", (e) => {
  2696. if (clicked) {
  2697. e.preventDefault();
  2698. let x = e.touches[0].clientX;
  2699. let y = e.touches[0].clientY;
  2700. const position = snapPos(pix2pos({ x: x - dragOffsetX, y: y - dragOffsetY }));
  2701. clicked.dataset.x = position.x;
  2702. clicked.dataset.y = position.y;
  2703. updateEntityElement(entities[clicked.dataset.key], clicked);
  2704. // what a hack
  2705. // I should centralize this 'fake event' creation...
  2706. if (hoveringInDeleteArea({ clientY: y })) {
  2707. document.querySelector("#menubar").classList.add("hover-delete");
  2708. } else {
  2709. document.querySelector("#menubar").classList.remove("hover-delete");
  2710. }
  2711. }
  2712. if (panning && panReady) {
  2713. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  2714. const worldHeight = config.height.toNumber("meters");
  2715. config.x -= (e.touches[0].clientX - panOffsetX) / canvasWidth * worldWidth;
  2716. config.y += (e.touches[0].clientY - panOffsetY) / canvasHeight * worldHeight;
  2717. panOffsetX = e.touches[0].clientX;
  2718. panOffsetY = e.touches[0].clientY;
  2719. updateSizes();
  2720. panReady = false;
  2721. setTimeout(() => panReady=true, 1000/60);
  2722. }
  2723. }, { passive: false });
  2724. function checkFitWorld() {
  2725. if (config.autoFit) {
  2726. fitWorld();
  2727. return true;
  2728. }
  2729. return false;
  2730. }
  2731. function fitWorld(manual = false, factor = 1.1) {
  2732. fitEntities(entities, factor);
  2733. }
  2734. function fitEntities(targetEntities, manual = false, factor = 1.1) {
  2735. let minX = Infinity;
  2736. let maxX = -Infinity;
  2737. let minY = Infinity;
  2738. let maxY = -Infinity;
  2739. let count = 0;
  2740. const worldWidth = config.height.toNumber("meters") / canvasHeight * canvasWidth;
  2741. const worldHeight = config.height.toNumber("meters");
  2742. Object.entries(targetEntities).forEach(([key, entity]) => {
  2743. const view = entity.view;
  2744. let extra = entity.views[view].image.extra;
  2745. extra = extra === undefined ? 1 : extra;
  2746. const image = document.querySelector("#entity-" + key + " > .entity-image");
  2747. const x = parseFloat(document.querySelector("#entity-" + key).dataset.x);
  2748. let width = image.width;
  2749. let height = image.height;
  2750. // only really relevant if the images haven't loaded in yet
  2751. if (height == 0) {
  2752. height = 100;
  2753. }
  2754. if (width == 0) {
  2755. width = height;
  2756. }
  2757. const xBottom = x - entity.views[view].height.toNumber("meters") * width / height / 2;
  2758. const xTop = x + entity.views[view].height.toNumber("meters") * width / height / 2;
  2759. const y = parseFloat(document.querySelector("#entity-" + key).dataset.y);
  2760. const yBottom = y;
  2761. const yTop = entity.views[view].height.toNumber("meters") + yBottom;
  2762. minX = Math.min(minX, xBottom);
  2763. maxX = Math.max(maxX, xTop);
  2764. minY = Math.min(minY, yBottom);
  2765. maxY = Math.max(maxY, yTop);
  2766. count += 1;
  2767. });
  2768. if (config.lockYAxis) {
  2769. minY = 0;
  2770. }
  2771. let ySize = (maxY - minY) * factor;
  2772. let xSize = (maxX - minX) * factor;
  2773. if (xSize / ySize > worldWidth / worldHeight) {
  2774. ySize *= ((xSize / ySize) / (worldWidth / worldHeight));
  2775. }
  2776. config.x = (maxX + minX) / 2;
  2777. config.y = minY;
  2778. height = math.unit(ySize, "meter")
  2779. setWorldHeight(config.height, math.multiply(height, factor));
  2780. }
  2781. // TODO why am I doing this
  2782. function updateWorldHeight() {
  2783. const unit = document.querySelector("#options-height-unit").value;
  2784. const value = Math.max(0.000000001, document.querySelector("#options-height-value").value);
  2785. const oldHeight = config.height;
  2786. setWorldHeight(oldHeight, math.unit(value, unit));
  2787. }
  2788. function setWorldHeight(oldHeight, newHeight) {
  2789. worldSizeDirty = true;
  2790. config.height = newHeight.to(document.querySelector("#options-height-unit").value)
  2791. const unit = document.querySelector("#options-height-unit").value;
  2792. setNumericInput(document.querySelector("#options-height-value"), config.height.toNumber(unit));
  2793. Object.entries(entities).forEach(([key, entity]) => {
  2794. const element = document.querySelector("#entity-" + key);
  2795. let newPosition;
  2796. if (altHeld) {
  2797. newPosition = adjustAbs({ x: element.dataset.x, y: element.dataset.y }, oldHeight, config.height);
  2798. } else {
  2799. newPosition = { x: element.dataset.x, y: element.dataset.y };
  2800. }
  2801. element.dataset.x = newPosition.x;
  2802. element.dataset.y = newPosition.y;
  2803. });
  2804. updateSizes();
  2805. }
  2806. function loadScene(name = "default") {
  2807. try {
  2808. const data = JSON.parse(localStorage.getItem("macrovision-save-" + name));
  2809. if (data === null) {
  2810. return false;
  2811. }
  2812. importScene(data);
  2813. return true;
  2814. } catch (err) {
  2815. alert("Something went wrong while loading (maybe you didn't have anything saved. Check the F12 console for the error.")
  2816. console.error(err);
  2817. return false;
  2818. }
  2819. }
  2820. function saveScene(name = "default") {
  2821. try {
  2822. const string = JSON.stringify(exportScene());
  2823. localStorage.setItem("macrovision-save-" + name, string);
  2824. } catch (err) {
  2825. alert("Something went wrong while saving (maybe I don't have localStorage permissions, or exporting failed). Check the F12 console for the error.")
  2826. console.error(err);
  2827. }
  2828. }
  2829. function deleteScene(name = "default") {
  2830. try {
  2831. localStorage.removeItem("macrovision-save-" + name)
  2832. } catch (err) {
  2833. console.error(err);
  2834. }
  2835. }
  2836. function exportScene() {
  2837. const results = {};
  2838. results.entities = [];
  2839. Object.entries(entities).filter(([key, entity]) => entity.ephemeral !== true).forEach(([key, entity]) => {
  2840. const element = document.querySelector("#entity-" + key);
  2841. results.entities.push({
  2842. name: entity.identifier,
  2843. scale: entity.scale,
  2844. view: entity.view,
  2845. x: element.dataset.x,
  2846. y: element.dataset.y,
  2847. priority: entity.priority,
  2848. brightness: entity.brightness
  2849. });
  2850. });
  2851. const unit = document.querySelector("#options-height-unit").value;
  2852. results.world = {
  2853. height: config.height.toNumber(unit),
  2854. unit: unit,
  2855. x: config.x,
  2856. y: config.y
  2857. }
  2858. results.version = migrationDefs.length;
  2859. return results;
  2860. }
  2861. // btoa doesn't like anything that isn't ASCII
  2862. // great
  2863. // thanks to https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
  2864. // for providing an alternative
  2865. function b64EncodeUnicode(str) {
  2866. // first we use encodeURIComponent to get percent-encoded UTF-8,
  2867. // then we convert the percent encodings into raw bytes which
  2868. // can be fed into btoa.
  2869. return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
  2870. function toSolidBytes(match, p1) {
  2871. return String.fromCharCode('0x' + p1);
  2872. }));
  2873. }
  2874. function b64DecodeUnicode(str) {
  2875. // Going backwards: from bytestream, to percent-encoding, to original string.
  2876. return decodeURIComponent(atob(str).split('').map(function (c) {
  2877. return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
  2878. }).join(''));
  2879. }
  2880. function linkScene() {
  2881. loc = new URL(window.location);
  2882. window.location = loc.protocol + "//" + loc.host + loc.pathname + "?scene=" + b64EncodeUnicode(JSON.stringify(exportScene()));
  2883. }
  2884. function copyScene() {
  2885. const results = exportScene();
  2886. navigator.clipboard.writeText(JSON.stringify(results));
  2887. }
  2888. function pasteScene() {
  2889. try {
  2890. navigator.clipboard.readText().then(text => {
  2891. const data = JSON.parse(text);
  2892. if (data.entities === undefined) {
  2893. return;
  2894. }
  2895. if (data.world === undefined) {
  2896. return;
  2897. }
  2898. importScene(data);
  2899. }).catch(err => alert(err));
  2900. } catch (err) {
  2901. console.error(err);
  2902. // probably wasn't valid data
  2903. }
  2904. }
  2905. // TODO - don't just search through every single entity
  2906. // probably just have a way to do lookups directly
  2907. function findEntity(name) {
  2908. return availableEntitiesByName[name];
  2909. }
  2910. const migrationDefs = [
  2911. /*
  2912. Migration: 0 -> 1
  2913. Adds x and y coordinates for the camera
  2914. */
  2915. data => {
  2916. data.world.x = 0;
  2917. data.world.y = 0;
  2918. },
  2919. /*
  2920. Migration: 1 -> 2
  2921. Adds priority and brightness to each entity
  2922. */
  2923. data => {
  2924. data.entities.forEach(entity => {
  2925. entity.priority = 0;
  2926. entity.brightness = 1;
  2927. });
  2928. }
  2929. ]
  2930. function migrateScene(data) {
  2931. if (data.version === undefined) {
  2932. alert("This save was created before save versions were tracked. The scene may import incorrectly.");
  2933. console.trace()
  2934. data.version = 0;
  2935. } else if (data.version < migrationDefs.length) {
  2936. migrationDefs[data.version](data);
  2937. data.version += 1;
  2938. migrateScene(data);
  2939. }
  2940. }
  2941. function importScene(data) {
  2942. removeAllEntities();
  2943. migrateScene(data);
  2944. data.entities.forEach(entityInfo => {
  2945. const entity = findEntity(entityInfo.name).constructor();
  2946. entity.scale = entityInfo.scale;
  2947. entity.priority = entityInfo.priority;
  2948. entity.brightness = entityInfo.brightness;
  2949. displayEntity(entity, entityInfo.view, entityInfo.x, entityInfo.y);
  2950. });
  2951. config.height = math.unit(data.world.height, data.world.unit);
  2952. config.x = data.world.x;
  2953. config.y = data.world.y;
  2954. document.querySelector("#options-height-value").value = data.world.height;
  2955. document.querySelector("#options-height-unit").value = data.world.unit;
  2956. if (data.canvasWidth) {
  2957. doHorizReposition(data.canvasWidth / canvasWidth);
  2958. }
  2959. updateSizes();
  2960. }
  2961. function renderToCanvas() {
  2962. const ctx = document.querySelector("#display").getContext("2d");
  2963. Object.entries(entities).sort((ent1, ent2) => {
  2964. z1 = document.querySelector("#entity-" + ent1[0]).style.zIndex;
  2965. z2 = document.querySelector("#entity-" + ent2[0]).style.zIndex;
  2966. return z1 - z2;
  2967. }).forEach(([id, entity]) => {
  2968. element = document.querySelector("#entity-" + id);
  2969. img = element.querySelector("img");
  2970. let x = parseFloat(element.dataset.x);
  2971. let y = parseFloat(element.dataset.y);
  2972. let coords = pos2pix({x: x, y: y});
  2973. let offset = img.style.getPropertyValue("--offset");
  2974. offset = parseFloat(offset.substring(0, offset.length-1))
  2975. x = coords.x - img.getBoundingClientRect().width/2;
  2976. y = coords.y - img.getBoundingClientRect().height * (-offset/100);
  2977. let xSize = img.getBoundingClientRect().width;
  2978. let ySize = img.getBoundingClientRect().height;
  2979. ctx.drawImage(img, x, y, xSize, ySize);
  2980. });
  2981. }
  2982. function exportCanvas(callback) {
  2983. /** @type {CanvasRenderingContext2D} */
  2984. const ctx = document.querySelector("#display").getContext("2d");
  2985. const blob = ctx.canvas.toBlob(callback);
  2986. }
  2987. function generateScreenshot(callback) {
  2988. /** @type {CanvasRenderingContext2D} */
  2989. const ctx = document.querySelector("#display").getContext("2d");
  2990. if (checkBodyClass("toggle-bottom-cover")) {
  2991. ctx.fillStyle = "#000";
  2992. ctx.fillRect(0, pos2pix({x: 0, y: 0}).y, canvasWidth + 100, canvasHeight);
  2993. }
  2994. renderToCanvas();
  2995. ctx.fillStyle = "#555";
  2996. ctx.font = "normal normal lighter 16pt coda";
  2997. ctx.fillText("macrovision.crux.sexy", 10, 25);
  2998. exportCanvas(blob => {
  2999. callback(blob);
  3000. });
  3001. }
  3002. function copyScreenshot() {
  3003. if (window.ClipboardItem === undefined) {
  3004. alert("Sorry, this browser doesn't yet support writing images to the clipboard.");
  3005. return;
  3006. }
  3007. generateScreenshot(blob => {
  3008. navigator.clipboard.write([
  3009. new ClipboardItem({
  3010. "image/png": blob
  3011. })
  3012. ]);
  3013. });
  3014. drawScales(false);
  3015. }
  3016. function saveScreenshot() {
  3017. generateScreenshot(blob => {
  3018. const a = document.createElement("a");
  3019. a.href = URL.createObjectURL(blob);
  3020. a.setAttribute("download", "macrovision.png");
  3021. a.click();
  3022. });
  3023. drawScales(false);
  3024. }
  3025. function openScreenshot() {
  3026. generateScreenshot(blob => {
  3027. const a = document.createElement("a");
  3028. a.href = URL.createObjectURL(blob);
  3029. a.setAttribute("target", "_blank");
  3030. a.click();
  3031. });
  3032. drawScales(false);
  3033. }
  3034. const rateLimits = {};
  3035. function toast(msg) {
  3036. let div = document.createElement("div");
  3037. div.innerHTML = msg;
  3038. div.classList.add("toast");
  3039. document.body.appendChild(div);
  3040. setTimeout(() => {
  3041. document.body.removeChild(div);
  3042. }, 5000)
  3043. }
  3044. function toastRateLimit(msg, key, delay) {
  3045. if (!rateLimits[key]) {
  3046. toast(msg);
  3047. rateLimits[key] = setTimeout(() => {
  3048. delete rateLimits[key]
  3049. }, delay);
  3050. }
  3051. }