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.

476 lines
15 KiB

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