You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CircuitSim.svelte 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. <canvas bind:this={canvas}></canvas>
  2. <style>
  3. canvas {
  4. width: 100vw;
  5. height: 100vh;
  6. position: absolute;
  7. top: 0px;
  8. left: 0px;
  9. }
  10. </style>
  11. <script>
  12. import {onMount} from 'svelte';
  13. class Link {
  14. constructor(from, index) {
  15. this.from = from;
  16. this.index = index;
  17. this.current = false;
  18. this.next = false;
  19. this.connections = [];
  20. }
  21. connect(node, index) {
  22. node.inputs[index].links.push(this);
  23. this.connections.push({node, index});
  24. }
  25. disconnect(node, index) {
  26. for (let i = this.connections.length - 1; i >= 0; --i) {
  27. let conn = this.connections[i];
  28. if (conn.node == node && conn.index == index) {
  29. this.connections.splice(i, 1);
  30. }
  31. }
  32. }
  33. destroy() {
  34. for (let conn of this.connections) {
  35. for (let i = conn.node.inputs[conn.index].links.length - 1; i >= 0; --i) {
  36. if (conn.node.inputs[conn.index].links[i] == this) {
  37. conn.node.inputs[conn.index].links.splice(i, 1);
  38. }
  39. }
  40. }
  41. }
  42. commit() {
  43. this.current = this.next;
  44. }
  45. }
  46. class Input {
  47. constructor(x, y) {
  48. this.name = "IN";
  49. this.x = x;
  50. this.y = y;
  51. this.inputs = [];
  52. this.outputs = [{name: "Out", link: new Link(this, 0)}];
  53. this.width = 3;
  54. this.height = 1;
  55. this.lit = false;
  56. }
  57. activate() {
  58. this.lit = !this.lit;
  59. }
  60. tick() {
  61. if (this.lit) {
  62. this.outputs[0].link.next = true;
  63. } else {
  64. this.outputs[0].link.next = false;
  65. }
  66. }
  67. commit() {
  68. this.outputs[0].link.commit();
  69. }
  70. }
  71. class NotGate {
  72. constructor(x, y) {
  73. this.name = "NOT";
  74. this.x = x;
  75. this.y = y;
  76. this.inputs = [{name: "In", links: []}];
  77. this.outputs = [{name: "Out", link: new Link(this, 0)}];
  78. this.width = 4;
  79. this.height = 1;
  80. this.lit = false;
  81. }
  82. tick() {
  83. this.outputs[0].link.next = true;
  84. for (let link of this.inputs[0].links) {
  85. if (link.current) {
  86. this.outputs[0].link.next = false;
  87. break;
  88. }
  89. }
  90. }
  91. commit() {
  92. this.outputs[0].link.commit();
  93. this.lit = this.outputs[0].link.current;
  94. }
  95. }
  96. class Diode {
  97. constructor(x, y) {
  98. this.name = "DIODE";
  99. this.x = x;
  100. this.y = y;
  101. this.inputs = [{name: "In", links: []}];
  102. this.outputs = [{name: "Out", link: new Link(this, 0)}];
  103. this.width = 4;
  104. this.height = 1;
  105. this.lit = false;
  106. }
  107. tick() {
  108. this.outputs[0].link.next = false;
  109. for (let link of this.inputs[0].links) {
  110. if (link.current) {
  111. this.outputs[0].link.next = true;
  112. break;
  113. }
  114. }
  115. }
  116. commit() {
  117. this.outputs[0].link.commit();
  118. this.lit = this.outputs[0].link.current;
  119. }
  120. }
  121. class Lamp {
  122. constructor(x, y) {
  123. this.name = "LAMP";
  124. this.x = x;
  125. this.y = y;
  126. this.inputs = [{name: "In", links: []}];
  127. this.outputs = [];
  128. this.width = 4;
  129. this.height = 1;
  130. this.lit = false;
  131. this.nextLit = false;
  132. }
  133. tick() {
  134. this.nextLit = false;
  135. for (let link of this.inputs[0].links) {
  136. if (link.current) {
  137. this.nextLit = true;
  138. break;
  139. }
  140. }
  141. }
  142. commit() {
  143. this.lit = this.nextLit;
  144. }
  145. }
  146. class LogicSim {
  147. constructor(can) {
  148. this.scale = 40;
  149. this.can = can;
  150. this.ctx = can.getContext("2d");
  151. this.nodes = [];
  152. this.frameRequested = false;
  153. this.clickCancelled = false;
  154. this.currentTouch = null;
  155. this.x = 0;
  156. this.y = 0;
  157. this.tooltip = null;
  158. this.selectedNodes = [];
  159. this.mouseMoveStart = null;
  160. this.currentLink = null;
  161. this.selection = null;
  162. this.requestFrame();
  163. window.addEventListener("resize", () => {
  164. this.requestFrame();
  165. });
  166. window.addEventListener("keydown", evt => {
  167. this.onKeyDown(evt.key);
  168. });
  169. can.addEventListener("wheel", evt => {
  170. this.onScroll(evt.deltaY / 10);
  171. });
  172. can.addEventListener("mousedown", evt => {
  173. this.onMouseDown(evt.offsetX, evt.offsetY, evt.buttons, evt);
  174. });
  175. can.addEventListener("mouseup", evt => {
  176. this.onMouseUp();
  177. });
  178. can.addEventListener("mousemove", evt => {
  179. this.onMouseMove(evt.offsetX, evt.offsetY, evt.movementX, evt.movementY, evt.buttons);
  180. });
  181. can.addEventListener("click", evt => {
  182. this.onClick(evt.offsetX, evt.offsetY);
  183. });
  184. can.addEventListener("touchstart", evt => {
  185. if (this.currentTouch != null || evt.changedTouches.length == 0) {
  186. return;
  187. }
  188. let relevantTouch = evt.changedTouches[0];
  189. this.currentTouch = relevantTouch;
  190. this.onMouseDown(relevantTouch.clientX, relevantTouch.clientY, 1, evt);
  191. });
  192. can.addEventListener("touchend", evt => {
  193. if (this.currentTouch == null) {
  194. return;
  195. }
  196. let relevantTouch = null;
  197. for (let touch of evt.changedTouches) {
  198. if (touch.identifier == this.currentTouch.identifier) {
  199. relevantTouch = touch;
  200. break;
  201. }
  202. }
  203. if (relevantTouch == null) {
  204. return;
  205. }
  206. this.currentTouch = null;
  207. tis.onMouseUp();
  208. });
  209. can.addEventListener("touchmove", evt => {
  210. if (this.currentTouch == null) {
  211. return;
  212. }
  213. let relevantTouch = null;
  214. for (let touch of evt.changedTouches) {
  215. if (touch.identifier == this.currentTouch.identifier) {
  216. relevantTouch = touch;
  217. break;
  218. }
  219. }
  220. if (relevantTouch == null) {
  221. return;
  222. }
  223. let deltaX = this.currentTouch.clientX - relevantTouch.clientX;
  224. let deltaY = this.currentTouch.clientY - relevantTouch.clientY;
  225. this.currentTouch = relevantTouch;
  226. this.onMouseMove(relevantTouch.clientX, relevantTouch.clientY, -deltaX, -deltaY, 1);
  227. });
  228. }
  229. onKeyDown(key) {
  230. let responded = true;
  231. if (key == "Escape" || key == "q") {
  232. this.tooltip = null;
  233. this.selectedNodes = [];
  234. this.mouseMoveStart = null;
  235. this.currentLink = null;
  236. this.selection = null;
  237. this.requestFrame();
  238. } else if (key == "Delete") {
  239. for (let node of this.selectedNodes) {
  240. this.deleteNode(node);
  241. }
  242. this.selectedNodes = [];
  243. } else if (key == "Enter") {
  244. for (let node of this.selectedNodes) {
  245. if (node.activate) {
  246. node.activate();
  247. this.requestFrame();
  248. }
  249. }
  250. } else {
  251. responded = false;
  252. }
  253. if (responded) {
  254. evt.preventDefault();
  255. }
  256. }
  257. onScroll(delta) {
  258. this.scale -= delta * (this.scale / 40);
  259. if (this.scale > 200) {
  260. this.scale = 200;
  261. } else if (this.scale < 10) {
  262. this.scale = 10;
  263. }
  264. this.requestFrame();
  265. }
  266. onMouseDown(offsetX, offsetY, buttons, keys) {
  267. this.clickCancelled = false;
  268. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  269. let node = this.getNodeAt(x, y);
  270. this.mouseMoveStart = {x, y, node};
  271. if (keys.shiftKey && buttons == 1) {
  272. this.selectedNodes = [];
  273. this.selection = {fromX: x, fromY: y, toX: x, toY: y};
  274. this.tooltip = null;
  275. this.requestFrame();
  276. } else if (node != null && buttons == 1) {
  277. if (this.selectedNodes.indexOf(node) == -1) {
  278. this.selectedNodes = [node];
  279. this.requestFrame();
  280. }
  281. } else if (buttons == 1) {
  282. this.selectedNodes = [];
  283. this.requestFrame();
  284. }
  285. }
  286. onMouseUp() {
  287. this.mouseMoveStart = null;
  288. if (this.selection) {
  289. this.selectedNodes = [];
  290. let [x1, y1] = [this.selection.fromX, this.selection.fromY];
  291. let [x2, y2] = [this.selection.toX, this.selection.toY];
  292. // Make sure we actually have a rect from top-left to bottom-right
  293. let tmp;
  294. if (x1 > x2) {
  295. tmp = x1;
  296. x1 = x2;
  297. x2 = tmp;
  298. }
  299. if (y1 > y2) {
  300. tmp = y1;
  301. y1 = y2;
  302. y2 = tmp;
  303. }
  304. for (let node of this.nodes) {
  305. let nodeX1 = node.x;
  306. let nodeY1 = node.y - 0.5;
  307. let nodeX2 = nodeX1 + node.width;
  308. let nodeY2 = nodeY1 + node.height;
  309. if (x1 < nodeX2 && x2 > nodeX1 && y1 < nodeY2 && y2 > nodeY1) {
  310. this.selectedNodes.push(node);
  311. }
  312. }
  313. this.selection = null;
  314. this.requestFrame();
  315. }
  316. }
  317. onMouseMove(offsetX, offsetY, movementX, movementY, buttons) {
  318. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  319. let dx = 0, dy = 0;
  320. if (this.mouseMoveStart) {
  321. dx = x - this.mouseMoveStart.x;
  322. dy = y - this.mouseMoveStart.y;
  323. }
  324. if (
  325. buttons == 1 && this.selection == null &&
  326. this.mouseMoveStart != null && this.currentLink == null) {
  327. if (this.mouseMoveStart.node == null) {
  328. this.x -= movementX / this.scale;
  329. this.y -= movementY / this.scale;
  330. this.requestFrame();
  331. } else {
  332. let nodeDX = Math.round(dx);
  333. let nodeDY = Math.round(dy);
  334. if (nodeDX != 0 || nodeDY != 0) {
  335. this.clickCancelled = true;
  336. this.mouseMoveStart.x += nodeDX;
  337. this.mouseMoveStart.y += nodeDY;
  338. for (let node of this.selectedNodes) {
  339. node.x += nodeDX;
  340. node.y += nodeDY;
  341. }
  342. this.requestFrame();
  343. }
  344. }
  345. }
  346. let node = this.getNodeAt(x, y);
  347. let io = null;
  348. if (node != null) {
  349. io = this.getNodeIOAt(node, x, y);
  350. }
  351. if (io == null && this.tooltip == null) {
  352. // Do nothing
  353. } else if (io == null && this.tooltip != null) {
  354. this.tooltip = null;
  355. this.requestFrame();
  356. } else {
  357. this.tooltip = {
  358. text: io.io.name,
  359. x, y,
  360. };
  361. this.requestFrame();
  362. }
  363. if (this.selection != null) {
  364. this.selection.toX = x;
  365. this.selection.toY = y;
  366. this.requestFrame();
  367. }
  368. if (this.currentLink != null) {
  369. let p = this.currentLink.path[this.currentLink.path.length - 1];
  370. p.x = x;
  371. p.y = y;
  372. this.requestFrame();
  373. }
  374. }
  375. onClick(offsetX, offsetY) {
  376. if (this.clickCancelled) {
  377. return;
  378. }
  379. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  380. let node = this.getNodeAt(x, y);
  381. let io = null;
  382. if (node != null) {
  383. io = this.getNodeIOAt(node, x, y);
  384. }
  385. if (this.currentLink != null) {
  386. if (io) {
  387. let fromNode = this.currentLink.from.node;
  388. let fromIO = this.currentLink.from.io;
  389. if (fromIO.type == "input" && io.type == "output") {
  390. io.io.link.connect(fromNode, fromIO.index);
  391. } else if (fromIO.type == "output" && io.type == "input") {
  392. fromIO.io.link.connect(node, io.index);
  393. }
  394. if (fromIO.type != io.type) {
  395. this.currentLink = null;
  396. this.requestFrame();
  397. }
  398. } else {
  399. let path = this.currentLink.path;
  400. path[path.length - 1].x = Math.round(path[path.length - 1].x);
  401. path[path.length - 1].y = Math.round(path[path.length - 1].y);
  402. path.push({x, y});
  403. this.requestFrame();
  404. }
  405. } else if (io != null) {
  406. this.currentLink = {from: {node, io}, path: [{x, y}]};
  407. } else {
  408. if (node && node.activate) {
  409. node.activate();
  410. this.requestFrame();
  411. }
  412. }
  413. }
  414. deleteNode(node) {
  415. for (let i = 0; i < node.inputs.length; ++i) {
  416. let input = node.inputs[i];
  417. for (let link of input.links) {
  418. link.disconnect(node, i);
  419. }
  420. }
  421. for (let out of node.outputs) {
  422. out.link.destroy();
  423. }
  424. this.nodes.splice(this.nodes.indexOf(node), 1);
  425. }
  426. coordsFromScreenPos(screenX, screenY) {
  427. return [
  428. (screenX - this.can.offsetWidth / 2) / this.scale + this.x,
  429. (screenY - this.can.offsetHeight / 2) / this.scale + this.y,
  430. ];
  431. }
  432. getNodeAt(x, y) {
  433. for (let i = this.nodes.length - 1; i >= 0; --i) {
  434. let node = this.nodes[i];
  435. let nodeX1 = node.x - 0.5;
  436. let nodeY1 = node.y - 0.5;
  437. let nodeX2 = node.x + node.width + 0.5;
  438. let nodeY2 = node.y - 0.5 + node.height;
  439. if (
  440. x >= nodeX1 && x <= nodeX2 &&
  441. y >= nodeY1 && y <= nodeY2) {
  442. return node;
  443. }
  444. }
  445. return null;
  446. }
  447. getNodeIOAt(node, x, y) {
  448. for (let n = 0; n < node.inputs.length; ++n) {
  449. let dx = node.x - x;
  450. let dy = node.y + n - y;
  451. let dist = Math.abs(Math.sqrt(dx * dx + dy * dy));
  452. if (dist < 0.5) {
  453. return {type: "input", io: node.inputs[n], index: n};
  454. }
  455. }
  456. for (let n = 0; n < node.outputs.length; ++n) {
  457. let dx = node.x + node.width - x;
  458. let dy = node.y + n - y;
  459. let dist = Math.abs(Math.sqrt(dx * dx + dy * dy));
  460. if (dist < 0.5) {
  461. return {type: "output", io: node.outputs[n], index: n};
  462. }
  463. }
  464. return null;
  465. }
  466. requestFrame() {
  467. if (this.frameRequested) {
  468. return;
  469. }
  470. this.frameRequested = true;
  471. window.requestAnimationFrame(() => {
  472. this.frameRequested = false;
  473. this.draw();
  474. });
  475. }
  476. addNodeAtCursor(node) {
  477. node.x = Math.round(this.x);
  478. node.y = Math.round(this.y);
  479. this.nodes.push(node);
  480. this.requestFrame();
  481. }
  482. draw() {
  483. let pixelRatio = 1;
  484. if (window.devicePixelRatio != null) {
  485. pixelRatio = window.devicePixelRatio;
  486. }
  487. this.can.width = this.can.offsetWidth * pixelRatio;
  488. this.can.height = this.can.offsetHeight * pixelRatio;
  489. let canWidth = this.can.width / this.scale;
  490. let canHeight = this.can.height / this.scale;
  491. this.ctx.save();
  492. this.ctx.translate(this.can.width / 2, this.can.height / 2);
  493. this.ctx.scale(this.scale * pixelRatio, this.scale * pixelRatio);
  494. this.ctx.translate(-this.x, -this.y);
  495. this.ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
  496. this.ctx.lineWidth = 1 / this.scale;
  497. let gridStartX = Math.floor(this.x - canWidth / 2);
  498. let gridStartY = Math.floor(this.y - canHeight / 2);
  499. for (let x = gridStartX; x < canWidth + gridStartX + 1; x += 1) {
  500. this.ctx.beginPath();
  501. this.ctx.moveTo(x, gridStartY - canHeight - 1);
  502. this.ctx.lineTo(x, gridStartY + canHeight + 1);
  503. this.ctx.stroke();
  504. }
  505. for (let y = gridStartY; y < canHeight + gridStartY + 1; y += 1) {
  506. this.ctx.beginPath();
  507. this.ctx.moveTo(gridStartX - canWidth - 1, y);
  508. this.ctx.lineTo(gridStartX + canWidth + 1, y);
  509. this.ctx.stroke();
  510. }
  511. this.ctx.font = "0.8px Arial";
  512. this.ctx.lineWidth = 0.1;
  513. for (let node of this.nodes) {
  514. for (let output of node.outputs) {
  515. let link = output.link;
  516. if (link.current) {
  517. this.ctx.strokeStyle = "red";
  518. } else {
  519. this.ctx.strokeStyle = "brown";
  520. }
  521. for (let conn of link.connections) {
  522. this.ctx.beginPath();
  523. this.ctx.moveTo(link.from.x + link.from.width, link.from.y + link.index);
  524. this.ctx.lineTo(conn.node.x, conn.node.y + conn.index);
  525. this.ctx.stroke();
  526. }
  527. }
  528. }
  529. this.ctx.strokeStyle = "black";
  530. this.ctx.lineWidth = 0.1;
  531. this.ctx.setLineDash([0.2, 0.1]);
  532. for (let node of this.selectedNodes) {
  533. this.ctx.beginPath();
  534. this.ctx.strokeRect(node.x - 0.1, node.y - 0.5 - 0.1, node.width + 0.2, node.height + 0.2);
  535. }
  536. this.ctx.lineWidth = 0.1;
  537. this.ctx.setLineDash([]);
  538. for (let node of this.nodes) {
  539. if (node.lit) {
  540. this.ctx.fillStyle = "red";
  541. } else {
  542. this.ctx.fillStyle = "brown";
  543. }
  544. this.ctx.strokeStyle = "black";
  545. let metrics = this.ctx.measureText(node.name);
  546. let textWidth = metrics.width;
  547. this.ctx.fillRect(node.x, node.y - 0.5, node.width, node.height);
  548. this.ctx.fillStyle = "white";
  549. this.ctx.fillText(
  550. node.name,
  551. node.x + node.width / 2 - textWidth / 2,
  552. node.y - 0.5 + 0.8);
  553. this.ctx.fillStyle = "black";
  554. this.ctx.strokeStyle = "white";
  555. for (let y = 0; y < node.inputs.length; ++y) {
  556. this.ctx.beginPath();
  557. this.ctx.arc(node.x, node.y + y, 0.45, 0, 2 * Math.PI);
  558. this.ctx.fill();
  559. this.ctx.stroke();
  560. }
  561. for (let y = 0; y < node.outputs.length; ++y) {
  562. this.ctx.beginPath();
  563. this.ctx.arc(node.x + node.width, node.y + y, 0.45, 0, 2 * Math.PI);
  564. this.ctx.fill();
  565. this.ctx.stroke();
  566. }
  567. }
  568. if (this.currentLink != null) {
  569. this.ctx.strokeStyle = "black";
  570. let from = this.currentLink.from;
  571. let fromX = from.node.x;
  572. let fromY = from.node.y + from.io.index;
  573. if (from.io.type == "output") {
  574. fromX += from.node.width;
  575. }
  576. this.ctx.beginPath();
  577. this.ctx.moveTo(fromX, fromY);
  578. for (let el of this.currentLink.path) {
  579. this.ctx.lineTo(el.x, el.y);
  580. }
  581. this.ctx.stroke();
  582. }
  583. if (this.tooltip != null) {
  584. this.ctx.fillStyle = "black";
  585. this.ctx.strokeStyle = "grey";
  586. let metrics = this.ctx.measureText(this.tooltip.text);
  587. let textWidth = metrics.width;
  588. let textAscent = metrics.actualBoundingBoxAscent;
  589. let x = this.tooltip.x - (textWidth / 4);
  590. let y = this.tooltip.y - 1.2;
  591. this.ctx.beginPath();
  592. this.ctx.rect(
  593. x - 0.17, y - 0.2, textWidth + 0.4, 1.1);
  594. this.ctx.fill();
  595. this.ctx.stroke();
  596. this.ctx.fillStyle = "white";
  597. this.ctx.fillText(this.tooltip.text, x, y + 0.6);
  598. }
  599. if (this.selection != null) {
  600. this.ctx.fillStyle = "rgba(100, 100, 250, 0.2)";
  601. this.ctx.strokeStyle = "black";
  602. this.ctx.beginPath();
  603. this.ctx.rect(
  604. this.selection.fromX, this.selection.fromY,
  605. this.selection.toX - this.selection.fromX,
  606. this.selection.toY - this.selection.fromY);
  607. this.ctx.fill();
  608. this.ctx.stroke();
  609. }
  610. this.ctx.restore();
  611. }
  612. update() {
  613. for (let node of this.nodes) {
  614. node.tick();
  615. }
  616. for (let node of this.nodes) {
  617. node.commit();
  618. }
  619. this.requestFrame();
  620. }
  621. }
  622. let canvas;
  623. let sim;
  624. onMount(() => {
  625. sim = new LogicSim(canvas);
  626. });
  627. </script>