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.

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