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.
 
 
 

585 lignes
17 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. "kilometers"
  14. ],
  15. mass: [
  16. "kilograms"
  17. ]
  18. }
  19. const config = {
  20. height: math.unit(10, "meters"),
  21. minLineSize: 50,
  22. maxLineSize: 250
  23. }
  24. const entities = {
  25. }
  26. function constrainRel(coords) {
  27. return {
  28. x: Math.min(Math.max(coords.x, 0), 1),
  29. y: Math.min(Math.max(coords.y, 0), 1)
  30. }
  31. }
  32. function snapRel(coords) {
  33. return constrainRel({
  34. x: coords.x,
  35. y: altHeld ? coords.y : (Math.abs(coords.y - 1) < 0.05 ? 1 : coords.y)
  36. });
  37. }
  38. function adjustAbs(coords, oldHeight, newHeight) {
  39. return { x: coords.x, y: 1 + (coords.y - 1) * math.divide(oldHeight, newHeight) };
  40. }
  41. function rel2abs(coords) {
  42. const canvasWidth = document.querySelector("#display").clientWidth - 50;
  43. const canvasHeight = document.querySelector("#display").clientHeight - 50;
  44. return { x: coords.x * canvasWidth, y: coords.y * canvasHeight };
  45. }
  46. function abs2rel(coords) {
  47. const canvasWidth = document.querySelector("#display").clientWidth - 50;
  48. const canvasHeight = document.querySelector("#display").clientHeight - 50;
  49. return { x: coords.x / canvasWidth, y: coords.y / canvasHeight };
  50. }
  51. function updateEntityElement(entity, element) {
  52. const position = rel2abs({ x: element.dataset.x, y: element.dataset.y });
  53. const view = element.dataset.view;
  54. element.style.left = position.x + "px";
  55. element.style.top = position.y + "px";
  56. const canvasHeight = document.querySelector("#display").clientHeight;
  57. const pixels = math.divide(entity.views[view].height, config.height) * (canvasHeight - 100);
  58. element.style.setProperty("--height", pixels + "px");
  59. element.querySelector(".entity-name").innerText = entity.name;
  60. }
  61. function updateSizes() {
  62. drawScale();
  63. Object.entries(entities).forEach(([key, entity]) => {
  64. const element = document.querySelector("#entity-" + key);
  65. updateEntityElement(entity, element);
  66. });
  67. }
  68. function drawScale() {
  69. function drawTicks(/** @type {CanvasRenderingContext2D} */ ctx, pixelsPer, heightPer) {
  70. let total = heightPer.clone();
  71. total.value = 0;
  72. for (let y = ctx.canvas.clientHeight - 50; y >= 50; y -= pixelsPer) {
  73. drawTick(ctx, 50, y, total);
  74. total = math.add(total, heightPer);
  75. }
  76. }
  77. function drawTick(/** @type {CanvasRenderingContext2D} */ ctx, x, y, value) {
  78. const oldStroke = ctx.strokeStyle;
  79. const oldFill = ctx.fillStyle;
  80. ctx.beginPath();
  81. ctx.moveTo(x, y);
  82. ctx.lineTo(x + 20, y);
  83. ctx.strokeStyle = "#000000";
  84. ctx.stroke();
  85. ctx.beginPath();
  86. ctx.moveTo(x + 20, y);
  87. ctx.lineTo(ctx.canvas.clientWidth - 70, y);
  88. ctx.strokeStyle = "#aaaaaa";
  89. ctx.stroke();
  90. ctx.beginPath();
  91. ctx.moveTo(ctx.canvas.clientWidth - 70, y);
  92. ctx.lineTo(ctx.canvas.clientWidth - 50, y);
  93. ctx.strokeStyle = "#000000";
  94. ctx.stroke();
  95. const oldFont = ctx.font;
  96. ctx.font = 'normal 24pt coda';
  97. ctx.fillStyle = "#dddddd";
  98. ctx.beginPath();
  99. ctx.fillText(value.format({ precision: 3 }), x + 20, y + 35);
  100. ctx.font = oldFont;
  101. ctx.strokeStyle = oldStroke;
  102. ctx.fillStyle = oldFill;
  103. }
  104. const canvas = document.querySelector("#display");
  105. /** @type {CanvasRenderingContext2D} */
  106. const ctx = canvas.getContext("2d");
  107. let pixelsPer = (ctx.canvas.clientHeight - 100) / config.height.value;
  108. let heightPer = config.height.clone();
  109. heightPer.value = 1;
  110. if (pixelsPer < config.minLineSize) {
  111. heightPer.value /= pixelsPer / config.minLineSize;
  112. pixelsPer = config.minLineSize;
  113. }
  114. if (pixelsPer > config.maxLineSize) {
  115. heightPer.value /= pixelsPer / config.maxLineSize;
  116. pixelsPer = config.maxLineSize;
  117. }
  118. ctx.clearRect(0, 0, canvas.width, canvas.height);
  119. ctx.scale(1, 1);
  120. ctx.canvas.width = canvas.clientWidth;
  121. ctx.canvas.height = canvas.clientHeight;
  122. ctx.beginPath();
  123. ctx.moveTo(50, 50);
  124. ctx.lineTo(50, ctx.canvas.clientHeight - 50);
  125. ctx.stroke();
  126. ctx.beginPath();
  127. ctx.moveTo(ctx.canvas.clientWidth - 50, 50);
  128. ctx.lineTo(ctx.canvas.clientWidth - 50, ctx.canvas.clientHeight - 50);
  129. ctx.stroke();
  130. drawTicks(ctx, pixelsPer, heightPer);
  131. }
  132. function makeEntity() {
  133. const entityTemplate = {
  134. name: "",
  135. author: "",
  136. scale: 1,
  137. views: {
  138. body: {
  139. attributes: {
  140. height: {
  141. name: "Height",
  142. power: 1,
  143. type: "length",
  144. base: math.unit(1, "meter")
  145. },
  146. weight: {
  147. name: "Weight",
  148. power: 3,
  149. type: "mass",
  150. base: math.unit(80, "kg")
  151. }
  152. },
  153. image: "./man.svg",
  154. name: "Body"
  155. },
  156. pepper: {
  157. attributes: {
  158. height: {
  159. name: "Height",
  160. power: 1,
  161. type: "length",
  162. base: math.unit(50, "centimeter")
  163. },
  164. weight: {
  165. name: "Weight",
  166. power: 3,
  167. type: "mass",
  168. base: math.unit(1, "kg")
  169. }
  170. },
  171. image: "./pepper.png",
  172. name: "Pepper"
  173. }
  174. },
  175. init: function () {
  176. Object.values(this.views).forEach(view => {
  177. view.parent = this;
  178. Object.entries(view.attributes).forEach(([key, val]) => {
  179. Object.defineProperty(
  180. view,
  181. key,
  182. {
  183. get: function() {
  184. return math.multiply(Math.pow(this.parent.scale, this.attributes[key].power), this.attributes[key].base);
  185. },
  186. set: function(value) {
  187. const newScale = Math.pow(math.divide(value, this.attributes[key].base), 1 / this.attributes[key].power);
  188. this.parent.scale = newScale;
  189. }
  190. }
  191. )
  192. });
  193. });
  194. delete this.init;
  195. return this;
  196. }
  197. }.init();
  198. return entityTemplate;
  199. }
  200. function clickDown(target, x, y) {
  201. clicked = target;
  202. const rect = target.getBoundingClientRect();
  203. let entX = document.querySelector("#entities").getBoundingClientRect().x;
  204. let entY = document.querySelector("#entities").getBoundingClientRect().y;
  205. dragOffsetX = x - rect.left + entX;
  206. dragOffsetY = y - rect.top + entY;
  207. clickTimeout = setTimeout(() => { dragging = true }, 200)
  208. }
  209. function clickUp() {
  210. clearTimeout(clickTimeout);
  211. if (clicked) {
  212. if (dragging) {
  213. dragging = false;
  214. } else {
  215. select(clicked);
  216. }
  217. clicked = null;
  218. }
  219. }
  220. function deselect() {
  221. if (selected) {
  222. selected.classList.remove("selected");
  223. }
  224. selected = null;
  225. clearViewList();
  226. clearEntityOptions();
  227. clearViewOptions();
  228. }
  229. function select(target) {
  230. deselect();
  231. selected = target;
  232. selectedEntity = entities[target.dataset.key];
  233. selected.classList.add("selected");
  234. entityInfo(selectedEntity, target.dataset.view);
  235. configViewList(selectedEntity, target.dataset.view);
  236. configEntityOptions(selectedEntity);
  237. configViewOptions(selectedEntity, target.dataset.view);
  238. }
  239. function entityInfo(entity, view) {
  240. document.querySelector("#entity-name").innerText = "Name: " + entity.name;
  241. document.querySelector("#entity-author").innerText = "Author: " + entity.author;
  242. document.querySelector("#entity-height").innerText = "Height: " + entity.views[view].height.format({ precision: 3 });
  243. }
  244. function configViewList(entity, selectedView) {
  245. const list = document.querySelector("#entity-view");
  246. list.innerHTML = "";
  247. list.style.display = "block";
  248. console.log
  249. Object.keys(entity.views).forEach(view => {
  250. const option = document.createElement("option");
  251. option.innerText = entity.views[view].name;
  252. option.value = view;
  253. if (view === selectedView) {
  254. option.selected = true;
  255. }
  256. list.appendChild(option);
  257. });
  258. }
  259. function clearViewList() {
  260. const list = document.querySelector("#entity-view");
  261. list.innerHTML = "";
  262. list.style.display = "none";
  263. }
  264. function configEntityOptions(entity) {
  265. const holder = document.querySelector("#options-entity");
  266. holder.innerHTML = "";
  267. const scaleLabel = document.createElement("div");
  268. scaleLabel.classList.add("options-label");
  269. scaleLabel.innerText = "Scale";
  270. const scaleRow = document.createElement("div");
  271. scaleRow.classList.add("options-row");
  272. const scaleInput = document.createElement("input");
  273. scaleInput.classList.add("options-field-numeric");
  274. scaleInput.addEventListener("input", e => {
  275. entity.scale = e.target.value;
  276. updateSizes();
  277. });
  278. scaleInput.setAttribute("min", 1);
  279. scaleInput.setAttribute("type", "number");
  280. scaleInput.value = entity.scale;
  281. scaleRow.appendChild(scaleInput);
  282. holder.appendChild(scaleLabel);
  283. holder.appendChild(scaleRow);
  284. const nameLabel = document.createElement("div");
  285. nameLabel.classList.add("options-label");
  286. nameLabel.innerText = "Name";
  287. const nameRow = document.createElement("div");
  288. nameRow.classList.add("options-row");
  289. const nameInput = document.createElement("input");
  290. nameInput.classList.add("options-field-text");
  291. nameInput.value = entity.name;
  292. nameInput.addEventListener("input", e => {
  293. entity.name = e.target.value;
  294. updateSizes();
  295. })
  296. nameRow.appendChild(nameInput);
  297. holder.appendChild(nameLabel);
  298. holder.appendChild(nameRow);
  299. }
  300. function clearEntityOptions() {
  301. const holder = document.querySelector("#options-entity");
  302. holder.innerHTML = "";
  303. }
  304. function configViewOptions(entity, view) {
  305. const holder = document.querySelector("#options-view");
  306. holder.innerHTML = "";
  307. Object.entries(entity.views[view].attributes).forEach(([key, val]) => {
  308. const label = document.createElement("div");
  309. label.classList.add("options-label");
  310. label.innerText = val.name;
  311. holder.appendChild(label);
  312. const row = document.createElement("div");
  313. row.classList.add("options-row");
  314. holder.appendChild(row);
  315. const input = document.createElement("input");
  316. input.classList.add("options-field-numeric");
  317. input.value = entity.views[view][key].value;
  318. const unit = document.createElement("select");
  319. unitChoices[val.type].forEach(name => {
  320. const option = document.createElement("option");
  321. option.innerText = name;
  322. unit.appendChild(option);
  323. });
  324. input.addEventListener("input", e => {
  325. entity.views[view][key] = math.unit(input.value, unit.value);
  326. updateSizes();
  327. });
  328. unit.addEventListener("input", e => {
  329. entity.views[view][key] = math.unit(input.value, unit.value);
  330. updateSizes();
  331. });
  332. row.appendChild(input);
  333. row.appendChild(unit);
  334. });
  335. }
  336. function clearViewOptions(entity, view) {
  337. const holder = document.querySelector("#options-view");
  338. holder.innerHTML = "";
  339. }
  340. // this is a crime against humanity, and also stolen from
  341. // stack overflow
  342. // https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent
  343. const testCanvas = document.createElement("canvas");
  344. testCanvas.id = "test-canvas";
  345. const testCtx = testCanvas.getContext("2d");
  346. function testClick(event) {
  347. console.log(event)
  348. const target = event.target;
  349. // Get click coordinates
  350. var x = event.clientX - target.getBoundingClientRect().x,
  351. y = event.clientY - target.getBoundingClientRect().y,
  352. w = testCtx.canvas.width = target.width,
  353. h = testCtx.canvas.height = target.height,
  354. alpha;
  355. // Draw image to canvas
  356. // and read Alpha channel value
  357. testCtx.drawImage(target, 0, 0, w, h);
  358. alpha = testCtx.getImageData(x, y, 1, 1).data[3]; // [0]R [1]G [2]B [3]A
  359. // If pixel is transparent,
  360. // retrieve the element underneath and trigger it's click event
  361. if (alpha === 0) {
  362. const oldDisplay = target.style.display;
  363. target.style.display = "none";
  364. const newTarget = document.elementFromPoint(event.clientX, event.clientY);
  365. newTarget.dispatchEvent(new MouseEvent(event.type, {
  366. "clientX": event.clientX,
  367. "clientY": event.clientY
  368. }));
  369. target.style.display = oldDisplay;
  370. } else {
  371. clickDown(target.parentElement, event.clientX, event.clientY);
  372. }
  373. }
  374. function displayEntity(entity, view, x, y) {
  375. const box = document.createElement("div");
  376. box.classList.add("entity-box");
  377. const img = document.createElement("img");
  378. img.classList.add("entity-image");
  379. const nameTag = document.createElement("div");
  380. nameTag.classList.add("entity-name");
  381. nameTag.innerText = entity.name;
  382. box.appendChild(img);
  383. box.appendChild(nameTag);
  384. img.src = entity.views[view].image
  385. box.dataset.x = x;
  386. box.dataset.y = y;
  387. img.addEventListener("mousedown", e => { testClick(e); e.stopPropagation() });
  388. img.addEventListener("touchstart", e => {
  389. const fakeEvent = {
  390. target: e.target,
  391. clientX: e.touches[0].clientX,
  392. clientY: e.touches[0].clientY
  393. };
  394. testClick(fakeEvent);});
  395. box.id = "entity-" + entityIndex;
  396. box.dataset.key = entityIndex;
  397. box.dataset.view = view;
  398. entities[entityIndex] = entity;
  399. entityIndex += 1;
  400. const world = document.querySelector("#entities");
  401. world.appendChild(box);
  402. updateEntityElement(entity, box);
  403. }
  404. document.addEventListener("DOMContentLoaded", () => {
  405. for (let x = 0; x < 5; x++) {
  406. const entity = makeEntity();
  407. entity.name = "Dude";
  408. entity.author = "Fen"
  409. const x = 0.25 + Math.random() * 0.5;
  410. const y = 0.25 + Math.random() * 0.5;
  411. displayEntity(entity, "body", x, y);
  412. }
  413. document.querySelector("body").appendChild(testCtx.canvas);
  414. updateSizes();
  415. document.querySelector("#options-height-value").addEventListener("input", e => {
  416. updateWorldHeight();
  417. })
  418. document.querySelector("#options-height-unit").addEventListener("input", e => {
  419. updateWorldHeight();
  420. })
  421. world.addEventListener("mousedown", e => deselect());
  422. document.addEventListener("mouseup", e => clickUp());
  423. document.addEventListener("touchend", e => clickUp());
  424. document.querySelector("#entity-view").addEventListener("input", e => {
  425. console.log(e.target.value)
  426. selected.dataset.view = e.target.value
  427. selected.querySelector(".entity-image").src = entities[selected.dataset.key].views[e.target.value].image;
  428. updateSizes();
  429. });
  430. clearViewList();
  431. });
  432. window.addEventListener("resize", () => {
  433. updateSizes();
  434. })
  435. document.addEventListener("mousemove", (e) => {
  436. if (clicked) {
  437. const position = snapRel(abs2rel({ x: e.clientX - dragOffsetX, y: e.clientY - dragOffsetY }));
  438. clicked.dataset.x = position.x;
  439. clicked.dataset.y = position.y;
  440. updateEntityElement(entities[clicked.dataset.key], clicked);
  441. }
  442. });
  443. document.addEventListener("touchmove", (e) => {
  444. if (clicked) {
  445. e.preventDefault();
  446. let x = e.touches[0].clientX;
  447. let y = e.touches[0].clientY;
  448. const position = snapRel(abs2rel({ x: x - dragOffsetX, y: y - dragOffsetY }));
  449. clicked.dataset.x = position.x;
  450. clicked.dataset.y = position.y;
  451. updateEntityElement(entities[clicked.dataset.key], clicked);
  452. }
  453. });
  454. function updateWorldHeight() {
  455. const value = Math.max(1, document.querySelector("#options-height-value").value);
  456. const unit = document.querySelector("#options-height-unit").value;
  457. const oldHeight = config.height;
  458. config.height = math.unit(value + " " + unit)
  459. Object.entries(entities).forEach(([key, entity]) => {
  460. const element = document.querySelector("#entity-" + key);
  461. const newPosition = adjustAbs({ x: element.dataset.x, y: element.dataset.y }, oldHeight, config.height);
  462. element.dataset.x = newPosition.x;
  463. element.dataset.y = newPosition.y;
  464. });
  465. updateSizes();
  466. }