Skip to content

Instantly share code, notes, and snippets.

@starcalibre
Created January 19, 2017 22:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save starcalibre/71409a4aab6c85bc13d9fc5a4fd4d7af to your computer and use it in GitHub Desktop.
Save starcalibre/71409a4aab6c85bc13d9fc5a4fd4d7af to your computer and use it in GitHub Desktop.
3D Fruchterman-Reingold Layout
.idea/
*.iml

The Fruchterman-Reingold algorithm is a force directed graph layout algorithm. This is a demonstration for the algorithm applied to a few different randomly generated graphs rendered in 3D. The algorithm finds an aesthetically pleasing graph layout by creating an attraction between vertices that share an edge, and a repulsion between those that don't. An additional gravitational force pulls all the vertices towards the center to ensure a spherical layout, and prevents any 0-degree vertices from straying too too far from the layout.

Force-directed layouts are quite good at uncovering inherent structures in graphs. Try generating a few different graphs to see for yourself! This demonstration can create three different types of graphs:

  • Random Graphs - These are generared using the Erdős–Rényi model. These graphs contain a fixed number of vertices and edges. The edges are assigned to vertices randomly.
  • Community Graphs - A graph with "community structure". Each vertex in the graph is randomly assigned to a community. Edges are assigned between each such that vertices belonging to the same community will much, much more likely to share an edge.
  • Power Law Graphs - These are generated using the Barabási–Albert model. New vertices are added to the graph one-by-one, and connect themselves to one other vertex in the graph. The probably of an existing vertex receiving a new edge is proportional to its degree; vertices with a higher degree are much more likely to receive the new vertex.

Orbit-controls are enabled, so the graphs can be rotated and zoomed in and out.

'use strict';
/**
* Randomise the contents of an array using the Fisher-Yates shuffle.
* @param array
* @returns {*}
*/
function shuffleArray(array) {
var counter = array.length;
while (counter > 0) {
var index = randInt(0, counter);
counter--;
var temp = array[counter];
array[counter] = array[index];
array[index] = temp;
}
return array;
}
/**
* Generate a random integer in the range [min, max)
*
* @param min
* @param max
* @returns {*}
*/
function randInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
/**
* Clamp a number n to the range [min, max]
*
* @param n
* @param min
* @param max
* @returns {number}
*/
function clamp(n, min, max) {
return Math.min(Math.max(n, min), max);
}
'use strict';
/* globals THREE */
var edgeMaterial = new THREE.LineBasicMaterial({
color: 0x686765
});
function Edge(source, target) {
this.source = source;
this.target = target;
var edgeGeometry = new THREE.Geometry();
edgeGeometry.vertices.push(new THREE.Vector3().copy(source.pos));
edgeGeometry.vertices.push(new THREE.Vector3().copy(target.pos));
this.mesh = new THREE.Line(edgeGeometry, edgeMaterial);
}
/**
* Update edge mesh with its vertices current positions.
*/
Edge.prototype.update = function() {
this.mesh.geometry.vertices[0].copy(this.source.pos);
this.mesh.geometry.vertices[1].copy(this.target.pos);
};
'use strict';
/* globals THREE, shuffleArray */
function Graph(minX, maxX, minY, maxY, minZ, maxZ) {
this.vertices = [];
this.edges = [];
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
this.minZ = minZ;
this.maxZ = maxZ;
}
/**
* Add an edge to the graph from source to vertex. The function will add vertices to
* the graph such that from > to. Calls to invalid vertex id's are ignored silently.
*
* @param source
* @param target
*/
Graph.prototype.addEdge = function(source, target) {
var newEdge = new Edge(source, target);
this.edges.push(newEdge);
source.degree++;
target.degree++;
};
/**
* Apply the Frucherman-Reingold algorith to the graph using the given
* parameters.
*
* @param iter
* @param scale
* @param gravity
*/
Graph.prototype.applyLayout = function(iter, scale, gravity) {
var width = (this.maxX - this.minX);
var height = (this.maxY - this.minY);
var depth = (this.maxZ - this.minZ);
var area = scale * this.vertices.length * this.vertices.length;
var k = Math.pow(area / this.vertices.length + 1, 1 / 3);
function fAttr(x) {
return (x * x) / k;
}
function fRepl(x) {
return (k * k) /x ;
}
var t = Math.sqrt(this.vertices.length);
var dt = t / (iter + 1);
var eps = 0.05;
for(var i = 0; i < iter; i++) {
// calculate repulsive forces
this.vertices.forEach(function(v) {
v.disp = new THREE.Vector3();
this.vertices.forEach(function(u) {
if(u.id != v.id) {
var delta = new THREE.Vector3().subVectors(v.pos, u.pos);
var deltaMag = Math.max(eps, delta.length());
v.disp.x += (delta.x / deltaMag) * fRepl(deltaMag);
v.disp.y += (delta.y / deltaMag) * fRepl(deltaMag);
v.disp.z += (delta.z / deltaMag) * fRepl(deltaMag);
}
});
}.bind(this));
// calculate attractive forces
this.edges.forEach(function(e, i) {
var delta = new THREE.Vector3().subVectors(e.source.pos, e.target.pos);
var deltaMag = Math.max(eps, delta.length());
e.source.disp.x -= (delta.x / deltaMag) * fAttr(deltaMag);
e.source.disp.y -= (delta.y / deltaMag) * fAttr(deltaMag);
e.source.disp.z -= (delta.z / deltaMag) * fAttr(deltaMag);
e.target.disp.x += (delta.x / deltaMag) * fAttr(deltaMag);
e.target.disp.y += (delta.y / deltaMag) * fAttr(deltaMag);
e.target.disp.z += (delta.z / deltaMag) * fAttr(deltaMag);
}.bind(this));
// apply gravitational forces
this.vertices.forEach(function(v, i) {
// three.js co-ordinate space is the center, so we
// just need the regular magnitude for each vertices position
var deltaMag = Math.max(eps, v.pos.length());
var gravityForce = 0.1 * k * gravity * deltaMag;
v.disp.x -= (v.pos.x / deltaMag) * gravityForce;
v.disp.y -= (v.pos.y / deltaMag) * gravityForce;
v.disp.z -= (v.pos.z / deltaMag) * gravityForce;
});
this.vertices.forEach(function(v) {
// limit displacement to temperature t
var dispMag = v.disp.length();
v.pos.x += (v.disp.x / dispMag) * Math.min(Math.abs(v.disp.x), t);
v.pos.y += (v.disp.y / dispMag) * Math.min(Math.abs(v.disp.y), t);
v.pos.z += (v.disp.z / dispMag) * Math.min(Math.abs(v.disp.z), t);
// keep vertices within the world space
v.pos.x = clamp(v.pos.x, this.minX + width / 10, this.maxX - width / 10);
v.pos.y = clamp(v.pos.y, this.minY + height / 10, this.maxY - height / 10);
v.pos.z = clamp(v.pos.z, this.minZ + depth / 10, this.maxZ - depth / 10);
}.bind(this));
t -= dt;
}
};
/**
* Clear the current graph and generate a Erdos-Renyi graph
* with n vertices and m edges.
*
* @param n
* @param m
*/
Graph.prototype.initRandomGraph = function(n, m) {
var i;
this.vertices = [];
this.edges = [];
// add nodes to graph
for(i = 0; i < n; i++) {
var newPos = new THREE.Vector3(
this._getCenterValue(this.minX, this.maxX),
this._getCenterValue(this.minY, this.maxY),
this._getCenterValue(this.minZ, this.maxZ));
var newVertex = new Vertex(i, newPos);
this.vertices.push(newVertex);
}
// create a list of every possible edge, randomise this
// and choose the first m... inefficient but simple way
// to generate a random fixed number of edges
var allEdges = [];
for(i = 0; i < n; i++) {
for(var j = i + 1; j < n; j++) {
allEdges.push([i, j]);
}
}
// select the first m edges as our edges, add them.
allEdges = shuffleArray(allEdges);
allEdges = allEdges.splice(0, m);
allEdges.forEach(function(e) {
this.addEdge(this.vertices[e[0]], this.vertices[e[1]]);
}.bind(this));
};
/**
* Clear the current graph and generate a random graph with community structure.
*
* @param n
* @param numberGroups
* @param withinP
* @param betweenP
*/
Graph.prototype.initCommunityGraph = function(n, numberGroups, withinP, betweenP) {
this.vertices = [];
this.edges = [];
var i, j;
// create set of vertices and assign them to random communities
for(i = 0; i < n; i++) {
var newPos = new THREE.Vector3(
this._getCenterValue(this.minX, this.maxX),
this._getCenterValue(this.minY, this.maxY),
this._getCenterValue(this.minZ, this.maxZ));
var newVertex = new Vertex(i, newPos);
newVertex.community = randInt(0, numberGroups);
this.vertices.push(newVertex);
}
// create edges between vertices using given probabilities
var source, target, rand;
for(i = 0; i < n; i++) {
for(j = i + 1; j < n; j++) {
source = this.vertices[i];
target = this.vertices[j];
rand = Math.random();
if(source.community == target.community) {
if(rand < withinP) this.addEdge(source, target);
}
else {
if(rand < betweenP) this.addEdge(source, target);
}
}
}
};
/**
* Generate a graph with a power-law degree distribution
* using the Barabási–Albert model.
*
* See: https://en.wikipedia.org/wiki/Barab%C3%A1si%E2%80%93Albert_model
*
* @param n
*/
Graph.prototype.initPowerLawGraph = function(n) {
this.vertices = [];
this.edges = [];
var i, newVertex, newPos;
// create set of vertices and assign them to random communities
for(i = 0; i < 4; i++) {
newPos = new THREE.Vector3(
this._getCenterValue(this.minX, this.maxX),
this._getCenterValue(this.minY, this.maxY),
this._getCenterValue(this.minZ, this.maxZ));
newVertex = new Vertex(i, newPos);
this.vertices.push(newVertex);
}
this.addEdge(this.vertices[0], this.vertices[1]);
this.addEdge(this.vertices[0], this.vertices[2]);
this.addEdge(this.vertices[0], this.vertices[3]);
// add the remaining edges using the preferential attachment model
for(i = 4; i < n; i++) {
var targetID = this._getPAVertex();
newPos = new THREE.Vector3(
this._getCenterValue(this.minX, this.maxX),
this._getCenterValue(this.minY, this.maxY),
this._getCenterValue(this.minZ, this.maxZ));
newVertex = new Vertex(i, newPos);
this.vertices.push(newVertex);
this.addEdge(newVertex, this.vertices[targetID]);
}
};
/**
* Add all the meshes belonging to this graph from the given THREE.js scene.
*
* @param scene
*/
Graph.prototype.initMeshes = function(scene) {
this.edges.forEach(function(e) {
scene.add(e.mesh);
}.bind(this));
this.vertices.forEach(function(v) {
scene.add(v.mesh);
}.bind(this));
};
/**
* Remove all the meshes belonging to this graph from the given THREE.js scene.
*
* @param scene
*/
Graph.prototype.removeMeshes = function(scene) {
this.edges.forEach(function(e) {
scene.remove(e.mesh);
});
this.vertices.forEach(function(v) {
scene.remove(v.mesh);
});
};
/**
* Update position of all graph meshes.
*
*/
Graph.prototype.update = function() {
this.edges.forEach(function(e) {
e.update();
});
this.vertices.forEach(function(v) {
v.update();
});
};
/**
* Generates a random starting position. This is the midpoint of the supplied min and max
* value, perturbed by 10% of the range of the values.
*
* @private
*/
Graph.prototype._getCenterValue = function(min, max) {
var sign = Math.random() > 0.5 ? -1 : 1;
var range = max - min;
var mid = (max + min) / 2;
return mid + Math.random() * sign * range / 10;
};
/**
* This method is used for selecting which vertex should received an edge from
* a newly added edge generating a graph with a preferential attachment model.
*
* A vertex is randomly selected based on it's degree value.
*
* @private
*/
Graph.prototype._getPAVertex = function() {
// get the total number of degrees for the full set of vertices
var totalDegree = this.vertices.reduce(function(pre, cur) {
return pre + cur.degree;
}, 0);
// update the attachment probability for every vertex
this.vertices.forEach(function(v) {
v.prob = v.degree / totalDegree;
});
// sort the vertices ascending by their attachment probabilities
// copy the array so the original array isn't mutated
var sorted = this.vertices.slice(0);
sorted.sort(function(a, b) {
return a.prob - b.prob;
});
// randomly choose a vertex based on the cumulative distribution
// we've just created!
var rand = Math.random();
var cumulativeProb = 0;
for(var i = 0; i < sorted.length; i++) {
cumulativeProb += sorted[i].prob;
if (rand < cumulativeProb) {
return i;
}
}
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>3D Fructerman-Reingold Layouts</title>
<style>
body {
text-align: center;
}
#buttons {
padding: 5px 5px 5px 5px;
}
</style>
</head>
<body>
<div id="buttons">
<button id="random-button">Random</button>
<button id="community-button">Community</button>
<button id="powerlaw-button">Power Law</button>
</div>
</body>
<script src="Common.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.min.js"></script>
<script src="./OrbitControls.js"></script>
<script src="./Graph.js"></script>
<script src="./Vertex.js"></script>
<script src="./Edge.js"></script>
<script>
var width = 500;
var height = 500;
var aspectRatio = width / height;
var viewSize = 20;
var worldMax = 200;
var numberOfVertices = 100;
var numberOfEdges = 200;
var layoutIterations = 40;
var layoutScale = 25;
var layoutGravity = 1;
var scene = new THREE.Scene();
// initialise the camera
var camera = new THREE.OrthographicCamera(
-aspectRatio * viewSize / 2, aspectRatio * viewSize / 2,
viewSize / 2, -viewSize / 2, -500, 500);
camera.position.set(1, 1, 1).normalize();
camera.zoom = 0.09;
camera.updateProjectionMatrix();
// initialise the lighting
var ambientLight = new THREE.AmbientLight(0x404040, 3.0);
var light = new THREE.DirectionalLight(0xffffff, 0.75);
light.position.set(1, 1, 1).normalize();
scene.add(ambientLight);
scene.add(light);
// initialise the orbital controls
var controls = new THREE.OrbitControls(camera);
controls.enablePan = true;
controls.enableRotate = true;
// create a renderer and append to the dom
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
renderer.setClearColor(0x141311, 1);
document.body.appendChild(renderer.domElement);
// create a new graph object
var graph = new Graph(-worldMax, worldMax, -worldMax, worldMax, -worldMax, worldMax);
// start the show!
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
// attach the event listeners to create new graphs
// add event listeners to button
document.getElementById('random-button').addEventListener('click', function() {
graph.removeMeshes(scene);
graph.initRandomGraph(numberOfVertices, numberOfEdges);
graph.applyLayout(layoutIterations, layoutScale, layoutGravity);
graph.initMeshes(scene);
graph.update();
});
document.getElementById('community-button').addEventListener('click', function() {
graph.removeMeshes(scene);
graph.initCommunityGraph(numberOfVertices, 4, 0.35, 0.005);
graph.applyLayout(layoutIterations, layoutScale, layoutGravity);
graph.initMeshes(scene);
graph.update();
});
document.getElementById('powerlaw-button').addEventListener('click', function() {
graph.removeMeshes(scene);
graph.initPowerLawGraph(numberOfVertices);
graph.applyLayout(layoutIterations, layoutScale, layoutGravity);
graph.initMeshes(scene);
graph.update();
});
</script>
</html>

MIT License

Copyright (c) 2017 Adrian Hecker

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
* @author erich666 / http://erichaines.com
*/
// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
// Orbit - left mouse / touch: one finger move
// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
// Pan - right mouse, or arrow keys / touch: three finger swipe
THREE.OrbitControls = function ( object, domElement ) {
this.object = object;
this.domElement = ( domElement !== undefined ) ? domElement : document;
// Set to false to disable this control
this.enabled = true;
// "target" sets the location of focus, where the object orbits around
this.target = new THREE.Vector3();
// How far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = 0;
this.maxDistance = Infinity;
// How far you can zoom in and out ( OrthographicCamera only )
this.minZoom = 0;
this.maxZoom = Infinity;
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
this.minAzimuthAngle = - Infinity; // radians
this.maxAzimuthAngle = Infinity; // radians
// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
this.enableDamping = false;
this.dampingFactor = 0.25;
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true;
this.zoomSpeed = 1.0;
// Set to false to disable rotating
this.enableRotate = true;
this.rotateSpeed = 1.0;
// Set to false to disable panning
this.enablePan = true;
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, you must call controls.update() in your animation loop
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
// Set to false to disable use of the keys
this.enableKeys = true;
// The four arrow keys
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
// Mouse buttons
this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
//
// public methods
//
this.getPolarAngle = function () {
return spherical.phi;
};
this.getAzimuthalAngle = function () {
return spherical.theta;
};
this.reset = function () {
scope.target.copy( scope.target0 );
scope.object.position.copy( scope.position0 );
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent( changeEvent );
scope.update();
state = STATE.NONE;
};
// this method is exposed, but perhaps it would be better if we can make it private...
this.update = function () {
var offset = new THREE.Vector3();
// so camera.up is the orbit axis
var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
var quatInverse = quat.clone().inverse();
var lastPosition = new THREE.Vector3();
var lastQuaternion = new THREE.Quaternion();
return function update() {
var position = scope.object.position;
offset.copy( position ).sub( scope.target );
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion( quat );
// angle from z-axis around y-axis
spherical.setFromVector3( offset );
if ( scope.autoRotate && state === STATE.NONE ) {
rotateLeft( getAutoRotationAngle() );
}
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
// restrict theta to be between desired limits
spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
// restrict phi to be between desired limits
spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
spherical.makeSafe();
spherical.radius *= scale;
// restrict radius to be between desired limits
spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
// move target to panned location
scope.target.add( panOffset );
offset.setFromSpherical( spherical );
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion( quatInverse );
position.copy( scope.target ).add( offset );
scope.object.lookAt( scope.target );
if ( scope.enableDamping === true ) {
sphericalDelta.theta *= ( 1 - scope.dampingFactor );
sphericalDelta.phi *= ( 1 - scope.dampingFactor );
} else {
sphericalDelta.set( 0, 0, 0 );
}
scale = 1;
panOffset.set( 0, 0, 0 );
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if ( zoomChanged ||
lastPosition.distanceToSquared( scope.object.position ) > EPS ||
8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
scope.dispatchEvent( changeEvent );
lastPosition.copy( scope.object.position );
lastQuaternion.copy( scope.object.quaternion );
zoomChanged = false;
return true;
}
return false;
};
}();
this.dispose = function () {
scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
window.removeEventListener( 'keydown', onKeyDown, false );
//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
};
//
// internals
//
var scope = this;
var changeEvent = { type: 'change' };
var startEvent = { type: 'start' };
var endEvent = { type: 'end' };
var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 };
var state = STATE.NONE;
var EPS = 0.000001;
// current position in spherical coordinates
var spherical = new THREE.Spherical();
var sphericalDelta = new THREE.Spherical();
var scale = 1;
var panOffset = new THREE.Vector3();
var zoomChanged = false;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var panStart = new THREE.Vector2();
var panEnd = new THREE.Vector2();
var panDelta = new THREE.Vector2();
var dollyStart = new THREE.Vector2();
var dollyEnd = new THREE.Vector2();
var dollyDelta = new THREE.Vector2();
function getAutoRotationAngle() {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
function getZoomScale() {
return Math.pow( 0.95, scope.zoomSpeed );
}
function rotateLeft( angle ) {
sphericalDelta.theta -= angle;
}
function rotateUp( angle ) {
sphericalDelta.phi -= angle;
}
var panLeft = function () {
var v = new THREE.Vector3();
return function panLeft( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
v.multiplyScalar( - distance );
panOffset.add( v );
};
}();
var panUp = function () {
var v = new THREE.Vector3();
return function panUp( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix
v.multiplyScalar( distance );
panOffset.add( v );
};
}();
// deltaX and deltaY are in pixels; right and down are positive
var pan = function () {
var offset = new THREE.Vector3();
return function pan( deltaX, deltaY ) {
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
if ( scope.object instanceof THREE.PerspectiveCamera ) {
// perspective
var position = scope.object.position;
offset.copy( position ).sub( scope.target );
var targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
// we actually don't use screenWidth, since perspective camera is fixed to screen height
panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
// orthographic
panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
} else {
// camera neither orthographic nor perspective
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
scope.enablePan = false;
}
};
}();
function dollyIn( dollyScale ) {
if ( scope.object instanceof THREE.PerspectiveCamera ) {
scale /= dollyScale;
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
function dollyOut( dollyScale ) {
if ( scope.object instanceof THREE.PerspectiveCamera ) {
scale *= dollyScale;
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else {
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
//
// event callbacks - update the object state
//
function handleMouseDownRotate( event ) {
//console.log( 'handleMouseDownRotate' );
rotateStart.set( event.clientX, event.clientY );
}
function handleMouseDownDolly( event ) {
//console.log( 'handleMouseDownDolly' );
dollyStart.set( event.clientX, event.clientY );
}
function handleMouseDownPan( event ) {
//console.log( 'handleMouseDownPan' );
panStart.set( event.clientX, event.clientY );
}
function handleMouseMoveRotate( event ) {
//console.log( 'handleMouseMoveRotate' );
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart );
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
// rotating across whole screen goes 360 degrees around
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
// rotating up and down along whole screen attempts to go 360, but limited to 180
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleMouseMoveDolly( event ) {
//console.log( 'handleMouseMoveDolly' );
dollyEnd.set( event.clientX, event.clientY );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyIn( getZoomScale() );
} else if ( dollyDelta.y < 0 ) {
dollyOut( getZoomScale() );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleMouseMovePan( event ) {
//console.log( 'handleMouseMovePan' );
panEnd.set( event.clientX, event.clientY );
panDelta.subVectors( panEnd, panStart );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleMouseUp( event ) {
// console.log( 'handleMouseUp' );
}
function handleMouseWheel( event ) {
// console.log( 'handleMouseWheel' );
if ( event.deltaY < 0 ) {
dollyOut( getZoomScale() );
} else if ( event.deltaY > 0 ) {
dollyIn( getZoomScale() );
}
scope.update();
}
function handleKeyDown( event ) {
//console.log( 'handleKeyDown' );
switch ( event.keyCode ) {
case scope.keys.UP:
pan( 0, scope.keyPanSpeed );
scope.update();
break;
case scope.keys.BOTTOM:
pan( 0, - scope.keyPanSpeed );
scope.update();
break;
case scope.keys.LEFT:
pan( scope.keyPanSpeed, 0 );
scope.update();
break;
case scope.keys.RIGHT:
pan( - scope.keyPanSpeed, 0 );
scope.update();
break;
}
}
function handleTouchStartRotate( event ) {
//console.log( 'handleTouchStartRotate' );
rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
}
function handleTouchStartDolly( event ) {
//console.log( 'handleTouchStartDolly' );
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
var distance = Math.sqrt( dx * dx + dy * dy );
dollyStart.set( 0, distance );
}
function handleTouchStartPan( event ) {
//console.log( 'handleTouchStartPan' );
panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
}
function handleTouchMoveRotate( event ) {
//console.log( 'handleTouchMoveRotate' );
rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
rotateDelta.subVectors( rotateEnd, rotateStart );
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
// rotating across whole screen goes 360 degrees around
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
// rotating up and down along whole screen attempts to go 360, but limited to 180
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleTouchMoveDolly( event ) {
//console.log( 'handleTouchMoveDolly' );
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
var distance = Math.sqrt( dx * dx + dy * dy );
dollyEnd.set( 0, distance );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyOut( getZoomScale() );
} else if ( dollyDelta.y < 0 ) {
dollyIn( getZoomScale() );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleTouchMovePan( event ) {
//console.log( 'handleTouchMovePan' );
panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
panDelta.subVectors( panEnd, panStart );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleTouchEnd( event ) {
//console.log( 'handleTouchEnd' );
}
//
// event handlers - FSM: listen for events and reset state
//
function onMouseDown( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
if ( event.button === scope.mouseButtons.ORBIT ) {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
} else if ( event.button === scope.mouseButtons.ZOOM ) {
if ( scope.enableZoom === false ) return;
handleMouseDownDolly( event );
state = STATE.DOLLY;
} else if ( event.button === scope.mouseButtons.PAN ) {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
}
if ( state !== STATE.NONE ) {
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
scope.dispatchEvent( startEvent );
}
}
function onMouseMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
if ( state === STATE.ROTATE ) {
if ( scope.enableRotate === false ) return;
handleMouseMoveRotate( event );
} else if ( state === STATE.DOLLY ) {
if ( scope.enableZoom === false ) return;
handleMouseMoveDolly( event );
} else if ( state === STATE.PAN ) {
if ( scope.enablePan === false ) return;
handleMouseMovePan( event );
}
}
function onMouseUp( event ) {
if ( scope.enabled === false ) return;
handleMouseUp( event );
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
scope.dispatchEvent( endEvent );
state = STATE.NONE;
}
function onMouseWheel( event ) {
if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
event.preventDefault();
event.stopPropagation();
handleMouseWheel( event );
scope.dispatchEvent( startEvent ); // not sure why these are here...
scope.dispatchEvent( endEvent );
}
function onKeyDown( event ) {
if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
handleKeyDown( event );
}
function onTouchStart( event ) {
if ( scope.enabled === false ) return;
switch ( event.touches.length ) {
case 1: // one-fingered touch: rotate
if ( scope.enableRotate === false ) return;
handleTouchStartRotate( event );
state = STATE.TOUCH_ROTATE;
break;
case 2: // two-fingered touch: dolly
if ( scope.enableZoom === false ) return;
handleTouchStartDolly( event );
state = STATE.TOUCH_DOLLY;
break;
case 3: // three-fingered touch: pan
if ( scope.enablePan === false ) return;
handleTouchStartPan( event );
state = STATE.TOUCH_PAN;
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( startEvent );
}
}
function onTouchMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
event.stopPropagation();
switch ( event.touches.length ) {
case 1: // one-fingered touch: rotate
if ( scope.enableRotate === false ) return;
if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?...
handleTouchMoveRotate( event );
break;
case 2: // two-fingered touch: dolly
if ( scope.enableZoom === false ) return;
if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?...
handleTouchMoveDolly( event );
break;
case 3: // three-fingered touch: pan
if ( scope.enablePan === false ) return;
if ( state !== STATE.TOUCH_PAN ) return; // is this needed?...
handleTouchMovePan( event );
break;
default:
state = STATE.NONE;
}
}
function onTouchEnd( event ) {
if ( scope.enabled === false ) return;
handleTouchEnd( event );
scope.dispatchEvent( endEvent );
state = STATE.NONE;
}
function onContextMenu( event ) {
event.preventDefault();
}
//
scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
window.addEventListener( 'keydown', onKeyDown, false );
// force an update at start
this.update();
};
THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
Object.defineProperties( THREE.OrbitControls.prototype, {
center: {
get: function () {
console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
return this.target;
}
},
// backward compatibility
noZoom: {
get: function () {
console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
return ! this.enableZoom;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
this.enableZoom = ! value;
}
},
noRotate: {
get: function () {
console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
return ! this.enableRotate;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
this.enableRotate = ! value;
}
},
noPan: {
get: function () {
console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
return ! this.enablePan;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
this.enablePan = ! value;
}
},
noKeys: {
get: function () {
console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
return ! this.enableKeys;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
this.enableKeys = ! value;
}
},
staticMoving: {
get: function () {
console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
return ! this.enableDamping;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
this.enableDamping = ! value;
}
},
dynamicDampingFactor: {
get: function () {
console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
return this.dampingFactor;
},
set: function ( value ) {
console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
this.dampingFactor = value;
}
}
} );
'use strict';
/* globals THREE */
var vertexGeometry = new THREE.SphereGeometry(2, 16, 16);
var vertexMaterial = new THREE.MeshLambertMaterial({
color: 0xB7B7B7
});
function Vertex(id, pos) {
this.id = id;
this.pos = pos;
this.disp = new THREE.Vector3();
this.community = 0;
this.degree = 0;
this.mesh = new THREE.Mesh(vertexGeometry, vertexMaterial);
this.mesh.position.copy(this.pos);
}
/**
* Call this method to update the Vertex's mesh to its current
* position co-ordinates.
*
*/
Vertex.prototype.update = function() {
this.mesh.position.copy(this.pos);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment