├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── build ├── README.md └── graph.min.js ├── examples ├── simple_graph │ ├── index.html │ └── simple_graph.js └── sphere_graph │ ├── index.html │ └── sphere_graph.js ├── index.html ├── package.json ├── src ├── graph.js ├── layouts │ └── force-directed-layout.js └── utils │ ├── Label.js │ ├── ObjectSelection.js │ ├── Stats.js │ ├── TrackballControls.js │ └── Vector3.js └── webgl-frameworks ├── Three.js └── three.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | jshint: { 5 | all: ['Gruntfile.js', 'src/graph.js', 'src/layouts/*.js', 'src/utils/Label.js', 'src/utils/ObjectSelection.js'] 6 | }, 7 | uglify: { 8 | options: { 9 | banner: '/*! <%= pkg.name %> <%= pkg.version %> */\n' 10 | }, 11 | graphVisualization: { 12 | files: { 13 | 'build/graph.min.js': ['webgl-frameworks/three.min.js', 'src/graph.js', 'src/utils/*.js', 'src/layouts/*.js'] 14 | } 15 | } 16 | } 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-contrib-jshint'); 20 | grunt.loadNpmTasks('grunt-contrib-uglify'); 21 | 22 | grunt.registerTask('default', ['jshint', 'uglify']); 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /*! 2 | MIT License 3 | 4 | Copyright (c) 2011 David Piegza 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of 11 | the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Notice: This repo is not maintained anymore but PRs are still welcome. 2 | 3 | # Graph-Visualization 4 | 5 | This project is about 3D graph visualization with WebGL. The aim of this project is to evaluate the possibilities of graph drawing in WebGL. 6 | 7 | It uses [Three.js](https://threejs.org/) for drawing and currently supports a force directed layout. 8 | 9 | 10 | ### Run the example 11 | 12 | You can see the examples at http://davidpiegza.github.io/Graph-Visualization/index.html or: 13 | 14 | 1. Clone or download the project 15 | 2. Open the index.html in a WebGL-compatible browser 16 | 17 | The `examples` folder contains all examples. 18 | 19 | ## Project Description 20 | 21 | The project consists of 22 | 23 | - a graph structure 24 | - a graph layout implementation 25 | - and a graph drawing implementation 26 | 27 | ### Graph Structure 28 | 29 | This is implemented in [src/graph.js](https://github.com/davidpiegza/Graph-Visualization/blob/master/src/graph.js). 30 | 31 | Usage: 32 | 33 | ```js 34 | // create a graph with maximum number of nodes (optional) 35 | var graph = new GRAPHVIS.Graph({limit: 100}); 36 | // create nodes with an id 37 | var node1 = new GRAPHVIS.Node(1); 38 | var node2 = new GRAPHVIS.Node(2); 39 | // add nodes to the graph 40 | graph.addNode(node1); 41 | graph.addNode(node2); 42 | // create edge between nodes 43 | graph.addEdge(node1, node2); 44 | ``` 45 | 46 | Node: 47 | 48 | A node has the properties 49 | 50 | - `ID` 51 | - `nodesTo`, Array with connected nodes 52 | - `nodesFrom`, Array with connected nodes 53 | - `position`, Object for x, y, z position, default is {} 54 | - `data`, Object with further properties, e.g. properties for a graph layout 55 | 56 | For more details have a look at the [source code](https://github.com/davidpiegza/Graph-Visualization/blob/master/src/graph.js). 57 | 58 | ### Graph Layout 59 | 60 | A graph layout has the basic structure: 61 | 62 | ```js 63 | var Layout = Layout || {}; 64 | Layout.ForceDirected = function(graph, options) { 65 | this.init = function() { 66 | ... 67 | }; 68 | 69 | this.generate = function() { 70 | ... 71 | }; 72 | } 73 | ``` 74 | 75 | The `init()` function is called after graph creation, the `generate()` function is called on each render-call. 76 | 77 | The graph layout gets the created graph and calculates new positions for the nodes. The `generate()` function is called repeatedly, so there must be a stop condition after finished calculation. 78 | 79 | The graph layout may extend the nodes and edges with custom properties in the data object. 80 | 81 | See [force-directed-layout.js](https://github.com/davidpiegza/Graph-Visualization/blob/master/src/layouts/force-directed-layout.js) for example usage. 82 | 83 | 84 | ## Contribution 85 | 86 | It would be great to have more examples of force-directed graphs or other 3d graph layouts. To add a new example, just copy one of the existing [examples](https://github.com/davidpiegza/Graph-Visualization/blob/master/examples), add a new `Drawing` object and update the `index.html`. 87 | 88 | ***Check out the [open issues](https://github.com/davidpiegza/Graph-Visualization/issues) for a specific task***. 89 | 90 | This project uses [Grunt](http://gruntjs.com/) to run several tasks in development. You should have `npm` and `grunt` installed. To install `grunt` run 91 | 92 | npm install -g grunt-cli 93 | 94 | And to install all dependencies run 95 | 96 | npm install 97 | 98 | For more info check the [Grunt - Getting started guide](http://gruntjs.com/getting-started). 99 | 100 | If you added some changes, run `grunt` to check the code. 101 | 102 | ## Changelog 103 | 104 | See [releases](https://github.com/davidpiegza/Graph-Visualization/releases). 105 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Create minified version 2 | 3 | Using `grunt`: 4 | 5 | npm install -g grunt-cli 6 | grunt 7 | -------------------------------------------------------------------------------- /examples/simple_graph/index.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | Graph Visualization 19 | 20 | 21 | 22 | 28 | 29 | 52 | 53 | 54 |
55 |
56 |

57 | 58 | 59 |

60 |

61 | 62 |

