Browse Source

reasonably success in decoupling the rendering and the network by segmenting a canvas, view (for camera controls) and renderer.

flowchartTest
Alex de Mulder 9 years ago
parent
commit
fd06e8e81e
8 changed files with 2796 additions and 2453 deletions
  1. +1649
    -1358
      dist/vis.js
  2. +92
    -1068
      lib/network/Network.js
  3. +3
    -17
      lib/network/Node.js
  4. +229
    -0
      lib/network/modules/Canvas.js
  5. +243
    -0
      lib/network/modules/CanvasRenderer.js
  6. +236
    -9
      lib/network/modules/PhysicsEngine.js
  7. +343
    -0
      lib/network/modules/View.js
  8. +1
    -1
      lib/timeline/component/CurrentTime.js

+ 1649
- 1358
dist/vis.js
File diff suppressed because it is too large
View File


+ 92
- 1068
lib/network/Network.js
File diff suppressed because it is too large
View File


+ 3
- 17
lib/network/Node.js View File

@ -54,10 +54,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.grouplist = grouplist;
// physics properties
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.x = null;
this.y = null;
this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate
@ -65,7 +61,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
// used for reverting to previous position on stabilization
this.previousState = {vx:0,vy:0,x:0,y:0};
this.damping = networkConstants.physics.damping; // written every time gravity is calculated
this.fixedData = {x:null,y:null};
this.setProperties(properties, constants);
@ -899,13 +894,14 @@ Node.prototype.getTextSize = function(ctx) {
width = Math.max(width, ctx.measureText(lines[i]).width);
}
return {"width": width, "height": height, lineCount: lines.length};
return {width: width, height: height, lineCount: lines.length};
}
else {
return {"width": 0, "height": 0, lineCount: 0};
return {width: 0, height: 0, lineCount: 0};
}
};
/**
* this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
* there is a safety margin of 0.3 * width;
@ -924,16 +920,6 @@ Node.prototype.inArea = function() {
}
};
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
* @returns {boolean}
*/
Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x &&
this.y >= this.canvasTopLeft.y &&
this.y < this.canvasBottomRight.y);
};
/**
* This allows the zoom level of the network to influence the rendering

+ 229
- 0
lib/network/modules/Canvas.js View File

@ -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};

+ 243
- 0
lib/network/modules/CanvasRenderer.js View File

@ -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};

+ 236
- 9
lib/network/modules/PhysicsEngine.js View File

@ -11,19 +11,76 @@ import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringS
import {CentralGravitySolver} from "./components/physics/CentralGravitySolver";
var util = require('../../util');
class PhysicsEngine {
constructor(body, options) {
this.body = body;
this.physicsBody = {calculationNodes: {}, calculationNodeIndices:[], forces: {}, velocities: {}};
this.scale = 1;
this.viewFunction = undefined;
this.body.emitter.on("_setScale", (scale) => this.scale = scale);
this.simulationInterval = 1000 / 60;
this.requiresTimeout = true;
this.previousStates = {};
this.renderTimer == undefined;
this.stabilized = false;
this.stabilizationIterations = 0;
// default options
this.options = {
barnesHut: {
thetaInverted: 1 / 0.5, // inverted to save time during calculation
gravitationalConstant: -2000,
centralGravity: 0.3,
springLength: 95,
springConstant: 0.04,
damping: 0.09
},
repulsion: {
centralGravity: 0.0,
springLength: 200,
springConstant: 0.05,
nodeDistance: 100,
damping: 0.09
},
hierarchicalRepulsion: {
centralGravity: 0.0,
springLength: 100,
springConstant: 0.01,
nodeDistance: 150,
damping: 0.09
},
model: 'BarnesHut',
timestep: 0.5,
maxVelocity: 50,
minVelocity: 0.1, // px/s
stabilization: {
enabled: true,
iterations: 1000, // maximum number of iteration to stabilize
updateInterval: 100,
onlyDynamicEdges: false,
zoomExtent: true
}
}
this.setOptions(options);
}
setOptions(options) {
if (options !== undefined) {
this.options = options;
this.init();
if (typeof options.stabilization == 'boolean') {
options.stabilization = {
enabled: options.stabilization
}
}
util.deepExtend(this.options, options);
}
this.init();
}
@ -49,6 +106,87 @@ class PhysicsEngine {
this.modelOptions = options;
}
startSimulation() {
this.stabilized = false;
if (this.options.stabilization.enabled === true) {
this.stabilize();
}
else {
this.runSimulation();
}
}
runSimulation() {
if (this.viewFunction === undefined) {
this.viewFunction = this.simulationStep.bind(this);
this.body.emitter.on("_beforeRender", this.viewFunction);
this.body.emitter.emit("_startRendering");
}
}
simulationStep() {
// check if the physics have settled
var startTime = Date.now();
this.physicsTick();
var physicsTime = Date.now() - startTime;
// run double speed if it is a little graph
if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) {
this.physicsTick();
// this makes sure there is no jitter. The decision is taken once to run it at double speed.
this.runDoubleSpeed = true;
}
if (this.stabilized === true) {
if (this.stabilizationIterations > 1) {
// trigger the "stabilized" event.
// The event is triggered on the next tick, to prevent the case that
// it is fired while initializing the Network, in which case you would not
// be able to catch it
var me = this;
var params = {
iterations: this.stabilizationIterations
};
this.stabilizationIterations = 0;
this.startedStabilization = false;
setTimeout(function () {
me.body.emitter.emit("stabilized", params);
}, 0);
}
else {
this.stabilizationIterations = 0;
}
this.body.emitter.emit("_stopRendering");
}
}
/**
* A single simulation step (or "tick") in the physics simulation
*
* @private
*/
physicsTick() {
if (this.stabilized === false) {
this.calculateForces();
this.stabilized = this.moveNodes();
// determine if the network has stabilzied
if (this.stabilized === true) {
this.revert();
}
else {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization == false) {
this.body.emitter.emit("startStabilizing");
this.startedStabilization = true;
}
}
this.stabilizationIterations++;
}
}
/**
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
* handled in the calculateForces function. We then use a quadratic curve with the center node as control.
@ -122,26 +260,27 @@ class PhysicsEngine {
var nodesPresent = false;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var maxVelocity = this.options.maxVelocity === 0 ? 1e9 : this.options.maxVelocity;
var moving = false;
var vminCorrected = this.options.minVelocity / Math.max(this.body.functions.getScale(),0.05);
var stabilized = true;
var vminCorrected = this.options.minVelocity / Math.max(this.scale,0.05);
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
moving = nodeVelocity > vminCorrected;
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
stabilized = nodeVelocity < vminCorrected && stabilized === true;
nodesPresent = true;
}
if (nodesPresent == true) {
if (vminCorrected > 0.5*this.options.maxVelocity) {
return true;
return false;
}
else {
return moving;
return stabilized;
}
}
return false;
return true;
}
_performStep(nodeId,maxVelocity) {
@ -181,12 +320,100 @@ class PhysicsEngine {
return totalVelocity;
}
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
/**
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
* because only the supportnodes for the smoothCurves have to settle.
*
* @private
*/
_freezeNodes() {
var nodes = this.body.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].x != null && nodes[id].y != null) {
nodes[id].fixedData.x = nodes[id].xFixed;
nodes[id].fixedData.y = nodes[id].yFixed;
nodes[id].xFixed = true;
nodes[id].yFixed = true;
}
}
}
}
/**
* Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
*
* @private
*/
_restoreFrozenNodes() {
var nodes = this.body.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].fixedData.x != null) {
nodes[id].xFixed = nodes[id].fixedData.x;
nodes[id].yFixed = nodes[id].fixedData.y;
}
}
}
}
/**
* Find a stable position for all nodes
* @private
*/
stabilize() {
if (this.options.stabilization.onlyDynamicEdges == true) {
this._freezeNodes();
}
this.stabilizationSteps = 0;
setTimeout(this._stabilizationBatch.bind(this),0);
}
_stabilizationBatch() {
var count = 0;
while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) {
this.physicsTick();
this.stabilizationSteps++;
count++;
}
if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) {
this.body.emitter.emit("stabilizationProgress", {steps: this.stabilizationSteps, total: this.options.stabilization.iterations});
setTimeout(this._stabilizationBatch.bind(this),0);
}
else {
this._finalizeStabilization();
}
}
_finalizeStabilization() {
if (this.options.stabilization.zoomExtent == true) {
this.body.emitter.emit("zoomExtent", {duration:0});
}
if (this.options.stabilization.onlyDynamicEdges == true) {
this._restoreFrozenNodes();
}
this.body.emitter.emit("stabilizationIterationsDone");
this.body.emitter.emit("_requestRedraw");
}
}
export {PhysicsEngine};

+ 343
- 0
lib/network/modules/View.js View File

@ -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};

+ 1
- 1
lib/timeline/component/CurrentTime.js View File

@ -122,7 +122,7 @@ CurrentTime.prototype.start = function() {
me.redraw();
// start a timer to adjust for the new time
// start a renderTimer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);
}

Loading…
Cancel
Save