├── .gitattributes ├── .gitignore ├── DRAFT-cljs-gravity.pdf ├── README.md ├── project.clj ├── resources └── public │ ├── assets │ └── img │ │ ├── ball.png │ │ ├── ball.xcf │ │ ├── circle.png │ │ └── circle.xcf │ ├── css │ └── style.css │ ├── force-worker │ └── worker.js │ ├── index.html │ └── libs │ ├── OrbitControls.js │ ├── d3-3d.js │ ├── d3.js │ ├── d3.layout.force3d.js │ ├── stats.min.js │ └── three.js └── src └── gravity ├── events.cljs ├── force ├── proxy.cljs └── worker.cljs ├── graph.cljs ├── tools.cljs └── view ├── events_generator.cljs ├── graph.cljs ├── graph_tools.cljs ├── node.cljs └── nodeset.cljs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /out/ 6 | /release/ 7 | /target/ 8 | /repl/ 9 | /.repl*/ 10 | /temp/ 11 | /scripts/ 12 | .lein-deps-sum 13 | .lein-repl-history 14 | .lein-plugins/ 15 | 16 | .nrepl* 17 | 18 | /resources/public/js/compiled/** 19 | figwheel_server.log 20 | pom.xml 21 | *jar 22 | /lib/ 23 | /classes/ 24 | /out/ 25 | /target/ 26 | .lein-deps-sum 27 | .lein-repl-history 28 | .lein-plugins/ 29 | .repl 30 | .nrepl-port 31 | project.sublime-workspace 32 | project.sublime-project -------------------------------------------------------------------------------- /DRAFT-cljs-gravity.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/DRAFT-cljs-gravity.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A 3D force layout using D3.js and THREE.js 2 | 3 | Cljs-gravity (please help me to find a better name!) is a ClojureScript library that plot an interactive graph, animated by a Barnes-Hut simulation. 4 | 5 | **This is a work in progress**, see the demo: http://ggeoffrey.github.io/ (Chrome or Chromium recomended ATM) 6 | 7 | The goal is to make a safe and stable 3D graph visualisation that: 8 | - do one thing and do it well, 9 | - rely on quasi-standard tools, 10 | - ensure there is no side effects, 11 | - ensure there is no memory leaks, 12 | - use webworkers in an easy, safe and elegant way, 13 | - provide a rich set of events. 14 | 15 | ### Report 16 | See [this document](https://github.com/ggeoffrey/cljs-gravity/raw/master/DRAFT-cljs-gravity.pdf) 17 | for full knowledge on the library's content and rationals. 18 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject gravity "0.1.0-SNAPSHOT" 2 | :description "FIXME: write this!" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.7.0"] 8 | [org.clojure/clojurescript "1.7.122"] 9 | [org.clojure/core.async "0.1.346.0-17112a-alpha"]] 10 | 11 | :plugins [[lein-cljsbuild "1.1.0"] 12 | [lein-figwheel "0.4.0"]] 13 | 14 | :source-paths ["src"] 15 | 16 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 17 | 18 | :cljsbuild { 19 | :builds [{:id "dev" 20 | :source-paths ["src"] 21 | 22 | :figwheel { :on-jsload "gravity.graph/on-js-reload" } 23 | 24 | :compiler {:main gravity.graph 25 | :asset-path "js/compiled/out" 26 | :output-to "resources/public/js/compiled/gravity.js" 27 | :output-dir "resources/public/js/compiled/out" 28 | :source-map-timestamp true }} 29 | {:id "min" 30 | :source-paths ["src"] 31 | :compiler {:output-to "resources/public/js/compiled/gravity.js" 32 | :main gravity.graph 33 | :optimizations :advanced 34 | :pretty-print false 35 | :externs ["resources/public/libs/three.js" 36 | "resources/public/libs/OrbitControls.js" 37 | "resources/public/libs/d3-3d.js" 38 | "resources/public/libs/stats.min.js"] 39 | :closure-warnings {:externs-validation :off}}}]} 40 | 41 | :figwheel { 42 | ;; :http-server-root "public" ;; default and assumes "resources" 43 | ;; :server-port 3449 ;; default 44 | ;; :server-ip "127.0.0.1" 45 | 46 | :css-dirs ["resources/public/css"] ;; watch and update CSS 47 | 48 | ;; Start an nREPL server into the running figwheel process 49 | ;; :nrepl-port 7888 50 | 51 | ;; Server Ring Handler (optional) 52 | ;; if you want to embed a ring handler into the figwheel http-kit 53 | ;; server, this is for simple ring servers, if this 54 | ;; doesn't work for you just run your own server :) 55 | ;; :ring-handler hello_world.server/handler 56 | 57 | ;; To be able to open files in your editor from the heads up display 58 | ;; you will need to put a script on your path. 59 | ;; that script will have to take a file path and a line number 60 | ;; ie. in ~/bin/myfile-opener 61 | ;; #! /bin/sh 62 | ;; emacsclient -n +$2 $1 63 | ;; 64 | ;; :open-file-command "myfile-opener" 65 | 66 | ;; if you want to disable the REPL 67 | ;; :repl false 68 | 69 | ;; to configure a different figwheel logfile path 70 | ;; :server-logfile "tmp/logs/figwheel-logfile.log" 71 | }) 72 | -------------------------------------------------------------------------------- /resources/public/assets/img/ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/ball.png -------------------------------------------------------------------------------- /resources/public/assets/img/ball.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/ball.xcf -------------------------------------------------------------------------------- /resources/public/assets/img/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/circle.png -------------------------------------------------------------------------------- /resources/public/assets/img/circle.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/circle.xcf -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* some style */ 2 | 3 | html, body{ 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | #container { 9 | width : 50%; 10 | height : auto; 11 | margin : 0em; 12 | } 13 | 14 | #target{ 15 | width : 100%; 16 | height: 100%; 17 | } 18 | 19 | #tooltip{ 20 | position: absolute; 21 | padding: 1em; 22 | background-color: #B0B0B0; 23 | opacity: 0.95; 24 | font-weight: bold; 25 | 26 | margin: 1em; 27 | display: none; 28 | } 29 | -------------------------------------------------------------------------------- /resources/public/force-worker/worker.js: -------------------------------------------------------------------------------- 1 | // BOOTSTRAP 2 | 3 | //* 4 | importScripts( "../js/compiled/out/goog/base.js", 5 | "../js/compiled/out/goog/deps.js", 6 | "../js/compiled/out/goog/object/object.js", 7 | "../js/compiled/out/cljs_deps.js", 8 | "../js/compiled/out/cljs/core.js", 9 | "../js/compiled/out/gravity/force/worker.js" 10 | ); 11 | 12 | 13 | importScripts("../libs/d3-3d.js"); 14 | 15 | 16 | goog.require("gravity.force.worker"); 17 | gravity.force.worker.create(); 18 | //*/ 19 | 20 | /* 21 | importScripts("../out/gravity.js"); 22 | 23 | goog.require("gravity.force.worker"); 24 | gravity.force.worker.main(); 25 | 26 | //*/ 27 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /resources/public/libs/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | 18 | THREE.OrbitControls = function ( object, domElement ) { 19 | 20 | this.object = object; 21 | this.domElement = ( domElement !== undefined ) ? domElement : document; 22 | 23 | // API 24 | 25 | // Set to false to disable this control 26 | this.enabled = true; 27 | 28 | // "target" sets the location of focus, where the control orbits around 29 | // and where it pans with respect to. 30 | this.target = new THREE.Vector3(); 31 | 32 | // center is old, deprecated; use "target" instead 33 | this.center = this.target; 34 | 35 | // This option actually enables dollying in and out; left as "zoom" for 36 | // backwards compatibility 37 | this.noZoom = false; 38 | this.zoomSpeed = 1.0; 39 | 40 | // Limits to how far you can dolly in and out ( PerspectiveCamera only ) 41 | this.minDistance = 0; 42 | this.maxDistance = Infinity; 43 | 44 | // Limits to how far you can zoom in and out ( OrthographicCamera only ) 45 | this.minZoom = 0; 46 | this.maxZoom = Infinity; 47 | 48 | // Set to true to disable this control 49 | this.noRotate = false; 50 | this.rotateSpeed = 1.0; 51 | 52 | // Set to true to disable this control 53 | this.noPan = false; 54 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 55 | 56 | // Set to true to automatically rotate around the target 57 | this.autoRotate = false; 58 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 59 | 60 | // How far you can orbit vertically, upper and lower limits. 61 | // Range is 0 to Math.PI radians. 62 | this.minPolarAngle = 0; // radians 63 | this.maxPolarAngle = Math.PI; // radians 64 | 65 | // How far you can orbit horizontally, upper and lower limits. 66 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 67 | this.minAzimuthAngle = - Infinity; // radians 68 | this.maxAzimuthAngle = Infinity; // radians 69 | 70 | // Set to true to disable use of the keys 71 | this.noKeys = false; 72 | 73 | // The four arrow keys 74 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 75 | 76 | // Mouse buttons 77 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 78 | 79 | //////////// 80 | // internals 81 | 82 | var scope = this; 83 | 84 | var EPS = 0.000001; 85 | 86 | var rotateStart = new THREE.Vector2(); 87 | var rotateEnd = new THREE.Vector2(); 88 | var rotateDelta = new THREE.Vector2(); 89 | 90 | var panStart = new THREE.Vector2(); 91 | var panEnd = new THREE.Vector2(); 92 | var panDelta = new THREE.Vector2(); 93 | var panOffset = new THREE.Vector3(); 94 | 95 | var offset = new THREE.Vector3(); 96 | 97 | var dollyStart = new THREE.Vector2(); 98 | var dollyEnd = new THREE.Vector2(); 99 | var dollyDelta = new THREE.Vector2(); 100 | 101 | var theta; 102 | var phi; 103 | var phiDelta = 0; 104 | var thetaDelta = 0; 105 | var scale = 1; 106 | var pan = new THREE.Vector3(); 107 | 108 | var lastPosition = new THREE.Vector3(); 109 | var lastQuaternion = new THREE.Quaternion(); 110 | 111 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 112 | 113 | var state = STATE.NONE; 114 | 115 | // for reset 116 | 117 | this.target0 = this.target.clone(); 118 | this.position0 = this.object.position.clone(); 119 | this.zoom0 = this.object.zoom; 120 | 121 | // so camera.up is the orbit axis 122 | 123 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 124 | var quatInverse = quat.clone().inverse(); 125 | 126 | // events 127 | 128 | var changeEvent = { type: 'change' }; 129 | var startEvent = { type: 'start' }; 130 | var endEvent = { type: 'end' }; 131 | 132 | this.rotateLeft = function ( angle ) { 133 | 134 | if ( angle === undefined ) { 135 | 136 | angle = getAutoRotationAngle(); 137 | 138 | } 139 | 140 | thetaDelta -= angle; 141 | 142 | }; 143 | 144 | this.rotateUp = function ( angle ) { 145 | 146 | if ( angle === undefined ) { 147 | 148 | angle = getAutoRotationAngle(); 149 | 150 | } 151 | 152 | phiDelta -= angle; 153 | 154 | }; 155 | 156 | // pass in distance in world space to move left 157 | this.panLeft = function ( distance ) { 158 | 159 | var te = this.object.matrix.elements; 160 | 161 | // get X column of matrix 162 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 163 | panOffset.multiplyScalar( - distance ); 164 | 165 | pan.add( panOffset ); 166 | 167 | }; 168 | 169 | // pass in distance in world space to move up 170 | this.panUp = function ( distance ) { 171 | 172 | var te = this.object.matrix.elements; 173 | 174 | // get Y column of matrix 175 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 176 | panOffset.multiplyScalar( distance ); 177 | 178 | pan.add( panOffset ); 179 | 180 | }; 181 | 182 | // pass in x,y of change desired in pixel space, 183 | // right and down are positive 184 | this.pan = function ( deltaX, deltaY ) { 185 | 186 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 187 | 188 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 189 | 190 | // perspective 191 | var position = scope.object.position; 192 | var offset = position.clone().sub( scope.target ); 193 | var targetDistance = offset.length(); 194 | 195 | // half of the fov is center to top of screen 196 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 197 | 198 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 199 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 200 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 201 | 202 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 203 | 204 | // orthographic 205 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 206 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 207 | 208 | } else { 209 | 210 | // camera neither orthographic or perspective 211 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 212 | 213 | } 214 | 215 | }; 216 | 217 | this.dollyIn = function ( dollyScale ) { 218 | 219 | if ( dollyScale === undefined ) { 220 | 221 | dollyScale = getZoomScale(); 222 | 223 | } 224 | 225 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 226 | 227 | scale /= dollyScale; 228 | 229 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 230 | 231 | scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) ); 232 | scope.object.updateProjectionMatrix(); 233 | scope.dispatchEvent( changeEvent ); 234 | 235 | } else { 236 | 237 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 238 | 239 | } 240 | 241 | }; 242 | 243 | this.dollyOut = function ( dollyScale ) { 244 | 245 | if ( dollyScale === undefined ) { 246 | 247 | dollyScale = getZoomScale(); 248 | 249 | } 250 | 251 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 252 | 253 | scale *= dollyScale; 254 | 255 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 256 | 257 | scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) ); 258 | scope.object.updateProjectionMatrix(); 259 | scope.dispatchEvent( changeEvent ); 260 | 261 | } else { 262 | 263 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 264 | 265 | } 266 | 267 | }; 268 | 269 | this.update = function () { 270 | 271 | var position = this.object.position; 272 | 273 | offset.copy( position ).sub( this.target ); 274 | 275 | // rotate offset to "y-axis-is-up" space 276 | offset.applyQuaternion( quat ); 277 | 278 | // angle from z-axis around y-axis 279 | 280 | theta = Math.atan2( offset.x, offset.z ); 281 | 282 | // angle from y-axis 283 | 284 | phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 285 | 286 | if ( this.autoRotate && state === STATE.NONE ) { 287 | 288 | this.rotateLeft( getAutoRotationAngle() ); 289 | 290 | } 291 | 292 | theta += thetaDelta; 293 | phi += phiDelta; 294 | 295 | // restrict theta to be between desired limits 296 | theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); 297 | 298 | // restrict phi to be between desired limits 299 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 300 | 301 | // restrict phi to be betwee EPS and PI-EPS 302 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 303 | 304 | var radius = offset.length() * scale; 305 | 306 | // restrict radius to be between desired limits 307 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 308 | 309 | // move target to panned location 310 | this.target.add( pan ); 311 | 312 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 313 | offset.y = radius * Math.cos( phi ); 314 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 315 | 316 | // rotate offset back to "camera-up-vector-is-up" space 317 | offset.applyQuaternion( quatInverse ); 318 | 319 | position.copy( this.target ).add( offset ); 320 | 321 | this.object.lookAt( this.target ); 322 | 323 | thetaDelta = 0; 324 | phiDelta = 0; 325 | scale = 1; 326 | pan.set( 0, 0, 0 ); 327 | 328 | // update condition is: 329 | // min(camera displacement, camera rotation in radians)^2 > EPS 330 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 331 | 332 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS 333 | || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) { 334 | 335 | this.dispatchEvent( changeEvent ); 336 | 337 | lastPosition.copy( this.object.position ); 338 | lastQuaternion.copy (this.object.quaternion ); 339 | 340 | } 341 | 342 | }; 343 | 344 | 345 | this.reset = function () { 346 | 347 | state = STATE.NONE; 348 | 349 | this.target.copy( this.target0 ); 350 | this.object.position.copy( this.position0 ); 351 | this.object.zoom = this.zoom0; 352 | 353 | this.object.updateProjectionMatrix(); 354 | this.dispatchEvent( changeEvent ); 355 | 356 | this.update(); 357 | 358 | }; 359 | 360 | this.getPolarAngle = function () { 361 | 362 | return phi; 363 | 364 | }; 365 | 366 | this.getAzimuthalAngle = function () { 367 | 368 | return theta 369 | 370 | }; 371 | 372 | function getAutoRotationAngle() { 373 | 374 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 375 | 376 | } 377 | 378 | function getZoomScale() { 379 | 380 | return Math.pow( 0.95, scope.zoomSpeed ); 381 | 382 | } 383 | 384 | function onMouseDown( event ) { 385 | 386 | if ( scope.enabled === false ) return; 387 | event.preventDefault(); 388 | 389 | if ( event.button === scope.mouseButtons.ORBIT ) { 390 | if ( scope.noRotate === true ) return; 391 | 392 | state = STATE.ROTATE; 393 | 394 | rotateStart.set( event.clientX, event.clientY ); 395 | 396 | } else if ( event.button === scope.mouseButtons.ZOOM ) { 397 | if ( scope.noZoom === true ) return; 398 | 399 | state = STATE.DOLLY; 400 | 401 | dollyStart.set( event.clientX, event.clientY ); 402 | 403 | } else if ( event.button === scope.mouseButtons.PAN ) { 404 | if ( scope.noPan === true ) return; 405 | 406 | state = STATE.PAN; 407 | 408 | panStart.set( event.clientX, event.clientY ); 409 | 410 | } 411 | 412 | if ( state !== STATE.NONE ) { 413 | document.addEventListener( 'mousemove', onMouseMove, false ); 414 | document.addEventListener( 'mouseup', onMouseUp, false ); 415 | scope.dispatchEvent( startEvent ); 416 | } 417 | 418 | } 419 | 420 | function onMouseMove( event ) { 421 | 422 | if ( scope.enabled === false ) return; 423 | 424 | event.preventDefault(); 425 | 426 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 427 | 428 | if ( state === STATE.ROTATE ) { 429 | 430 | if ( scope.noRotate === true ) return; 431 | 432 | rotateEnd.set( event.clientX, event.clientY ); 433 | rotateDelta.subVectors( rotateEnd, rotateStart ); 434 | 435 | // rotating across whole screen goes 360 degrees around 436 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 437 | 438 | // rotating up and down along whole screen attempts to go 360, but limited to 180 439 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 440 | 441 | rotateStart.copy( rotateEnd ); 442 | 443 | } else if ( state === STATE.DOLLY ) { 444 | 445 | if ( scope.noZoom === true ) return; 446 | 447 | dollyEnd.set( event.clientX, event.clientY ); 448 | dollyDelta.subVectors( dollyEnd, dollyStart ); 449 | 450 | if ( dollyDelta.y > 0 ) { 451 | 452 | scope.dollyIn(); 453 | 454 | } else if ( dollyDelta.y < 0 ) { 455 | 456 | scope.dollyOut(); 457 | 458 | } 459 | 460 | dollyStart.copy( dollyEnd ); 461 | 462 | } else if ( state === STATE.PAN ) { 463 | 464 | if ( scope.noPan === true ) return; 465 | 466 | panEnd.set( event.clientX, event.clientY ); 467 | panDelta.subVectors( panEnd, panStart ); 468 | 469 | scope.pan( panDelta.x, panDelta.y ); 470 | 471 | panStart.copy( panEnd ); 472 | 473 | } 474 | 475 | if ( state !== STATE.NONE ) scope.update(); 476 | 477 | } 478 | 479 | function onMouseUp( /* event */ ) { 480 | 481 | if ( scope.enabled === false ) return; 482 | 483 | document.removeEventListener( 'mousemove', onMouseMove, false ); 484 | document.removeEventListener( 'mouseup', onMouseUp, false ); 485 | scope.dispatchEvent( endEvent ); 486 | state = STATE.NONE; 487 | 488 | } 489 | 490 | function onMouseWheel( event ) { 491 | 492 | if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return; 493 | 494 | event.preventDefault(); 495 | event.stopPropagation(); 496 | 497 | var delta = 0; 498 | 499 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 500 | 501 | delta = event.wheelDelta; 502 | 503 | } else if ( event.detail !== undefined ) { // Firefox 504 | 505 | delta = - event.detail; 506 | 507 | } 508 | 509 | if ( delta > 0 ) { 510 | 511 | scope.dollyOut(); 512 | 513 | } else if ( delta < 0 ) { 514 | 515 | scope.dollyIn(); 516 | 517 | } 518 | 519 | scope.update(); 520 | scope.dispatchEvent( startEvent ); 521 | scope.dispatchEvent( endEvent ); 522 | 523 | } 524 | 525 | function onKeyDown( event ) { 526 | 527 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 528 | 529 | switch ( event.keyCode ) { 530 | 531 | case scope.keys.UP: 532 | scope.pan( 0, scope.keyPanSpeed ); 533 | scope.update(); 534 | break; 535 | 536 | case scope.keys.BOTTOM: 537 | scope.pan( 0, - scope.keyPanSpeed ); 538 | scope.update(); 539 | break; 540 | 541 | case scope.keys.LEFT: 542 | scope.pan( scope.keyPanSpeed, 0 ); 543 | scope.update(); 544 | break; 545 | 546 | case scope.keys.RIGHT: 547 | scope.pan( - scope.keyPanSpeed, 0 ); 548 | scope.update(); 549 | break; 550 | 551 | } 552 | 553 | } 554 | 555 | function touchstart( event ) { 556 | 557 | if ( scope.enabled === false ) return; 558 | 559 | switch ( event.touches.length ) { 560 | 561 | case 1: // one-fingered touch: rotate 562 | 563 | if ( scope.noRotate === true ) return; 564 | 565 | state = STATE.TOUCH_ROTATE; 566 | 567 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 568 | break; 569 | 570 | case 2: // two-fingered touch: dolly 571 | 572 | if ( scope.noZoom === true ) return; 573 | 574 | state = STATE.TOUCH_DOLLY; 575 | 576 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 577 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 578 | var distance = Math.sqrt( dx * dx + dy * dy ); 579 | dollyStart.set( 0, distance ); 580 | break; 581 | 582 | case 3: // three-fingered touch: pan 583 | 584 | if ( scope.noPan === true ) return; 585 | 586 | state = STATE.TOUCH_PAN; 587 | 588 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 589 | break; 590 | 591 | default: 592 | 593 | state = STATE.NONE; 594 | 595 | } 596 | 597 | if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent ); 598 | 599 | } 600 | 601 | function touchmove( event ) { 602 | 603 | if ( scope.enabled === false ) return; 604 | 605 | event.preventDefault(); 606 | event.stopPropagation(); 607 | 608 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 609 | 610 | switch ( event.touches.length ) { 611 | 612 | case 1: // one-fingered touch: rotate 613 | 614 | if ( scope.noRotate === true ) return; 615 | if ( state !== STATE.TOUCH_ROTATE ) return; 616 | 617 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 618 | rotateDelta.subVectors( rotateEnd, rotateStart ); 619 | 620 | // rotating across whole screen goes 360 degrees around 621 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 622 | // rotating up and down along whole screen attempts to go 360, but limited to 180 623 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 624 | 625 | rotateStart.copy( rotateEnd ); 626 | 627 | scope.update(); 628 | break; 629 | 630 | case 2: // two-fingered touch: dolly 631 | 632 | if ( scope.noZoom === true ) return; 633 | if ( state !== STATE.TOUCH_DOLLY ) return; 634 | 635 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 636 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 637 | var distance = Math.sqrt( dx * dx + dy * dy ); 638 | 639 | dollyEnd.set( 0, distance ); 640 | dollyDelta.subVectors( dollyEnd, dollyStart ); 641 | 642 | if ( dollyDelta.y > 0 ) { 643 | 644 | scope.dollyOut(); 645 | 646 | } else if ( dollyDelta.y < 0 ) { 647 | 648 | scope.dollyIn(); 649 | 650 | } 651 | 652 | dollyStart.copy( dollyEnd ); 653 | 654 | scope.update(); 655 | break; 656 | 657 | case 3: // three-fingered touch: pan 658 | 659 | if ( scope.noPan === true ) return; 660 | if ( state !== STATE.TOUCH_PAN ) return; 661 | 662 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 663 | panDelta.subVectors( panEnd, panStart ); 664 | 665 | scope.pan( panDelta.x, panDelta.y ); 666 | 667 | panStart.copy( panEnd ); 668 | 669 | scope.update(); 670 | break; 671 | 672 | default: 673 | 674 | state = STATE.NONE; 675 | 676 | } 677 | 678 | } 679 | 680 | function touchend( /* event */ ) { 681 | 682 | if ( scope.enabled === false ) return; 683 | 684 | scope.dispatchEvent( endEvent ); 685 | state = STATE.NONE; 686 | 687 | } 688 | 689 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 690 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 691 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 692 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 693 | 694 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 695 | this.domElement.addEventListener( 'touchend', touchend, false ); 696 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 697 | 698 | window.addEventListener( 'keydown', onKeyDown, false ); 699 | 700 | // force an update at start 701 | this.update(); 702 | 703 | }; 704 | 705 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 706 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 707 | -------------------------------------------------------------------------------- /resources/public/libs/d3.layout.force3d.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // D3.layout.force3d.js 3 | // (C) 2012 ziggy.jonsson.nyc@gmail.com 4 | // BSD license (http://opensource.org/licenses/BSD-3-Clause) 5 | 6 | d3.layout.force3d = function() { 7 | var forceXY = d3.layout.force() 8 | ,forceZ = d3.layout.force() 9 | ,zNodes = {} 10 | ,zLinks = {} 11 | ,nodeID = 1 12 | ,linkID = 1 13 | ,tickFunction = Object 14 | 15 | var force3d = {} 16 | 17 | Object.keys(forceXY).forEach(function(d) { 18 | force3d[d] = function() { 19 | var result = forceXY[d].apply(this,arguments) 20 | if (d !="nodes" && d!="links") forceZ[d].apply(this,arguments) 21 | return (result == forceXY) ? force3d : result 22 | } 23 | }) 24 | 25 | 26 | force3d.on = function(name,fn) { 27 | if(name == "tick"){ 28 | tickFunction = fn; 29 | } 30 | else{ 31 | forceXY.on(name, fn); 32 | } 33 | return force3d 34 | } 35 | 36 | 37 | forceXY.on("tick",function() { 38 | 39 | // Refresh zNodes add new, delete removed 40 | var _zNodes = {} 41 | forceXY.nodes().forEach(function(d,i) { 42 | if (!d.id) d.id = nodeID++ 43 | _zNodes[d.id] = zNodes[d.id] || {x:d.z,px:d.z,py:d.z,y:d.z,id:d.id} 44 | d.z = _zNodes[d.id].x 45 | }) 46 | zNodes = _zNodes 47 | 48 | // Refresh zLinks add new, delete removed 49 | var _zLinks = {} 50 | forceXY.links().forEach(function(d) { 51 | var nytt = false 52 | if (!d.linkID) { d.linkID = linkID++;nytt=true} 53 | _zLinks[d.linkID] = zLinks[d.linkID] || {target:zNodes[d.target.id],source:zNodes[d.source.id]} 54 | 55 | }) 56 | zLinks = _zLinks 57 | 58 | // Update the nodes/links in forceZ 59 | forceZ.nodes(d3.values(zNodes)) 60 | forceZ.links(d3.values(zLinks)) 61 | forceZ.start() // Need to kick forceZ so we don't lose the update mechanism 62 | 63 | // And run the user defined function, if defined 64 | if(tickFunction) tickFunction(); 65 | }) 66 | 67 | // Expose the sub-forces for debugging purposes 68 | force3d.xy = forceXY 69 | force3d.z = forceZ 70 | 71 | return force3d 72 | } 73 | })() -------------------------------------------------------------------------------- /resources/public/libs/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){function f(a,e,b){a=document.createElement(a);a.id=e;a.style.cssText=b;return a}function l(a,e,b){var c=f("div",a,"padding:0 0 3px 3px;text-align:left;background:"+b),d=f("div",a+"Text","font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px;color:"+e);d.innerHTML=a.toUpperCase();c.appendChild(d);a=f("div",a+"Graph","width:74px;height:30px;background:"+e);c.appendChild(a);for(e=0;74>e;e++)a.appendChild(f("span","","width:1px;height:30px;float:left;opacity:0.9;background:"+ 3 | b));return c}function m(a){for(var b=c.children,d=0;dr+1E3&&(d=Math.round(1E3* 5 | t/(a-r)),u=Math.min(u,d),v=Math.max(v,d),A.textContent=d+" FPS ("+u+"-"+v+")",p(B,d/100),r=a,t=0,void 0!==h)){var b=performance.memory.usedJSHeapSize,c=performance.memory.jsHeapSizeLimit;h=Math.round(9.54E-7*b);y=Math.min(y,h);z=Math.max(z,h);E.textContent=h+" MB ("+y+"-"+z+")";p(F,b/c)}return a},update:function(){k=this.end()}}};"object"===typeof module&&(module.exports=Stats); 6 | -------------------------------------------------------------------------------- /src/gravity/events.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.events 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [cljs.core.async :refer [! chan]] 4 | [gravity.tools :refer [log]])) 5 | 6 | 7 | 8 | (defn create-chan 9 | "Return a simple chan 10 | - avoiding a :require into core 11 | - centralizing the creation" 12 | [] 13 | (chan 512)) 14 | 15 | 16 | (defn create-store 17 | "Create an atom and two closures ':on' and ':get-callbacks'. 18 | - Use :on to store a callback 19 | - Use :get-callbacks to retrive a deref of the callbacks atom map. 20 | Warning : the keys are transformed to keywords." 21 | [] 22 | (let [callbacks-map (atom {})] 23 | 24 | ;;return 'on' 25 | {:on (fn [cb-key callback] 26 | (swap! callbacks-map assoc (keyword cb-key) callback)) 27 | :off (fn 28 | ([] 29 | (reset! callbacks-map {})) 30 | ([cb-key] 31 | (swap! callbacks-map assoc (keyword cb-key) nil)) ) 32 | 33 | :get-callbacks (fn [] @callbacks-map)})) 34 | 35 | 36 | 37 | 38 | ;; Events triggers 39 | 40 | (defn- get-callbacks 41 | "Extract and execute the :get-callbacks closure from the store" 42 | [store] 43 | (let [deref-callbacks (:get-callbacks store)] 44 | (deref-callbacks))) 45 | 46 | 47 | 48 | (defn- call 49 | [callback & args] 50 | (when-not (nil? callback) 51 | (apply callback args))) 52 | 53 | 54 | (defn- trigger 55 | "Trigger an event taking no arguments" 56 | ([callback-key store] 57 | (trigger callback-key store nil nil)) 58 | 59 | ([callback-key store object-to-pass] 60 | (trigger callback-key store object-to-pass nil)) 61 | 62 | ([callback-key store object-to-pass original-event] 63 | (let [store (get-callbacks store) 64 | callback (callback-key store)] 65 | (call callback object-to-pass original-event)))) 66 | 67 | 68 | 69 | 70 | 71 | 72 | ;; dispatcher 73 | 74 | (defn listen-outgoing-events 75 | "Listen to an output chan and trigger the appropriate callbacks if found in the events-store." 76 | [chan-out store] 77 | (let [state (atom {:target nil})] 78 | (go 79 | (while true 80 | (let [event (js {:type flag}))) 12 | ([worker flag data] 13 | (.postMessage worker (clj->js { :type flag 14 | :data data})))) 15 | 16 | 17 | (defn make-worker! 18 | [path] 19 | (log (str "A worker will be created to the given path '" path "'. If the canvas stay empty, be sure to check the path.")) 20 | (new js/Worker path)) 21 | 22 | (defn create 23 | "Create a webworker with the given script path" 24 | [path params] 25 | (when-not path 26 | (warn (str "Invalid worker path ! Unable to create a graph without a correct worker file specified. (null)"))) 27 | (let [worker (make-worker! path)] 28 | (if worker 29 | (send worker :init params) 30 | (err (str "Invalid worker for path '" path "'. Graph creation will fail."))) 31 | worker)) 32 | 33 | (defn listen 34 | "Listen to a given worker by setting the given function as a callback of onMessage" 35 | [worker callback] 36 | (.addEventListener worker "message" callback)) 37 | 38 | 39 | (defn serialize 40 | "Serialize a value (number, function, etc…) to be evaluated by another thread" 41 | [value] 42 | (str "(" (.toString value) ")")) 43 | -------------------------------------------------------------------------------- /src/gravity/force/worker.cljs: -------------------------------------------------------------------------------- 1 | ;; (when-not (undefined? js/self.importScripts) 2 | ;; (.importScripts js/self "../libs/d3.js" "../libs/d3.layout.force3d.js")) 3 | 4 | ;;--------------------------------- 5 | 6 | (ns gravity.force.worker 7 | (:refer-clojure :exclude [str force]) 8 | (:import [goog.object])) 9 | 10 | (defn answer 11 | "Post a message back" 12 | ([message] 13 | (.postMessage js/self (clj->js message))) 14 | ([message data] 15 | (.postMessage js/self (clj->js message) (clj->js data)))) 16 | 17 | 18 | 19 | 20 | (defn- get-args 21 | "Return the first arg or all the list as a js-obj" 22 | [coll] 23 | (if (= (count coll) 1) 24 | (clj->js (first coll)) 25 | (clj->js coll))) 26 | 27 | (defn log 28 | "Log in the console" 29 | [args] 30 | (.log js/console "[force.worker/log]: " (get-args args))) 31 | 32 | (defn warn 33 | "Warn in the console" 34 | [args] 35 | (.warn js/console "[force.worker/warn]: " (get-args args))) 36 | 37 | (defn str 38 | [& args] 39 | (let [arr (clj->js args)] 40 | (.join arr ""))) 41 | 42 | (defn eval 43 | [value] 44 | (js/eval value)) 45 | 46 | 47 | (def force (atom nil)) 48 | (def parameters (atom nil)) 49 | 50 | ;; -------------------------------- 51 | 52 | 53 | 54 | 55 | (defn tick 56 | "Tick function for the force layout" 57 | [_] 58 | (let [nodes (.nodes @force) 59 | size (.-length nodes)] 60 | ;;(log [(first nodes)]) 61 | (when (> size 0) 62 | (let [arr (new js/Float32Array (* size 3)) 63 | buffer (.-buffer arr)] 64 | (loop [i 0] 65 | (let [j (* i 3) 66 | node (aget nodes i)] 67 | (aset arr j (.-x node)) 68 | (aset arr (+ j 1) (.-y node)) 69 | (if (js/isNaN (.-z node)) 70 | (aset arr (+ j 2) 0) 71 | (aset arr (+ j 2) (.-z node)))) 72 | (when (< i (dec size)) 73 | (recur (inc i)))) 74 | 75 | (answer {:type "nodes-positions" :data arr} [buffer]))))) 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | (defn init 87 | [params] 88 | (let [params (js->clj params :keywordize-keys true)] 89 | (reset! parameters params) 90 | (answer {:type :ready}) 91 | nil)) 92 | 93 | (defn make-force 94 | [] 95 | (let [params @parameters 96 | force-instance (-> (.force3d js/d3.layout) 97 | (.size (clj->js (:size params))) 98 | (.linkStrength (:linkStrength params)) 99 | (.friction (:friction params)) 100 | (.linkDistance (:linkDistance params)) 101 | (.charge (:charge params)) 102 | (.gravity (:gravity params)) 103 | (.theta (:theta params)) 104 | (.alpha (:alpha params)) 105 | )] 106 | (.on force-instance "tick" tick) 107 | force-instance)) 108 | 109 | 110 | (defn- update-nodes-array 111 | "Add or remove the correct amount of nodes and keep their positions" 112 | [current-array nb-nodes] 113 | (let [size (count current-array)] 114 | (if (= size nb-nodes) 115 | current-array 116 | ;else 117 | (if (< size nb-nodes) 118 | (let [diff (- nb-nodes size)] 119 | (doseq [i (range 0 diff)] 120 | (.push current-array #js {})) 121 | current-array) 122 | ;else (> size nb-nodes) 123 | (let [diff (- size nb-nodes)] 124 | (.splice current-array nb-nodes diff) 125 | current-array))))) 126 | 127 | 128 | 129 | (defn start 130 | "start the force" 131 | [] 132 | (when-not (nil? @force) 133 | (.start @force))) 134 | 135 | (defn stop 136 | "Stop the force" 137 | [] 138 | (when-not (nil? @force) 139 | (.stop @force))) 140 | 141 | (defn resume 142 | "Resume the force" 143 | [] 144 | (when-not (nil? @force) 145 | (.resume @force))) 146 | 147 | (defn set-nodes 148 | "Set the nodes list" 149 | [nb-nodes] 150 | (stop) 151 | (let [new-force (make-force) 152 | nodes (if-not (nil? @force) 153 | (.nodes @force) 154 | (array)) 155 | nodes (update-nodes-array nodes nb-nodes)] 156 | (.nodes new-force nodes) 157 | (reset! force new-force) 158 | (start))) 159 | 160 | 161 | (defn set-links 162 | "Set the links list" 163 | [links] 164 | (stop) 165 | (when-not (nil? @force) 166 | (.links @force links) 167 | (start))) 168 | 169 | 170 | (defn precompute 171 | "Force the layout to precompute" 172 | [steps] 173 | (if (or (< steps 0) (nil? steps)) 174 | (do 175 | (.log js/console "Precomputing layout with default value. Argument given was <0. Expected unsigned integer, Given:" steps ) 176 | (precompute 50)) 177 | (do 178 | (let [start (.now js/Date)] 179 | (.on @force "tick" nil) 180 | (dotimes [i steps] 181 | (.tick @force)) 182 | (.on @force "tick" tick) 183 | (log (str "Pre-computed in " (/ (- (.now js/Date) start) 1000) "ms."))) 184 | ))) 185 | 186 | 187 | 188 | 189 | 190 | 191 | (defn set-position 192 | "Set a node's position" 193 | [data] 194 | (let [index (-> data .-index) 195 | position (-> data .-position) 196 | node (aget (.nodes @force) index) 197 | alpha (.alpha @force)] 198 | 199 | (stop) 200 | 201 | ;;(when-not (> alpha 0) 202 | ;;(.alpha @force 0.01)) 203 | 204 | (set! (.-x node) (.-x position)) 205 | (set! (.-y node) (.-y position)) 206 | (set! (.-z node) (.-z position)) 207 | 208 | 209 | (set! (.-px node) (.-x position)) 210 | (set! (.-py node) (.-y position)) 211 | (set! (.-pz node) (.-z position)) 212 | 213 | ;;(set! (.-fixed node) false) 214 | 215 | (tick nil) 216 | )) 217 | 218 | 219 | (defn pin 220 | "pin a node by index" 221 | [data] 222 | (let [index (-> data .-index) 223 | node (aget (.nodes @force) index)] 224 | (set! (.-fixed node) true))) 225 | 226 | 227 | (defn unpin 228 | "unpin a node by index" 229 | [data] 230 | (let [index (-> data .-index) 231 | node (aget (.nodes @force) index)] 232 | (set! (.-fixed node) false))) 233 | 234 | 235 | 236 | 237 | (defn dispatcher 238 | "Dispatch a message to the corresponding action (route)." 239 | [event] 240 | 241 | (let [message (.-data event) 242 | type (.-type message) 243 | data (.-data message)] 244 | (case type 245 | "init" (init data) 246 | "start" (start) 247 | "stop" (stop) 248 | "resume" (resume) 249 | "tick" (tick nil) 250 | "set-nodes" (set-nodes data) 251 | "set-links" (set-links data) 252 | "precompute" (precompute data) 253 | 254 | "set-position" (set-position data) 255 | "pin" (pin data) 256 | "unpin" (unpin data) 257 | 258 | ;set params 259 | "size" (swap! parameters assoc :size (js->clj data)) 260 | "linkStrength" (swap! parameters assoc :linkStrength (eval data)) 261 | "friction" (swap! parameters assoc :friction data) 262 | "linkDistance" (swap! parameters assoc :linkDistance (eval data)) 263 | "charge" (swap! parameters assoc :charge (eval data)) 264 | "gravity" (swap! parameters assoc :gravity data) 265 | "theta" (swap! parameters assoc :theta data) 266 | "alpha" (swap! parameters assoc :alpha data) 267 | 268 | (warn (str "Unable to dispatch '" type "'"))))) 269 | 270 | 271 | ;; :size [1 1] 272 | ;; :linkStrength 1 273 | ;; :friction 0.9 274 | ;; :linkDistance 20 275 | ;; :charge -30 276 | ;; :gravity 0.1 277 | ;; :theta 0.8 278 | ;; :alpha 0.1 279 | 280 | 281 | 282 | (defn ^:export create 283 | "Main entry point" 284 | [] 285 | (.addEventListener js/self "message" dispatcher)) 286 | -------------------------------------------------------------------------------- /src/gravity/graph.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-always gravity.graph 2 | (:require [gravity.view.graph :as graph] 3 | [gravity.view.graph-tools :as tools] 4 | [gravity.events :as events] 5 | [gravity.force.proxy :as worker] 6 | [gravity.force.worker :as webworker] 7 | [gravity.tools :refer [log]])) 8 | 9 | (enable-console-print!) 10 | 11 | (defonce app-state (atom {})) 12 | 13 | 14 | (def default-parameters {:color (.category10 js/d3.scale) 15 | :worker-path "./gravity-worker.js" 16 | :stats false 17 | :force {:size [1 1 1] 18 | :linkStrength 1 19 | :friction 0.9 20 | :linkDistance 20 21 | :charge -30 22 | :gravity 0.1 23 | :theta 0.8 24 | :alpha 0.1} 25 | :webgl {:antialias true 26 | :background false 27 | :lights true 28 | :shadows true}}) 29 | 30 | 31 | 32 | (defn bind-dev-events 33 | [graph] 34 | 35 | (let [{on :on 36 | canvas :canvas} graph] 37 | (on "node-over" (fn [node] 38 | (log :over) 39 | (set! (-> canvas .-style .-cursor) "pointer"))) 40 | (on "node-blur" (fn [] 41 | (log :blur) 42 | (set! (-> canvas .-style .-cursor) "inherit"))) 43 | (on "node-select" (fn [node] 44 | (log :void) 45 | (log [:select (.-name node) node]))) 46 | (on "void-click" (fn [] 47 | (log [:void]))) 48 | (on "node-click" (fn [node] 49 | (log :node-click) 50 | (let [select (:selectNode graph)] 51 | (select node)))) 52 | (on "node-dbl-click" (fn [node] 53 | (log :dbl-click) 54 | (let [unpin (:unpinNode graph) 55 | resume (:resume graph)] 56 | (unpin node) 57 | (resume)))) 58 | (on "drag-start" (fn [node] 59 | (log :drag-start))) 60 | (on "drag-end" (fn [node] 61 | (log :drag-end) 62 | (log node) 63 | (let [pin (:pinNode graph) 64 | resume (:resume graph)] 65 | (pin node) 66 | (resume)))) 67 | (on "ready" (fn [] 68 | (let [set-nodes (:nodes graph) 69 | set-links (:links graph) 70 | update-force (:updateForce graph) 71 | 72 | nodes (clj->js [{:name "foo" :group 0} {:name "bar" :group 1}]) 73 | links (clj->js [{:source 0 :target 1}])] 74 | (set-nodes nodes) 75 | (set-links links) 76 | (update-force) 77 | ))))) 78 | 79 | 80 | (defn unbind-old-events 81 | [last-instance] 82 | (let [off (-> last-instance .-off)] 83 | (when-not (nil? off) 84 | (off)))) 85 | 86 | 87 | 88 | (defn init-parameters 89 | [user-map] 90 | (let [user-map (js->clj user-map :keywordize-keys true) 91 | webgl-params (:webgl user-map) 92 | force-params (:force user-map) 93 | color (if (:color user-map) 94 | (:color user-map) 95 | (:color default-parameters)) 96 | force-merged (merge (:force default-parameters) force-params) 97 | webgl-merged (merge (:webgl default-parameters) webgl-params)] 98 | {:color color 99 | :force force-merged 100 | :webgl webgl-merged})) 101 | 102 | 103 | (defn- main 104 | 105 | ([user-map] 106 | (let [graph (main user-map false)] 107 | (clj->js graph))) 108 | 109 | ([user-map dev-mode] 110 | 111 | (let [chan-out (events/create-chan) 112 | store (events/create-store) 113 | graph (graph/create user-map chan-out dev-mode) ;; <-- 114 | graph (-> graph 115 | (merge store) 116 | (dissoc :get-callbacks))] 117 | (events/listen-outgoing-events chan-out store) 118 | (bind-dev-events graph) 119 | graph))) 120 | 121 | 122 | 123 | 124 | (defn on-js-reload 125 | ([] 126 | (let [state @app-state 127 | last-instance (:last-instance state)] 128 | 129 | (when-not (or (nil? last-instance) (empty? last-instance)) 130 | (unbind-old-events last-instance) 131 | (apply (:stop last-instance) [])) 132 | 133 | 134 | (let [graph (main state true)] 135 | (swap! app-state assoc-in [:last-instance] graph) 136 | (swap! app-state assoc :first-run false) 137 | 138 | graph)))) 139 | 140 | 141 | 142 | 143 | (defn ^:export init-dev-mode 144 | "Set some params to use live-reload in dev mode" 145 | [user-map] 146 | (let [user-map (js->clj user-map :keywordize-keys true) 147 | dev-app-state {:stats (tools/make-stats) 148 | :last-instance {} 149 | :first-run true} 150 | params (init-parameters user-map) 151 | state (merge dev-app-state user-map params)] 152 | (reset! app-state state) 153 | (swap! app-state assoc :force-worker (worker/create "force-worker/worker.js" (:force state))) 154 | 155 | (clj->js 156 | (on-js-reload)))) 157 | 158 | 159 | 160 | (defn ^:export create 161 | [user-map] 162 | (let [user-map (js->clj user-map :keywordize-keys true) 163 | params (init-parameters user-map) 164 | state (if (:stats user-map) 165 | (merge user-map params {:stats (tools/make-stats)}) 166 | (merge user-map params)) 167 | 168 | graph (main state false)] 169 | (clj->js graph))) 170 | 171 | (def ^:export create-worker gravity.force.worker/create) 172 | -------------------------------------------------------------------------------- /src/gravity/tools.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.tools) 2 | 3 | (defn- get-args 4 | "Return the first arg or all the list as a js-obj" 5 | [coll] 6 | (if (= (count coll) 1) 7 | (clj->js (first coll)) 8 | (clj->js coll))) 9 | 10 | 11 | (defn log 12 | "Log in the console" 13 | [& args] 14 | (.log js/console (get-args args)) 15 | ) 16 | 17 | (defn warn 18 | "Warn in the console" 19 | [& args] 20 | (.warn js/console (get-args args))) 21 | 22 | (defn err 23 | "Error in the console" 24 | [& args] 25 | (.error js/console (get-args args))) 26 | -------------------------------------------------------------------------------- /src/gravity/view/events_generator.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.view.events-generator 2 | "Events listeners on the canvas, mouse, etc…" 3 | (:require 4 | [gravity.tools :refer [log]] 5 | [gravity.view.graph-tools :as tools] 6 | [gravity.force.proxy :as force :refer [send]] 7 | [cljs.core.async :refer [chan >! (.-clientX event) 40 | (- (.-left bounding-rect)) 41 | (/ scene-width);;(.-offsetWidth canvas)) 42 | (* 2) 43 | (- 1)) 44 | y (-> (.-clientY event) 45 | (- (.-top bounding-rect)) 46 | (-) 47 | (/ scene-height);;(.-offsetHeight canvas)) 48 | (* 2) 49 | (+ 1)) 50 | cam-position (.-position camera)] 51 | (.set mouse-pos x y 1) 52 | (.unproject mouse-pos camera) 53 | (.set raycaster cam-position (.normalize (.sub mouse-pos cam-position))) 54 | ;;return 55 | (first (.intersectObjects raycaster objects))))) 56 | 57 | 58 | 59 | 60 | 61 | 62 | (defn- move 63 | "Callback for the mouseMove event on the canvas node" 64 | [event canvas camera raycaster state chan events-state controls intersect-plane] 65 | (let [colliders (:meshes @state) 66 | target (get-target event canvas camera raycaster colliders)] 67 | (if-not (nil? target) 68 | (let [node (.-node (.-object target))] 69 | ;; disable controls 70 | (set! (-> controls .-enabled) false) 71 | ;; move plane 72 | (.copy (-> intersect-plane .-position) (-> node .-position)) 73 | (.lookAt intersect-plane (-> camera .-position)) 74 | ;; send event to the user 75 | (when-not (= :node-over (:last @events-state)) 76 | (swap! events-state assoc :last :node-over) 77 | (go 78 | (>! chan {:type :node-over 79 | :target node 80 | :original-event event})))) 81 | ;else 82 | (when (or 83 | (= :node-over (:last @events-state)) 84 | (= :drag (:last @events-state)) 85 | (= :up (:last @events-state))) 86 | (set! (-> controls .-enabled) true) 87 | (swap! events-state assoc :last :blur) 88 | (go (>! chan {:type :node-blur 89 | :original-event event})))))) 90 | 91 | 92 | (defn- click 93 | "click event" 94 | [event canvas camera raycaster state chan] 95 | (log "click") 96 | (let [colliders (:meshes @state) 97 | target (get-target event canvas camera raycaster colliders)] 98 | (when-not (nil? target) 99 | (let [node (-> target .-object .-node)] 100 | (go (>! chan {:type :node-click 101 | :target node 102 | :original-event event})))) 103 | false)) 104 | 105 | 106 | (defn double-click 107 | "Callback for the click event" 108 | [event canvas camera raycaster state chan] 109 | 110 | (let [colliders (:meshes @state) 111 | target (get-target event canvas camera raycaster colliders)] 112 | (when-not (nil? target) 113 | (let [node (.-node (.-object target))] 114 | (go (>! chan {:type :node-dbl-click 115 | :target node 116 | :original-event event}))))) 117 | false) 118 | 119 | 120 | 121 | 122 | 123 | (defn- drag 124 | [event canvas camera raycaster events-state intersect-plane force-worker chan-out] 125 | (let [node (:target @events-state)] 126 | (when-not (nil? node) 127 | (let [node (-> node .-object) 128 | index (-> node .-node .-index) 129 | intersect (get-target event canvas camera raycaster (array intersect-plane))] 130 | (when-not (nil? intersect) 131 | ;;(.copy (-> node .-position) (-> intersect .-point)) 132 | (force/send force-worker :set-position {:index index 133 | :position (-> intersect .-point)}) 134 | (when (= :down (:last @events-state)) 135 | (force/send force-worker "stop") 136 | (go (>! chan-out {:type :drag-start 137 | :target node 138 | :original-event event}))) 139 | (swap! events-state assoc :last :drag) 140 | ))))) 141 | 142 | 143 | 144 | 145 | (defn- down 146 | [event canvas camera raycaster state events-state force-worker] 147 | (let [colliders (:meshes @state) 148 | target (get-target event canvas camera raycaster colliders)] 149 | (when-not (nil? target) 150 | (force/send force-worker "stop") 151 | (swap! events-state assoc :last :down) 152 | (swap! events-state assoc :target target)))) 153 | 154 | 155 | (defn- up 156 | [event events-state force-worker chan-out] 157 | (when (= :drag (:last @events-state)) 158 | (let [target (:target @events-state)] 159 | (log "drag-end-before") 160 | (when-not (nil? target) 161 | (log "drag-end-with-target") 162 | (go (>! chan-out {:type :drag-end 163 | :target (-> target .-object .-node) 164 | :original-event event}))))) 165 | (swap! events-state assoc :last :up) 166 | (swap! events-state dissoc :target)) 167 | 168 | 169 | 170 | 171 | (defn notify-user-ready 172 | [chan] 173 | (go (>! chan {:type :ready}))) 174 | 175 | 176 | 177 | 178 | 179 | 180 | ;; Events factory 181 | 182 | 183 | 184 | (defn- listen-to-mouse-events 185 | "Take chans with events from the dom and alt! them to generate meaningful events." 186 | [mouse-down mouse-up mouse-move] 187 | (let [timeout-time 350 188 | 189 | out-chan (chan 10) 190 | events-state (atom {})] 191 | (go-loop [] 192 | (loop [] 193 | (alt! [mouse-down] ([transducted] (do 194 | (swap! events-state assoc :event :down) 195 | (go (>! out-chan (merge {:type :down} transducted))) 196 | 197 | (js/setTimeout #(swap! events-state assoc :event nil) timeout-time) 198 | nil)) 199 | 200 | ;; We stay in this loop while the mouse move without mousedown 201 | [mouse-move] ([transducted] (do 202 | (go (>! out-chan (merge {:type :move} transducted))) 203 | (recur))))) 204 | (loop [nb-drags 0] 205 | (alt! [mouse-up] ([transducted] (do 206 | (go (>! out-chan (merge {:type :up} transducted))) 207 | 208 | (when (= :down (:event @events-state)) 209 | ;; the last event was a :down -> we trigger a click 210 | ;; if we already had a click before it's a double-click 211 | (if (and 212 | (:last-was-a-click @events-state) 213 | (= (:coords transducted) (:last-coords @events-state))) 214 | (go 215 | (swap! events-state assoc :last-was-a-click false) 216 | (>! out-chan (merge {:type :double-click} transducted))) 217 | ;else it's a simple click 218 | (go (swap! events-state assoc :last-was-a-click true) 219 | (>! out-chan (merge {:type :click} transducted)) 220 | (js/setTimeout #(swap! events-state assoc :last-was-a-click false) timeout-time)))) 221 | 222 | (swap! events-state assoc :event :up) 223 | (swap! events-state assoc :last-coords (:coords transducted)) 224 | 225 | nil)) 226 | [mouse-move] ([transducted] 227 | (do 228 | (when (> nb-drags 3) 229 | (swap! events-state assoc :last-was-a-click false) 230 | (swap! events-state assoc :event :drag) 231 | (swap! events-state assoc :last-coords (:coords transducted)) 232 | (go (>! out-chan (merge {:type :drag} transducted)))) 233 | (recur (inc nb-drags)))))) 234 | (recur)) 235 | out-chan 236 | )) 237 | 238 | 239 | (defn listen-to-canvas 240 | "Listen to a canvas and return a chan of events." 241 | [canvas] 242 | (let [transduct-mouse (map (fn [e] 243 | ;;(.preventDefault e) 244 | ;;(.stopPropagation e) 245 | {:event e 246 | :coords {:x (.-offsetX e) 247 | :y (.-offsetY e)}})) 248 | mousedown-chan (chan (sliding-buffer 1) transduct-mouse) 249 | mouseup-chan (chan (sliding-buffer 1) transduct-mouse) 250 | mousemove-chan (chan (sliding-buffer 1) transduct-mouse)] 251 | 252 | (.addEventListener canvas "mousedown" (fn [e] (put! mousedown-chan e) false)) 253 | (.addEventListener canvas "mousemove" (fn [e] (put! mousemove-chan e) false)) 254 | (.addEventListener canvas "mouseup" (fn [e] (put! mouseup-chan e) false)) 255 | 256 | (listen-to-mouse-events mousedown-chan mouseup-chan mousemove-chan))) 257 | 258 | 259 | 260 | 261 | (defn apply-events-to 262 | [mouse canvas camera raycaster intersect-plane controls state force-worker chan-out] 263 | (let [events-state (atom {})] 264 | (go-loop [] 265 | (let [event ( old-node .-selected) false) 291 | (.remove (-> old-node .-mesh) circle) 292 | (set! (-> circle .-visible) true)) 293 | (when-not (nil? new-node) 294 | (set! (-> new-node .-selected) true) 295 | (.add (-> new-node .-mesh) circle) 296 | (set! (-> circle .-visible) true)))) 297 | 298 | 299 | (defn watch-state 300 | [state watch-id] 301 | (add-watch state watch-id 302 | (fn [id state old-state new-state] 303 | (put-select-circle-on-node old-state new-state) 304 | ))) 305 | -------------------------------------------------------------------------------- /src/gravity/view/graph.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.view.graph 2 | (:require 3 | [gravity.tools :refer [log]] 4 | [gravity.view.node :as node] 5 | [gravity.view.nodeset :as points] 6 | [gravity.view.graph-tools :as tools] 7 | [gravity.view.events-generator :as events] 8 | [gravity.force.proxy :as worker])) 9 | 10 | 11 | 12 | 13 | 14 | 15 | ;; Init view's parameters 16 | 17 | 18 | (defn get-components 19 | "Generate or re-use all the necessary components of the 3D view" 20 | [user-map dev-mode] 21 | (if (:canvas user-map) 22 | (tools/fill-window! (:canvas user-map))) 23 | 24 | (let [webgl-params (:webgl user-map) 25 | width (.-width (:canvas user-map)) 26 | height (.-height (:canvas user-map)) 27 | camera (new js/THREE.PerspectiveCamera 75 (/ width height) 0.1 100000 )] 28 | 29 | (set! (.-z (.-position camera)) 300) 30 | 31 | { :scene (new js/THREE.Scene) 32 | :width width 33 | :height height 34 | :camera camera 35 | :stats (:stats user-map) 36 | :controls (new js/THREE.OrbitControls camera) 37 | :renderer (new js/THREE.WebGLRenderer #js {:antialias (:antialias webgl-params) 38 | :canvas (:canvas user-map)}) 39 | :raycaster (new THREE.Raycaster) 40 | :classifier (:color user-map) 41 | :force-worker (if (:force-worker user-map) 42 | (:force-worker user-map) 43 | (worker/create (:worker-path user-map) (:force user-map))) 44 | 45 | :state (atom {:should-run true}) 46 | :first-run (:first-run user-map)})) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ;; CALLBACKS 55 | 56 | 57 | 58 | (defn- render-callback 59 | "Return a function rendering the context" 60 | [renderer scene camera stats state controls select-circle] 61 | (fn render [] 62 | 63 | (.update controls) 64 | 65 | (when-not (nil? stats) 66 | (.begin stats)) 67 | 68 | (if (get @state :should-run) 69 | (do 70 | (when-not (nil? select-circle) 71 | (let [x1 (-> select-circle .-rotation .-x) 72 | y1 (-> select-circle .-rotation .-y) 73 | x2 (+ x1 0.01) 74 | y2 (+ y1 0.1)] 75 | (.set (-> select-circle .-rotation) x2 y2 0))) 76 | (.requestAnimationFrame js/window render) 77 | (.render renderer scene camera))) 78 | 79 | (when-not (nil? stats) 80 | (.end stats)))) 81 | 82 | 83 | 84 | 85 | (defn- start-callback! 86 | "Return a closure affecting the application state atom 87 | and triggering the render function once" 88 | [state render] 89 | (fn [] 90 | (when-not (:should-run @state) 91 | (swap! state assoc :should-run true) 92 | (render)) 93 | nil)) 94 | 95 | 96 | 97 | (defn- stop-callback! 98 | "Return a closure affecting the application state atom" 99 | [state] 100 | (fn [] 101 | (swap! state assoc :should-run false) nil)) 102 | 103 | (defn- resume-force-callback 104 | "Send a resume event to the force worker" 105 | [force-worker] 106 | (fn [] 107 | (worker/send force-worker "resume"))) 108 | 109 | 110 | 111 | 112 | 113 | (defn- set-links 114 | "Remove the old links and add the new ones" 115 | [state scene links] 116 | (let [old-links (:links-set @state) 117 | nodes (:nodes @state) 118 | new-links (points/create-links nodes links)] 119 | (.remove scene old-links) 120 | (.add scene new-links) 121 | (swap! state assoc :links links) 122 | (swap! state assoc :links-set new-links) 123 | links)) 124 | 125 | (defn- set-nodes 126 | "Remove the old nodes and add the new ones" 127 | [state scene nodes] 128 | (let [classifier (:classifier @state) 129 | old-nodes (:nodes @state) 130 | {nodes :nodes 131 | meshes :meshes} (points/prepare-nodes nodes classifier)] 132 | (doseq [old old-nodes] 133 | (.remove scene (-> old .-mesh))) 134 | (doseq [new-node meshes] 135 | (.add scene new-node)) 136 | (swap! state assoc :nodes nodes) 137 | (swap! state assoc :meshes meshes) 138 | (set-links state scene []) 139 | nodes)) 140 | 141 | 142 | 143 | (defn- set-nodes-callback 144 | "Allow the user to replace nodes with a new set of nodes" 145 | [state scene] 146 | (fn [nodes] 147 | (if (nil? nodes) 148 | (:nodes @state) 149 | (set-nodes state scene nodes)))) 150 | 151 | (defn- set-links-callback 152 | [state scene] 153 | (fn [links] 154 | (if (nil? links) 155 | (:links @state) 156 | (set-links state scene links)))) 157 | 158 | 159 | 160 | 161 | (defn- update-force-callback 162 | [state force-worker] 163 | (fn [] 164 | (let [nodes (:nodes @state) 165 | links (:links @state)] 166 | (worker/send force-worker "set-nodes" (count nodes)) 167 | (worker/send force-worker "set-links" links) 168 | ;;(worker/send force-worker "start") 169 | ))) 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | ;; USE ALL THE ABOVE 180 | 181 | 182 | (defn init-force 183 | [force-worker dev-mode first-run] 184 | (when (and (not first-run) dev-mode) 185 | (worker/send force-worker "tick"))) 186 | 187 | 188 | (defn create 189 | "Initialise a context in the specified element id" 190 | [user-map chan-out dev-mode] 191 | (let [{ first-run :first-run 192 | scene :scene 193 | width :width 194 | height :height 195 | camera :camera 196 | stats :stats 197 | controls :controls 198 | renderer :renderer 199 | raycaster :raycaster 200 | classifier :classifier 201 | force-worker :force-worker 202 | state :state} (get-components user-map dev-mode) 203 | 204 | canvas (if-not (nil? (:canvas user-map)) 205 | (:canvas user-map) 206 | (.-domElement renderer)) 207 | 208 | select-circle (tools/get-circle) 209 | intersect-plane (tools/get-intersect-plane) 210 | ;;data-test (demo/get-demo-graph) 211 | ;;{nodes :nodes 212 | ;; meshes :meshes} (points/prepare-nodes (.-nodes data-test) classifier) 213 | 214 | ;;links (.-links data-test) 215 | ;;nodeset (points/create nodes classifier) 216 | ;;links-set (points/create-links nodes links) 217 | render (render-callback renderer scene camera stats state controls select-circle)] 218 | 219 | 220 | ;; (swap! state assoc :nodes nodes) 221 | ;; (swap! state assoc :meshes meshes) 222 | ;; (swap! state assoc :links links) 223 | ;; (swap! state assoc :links-set links-set) 224 | (swap! state assoc :classifier classifier) 225 | (swap! state assoc :select-circle select-circle) 226 | 227 | 228 | ;; renderer 229 | (.setSize renderer width height) 230 | (.setClearColor renderer 0x404040) 231 | 232 | ;;shadows 233 | (when (:shadows (:webgl user-map)) 234 | (set! (-> renderer .-shadowMap .-enabled) true) 235 | (set! (-> renderer .-shadowMap .-type) js/THREE.PCFSoftShadowMap)) 236 | 237 | 238 | 239 | (worker/listen force-worker (fn [event] 240 | (let [message (.-data event) 241 | type (.-type message) 242 | data (.-data message)] 243 | (case type 244 | "ready" (do 245 | (init-force force-worker dev-mode first-run) 246 | (events/notify-user-ready chan-out)) 247 | "nodes-positions" (let [state @state] 248 | (points/update-positions! (:nodes state) data) 249 | ;;(points/update nodeset) 250 | (points/update-geometry (:links-set state)) 251 | ))))) 252 | 253 | 254 | 255 | 256 | ;; if it's not the first time in dev mode 257 | (when (and (not first-run) dev-mode) 258 | (tools/fill-window! canvas) 259 | (.removeEventListener canvas "mousemove") 260 | (.removeEventListener canvas "click") 261 | (events/notify-user-ready chan-out)) 262 | 263 | 264 | ;;(worker/send force-worker "precompute" 50) 265 | 266 | (.add scene select-circle) 267 | (set! (-> select-circle .-visible) false) 268 | 269 | (.add scene intersect-plane) 270 | 271 | (.addEventListener js/window "resize" (events/onWindowResize canvas renderer camera)) 272 | 273 | 274 | 275 | (let [;mouse (events/listen-to-canvas canvas) 276 | mouse (events/listen-to-canvas js/window.document)] 277 | (events/apply-events-to mouse canvas camera raycaster intersect-plane controls state force-worker chan-out)) 278 | 279 | 280 | 281 | 282 | 283 | 284 | (let [webgl-params (:webgl user-map) 285 | background? (:background webgl-params) 286 | lights? (:lights webgl-params)] 287 | ;; add background 288 | (when background? 289 | (.add scene (tools/get-background))) 290 | 291 | ;; add lights 292 | 293 | (doseq [light (tools/get-lights lights?)] 294 | (.add scene light))) 295 | 296 | 297 | (events/watch-state state :main-watcher) 298 | 299 | 300 | (render) 301 | 302 | ;; return closures to user 303 | 304 | {:start (start-callback! state render) 305 | :stop (stop-callback! state) 306 | :resume (resume-force-callback force-worker) 307 | :tick (fn [] (worker/send force-worker "tick")) 308 | :canvas (.-domElement renderer) 309 | :stats stats 310 | :nodes (set-nodes-callback state scene) 311 | :links (set-links-callback state scene) 312 | :updateForce (update-force-callback state force-worker) 313 | 314 | :force {:size (fn [array] (worker/send force-worker "size" array)) 315 | :linkStrength (fn [val] (worker/send force-worker "linkStrength" (worker/serialize val))) 316 | :friction (fn [val] (worker/send force-worker "friction" val)) 317 | :linkDistance (fn [val] (worker/send force-worker "linkDistance" (worker/serialize val))) 318 | :charge (fn [val] (worker/send force-worker "charge" (worker/serialize val))) 319 | :gravity (fn [val] (worker/send force-worker "gravity" val)) 320 | :theta (fn [val] (worker/send force-worker "theta" val)) 321 | :alpha (fn [val] (worker/send force-worker "alpha" val))} 322 | 323 | :selectNode (fn [node] 324 | (swap! state assoc :selected node) 325 | (:selected @state)) 326 | :pinNode (fn [node] 327 | (let [circle (tools/get-circle) 328 | mesh (-> node .-mesh)] 329 | (set! (-> mesh .-circle) circle) 330 | (.add mesh circle)) 331 | (worker/send force-worker "pin" {:index (-> node .-index)})) 332 | :unpinNode (fn [node] 333 | (tools/remove-children (-> node .-mesh)) 334 | (.remove (-> node .-mesh) (-> node .-mesh .-circle)) 335 | (worker/send force-worker "unpin" {:index (-> node .-index)})) 336 | :camera camera 337 | })) 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | -------------------------------------------------------------------------------- /src/gravity/view/graph_tools.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.view.graph-tools 2 | "Contain tools like selection animation, lights, background, etc…" 3 | 4 | (:require [gravity.tools :refer [log]])) 5 | 6 | 7 | 8 | (defn fill-window! 9 | "Resize a canvas to make it fill the window" 10 | [canvas] 11 | (set! (-> canvas .-style .-width) "100%") 12 | (set! (-> canvas .-style .-height) "100%") 13 | (let [width (.-offsetWidth canvas) 14 | height (.-offsetHeight canvas)] 15 | (set! (.-width canvas) width) 16 | (set! (.-height canvas) height)) 17 | nil) 18 | 19 | 20 | 21 | (defn make-stats 22 | "Create a stat view to monitor performances" 23 | [] 24 | (let [stats (new js/Stats) 25 | style (-> stats 26 | (.-domElement) 27 | (.-style))] 28 | (set! (.-position style) "absolute") 29 | (set! (.-left style) "0px") 30 | (set! (.-top style) "0px") 31 | stats)) 32 | 33 | (defn make-fake-stats 34 | "Create a fake object that will be returned in place of stats 35 | Used in prod mode" 36 | [] 37 | #js {:domElement nil}) 38 | 39 | 40 | 41 | 42 | (defn- get-background 43 | "Generate a gray sphere as a background" 44 | [] 45 | (let [material (new js/THREE.MeshLambertMaterial #js {:color 0xa0a0a0 46 | ;:ambiant 0xffffff 47 | :side 1}) 48 | geometry (new js/THREE.SphereGeometry 20 20 20) 49 | background (new js/THREE.Mesh geometry material)] 50 | (.set (.-scale background) 100 100 100) 51 | (set! (.-receiveShadow background) true) 52 | background)) 53 | 54 | 55 | 56 | (defn- get-flat-light 57 | "Generate an ambient light" 58 | [] 59 | (new js/THREE.AmbientLight 0xffffff)) 60 | 61 | 62 | (defn- get-spot-lights 63 | [] 64 | (let [color (new js/THREE.Color 0xffffff) 65 | strength 0.8 66 | shadow-map 2048 67 | positions [[-1000 1000 1000] 68 | [1000 1000 1000] 69 | [-1000 -1000 1000] 70 | [1000 -1000 1000] 71 | 72 | [1000 1000 -1000]] 73 | lights (map (fn [pos] 74 | (let [light (new js/THREE.SpotLight color strength) 75 | [x y z] pos] 76 | (.set (.-position light) x y z) 77 | (set! (.-shadowCameraFar light) 4000) 78 | 79 | light)) 80 | positions)] 81 | (let [main-light (first lights)] 82 | (set! (.-castShadow main-light) true) 83 | (set! (.-shadowCameraVisible main-light) false)) 84 | 85 | lights)) 86 | 87 | 88 | (defn get-lights 89 | "Make light(s) for the scene. If spots is true, generate directional lights" 90 | [spots] 91 | (if-not spots 92 | (conj [] (get-flat-light)) 93 | (get-spot-lights))) 94 | 95 | 96 | 97 | (defn get-circle 98 | "Return a circle meant to be placed and animated on a node." 99 | ([] 100 | (get-circle 32 15)) 101 | 102 | ([nb-segments radius] 103 | (let [material (new js/THREE.LineBasicMaterial #js {:color 0xff0000}) 104 | geometry (new js/THREE.Geometry)] 105 | (doseq [i (range nb-segments)] 106 | (let [theta (-> i 107 | (/ nb-segments) 108 | (* Math/PI) 109 | (* 2)) 110 | cos (-> (Math/cos theta) 111 | (* radius)) 112 | sin (-> (Math/sin theta) 113 | (* radius)) 114 | vect (new js/THREE.Vector3 cos sin 0)] 115 | (.push (-> geometry .-vertices) vect))) 116 | ;; close circle 117 | (.push (-> geometry .-vertices) (aget (-> geometry .-vertices) 0)) 118 | ;ret 119 | (new js/THREE.Line geometry material)))) 120 | 121 | 122 | 123 | 124 | 125 | (defn get-intersect-plane 126 | "Return a big plane filling the sphere. Used to drag nodes" 127 | [] 128 | (let [side 4000 129 | material (new js/THREE.MeshBasicMaterial #js {:wireframe true 130 | :visible false}) 131 | geometry (new js/THREE.PlaneGeometry side side 1 1) 132 | mesh (new js/THREE.Mesh geometry material)] 133 | mesh)) 134 | 135 | 136 | 137 | 138 | 139 | (defn remove-children 140 | [object3D] 141 | (loop [len (dec (-> object3D .-children .-length)) 142 | i (range 0 len)] 143 | 144 | (.remove object3D (aget (-> object3D .-children) 0)) 145 | 146 | (when (< i len) 147 | (recur len (inc i)))) 148 | nil) 149 | -------------------------------------------------------------------------------- /src/gravity/view/node.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.view.node) 2 | 3 | 4 | 5 | (defn- get-color ;; TODO 6 | "Give a color for a given node" 7 | [node classifier] 8 | (let [key (.-group node)] 9 | (new js/THREE.Color (classifier key)))) 10 | 11 | (def get-unique-color 12 | (memoize get-color)) 13 | 14 | 15 | 16 | (defn get-rand-pos 17 | "Give a random position between -extent and +extent" 18 | [extent] 19 | (-> (* 2 extent) 20 | (rand) 21 | (- extent))) 22 | 23 | 24 | 25 | (defn generate-geometry 26 | "Generate a generic geometry" 27 | [] 28 | (new js/THREE.SphereGeometry 10 10 10)) 29 | 30 | 31 | (def get-unique-geometry 32 | (memoize generate-geometry)) 33 | 34 | (defn generate-material 35 | "Generate a generic material" 36 | [color-key] 37 | (new js/THREE.MeshLambertMaterial (clj->js {:color (new js/THREE.Color color-key) 38 | :visible true 39 | :wireframe false}))) 40 | 41 | 42 | (def get-unique-material 43 | (memoize generate-material)) 44 | 45 | 46 | (defn generate-collider 47 | "create and return a new node mesh used for collisions" 48 | [node classifier] 49 | (let [geometry (get-unique-geometry) 50 | material (get-unique-material (get-unique-color node classifier)) 51 | sphere (new js/THREE.Mesh geometry material)] 52 | (.set (.-scale sphere) 0.45 0.45 0.45) 53 | sphere)) 54 | 55 | 56 | 57 | (defn create 58 | "Return a cloned node with a random position and a collider object" 59 | [node classifier index] 60 | (let [ext 2000 61 | node (.clone js/goog.object node) 62 | collider (generate-collider node classifier) 63 | position (new js/THREE.Vector3 (get-rand-pos ext) (get-rand-pos ext) 0)] 64 | (set! (.-index node) index) 65 | (set! (.-position node) position) 66 | (set! (.-mesh node) collider) 67 | (set! (.-castShadow collider) true) 68 | (set! (.-node collider) node) 69 | (set! (.-selected node) false) 70 | node)) 71 | 72 | -------------------------------------------------------------------------------- /src/gravity/view/nodeset.cljs: -------------------------------------------------------------------------------- 1 | (ns gravity.view.nodeset 2 | (:refer-clojure :exclude [update]) 3 | (:require [gravity.tools :refer [log]] 4 | [gravity.view.node :as node])) 5 | 6 | 7 | (declare get-unique-color) 8 | (declare get-shader-material) 9 | 10 | 11 | 12 | 13 | (defn create-links 14 | "Given a js-array of nodes and a js array of links, will return a THREE.LineSegments" 15 | [nodes links] 16 | (let [geometry (new js/THREE.Geometry) 17 | vertices (.-vertices geometry) 18 | material (new js/THREE.LineBasicMaterial #js {"color" 0xfafafa}) 19 | system (new js/THREE.LineSegments geometry material) 20 | size (dec (.-length links))] 21 | (doseq [link links] 22 | (let [source (get nodes (.-source link)) 23 | target (get nodes (.-target link))] 24 | (.push vertices (.-position source)) 25 | (.push vertices (.-position target)))) 26 | (set! (.-verticesNeedUpdate geometry) true) 27 | (set! (.-castShadow system) true) 28 | system)) 29 | 30 | (defn prepare-nodes 31 | "Create a array of cloned nodes containing a position and a collider object. 32 | Return a map {nodes[] colliders[]} meant to be destructured. 33 | The nodes and the colliders are in the same order and share the same position Vector3." 34 | [nodes classifier] 35 | (let [counter (atom 0) 36 | pairs (map (fn [node] 37 | (let [index @counter 38 | prepared-node (node/create node classifier index) 39 | mesh (.-mesh prepared-node)] 40 | (swap! counter inc) 41 | [prepared-node mesh])) 42 | nodes)] 43 | {:nodes (clj->js (mapv first pairs)) 44 | :meshes (clj->js (mapv last pairs))})) 45 | 46 | 47 | (defn update-geometry 48 | "Update a nodeset Points geometry or a LineSegments geometry" 49 | [geom-based-item] 50 | (set! (.-verticesNeedUpdate (.-geometry geom-based-item)) true) 51 | geom-based-item) 52 | 53 | 54 | (defn update-positions! 55 | "Update the nodes' positions according to the raw array given by the force" 56 | [nodes positions] 57 | (let [size (dec (.-length nodes))] 58 | (loop [i 0] 59 | (let [j (* i 3) 60 | node (get nodes i) 61 | x (aget positions j) 62 | y (aget positions (+ j 1)) 63 | z (aget positions (+ j 2))] 64 | (.set (-> node .-position) x y z) 65 | (.set (-> node .-mesh .-position) x y z) 66 | (when (< i size) 67 | (recur (inc i))))))) 68 | 69 | 70 | ; Colors & shape 71 | 72 | 73 | (defn get-color ;; TODO 74 | "Give a color for a given node" 75 | [classifier node] 76 | (let [key (.-group node)] 77 | (new js/THREE.Color (classifier key)))) 78 | 79 | (def get-unique-color 80 | (memoize get-color)) 81 | 82 | --------------------------------------------------------------------------------