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.
 
 
 

920 lignes
28 KiB

  1. let selected = null;
  2. let selectedEntity = null;
  3. let entityIndex = 0;
  4. let clicked = null;
  5. let dragging = false;
  6. let clickTimeout = null;
  7. let dragOffsetX = null;
  8. let dragOffsetY = null;
  9. let altHeld = false;
  10. const unitChoices = {
  11. length: [
  12. "meters",
  13. "millimeters",
  14. "centimeters",
  15. "kilometers",
  16. "inches",
  17. "feet",
  18. "miles",
  19. "parsecs",
  20. ],
  21. area: [
  22. "meters^2",
  23. "cm^2",
  24. "kilometers^2",
  25. "acres",
  26. "miles^2"
  27. ],
  28. mass: [
  29. "kilograms"
  30. ]
  31. }
  32. const config = {
  33. height: math.unit(1500, "meters"),
  34. minLineSize: 50,
  35. maxLineSize: 250,
  36. autoFit: false
  37. }
  38. const availableEntities = {
  39. }
  40. const entities = {
  41. }
  42. function constrainRel(coords) {
  43. return {
  44. x: Math.min(Math.max(coords.x, 0), 1),
  45. y: Math.min(Math.max(coords.y, 0), 1)
  46. }
  47. }
  48. function snapRel(coords) {
  49. return constrainRel({
  50. x: coords.x,
  51. y: altHeld ? coords.y : (Math.abs(coords.y - 1) < 0.05 ? 1 : coords.y)
  52. });
  53. }
  54. function adjustAbs(coords, oldHeight, newHeight) {
  55. return { x: coords.x, y: 1 + (coords.y - 1) * math.divide(oldHeight, newHeight) };
  56. }
  57. function rel2abs(coords) {
  58. const canvasWidth = document.querySelector("#display").clientWidth - 100;
  59. const canvasHeight = document.querySelector("#display").clientHeight - 50;
  60. return { x: coords.x * canvasWidth + 50, y: coords.y * canvasHeight };
  61. }
  62. function abs2rel(coords) {
  63. const canvasWidth = document.querySelector("#display").clientWidth - 100;
  64. const canvasHeight = document.querySelector("#display").clientHeight - 50;
  65. return { x: (coords.x - 50) / canvasWidth, y: coords.y / canvasHeight };
  66. }
  67. function updateEntityElement(entity, element, zIndex) {
  68. const position = rel2abs({ x: element.dataset.x, y: element.dataset.y });
  69. const view = element.dataset.view;
  70. element.style.left = position.x + "px";
  71. element.style.top = position.y + "px";
  72. const canvasHeight = document.querySelector("#display").clientHeight;
  73. const pixels = math.divide(entity.views[view].height, config.height) * (canvasHeight - 100);
  74. const bonus = (entity.views[view].image.extra ? entity.views[view].image.extra : 1);
  75. element.style.setProperty("--height", pixels * bonus + "px");
  76. element.querySelector(".entity-name").innerText = entity.name;
  77. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  78. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  79. bottomName.style.left = position.x + entX + "px";
  80. bottomName.style.top = "95vh";
  81. bottomName.innerText = entity.name;
  82. if (zIndex) {
  83. element.style.zIndex = zIndex;
  84. }
  85. }
  86. function updateSizes() {
  87. drawScale();
  88. let ordered = Object.entries(entities);
  89. ordered.sort((e1, e2) => {
  90. return e1[1].views[e1[1].view].height.toNumber("meters") - e2[1].views[e2[1].view].height.toNumber("meters")
  91. });
  92. let zIndex = ordered.length;
  93. ordered.forEach(entity => {
  94. const element = document.querySelector("#entity-" + entity[0]);
  95. updateEntityElement(entity[1], element, zIndex);
  96. zIndex -= 1;
  97. });
  98. }
  99. function drawScale() {
  100. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  101. let total = heightPer.clone();
  102. total.value = 0;
  103. for (let y = ctx.canvas.clientHeight - 50; y >= 50; y -= pixelsPer) {
  104. drawTick(ctx, 50, y, total);
  105. total = math.add(total, heightPer);
  106. }
  107. }
  108. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, value) {
  109. const oldStroke = ctx.strokeStyle;
  110. const oldFill = ctx.fillStyle;
  111. ctx.beginPath();
  112. ctx.moveTo(x, y);
  113. ctx.lineTo(x + 20, y);
  114. ctx.strokeStyle = "#000000";
  115. ctx.stroke();
  116. ctx.beginPath();
  117. ctx.moveTo(x + 20, y);
  118. ctx.lineTo(ctx.canvas.clientWidth - 70, y);
  119. ctx.strokeStyle = "#aaaaaa";
  120. ctx.stroke();
  121. ctx.beginPath();
  122. ctx.moveTo(ctx.canvas.clientWidth - 70, y);
  123. ctx.lineTo(ctx.canvas.clientWidth - 50, y);
  124. ctx.strokeStyle = "#000000";
  125. ctx.stroke();
  126. const oldFont = ctx.font;
  127. ctx.font = 'normal 24pt coda';
  128. ctx.fillStyle = "#dddddd";
  129. ctx.beginPath();
  130. ctx.fillText(value.format({ precision: 3 }), x + 20, y + 35);
  131. ctx.font = oldFont;
  132. ctx.strokeStyle = oldStroke;
  133. ctx.fillStyle = oldFill;
  134. }
  135. const canvas = document.querySelector("#display");
  136. /** @type {CanvasRenderingContext2D} */
  137. const ctx = canvas.getContext("2d");
  138. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.value;
  139. let heightPer = config.height.clone();
  140. heightPer.value = 1;
  141. if (pixelsPer < config.minLineSize) {
  142. heightPer.value /= pixelsPer / config.minLineSize;
  143. pixelsPer = config.minLineSize;
  144. }
  145. if (pixelsPer > config.maxLineSize) {
  146. heightPer.value /= pixelsPer / config.maxLineSize;
  147. pixelsPer = config.maxLineSize;
  148. }
  149. ctx.clearRect(0, 0, canvas.width, canvas.height);
  150. ctx.scale(1, 1);
  151. ctx.canvas.width = canvas.clientWidth;
  152. ctx.canvas.height = canvas.clientHeight;
  153. ctx.beginPath();
  154. ctx.moveTo(50, 50);
  155. ctx.lineTo(50, ctx.canvas.clientHeight - 50);
  156. ctx.stroke();
  157. ctx.beginPath();
  158. ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
  159. ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
  160. ctx.stroke();
  161. drawTicks(ctx, pixelsPer, heightPer);
  162. }
  163. function makeEntity(name, author, views) {
  164. const entityTemplate = {
  165. name: name,
  166. author: author,
  167. scale: 1,
  168. views: views,
  169. defaults: [],
  170. init: function () {
  171. Object.entries(this.views).forEach(([viewKey, view]) => {
  172. view.parent = this;
  173. if (this.defaultView === undefined) {
  174. this.defaultView = viewKey;
  175. }
  176. Object.entries(view.attributes).forEach(([key, val]) => {
  177. Object.defineProperty(
  178. view,
  179. key,
  180. {
  181. get: function () {
  182. return math.multiply(Math.pow(this.parent.scale, this.attributes[key].power), this.attributes[key].base);
  183. },
  184. set: function (value) {
  185. const newScale = Math.pow(math.divide(value, this.attributes[key].base), 1 / this.attributes[key].power);
  186. this.parent.scale = newScale;
  187. }
  188. }
  189. )
  190. });
  191. });
  192. delete this.init;
  193. return this;
  194. }
  195. }.init();
  196. return entityTemplate;
  197. }
  198. function clickDown(target, x, y) {
  199. clicked = target;
  200. const rect = target.getBoundingClientRect();
  201. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  202. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  203. dragOffsetX = x - rect.left + entX;
  204. dragOffsetY = y - rect.top + entY;
  205. clickTimeout = setTimeout(() => { dragging = true }, 200)
  206. }
  207. // could we make this actually detect the menu area?
  208. function hoveringInDeleteArea(e) {
  209. return e.clientY < document.body.clientHeight / 10;
  210. }
  211. function clickUp(e) {
  212. clearTimeout(clickTimeout);
  213. if (clicked) {
  214. if (dragging) {
  215. dragging = false;
  216. if (hoveringInDeleteArea(e)) {
  217. removeEntity(clicked);
  218. document.querySelector("#menubar").classList.remove("hover-delete");
  219. }
  220. } else {
  221. select(clicked);
  222. }
  223. clicked = null;
  224. }
  225. }
  226. function deselect() {
  227. if (selected) {
  228. selected.classList.remove("selected");
  229. }
  230. selected = null;
  231. clearViewList();
  232. clearEntityOptions();
  233. clearViewOptions();
  234. }
  235. function select(target) {
  236. deselect();
  237. selected = target;
  238. selectedEntity = entities[target.dataset.key];
  239. selected.classList.add("selected");
  240. configViewList(selectedEntity, target.dataset.view);
  241. configEntityOptions(selectedEntity, target.dataset.view);
  242. configViewOptions(selectedEntity, target.dataset.view);
  243. }
  244. function configViewList(entity, selectedView) {
  245. const list = document.querySelector("#entity-view");
  246. list.innerHTML = "";
  247. list.style.display = "block";
  248. Object.keys(entity.views).forEach(view => {
  249. const option = document.createElement("option");
  250. option.innerText = entity.views[view].name;
  251. option.value = view;
  252. if (view === selectedView) {
  253. option.selected = true;
  254. }
  255. list.appendChild(option);
  256. });
  257. }
  258. function clearViewList() {
  259. const list = document.querySelector("#entity-view");
  260. list.innerHTML = "";
  261. list.style.display = "none";
  262. }
  263. function updateWorldOptions(entity, view) {
  264. const heightInput = document.querySelector("#options-height-value");
  265. const heightSelect = document.querySelector("#options-height-unit");
  266. const converted = config.height.toNumber(heightSelect.value);
  267. heightInput.value = math.round(converted, 3);
  268. }
  269. function configEntityOptions(entity, view) {
  270. const holder = document.querySelector("#options-entity");
  271. holder.innerHTML = "";
  272. const scaleLabel = document.createElement("div");
  273. scaleLabel.classList.add("options-label");
  274. scaleLabel.innerText = "Scale";
  275. const scaleRow = document.createElement("div");
  276. scaleRow.classList.add("options-row");
  277. const scaleInput = document.createElement("input");
  278. scaleInput.classList.add("options-field-numeric");
  279. scaleInput.id = "options-entity-scale";
  280. scaleInput.addEventListener("input", e => {
  281. entity.scale = e.target.value == 0 ? 1 : e.target.value;
  282. if (config.autoFit) {
  283. fitWorld();
  284. }
  285. updateSizes();
  286. updateEntityOptions(entity, view);
  287. updateViewOptions(entity, view);
  288. });
  289. scaleInput.setAttribute("min", 1);
  290. scaleInput.setAttribute("type", "number");
  291. scaleInput.value = entity.scale;
  292. scaleRow.appendChild(scaleInput);
  293. holder.appendChild(scaleLabel);
  294. holder.appendChild(scaleRow);
  295. const nameLabel = document.createElement("div");
  296. nameLabel.classList.add("options-label");
  297. nameLabel.innerText = "Name";
  298. const nameRow = document.createElement("div");
  299. nameRow.classList.add("options-row");
  300. const nameInput = document.createElement("input");
  301. nameInput.classList.add("options-field-text");
  302. nameInput.value = entity.name;
  303. nameInput.addEventListener("input", e => {
  304. entity.name = e.target.value;
  305. updateSizes();
  306. })
  307. nameRow.appendChild(nameInput);
  308. holder.appendChild(nameLabel);
  309. holder.appendChild(nameRow);
  310. const defaultHolder = document.querySelector("#options-entity-defaults");
  311. defaultHolder.innerHTML = "";
  312. entity.defaults.forEach(defaultInfo => {
  313. const button = document.createElement("button");
  314. button.classList.add("options-button");
  315. button.innerText = defaultInfo.name;
  316. button.addEventListener("click", e => {
  317. entity.views[entity.defaultView].height = defaultInfo.height;
  318. updateEntityOptions(entity, view);
  319. updateViewOptions(entity, view);
  320. updateSizes();
  321. });
  322. defaultHolder.appendChild(button);
  323. });
  324. }
  325. function updateEntityOptions(entity, view) {
  326. const scaleInput = document.querySelector("#options-entity-scale");
  327. scaleInput.value = entity.scale;
  328. }
  329. function clearEntityOptions() {
  330. const holder = document.querySelector("#options-entity");
  331. holder.innerHTML = "";
  332. }
  333. function configViewOptions(entity, view) {
  334. const holder = document.querySelector("#options-view");
  335. holder.innerHTML = "";
  336. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  337. const label = document.createElement("div");
  338. label.classList.add("options-label");
  339. label.innerText = val.name;
  340. holder.appendChild(label);
  341. const row = document.createElement("div");
  342. row.classList.add("options-row");
  343. holder.appendChild(row);
  344. const input = document.createElement("input");
  345. input.classList.add("options-field-numeric");
  346. input.id = "options-view-" + key + "-input";
  347. input.setAttribute("type", "number");
  348. input.setAttribute("min", 1);
  349. input.value = entity.views[view][key].value;
  350. const select = document.createElement("select");
  351. select.id = "options-view-" + key + "-select"
  352. unitChoices[val.type].forEach(name => {
  353. const option = document.createElement("option");
  354. option.innerText = name;
  355. select.appendChild(option);
  356. });
  357. input.addEventListener("input", e => {
  358. const value = input.value == 0 ? 1 : input.value;
  359. entity.views[view][key] = math.unit(value, select.value);
  360. if (config.autoFit) {
  361. fitWorld();
  362. }
  363. updateSizes();
  364. updateEntityOptions(entity, view);
  365. updateViewOptions(entity, view, key);
  366. });
  367. select.setAttribute("oldUnit", select.value);
  368. select.addEventListener("input", e => {
  369. const value = input.value == 0 ? 1 : input.value;
  370. const oldUnit = select.getAttribute("oldUnit");
  371. entity.views[view][key] = math.unit(value, oldUnit).to(select.value);
  372. input.value = entity.views[view][key].toNumber(select.value);
  373. select.setAttribute("oldUnit", select.value);
  374. if (config.autoFit) {
  375. fitWorld();
  376. }
  377. updateSizes();
  378. updateEntityOptions(entity, view);
  379. updateViewOptions(entity, view, key);
  380. });
  381. row.appendChild(input);
  382. row.appendChild(select);
  383. });
  384. }
  385. function updateViewOptions(entity, view, changed) {
  386. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  387. if (key != changed) {
  388. const input = document.querySelector("#options-view-" + key + "-input");
  389. const select = document.querySelector("#options-view-" + key + "-select");
  390. const currentUnit = select.value;
  391. const convertedAmount = entity.views[view][key].toNumber(currentUnit);
  392. input.value = math.round(convertedAmount, 5);
  393. }
  394. });
  395. }
  396. function clearViewOptions() {
  397. const holder = document.querySelector("#options-view");
  398. holder.innerHTML = "";
  399. }
  400. // this is a crime against humanity, and also stolen from
  401. // stack overflow
  402. // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
  403. const testCanvas = document.createElement("canvas");
  404. testCanvas.id = "test-canvas";
  405. const testCtx = testCanvas.getContext("2d");
  406. function testClick(event) {
  407. // oh my god I can't believe I'm doing this
  408. const target = event.target;
  409. if (navigator.userAgent.indexOf("Firefox") != -1) {
  410. clickDown(target.parentElement, event.clientX, event.clientY);
  411. return;
  412. }
  413. // Get click coordinates
  414. let w = target.width;
  415. let h = target.height;
  416. let ratioW = 1, ratioH = 1;
  417. // Limit the size of the canvas so that very large images don't cause problems)
  418. if (w > 4000) {
  419. ratioW = w / 4000;
  420. w /= ratioW;
  421. h /= ratioW;
  422. }
  423. if (h > 4000) {
  424. ratioH = h / 4000;
  425. w /= ratioH;
  426. h /= ratioH;
  427. }
  428. const ratio = ratioW * ratioH;
  429. var x = event.clientX - target.getBoundingClientRect().x,
  430. y = event.clientY - target.getBoundingClientRect().y,
  431. alpha;
  432. testCtx.canvas.width = w;
  433. testCtx.canvas.height = h;
  434. // Draw image to canvas
  435. // and read Alpha channel value
  436. testCtx.drawImage(target, 0, 0, w, h);
  437. alpha = testCtx.getImageData(Math.floor(x / ratio), Math.floor(y / ratio), 1, 1).data[3]; // [0]R [1]G [2]B [3]A
  438. // If pixel is transparent,
  439. // retrieve the element underneath and trigger it's click event
  440. if (alpha === 0) {
  441. const oldDisplay = target.style.display;
  442. target.style.display = "none";
  443. const newTarget = document.elementFromPoint(event.clientX, event.clientY);
  444. newTarget.dispatchEvent(new MouseEvent(event.type, {
  445. "clientX": event.clientX,
  446. "clientY": event.clientY
  447. }));
  448. target.style.display = oldDisplay;
  449. } else {
  450. clickDown(target.parentElement, event.clientX, event.clientY);
  451. }
  452. }
  453. function arrangeEntities(order) {
  454. let x = 0.1;
  455. order.forEach(key => {
  456. document.querySelector("#entity-" + key).dataset.x = x;
  457. x += 0.8 / order.length
  458. });
  459. updateSizes();
  460. }
  461. function removeAllEntities() {
  462. Object.keys(entities).forEach(key => {
  463. removeEntity(document.querySelector("#entity-" + key));
  464. });
  465. }
  466. function removeEntity(element) {
  467. delete entities[element.dataset.key];
  468. const bottomName = document.querySelector("#bottom-name-" + element.dataset.key);
  469. bottomName.parentElement.removeChild(bottomName);
  470. element.parentElement.removeChild(element);
  471. }
  472. function displayEntity(entity, view, x, y) {
  473. const box = document.createElement("div");
  474. box.classList.add("entity-box");
  475. const img = document.createElement("img");
  476. img.classList.add("entity-image");
  477. img.addEventListener("dragstart", e => {
  478. e.preventDefault();
  479. });
  480. const nameTag = document.createElement("div");
  481. nameTag.classList.add("entity-name");
  482. nameTag.innerText = entity.name;
  483. box.appendChild(img);
  484. box.appendChild(nameTag);
  485. const image = entity.views[view].image;
  486. img.src = image.source;
  487. if (image.bottom) {
  488. img.style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  489. }
  490. box.dataset.x = x;
  491. box.dataset.y = y;
  492. img.addEventListener("mousedown", e => { testClick(e); e.stopPropagation() });
  493. img.addEventListener("touchstart", e => {
  494. const fakeEvent = {
  495. target: e.target,
  496. clientX: e.touches[0].clientX,
  497. clientY: e.touches[0].clientY
  498. };
  499. testClick(fakeEvent);
  500. });
  501. box.id = "entity-" + entityIndex;
  502. box.dataset.key = entityIndex;
  503. box.dataset.view = view;
  504. entity.view = view;
  505. entities[entityIndex] = entity;
  506. entity.index = entityIndex;
  507. const world = document.querySelector("#entities");
  508. world.appendChild(box);
  509. const bottomName = document.createElement("div");
  510. bottomName.classList.add("bottom-name");
  511. bottomName.id = "bottom-name-" + entityIndex;
  512. bottomName.innerText = entity.name;
  513. bottomName.addEventListener("click", () => select(box));
  514. world.appendChild(bottomName);
  515. entityIndex += 1;
  516. updateEntityElement(entity, box);
  517. if (config.autoFit) {
  518. fitWorld();
  519. }
  520. }
  521. document.addEventListener("DOMContentLoaded", () => {
  522. prepareEntities();
  523. const stuff = availableEntities.characters.map(x => x.constructor).filter(x => {
  524. const result = x();
  525. return result.views[result.defaultView].height.toNumber("meters") < 1000;
  526. })
  527. let x = 0.2;
  528. stuff.forEach(entity => {
  529. displayEntity(entity(), entity().defaultView, x, 1);
  530. x += 0.7 / stuff.length;
  531. })
  532. const order = Object.keys(entities).sort((a, b) => {
  533. const entA = entities[a];
  534. const entB = entities[b];
  535. const viewA = document.querySelector("#entity-" + a).dataset.view;
  536. const viewB = document.querySelector("#entity-" + b).dataset.view;
  537. const heightA = entA.views[viewA].height.to("meter").value;
  538. const heightB = entB.views[viewB].height.to("meter").value;
  539. return heightA - heightB;
  540. });
  541. arrangeEntities(order);
  542. fitWorld();
  543. window.addEventListener("wheel", e => {
  544. const dir = e.deltaY < 0 ? 0.9 : 1.1;
  545. config.height = math.multiply(config.height, dir);
  546. updateSizes();
  547. updateWorldOptions();
  548. })
  549. document.querySelector("body").appendChild(testCtx.canvas);
  550. updateSizes();
  551. document.querySelector("#options-height-value").addEventListener("input", e => {
  552. updateWorldHeight();
  553. })
  554. document.querySelector("#options-height-unit").addEventListener("input", e => {
  555. updateWorldHeight();
  556. })
  557. world.addEventListener("mousedown", e => deselect());
  558. document.querySelector("#display").addEventListener("mousedown", deselect);
  559. document.addEventListener("mouseup", e => clickUp(e));
  560. document.addEventListener("touchend", e => {
  561. const fakeEvent = {
  562. target: e.target,
  563. clientX: e.changedTouches[0].clientX,
  564. clientY: e.changedTouches[0].clientY
  565. };
  566. clickUp(fakeEvent);
  567. });
  568. document.querySelector("#entity-view").addEventListener("input", e => {
  569. selected.dataset.view = e.target.value;
  570. entities[selected.dataset.key].view = e.target.value;
  571. const image = entities[selected.dataset.key].views[e.target.value].image
  572. selected.querySelector(".entity-image").src = image.source;
  573. if (image.bottom) {
  574. selected.querySelector(".entity-image").style.setProperty("--offset", ((-1 + image.bottom) * 100) + "%")
  575. }
  576. updateSizes();
  577. updateEntityOptions(entities[selected.dataset.key], e.target.value);
  578. updateViewOptions(entities[selected.dataset.key], e.target.value);
  579. });
  580. clearViewList();
  581. document.querySelector("#menu-clear").addEventListener("click", e => {
  582. removeAllEntities();
  583. });
  584. document.querySelector("#menu-order-height").addEventListener("click", e => {
  585. const order = Object.keys(entities).sort((a, b) => {
  586. const entA = entities[a];
  587. const entB = entities[b];
  588. const viewA = document.querySelector("#entity-" + a).dataset.view;
  589. const viewB = document.querySelector("#entity-" + b).dataset.view;
  590. const heightA = entA.views[viewA].height.to("meter").value;
  591. const heightB = entB.views[viewB].height.to("meter").value;
  592. return heightA - heightB;
  593. });
  594. arrangeEntities(order);
  595. });
  596. document.querySelector("#options-world-fit").addEventListener("click", fitWorld);
  597. document.querySelector("#options-world-autofit").addEventListener("input", e => {
  598. config.autoFit = e.target.value;
  599. if (config.autoFit) {
  600. fitWorld();
  601. }
  602. });
  603. document.addEventListener("keydown", e => {
  604. if (e.key == "Delete") {
  605. if (selected) {
  606. removeEntity(selected);
  607. selected = null;
  608. }
  609. }
  610. })
  611. });
  612. function prepareEntities() {
  613. availableEntities["buildings"] = makeBuildings();
  614. availableEntities["characters"] = makeCharacters();
  615. availableEntities["objects"] = makeObjects();
  616. availableEntities["naturals"] = makeNaturals();
  617. availableEntities["vehicles"] = makeVehicles();
  618. availableEntities["characters"].sort((x,y) => {
  619. return x.name < y.name ? -1 : 1
  620. });
  621. const holder = document.querySelector("#spawners");
  622. const categorySelect = document.createElement("select");
  623. categorySelect.id = "category-picker";
  624. holder.appendChild(categorySelect);
  625. Object.entries(availableEntities).forEach(([category, entityList]) => {
  626. const select = document.createElement("select");
  627. select.id = "create-entity-" + category;
  628. for (let i = 0; i < entityList.length; i++) {
  629. const entity = entityList[i];
  630. const option = document.createElement("option");
  631. option.value = i;
  632. option.innerText = entity.name;
  633. select.appendChild(option);
  634. };
  635. const button = document.createElement("button");
  636. button.id = "create-entity-" + category + "-button";
  637. button.innerText = "Create";
  638. button.addEventListener("click", e => {
  639. const newEntity = entityList[select.value].constructor()
  640. displayEntity(newEntity, newEntity.defaultView, 0.5, 1);
  641. });
  642. const categoryOption = document.createElement("option");
  643. categoryOption.value = category
  644. categoryOption.innerText = category;
  645. if (category == "characters") {
  646. categoryOption.selected = true;
  647. select.classList.add("category-visible");
  648. button.classList.add("category-visible");
  649. }
  650. categorySelect.appendChild(categoryOption);
  651. holder.appendChild(button);
  652. holder.appendChild(select);
  653. });
  654. categorySelect.addEventListener("input", e => {
  655. const oldSelect = document.querySelector("select.category-visible");
  656. oldSelect.classList.remove("category-visible");
  657. const oldButton = document.querySelector("button.category-visible");
  658. oldButton.classList.remove("category-visible");
  659. const newSelect = document.querySelector("#create-entity-" + e.target.value);
  660. newSelect.classList.add("category-visible");
  661. const newButton = document.querySelector("#create-entity-" + e.target.value + "-button");
  662. newButton.classList.add("category-visible");
  663. });
  664. }
  665. window.addEventListener("resize", () => {
  666. updateSizes();
  667. })
  668. document.addEventListener("mousemove", (e) => {
  669. if (clicked) {
  670. const position = snapRel(abs2rel({ x: e.clientX - dragOffsetX, y: e.clientY - dragOffsetY }));
  671. clicked.dataset.x = position.x;
  672. clicked.dataset.y = position.y;
  673. updateEntityElement(entities[clicked.dataset.key], clicked);
  674. if (hoveringInDeleteArea(e)) {
  675. document.querySelector("#menubar").classList.add("hover-delete");
  676. } else {
  677. document.querySelector("#menubar").classList.remove("hover-delete");
  678. }
  679. }
  680. });
  681. document.addEventListener("touchmove", (e) => {
  682. if (clicked) {
  683. e.preventDefault();
  684. let x = e.touches[0].clientX;
  685. let y = e.touches[0].clientY;
  686. const position = snapRel(abs2rel({ x: x - dragOffsetX, y: y - dragOffsetY }));
  687. clicked.dataset.x = position.x;
  688. clicked.dataset.y = position.y;
  689. updateEntityElement(entities[clicked.dataset.key], clicked);
  690. // what a hack
  691. // I should centralize this 'fake event' creation...
  692. if (hoveringInDeleteArea({ clientY: y })) {
  693. document.querySelector("#menubar").classList.add("hover-delete");
  694. } else {
  695. document.querySelector("#menubar").classList.remove("hover-delete");
  696. }
  697. }
  698. }, { passive: false });
  699. function fitWorld() {
  700. let max = math.unit(0, "meter");
  701. Object.entries(entities).forEach(([key, entity]) => {
  702. const view = document.querySelector("#entity-" + key).dataset.view;
  703. max = math.max(max, entity.views[view].height);
  704. });
  705. setWorldHeight(config.height, math.multiply(max, 1.1));
  706. }
  707. function updateWorldHeight() {
  708. const value = Math.max(1, document.querySelector("#options-height-value").value);
  709. const unit = document.querySelector("#options-height-unit").value;
  710. const oldHeight = config.height;
  711. setWorldHeight(oldHeight, math.unit(value, unit));
  712. }
  713. function setWorldHeight(oldHeight, newHeight) {
  714. config.height = newHeight;
  715. const unit = document.querySelector("#options-height-unit").value;
  716. document.querySelector("#options-height-value").value = config.height.toNumber(unit);
  717. Object.entries(entities).forEach(([key, entity]) => {
  718. const element = document.querySelector("#entity-" + key);
  719. const newPosition = adjustAbs({ x: element.dataset.x, y: element.dataset.y }, oldHeight, config.height);
  720. element.dataset.x = newPosition.x;
  721. element.dataset.y = newPosition.y;
  722. });
  723. updateSizes();
  724. }