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.

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