63 |
64 |
65 | 66 |
67 | Rotate: Left Mouse Button and Move
68 | Zoom: Press Key S + Left Mouse Button and Move
69 | Drag: Press Key D + Left Mouse Button and Move 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/simple_graph/simple_graph.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza 3 | 4 | Implements a simple graph drawing with force-directed placement in 2D and 3D. 5 | 6 | It uses the force-directed-layout implemented in: 7 | https://github.com/davidpiegza/Graph-Visualization/blob/master/layouts/force-directed-layout.js 8 | 9 | Drawing is done with Three.js: http://github.com/mrdoob/three.js 10 | 11 | To use this drawing, include the graph-min.js file and create a SimpleGraph object: 12 | 13 | 14 | 15 | 16 | Graph Visualization 17 | 18 | 19 | 20 | 21 | 22 | 23 | Parameters: 24 | options = { 25 | layout: "2d" or "3d" 26 | 27 | showStats: , displays FPS box 28 | showInfo: , displays some info on the graph and layout 29 | The info box is created as
, it must be 30 | styled and positioned with CSS. 31 | 32 | 33 | selection: , enables selection of nodes on mouse over (it displays some info 34 | when the showInfo flag is set) 35 | 36 | 37 | limit: , maximum number of nodes 38 | 39 | numNodes: - sets the number of nodes to create. 40 | numEdges: - sets the maximum number of edges for a node. A node will have 41 | 1 to numEdges edges, this is set randomly. 42 | } 43 | 44 | 45 | Feel free to contribute a new drawing! 46 | 47 | */ 48 | 49 | var Drawing = Drawing || {}; 50 | 51 | Drawing.SimpleGraph = function(options) { 52 | options = options || {}; 53 | 54 | this.layout = options.layout || "2d"; 55 | this.layout_options = options.graphLayout || {}; 56 | this.show_stats = options.showStats || false; 57 | this.show_info = options.showInfo || false; 58 | this.show_labels = options.showLabels || false; 59 | this.selection = options.selection || false; 60 | this.limit = options.limit || 10; 61 | this.nodes_count = options.numNodes || 20; 62 | this.edges_count = options.numEdges || 10; 63 | 64 | var camera, controls, scene, renderer, interaction, geometry, object_selection; 65 | var stats; 66 | var info_text = {}; 67 | var graph = new GRAPHVIS.Graph({limit: options.limit}); 68 | 69 | var geometries = []; 70 | 71 | var that=this; 72 | 73 | init(); 74 | createGraph(); 75 | animate(); 76 | 77 | function init() { 78 | // Three.js initialization 79 | renderer = new THREE.WebGLRenderer({alpha: true, antialias: true}); 80 | renderer.setPixelRatio(window.devicePixelRatio); 81 | renderer.setSize(window.innerWidth, window.innerHeight); 82 | 83 | 84 | camera = new THREE.PerspectiveCamera(40, window.innerWidth/window.innerHeight, 1, 1000000); 85 | camera.position.z = 10000; 86 | 87 | controls = new THREE.TrackballControls(camera); 88 | 89 | controls.rotateSpeed = 0.5; 90 | controls.zoomSpeed = 5.2; 91 | controls.panSpeed = 1; 92 | 93 | controls.noZoom = false; 94 | controls.noPan = false; 95 | 96 | controls.staticMoving = false; 97 | controls.dynamicDampingFactor = 0.3; 98 | 99 | controls.keys = [ 65, 83, 68 ]; 100 | 101 | controls.addEventListener('change', render); 102 | 103 | scene = new THREE.Scene(); 104 | 105 | // Node geometry 106 | if(that.layout === "3d") { 107 | geometry = new THREE.SphereGeometry(30); 108 | } else { 109 | geometry = new THREE.BoxGeometry( 50, 50, 0 ); 110 | } 111 | 112 | // Create node selection, if set 113 | if(that.selection) { 114 | object_selection = new THREE.ObjectSelection({ 115 | domElement: renderer.domElement, 116 | selected: function(obj) { 117 | // display info 118 | if(obj !== null) { 119 | info_text.select = "Object " + obj.id; 120 | } else { 121 | delete info_text.select; 122 | } 123 | }, 124 | clicked: function(obj) { 125 | } 126 | }); 127 | } 128 | 129 | document.body.appendChild( renderer.domElement ); 130 | 131 | // Stats.js 132 | if(that.show_stats) { 133 | stats = new Stats(); 134 | stats.domElement.style.position = 'absolute'; 135 | stats.domElement.style.top = '0px'; 136 | document.body.appendChild( stats.domElement ); 137 | } 138 | 139 | // Create info box 140 | if(that.show_info) { 141 | var info = document.createElement("div"); 142 | var id_attr = document.createAttribute("id"); 143 | id_attr.nodeValue = "graph-info"; 144 | info.setAttributeNode(id_attr); 145 | document.body.appendChild( info ); 146 | } 147 | } 148 | 149 | 150 | /** 151 | * Creates a graph with random nodes and edges. 152 | * Number of nodes and edges can be set with 153 | * numNodes and numEdges. 154 | */ 155 | function createGraph() { 156 | 157 | var node = new GRAPHVIS.Node(0); 158 | node.data.title = "This is node " + node.id; 159 | graph.addNode(node); 160 | drawNode(node); 161 | 162 | var nodes = []; 163 | nodes.push(node); 164 | 165 | var steps = 1; 166 | while(nodes.length !== 0 && steps < that.nodes_count) { 167 | node = nodes.shift(); 168 | 169 | var numEdges = randomFromTo(1, that.edges_count); 170 | for(var i=1; i <= numEdges; i++) { 171 | var target_node = new GRAPHVIS.Node(i*steps); 172 | if(graph.addNode(target_node)) { 173 | target_node.data.title = "This is node " + target_node.id; 174 | 175 | drawNode(target_node); 176 | nodes.push(target_node); 177 | if(graph.addEdge(node, target_node)) { 178 | drawEdge(node, target_node); 179 | } 180 | } 181 | } 182 | steps++; 183 | } 184 | 185 | that.layout_options.width = that.layout_options.width || 2000; 186 | that.layout_options.height = that.layout_options.height || 2000; 187 | that.layout_options.iterations = that.layout_options.iterations || 100000; 188 | that.layout_options.layout = that.layout_options.layout || that.layout; 189 | graph.layout = new Layout.ForceDirected(graph, that.layout_options); 190 | graph.layout.init(); 191 | info_text.nodes = "Nodes " + graph.nodes.length; 192 | info_text.edges = "Edges " + graph.edges.length; 193 | } 194 | 195 | 196 | /** 197 | * Create a node object and add it to the scene. 198 | */ 199 | function drawNode(node) { 200 | var draw_object = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: Math.random() * 0xe0e0e0, opacity: 0.8 } ) ); 201 | var label_object; 202 | 203 | if(that.show_labels) { 204 | if(node.data.title !== undefined) { 205 | label_object = new THREE.Label(node.data.title); 206 | } else { 207 | label_object = new THREE.Label(node.id); 208 | } 209 | node.data.label_object = label_object; 210 | scene.add( node.data.label_object ); 211 | } 212 | 213 | var area = 5000; 214 | draw_object.position.x = Math.floor(Math.random() * (area + area + 1) - area); 215 | draw_object.position.y = Math.floor(Math.random() * (area + area + 1) - area); 216 | 217 | if(that.layout === "3d") { 218 | draw_object.position.z = Math.floor(Math.random() * (area + area + 1) - area); 219 | } 220 | 221 | draw_object.id = node.id; 222 | node.data.draw_object = draw_object; 223 | node.position = draw_object.position; 224 | scene.add( node.data.draw_object ); 225 | } 226 | 227 | 228 | /** 229 | * Create an edge object (line) and add it to the scene. 230 | */ 231 | function drawEdge(source, target) { 232 | material = new THREE.LineBasicMaterial({ color: 0x606060 }); 233 | 234 | var tmp_geo = new THREE.Geometry(); 235 | tmp_geo.vertices.push(source.data.draw_object.position); 236 | tmp_geo.vertices.push(target.data.draw_object.position); 237 | 238 | line = new THREE.LineSegments( tmp_geo, material ); 239 | line.scale.x = line.scale.y = line.scale.z = 1; 240 | line.originalScale = 1; 241 | 242 | // NOTE: Deactivated frustumCulled, otherwise it will not draw all lines (even though 243 | // it looks like the lines are in the view frustum). 244 | line.frustumCulled = false; 245 | 246 | geometries.push(tmp_geo); 247 | 248 | scene.add( line ); 249 | } 250 | 251 | 252 | function animate() { 253 | requestAnimationFrame( animate ); 254 | controls.update(); 255 | render(); 256 | if(that.show_info) { 257 | printInfo(); 258 | } 259 | } 260 | 261 | 262 | function render() { 263 | var i, length, node; 264 | 265 | // Generate layout if not finished 266 | if(!graph.layout.finished) { 267 | info_text.calc = "Calculating layout..."; 268 | graph.layout.generate(); 269 | } else { 270 | info_text.calc = ""; 271 | } 272 | 273 | // Update position of lines (edges) 274 | for(i=0; i 5 | 6 | 7 | Graph Visualization 8 | 9 | 10 | 11 | 12 | 13 | --> 14 | 15 | 16 | 17 | 18 | Graph Visualization 19 | 20 | 21 | 22 | 28 | 29 | 52 | 53 | 54 |
55 |
56 |

57 | 58 | 59 |

60 |

61 | 62 |

63 |
64 |
65 | 66 |
67 | Rotate: Left Mouse Button and Move
68 | Zoom: Press Key S + Left Mouse Button and Move
69 | Drag: Press Key D + Left Mouse Button and Move 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/sphere_graph/sphere_graph.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza 3 | 4 | Implements a sphere graph drawing with force-directed placement. 5 | 6 | It uses the force-directed-layout implemented in: 7 | https://github.com/davidpiegza/Graph-Visualization/blob/master/layouts/force-directed-layout.js 8 | 9 | Drawing is done with Three.js: http://github.com/mrdoob/three.js 10 | 11 | To use this drawing, include the graph-min.js file and create a SphereGraph object: 12 | 13 | 14 | 15 | 16 | Graph Visualization 17 | 18 | 19 | 20 | 21 | 22 | 23 | Parameters: 24 | options = { 25 | layout: "2d" or "3d" 26 | 27 | showStats: , displays FPS box 28 | showInfo: , displays some info on the graph and layout 29 | The info box is created as
, it must be 30 | styled and positioned with CSS. 31 | 32 | 33 | selection: , enables selection of nodes on mouse over (it displays some info 34 | when the showInfo flag is set) 35 | 36 | 37 | limit: , maximum number of nodes 38 | 39 | numNodes: - sets the number of nodes to create. 40 | numEdges: - sets the maximum number of edges for a node. A node will have 41 | 1 to numEdges edges, this is set randomly. 42 | } 43 | 44 | 45 | Feel free to contribute a new drawing! 46 | 47 | */ 48 | 49 | 50 | var Drawing = Drawing || {}; 51 | 52 | Drawing.SphereGraph = function(options) { 53 | options = options || {}; 54 | 55 | this.layout = options.layout || "2d"; 56 | this.show_stats = options.showStats || false; 57 | this.show_info = options.showInfo || false; 58 | this.selection = options.selection || false; 59 | this.limit = options.limit || 10; 60 | this.nodes_count = options.numNodes || 20; 61 | this.edges_count = options.numEdges || 10; 62 | 63 | var camera, controls, scene, renderer, interaction, geometry, object_selection; 64 | var stats; 65 | var info_text = {}; 66 | var graph = new GRAPHVIS.Graph({limit: options.limit}); 67 | 68 | var geometries = []; 69 | 70 | var sphere_radius = 4900; 71 | var max_X = sphere_radius; 72 | var min_X = -sphere_radius; 73 | var max_Y = sphere_radius; 74 | var min_Y = -sphere_radius; 75 | 76 | var that=this; 77 | 78 | init(); 79 | createGraph(); 80 | animate(); 81 | 82 | function init() { 83 | // Three.js initialization 84 | renderer = new THREE.WebGLRenderer({alpha: true}); 85 | renderer.setSize( window.innerWidth, window.innerHeight ); 86 | 87 | camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 100000); 88 | camera.position.z = 20000; 89 | 90 | controls = new THREE.TrackballControls(camera); 91 | 92 | controls.rotateSpeed = 0.5; 93 | controls.zoomSpeed = 5.2; 94 | controls.panSpeed = 1; 95 | 96 | controls.noZoom = false; 97 | controls.noPan = false; 98 | 99 | controls.staticMoving = false; 100 | controls.dynamicDampingFactor = 0.3; 101 | 102 | controls.keys = [ 65, 83, 68 ]; 103 | 104 | controls.addEventListener('change', render); 105 | 106 | scene = new THREE.Scene(); 107 | 108 | // Create sphere geometry and add it to the scene 109 | var sphere_geometry = new THREE.SphereGeometry(sphere_radius, 110, 100); 110 | material = new THREE.MeshBasicMaterial({ color: 0x000000, opacity: 0.8 }); 111 | mesh = new THREE.Mesh(sphere_geometry, material); 112 | scene.add(mesh); 113 | 114 | // Create node geometry (will be used in drawNode()) 115 | geometry = new THREE.SphereGeometry( 25, 25, 0 ); 116 | 117 | // Create node selection, if set 118 | if(that.selection) { 119 | object_selection = new THREE.ObjectSelection({ 120 | domElement: renderer.domElement, 121 | selected: function(obj) { 122 | // display info 123 | if(obj !== null) { 124 | info_text.select = "Object " + obj.id; 125 | } else { 126 | delete info_text.select; 127 | } 128 | } 129 | }); 130 | } 131 | 132 | document.body.appendChild( renderer.domElement ); 133 | 134 | // Stats.js 135 | if(that.show_stats) { 136 | stats = new Stats(); 137 | stats.domElement.style.position = 'absolute'; 138 | stats.domElement.style.top = '0px'; 139 | document.body.appendChild( stats.domElement ); 140 | } 141 | 142 | // Create info box 143 | if(that.show_info) { 144 | var info = document.createElement("div"); 145 | var id_attr = document.createAttribute("id"); 146 | id_attr.nodeValue = "graph-info"; 147 | info.setAttributeNode(id_attr); 148 | document.body.appendChild( info ); 149 | } 150 | } 151 | 152 | 153 | /** 154 | * Creates a graph with random nodes and edges. 155 | * Number of nodes and edges can be set with 156 | * numNodes and numEdges. 157 | */ 158 | function createGraph() { 159 | var node = new GRAPHVIS.Node(0); 160 | graph.addNode(node); 161 | drawNode(node); 162 | 163 | var nodes = []; 164 | nodes.push(node); 165 | 166 | var steps = 1; 167 | while(nodes.length !== 0 && steps < that.nodes_count) { 168 | node = nodes.shift(); 169 | 170 | var numEdges = randomFromTo(1, that.edges_count); 171 | for(var i=1; i <= numEdges; i++) { 172 | var target_node = new GRAPHVIS.Node(i*steps); 173 | if(graph.addNode(target_node)) { 174 | drawNode(target_node); 175 | nodes.push(target_node); 176 | if(graph.addEdge(node, target_node)) { 177 | drawEdge(node, target_node); 178 | } 179 | } 180 | } 181 | steps++; 182 | } 183 | 184 | // Transform a lat, lng-position to x,y. 185 | graph.layout = new Layout.ForceDirected(graph, {width: 2000, height: 2000, iterations: 1000, positionUpdated: function(node) { 186 | max_X = Math.max(max_X, node.position.x); 187 | min_X = Math.min(min_X, node.position.x); 188 | max_Y = Math.max(max_Y, node.position.y); 189 | min_Y = Math.min(min_Y, node.position.y); 190 | 191 | var lat, lng; 192 | if(node.position.x < 0) { 193 | lat = (-90/min_X) * node.position.x; 194 | } else { 195 | lat = (90/max_X) * node.position.x; 196 | } 197 | if(node.position.y < 0) { 198 | lng = (-180/min_Y) * node.position.y; 199 | } else { 200 | lng = (180/max_Y) * node.position.y; 201 | } 202 | 203 | var area = 5000; 204 | var phi = (90 - lat) * Math.PI / 180; 205 | var theta = (180 - lng) * Math.PI / 180; 206 | node.data.draw_object.position.x = area * Math.sin(phi) * Math.cos(theta); 207 | node.data.draw_object.position.y = area * Math.cos(phi); 208 | node.data.draw_object.position.z = area * Math.sin(phi) * Math.sin(theta); 209 | 210 | }}); 211 | graph.layout.init(); 212 | info_text.nodes = "Nodes " + graph.nodes.length; 213 | info_text.edges = "Edges " + graph.edges.length; 214 | } 215 | 216 | 217 | /** 218 | * Create a node object and add it to the scene. 219 | */ 220 | function drawNode(node) { 221 | var draw_object = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: Math.random() * 0xffffff } ) ); 222 | 223 | var area = 2000; 224 | draw_object.position.x = Math.floor(Math.random() * (area + area + 1) - area); 225 | draw_object.position.y = Math.floor(Math.random() * (area + area + 1) - area); 226 | 227 | node.position.x = Math.floor(Math.random() * (area + area + 1) - area); 228 | node.position.y = Math.floor(Math.random() * (area + area + 1) - area); 229 | 230 | draw_object.id = node.id; 231 | node.data.draw_object = draw_object; 232 | node.layout = {}; 233 | node.layout.max_X = 90; 234 | node.layout.min_X = -90; 235 | node.layout.max_Y = 180; 236 | node.layout.min_Y = -180; 237 | 238 | // node.position = draw_object.position; 239 | scene.add( node.data.draw_object ); 240 | } 241 | 242 | 243 | /** 244 | * Create an edge object (line) and add it to the scene. 245 | */ 246 | function drawEdge(source, target) { 247 | material = new THREE.LineBasicMaterial( { color: 0xCCCCCC, opacity: 0.5, linewidth: 0.5 } ); 248 | var tmp_geo = new THREE.Geometry(); 249 | 250 | tmp_geo.vertices.push(source.data.draw_object.position); 251 | tmp_geo.vertices.push(target.data.draw_object.position); 252 | 253 | line = new THREE.LineSegments( tmp_geo, material ); 254 | line.scale.x = line.scale.y = line.scale.z = 1; 255 | line.originalScale = 1; 256 | 257 | geometries.push(tmp_geo); 258 | 259 | scene.add( line ); 260 | } 261 | 262 | 263 | function animate() { 264 | requestAnimationFrame( animate ); 265 | controls.update(); 266 | render(); 267 | if(that.show_info) { 268 | printInfo(); 269 | } 270 | } 271 | 272 | 273 | function render() { 274 | var i; 275 | 276 | // Generate layout if not finished 277 | if(!graph.layout.finished) { 278 | info_text.calc = "Calculating layout..."; 279 | graph.layout.generate(); 280 | } else { 281 | info_text.calc = ""; 282 | } 283 | 284 | // Update position of lines (edges) 285 | for(i=0; i 2 | 3 | 4 | 5 | Graph Visualization 6 | 7 | 53 | 54 | 55 |
56 | 89 | 90 | 91 |
92 | 93 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-visualization", 3 | "version": "0.6.0", 4 | "devDependencies": { 5 | "grunt": "~1.0.2", 6 | "grunt-contrib-jshint": "~1.1.0", 7 | "grunt-contrib-uglify": "~3.3.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza 3 | 4 | Implements a graph structure. 5 | Consists of Graph, Nodes and Edges. 6 | 7 | 8 | Nodes: 9 | Create a new Node with an id. A node has the properties 10 | id, position and data. 11 | 12 | Example: 13 | node = new GRAPHVIS.Node(1); 14 | node.position.x = 100; 15 | node.position.y = 100; 16 | node.data.title = "Title of the node"; 17 | 18 | The data property can be used to extend the node with custom 19 | informations. Then, they can be used in a visualization. 20 | 21 | 22 | Edges: 23 | Connects to nodes together. 24 | 25 | Example: 26 | edge = new GRAPHVIS.Edge(node1, node2); 27 | 28 | An edge can also be extended with the data attribute. E.g. set a 29 | type like "friends", different types can then be draw in differnt ways. 30 | 31 | 32 | Graph: 33 | 34 | Parameters: 35 | options = { 36 | limit: , maximum number of nodes 37 | } 38 | 39 | Methods: 40 | addNode(node) - adds a new node and returns true if the node has been added, 41 | otherwise false. 42 | getNode(node_id) - returns the node with node_id or undefined, if it not exist 43 | addEdge(node1, node2) - adds an edge for node1 and node2. Returns true if the 44 | edge has been added, otherwise false (e.g.) when the 45 | edge between these nodes already exist. 46 | 47 | reached_limit() - returns true if the limit has been reached, otherwise false 48 | 49 | */ 50 | 51 | var GRAPHVIS = GRAPHVIS || {}; 52 | 53 | GRAPHVIS.Graph = function(options) { 54 | this.options = options || {}; 55 | this.nodeSet = {}; 56 | this.nodes = []; 57 | this.edges = []; 58 | this.layout = undefined; 59 | }; 60 | 61 | GRAPHVIS.Graph.prototype.addNode = function(node) { 62 | if(this.nodeSet[node.id] === undefined && !this.reached_limit()) { 63 | this.nodeSet[node.id] = node; 64 | this.nodes.push(node); 65 | return true; 66 | } 67 | return false; 68 | }; 69 | 70 | GRAPHVIS.Graph.prototype.getNode = function(node_id) { 71 | return this.nodeSet[node_id]; 72 | }; 73 | 74 | GRAPHVIS.Graph.prototype.addEdge = function(source, target) { 75 | if(source.addConnectedTo(target) === true) { 76 | var edge = new GRAPHVIS.Edge(source, target); 77 | this.edges.push(edge); 78 | return true; 79 | } 80 | return false; 81 | }; 82 | 83 | GRAPHVIS.Graph.prototype.reached_limit = function() { 84 | if(this.options.limit !== undefined) 85 | return this.options.limit <= this.nodes.length; 86 | else 87 | return false; 88 | }; 89 | 90 | 91 | GRAPHVIS.Node = function(node_id) { 92 | this.id = node_id; 93 | this.nodesTo = []; 94 | this.nodesFrom = []; 95 | this.position = {}; 96 | this.data = {}; 97 | }; 98 | 99 | GRAPHVIS.Node.prototype.addConnectedTo = function(node) { 100 | if(this.connectedTo(node) === false) { 101 | this.nodesTo.push(node); 102 | return true; 103 | } 104 | return false; 105 | }; 106 | 107 | GRAPHVIS.Node.prototype.connectedTo = function(node) { 108 | for(var i=0; i < this.nodesTo.length; i++) { 109 | var connectedNode = this.nodesTo[i]; 110 | if(connectedNode.id == node.id) { 111 | return true; 112 | } 113 | } 114 | return false; 115 | }; 116 | 117 | 118 | GRAPHVIS.Edge = function(source, target) { 119 | this.source = source; 120 | this.target = target; 121 | this.data = {}; 122 | }; 123 | -------------------------------------------------------------------------------- /src/layouts/force-directed-layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza (@davidpiegza) 3 | @author Timofey Rechkalov (@TRechkalov) 4 | 5 | Implements a force-directed layout, the algorithm is based on Fruchterman and Reingold and 6 | the JUNG implementation. 7 | 8 | Needs the graph data structure Graph.js and the Vector3 object: 9 | https://github.com/davidpiegza/Graph-Visualization/blob/master/Graph.js 10 | https://github.com/davidpiegza/Graph-Visualization/blob/master/utils/Vector3.js 11 | 12 | Parameters: 13 | graph - data structure 14 | options = { 15 | layout: "2d" or "3d" 16 | attraction: , attraction value for force-directed layout 17 | repulsion: , repulsion value for force-directed layout 18 | iterations: , maximum number of iterations 19 | width: , width of the viewport 20 | height: , height of the viewport 21 | 22 | positionUpdated: , called when the position of the node has been updated 23 | } 24 | 25 | Examples: 26 | 27 | create: 28 | layout = new Layout.ForceDirected(graph, {width: 2000, height: 2000, iterations: 1000, layout: "3d"}); 29 | 30 | call init when graph is loaded (and for reset or when new nodes has been added to the graph): 31 | layout.init(); 32 | 33 | call generate in a render method, returns true if it's still calculating and false if it's finished 34 | layout.generate(); 35 | 36 | 37 | Feel free to contribute a new layout! 38 | 39 | */ 40 | 41 | var Layout = Layout || {}; 42 | 43 | Layout.ForceDirected = function(graph, options) { 44 | options = options || {}; 45 | 46 | this.layout = options.layout || "2d"; 47 | this.attraction_multiplier = options.attraction || 5; 48 | this.repulsion_multiplier = options.repulsion || 0.75; 49 | this.max_iterations = options.iterations || 1000; 50 | this.graph = graph; 51 | this.width = options.width || 200; 52 | this.height = options.height || 200; 53 | this.finished = false; 54 | 55 | var callback_positionUpdated = options.positionUpdated; 56 | 57 | var EPSILON = 0.000001; 58 | var attraction_constant; 59 | var repulsion_constant; 60 | var forceConstant; 61 | var layout_iterations = 0; 62 | var temperature = 0; 63 | var nodes_length; 64 | var edges_length; 65 | var that = this; 66 | 67 | // performance test 68 | var mean_time = 0; 69 | 70 | /** 71 | * Initialize parameters used by the algorithm. 72 | */ 73 | this.init = function() { 74 | this.finished = false; 75 | temperature = this.width / 10.0; 76 | nodes_length = this.graph.nodes.length; 77 | edges_length = this.graph.edges.length; 78 | forceConstant = Math.sqrt(this.height * this.width / nodes_length); 79 | attraction_constant = this.attraction_multiplier * forceConstant; 80 | repulsion_constant = this.repulsion_multiplier * forceConstant; 81 | }; 82 | 83 | /** 84 | * Generates the force-directed layout. 85 | * 86 | * It finishes when the number of max_iterations has been reached or when 87 | * the temperature is nearly zero. 88 | */ 89 | this.generate = function() { 90 | if(layout_iterations < this.max_iterations && temperature > 0.000001) { 91 | var start = new Date().getTime(); 92 | var i, j, delta, delta_length, force, change; 93 | 94 | // calculate repulsion 95 | for(i=0; i < nodes_length; i++) { 96 | var node_v = graph.nodes[i]; 97 | node_v.layout = node_v.layout || {}; 98 | if(i === 0) { 99 | node_v.layout.offset = new Vector3(); 100 | } 101 | 102 | node_v.layout.force = 0; 103 | node_v.layout.tmp_pos = node_v.layout.tmp_pos || new Vector3().setVector(node_v.position); 104 | 105 | for(j=i+1; j < nodes_length; j++) { 106 | var node_u = graph.nodes[j]; 107 | if(i != j) { 108 | node_u.layout = node_u.layout || {}; 109 | 110 | node_u.layout.tmp_pos = node_u.layout.tmp_pos || new Vector3().setVector(node_u.position); 111 | 112 | delta = node_v.layout.tmp_pos.clone().sub(node_u.layout.tmp_pos); 113 | delta_length = Math.max(EPSILON, Math.sqrt(delta.clone().multiply(delta).sum())); 114 | 115 | force = (repulsion_constant * repulsion_constant) / delta_length; 116 | 117 | node_v.layout.force += force; 118 | node_u.layout.force += force; 119 | 120 | if(i === 0) { 121 | node_u.layout.offset = new Vector3(); 122 | } 123 | 124 | change = delta.clone().multiply(new Vector3().setScalar(force/delta_length)); 125 | node_v.layout.offset.add(change); 126 | node_u.layout.offset.sub(change); 127 | } 128 | } 129 | } 130 | 131 | // calculate attraction 132 | for(i=0; i < edges_length; i++) { 133 | var edge = graph.edges[i]; 134 | delta = edge.source.layout.tmp_pos.clone().sub(edge.target.layout.tmp_pos); 135 | delta_length = Math.max(EPSILON, Math.sqrt(delta.clone().multiply(delta).sum())); 136 | 137 | force = (delta_length * delta_length) / attraction_constant; 138 | 139 | edge.source.layout.force -= force; 140 | edge.target.layout.force += force; 141 | 142 | change = delta.clone().multiply(new Vector3().setScalar(force/delta_length)); 143 | edge.target.layout.offset.add(change); 144 | edge.source.layout.offset.sub(change); 145 | } 146 | 147 | // calculate positions 148 | for(i=0; i < nodes_length; i++) { 149 | var node = graph.nodes[i]; 150 | 151 | delta_length = Math.max(EPSILON, Math.sqrt(node.layout.offset.clone().multiply(node.layout.offset).sum())); 152 | 153 | node.layout.tmp_pos.add(node.layout.offset.clone().multiply(new Vector3().setScalar(Math.min(delta_length, temperature) / delta_length))); 154 | 155 | var updated = true; 156 | 157 | var tmpPosition = new Vector3(node.position.x, node.position.y, node.position.z); 158 | tmpPosition.sub(node.layout.tmp_pos).divide(new Vector3().setScalar(10)); 159 | 160 | node.position.x -= tmpPosition.x; 161 | node.position.y -= tmpPosition.y; 162 | 163 | if(this.layout === '3d') { 164 | node.position.z -= tmpPosition.z; 165 | } 166 | 167 | // execute callback function if position has been updated 168 | if(updated && typeof callback_positionUpdated === 'function') { 169 | callback_positionUpdated(node); 170 | } 171 | } 172 | temperature *= (1 - (layout_iterations / this.max_iterations)); 173 | layout_iterations++; 174 | 175 | var end = new Date().getTime(); 176 | mean_time += end - start; 177 | } else { 178 | if(!this.finished) { 179 | console.log("Average time: " + (mean_time/layout_iterations) + " ms"); 180 | } 181 | this.finished = true; 182 | return false; 183 | } 184 | return true; 185 | }; 186 | 187 | /** 188 | * Stops the calculation by setting the current_iterations to max_iterations. 189 | */ 190 | this.stop_calculating = function() { 191 | layout_iterations = this.max_iterations; 192 | }; 193 | }; 194 | -------------------------------------------------------------------------------- /src/utils/Label.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza 3 | 4 | Implements a label for an object. 5 | 6 | It creates an text in canvas and sets the text-canvas as 7 | texture of a cube geometry. 8 | 9 | Parameters: 10 | text: , text of the label 11 | 12 | Example: 13 | var label = new THREE.Label("Text of the label"); 14 | label.position.x = 100; 15 | label.position.y = 100; 16 | scene.addObject(label); 17 | */ 18 | 19 | THREE.Label = function(text, parameters) { 20 | parameters = parameters || {}; 21 | 22 | var labelCanvas = document.createElement( "canvas" ); 23 | 24 | function create() { 25 | var xc = labelCanvas.getContext("2d"); 26 | var fontsize = "40pt"; 27 | 28 | // set font size to measure the text 29 | xc.font = fontsize + " Arial"; 30 | var len = xc.measureText(text).width; 31 | 32 | labelCanvas.setAttribute('width', len); 33 | 34 | // set font size again cause it will be reset 35 | // when setting a new width 36 | xc.font = fontsize + " Arial"; 37 | xc.textBaseline = 'top'; 38 | xc.fillText(text, 0, 0); 39 | 40 | var geometry = new THREE.BoxGeometry(len, 200, 0); 41 | var xm = new THREE.MeshBasicMaterial({ 42 | map: new THREE.CanvasTexture( 43 | labelCanvas, 44 | THREE.UVMapping, 45 | THREE.ClampToEdgeWrapping, 46 | THREE.ClampToEdgeWrapping, 47 | THREE.LinearFilter, 48 | THREE.LinearFilter 49 | ), 50 | transparent: true 51 | }); 52 | xm.map.needsUpdate = true; 53 | 54 | // set text canvas to cube geometry 55 | var labelObject = new THREE.Mesh(geometry, xm); 56 | return labelObject; 57 | } 58 | 59 | return create(); 60 | }; 61 | -------------------------------------------------------------------------------- /src/utils/ObjectSelection.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza 3 | 4 | Implements a selection for objects in a scene. 5 | 6 | It invokes a callback function when the mouse enters and when it leaves the object. 7 | Based on a Three.js selection example. 8 | 9 | Parameters: 10 | domElement: HTMLDomElement 11 | selected: callback function, passes the current selected object (on mouseover) 12 | clicked: callback function, passes the current clicked object 13 | */ 14 | 15 | THREE.ObjectSelection = function(parameters) { 16 | parameters = parameters || {}; 17 | 18 | this.domElement = parameters.domElement || document; 19 | this.INTERSECTED = null; 20 | 21 | var _this = this; 22 | 23 | var callbackSelected = parameters.selected; 24 | var callbackClicked = parameters.clicked; 25 | var mouse = { x: 0, y: 0 }; 26 | 27 | this.domElement.addEventListener( 'mousemove', onDocumentMouseMove, false ); 28 | function onDocumentMouseMove( event ) { 29 | mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; 30 | mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; 31 | } 32 | 33 | this.domElement.addEventListener( 'click', onDocumentMouseClick, false ); 34 | function onDocumentMouseClick( event ) { 35 | if(_this.INTERSECTED) { 36 | if(typeof callbackClicked === 'function') { 37 | callbackClicked(_this.INTERSECTED); 38 | } 39 | } 40 | } 41 | 42 | this.render = function(scene, camera) { 43 | var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ); 44 | vector.unproject(camera); 45 | 46 | var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize()); 47 | 48 | var intersects = raycaster.intersectObject(scene, true); 49 | 50 | if( intersects.length > 0 ) { 51 | if ( this.INTERSECTED != intersects[ 0 ].object ) { 52 | if ( this.INTERSECTED ) { 53 | this.INTERSECTED.material.color.setHex( this.INTERSECTED.currentHex ); 54 | } 55 | 56 | this.INTERSECTED = intersects[ 0 ].object; 57 | this.INTERSECTED.currentHex = this.INTERSECTED.material.color.getHex(); 58 | this.INTERSECTED.material.color.setHex( 0xff0000 ); 59 | if(typeof callbackSelected === 'function') { 60 | callbackSelected(this.INTERSECTED); 61 | } 62 | } 63 | } else { 64 | if ( this.INTERSECTED ) { 65 | this.INTERSECTED.material.color.setHex( this.INTERSECTED.currentHex ); 66 | } 67 | this.INTERSECTED = null; 68 | if(typeof callbackSelected === 'function') { 69 | callbackSelected(this.INTERSECTED); 70 | } 71 | } 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/utils/Stats.js: -------------------------------------------------------------------------------- 1 | /*! stats.js r5 - http://github.com/mrdoob/stats.js */ 2 | var Stats=function(){function w(d,K,n){var u,f,c;for(f=0;f<30;f++)for(u=0;u<73;u++){c=(u+f*74)*4;d[c]=d[c+4];d[c+1]=d[c+5];d[c+2]=d[c+6]}for(f=0;f<30;f++){c=(73+f*74)*4;if(f'+q+" MS ("+D+"-"+E+")";r.putImageData(F,0,0);J=l;if(l> 9 | z+1E3){o=Math.round(y*1E3/(l-z));A=Math.min(A,o);B=Math.max(B,o);w(C.data,Math.min(30,30-o/100*30),"fps");g.innerHTML=''+o+" FPS ("+A+"-"+B+")";p.putImageData(C,0,0);if(x==3){s=performance.memory.usedJSHeapSize*9.54E-7;G=Math.min(G,s);H=Math.max(H,s);w(I.data,Math.min(30,30-s/2),"mem");k.innerHTML=''+Math.round(s)+" MEM ("+Math.round(G)+"-"+Math.round(H)+")";t.putImageData(I,0,0)}z=l;y=0}}}}; 10 | 11 | -------------------------------------------------------------------------------- /src/utils/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | */ 5 | 6 | THREE.TrackballControls = function ( object, domElement ) { 7 | 8 | var _this = this; 9 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM: 4, TOUCH_PAN: 5 }; 10 | 11 | this.object = object; 12 | this.domElement = ( domElement !== undefined ) ? domElement : document; 13 | 14 | // API 15 | 16 | this.enabled = true; 17 | 18 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 19 | 20 | this.rotateSpeed = 1.0; 21 | this.zoomSpeed = 1.2; 22 | this.panSpeed = 0.3; 23 | 24 | this.noRotate = false; 25 | this.noZoom = false; 26 | this.noPan = false; 27 | this.noRoll = false; 28 | 29 | this.staticMoving = false; 30 | this.dynamicDampingFactor = 0.2; 31 | 32 | this.minDistance = 0; 33 | this.maxDistance = Infinity; 34 | 35 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 36 | 37 | // internals 38 | 39 | this.target = new THREE.Vector3(); 40 | 41 | var lastPosition = new THREE.Vector3(); 42 | 43 | var _state = STATE.NONE, 44 | _prevState = STATE.NONE, 45 | 46 | _eye = new THREE.Vector3(), 47 | 48 | _rotateStart = new THREE.Vector3(), 49 | _rotateEnd = new THREE.Vector3(), 50 | 51 | _zoomStart = new THREE.Vector2(), 52 | _zoomEnd = new THREE.Vector2(), 53 | 54 | _touchZoomDistanceStart = 0, 55 | _touchZoomDistanceEnd = 0, 56 | 57 | _panStart = new THREE.Vector2(), 58 | _panEnd = new THREE.Vector2(); 59 | 60 | // for reset 61 | 62 | this.target0 = this.target.clone(); 63 | this.position0 = this.object.position.clone(); 64 | this.up0 = this.object.up.clone(); 65 | 66 | // events 67 | 68 | var changeEvent = { type: 'change' }; 69 | var startEvent = { type: 'start'}; 70 | var endEvent = { type: 'end'}; 71 | 72 | 73 | // methods 74 | 75 | this.handleResize = function () { 76 | 77 | if ( this.domElement === document ) { 78 | 79 | this.screen.left = 0; 80 | this.screen.top = 0; 81 | this.screen.width = window.innerWidth; 82 | this.screen.height = window.innerHeight; 83 | 84 | } else { 85 | 86 | this.screen = this.domElement.getBoundingClientRect(); 87 | // adjustments come from similar code in the jquery offset() function 88 | var d = this.domElement.ownerDocument.documentElement 89 | this.screen.left += window.pageXOffset - d.clientLeft 90 | this.screen.top += window.pageYOffset - d.clientTop 91 | 92 | } 93 | 94 | }; 95 | 96 | this.handleEvent = function ( event ) { 97 | 98 | if ( typeof this[ event.type ] == 'function' ) { 99 | 100 | this[ event.type ]( event ); 101 | 102 | } 103 | 104 | }; 105 | 106 | this.getMouseOnScreen = function ( pageX, pageY, vector ) { 107 | 108 | return vector.set( 109 | ( pageX - _this.screen.left ) / _this.screen.width, 110 | ( pageY - _this.screen.top ) / _this.screen.height 111 | ); 112 | 113 | }; 114 | 115 | this.getMouseProjectionOnBall = (function(){ 116 | 117 | var objectUp = new THREE.Vector3(), 118 | mouseOnBall = new THREE.Vector3(); 119 | 120 | 121 | return function ( pageX, pageY, projection ) { 122 | 123 | mouseOnBall.set( 124 | ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / (_this.screen.width*.5), 125 | ( _this.screen.height * 0.5 + _this.screen.top - pageY ) / (_this.screen.height*.5), 126 | 0.0 127 | ); 128 | 129 | var length = mouseOnBall.length(); 130 | 131 | if ( _this.noRoll ) { 132 | 133 | if ( length < Math.SQRT1_2 ) { 134 | 135 | mouseOnBall.z = Math.sqrt( 1.0 - length*length ); 136 | 137 | } else { 138 | 139 | mouseOnBall.z = .5 / length; 140 | 141 | } 142 | 143 | } else if ( length > 1.0 ) { 144 | 145 | mouseOnBall.normalize(); 146 | 147 | } else { 148 | 149 | mouseOnBall.z = Math.sqrt( 1.0 - length * length ); 150 | 151 | } 152 | 153 | _eye.copy( _this.object.position ).sub( _this.target ); 154 | 155 | projection.copy( _this.object.up ).setLength( mouseOnBall.y ) 156 | projection.add( objectUp.copy( _this.object.up ).cross( _eye ).setLength( mouseOnBall.x ) ); 157 | projection.add( _eye.setLength( mouseOnBall.z ) ); 158 | 159 | return projection; 160 | } 161 | 162 | }()); 163 | 164 | this.rotateCamera = (function(){ 165 | 166 | var axis = new THREE.Vector3(), 167 | quaternion = new THREE.Quaternion(); 168 | 169 | 170 | return function () { 171 | 172 | var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() ); 173 | 174 | if ( angle ) { 175 | 176 | axis.crossVectors( _rotateStart, _rotateEnd ).normalize(); 177 | 178 | angle *= _this.rotateSpeed; 179 | 180 | quaternion.setFromAxisAngle( axis, -angle ); 181 | 182 | _eye.applyQuaternion( quaternion ); 183 | _this.object.up.applyQuaternion( quaternion ); 184 | 185 | _rotateEnd.applyQuaternion( quaternion ); 186 | 187 | if ( _this.staticMoving ) { 188 | 189 | _rotateStart.copy( _rotateEnd ); 190 | 191 | } else { 192 | 193 | quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) ); 194 | _rotateStart.applyQuaternion( quaternion ); 195 | 196 | } 197 | 198 | } 199 | } 200 | 201 | }()); 202 | 203 | this.zoomCamera = function () { 204 | 205 | if ( _state === STATE.TOUCH_ZOOM ) { 206 | 207 | var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 208 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 209 | _eye.multiplyScalar( factor ); 210 | 211 | } else { 212 | 213 | var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 214 | 215 | if ( factor !== 1.0 && factor > 0.0 ) { 216 | 217 | _eye.multiplyScalar( factor ); 218 | 219 | if ( _this.staticMoving ) { 220 | 221 | _zoomStart.copy( _zoomEnd ); 222 | 223 | } else { 224 | 225 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 226 | 227 | } 228 | 229 | } 230 | 231 | } 232 | 233 | }; 234 | 235 | this.panCamera = (function(){ 236 | 237 | var mouseChange = new THREE.Vector2(), 238 | objectUp = new THREE.Vector3(), 239 | pan = new THREE.Vector3(); 240 | 241 | return function () { 242 | 243 | mouseChange.copy( _panEnd ).sub( _panStart ); 244 | 245 | if ( mouseChange.lengthSq() ) { 246 | 247 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 248 | 249 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 250 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 251 | 252 | _this.object.position.add( pan ); 253 | _this.target.add( pan ); 254 | 255 | if ( _this.staticMoving ) { 256 | 257 | _panStart.copy( _panEnd ); 258 | 259 | } else { 260 | 261 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 262 | 263 | } 264 | 265 | } 266 | } 267 | 268 | }()); 269 | 270 | this.checkDistances = function () { 271 | 272 | if ( !_this.noZoom || !_this.noPan ) { 273 | 274 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 275 | 276 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 277 | 278 | } 279 | 280 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 281 | 282 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 283 | 284 | } 285 | 286 | } 287 | 288 | }; 289 | 290 | this.update = function () { 291 | 292 | _eye.subVectors( _this.object.position, _this.target ); 293 | 294 | if ( !_this.noRotate ) { 295 | 296 | _this.rotateCamera(); 297 | 298 | } 299 | 300 | if ( !_this.noZoom ) { 301 | 302 | _this.zoomCamera(); 303 | 304 | } 305 | 306 | if ( !_this.noPan ) { 307 | 308 | _this.panCamera(); 309 | 310 | } 311 | 312 | _this.object.position.addVectors( _this.target, _eye ); 313 | 314 | _this.checkDistances(); 315 | 316 | _this.object.lookAt( _this.target ); 317 | 318 | if ( lastPosition.distanceToSquared( _this.object.position ) > 0 ) { 319 | 320 | _this.dispatchEvent( changeEvent ); 321 | 322 | lastPosition.copy( _this.object.position ); 323 | 324 | } 325 | 326 | }; 327 | 328 | this.reset = function () { 329 | 330 | _state = STATE.NONE; 331 | _prevState = STATE.NONE; 332 | 333 | _this.target.copy( _this.target0 ); 334 | _this.object.position.copy( _this.position0 ); 335 | _this.object.up.copy( _this.up0 ); 336 | 337 | _eye.subVectors( _this.object.position, _this.target ); 338 | 339 | _this.object.lookAt( _this.target ); 340 | 341 | _this.dispatchEvent( changeEvent ); 342 | 343 | lastPosition.copy( _this.object.position ); 344 | 345 | }; 346 | 347 | // listeners 348 | 349 | function keydown( event ) { 350 | 351 | if ( _this.enabled === false ) return; 352 | 353 | window.removeEventListener( 'keydown', keydown ); 354 | 355 | _prevState = _state; 356 | 357 | if ( _state !== STATE.NONE ) { 358 | 359 | return; 360 | 361 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { 362 | 363 | _state = STATE.ROTATE; 364 | 365 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { 366 | 367 | _state = STATE.ZOOM; 368 | 369 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { 370 | 371 | _state = STATE.PAN; 372 | 373 | } 374 | 375 | } 376 | 377 | function keyup( event ) { 378 | 379 | if ( _this.enabled === false ) return; 380 | 381 | _state = _prevState; 382 | 383 | window.addEventListener( 'keydown', keydown, false ); 384 | 385 | } 386 | 387 | function mousedown( event ) { 388 | 389 | if ( _this.enabled === false ) return; 390 | 391 | if ( _state === STATE.NONE ) { 392 | 393 | _state = event.button; 394 | 395 | } 396 | 397 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 398 | 399 | _this.getMouseProjectionOnBall( event.pageX, event.pageY, _rotateStart ); 400 | _rotateEnd.copy(_rotateStart) 401 | 402 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 403 | 404 | _this.getMouseOnScreen( event.pageX, event.pageY, _zoomStart ); 405 | _zoomEnd.copy(_zoomStart); 406 | 407 | } else if ( _state === STATE.PAN && !_this.noPan ) { 408 | 409 | _this.getMouseOnScreen( event.pageX, event.pageY, _panStart ); 410 | _panEnd.copy(_panStart) 411 | 412 | } 413 | 414 | document.addEventListener( 'mousemove', mousemove, false ); 415 | document.addEventListener( 'mouseup', mouseup, false ); 416 | _this.dispatchEvent( startEvent ); 417 | 418 | 419 | } 420 | 421 | function mousemove( event ) { 422 | 423 | if ( _this.enabled === false ) return; 424 | 425 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 426 | 427 | _this.getMouseProjectionOnBall( event.pageX, event.pageY, _rotateEnd ); 428 | 429 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 430 | 431 | _this.getMouseOnScreen( event.pageX, event.pageY, _zoomEnd ); 432 | 433 | } else if ( _state === STATE.PAN && !_this.noPan ) { 434 | 435 | _this.getMouseOnScreen( event.pageX, event.pageY, _panEnd ); 436 | 437 | } 438 | 439 | } 440 | 441 | function mouseup( event ) { 442 | 443 | if ( _this.enabled === false ) return; 444 | 445 | _state = STATE.NONE; 446 | 447 | document.removeEventListener( 'mousemove', mousemove ); 448 | document.removeEventListener( 'mouseup', mouseup ); 449 | _this.dispatchEvent( endEvent ); 450 | 451 | } 452 | 453 | function mousewheel( event ) { 454 | 455 | if ( _this.enabled === false ) return; 456 | 457 | event.preventDefault(); 458 | event.stopPropagation(); 459 | 460 | var delta = 0; 461 | 462 | if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 463 | 464 | delta = event.wheelDelta / 40; 465 | 466 | } else if ( event.detail ) { // Firefox 467 | 468 | delta = - event.detail / 3; 469 | 470 | } 471 | 472 | _zoomStart.y += delta * 0.01; 473 | _this.dispatchEvent( startEvent ); 474 | _this.dispatchEvent( endEvent ); 475 | 476 | } 477 | 478 | function touchstart( event ) { 479 | 480 | if ( _this.enabled === false ) return; 481 | 482 | switch ( event.touches.length ) { 483 | 484 | case 1: 485 | _state = STATE.TOUCH_ROTATE; 486 | _rotateEnd.copy( _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, _rotateStart )); 487 | break; 488 | 489 | case 2: 490 | _state = STATE.TOUCH_ZOOM; 491 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 492 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 493 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 494 | break; 495 | 496 | case 3: 497 | _state = STATE.TOUCH_PAN; 498 | _panEnd.copy( _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, _panStart )); 499 | break; 500 | 501 | default: 502 | _state = STATE.NONE; 503 | 504 | } 505 | _this.dispatchEvent( startEvent ); 506 | 507 | 508 | } 509 | 510 | function touchmove( event ) { 511 | 512 | if ( _this.enabled === false ) return; 513 | 514 | event.preventDefault(); 515 | event.stopPropagation(); 516 | 517 | switch ( event.touches.length ) { 518 | 519 | case 1: 520 | _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, _rotateEnd ); 521 | break; 522 | 523 | case 2: 524 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 525 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 526 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ) 527 | break; 528 | 529 | case 3: 530 | _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, _panEnd ); 531 | break; 532 | 533 | default: 534 | _state = STATE.NONE; 535 | 536 | } 537 | 538 | } 539 | 540 | function touchend( event ) { 541 | 542 | if ( _this.enabled === false ) return; 543 | 544 | switch ( event.touches.length ) { 545 | 546 | case 1: 547 | _rotateStart.copy( _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, _rotateEnd )); 548 | break; 549 | 550 | case 2: 551 | _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; 552 | break; 553 | 554 | case 3: 555 | _panStart.copy( _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, _panEnd )); 556 | break; 557 | 558 | } 559 | 560 | _state = STATE.NONE; 561 | _this.dispatchEvent( endEvent ); 562 | 563 | } 564 | 565 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 566 | 567 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 568 | 569 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 570 | this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox 571 | 572 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 573 | this.domElement.addEventListener( 'touchend', touchend, false ); 574 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 575 | 576 | window.addEventListener( 'keydown', keydown, false ); 577 | window.addEventListener( 'keyup', keyup, false ); 578 | 579 | this.handleResize(); 580 | 581 | }; 582 | 583 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 584 | -------------------------------------------------------------------------------- /src/utils/Vector3.js: -------------------------------------------------------------------------------- 1 | /** 2 | @author David Piegza 3 | 4 | Class representing a 3D vector. It is based on the three.js (https://threejs.org) Vector3 class. 5 | */ 6 | 7 | function Vector3(x, y, z) { 8 | this.x = x || 0; 9 | this.y = y || 0; 10 | this.z = z || 0; 11 | } 12 | 13 | Object.assign(Vector3.prototype, { 14 | set: function(x, y, z) { 15 | this.x = x; 16 | this.y = y; 17 | this.z = z; 18 | 19 | return this; 20 | }, 21 | 22 | setScalar: function(scalar) { 23 | this.x = scalar; 24 | this.y = scalar; 25 | this.z = scalar; 26 | 27 | return this; 28 | }, 29 | 30 | setVector: function(v) { 31 | this.x = v.x || 0; 32 | this.y = v.y || 0; 33 | this.z = v.z || 0; 34 | 35 | return this; 36 | }, 37 | 38 | setX: function(x) { 39 | this.x = x; 40 | 41 | return this; 42 | }, 43 | 44 | setY: function(y) { 45 | this.y = y; 46 | 47 | return this; 48 | }, 49 | 50 | setZ: function(z) { 51 | this.z = z; 52 | 53 | return this; 54 | }, 55 | 56 | setComponent: function(index, value) { 57 | switch (index) { 58 | case 0: this.x = value; break; 59 | case 1: this.y = value; break; 60 | case 2: this.z = value; break; 61 | default: throw new Error('index is out of range: ' + index); 62 | } 63 | 64 | return this; 65 | }, 66 | 67 | getComponent: function(index) { 68 | switch(index) { 69 | case 0: return this.x; 70 | case 1: return this.y; 71 | case 2: return this.z; 72 | default: throw new Error('index is out of range: ' + index); 73 | } 74 | }, 75 | 76 | clone: function() { 77 | return new this.constructor(this.x, this.y, this.z); 78 | }, 79 | 80 | copy: function(v) { 81 | this.x = v.x; 82 | this.y = v.y; 83 | this.z = v.z; 84 | 85 | return this; 86 | }, 87 | 88 | add: function(v) { 89 | this.x += v.x; 90 | this.y += v.y; 91 | this.z += v.z; 92 | 93 | return this; 94 | }, 95 | 96 | addScalar: function(s) { 97 | this.x += s; 98 | this.y += s; 99 | this.z += s; 100 | 101 | return this; 102 | }, 103 | 104 | addVectors: function(a, b) { 105 | this.x = a.x + b.x; 106 | this.y = a.y + b.y; 107 | this.z = a.z + b.z; 108 | 109 | return this; 110 | }, 111 | 112 | addScaledVector: function(v, s) { 113 | this.x += v.x * s; 114 | this.y += v.y * s; 115 | this.z += v.z * s; 116 | 117 | return this; 118 | }, 119 | 120 | sub: function(v) { 121 | this.x -= v.x; 122 | this.y -= v.y; 123 | this.z -= v.z; 124 | 125 | return this; 126 | }, 127 | 128 | subScalar: function(s) { 129 | this.x -= s; 130 | this.y -= s; 131 | this.z -= s; 132 | 133 | return this; 134 | }, 135 | 136 | subVectors: function(a, b) { 137 | this.x = a.x - b.x; 138 | this.y = a.y - b.y; 139 | this.z = a.z - b.z; 140 | 141 | return this; 142 | }, 143 | 144 | multiply: function(v) { 145 | this.x *= v.x; 146 | this.y *= v.y; 147 | this.z *= v.z; 148 | 149 | return this; 150 | }, 151 | 152 | multiplyScalar: function(scalar) { 153 | this.x *= scalar; 154 | this.y *= scalar; 155 | this.z *= scalar; 156 | 157 | return this; 158 | }, 159 | 160 | multiplyVectors: function(a, b) { 161 | this.x = a.x * b.x; 162 | this.y = a.y * b.y; 163 | this.z = a.z * b.z; 164 | 165 | return this; 166 | }, 167 | 168 | divide: function(v) { 169 | this.x /= v.x; 170 | this.y /= v.y; 171 | this.z /= v.z; 172 | 173 | return this; 174 | }, 175 | 176 | divideScalar: function (scalar) { 177 | return this.multiplyScalar(1 / scalar); 178 | }, 179 | 180 | min: function(v) { 181 | this.x = Math.min(this.x, v.x); 182 | this.y = Math.min(this.y, v.y); 183 | this.z = Math.min(this.z, v.z); 184 | 185 | return this; 186 | }, 187 | 188 | max: function(v) { 189 | this.x = Math.max(this.x, v.x); 190 | this.y = Math.max(this.y, v.y); 191 | this.z = Math.max(this.z, v.z); 192 | 193 | return this; 194 | }, 195 | 196 | sum: function() { 197 | return this.x + this.y + this.z; 198 | }, 199 | 200 | floor: function() { 201 | this.x = Math.floor(this.x); 202 | this.y = Math.floor(this.y); 203 | this.z = Math.floor(this.z); 204 | 205 | return this; 206 | }, 207 | 208 | ceil: function() { 209 | this.x = Math.ceil(this.x); 210 | this.y = Math.ceil(this.y); 211 | this.z = Math.ceil(this.z); 212 | 213 | return this; 214 | }, 215 | 216 | round: function() { 217 | this.x = Math.round(this.x); 218 | this.y = Math.round(this.y); 219 | this.z = Math.round(this.z); 220 | 221 | return this; 222 | }, 223 | 224 | equals: function(v) { 225 | return ((v.x === this.x) && (v.y === this.y) && (v.z === this.z)); 226 | }, 227 | }); 228 | --------------------------------------------------------------------------------