mxFreehand.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. function mxFreehand(graph)
  2. {
  3. // Graph must have a container
  4. var svgElement = (graph.view != null && graph.view.canvas != null) ? graph.view.canvas.ownerSVGElement : null;
  5. if (graph.container == null || svgElement == null)
  6. {
  7. return;
  8. }
  9. // Stops drawing on escape
  10. graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function()
  11. {
  12. this.stopDrawing();
  13. }));
  14. //Code inspired by https://stackoverflow.com/questions/40324313/svg-smooth-freehand-drawing
  15. var bufferSize = mxFreehand.prototype.MILD_SMOOTHING;
  16. var path = null;
  17. var partPathes = [];
  18. var strPath;
  19. var drawPoints = [];
  20. var lastPart;
  21. var closedPath = false;
  22. var autoClose = true;
  23. var autoInsert = true;
  24. var autoScroll = true;
  25. var openFill = true;
  26. var buffer = []; // Contains the last positions of the mouse cursor
  27. var enabled = false;
  28. var stopClickEnabled = false;
  29. var selectInserted = false;
  30. var currentStrokeColor = 'default';
  31. var perfectFreehandOptions = {
  32. size: 5,
  33. thinning: 0.5,
  34. smoothing: 0.5,
  35. streamline: 0.5,
  36. // easing: (t) => t,
  37. start: {
  38. taper: 0,
  39. // easing: (t) => t,
  40. cap: true
  41. },
  42. end: {
  43. taper: 0,
  44. // easing: (t) => t,
  45. cap: true
  46. }
  47. };
  48. var perfectFreehandMode = true;
  49. this.setClosedPath = function(isClosed)//TODO add closed settings
  50. {
  51. closedPath = isClosed;
  52. };
  53. this.setAutoClose = function(isAutoClose)//TODO add auto closed settings
  54. {
  55. autoClose = isAutoClose;
  56. };
  57. this.setAutoInsert = function(value)
  58. {
  59. autoInsert = value;
  60. };
  61. this.setAutoScroll = function(value)
  62. {
  63. autoScroll = value;
  64. };
  65. this.setOpenFill = function(value)
  66. {
  67. openFill = value;
  68. };
  69. this.setStopClickEnabled = function(enabled)
  70. {
  71. stopClickEnabled = enabled;
  72. };
  73. this.setSelectInserted = function(value)
  74. {
  75. selectInserted = value;
  76. };
  77. this.setSmoothing = function(smoothing)
  78. {
  79. bufferSize = smoothing;
  80. };
  81. this.getSmoothing = function()
  82. {
  83. return bufferSize;
  84. };
  85. this.setPerfectFreehandMode = function(value)
  86. {
  87. perfectFreehandMode = value;
  88. };
  89. this.isPerfectFreehandMode = function()
  90. {
  91. return perfectFreehandMode;
  92. };
  93. this.setBrushSize = function(value)
  94. {
  95. perfectFreehandOptions.size = value;
  96. };
  97. this.getBrushSize = function()
  98. {
  99. return perfectFreehandOptions.size;
  100. };
  101. var setEnabled = function(isEnabled)
  102. {
  103. enabled = isEnabled;
  104. graph.getRubberband().setEnabled(!isEnabled);
  105. graph.graphHandler.setSelectEnabled(!isEnabled);
  106. graph.graphHandler.setMoveEnabled(!isEnabled);
  107. graph.container.style.cursor = (isEnabled) ? 'crosshair' : '';
  108. graph.fireEvent(new mxEventObject('freehandStateChanged'));
  109. };
  110. this.startDrawing = function()
  111. {
  112. setEnabled(true);
  113. }
  114. this.isDrawing = function()
  115. {
  116. return enabled;
  117. };
  118. var endPath = mxUtils.bind(this, function(e)
  119. {
  120. if (path)
  121. {
  122. var lastLength = lastPart.length;
  123. // Click stops drawing
  124. var doStop = stopClickEnabled && drawPoints.length > 0 &&
  125. lastPart != null && lastPart.length < 2;
  126. if (!doStop)
  127. {
  128. drawPoints.push.apply(drawPoints, lastPart);
  129. }
  130. lastPart = [];
  131. drawPoints.push(null);
  132. partPathes.push(path);
  133. path = null;
  134. if (doStop || autoInsert)
  135. {
  136. this.stopDrawing();
  137. }
  138. if (autoInsert && (!doStop || lastLength >= 2))
  139. {
  140. this.startDrawing();
  141. }
  142. mxEvent.consume(e);
  143. }
  144. });
  145. // Used to retrieve default styles
  146. var edge = new mxCell();
  147. edge.edge = true;
  148. this.getStrokeColor = function(allowDefault)
  149. {
  150. var strokeColor = (currentStrokeColor != null) ? currentStrokeColor :
  151. mxUtils.getValue(graph.currentVertexStyle, mxConstants.STYLE_STROKECOLOR,
  152. mxUtils.getValue(graph.getCurrentCellStyle(edge),
  153. mxConstants.STYLE_STROKECOLOR, '#000'))
  154. if (strokeColor == 'default' && !allowDefault)
  155. {
  156. strokeColor = graph.shapeForegroundColor;
  157. }
  158. return strokeColor;
  159. };
  160. this.setStrokeColor = function(value)
  161. {
  162. currentStrokeColor = value;
  163. };
  164. this.createStyle = function(stencil)
  165. {
  166. var style = ';fillColor=none;';
  167. if (perfectFreehandMode)
  168. {
  169. style = ';lineShape=1;';
  170. }
  171. style += 'strokeColor=' + this.getStrokeColor(true) + ';';
  172. return mxConstants.STYLE_SHAPE + '=' + stencil + style;
  173. };
  174. this.stopDrawing = function()
  175. {
  176. if (partPathes.length > 0)
  177. {
  178. if (perfectFreehandMode)
  179. {
  180. var tmpPoints = [];
  181. for (var i = 0; i < drawPoints.length; i++)
  182. {
  183. if (drawPoints[i] != null)
  184. {
  185. tmpPoints.push([drawPoints[i].x, drawPoints[i].y]);
  186. }
  187. }
  188. var output = PerfectFreehand.getStroke(tmpPoints, perfectFreehandOptions);
  189. drawPoints = [];
  190. for (var i = 0; i < output.length; i++)
  191. {
  192. drawPoints.push({x: output[i][0], y: output[i][1]});
  193. }
  194. drawPoints.push(null);
  195. }
  196. var maxX = drawPoints[0].x, minX = drawPoints[0].x, maxY = drawPoints[0].y, minY = drawPoints[0].y;
  197. for (var i = 1; i < drawPoints.length; i++)
  198. {
  199. if (drawPoints[i] == null) continue;
  200. maxX = Math.max(maxX, drawPoints[i].x);
  201. minX = Math.min(minX, drawPoints[i].x);
  202. maxY = Math.max(maxY, drawPoints[i].y);
  203. minY = Math.min(minY, drawPoints[i].y);
  204. }
  205. var w = maxX - minX, h = maxY - minY;
  206. if (w > 0 && h > 0)
  207. {
  208. var xScale = 100 / w;
  209. var yScale = 100 / h;
  210. drawPoints.map(function(p)
  211. {
  212. if (p == null) return p;
  213. p.x = (p.x - minX) * xScale;
  214. p.y = (p.y - minY) * yScale;
  215. return p;
  216. });
  217. //toFixed(2) to reduce size of output
  218. var drawShape = '<shape strokewidth="inherit"><foreground>';
  219. var start = 0;
  220. for (var i = 0; i < drawPoints.length; i++)
  221. {
  222. var p = drawPoints[i];
  223. if (p == null)
  224. {
  225. var tmpClosedPath = false;
  226. var startP = drawPoints[start], endP = drawPoints[i - 1];
  227. if (!closedPath && autoClose)
  228. {
  229. var xdiff = startP.x - endP.x, ydiff = startP.y - endP.y;
  230. var startEndDist = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
  231. tmpClosedPath = startEndDist <= graph.tolerance;
  232. }
  233. if (closedPath || tmpClosedPath)
  234. {
  235. drawShape += '<line x="'+ startP.x.toFixed(2) + '" y="' + startP.y.toFixed(2) + '"/>';
  236. }
  237. drawShape += '</path>' + ((openFill || closedPath || tmpClosedPath)? '<fillstroke/>' : '<stroke/>');
  238. start = i + 1;
  239. }
  240. else if (i == start)
  241. {
  242. drawShape += '<path><move x="'+ p.x.toFixed(2) + '" y="' + p.y.toFixed(2) + '"/>'
  243. }
  244. else
  245. {
  246. drawShape += '<line x="'+ p.x.toFixed(2) + '" y="' + p.y.toFixed(2) + '"/>';
  247. }
  248. }
  249. drawShape += '</foreground></shape>';
  250. if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
  251. {
  252. var style = this.createStyle('stencil(' + Graph.compress(drawShape) + ')');
  253. var s = graph.view.scale;
  254. var tr = graph.view.translate;
  255. var cell = new mxCell('', new mxGeometry(minX / s - tr.x, minY / s - tr.y, w / s, h / s), style);
  256. cell.vertex = 1;
  257. graph.model.beginUpdate();
  258. try
  259. {
  260. cell = graph.addCell(cell);
  261. graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [cell]));
  262. graph.fireEvent(new mxEventObject('freehandInserted', 'cell', cell));
  263. }
  264. finally
  265. {
  266. graph.model.endUpdate();
  267. }
  268. if (selectInserted)
  269. {
  270. graph.setSelectionCells([cell]);
  271. }
  272. }
  273. }
  274. for (var i = 0; i < partPathes.length; i++)
  275. {
  276. partPathes[i].parentNode.removeChild(partPathes[i]);
  277. }
  278. path = null;
  279. partPathes = [];
  280. drawPoints = [];
  281. }
  282. setEnabled(false);
  283. };
  284. // Stops all interactions if freehand is enabled
  285. graph.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt)
  286. {
  287. var evtName = evt.getProperty('eventName');
  288. var me = evt.getProperty('event');
  289. if (evtName == mxEvent.MOUSE_MOVE && enabled)
  290. {
  291. if (me.sourceState != null)
  292. {
  293. me.sourceState.setCursor('crosshair');
  294. }
  295. me.consume();
  296. }
  297. }));
  298. // Implements a listener for hover and click handling
  299. graph.addMouseListener(
  300. {
  301. mouseDown: mxUtils.bind(this, function(sender, me)
  302. {
  303. if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))
  304. {
  305. var e = me.getEvent();
  306. if (!enabled || mxEvent.isPopupTrigger(e) ||
  307. mxEvent.isMiddleMouseButton(e) ||
  308. mxEvent.isMultiTouchEvent(e))
  309. {
  310. return;
  311. }
  312. var strokeWidth = parseFloat(graph.currentVertexStyle[mxConstants.STYLE_STROKEWIDTH] || 1);
  313. strokeWidth = Math.max(1, strokeWidth * graph.view.scale);
  314. var strokeColor = this.getStrokeColor();
  315. path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  316. path.setAttribute('fill', perfectFreehandMode? strokeColor : 'none');
  317. path.setAttribute('pointer-events', 'none');
  318. path.setAttribute('stroke', strokeColor);
  319. path.setAttribute('stroke-width', strokeWidth);
  320. if (graph.currentVertexStyle[mxConstants.STYLE_DASHED] == '1')
  321. {
  322. var dashPattern = graph.currentVertexStyle[mxConstants.STYLE_DASH_PATTERN] || '3 3';
  323. dashPattern = dashPattern.split(' ').map(function(p)
  324. {
  325. return parseFloat(p) * strokeWidth;
  326. }).join(' ');
  327. path.setAttribute('stroke-dasharray', dashPattern);
  328. }
  329. buffer = [];
  330. var pt = getMousePosition(e);
  331. appendToBuffer(pt);
  332. strPath = 'M' + pt.x + ' ' + pt.y;
  333. drawPoints.push(pt);
  334. lastPart = [];
  335. path.setAttribute('d', perfectFreehandMode?
  336. PerfectFreehand.getSvgPathFromStroke([[pt.x, pt.y]], perfectFreehandOptions)
  337. : strPath);
  338. svgElement.appendChild(path);
  339. me.consume();
  340. }
  341. }),
  342. mouseMove: mxUtils.bind(this, function(sender, me)
  343. {
  344. if (path != null && graph.isEnabled() &&
  345. !graph.isCellLocked(graph.getDefaultParent()))
  346. {
  347. var e = me.getEvent();
  348. var pt = getMousePosition(e);
  349. appendToBuffer(pt);
  350. updateSvgPath();
  351. if (autoScroll)
  352. {
  353. var tr = graph.view.translate;
  354. graph.scrollRectToVisible(new mxRectangle(pt.x - tr.x, pt.y - tr.y).grow(20));
  355. }
  356. me.consume();
  357. }
  358. }),
  359. mouseUp: mxUtils.bind(this, function(sender, me)
  360. {
  361. if (path != null && graph.isEnabled() &&
  362. !graph.isCellLocked(graph.getDefaultParent()))
  363. {
  364. endPath(me.getEvent());
  365. me.consume();
  366. }
  367. })
  368. });
  369. var getMousePosition = function (e)
  370. {
  371. return mxUtils.convertPoint(graph.container, mxEvent.getClientX(e), mxEvent.getClientY(e));
  372. };
  373. var appendToBuffer = function (pt)
  374. {
  375. buffer.push(pt);
  376. while (buffer.length > bufferSize)
  377. {
  378. buffer.shift();
  379. }
  380. };
  381. // Calculate the average point, starting at offset in the buffer
  382. var getAveragePoint = function (offset)
  383. {
  384. var len = buffer.length;
  385. if (len % 2 === 1 || len >= bufferSize)
  386. {
  387. var totalX = 0;
  388. var totalY = 0;
  389. var pt, i;
  390. var count = 0;
  391. for (i = offset; i < len; i++)
  392. {
  393. count++;
  394. pt = buffer[i];
  395. totalX += pt.x;
  396. totalY += pt.y;
  397. }
  398. return {
  399. x: totalX / count,
  400. y: totalY / count
  401. }
  402. }
  403. return null;
  404. };
  405. var updateSvgPath = function ()
  406. {
  407. var pt = getAveragePoint(0);
  408. if (pt)
  409. {
  410. drawPoints.push(pt);
  411. if (perfectFreehandMode)
  412. {
  413. var tmpPoints = [];
  414. for (var i = 0; i < drawPoints.length; i++)
  415. {
  416. tmpPoints.push([drawPoints[i].x, drawPoints[i].y]);
  417. }
  418. lastPart = [];
  419. for (var offset = 2; offset < buffer.length; offset += 2)
  420. {
  421. pt = getAveragePoint(offset);
  422. tmpPoints.push([pt.x, pt.y]);
  423. lastPart.push(pt);
  424. }
  425. path.setAttribute('d', PerfectFreehand.getSvgPathFromStroke(tmpPoints, perfectFreehandOptions));
  426. }
  427. else
  428. {
  429. // Get the smoothed part of the path that will not change
  430. strPath += ' L' + pt.x + ' ' + pt.y;
  431. // Get the last part of the path (close to the current mouse position)
  432. // This part will change if the mouse moves again
  433. var tmpPath = '';
  434. lastPart = [];
  435. for (var offset = 2; offset < buffer.length; offset += 2)
  436. {
  437. pt = getAveragePoint(offset);
  438. tmpPath += ' L' + pt.x + ' ' + pt.y;
  439. lastPart.push(pt);
  440. }
  441. // Set the complete current path coordinates
  442. path.setAttribute('d', strPath + tmpPath);
  443. }
  444. }
  445. };
  446. };
  447. mxFreehand.prototype.NO_SMOOTHING = 1;
  448. mxFreehand.prototype.MILD_SMOOTHING = 4;
  449. mxFreehand.prototype.NORMAL_SMOOTHING = 8;
  450. mxFreehand.prototype.VERY_SMOOTH_SMOOTHING = 12;
  451. mxFreehand.prototype.SUPER_SMOOTH_SMOOTHING = 16;
  452. mxFreehand.prototype.HYPER_SMOOTH_SMOOTHING = 20;