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.

hand.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. /**
  2. * WHITEBOPHIR
  3. *********************************************************
  4. * @licstart The following is the entire license notice for the
  5. * JavaScript code in this page.
  6. *
  7. * Copyright (C) 2013 Ophir LOJKINE
  8. *
  9. *
  10. * The JavaScript code in this page is free software: you can
  11. * redistribute it and/or modify it under the terms of the GNU
  12. * General Public License (GNU GPL) as published by the Free Software
  13. * Foundation, either version 3 of the License, or (at your option)
  14. * any later version. The code is distributed WITHOUT ANY WARRANTY;
  15. * without even the implied warranty of MERCHANTABILITY or FITNESS
  16. * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
  17. *
  18. * As additional permission under GNU GPL version 3 section 7, you
  19. * may distribute non-source (e.g., minimized or compacted) forms of
  20. * that code without the copy of the GNU GPL normally required by
  21. * section 4, provided you include this license notice and a URL
  22. * through which recipients can access the Corresponding Source.
  23. *
  24. * @licend
  25. */
  26. (function hand() { //Code isolation
  27. var selectorStates = {
  28. pointing: 0,
  29. selecting: 1,
  30. transform: 2
  31. }
  32. var selected = null;
  33. var selected_els = [];
  34. var selectionRect = createSelectorRect();
  35. var selectionRectTransform;
  36. var currentTransform = null;
  37. var transform_elements = [];
  38. var selectorState = selectorStates.pointing;
  39. var last_sent = 0;
  40. var deleteButton = createButton("delete", "delete", 22, 22,
  41. function(me, bbox, s) {
  42. me.width.baseVal.value = me.origWidth / s;
  43. me.height.baseVal.value = me.origHeight / s;
  44. me.x.baseVal.value = bbox.r[0];
  45. me.y.baseVal.value = bbox.r[1] - (me.origHeight + 3) / s;
  46. me.style.display = "";
  47. },
  48. deleteSelection);
  49. var duplicateButton = createButton("duplicate", "duplicate", 22, 22,
  50. function(me, bbox, s) {
  51. me.width.baseVal.value = me.origWidth / s;
  52. me.height.baseVal.value = me.origHeight / s;
  53. me.x.baseVal.value = bbox.r[0] + (me.origWidth + 2) / s;
  54. me.y.baseVal.value = bbox.r[1] - (me.origHeight + 3) / s;
  55. me.style.display = "";
  56. },
  57. duplicateSelection);
  58. var scaleHandle = createButton("scaleHandle", "handle", 14, 14,
  59. function(me, bbox, s) {
  60. me.width.baseVal.value = me.origWidth / s;
  61. me.height.baseVal.value = me.origHeight / s;
  62. me.x.baseVal.value = bbox.r[0] + bbox.a[0] - me.origWidth/(2*s);
  63. me.y.baseVal.value = bbox.r[1] + bbox.b[1] - me.origHeight/(2*s);
  64. me.style.display = "";
  65. },
  66. startScalingTransform);
  67. var selectionButtons = [deleteButton, duplicateButton, scaleHandle];
  68. var getScale = Tools.getScale;
  69. function getParentMathematics(el) {
  70. var target;
  71. var a = el;
  72. var els = [];
  73. while (a) {
  74. els.unshift(a);
  75. a = a.parentElement;
  76. }
  77. var parentMathematics = els.find(function(el) {
  78. return el.getAttribute("class") === "MathElement";
  79. });
  80. if ((parentMathematics) && parentMathematics.tagName === "svg") {
  81. target = parentMathematics;
  82. }
  83. return target || el;
  84. }
  85. function deleteSelection() {
  86. var msgs = selected_els.map(function(el) {
  87. return ({
  88. "type": "delete",
  89. "id": el.id
  90. });
  91. });
  92. var data = {
  93. _children: msgs
  94. }
  95. Tools.drawAndSend(data);
  96. selected_els = [];
  97. hideSelectionUI();
  98. }
  99. function duplicateSelection() {
  100. if (!(selectorState == selectorStates.pointing)
  101. || (selected_els.length == 0)) return;
  102. var msgs = [];
  103. var newids = [];
  104. for (var i=0; i<selected_els.length; i++) {
  105. var id = selected_els[i].id;
  106. msgs[i] = {
  107. type: "copy",
  108. id: id,
  109. newid: Tools.generateUID(id[0])
  110. };
  111. newids[i] = id;
  112. }
  113. Tools.drawAndSend({_children: msgs});
  114. selected_els = newids.map(function(id) {
  115. return Tools.svg.getElementById(id);
  116. });
  117. }
  118. function createSelectorRect() {
  119. var shape = Tools.createSVGElement("rect");
  120. shape.id = "selectionRect";
  121. shape.x.baseVal.value = 0;
  122. shape.y.baseVal.value = 0;
  123. shape.width.baseVal.value = 0;
  124. shape.height.baseVal.value = 0;
  125. shape.setAttribute("stroke", "black");
  126. shape.setAttribute("stroke-width", 1);
  127. shape.setAttribute("vector-effect", "non-scaling-stroke");
  128. shape.setAttribute("fill", "none");
  129. shape.setAttribute("stroke-dasharray", "5 5");
  130. shape.setAttribute("opacity", 1);
  131. Tools.svg.appendChild(shape);
  132. return shape;
  133. }
  134. function createButton(name, icon , width, height, drawCallback, clickCallback) {
  135. var shape = Tools.createSVGElement("image", {
  136. id: name + "Icon",
  137. href: "tools/hand/" + icon + ".svg",
  138. width: width,
  139. height: height,
  140. });
  141. shape.style.display = "none";
  142. shape.origWidth = width;
  143. shape.origHeight = height;
  144. shape.drawCallback = drawCallback;
  145. shape.clickCallback = clickCallback;
  146. Tools.svg.appendChild(shape);
  147. return shape;
  148. }
  149. function showSelectionButtons() {
  150. var scale = getScale();
  151. var selectionBBox = selectionRect.transformedBBox(scale);
  152. for (var i = 0; i < selectionButtons.length; i++) {
  153. selectionButtons[i].drawCallback(selectionButtons[i],
  154. selectionBBox,
  155. scale);
  156. }
  157. }
  158. function hideSelectionButtons() {
  159. for (var i = 0; i < selectionButtons.length; i++) {
  160. selectionButtons[i].style.display = "none";
  161. }
  162. }
  163. function hideSelectionUI() {
  164. hideSelectionButtons();
  165. selectionRect.style.display = "none";
  166. }
  167. function startMovingElements(x, y, evt) {
  168. evt.preventDefault();
  169. selectorState = selectorStates.transform;
  170. currentTransform = moveSelection;
  171. selected = { x: x, y: y };
  172. // Some of the selected elements could have been deleted
  173. selected_els = selected_els.filter(function(el) {
  174. return Tools.svg.getElementById(el.id) !== null;
  175. });
  176. transform_elements = selected_els.map(function(el) {
  177. var tmatrix = get_transform_matrix(el);
  178. return {
  179. a: tmatrix.a, b: tmatrix.b, c: tmatrix.c,
  180. d: tmatrix.d, e: tmatrix.e, f: tmatrix.f
  181. };
  182. });
  183. var tmatrix = get_transform_matrix(selectionRect);
  184. selectionRectTransform = { x: tmatrix.e, y: tmatrix.f };
  185. }
  186. function startScalingTransform(x, y, evt) {
  187. evt.preventDefault();
  188. hideSelectionButtons();
  189. selectorState = selectorStates.transform;
  190. var scale = getScale();
  191. var bbox = selectionRect.transformedBBox(scale);
  192. selected = {
  193. x: bbox.r[0],
  194. y: bbox.r[1],
  195. w: bbox.a[0],
  196. h: bbox.b[1],
  197. s: scale
  198. };
  199. transform_elements = selected_els.map(function(el) {
  200. var tmatrix = get_transform_matrix(el);
  201. return {
  202. a: tmatrix.a, b: tmatrix.b, c: tmatrix.c,
  203. d: tmatrix.d, e: tmatrix.e, f: tmatrix.f
  204. };
  205. });
  206. var tmatrix = get_transform_matrix(selectionRect);
  207. selectionRectTransform = {
  208. a: tmatrix.a, d: tmatrix.d,
  209. e: tmatrix.e, f: tmatrix.f
  210. };
  211. currentTransform = scaleSelection;
  212. }
  213. function startSelector(x, y, evt) {
  214. evt.preventDefault();
  215. selected = { x: x, y: y };
  216. selected_els = [];
  217. selectorState = selectorStates.selecting;
  218. selectionRect.x.baseVal.value = x;
  219. selectionRect.y.baseVal.value = y;
  220. selectionRect.width.baseVal.value = 0;
  221. selectionRect.height.baseVal.value = 0;
  222. selectionRect.style.display = "";
  223. tmatrix = get_transform_matrix(selectionRect);
  224. tmatrix.e = 0;
  225. tmatrix.f = 0;
  226. }
  227. function calculateSelection() {
  228. var scale = getScale();
  229. var selectionTBBox = selectionRect.transformedBBox(scale);
  230. var elements = Tools.drawingArea.children;
  231. var selected = [];
  232. for (var i=0; i < elements.length; i++) {
  233. if (transformedBBoxIntersects(selectionTBBox, elements[i].transformedBBox(scale)))
  234. selected.push(Tools.drawingArea.children[i]);
  235. }
  236. return selected;
  237. }
  238. function moveSelection(x, y) {
  239. var dx = x - selected.x;
  240. var dy = y - selected.y;
  241. var msgs = selected_els.map(function(el, i) {
  242. var oldTransform = transform_elements[i];
  243. return {
  244. type: "update",
  245. id: el.id,
  246. transform: {
  247. a: oldTransform.a,
  248. b: oldTransform.b,
  249. c: oldTransform.c,
  250. d: oldTransform.d,
  251. e: dx + oldTransform.e,
  252. f: dy + oldTransform.f
  253. }
  254. };
  255. })
  256. var msg = {
  257. _children: msgs
  258. };
  259. var tmatrix = get_transform_matrix(selectionRect);
  260. tmatrix.e = dx + selectionRectTransform.x;
  261. tmatrix.f = dy + selectionRectTransform.y;
  262. var now = performance.now();
  263. if (now - last_sent > 70) {
  264. last_sent = now;
  265. Tools.drawAndSend(msg);
  266. } else {
  267. draw(msg);
  268. }
  269. }
  270. function scaleSelection(x, y) {
  271. var rx = (x - selected.x)/(selected.w);
  272. var ry = (y - selected.y)/(selected.h);
  273. var scale = getScale();
  274. var msgs = selected_els.map(function(el, i) {
  275. var oldTransform = transform_elements[i];
  276. var x = el.transformedBBox(scale).r[0];
  277. var y = el.transformedBBox(scale).r[1];
  278. var a = oldTransform.a * rx;
  279. var d = oldTransform.d * ry;
  280. var e = selected.x * (1 - rx) - x * a +
  281. (x * oldTransform.a + oldTransform.e) * rx
  282. var f = selected.y * (1 - ry) - y * d +
  283. (y * oldTransform.d + oldTransform.f) * ry
  284. return {
  285. type: "update",
  286. id: el.id,
  287. transform: {
  288. a: a,
  289. b: oldTransform.b,
  290. c: oldTransform.c,
  291. d: d,
  292. e: e,
  293. f: f
  294. }
  295. };
  296. })
  297. var msg = {
  298. _children: msgs
  299. };
  300. var tmatrix = get_transform_matrix(selectionRect);
  301. tmatrix.a = rx;
  302. tmatrix.d = ry;
  303. tmatrix.e = selectionRectTransform.e +
  304. selectionRect.x.baseVal.value * (selectionRectTransform.a - rx)
  305. tmatrix.f = selectionRectTransform.f +
  306. selectionRect.y.baseVal.value * (selectionRectTransform.d - ry)
  307. var now = performance.now();
  308. if (now - last_sent > 70) {
  309. last_sent = now;
  310. Tools.drawAndSend(msg);
  311. } else {
  312. draw(msg);
  313. }
  314. }
  315. function updateRect(x, y, rect) {
  316. rect.x.baseVal.value = Math.min(x, selected.x);
  317. rect.y.baseVal.value = Math.min(y, selected.y);
  318. rect.width.baseVal.value = Math.abs(x - selected.x);
  319. rect.height.baseVal.value = Math.abs(y - selected.y);
  320. }
  321. function resetSelectionRect() {
  322. var bbox = selectionRect.transformedBBox(getScale());
  323. var tmatrix = get_transform_matrix(selectionRect);
  324. selectionRect.x.baseVal.value = bbox.r[0];
  325. selectionRect.y.baseVal.value = bbox.r[1];
  326. selectionRect.width.baseVal.value = bbox.a[0];
  327. selectionRect.height.baseVal.value = bbox.b[1];
  328. tmatrix.a = 1; tmatrix.b = 0; tmatrix.c = 0;
  329. tmatrix.d = 1; tmatrix.e = 0; tmatrix.f = 0;
  330. }
  331. function get_transform_matrix(elem) {
  332. // Returns the first translate or transform matrix or makes one
  333. var transform = null;
  334. for (var i = 0; i < elem.transform.baseVal.numberOfItems; ++i) {
  335. var baseVal = elem.transform.baseVal[i];
  336. // quick tests showed that even if one changes only the fields e and f or uses createSVGTransformFromMatrix
  337. // the brower may add a SVG_TRANSFORM_MATRIX instead of a SVG_TRANSFORM_TRANSLATE
  338. if (baseVal.type === SVGTransform.SVG_TRANSFORM_MATRIX) {
  339. transform = baseVal;
  340. break;
  341. }
  342. }
  343. if (transform == null) {
  344. transform = elem.transform.baseVal.createSVGTransformFromMatrix(Tools.svg.createSVGMatrix());
  345. elem.transform.baseVal.appendItem(transform);
  346. }
  347. return transform.matrix;
  348. }
  349. function draw(data) {
  350. if (data._children) {
  351. batchCall(draw, data._children);
  352. }
  353. else {
  354. switch (data.type) {
  355. case "update":
  356. var elem = Tools.svg.getElementById(data.id);
  357. if (!elem) throw new Error("Mover: Tried to move an element that does not exist.");
  358. var tmatrix = get_transform_matrix(elem);
  359. for (i in data.transform) {
  360. tmatrix[i] = data.transform[i]
  361. }
  362. break;
  363. case "copy":
  364. var newElement = Tools.svg.getElementById(data.id).cloneNode(true);
  365. newElement.id = data.newid;
  366. Tools.drawingArea.appendChild(newElement);
  367. break;
  368. case "delete":
  369. data.tool = "Eraser";
  370. messageForTool(data);
  371. break;
  372. default:
  373. throw new Error("Mover: 'move' instruction with unknown type. ", data);
  374. }
  375. }
  376. }
  377. function clickSelector(x, y, evt) {
  378. var scale = getScale();
  379. selectionRect = selectionRect || createSelectorRect();
  380. var button = null;
  381. for (var i=0; i<selectionButtons.length; i++) {
  382. if (selectionButtons[i].contains(evt.target)) {
  383. button = selectionButtons[i];
  384. }
  385. }
  386. if (button) {
  387. button.clickCallback(x, y, evt);
  388. } else if (pointInTransformedBBox([x, y], selectionRect.transformedBBox(scale))) {
  389. hideSelectionButtons();
  390. startMovingElements(x, y, evt);
  391. } else if (Tools.drawingArea.contains(evt.target)) {
  392. hideSelectionUI();
  393. selected_els = [getParentMathematics(evt.target)];
  394. startMovingElements(x, y, evt);
  395. } else {
  396. hideSelectionButtons();
  397. startSelector(x, y, evt);
  398. }
  399. }
  400. function releaseSelector(x, y, evt) {
  401. if (selectorState == selectorStates.selecting) {
  402. selected_els = calculateSelection();
  403. if (selected_els.length == 0) {
  404. hideSelectionUI();
  405. }
  406. } else if (selectorState == selectorStates.transform)
  407. resetSelectionRect();
  408. if (selected_els.length != 0) showSelectionButtons();
  409. transform_elements = [];
  410. selectorState = selectorStates.pointing;
  411. }
  412. function moveSelector(x, y, evt) {
  413. if (selectorState == selectorStates.selecting) {
  414. updateRect(x, y, selectionRect);
  415. } else if (selectorState == selectorStates.transform && currentTransform) {
  416. currentTransform(x, y);
  417. }
  418. }
  419. function startHand(x, y, evt, isTouchEvent) {
  420. if (!isTouchEvent) {
  421. selected = {
  422. x: document.documentElement.scrollLeft + evt.clientX,
  423. y: document.documentElement.scrollTop + evt.clientY,
  424. }
  425. }
  426. }
  427. function moveHand(x, y, evt, isTouchEvent) {
  428. if (selected && !isTouchEvent) { //Let the browser handle touch to scroll
  429. window.scrollTo(selected.x - evt.clientX, selected.y - evt.clientY);
  430. }
  431. }
  432. function press(x, y, evt, isTouchEvent) {
  433. if (!handTool.secondary.active) startHand(x, y, evt, isTouchEvent);
  434. else clickSelector(x, y, evt, isTouchEvent);
  435. }
  436. function move(x, y, evt, isTouchEvent) {
  437. if (!handTool.secondary.active) moveHand(x, y, evt, isTouchEvent);
  438. else moveSelector(x, y, evt, isTouchEvent);
  439. }
  440. function release(x, y, evt, isTouchEvent) {
  441. move(x, y, evt, isTouchEvent);
  442. if (handTool.secondary.active) releaseSelector(x, y, evt, isTouchEvent);
  443. selected = null;
  444. }
  445. function deleteShortcut(e) {
  446. if (e.key == "Delete" &&
  447. !e.target.matches("input[type=text], textarea"))
  448. deleteSelection();
  449. }
  450. function duplicateShortcut(e) {
  451. if (e.key == "d" &&
  452. !e.target.matches("input[type=text], textarea"))
  453. duplicateSelection();
  454. }
  455. function switchTool() {
  456. selected = null;
  457. if (handTool.secondary.active) {
  458. window.addEventListener("keydown", deleteShortcut);
  459. window.addEventListener("keydown", duplicateShortcut);
  460. }
  461. else {
  462. window.removeEventListener("keydown", deleteShortcut);
  463. window.removeEventListener("keydown", duplicateShortcut);
  464. }
  465. }
  466. var handTool = { //The new tool
  467. "name": "Hand",
  468. "shortcut": "h",
  469. "listeners": {
  470. "press": press,
  471. "move": move,
  472. "release": release,
  473. },
  474. "secondary": {
  475. "name": "Selector",
  476. "icon": "tools/hand/selector.svg",
  477. "active": false,
  478. "switch": switchTool,
  479. },
  480. "draw": draw,
  481. "icon": "tools/hand/hand.svg",
  482. "mouseCursor": "move",
  483. "showMarker": true,
  484. };
  485. Tools.add(handTool);
  486. Tools.change("Hand"); // Use the hand tool by default
  487. })(); //End of code isolation