vis.js is a dynamic, browser-based visualization library
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.

370 lines
13 KiB

9 years ago
9 years ago
9 years ago
  1. let Hammer = require('../../module/hammer');
  2. let hammerUtil = require('../../hammerUtil');
  3. let util = require('../../util');
  4. /**
  5. * Create the main frame for the Network.
  6. * This function is executed once when a Network object is created. The frame
  7. * contains a canvas, and this canvas contains all objects like the axis and
  8. * nodes.
  9. * @private
  10. */
  11. class Canvas {
  12. constructor(body) {
  13. this.body = body;
  14. this.pixelRatio = 1;
  15. this.resizeTimer = undefined;
  16. this.resizeFunction = this._onResize.bind(this);
  17. this.cameraState = {};
  18. this.options = {};
  19. this.defaultOptions = {
  20. autoResize: true,
  21. height: '100%',
  22. width: '100%'
  23. };
  24. util.extend(this.options, this.defaultOptions);
  25. this.bindEventListeners();
  26. }
  27. bindEventListeners() {
  28. // bind the events
  29. this.body.emitter.once("resize", (obj) => {
  30. if (obj.width !== 0) {
  31. this.body.view.translation.x = obj.width * 0.5;
  32. }
  33. if (obj.height !== 0) {
  34. this.body.view.translation.y = obj.height * 0.5;
  35. }
  36. });
  37. this.body.emitter.on("setSize", this.setSize.bind(this));
  38. this.body.emitter.on("destroy", () => {
  39. this.hammerFrame.destroy();
  40. this.hammer.destroy();
  41. this._cleanUp();
  42. });
  43. }
  44. setOptions(options) {
  45. if (options !== undefined) {
  46. let fields = ['width','height','autoResize'];
  47. util.selectiveDeepExtend(fields,this.options, options);
  48. }
  49. if (this.options.autoResize === true) {
  50. // automatically adapt to a changing size of the browser.
  51. this._cleanUp();
  52. this.resizeTimer = setInterval(() => {
  53. let changed = this.setSize();
  54. if (changed === true) {
  55. this.body.emitter.emit("_requestRedraw");
  56. }
  57. }, 1000);
  58. this.resizeFunction = this._onResize.bind(this);
  59. util.addEventListener(window,'resize',this.resizeFunction);
  60. }
  61. }
  62. _cleanUp() {
  63. // automatically adapt to a changing size of the browser.
  64. if (this.resizeTimer !== undefined) {
  65. clearInterval(this.resizeTimer);
  66. }
  67. util.removeEventListener(window,'resize',this.resizeFunction);
  68. this.resizeFunction = undefined;
  69. }
  70. _onResize() {
  71. this.setSize();
  72. this.body.emitter.emit("_redraw");
  73. }
  74. /**
  75. * Get and store the cameraState
  76. * @private
  77. */
  78. _getCameraState(pixelRatio = this.pixelRatio) {
  79. this.cameraState.previousWidth = this.frame.canvas.width / pixelRatio;
  80. this.cameraState.scale = this.body.view.scale;
  81. this.cameraState.position = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.width / pixelRatio, y: 0.5 * this.frame.canvas.height / pixelRatio});
  82. }
  83. /**
  84. * Set the cameraState
  85. * @private
  86. */
  87. _setCameraState() {
  88. if (this.cameraState.scale !== undefined &&
  89. this.frame.canvas.clientWidth !== 0 &&
  90. this.frame.canvas.clientHeight !== 0 &&
  91. this.pixelRatio !== 0 &&
  92. this.cameraState.previousWidth > 0) {
  93. this.body.view.scale = this.cameraState.scale * ((this.frame.canvas.width / this.pixelRatio) / this.cameraState.previousWidth);
  94. // this comes from the view module.
  95. var currentViewCenter = this.DOMtoCanvas({
  96. x: 0.5 * this.frame.canvas.clientWidth,
  97. y: 0.5 * this.frame.canvas.clientHeight
  98. });
  99. var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
  100. x: currentViewCenter.x - this.cameraState.position.x,
  101. y: currentViewCenter.y - this.cameraState.position.y
  102. };
  103. this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale;
  104. this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale;
  105. }
  106. }
  107. _prepareValue(value) {
  108. if (typeof value === 'number') {
  109. return value + 'px';
  110. }
  111. else if (typeof value === 'string') {
  112. if (value.indexOf('%') !== -1 || value.indexOf('px') !== -1) {
  113. return value;
  114. }
  115. else if (value.indexOf('%') === -1) {
  116. return value + 'px';
  117. }
  118. }
  119. throw new Error('Could not use the value supplied for width or height:' + value);
  120. }
  121. /**
  122. * Create the HTML
  123. */
  124. _create() {
  125. // remove all elements from the container element.
  126. while (this.body.container.hasChildNodes()) {
  127. this.body.container.removeChild(this.body.container.firstChild);
  128. }
  129. this.frame = document.createElement('div');
  130. this.frame.className = 'vis-network';
  131. this.frame.style.position = 'relative';
  132. this.frame.style.overflow = 'hidden';
  133. this.frame.tabIndex = 900; // tab index is required for keycharm to bind keystrokes to the div instead of the window
  134. //////////////////////////////////////////////////////////////////
  135. this.frame.canvas = document.createElement("canvas");
  136. this.frame.canvas.style.position = 'relative';
  137. this.frame.appendChild(this.frame.canvas);
  138. if (!this.frame.canvas.getContext) {
  139. let noCanvas = document.createElement( 'DIV' );
  140. noCanvas.style.color = 'red';
  141. noCanvas.style.fontWeight = 'bold' ;
  142. noCanvas.style.padding = '10px';
  143. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  144. this.frame.canvas.appendChild(noCanvas);
  145. }
  146. else {
  147. let ctx = this.frame.canvas.getContext("2d");
  148. this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
  149. ctx.mozBackingStorePixelRatio ||
  150. ctx.msBackingStorePixelRatio ||
  151. ctx.oBackingStorePixelRatio ||
  152. ctx.backingStorePixelRatio || 1);
  153. this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
  154. }
  155. // add the frame to the container element
  156. this.body.container.appendChild(this.frame);
  157. this.body.view.scale = 1;
  158. this.body.view.translation = {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight};
  159. this._bindHammer();
  160. }
  161. /**
  162. * This function binds hammer, it can be repeated over and over due to the uniqueness check.
  163. * @private
  164. */
  165. _bindHammer() {
  166. if (this.hammer !== undefined) {
  167. this.hammer.destroy();
  168. }
  169. this.drag = {};
  170. this.pinch = {};
  171. // init hammer
  172. this.hammer = new Hammer(this.frame.canvas);
  173. this.hammer.get('pinch').set({enable: true});
  174. // enable to get better response, todo: test on mobile.
  175. this.hammer.get('pan').set({threshold:5, direction:30}); // 30 is ALL_DIRECTIONS in hammer.
  176. hammerUtil.onTouch(this.hammer, (event) => {this.body.eventListeners.onTouch(event)});
  177. this.hammer.on('tap', (event) => {this.body.eventListeners.onTap(event)});
  178. this.hammer.on('doubletap', (event) => {this.body.eventListeners.onDoubleTap(event)});
  179. this.hammer.on('press', (event) => {this.body.eventListeners.onHold(event)});
  180. this.hammer.on('panstart', (event) => {this.body.eventListeners.onDragStart(event)});
  181. this.hammer.on('panmove', (event) => {this.body.eventListeners.onDrag(event)});
  182. this.hammer.on('panend', (event) => {this.body.eventListeners.onDragEnd(event)});
  183. this.hammer.on('pinch', (event) => {this.body.eventListeners.onPinch(event)});
  184. // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work?
  185. this.frame.canvas.addEventListener('mousewheel', (event) => {this.body.eventListeners.onMouseWheel(event)});
  186. this.frame.canvas.addEventListener('DOMMouseScroll', (event) => {this.body.eventListeners.onMouseWheel(event)});
  187. this.frame.canvas.addEventListener('mousemove', (event) => {this.body.eventListeners.onMouseMove(event)});
  188. this.frame.canvas.addEventListener('contextmenu', (event) => {this.body.eventListeners.onContext(event)});
  189. this.hammerFrame = new Hammer(this.frame);
  190. hammerUtil.onRelease(this.hammerFrame, (event) => {this.body.eventListeners.onRelease(event)});
  191. }
  192. /**
  193. * Set a new size for the network
  194. * @param {string} width Width in pixels or percentage (for example '800px'
  195. * or '50%')
  196. * @param {string} height Height in pixels or percentage (for example '400px'
  197. * or '30%')
  198. */
  199. setSize(width = this.options.width, height = this.options.height) {
  200. width = this._prepareValue(width);
  201. height= this._prepareValue(height);
  202. let emitEvent = false;
  203. let oldWidth = this.frame.canvas.width;
  204. let oldHeight = this.frame.canvas.height;
  205. // update the pixelratio
  206. let ctx = this.frame.canvas.getContext("2d");
  207. let previousRation = this.pixelRatio; // we cache this because the camera state storage needs the old value
  208. this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
  209. ctx.mozBackingStorePixelRatio ||
  210. ctx.msBackingStorePixelRatio ||
  211. ctx.oBackingStorePixelRatio ||
  212. ctx.backingStorePixelRatio || 1);
  213. if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
  214. this._getCameraState(previousRation);
  215. this.frame.style.width = width;
  216. this.frame.style.height = height;
  217. this.frame.canvas.style.width = '100%';
  218. this.frame.canvas.style.height = '100%';
  219. this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
  220. this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
  221. this.options.width = width;
  222. this.options.height = height;
  223. emitEvent = true;
  224. }
  225. else {
  226. // this would adapt the width of the canvas to the width from 100% if and only if
  227. // there is a change.
  228. // store the camera if there is a change in size.
  229. if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio) || this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) {
  230. this._getCameraState(previousRation);
  231. }
  232. if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio)) {
  233. this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
  234. emitEvent = true;
  235. }
  236. if (this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) {
  237. this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
  238. emitEvent = true;
  239. }
  240. }
  241. if (emitEvent === true) {
  242. this.body.emitter.emit('resize', {
  243. width:Math.round(this.frame.canvas.width / this.pixelRatio),
  244. height:Math.round(this.frame.canvas.height / this.pixelRatio),
  245. oldWidth: Math.round(oldWidth / this.pixelRatio),
  246. oldHeight: Math.round(oldHeight / this.pixelRatio)
  247. });
  248. // restore the camera on change.
  249. this._setCameraState();
  250. }
  251. return emitEvent;
  252. };
  253. /**
  254. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  255. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  256. * @param {number} x
  257. * @returns {number}
  258. * @private
  259. */
  260. _XconvertDOMtoCanvas(x) {
  261. return (x - this.body.view.translation.x) / this.body.view.scale;
  262. }
  263. /**
  264. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  265. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  266. * @param {number} x
  267. * @returns {number}
  268. * @private
  269. */
  270. _XconvertCanvasToDOM(x) {
  271. return x * this.body.view.scale + this.body.view.translation.x;
  272. }
  273. /**
  274. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  275. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  276. * @param {number} y
  277. * @returns {number}
  278. * @private
  279. */
  280. _YconvertDOMtoCanvas(y) {
  281. return (y - this.body.view.translation.y) / this.body.view.scale;
  282. }
  283. /**
  284. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  285. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  286. * @param {number} y
  287. * @returns {number}
  288. * @private
  289. */
  290. _YconvertCanvasToDOM(y) {
  291. return y * this.body.view.scale + this.body.view.translation.y;
  292. }
  293. /**
  294. *
  295. * @param {object} pos = {x: number, y: number}
  296. * @returns {{x: number, y: number}}
  297. * @constructor
  298. */
  299. canvasToDOM (pos) {
  300. return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)};
  301. }
  302. /**
  303. *
  304. * @param {object} pos = {x: number, y: number}
  305. * @returns {{x: number, y: number}}
  306. * @constructor
  307. */
  308. DOMtoCanvas (pos) {
  309. return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)};
  310. }
  311. }
  312. export default Canvas;