| @ -0,0 +1,229 @@ | |||
| /** | |||
| * Created by Alex on 26-Feb-15. | |||
| */ | |||
| var Hammer = require('../../module/hammer'); | |||
| class Canvas { | |||
| /** | |||
| * Create the main frame for the Network. | |||
| * This function is executed once when a Network object is created. The frame | |||
| * contains a canvas, and this canvas contains all objects like the axis and | |||
| * nodes. | |||
| * @private | |||
| */ | |||
| constructor(body, options) { | |||
| this.body = body; | |||
| this.setOptions(options); | |||
| this.translation = {x: 0, y: 0}; | |||
| this.scale = 1.0; | |||
| this.body.emitter.on("_setScale", (scale) => {this.scale = scale}); | |||
| this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); | |||
| this.body.emitter.once("resize", (obj) => {this.translation.x = obj.width * 0.5; this.translation.y = obj.height * 0.5; this.body.emitter.emit("_setTranslation", this.translation)}); | |||
| this.pixelRatio = 1; | |||
| // remove all elements from the container element. | |||
| while (this.body.container.hasChildNodes()) { | |||
| this.body.container.removeChild(this.body.container.firstChild); | |||
| } | |||
| this.frame = document.createElement('div'); | |||
| this.frame.className = 'vis network-frame'; | |||
| this.frame.style.position = 'relative'; | |||
| this.frame.style.overflow = 'hidden'; | |||
| this.frame.tabIndex = 900; | |||
| ////////////////////////////////////////////////////////////////// | |||
| this.frame.canvas = document.createElement("canvas"); | |||
| this.frame.canvas.style.position = 'relative'; | |||
| this.frame.appendChild(this.frame.canvas); | |||
| if (!this.frame.canvas.getContext) { | |||
| var noCanvas = document.createElement( 'DIV' ); | |||
| noCanvas.style.color = 'red'; | |||
| noCanvas.style.fontWeight = 'bold' ; | |||
| noCanvas.style.padding = '10px'; | |||
| noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; | |||
| this.frame.canvas.appendChild(noCanvas); | |||
| } | |||
| else { | |||
| var ctx = this.frame.canvas.getContext("2d"); | |||
| this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || | |||
| ctx.mozBackingStorePixelRatio || | |||
| ctx.msBackingStorePixelRatio || | |||
| ctx.oBackingStorePixelRatio || | |||
| ctx.backingStorePixelRatio || 1); | |||
| //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. | |||
| this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); | |||
| } | |||
| // add the frame to the container element | |||
| this.body.container.appendChild(this.frame); | |||
| this.body.emitter.emit("_setScale", 1);; | |||
| this.body.emitter.emit("_setTranslation", {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight});; | |||
| this._bindHammer(); | |||
| } | |||
| /** | |||
| * This function binds hammer, it can be repeated over and over due to the uniqueness check. | |||
| * @private | |||
| */ | |||
| _bindHammer() { | |||
| var me = this; | |||
| if (this.hammer !== undefined) { | |||
| this.hammer.dispose(); | |||
| } | |||
| this.drag = {}; | |||
| this.pinch = {}; | |||
| this.hammer = Hammer(this.frame.canvas, { | |||
| prevent_default: true | |||
| }); | |||
| this.hammer.on('tap', me.body.eventListeners.onTap ); | |||
| this.hammer.on('doubletap', me.body.eventListeners.onDoubleTap ); | |||
| this.hammer.on('hold', me.body.eventListeners.onHold ); | |||
| this.hammer.on('touch', me.body.eventListeners.onTouch ); | |||
| this.hammer.on('dragstart', me.body.eventListeners.onDragStart ); | |||
| this.hammer.on('drag', me.body.eventListeners.onDrag ); | |||
| this.hammer.on('dragend', me.body.eventListeners.onDragEnd ); | |||
| if (this.options.zoomable == true) { | |||
| this.hammer.on('mousewheel', me.body.eventListeners.onMouseWheel.bind(me)); | |||
| this.hammer.on('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); // for FF | |||
| this.hammer.on('pinch', me.body.eventListeners.onPinch.bind(me) ); | |||
| } | |||
| this.hammer.on('mousemove', me.body.eventListeners.onMouseMove.bind(me) ); | |||
| this.hammerFrame = Hammer(this.frame, { | |||
| prevent_default: true | |||
| }); | |||
| this.hammerFrame.on('release', me.body.eventListeners.onRelease.bind(me) ); | |||
| } | |||
| setOptions(options = {}) { | |||
| this.options = options; | |||
| } | |||
| /** | |||
| * Set a new size for the network | |||
| * @param {string} width Width in pixels or percentage (for example '800px' | |||
| * or '50%') | |||
| * @param {string} height Height in pixels or percentage (for example '400px' | |||
| * or '30%') | |||
| */ | |||
| setSize(width, height) { | |||
| var emitEvent = false; | |||
| var oldWidth = this.frame.canvas.width; | |||
| var oldHeight = this.frame.canvas.height; | |||
| if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { | |||
| this.frame.style.width = width; | |||
| this.frame.style.height = height; | |||
| this.frame.canvas.style.width = '100%'; | |||
| this.frame.canvas.style.height = '100%'; | |||
| this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; | |||
| this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; | |||
| this.options.width = width; | |||
| this.options.height = height; | |||
| emitEvent = true; | |||
| } | |||
| else { | |||
| // this would adapt the width of the canvas to the width from 100% if and only if | |||
| // there is a change. | |||
| if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { | |||
| this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; | |||
| emitEvent = true; | |||
| } | |||
| if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { | |||
| this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; | |||
| emitEvent = true; | |||
| } | |||
| } | |||
| if (emitEvent === true) { | |||
| this.body.emitter.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio}); | |||
| } | |||
| }; | |||
| /** | |||
| * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to | |||
| * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) | |||
| * @param {number} x | |||
| * @returns {number} | |||
| * @private | |||
| */ | |||
| _XconvertDOMtoCanvas(x) { | |||
| return (x - this.translation.x) / this.scale; | |||
| } | |||
| /** | |||
| * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to | |||
| * the X coordinate in DOM-space (coordinate point in browser relative to the container div) | |||
| * @param {number} x | |||
| * @returns {number} | |||
| * @private | |||
| */ | |||
| _XconvertCanvasToDOM(x) { | |||
| return x * this.scale + this.translation.x; | |||
| } | |||
| /** | |||
| * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to | |||
| * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) | |||
| * @param {number} y | |||
| * @returns {number} | |||
| * @private | |||
| */ | |||
| _YconvertDOMtoCanvas(y) { | |||
| return (y - this.translation.y) / this.scale; | |||
| } | |||
| /** | |||
| * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to | |||
| * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) | |||
| * @param {number} y | |||
| * @returns {number} | |||
| * @private | |||
| */ | |||
| _YconvertCanvasToDOM(y) { | |||
| return y * this.scale + this.translation.y ; | |||
| } | |||
| /** | |||
| * | |||
| * @param {object} pos = {x: number, y: number} | |||
| * @returns {{x: number, y: number}} | |||
| * @constructor | |||
| */ | |||
| canvasToDOM (pos) { | |||
| return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)}; | |||
| } | |||
| /** | |||
| * | |||
| * @param {object} pos = {x: number, y: number} | |||
| * @returns {{x: number, y: number}} | |||
| * @constructor | |||
| */ | |||
| DOMtoCanvas (pos) { | |||
| return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)}; | |||
| } | |||
| } | |||
| export {Canvas}; | |||
| @ -0,0 +1,243 @@ | |||
| /** | |||
| * Created by Alex on 26-Feb-15. | |||
| */ | |||
| if (typeof window !== 'undefined') { | |||
| window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || | |||
| window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; | |||
| } | |||
| class CanvasRenderer { | |||
| constructor(body) { | |||
| this.body = body; | |||
| this.redrawRequested = false; | |||
| this.renderTimer = false; | |||
| this.requiresTimeout = true; | |||
| this.continueRendering = true; | |||
| this.renderRequests = 0; | |||
| this.translation = {x: 0, y: 0}; | |||
| this.scale = 1.0; | |||
| this.canvasTopLeft = {x: 0, y: 0}; | |||
| this.canvasBottomRight = {x: 0, y: 0}; | |||
| this.body.emitter.on("_setScale", (scale) => this.scale = scale); | |||
| this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); | |||
| this.body.emitter.on("_redraw", this._redraw.bind(this)); | |||
| this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true)); | |||
| this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); | |||
| this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.continueRendering = true; this.startRendering();}); | |||
| this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.continueRendering = this.renderRequests > 0;}); | |||
| this._determineBrowserMethod(); | |||
| } | |||
| startRendering() { | |||
| if (this.continueRendering === true) { | |||
| if (!this.renderTimer) { | |||
| if (this.requiresTimeout == true) { | |||
| this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function | |||
| } | |||
| else { | |||
| this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function | |||
| } | |||
| } | |||
| } | |||
| } | |||
| renderStep() { | |||
| // reset the renderTimer so a new scheduled animation step can be set | |||
| this.renderTimer = undefined; | |||
| if (this.requiresTimeout == true) { | |||
| // this schedules a new simulation step | |||
| this.startRendering(); | |||
| } | |||
| this._redraw(); | |||
| if (this.requiresTimeout == false) { | |||
| // this schedules a new simulation step | |||
| this.startRendering(); | |||
| } | |||
| } | |||
| setCanvas(canvas) { | |||
| this.canvas = canvas; | |||
| } | |||
| /** | |||
| * Redraw the network with the current data | |||
| * chart will be resized too. | |||
| */ | |||
| redraw() { | |||
| this.setSize(this.constants.width, this.constants.height); | |||
| this._redraw(); | |||
| } | |||
| /** | |||
| * Redraw the network with the current data | |||
| * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. | |||
| * @private | |||
| */ | |||
| _requestRedraw(hidden) { | |||
| if (this.redrawRequested !== true) { | |||
| this.redrawRequested = true; | |||
| if (this.requiresTimeout === true) { | |||
| window.setTimeout(this._redraw.bind(this, hidden),0); | |||
| } | |||
| else { | |||
| window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); | |||
| } | |||
| } | |||
| } | |||
| _redraw(hidden = false) { | |||
| this.body.emitter.emit("_beforeRender"); | |||
| this.redrawRequested = false; | |||
| var ctx = this.canvas.frame.canvas.getContext('2d'); | |||
| ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); | |||
| // clear the canvas | |||
| var w = this.canvas.frame.canvas.clientWidth; | |||
| var h = this.canvas.frame.canvas.clientHeight; | |||
| ctx.clearRect(0, 0, w, h); | |||
| // set scaling and translation | |||
| ctx.save(); | |||
| ctx.translate(this.translation.x, this.translation.y); | |||
| ctx.scale(this.scale, this.scale); | |||
| this.canvasTopLeft = this.canvas.DOMtoCanvas({x:0,y:0}); | |||
| this.canvasBottomRight = this.canvas.DOMtoCanvas({x:this.canvas.frame.canvas.clientWidth,y:this.canvas.frame.canvas.clientHeight}); | |||
| if (hidden === false) { | |||
| // todo: solve this | |||
| //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { | |||
| this._drawEdges(ctx); | |||
| //} | |||
| } | |||
| // todo: solve this | |||
| //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { | |||
| this._drawNodes(ctx, this.body.nodes, hidden); | |||
| //} | |||
| if (hidden === false) { | |||
| if (this.controlNodesActive == true) { | |||
| this._drawControlNodes(ctx); | |||
| } | |||
| } | |||
| //this._drawNodes(ctx,this.body.supportNodes,true); | |||
| // this.physics.nodesSolver._debug(ctx,"#F00F0F"); | |||
| // restore original scaling and translation | |||
| ctx.restore(); | |||
| if (hidden === true) { | |||
| ctx.clearRect(0, 0, w, h); | |||
| } | |||
| } | |||
| /** | |||
| * Redraw all nodes | |||
| * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); | |||
| * @param {CanvasRenderingContext2D} ctx | |||
| * @param {Boolean} [alwaysShow] | |||
| * @private | |||
| */ | |||
| _drawNodes(ctx,nodes,alwaysShow = false) { | |||
| // first draw the unselected nodes | |||
| var selected = []; | |||
| for (var id in nodes) { | |||
| if (nodes.hasOwnProperty(id)) { | |||
| nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); | |||
| if (nodes[id].isSelected()) { | |||
| selected.push(id); | |||
| } | |||
| else { | |||
| if (alwaysShow === true) { | |||
| nodes[id].draw(ctx); | |||
| } | |||
| else if (nodes[id].inArea() === true) { | |||
| nodes[id].draw(ctx); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // draw the selected nodes on top | |||
| for (var s = 0, sMax = selected.length; s < sMax; s++) { | |||
| if (nodes[selected[s]].inArea() || alwaysShow) { | |||
| nodes[selected[s]].draw(ctx); | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Redraw all edges | |||
| * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); | |||
| * @param {CanvasRenderingContext2D} ctx | |||
| * @private | |||
| */ | |||
| _drawEdges(ctx) { | |||
| var edges = this.body.edges; | |||
| for (var id in edges) { | |||
| if (edges.hasOwnProperty(id)) { | |||
| var edge = edges[id]; | |||
| edge.setScale(this.scale); | |||
| if (edge.connected === true) { | |||
| edges[id].draw(ctx); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Redraw all edges | |||
| * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); | |||
| * @param {CanvasRenderingContext2D} ctx | |||
| * @private | |||
| */ | |||
| _drawControlNodes(ctx) { | |||
| var edges = this.body.edges; | |||
| for (var id in edges) { | |||
| if (edges.hasOwnProperty(id)) { | |||
| edges[id]._drawControlNodes(ctx); | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because | |||
| * some implementations (safari and IE9) did not support requestAnimationFrame | |||
| * @private | |||
| */ | |||
| _determineBrowserMethod() { | |||
| if (typeof window !== 'undefined') { | |||
| var browserType = navigator.userAgent.toLowerCase(); | |||
| this.requiresTimeout = false; | |||
| if (browserType.indexOf('msie 9.0') != -1) { // IE 9 | |||
| this.requiresTimeout = true; | |||
| } | |||
| else if (browserType.indexOf('safari') != -1) { // safari | |||
| if (browserType.indexOf('chrome') <= -1) { | |||
| this.requiresTimeout = true; | |||
| } | |||
| } | |||
| } | |||
| else { | |||
| this.requiresTimeout = true; | |||
| } | |||
| } | |||
| } | |||
| export {CanvasRenderer}; | |||
| @ -0,0 +1,343 @@ | |||
| /** | |||
| * Created by Alex on 26-Feb-15. | |||
| */ | |||
| var util = require('../../util'); | |||
| class View { | |||
| constructor(body, options) { | |||
| this.body = body; | |||
| this.setOptions(options); | |||
| this.animationSpeed = 1/this.renderRefreshRate; | |||
| this.animationEasingFunction = "easeInOutQuint"; | |||
| this.easingTime = 0; | |||
| this.sourceScale = 0; | |||
| this.targetScale = 0; | |||
| this.sourceTranslation = 0; | |||
| this.targetTranslation = 0; | |||
| this.lockedOnNodeId = null; | |||
| this.lockedOnNodeOffset = null; | |||
| this.touchTime = 0; | |||
| this.translation = {x: 0, y: 0}; | |||
| this.scale = 1.0; | |||
| this.viewFunction = undefined; | |||
| this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); | |||
| this.body.emitter.on("zoomExtentInstantly", this.zoomExtent.bind(this,{duration:0})); | |||
| this.body.emitter.on("_setScale", (scale) => this.scale = scale); | |||
| this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); | |||
| this.body.emitter.on("animationFinished", () => {this.body.emitter.emit("_stopRendering");}); | |||
| this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); | |||
| } | |||
| setOptions(options = {}) { | |||
| this.options = options; | |||
| } | |||
| setCanvas(canvas) { | |||
| this.canvas = canvas; | |||
| } | |||
| // zoomExtent | |||
| /** | |||
| * Find the center position of the network | |||
| * @private | |||
| */ | |||
| _getRange(specificNodes = []) { | |||
| var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; | |||
| if (specificNodes.length > 0) { | |||
| for (var i = 0; i < specificNodes.length; i++) { | |||
| node = this.body.nodes[specificNodes[i]]; | |||
| if (minX > (node.boundingBox.left)) { | |||
| minX = node.boundingBox.left; | |||
| } | |||
| if (maxX < (node.boundingBox.right)) { | |||
| maxX = node.boundingBox.right; | |||
| } | |||
| if (minY > (node.boundingBox.bottom)) { | |||
| minY = node.boundingBox.top; | |||
| } // top is negative, bottom is positive | |||
| if (maxY < (node.boundingBox.top)) { | |||
| maxY = node.boundingBox.bottom; | |||
| } // top is negative, bottom is positive | |||
| } | |||
| } | |||
| else { | |||
| for (var nodeId in this.body.nodes) { | |||
| if (this.body.nodes.hasOwnProperty(nodeId)) { | |||
| node = this.body.nodes[nodeId]; | |||
| if (minX > (node.boundingBox.left)) { | |||
| minX = node.boundingBox.left; | |||
| } | |||
| if (maxX < (node.boundingBox.right)) { | |||
| maxX = node.boundingBox.right; | |||
| } | |||
| if (minY > (node.boundingBox.bottom)) { | |||
| minY = node.boundingBox.top; | |||
| } // top is negative, bottom is positive | |||
| if (maxY < (node.boundingBox.top)) { | |||
| maxY = node.boundingBox.bottom; | |||
| } // top is negative, bottom is positive | |||
| } | |||
| } | |||
| } | |||
| if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { | |||
| minY = 0, maxY = 0, minX = 0, maxX = 0; | |||
| } | |||
| return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; | |||
| } | |||
| /** | |||
| * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; | |||
| * @returns {{x: number, y: number}} | |||
| * @private | |||
| */ | |||
| _findCenter(range) { | |||
| return {x: (0.5 * (range.maxX + range.minX)), | |||
| y: (0.5 * (range.maxY + range.minY))}; | |||
| } | |||
| /** | |||
| * This function zooms out to fit all data on screen based on amount of nodes | |||
| * @param {Object} | |||
| * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; | |||
| * @param {Boolean} [disableStart] | If true, start is not called. | |||
| */ | |||
| zoomExtent(options = {nodes:[]}, initialZoom = false) { | |||
| var range; | |||
| var zoomLevel; | |||
| if (initialZoom == true) { | |||
| // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. | |||
| var positionDefined = 0; | |||
| for (var nodeId in this.body.nodes) { | |||
| if (this.body.nodes.hasOwnProperty(nodeId)) { | |||
| var node = this.body.nodes[nodeId]; | |||
| if (node.predefinedPosition == true) { | |||
| positionDefined += 1; | |||
| } | |||
| } | |||
| } | |||
| if (positionDefined > 0.5 * this.body.nodeIndices.length) { | |||
| this.zoomExtent(options,false); | |||
| return; | |||
| } | |||
| range = this._getRange(options.nodes); | |||
| var numberOfNodes = this.body.nodeIndices.length; | |||
| if (this.options.smoothCurves == true) { | |||
| zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. | |||
| } | |||
| else { | |||
| zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. | |||
| } | |||
| // correct for larger canvasses. | |||
| var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); | |||
| zoomLevel *= factor; | |||
| } | |||
| else { | |||
| this.body.emitter.emit("_redrawHidden"); | |||
| range = this._getRange(options.nodes); | |||
| var xDistance = Math.abs(range.maxX - range.minX) * 1.1; | |||
| var yDistance = Math.abs(range.maxY - range.minY) * 1.1; | |||
| var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; | |||
| var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; | |||
| zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; | |||
| } | |||
| if (zoomLevel > 1.0) { | |||
| zoomLevel = 1.0; | |||
| } | |||
| var center = this._findCenter(range); | |||
| var animationOptions = {position: center, scale: zoomLevel, animation: options}; | |||
| this.moveTo(animationOptions); | |||
| } | |||
| // animation | |||
| /** | |||
| * Center a node in view. | |||
| * | |||
| * @param {Number} nodeId | |||
| * @param {Number} [options] | |||
| */ | |||
| focusOnNode(nodeId, options = {}) { | |||
| if (this.body.nodes[nodeId] !== undefined) { | |||
| var nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y}; | |||
| options.position = nodePosition; | |||
| options.lockedOnNode = nodeId; | |||
| this.moveTo(options) | |||
| } | |||
| else { | |||
| console.log("Node: " + nodeId + " cannot be found."); | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels | |||
| * | options.scale = Number // scale to move to | |||
| * | options.position = {x:Number, y:Number} // position to move to | |||
| * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to | |||
| */ | |||
| moveTo(options) { | |||
| if (options === undefined) { | |||
| options = {}; | |||
| return; | |||
| } | |||
| if (options.offset === undefined) {options.offset = {x: 0, y: 0}; } | |||
| if (options.offset.x === undefined) {options.offset.x = 0; } | |||
| if (options.offset.y === undefined) {options.offset.y = 0; } | |||
| if (options.scale === undefined) {options.scale = this.scale; } | |||
| if (options.position === undefined) {options.position = this.translation;} | |||
| if (options.animation === undefined) {options.animation = {duration:0}; } | |||
| if (options.animation === false ) {options.animation = {duration:0}; } | |||
| if (options.animation === true ) {options.animation = {}; } | |||
| if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration | |||
| if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function | |||
| this.animateView(options); | |||
| } | |||
| /** | |||
| * | |||
| * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels | |||
| * | options.time = Number // animation time in milliseconds | |||
| * | options.scale = Number // scale to animate to | |||
| * | options.position = {x:Number, y:Number} // position to animate to | |||
| * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, | |||
| * // easeInCubic, easeOutCubic, easeInOutCubic, | |||
| * // easeInQuart, easeOutQuart, easeInOutQuart, | |||
| * // easeInQuint, easeOutQuint, easeInOutQuint | |||
| */ | |||
| animateView(options) { | |||
| if (options === undefined) { | |||
| return; | |||
| } | |||
| this.animationEasingFunction = options.animation.easingFunction; | |||
| // release if something focussed on the node | |||
| this.releaseNode(); | |||
| if (options.locked == true) { | |||
| this.lockedOnNodeId = options.lockedOnNode; | |||
| this.lockedOnNodeOffset = options.offset; | |||
| } | |||
| // forcefully complete the old animation if it was still running | |||
| if (this.easingTime != 0) { | |||
| this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. | |||
| } | |||
| this.sourceScale = this.scale; | |||
| this.sourceTranslation = this.translation; | |||
| this.targetScale = options.scale; | |||
| // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw | |||
| // but at least then we'll have the target transition | |||
| this.body.emitter.emit("_setScale",this.targetScale); | |||
| var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight}); | |||
| var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node | |||
| x: viewCenter.x - options.position.x, | |||
| y: viewCenter.y - options.position.y | |||
| }; | |||
| this.targetTranslation = { | |||
| x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, | |||
| y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y | |||
| }; | |||
| // if the time is set to 0, don't do an animation | |||
| if (options.animation.duration == 0) { | |||
| if (this.lockedOnNodeId != null) { | |||
| this.viewFunction = this._lockedRedraw.bind(this); | |||
| this.body.emitter.on("_beforeRender", this.viewFunction); | |||
| } | |||
| else { | |||
| this.body.emitter.emit("_setScale", this.targetScale);; | |||
| this.body.emitter.emit("_setTranslation", this.targetTranslation); | |||
| this.body.emitter.emit("_requestRedraw"); | |||
| } | |||
| } | |||
| else { | |||
| this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's | |||
| this.animationEasingFunction = options.animation.easingFunction; | |||
| this.viewFunction = this._transitionRedraw.bind(this); | |||
| this.body.emitter.on("_beforeRender", this.viewFunction); | |||
| this.body.emitter.emit("_startRendering"); | |||
| } | |||
| } | |||
| /** | |||
| * used to animate smoothly by hijacking the redraw function. | |||
| * @private | |||
| */ | |||
| _lockedRedraw() { | |||
| var nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y}; | |||
| var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); | |||
| var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node | |||
| x: viewCenter.x - nodePosition.x, | |||
| y: viewCenter.y - nodePosition.y | |||
| }; | |||
| var sourceTranslation = this.translation; | |||
| var targetTranslation = { | |||
| x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, | |||
| y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y | |||
| }; | |||
| this.body.emitter.emit("_setTranslation", targetTranslation); | |||
| } | |||
| releaseNode() { | |||
| if (this.lockedOnNodeId !== undefined) { | |||
| this.body.emitter.off("_beforeRender", this.viewFunction); | |||
| this.lockedOnNodeId = undefined; | |||
| this.lockedOnNodeOffset = undefined; | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| * @param easingTime | |||
| * @private | |||
| */ | |||
| _transitionRedraw(finished = false) { | |||
| this.easingTime += this.animationSpeed; | |||
| this.easingTime = finished === true ? 1.0 : this.easingTime; | |||
| var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); | |||
| this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress); | |||
| this.body.emitter.emit("_setTranslation", { | |||
| x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, | |||
| y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress | |||
| }); | |||
| // cleanup | |||
| if (this.easingTime >= 1.0) { | |||
| this.body.emitter.off("_beforeRender", this.viewFunction); | |||
| this.easingTime = 0; | |||
| if (this.lockedOnNodeId != null) { | |||
| this.viewFunction = this._lockedRedraw.bind(this); | |||
| this.body.emitter.on("_beforeRender", this.viewFunction); | |||
| } | |||
| this.body.emitter.emit("animationFinished"); | |||
| } | |||
| }; | |||
| } | |||
| export {View}; | |||