Advertisement
Guest User

Untitled

a guest
Oct 22nd, 2019
88
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.86 KB | None | 0 0
  1. <!doctype html>
  2. <html>
  3. <head>
  4. <style type="text/css">
  5. #container {
  6. width:800px;
  7. height:600px;
  8. border:1px solid lightgray;
  9. }
  10.  
  11. #canvas {
  12. width:100%;
  13. height:100%;
  14. }
  15.  
  16. </style>
  17. <script type="text/javascript">
  18.  
  19. class Box {
  20. constructor(args) {
  21. this.left = args.left;
  22. this.top = args.top;
  23. this.width = args.width;
  24. this.height = args.height;
  25. }
  26.  
  27. containsPoint(x, y) {
  28. let contains =
  29. x >= this.left
  30. && x <= this.left + this.width
  31. && y >= this.top
  32. && y <= this.top + this.height
  33. ;
  34. return contains;
  35. }
  36. };
  37.  
  38. class Rect {
  39.  
  40. constructor(x, y, w, h, z) {
  41. this.x = x;
  42. this.y = y;
  43. this.width = w;
  44. this.height = h;
  45. this.z = z === undefined ? 0 : z;
  46. }
  47.  
  48. moveTo(x, y) {
  49. this.x = x;
  50. this.y = y;
  51. }
  52.  
  53. moveBy(dx, dy) {
  54. this.moveTo(this.x + dx, this.y + dy);
  55. }
  56.  
  57. resize(w, h) {
  58. this.width = w;
  59. this.height = h;
  60. }
  61.  
  62. getX() {
  63. return this.x;
  64. }
  65.  
  66. getY() {
  67. return this.y;
  68. }
  69.  
  70. getRight() {
  71. return this.x + this.width;
  72. }
  73.  
  74. getBottom() {
  75. return this.y + this.height;
  76. }
  77.  
  78. getWidth() {
  79. return this.width;
  80. }
  81.  
  82. getHeight() {
  83. return this.height;
  84. }
  85.  
  86. _draw(ctx, bbox) {
  87. ctx.beginPath();
  88. ctx.rect(bbox.left + 0.5, bbox.top + 0.5, bbox.width, bbox.height);
  89. }
  90.  
  91. drawGhost(ctx, bbox) {
  92. this._draw(ctx, bbox);
  93. ctx.stroke();
  94. }
  95.  
  96. draw(ctx) {
  97. ctx.fillStyle='white';
  98. this._draw(ctx, this.bbox());
  99. ctx.fill();
  100. ctx.stroke();
  101. }
  102.  
  103. zIndex() {
  104. return this.z;
  105. }
  106.  
  107. containsPoint(x, y) {
  108. let contains =
  109. x >= this.x
  110. && x <= this.x + this.width
  111. && y >= this.y
  112. && y <= this.y + this.height;
  113. return contains;
  114. }
  115.  
  116. bbox() {
  117. return {
  118. left:this.x,
  119. top:this.y,
  120. width:this.width,
  121. height:this.height
  122. };
  123. }
  124. };
  125.  
  126. class Oval extends Rect {
  127.  
  128. constructor(x, y, w, h, z) {
  129. super(x, y, w, h, z);
  130. }
  131.  
  132. _drawEllipse(ctx, bbox) {
  133. let rx = bbox.width /2;
  134. let ry = bbox.height /2;
  135. if(rx <= 0 || ry <=0) {
  136. return;
  137. }
  138. let cx = bbox.left + rx;
  139. let cy = bbox.top + ry;
  140. ctx.beginPath();
  141. ctx.ellipse(cx + 0.5, cy + 0.5, rx, ry, 0, 0, 2 * Math.PI);
  142. }
  143.  
  144. draw(ctx) {
  145. this._drawEllipse(ctx, this.bbox());
  146. ctx.fillStyle='pink';
  147. ctx.fill();
  148. ctx.stroke();
  149. }
  150.  
  151. drawGhost(ctx, bbox) {
  152. this._drawEllipse(ctx, bbox);
  153. ctx.stroke();
  154. }
  155.  
  156. };
  157.  
  158. class Circle extends Oval {
  159. constructor(x, y, r, z) {
  160. super(x, y, r*2, r*2, z);
  161. }
  162.  
  163. resize(w, h) {
  164. let s = Math.max(w,h);
  165. super.resize(s,s);
  166. }
  167. };
  168.  
  169. class Group extends Rect {
  170.  
  171. constructor(items) {
  172. let left = items.reduce((r, i) => r.getX() < i.getX() ? r : i).getX();
  173. let top = items.reduce((r, i) => r.getY() < i.getY() ? r : i).getY();
  174. let right = items.reduce((r, i) => r.getRight() > i.getRight() ? r : i).getRight();
  175. let bottom = items.reduce((r, i) => r.getBottom() > i.getBottom() ? r : i).getBottom();
  176. super(left, top, right-left, bottom-top);
  177. this.items = items;
  178. }
  179.  
  180. moveTo(x, y) {
  181. for(let item of this.items){
  182. let dx = item.getX() - this.x;
  183. let dy = item.getY() - this.y;
  184. item.moveTo(x + dx, y + dy);
  185. }
  186. this.x = x;
  187. this.y = y;
  188. }
  189.  
  190. resize(w,h) {
  191. if(w < 0) {
  192. throw Error('negative w');
  193. }
  194. if(h < 0) {
  195. throw Error('negative h');
  196. }
  197. let rx = w / this.width;
  198. let ry = h / this.height;
  199. this.width = - Infinity;
  200. this.height = - Infinity;
  201. for(let item of this.items) {
  202. let dx = item.getX() - this.x;
  203. let dy = item.getY() - this.y;
  204. item.resize(Math.round(item.getWidth() * rx), Math.round(item.getHeight() * ry));
  205. this.width = Math.max(dx + item.getWidth(), this.width);
  206. this.height = Math.max(dx + item.getHeight(), this.height);
  207. item.moveTo(
  208. Math.round(this.x + dx * rx),
  209. Math.round(this.y + dy * ry)
  210. );
  211. }
  212. }
  213.  
  214. draw(ctx) {
  215. for(let item of this.items) {
  216. item.draw(ctx);
  217. }
  218. }
  219.  
  220. drawGhost(ctx, bbox) {
  221. let rx = bbox.width / this.width;
  222. let ry = bbox.height / this.height;
  223. for(let item of this.items) {
  224. let dx = item.getX() - this.x;
  225. let dy = item.getY() - this.y;
  226. let itembbox = {
  227. left : bbox.left + dx * rx,
  228. top : bbox.top + dy *ry,
  229. width : item.getWidth() * rx,
  230. height : item.getHeight() * ry
  231. };
  232. item.drawGhost(ctx, itembbox);
  233. }
  234. }
  235. };
  236.  
  237. function samePos(a, b) {
  238. return a.x == b.x && a.y == b.y;
  239. }
  240.  
  241. class DragAction {
  242. constructor(originX, originY) {
  243. if (new.target === DragAction) {
  244. throw new TypeError("Cannot instanciate an abstract class");
  245. }
  246. this.origin = {x:originX, y:originY};
  247. this.dragOffset = {x:0,y:0};
  248. this.hasMoved_ = false;
  249. }
  250.  
  251. update(x, y) {
  252. this.dragOffset = {
  253. x : x - this.origin.x,
  254. y : y - this.origin.y
  255. };
  256. this.hasMoved_ = true;
  257. }
  258.  
  259. hasMoved() {
  260. return this.hasMoved_;
  261. }
  262. };
  263.  
  264. class SelectAction extends DragAction {
  265. constructor(originX, originY, u) {
  266. super(originX, originY);
  267. this.u = u;
  268. }
  269. drawGhost(ctx) {
  270. ctx.setLineDash([3]);
  271. ctx.beginPath();
  272. ctx.rect(this.origin.x + 0.5, this.origin.y + 0.5, this.dragOffset.x, this.dragOffset.y);
  273. ctx.stroke();
  274. ctx.setLineDash([]);
  275. }
  276. done() {
  277. let outer = this.dragOffset.x < 0 || this.dragOffset.y < 0;
  278. let bbox = {
  279. left: Math.min(this.origin.x, this.origin.x + this.dragOffset.x),
  280. top: Math.min(this.origin.y, this.origin.y + this.dragOffset.y),
  281. width:Math.abs(this.dragOffset.x),
  282. height:Math.abs(this.dragOffset.y)
  283. };
  284.  
  285. if(outer) {
  286. this.u.selectedItems = this.u.findItemsOverlapping(bbox);
  287. } else {
  288. this.u.selectedItems = this.u.findItemsInside(bbox);
  289. }
  290.  
  291. }
  292. };
  293.  
  294. class MoveAction extends DragAction {
  295. constructor(originX, originY, item) {
  296. super(originX, originY);
  297. this.item = item;
  298. document.body.style.cursor = 'move';
  299. }
  300. drawGhost(ctx) {
  301. let bbox = {
  302. left : this.item.getX() + this.dragOffset.x,
  303. top : this.item.getY() + this.dragOffset.y,
  304. width : this.item.getWidth(),
  305. height : this.item.getHeight()
  306. };
  307. this.item.drawGhost(ctx, bbox);
  308. }
  309. done() {
  310. this.item.moveBy(this.dragOffset.x, this.dragOffset.y);
  311. document.body.style.cursor = 'auto';
  312. }
  313. };
  314.  
  315. class ResizeAction extends DragAction {
  316. constructor(originX, originY, item, direction) {
  317. super(originX, originY);
  318. this.item = item;
  319. this.bboxFn = {
  320. 'topLeft' : (dx, dy) => { return {
  321. left: item.getX() + dx,
  322. top: item.getY() + dy,
  323. width: item.getWidth() - dx,
  324. height:item.getHeight() - dy
  325. }},
  326. 'topRight' : (dx, dy) => { return {
  327. left: item.getX(),
  328. top: item.getY() + dy,
  329. width: item.getWidth() + dx,
  330. height:item.getHeight() - dy
  331. }},
  332. 'bottomLeft' : (dx, dy) => { return {
  333. left: item.getX() + dx,
  334. top: item.getY(),
  335. width: item.getWidth() - dx,
  336. height: item.getHeight() + dy
  337. }},
  338. 'bottomRight' : (dx, dy) => { return {
  339. left: item.getX(),
  340. top: item.getY(),
  341. width: item.getWidth() + dx,
  342. height: item.getHeight() + dy
  343. }}
  344. }[direction];
  345. document.body.style.cursor = {
  346. 'topLeft': 'se-resize',
  347. 'topRight': 'sw-resize',
  348. 'bottomLeft': 'ne-resize',
  349. 'bottomRight': 'nw-resize',
  350. }[direction];
  351. }
  352.  
  353. drawGhost(ctx) {
  354. let bbox = this.bboxFn(this.dragOffset.x, this.dragOffset.y);
  355. this.item.drawGhost(ctx, bbox);
  356. }
  357. done() {
  358. let bbox = this.bboxFn(this.dragOffset.x, this.dragOffset.y);
  359. this.item.resize(bbox.width, bbox.height);
  360. this.item.moveTo(bbox.left, bbox.top);
  361. document.body.style.cursor = 'auto';
  362. }
  363. };
  364.  
  365. const HANDLE_SIZE = 8;
  366.  
  367. class ItemsView {
  368.  
  369. constructor(ctx) {
  370. this.ctx = ctx;
  371. this.drawables = [];
  372. const that = this;
  373.  
  374. //install mouse handlers
  375. ctx.canvas.onmousemove = (e) => {
  376. that.dispatch('mousemove', that.fixEvent(e));
  377. };
  378. ctx.canvas.onmousedown = (e) => {
  379. that.dispatch('mousedown', that.fixEvent(e));
  380. };
  381. ctx.canvas.onmouseup = (e) => {
  382. that.dispatch('mouseup', that.fixEvent(e));
  383. };
  384.  
  385. // selected items
  386. this.selectedItems = [];
  387. this.dragState = 'idle';
  388. this.dragAction = null;
  389. }
  390.  
  391. findItemsOverlapping(bbox) {
  392. let box = new Box(bbox);
  393. return this.drawables.filter((item) => {
  394. return box.containsPoint(item.getX(), item.getY())
  395. || box.containsPoint(item.getX() + item.getWidth(), item.getY())
  396. || box.containsPoint(item.getX(), item.getY() + item.getHeight())
  397. || box.containsPoint(item.getX() + item.getWidth(), item.getY() + item.getHeight());
  398. });
  399. }
  400.  
  401. findItemsInside(bbox) {
  402. let box = new Box(bbox);
  403. return this.drawables.filter((item) => {
  404. return box.containsPoint(item.getX(), item.getY())
  405. && box.containsPoint(item.getX() + item.getWidth(), item.getY() + item.getHeight());
  406. });
  407. }
  408.  
  409. /* recompute correct mouse coordinates */
  410. fixEvent(e) {
  411. let canvas = this.ctx.canvas;
  412. let bounds = canvas.getBoundingClientRect();
  413. let x = canvas.width * ((e.pageX - bounds.left - window.scrollX) / bounds.width);
  414. let y = canvas.height * ((e.pageY - bounds.top - window.scrollY) / bounds.height);
  415. return {
  416. x:Math.round(x),
  417. y:Math.round(y),
  418. button:e.button,
  419. ctrlKey:e.ctrlKey,
  420. altKey:e.altKey
  421. };
  422. }
  423.  
  424. updateSelection(x, y, ctrlKey) {
  425. //update selected items
  426. let item = this.itemAt(x,y);
  427. if(item) {
  428. var index = this.selectedItems.indexOf(item);
  429. // the item was already selected
  430. if(index < 0) {
  431. if(ctrlKey) {
  432. this.selectedItems.push(item);
  433. } else {
  434. this.selectedItems = [item];
  435. }
  436. }
  437. } else {
  438. //click in the void -> deselect everything
  439. this.selectedItems = [];
  440. }
  441. return item;
  442. }
  443.  
  444. dispatch(evt, payload) {
  445.  
  446. if(evt === 'mousedown') {
  447. // if we clicked on a handle
  448. this.dragAction = this.findHandleAction(payload.x, payload.y);
  449. if(this.dragAction == null) {
  450. let item = this.updateSelection(payload.x, payload.y, payload.ctrlKey);
  451. if(item == null) {
  452. // and start a selection action
  453. this.dragAction = new SelectAction(payload.x, payload.y, this);
  454. } else {
  455. // clicked on a selected item : start a move action
  456. this.dragAction = new MoveAction(payload.x, payload.y, new Group(this.selectedItems));
  457. }
  458. }
  459. }
  460.  
  461. else if(evt === 'mousemove') {
  462. if(this.dragAction){
  463. this.dragAction.update(payload.x, payload.y);
  464. }
  465. }
  466.  
  467. else if(evt === 'mouseup') {
  468. // end of dragging event
  469. if(this.dragAction != null) {
  470. this.dragAction.done();
  471. this.dragAction = null;
  472. }
  473. }
  474. requestAnimationFrame(this.draw.bind(this));
  475. }
  476.  
  477. drawHandle(x, y) {
  478. const H = HANDLE_SIZE;
  479. const H2 = HANDLE_SIZE/2;
  480. this.ctx.fillStyle = 'white';
  481. this.ctx.strokeStyle = 'gray';
  482. this.ctx.fillRect(x-H2 + 0.5, y-H2 + 0.5, H, H);
  483. this.ctx.beginPath();
  484. this.ctx.rect(x-H2 + 0.5, y-H2 + 0.5, H, H);
  485. this.ctx.stroke();
  486. }
  487.  
  488. draw() {
  489.  
  490. this.ctx.save();
  491.  
  492. //clear canvas
  493. this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
  494. this.ctx.fillStyle = 'white';
  495. this.ctx.strokeStyle='black';
  496.  
  497. // draw items
  498. for(let item of this.drawables) {
  499. this.ctx.save();
  500. item.draw(this.ctx);
  501. this.ctx.restore();
  502. }
  503.  
  504. // highlight selected items
  505. this.selectedItems
  506. .map((item) => item.bbox())
  507. .forEach((bbox) => {
  508. this.ctx.setLineDash([3]);
  509. this.ctx.beginPath();
  510. this.ctx.rect(bbox.left + 0.5, bbox.top + 0.5, bbox.width, bbox.height);
  511. this.ctx.stroke();
  512. this.ctx.setLineDash([]);
  513. this.drawHandle(bbox.left, bbox.top);
  514. this.drawHandle(bbox.left + bbox.width, bbox.top);
  515. this.drawHandle(bbox.left, bbox.top + bbox.height);
  516. this.drawHandle(bbox.left + bbox.width, bbox.top + bbox.height);
  517. });
  518.  
  519. // draw the ghost of the items being resized / moved
  520. if(this.dragAction && this.dragAction.hasMoved()) {
  521. this.ctx.globalAlpha = 0.3;
  522. this.dragAction.drawGhost(this.ctx);
  523. this.ctx.globalAlpha = 1.0;
  524. }
  525.  
  526. this.ctx.restore();
  527. }
  528.  
  529. itemAt(x,y) {
  530. let candidates = this.itemsAt(x,y);
  531. if(candidates.length == 0) {
  532. return null;
  533. }
  534. candidates.sort((a,b) => b.zIndex() - a.zIndex());
  535. return candidates[0];
  536. }
  537.  
  538. itemsAt(x, y) {
  539. return this.drawables.filter((item) => item.containsPoint(x,y));
  540. }
  541.  
  542. findHandleAction(x,y) {
  543. const H2 = HANDLE_SIZE/2;
  544. for(let item of this.selectedItems) {
  545. const scanned = {
  546. 'topLeft' : {x:item.getX(), y:item.getY()},
  547. 'topRight' : {x:item.getX() + item.getWidth(), y:item.getY()},
  548. 'bottomLeft' : {x:item.getX(), y:item.getY() + item.getHeight()},
  549. 'bottomRight' : {x:item.getX() + item.getWidth(), y:item.getY() + item.getHeight()},
  550. };
  551. for(const key of Object.keys(scanned)) {
  552. let pos = scanned[key];
  553. let box = new Box({
  554. left : pos.x - H2,
  555. top : pos.y - H2,
  556. width : HANDLE_SIZE,
  557. height : HANDLE_SIZE
  558. });
  559. if(box.containsPoint(x,y)) {
  560. let group = new Group(this.selectedItems);
  561. return new ResizeAction(x, y, group, key);
  562. }
  563. }
  564. }
  565. return null;
  566. }
  567.  
  568. };
  569.  
  570. window.bodyOnLoad = () => {
  571.  
  572. let canvas = document.getElementById('canvas');
  573.  
  574. // make 1:1 pixel ratio
  575. let scale = window.devicePixelRatio;
  576. canvas.width = canvas.clientWidth * scale;
  577. canvas.height = canvas.clientHeight * scale;
  578.  
  579. let ctx = canvas.getContext('2d');
  580. ctx.scale(scale, scale);
  581.  
  582. let v = new ItemsView(ctx);
  583. v.drawables = [
  584. new Rect(100,100,100,100),
  585. new Rect(200,200,100, 100),
  586. new Circle(300,300,100),
  587. new Oval(400,400, 20,30)
  588. ];
  589. v.draw();
  590. };
  591.  
  592. </script>
  593.  
  594. </head>
  595.  
  596.  
  597. <body onload="bodyOnLoad()">
  598. <div id="container">
  599. <canvas id="canvas" />
  600. </div>
  601. </body>
  602.  
  603. </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement