├── .babelrc ├── .gitignore ├── README.md ├── app ├── bin │ ├── GPUPicker.js │ └── TrackballControls.js ├── images │ ├── 2_no_clouds_4k.jpg │ ├── earthbump1k.jpg │ ├── earthcloudmap.jpg │ ├── earthcloudmaptrans.jpg │ ├── earthmap1k.jpg │ ├── earthspec1k.jpg │ ├── elev_bump_4k.jpg │ ├── eso_dark.jpg │ ├── fair_clouds_4k.png │ ├── galaxy_starfield.png │ ├── jupitermap.jpg │ ├── marsbump1k.jpg │ ├── marsmap1k.jpg │ ├── mercurybump.jpg │ ├── mercurymap.jpg │ ├── moonbump1k.jpg │ ├── moonmap1k.jpg │ ├── neptunemap.jpg │ ├── plutobump1k.jpg │ ├── plutomap1k.jpg │ ├── saturnmap.jpg │ ├── saturnringcolor.jpg │ ├── saturnringpattern.gif │ ├── sunmap.jpg │ ├── sunsprite.png │ ├── uranusmap.jpg │ ├── uranusringcolour.jpg │ ├── uranusringtrans.gif │ ├── venusbump.jpg │ ├── venusmap.jpg │ └── water_4k.png ├── js │ ├── APIClient.js │ ├── Observable.js │ ├── controller │ │ └── GalaxyController.js │ ├── model │ │ ├── AstronomicalBody.js │ │ ├── Galaxy.js │ │ ├── Planet.js │ │ ├── SolarSystem.js │ │ └── Sun.js │ └── view │ │ ├── MainView.js │ │ ├── RenderingContext.js │ │ ├── ViewMediatorFactory.js │ │ ├── controls │ │ ├── DescriptionPanel.js │ │ └── ObjectPicker.js │ │ └── mediator │ │ ├── GalaxyViewMediator.js │ │ ├── PlanetViewMediator.js │ │ ├── SolarSystemViewMediator.js │ │ ├── SunViewMediator.js │ │ └── ViewMediator.js ├── main.js └── template.html ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | npm-debug.log 4 | node_modules/ 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THREE.JS MVC Example 2 | ======== 3 | [Example](https://lucasmajerowicz.github.io/threejs-mvc-example/app/) that implements the MVC approach for creating Three.js applications 4 | 5 | ## Installation 6 | Clone repository and run 7 | 8 | ``` 9 | npm install 10 | ``` 11 | 12 | ## Usage 13 | Run webpack dev server 14 | 15 | ``` 16 | npm run server 17 | ``` -------------------------------------------------------------------------------- /app/bin/GPUPicker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @author baoxuanxu https://github.com/brianxu 4 | */ 5 | ( function ( THREE ) { 6 | var FaceIDShader = { 7 | vertexShader: [ 8 | "attribute float id;", 9 | "", 10 | "uniform float size;", 11 | "uniform float scale;", 12 | "uniform float baseId;", 13 | "", 14 | "varying vec4 worldId;", 15 | "", 16 | "void main() {", 17 | " vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", 18 | " gl_PointSize = size * ( scale / length( mvPosition.xyz ) );", 19 | " float i = baseId + id;", 20 | " vec3 a = fract(vec3(1.0/255.0, 1.0/(255.0*255.0), 1.0/(255.0*255.0*255.0)) * i);", 21 | " a -= a.xxy * vec3(0.0, 1.0/255.0, 1.0/255.0);", 22 | " worldId = vec4(a,1);", 23 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 24 | "}" 25 | ].join("\n"), 26 | 27 | fragmentShader: [ 28 | "#ifdef GL_ES\n", 29 | "precision highp float;\n", 30 | "#endif\n", 31 | "", 32 | "varying vec4 worldId;", 33 | "", 34 | "void main() {", 35 | " gl_FragColor = worldId;", 36 | "}" 37 | ].join("\n") 38 | }; 39 | 40 | var FaceIDMaterial = function() { 41 | THREE.ShaderMaterial.call(this, { 42 | uniforms: { 43 | baseId: { 44 | type: "f", 45 | value: 0 46 | }, 47 | size: { 48 | type: "f", 49 | value: 0.01, 50 | }, 51 | scale: { 52 | type: "f", 53 | value: 400, 54 | } 55 | }, 56 | vertexShader: FaceIDShader.vertexShader, 57 | fragmentShader: FaceIDShader.fragmentShader 58 | 59 | }); 60 | }; 61 | FaceIDMaterial.prototype = Object.create(THREE.ShaderMaterial.prototype); 62 | FaceIDMaterial.prototype.constructor = FaceIDMaterial; 63 | FaceIDMaterial.prototype.setBaseID = function(baseId) { 64 | this.uniforms.baseId.value = baseId; 65 | }; 66 | FaceIDMaterial.prototype.setPointSize = function(size) { 67 | this.uniforms.size.value = size; 68 | }; 69 | FaceIDMaterial.prototype.setPointScale = function(scale) { 70 | this.uniforms.scale.value = scale; 71 | }; 72 | 73 | //add a originalObject to Object3D 74 | (function(clone) { 75 | THREE.Object3D.prototype.clone = function ( object, recursive ) { 76 | object = clone.call(this, object, recursive); 77 | 78 | // keep a ref to originalObject 79 | object.originalObject = this; 80 | object.priority = this.priority; 81 | 82 | return object; 83 | }; 84 | }(THREE.Object3D.prototype.clone)); 85 | 86 | THREE.Mesh.prototype.raycastWithID = ( function() { 87 | var vA = new THREE.Vector3(); 88 | var vB = new THREE.Vector3(); 89 | var vC = new THREE.Vector3(); 90 | var inverseMatrix = new THREE.Matrix4(); 91 | var ray = new THREE.Ray(); 92 | var triangle = new THREE.Triangle(); 93 | 94 | return function(elID, raycaster) { 95 | var geometry = this.geometry; 96 | var attributes = geometry.attributes; 97 | inverseMatrix.getInverse(this.matrixWorld); 98 | ray.copy(raycaster.ray).applyMatrix4(inverseMatrix); 99 | var a, b, c; 100 | if (attributes.index !== undefined) { 101 | console.log("WARNING: raycastWithID does not support indexed vertices"); 102 | } else { 103 | var positions = attributes.position.array; 104 | var j = elID * 9; 105 | vA.fromArray(positions, j); 106 | vB.fromArray(positions, j + 3); 107 | vC.fromArray(positions, j + 6); 108 | } 109 | triangle.set(vA, vB, vC); 110 | var intersectionPoint = ray.intersectPlane(triangle.plane()); 111 | 112 | if (intersectionPoint === null) { 113 | return null; 114 | } 115 | 116 | intersectionPoint.applyMatrix4(this.matrixWorld); 117 | 118 | var distance = raycaster.ray.origin.distanceTo(intersectionPoint); 119 | 120 | if (distance < raycaster.near || distance > raycaster.far) return; 121 | 122 | var intersect = { 123 | 124 | distance: distance, 125 | point: intersectionPoint, 126 | face: new THREE.Face3(a, b, c, THREE.Triangle.normal(vA, vB, vC)), 127 | index: elID, // triangle number in positions buffer semantics 128 | object: this 129 | 130 | }; 131 | return intersect; 132 | }; 133 | 134 | }() ); 135 | 136 | THREE.Line.prototype.raycastWithID = ( function () { 137 | var inverseMatrix = new THREE.Matrix4(); 138 | var ray = new THREE.Ray(); 139 | 140 | var vStart = new THREE.Vector3(); 141 | var vEnd = new THREE.Vector3(); 142 | var interSegment = new THREE.Vector3(); 143 | var interRay = new THREE.Vector3(); 144 | return function (elID, raycaster) { 145 | inverseMatrix.getInverse( this.matrixWorld ); 146 | ray.copy( raycaster.ray ).applyMatrix4( inverseMatrix ); 147 | var geometry = this.geometry; 148 | if ( geometry instanceof THREE.BufferGeometry ) { 149 | 150 | var attributes = geometry.attributes; 151 | 152 | if ( attributes.index !== undefined ) { 153 | console.log("WARNING: raycastWithID does not support indexed vertices"); 154 | } else { 155 | 156 | var positions = attributes.position.array; 157 | var i = elID * 6; 158 | vStart.fromArray(positions, i); 159 | vEnd.fromArray(positions, i + 3); 160 | 161 | var distSq = ray.distanceSqToSegment( vStart, vEnd, interRay, interSegment ); 162 | var distance = ray.origin.distanceTo( interRay ); 163 | 164 | if ( distance < raycaster.near || distance > raycaster.far ) return; 165 | 166 | var intersect = { 167 | 168 | distance: distance, 169 | // What do we want? intersection point on the ray or on the segment?? 170 | // point: raycaster.ray.at( distance ), 171 | point: interSegment.clone().applyMatrix4( this.matrixWorld ), 172 | index: i, 173 | face: null, 174 | faceIndex: null, 175 | object: this 176 | 177 | }; 178 | return intersect; 179 | 180 | } 181 | 182 | } 183 | }; 184 | 185 | })(); 186 | 187 | THREE.PointCloud.prototype.raycastWithID = ( function () { 188 | 189 | var inverseMatrix = new THREE.Matrix4(); 190 | var ray = new THREE.Ray(); 191 | 192 | return function ( elID, raycaster ) { 193 | var object = this; 194 | var geometry = object.geometry; 195 | 196 | inverseMatrix.getInverse( this.matrixWorld ); 197 | ray.copy( raycaster.ray ).applyMatrix4( inverseMatrix ); 198 | var position = new THREE.Vector3(); 199 | 200 | var testPoint = function ( point, index ) { 201 | var rayPointDistance = ray.distanceToPoint( point ); 202 | var intersectPoint = ray.closestPointToPoint( point ); 203 | intersectPoint.applyMatrix4( object.matrixWorld ); 204 | 205 | var distance = raycaster.ray.origin.distanceTo( intersectPoint ); 206 | 207 | if ( distance < raycaster.near || distance > raycaster.far ) return; 208 | 209 | var intersect = { 210 | 211 | distance: distance, 212 | distanceToRay: rayPointDistance, 213 | point: intersectPoint.clone(), 214 | index: index, 215 | face: null, 216 | object: object 217 | 218 | }; 219 | return intersect; 220 | }; 221 | var attributes = geometry.attributes; 222 | var positions = attributes.position.array; 223 | position.fromArray( positions, elID * 3 ); 224 | 225 | return testPoint( position, elID ); 226 | 227 | }; 228 | 229 | }() ); 230 | 231 | THREE.GPUPicker = function(option) { 232 | if (option === undefined){ 233 | option = {}; 234 | } 235 | this.pickingScene = new THREE.Scene(); 236 | this.pickingTexture = new THREE.WebGLRenderTarget(); 237 | this.pickingTexture.minFilter = THREE.LinearFilter; 238 | this.pickingTexture.generateMipmaps = false; 239 | this.lineShell = option.lineShell !== undefined ? option.lineShell:4; 240 | this.pointShell = option.pointShell !== undefined ? option.pointShell:0.1; 241 | this.debug = option.debug !== undefined ? option.debug:false; 242 | this.needUpdate = true; 243 | if (option.renderer) { 244 | this.setRenderer(option.renderer); 245 | } 246 | 247 | // array of original objects 248 | this.container = []; 249 | this.objectsMap = {}; 250 | //default filter 251 | this.setFilter(); 252 | }; 253 | THREE.GPUPicker.prototype.setRenderer = function(renderer) { 254 | this.renderer = renderer; 255 | var size = renderer.getSize(); 256 | this.resizeTexture(size.width, size.height); 257 | this.needUpdate = true; 258 | }; 259 | THREE.GPUPicker.prototype.resizeTexture = function(width, height) { 260 | this.pickingTexture.setSize(width, height); 261 | this.pixelBuffer = new Uint8Array(4 * width * height); 262 | this.needUpdate = true; 263 | }; 264 | THREE.GPUPicker.prototype.setCamera = function(camera) { 265 | this.camera = camera; 266 | this.needUpdate = true; 267 | }; 268 | THREE.GPUPicker.prototype.update = function() { 269 | if (this.needUpdate){ 270 | this.renderer.render(this.pickingScene, this.camera, this.pickingTexture); 271 | //read the rendering texture 272 | this.renderer.readRenderTargetPixels(this.pickingTexture, 0, 0, this.pickingTexture.width, this.pickingTexture.height, this.pixelBuffer); 273 | this.needUpdate = false; 274 | if (this.debug) console.log("GPUPicker rendering updated"); 275 | } 276 | }; 277 | THREE.GPUPicker.prototype.setFilter = function(func) { 278 | if (func instanceof Function){ 279 | this.filterFunc = func; 280 | } else { 281 | //default filter 282 | this.filterFunc = function (object) { 283 | return true; 284 | }; 285 | } 286 | 287 | }; 288 | THREE.GPUPicker.prototype.setScene = function(scene) { 289 | this.pickingScene = scene.clone(); 290 | this._processObject(this.pickingScene, 0); 291 | this.needUpdate = true; 292 | }; 293 | 294 | 295 | THREE.GPUPicker.prototype.pick = function(mouse, raycaster) { 296 | this.update(); 297 | var index = mouse.x + (this.pickingTexture.height - mouse.y) * this.pickingTexture.width; 298 | //interpret the pixel as an ID 299 | var id = (this.pixelBuffer[index*4+2] * 255 * 255) + (this.pixelBuffer[index*4+1] * 255) + (this.pixelBuffer[index*4+0]); 300 | // get object with this id in range 301 | // var object = this._getObject(id); 302 | if (this.debug) console.log("pick id:",id); 303 | var result = this._getObject(this.pickingScene, 0, id); 304 | var object = result[1]; 305 | var elementId = id - result[0]; 306 | if (object) { 307 | if (object.raycastWithID) { 308 | var intersect = object.raycastWithID(elementId, raycaster); 309 | 310 | if (intersect) { 311 | intersect.object = object; 312 | } 313 | return intersect; 314 | } 315 | 316 | } 317 | return; 318 | }; 319 | 320 | /* 321 | * get object by id 322 | */ 323 | THREE.GPUPicker.prototype._getObject = function(object, baseId, id) { 324 | // if (this.debug) console.log("_getObject ",baseId); 325 | if (object.elementsCount !== undefined && id >= baseId && id < baseId + object.elementsCount) { 326 | return [baseId, object]; 327 | } 328 | if (object.elementsCount !== undefined){ 329 | baseId += object.elementsCount; 330 | } 331 | var result = [baseId, undefined]; 332 | for ( var i = 0; i < object.children.length; i ++ ) { 333 | result = this._getObject(object.children[ i ], result[0], id); 334 | if(result[1] !== undefined) 335 | break; 336 | } 337 | return result; 338 | }; 339 | 340 | /* 341 | * process the object to add elementId information 342 | */ 343 | THREE.GPUPicker.prototype._processObject = function(object, baseId) { 344 | baseId += this._addElementID(object, baseId); 345 | for ( var i = 0; i < object.children.length; i ++ ) { 346 | baseId = this._processObject(object.children[ i ], baseId); 347 | 348 | } 349 | return baseId; 350 | }; 351 | 352 | THREE.GPUPicker.prototype._addElementID = function(object, baseId) { 353 | if (!this.filterFunc(object) && object.geometry !== undefined) { 354 | object.visible = false; 355 | return 0; 356 | } 357 | 358 | if (object.geometry) { 359 | var __pickingGeometry; 360 | //check if geometry has cached geometry for picking 361 | if (object.geometry.__pickingGeometry) { 362 | __pickingGeometry = object.geometry.__pickingGeometry; 363 | } else { 364 | __pickingGeometry = object.geometry; 365 | // convert geometry to buffer geometry 366 | if (object.geometry instanceof THREE.Geometry) { 367 | if (this.debug) console.log("convert geometry to buffer geometry"); 368 | __pickingGeometry = new THREE.BufferGeometry().setFromObject(object); 369 | } 370 | var units = 1; 371 | if (object instanceof THREE.PointCloud) { 372 | units = 1; 373 | } else if (object instanceof THREE.Line) { 374 | units = 2; 375 | } else if (object instanceof THREE.Mesh) { 376 | units = 3; 377 | } 378 | var el, el3, elementsCount, i, indices, positionBuffer, vertex3, verts, vertexIndex3; 379 | if (__pickingGeometry.attributes.index !== undefined) { 380 | __pickingGeometry = __pickingGeometry.clone(); 381 | if (this.debug) console.log("convert indexed geometry to non-indexed geometry"); 382 | 383 | indices = __pickingGeometry.attributes.index.array; 384 | verts = __pickingGeometry.attributes.position.array; 385 | delete __pickingGeometry.attributes.position; 386 | delete __pickingGeometry.attributes.index; 387 | delete __pickingGeometry.attributes.normal; 388 | elementsCount = indices.length / units; 389 | positionBuffer = new Float32Array(elementsCount * 3 * units); 390 | 391 | __pickingGeometry.addAttribute('position', new THREE.BufferAttribute(positionBuffer, 3)); 392 | for (el = 0; el < elementsCount; ++el) { 393 | el3 = units * el; 394 | for (i = 0; i < units; ++i) { 395 | vertexIndex3 = 3 * indices[el3 + i]; 396 | vertex3 = 3 * (el3 + i); 397 | positionBuffer[vertex3] = verts[vertexIndex3]; 398 | positionBuffer[vertex3 + 1] = verts[vertexIndex3 + 1]; 399 | positionBuffer[vertex3 + 2] = verts[vertexIndex3 + 2]; 400 | } 401 | } 402 | 403 | __pickingGeometry.computeVertexNormals(); 404 | } 405 | if (object instanceof THREE.Line && !(object instanceof THREE.LineSegments)) { 406 | if (this.debug) console.log("convert Line to LineSegments"); 407 | verts = __pickingGeometry.attributes.position.array; 408 | delete __pickingGeometry.attributes.position; 409 | elementsCount = verts.length / 3 -1 ; 410 | positionBuffer = new Float32Array(elementsCount * units * 3); 411 | 412 | __pickingGeometry.addAttribute('position', new THREE.BufferAttribute(positionBuffer, 3)); 413 | for (el = 0; el < elementsCount; ++el) { 414 | el3 = 3 * el; 415 | vertexIndex3 = el3; 416 | vertex3 = el3*2; 417 | positionBuffer[vertex3] = verts[vertexIndex3]; 418 | positionBuffer[vertex3 + 1] = verts[vertexIndex3 + 1]; 419 | positionBuffer[vertex3 + 2] = verts[vertexIndex3 + 2]; 420 | positionBuffer[vertex3 + 3] = verts[vertexIndex3 + 3]; 421 | positionBuffer[vertex3 + 4] = verts[vertexIndex3 + 4]; 422 | positionBuffer[vertex3 + 5] = verts[vertexIndex3 + 5]; 423 | 424 | } 425 | 426 | __pickingGeometry.computeVertexNormals(); 427 | object.__proto__ = THREE.LineSegments.prototype; //make the renderer render as line segments 428 | } 429 | var attributes = __pickingGeometry.attributes; 430 | var positions = attributes.position.array; 431 | var vertexCount = positions.length/3; 432 | var ids = new THREE.Float32Attribute(vertexCount, 1); 433 | //set vertex id color 434 | 435 | for (var i = 0, il = vertexCount/units; i < il; i++) { 436 | for (var j = 0; j < units; ++j) { 437 | ids.array[i*units + j] = i; 438 | } 439 | } 440 | __pickingGeometry.addAttribute('id', ids); 441 | __pickingGeometry.elementsCount = vertexCount/units; 442 | //cache __pickingGeometry inside geometry 443 | object.geometry.__pickingGeometry = __pickingGeometry; 444 | } 445 | 446 | //use __pickingGeometry in the picking mesh 447 | object.geometry = __pickingGeometry; 448 | object.elementsCount = __pickingGeometry.elementsCount;//elements count 449 | 450 | var pointSize = object.material.size || 0.01; 451 | var linewidth = object.material.linewidth || 1; 452 | object.material = new FaceIDMaterial(); 453 | object.material.linewidth = linewidth + this.lineShell;//make the line a little wider to hit 454 | object.material.setBaseID(baseId); 455 | object.material.setPointSize(pointSize + this.pointShell);//make the point a little wider to hit 456 | object.material.setPointScale(this.renderer.getSize().height*this.renderer.getPixelRatio()/2); 457 | return object.elementsCount; 458 | } 459 | return 0; 460 | }; 461 | }( THREE ) ); 462 | -------------------------------------------------------------------------------- /app/bin/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | */ 4 | 5 | THREE.TrackballControls = function ( object, domElement ) { 6 | 7 | var _this = this; 8 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM: 4, TOUCH_PAN: 5 }; 9 | 10 | this.object = object; 11 | this.domElement = ( domElement !== undefined ) ? domElement : document; 12 | 13 | // API 14 | 15 | this.enabled = true; 16 | 17 | this.screen = { width: 0, height: 0, offsetLeft: 0, offsetTop: 0 }; 18 | this.radius = ( this.screen.width + this.screen.height ) / 4; 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 | 28 | this.staticMoving = false; 29 | this.dynamicDampingFactor = 0.2; 30 | 31 | this.minDistance = 0; 32 | this.maxDistance = Infinity; 33 | 34 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 35 | 36 | // internals 37 | 38 | this.target = new THREE.Vector3(); 39 | 40 | var lastPosition = new THREE.Vector3(); 41 | 42 | var _state = STATE.NONE, 43 | _prevState = STATE.NONE, 44 | 45 | _eye = new THREE.Vector3(), 46 | 47 | _rotateStart = new THREE.Vector3(), 48 | _rotateEnd = new THREE.Vector3(), 49 | 50 | _zoomStart = new THREE.Vector2(), 51 | _zoomEnd = new THREE.Vector2(), 52 | 53 | _touchZoomDistanceStart = 0, 54 | _touchZoomDistanceEnd = 0, 55 | 56 | _panStart = new THREE.Vector2(), 57 | _panEnd = new THREE.Vector2(); 58 | 59 | // for reset 60 | 61 | this.target0 = this.target.clone(); 62 | this.position0 = this.object.position.clone(); 63 | this.up0 = this.object.up.clone(); 64 | 65 | // events 66 | 67 | var changeEvent = { type: 'change' }; 68 | 69 | 70 | // methods 71 | 72 | this.handleResize = function () { 73 | 74 | this.screen.width = window.innerWidth; 75 | this.screen.height = window.innerHeight; 76 | 77 | this.screen.offsetLeft = 0; 78 | this.screen.offsetTop = 0; 79 | 80 | this.radius = ( this.screen.width + this.screen.height ) / 4; 81 | 82 | }; 83 | 84 | this.handleEvent = function ( event ) { 85 | 86 | if ( typeof this[ event.type ] == 'function' ) { 87 | 88 | this[ event.type ]( event ); 89 | 90 | } 91 | 92 | }; 93 | 94 | this.getMouseOnScreen = function ( clientX, clientY ) { 95 | 96 | return new THREE.Vector2( 97 | ( clientX - _this.screen.offsetLeft ) / _this.radius * 0.5, 98 | ( clientY - _this.screen.offsetTop ) / _this.radius * 0.5 99 | ); 100 | 101 | }; 102 | 103 | this.getMouseProjectionOnBall = function ( clientX, clientY ) { 104 | 105 | var mouseOnBall = new THREE.Vector3( 106 | ( clientX - _this.screen.width * 0.5 - _this.screen.offsetLeft ) / _this.radius, 107 | ( _this.screen.height * 0.5 + _this.screen.offsetTop - clientY ) / _this.radius, 108 | 0.0 109 | ); 110 | 111 | var length = mouseOnBall.length(); 112 | 113 | if ( length > 1.0 ) { 114 | 115 | mouseOnBall.normalize(); 116 | 117 | } else { 118 | 119 | mouseOnBall.z = Math.sqrt( 1.0 - length * length ); 120 | 121 | } 122 | 123 | _eye.copy( _this.object.position ).sub( _this.target ); 124 | 125 | var projection = _this.object.up.clone().setLength( mouseOnBall.y ); 126 | projection.add( _this.object.up.clone().cross( _eye ).setLength( mouseOnBall.x ) ); 127 | projection.add( _eye.setLength( mouseOnBall.z ) ); 128 | 129 | return projection; 130 | 131 | }; 132 | 133 | this.rotateCamera = function () { 134 | 135 | var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() ); 136 | 137 | if ( angle ) { 138 | 139 | var axis = ( new THREE.Vector3() ).crossVectors( _rotateStart, _rotateEnd ).normalize(); 140 | var quaternion = new THREE.Quaternion(); 141 | 142 | angle *= _this.rotateSpeed; 143 | 144 | quaternion.setFromAxisAngle( axis, -angle ); 145 | 146 | _eye.applyQuaternion( quaternion ); 147 | _this.object.up.applyQuaternion( quaternion ); 148 | 149 | _rotateEnd.applyQuaternion( quaternion ); 150 | 151 | if ( _this.staticMoving ) { 152 | 153 | _rotateStart.copy( _rotateEnd ); 154 | 155 | } else { 156 | 157 | quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) ); 158 | _rotateStart.applyQuaternion( quaternion ); 159 | 160 | } 161 | 162 | } 163 | 164 | }; 165 | 166 | this.zoomCamera = function () { 167 | 168 | if ( _state === STATE.TOUCH_ZOOM ) { 169 | 170 | var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 171 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 172 | _eye.multiplyScalar( factor ); 173 | 174 | } else { 175 | 176 | var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 177 | 178 | if ( factor !== 1.0 && factor > 0.0 ) { 179 | 180 | _eye.multiplyScalar( factor ); 181 | 182 | if ( _this.staticMoving ) { 183 | 184 | _zoomStart.copy( _zoomEnd ); 185 | 186 | } else { 187 | 188 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 189 | 190 | } 191 | 192 | } 193 | 194 | } 195 | 196 | }; 197 | 198 | this.panCamera = function () { 199 | 200 | var mouseChange = _panEnd.clone().sub( _panStart ); 201 | 202 | if ( mouseChange.lengthSq() ) { 203 | 204 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 205 | 206 | var pan = _eye.clone().cross( _this.object.up ).setLength( mouseChange.x ); 207 | pan.add( _this.object.up.clone().setLength( mouseChange.y ) ); 208 | 209 | _this.object.position.add( pan ); 210 | _this.target.add( pan ); 211 | 212 | if ( _this.staticMoving ) { 213 | 214 | _panStart = _panEnd; 215 | 216 | } else { 217 | 218 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 219 | 220 | } 221 | 222 | } 223 | 224 | }; 225 | 226 | this.checkDistances = function () { 227 | 228 | if ( !_this.noZoom || !_this.noPan ) { 229 | 230 | if ( _this.object.position.lengthSq() > _this.maxDistance * _this.maxDistance ) { 231 | 232 | _this.object.position.setLength( _this.maxDistance ); 233 | 234 | } 235 | 236 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 237 | 238 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 239 | 240 | } 241 | 242 | } 243 | 244 | }; 245 | 246 | this.update = function () { 247 | 248 | _eye.subVectors( _this.object.position, _this.target ); 249 | 250 | if ( !_this.noRotate ) { 251 | 252 | _this.rotateCamera(); 253 | 254 | } 255 | 256 | if ( !_this.noZoom ) { 257 | 258 | _this.zoomCamera(); 259 | 260 | } 261 | 262 | if ( !_this.noPan ) { 263 | 264 | _this.panCamera(); 265 | 266 | } 267 | 268 | _this.object.position.addVectors( _this.target, _eye ); 269 | 270 | _this.checkDistances(); 271 | 272 | _this.object.lookAt( _this.target ); 273 | 274 | if ( lastPosition.distanceToSquared( _this.object.position ) > 0 ) { 275 | 276 | _this.dispatchEvent( changeEvent ); 277 | 278 | lastPosition.copy( _this.object.position ); 279 | 280 | } 281 | 282 | }; 283 | 284 | this.reset = function () { 285 | 286 | _state = STATE.NONE; 287 | _prevState = STATE.NONE; 288 | 289 | _this.target.copy( _this.target0 ); 290 | _this.object.position.copy( _this.position0 ); 291 | _this.object.up.copy( _this.up0 ); 292 | 293 | _eye.subVectors( _this.object.position, _this.target ); 294 | 295 | _this.object.lookAt( _this.target ); 296 | 297 | _this.dispatchEvent( changeEvent ); 298 | 299 | lastPosition.copy( _this.object.position ); 300 | 301 | }; 302 | 303 | // listeners 304 | 305 | function keydown( event ) { 306 | 307 | if ( _this.enabled === false ) return; 308 | 309 | window.removeEventListener( 'keydown', keydown ); 310 | 311 | _prevState = _state; 312 | 313 | if ( _state !== STATE.NONE ) { 314 | 315 | return; 316 | 317 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { 318 | 319 | _state = STATE.ROTATE; 320 | 321 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { 322 | 323 | _state = STATE.ZOOM; 324 | 325 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { 326 | 327 | _state = STATE.PAN; 328 | 329 | } 330 | 331 | } 332 | 333 | function keyup( event ) { 334 | 335 | if ( _this.enabled === false ) return; 336 | 337 | _state = _prevState; 338 | 339 | window.addEventListener( 'keydown', keydown, false ); 340 | 341 | } 342 | 343 | function mousedown( event ) { 344 | 345 | if ( _this.enabled === false ) return; 346 | 347 | event.preventDefault(); 348 | event.stopPropagation(); 349 | 350 | if ( _state === STATE.NONE ) { 351 | 352 | _state = event.button; 353 | 354 | } 355 | 356 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 357 | 358 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 359 | 360 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 361 | 362 | _zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 363 | 364 | } else if ( _state === STATE.PAN && !_this.noPan ) { 365 | 366 | _panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 367 | 368 | } 369 | 370 | document.addEventListener( 'mousemove', mousemove, false ); 371 | document.addEventListener( 'mouseup', mouseup, false ); 372 | 373 | } 374 | 375 | function mousemove( event ) { 376 | 377 | if ( _this.enabled === false ) return; 378 | 379 | event.preventDefault(); 380 | event.stopPropagation(); 381 | 382 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 383 | 384 | _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY ); 385 | 386 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 387 | 388 | _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 389 | 390 | } else if ( _state === STATE.PAN && !_this.noPan ) { 391 | 392 | _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY ); 393 | 394 | } 395 | 396 | } 397 | 398 | function mouseup( event ) { 399 | 400 | if ( _this.enabled === false ) return; 401 | 402 | event.preventDefault(); 403 | event.stopPropagation(); 404 | 405 | _state = STATE.NONE; 406 | 407 | document.removeEventListener( 'mousemove', mousemove ); 408 | document.removeEventListener( 'mouseup', mouseup ); 409 | 410 | } 411 | 412 | function mousewheel( event ) { 413 | 414 | if ( _this.enabled === false ) return; 415 | 416 | event.preventDefault(); 417 | event.stopPropagation(); 418 | 419 | var delta = 0; 420 | 421 | if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 422 | 423 | delta = event.wheelDelta / 40; 424 | 425 | } else if ( event.detail ) { // Firefox 426 | 427 | delta = - event.detail / 3; 428 | 429 | } 430 | 431 | _zoomStart.y += delta * 0.01; 432 | 433 | } 434 | 435 | function touchstart( event ) { 436 | 437 | if ( _this.enabled === false ) return; 438 | 439 | switch ( event.touches.length ) { 440 | 441 | case 1: 442 | _state = STATE.TOUCH_ROTATE; 443 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 444 | break; 445 | 446 | case 2: 447 | _state = STATE.TOUCH_ZOOM; 448 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 449 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 450 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 451 | break; 452 | 453 | case 3: 454 | _state = STATE.TOUCH_PAN; 455 | _panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 456 | break; 457 | 458 | default: 459 | _state = STATE.NONE; 460 | 461 | } 462 | 463 | } 464 | 465 | function touchmove( event ) { 466 | 467 | if ( _this.enabled === false ) return; 468 | 469 | event.preventDefault(); 470 | event.stopPropagation(); 471 | 472 | switch ( event.touches.length ) { 473 | 474 | case 1: 475 | _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 476 | break; 477 | 478 | case 2: 479 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 480 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 481 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ) 482 | break; 483 | 484 | case 3: 485 | _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 486 | break; 487 | 488 | default: 489 | _state = STATE.NONE; 490 | 491 | } 492 | 493 | } 494 | 495 | function touchend( event ) { 496 | 497 | if ( _this.enabled === false ) return; 498 | 499 | switch ( event.touches.length ) { 500 | 501 | case 1: 502 | _rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 503 | break; 504 | 505 | case 2: 506 | _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; 507 | break; 508 | 509 | case 3: 510 | _panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 511 | break; 512 | 513 | } 514 | 515 | _state = STATE.NONE; 516 | 517 | } 518 | 519 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 520 | 521 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 522 | 523 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 524 | this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox 525 | 526 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 527 | this.domElement.addEventListener( 'touchend', touchend, false ); 528 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 529 | 530 | window.addEventListener( 'keydown', keydown, false ); 531 | window.addEventListener( 'keyup', keyup, false ); 532 | 533 | this.handleResize(); 534 | 535 | }; 536 | 537 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); -------------------------------------------------------------------------------- /app/images/2_no_clouds_4k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/2_no_clouds_4k.jpg -------------------------------------------------------------------------------- /app/images/earthbump1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/earthbump1k.jpg -------------------------------------------------------------------------------- /app/images/earthcloudmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/earthcloudmap.jpg -------------------------------------------------------------------------------- /app/images/earthcloudmaptrans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/earthcloudmaptrans.jpg -------------------------------------------------------------------------------- /app/images/earthmap1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/earthmap1k.jpg -------------------------------------------------------------------------------- /app/images/earthspec1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/earthspec1k.jpg -------------------------------------------------------------------------------- /app/images/elev_bump_4k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/elev_bump_4k.jpg -------------------------------------------------------------------------------- /app/images/eso_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/eso_dark.jpg -------------------------------------------------------------------------------- /app/images/fair_clouds_4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/fair_clouds_4k.png -------------------------------------------------------------------------------- /app/images/galaxy_starfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/galaxy_starfield.png -------------------------------------------------------------------------------- /app/images/jupitermap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/jupitermap.jpg -------------------------------------------------------------------------------- /app/images/marsbump1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/marsbump1k.jpg -------------------------------------------------------------------------------- /app/images/marsmap1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/marsmap1k.jpg -------------------------------------------------------------------------------- /app/images/mercurybump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/mercurybump.jpg -------------------------------------------------------------------------------- /app/images/mercurymap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/mercurymap.jpg -------------------------------------------------------------------------------- /app/images/moonbump1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/moonbump1k.jpg -------------------------------------------------------------------------------- /app/images/moonmap1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/moonmap1k.jpg -------------------------------------------------------------------------------- /app/images/neptunemap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/neptunemap.jpg -------------------------------------------------------------------------------- /app/images/plutobump1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/plutobump1k.jpg -------------------------------------------------------------------------------- /app/images/plutomap1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/plutomap1k.jpg -------------------------------------------------------------------------------- /app/images/saturnmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/saturnmap.jpg -------------------------------------------------------------------------------- /app/images/saturnringcolor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/saturnringcolor.jpg -------------------------------------------------------------------------------- /app/images/saturnringpattern.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/saturnringpattern.gif -------------------------------------------------------------------------------- /app/images/sunmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/sunmap.jpg -------------------------------------------------------------------------------- /app/images/sunsprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/sunsprite.png -------------------------------------------------------------------------------- /app/images/uranusmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/uranusmap.jpg -------------------------------------------------------------------------------- /app/images/uranusringcolour.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/uranusringcolour.jpg -------------------------------------------------------------------------------- /app/images/uranusringtrans.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/uranusringtrans.gif -------------------------------------------------------------------------------- /app/images/venusbump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/venusbump.jpg -------------------------------------------------------------------------------- /app/images/venusmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/venusmap.jpg -------------------------------------------------------------------------------- /app/images/water_4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmajerowicz/threejs-mvc-example/4357023cbf83cd122f874a68ecb8e766b8e9bfa9/app/images/water_4k.png -------------------------------------------------------------------------------- /app/js/APIClient.js: -------------------------------------------------------------------------------- 1 | export default class APIClient { 2 | getRecord() { 3 | return APIClient.galaxyRecord; 4 | } 5 | } 6 | 7 | APIClient.galaxyRecord = { 8 | name: 'Milky Way', 9 | solarSystems: [ 10 | { 11 | name: 'Solar System', 12 | props: {}, 13 | sun: { 14 | name: 'Sun', 15 | props: { 16 | radius: 2, 17 | texture: 'images/sunmap.jpg' 18 | } 19 | }, 20 | planets: [ 21 | {name: 'Mercury', 22 | props: { 23 | texture: 'images/mercurymap.jpg', 24 | orbitalSpeed: 0.01, 25 | rotationSpeed: 0.05, 26 | radius: 0.4, 27 | distance: 5 28 | } 29 | }, 30 | {name: 'Venus', 31 | props: { 32 | texture: 'images/venusmap.jpg', 33 | orbitalSpeed: 0.007, 34 | rotationSpeed: -0.05, 35 | radius: 0.8, 36 | distance: 10 37 | } 38 | }, 39 | {name: 'Earth', 40 | satellites: [ 41 | { 42 | name: 'Moon', 43 | props: { 44 | texture: 'images/moonmap1k.jpg', 45 | orbitalSpeed: -0.05, 46 | rotationSpeed: 0.01, 47 | radius: 0.4, 48 | distance: 2 49 | } 50 | } 51 | ], 52 | props: { 53 | texture: 'images/2_no_clouds_4k.jpg', 54 | orbitalSpeed: 0.006, 55 | rotationSpeed: 0.005, 56 | radius: 0.8, 57 | distance: 15 58 | } 59 | }, 60 | {name: 'Mars', 61 | props: { 62 | texture: 'images/marsmap1k.jpg', 63 | orbitalSpeed: 0.005, 64 | rotationSpeed: 0.005, 65 | radius: 0.6, 66 | distance: 20 67 | } 68 | }, 69 | {name: 'Jupiter', 70 | props: { 71 | texture: 'images/jupitermap.jpg', 72 | orbitalSpeed: 0.003, 73 | rotationSpeed: 0.0025, 74 | radius: 1.6, 75 | distance: 26 76 | } 77 | }, 78 | {name: 'Saturn', 79 | props: { 80 | texture: 'images/saturnmap.jpg', 81 | orbitalSpeed: 0.002, 82 | rotationSpeed: 0.0025, 83 | radius: 1.2, 84 | distance: 33 85 | } 86 | }, 87 | {name: 'Uranus', 88 | props: { 89 | texture: 'images/uranusmap.jpg', 90 | orbitalSpeed: 0.002, 91 | rotationSpeed: -0.004, 92 | radius: 1, 93 | distance: 40 94 | } 95 | }, 96 | {name: 'Neptune', 97 | props: { 98 | texture: 'images/neptunemap.jpg', 99 | orbitalSpeed: 0.0015, 100 | rotationSpeed: 0.004, 101 | radius: 0.9, 102 | distance: 50 103 | } 104 | }, 105 | {name: 'Pluto', 106 | props: { 107 | texture: 'images/plutomap1k.jpg', 108 | orbitalSpeed: 0.002, 109 | rotationSpeed: 0.003, 110 | radius: 0.4, 111 | distance: 60 112 | } 113 | } 114 | ] 115 | } 116 | ] 117 | }; -------------------------------------------------------------------------------- /app/js/Observable.js: -------------------------------------------------------------------------------- 1 | export default class Observable { 2 | constructor() { 3 | this.observers = new Map(); 4 | } 5 | 6 | addObserver(label, callback) { 7 | this.observers.has(label) || this.observers.set(label, []); 8 | this.observers.get(label).push(callback); 9 | } 10 | 11 | emit(label, e) { 12 | const observers = this.observers.get(label); 13 | 14 | if (observers && observers.length) { 15 | observers.forEach((callback) => { 16 | callback(e); 17 | }); 18 | } 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/js/controller/GalaxyController.js: -------------------------------------------------------------------------------- 1 | import MainView from '../view/MainView'; 2 | 3 | export default class GalaxyController { 4 | constructor(galaxy) { 5 | this.galaxy = galaxy; 6 | this.view = new MainView(this, galaxy); 7 | this.view.initialize(); 8 | } 9 | 10 | setDescriptionPanelText(astronomicalBody, event) { 11 | if (astronomicalBody) { 12 | this.view.descriptionPanel.text = `${event}: ${astronomicalBody.name}`; 13 | } else { 14 | this.view.descriptionPanel.text = `${event}: ${this.galaxy.name}`; 15 | } 16 | } 17 | 18 | onClick(astronomicalBody) { 19 | this.setDescriptionPanelText(astronomicalBody, 'Clicked'); 20 | 21 | if (astronomicalBody && astronomicalBody.className === 'Planet') { 22 | astronomicalBody.isMoving = !astronomicalBody.isMoving; 23 | } 24 | } 25 | 26 | onDoubleClick(astronomicalBody) { 27 | if (astronomicalBody) { 28 | const parentElement = astronomicalBody.parent; 29 | 30 | if (parentElement.className === 'Planet') { 31 | parentElement.removeSatellite(astronomicalBody); 32 | } else if (parentElement.className === 'SolarSystem') { 33 | parentElement.removePlanet(astronomicalBody); 34 | } 35 | } 36 | } 37 | 38 | onMouseMove(astronomicalBody) { 39 | this.setDescriptionPanelText(astronomicalBody, 'Hovered'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/js/model/AstronomicalBody.js: -------------------------------------------------------------------------------- 1 | import Observable from '../Observable'; 2 | 3 | export default class AstronomicalBody extends Observable { 4 | constructor(name, properties = {}) { 5 | super(); 6 | this.name = name; 7 | this.properties = properties; 8 | } 9 | } -------------------------------------------------------------------------------- /app/js/model/Galaxy.js: -------------------------------------------------------------------------------- 1 | import AstronomicalBody from './AstronomicalBody'; 2 | 3 | export default class Galaxy extends AstronomicalBody { 4 | constructor(name, properties) { 5 | super(name, properties); 6 | this.solarSystems = []; 7 | this.className = 'Galaxy'; 8 | } 9 | 10 | addSolarSystem(solarSystem) { 11 | this.solarSystems.push(solarSystem); 12 | this.emit('SolarSystemAdded', { solarSystem }); 13 | } 14 | 15 | [Symbol.iterator]() { 16 | return this.solarSystems.values(); 17 | } 18 | } -------------------------------------------------------------------------------- /app/js/model/Planet.js: -------------------------------------------------------------------------------- 1 | import AstronomicalBody from './AstronomicalBody'; 2 | 3 | export default class Planet extends AstronomicalBody { 4 | constructor(name, properties) { 5 | super(name, properties); 6 | this.satellites = []; 7 | this.className = 'Planet'; 8 | this.isMoving = true; 9 | } 10 | 11 | addSatellite(satellite) { 12 | satellite.parent = this; 13 | this.satellites.push(satellite); 14 | this.emit('SatelliteAdded', { satellite }); 15 | } 16 | 17 | removeSatellite(satellite) { 18 | const index = this.satellites.indexOf(satellite); 19 | 20 | if (index !== -1) { 21 | this.satellites.splice(index, 1); 22 | this.emit('SatelliteRemoved', { satellite }); 23 | } 24 | } 25 | 26 | [Symbol.iterator]() { 27 | return this.satellites.values(); 28 | } 29 | } -------------------------------------------------------------------------------- /app/js/model/SolarSystem.js: -------------------------------------------------------------------------------- 1 | import AstronomicalBody from './AstronomicalBody'; 2 | 3 | export default class SolarSystem extends AstronomicalBody { 4 | constructor(name, sun, properties) { 5 | super(name, sun, properties); 6 | this.sun = sun; 7 | this.planets = []; 8 | this.className = 'SolarSystem'; 9 | } 10 | 11 | addPlanet(planet) { 12 | planet.parent = this; 13 | this.planets.push(planet); 14 | this.emit('PlanetAdded', { planet }); 15 | } 16 | 17 | removePlanet(planet) { 18 | const index = this.planets.indexOf(planet); 19 | 20 | if (index !== -1) { 21 | this.planets.splice(index, 1); 22 | this.emit('PlanetRemoved', { planet }); 23 | } 24 | } 25 | 26 | [Symbol.iterator]() { 27 | return this.planets.values(); 28 | } 29 | } -------------------------------------------------------------------------------- /app/js/model/Sun.js: -------------------------------------------------------------------------------- 1 | import AstronomicalBody from './AstronomicalBody'; 2 | 3 | export default class Sun extends AstronomicalBody { 4 | constructor(name, properties) { 5 | super(name, properties); 6 | this.className = 'Sun'; 7 | } 8 | } -------------------------------------------------------------------------------- /app/js/view/MainView.js: -------------------------------------------------------------------------------- 1 | import DescriptionPanel from './controls/DescriptionPanel'; 2 | import ObjectPicker from './controls/ObjectPicker'; 3 | import GalaxyViewMediator from './mediator/GalaxyViewMediator'; 4 | import ViewMediatorFactory from './ViewMediatorFactory'; 5 | import RenderingContext from './RenderingContext'; 6 | 7 | export default class MainView { 8 | constructor(controller, galaxy) { 9 | this.controller = controller; 10 | this.galaxy = galaxy; 11 | this.renderingContext = this.createRenderingContext(); 12 | this.galaxyViewMediator = new GalaxyViewMediator(galaxy, new ViewMediatorFactory()); 13 | this.objectPicker = new ObjectPicker(this.galaxyViewMediator, this.renderingContext); 14 | this.descriptionPanel = new DescriptionPanel(); 15 | } 16 | 17 | createRenderingContext() { 18 | const domContainer = document.createElement('div'); 19 | 20 | document.body.appendChild(domContainer); 21 | 22 | return RenderingContext.getDefault(domContainer); 23 | } 24 | 25 | initialize() { 26 | const scene = this.renderingContext.scene; 27 | const object3D = this.galaxyViewMediator.object3D; 28 | 29 | scene.add(object3D); 30 | 31 | this.objectPicker.initialize(); 32 | this.objectPicker.addObserver('doubleclick', (e) => this.controller.onDoubleClick(e.astronomicalBody)); 33 | this.objectPicker.addObserver('click', (e) => this.controller.onClick(e.astronomicalBody)); 34 | this.objectPicker.addObserver('mousemove', (e) => this.controller.onMouseMove(e.astronomicalBody)); 35 | 36 | window.addEventListener( 'resize', (e) => this.onWindowResize(), false ); 37 | this.render(); 38 | } 39 | 40 | render() { 41 | this.renderingContext.controls.update(); 42 | requestAnimationFrame(() => this.render()); 43 | 44 | this.galaxyViewMediator.onFrameRenderered(); 45 | this.renderingContext.renderer.render(this.renderingContext.scene, this.renderingContext.camera); 46 | } 47 | 48 | onWindowResize(){ 49 | this.renderingContext.camera.aspect = window.innerWidth / window.innerHeight; 50 | this.renderingContext.camera.updateProjectionMatrix(); 51 | 52 | this.renderingContext.renderer.setSize(window.innerWidth, window.innerHeight); 53 | this.objectPicker.notifyWindowResize(); 54 | } 55 | } -------------------------------------------------------------------------------- /app/js/view/RenderingContext.js: -------------------------------------------------------------------------------- 1 | export default class RenderingContext { 2 | constructor(scene, camera, renderer, controls) { 3 | this.scene = scene; 4 | this.camera = camera; 5 | this.renderer = renderer; 6 | this.controls = controls; 7 | } 8 | 9 | static getDefault(containerElement) { 10 | const width = window.innerWidth, height = window.innerHeight; 11 | const scene = new THREE.Scene(); 12 | const camera = new THREE.PerspectiveCamera(45, width / height, 0.01, 1000); 13 | const renderer = new THREE.WebGLRenderer(); 14 | const controls = new THREE.TrackballControls(camera); 15 | 16 | camera.position.z = 20; 17 | renderer.setSize(width, height); 18 | scene.add(new THREE.AmbientLight(0x333333)); 19 | 20 | const light = new THREE.DirectionalLight(0xffffff, 1); 21 | 22 | light.position.set(15,15,15); 23 | scene.add(light); 24 | 25 | containerElement.appendChild(renderer.domElement); 26 | 27 | return new RenderingContext(scene, camera, renderer, controls); 28 | } 29 | } -------------------------------------------------------------------------------- /app/js/view/ViewMediatorFactory.js: -------------------------------------------------------------------------------- 1 | import GalaxyViewMediator from './mediator/GalaxyViewMediator'; 2 | import SolarSystemViewMediator from './mediator/SolarSystemViewMediator'; 3 | import PlanetViewMediator from './mediator/PlanetViewMediator'; 4 | import SunViewMediator from './mediator/SunViewMediator'; 5 | 6 | export default class ViewMediatorFactory { 7 | getMediator(astronomicalBody) { 8 | switch (astronomicalBody.className) { 9 | case 'Galaxy': 10 | return new GalaxyViewMediator(astronomicalBody, this); 11 | case 'SolarSystem': 12 | return new SolarSystemViewMediator(astronomicalBody, this); 13 | case 'Sun': 14 | return new SunViewMediator(astronomicalBody, this); 15 | case 'Planet': 16 | return new PlanetViewMediator(astronomicalBody, this); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/js/view/controls/DescriptionPanel.js: -------------------------------------------------------------------------------- 1 | export default class DescriptionPanel { 2 | constructor() { 3 | this.domContainer = document.createElement('div'); 4 | this.domContainer.id = 'panel'; 5 | document.body.appendChild(this.domContainer); 6 | } 7 | 8 | set text(text) { 9 | this.domContainer.innerHTML = text; 10 | } 11 | } -------------------------------------------------------------------------------- /app/js/view/controls/ObjectPicker.js: -------------------------------------------------------------------------------- 1 | import 'bin/GPUPicker'; 2 | import Observable from '../../Observable'; 3 | 4 | export default class ObjectPicker extends Observable { 5 | constructor(mediator, renderingContext) { 6 | super(); 7 | this.mediator = mediator; 8 | this.renderingContext = renderingContext; 9 | } 10 | 11 | initialize() { 12 | this.raycaster = new THREE.Raycaster(); 13 | 14 | this.gpuPicker = new THREE.GPUPicker({renderer: this.renderingContext.renderer, debug: false}); 15 | this.gpuPicker.setScene(this.renderingContext.scene); 16 | this.gpuPicker.setCamera(this.renderingContext.camera); 17 | this.renderingContext.renderer.domElement.addEventListener('dblclick', (e) => this.onDoubleClick(e)); 18 | this.renderingContext.renderer.domElement.addEventListener('click', (e) => this.onClick(e)); 19 | this.renderingContext.renderer.domElement.addEventListener('mousemove', (e) => this.onMouseMove(e)); 20 | } 21 | 22 | onDoubleClick(e) { 23 | const astronomicalBody = this.getIntersection(e); 24 | 25 | this.emit('doubleclick', { astronomicalBody }); 26 | } 27 | 28 | onClick(e) { 29 | const astronomicalBody = this.getIntersection(e); 30 | 31 | this.emit('click', { astronomicalBody }); 32 | } 33 | 34 | onMouseMove(e) { 35 | const astronomicalBody = this.getIntersection(e); 36 | 37 | this.emit('mousemove', { astronomicalBody }); 38 | } 39 | 40 | notifyWindowResize() { 41 | this.gpuPicker.needUpdate = true; 42 | this.gpuPicker.resizeTexture(window.innerWidth, window.innerHeight); 43 | } 44 | 45 | getIntersection(e) { 46 | this.gpuPicker.setScene(this.renderingContext.scene); 47 | 48 | const mouse = new THREE.Vector2(); 49 | 50 | mouse.x = e.clientX; 51 | mouse.y = e.clientY; 52 | 53 | const raymouse = new THREE.Vector2(); 54 | 55 | raymouse.x = ( e.clientX / window.innerWidth ) * 2 - 1; 56 | raymouse.y = -( e.clientY / window.innerHeight ) * 2 + 1; 57 | this.raycaster.setFromCamera(raymouse, this.renderingContext.camera); 58 | 59 | const intersection = this.gpuPicker.pick(mouse, this.raycaster); 60 | let astronomicalBody = null; 61 | 62 | 63 | if (intersection) { 64 | const originalObject = intersection.object.parent.originalObject; 65 | 66 | if (originalObject.mediator) { 67 | astronomicalBody = originalObject.mediator.astronomicalBody; 68 | } 69 | } 70 | 71 | return astronomicalBody; 72 | } 73 | } -------------------------------------------------------------------------------- /app/js/view/mediator/GalaxyViewMediator.js: -------------------------------------------------------------------------------- 1 | import ViewMediator from './ViewMediator'; 2 | 3 | export default class GalaxyViewMediator extends ViewMediator { 4 | constructor(galaxy, mediatorFactory) { 5 | super(galaxy, mediatorFactory); 6 | this.astronomicalBody.addObserver("SolarSystemAdded", (e) => this.onSolarSystemAdded(e)); 7 | 8 | const stars = this.createStars(500, 60); 9 | 10 | this.object3D.add(stars); 11 | } 12 | 13 | onSolarSystemAdded(e) { 14 | this.addChild(e.solarSystem); 15 | } 16 | 17 | createStars(radius, segments) { 18 | return new THREE.Mesh( 19 | new THREE.SphereGeometry(radius, segments, segments), 20 | new THREE.MeshBasicMaterial({ 21 | map: THREE.ImageUtils.loadTexture('images/eso_dark.jpg'), 22 | side: THREE.BackSide 23 | }) 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /app/js/view/mediator/PlanetViewMediator.js: -------------------------------------------------------------------------------- 1 | import ViewMediator from './ViewMediator'; 2 | 3 | export default class PlanetViewMediator extends ViewMediator { 4 | constructor(planet, mediatorFactory) { 5 | super(planet, mediatorFactory); 6 | this.astronomicalBody.addObserver("SatelliteAdded", (e) => this.onSatelliteAdded(e)); 7 | this.astronomicalBody.addObserver("SatelliteRemoved", (e) => this.onSatelliteRemoved(e)); 8 | } 9 | 10 | makeObject3D() { 11 | const container = new THREE.Object3D(); 12 | const mesh = new THREE.Mesh( 13 | new THREE.SphereGeometry(this.astronomicalBody.properties.radius, PlanetViewMediator.SphereSegments, PlanetViewMediator.SphereSegments), 14 | new THREE.MeshPhongMaterial({ 15 | map : THREE.ImageUtils.loadTexture(this.astronomicalBody.properties.texture) 16 | }) 17 | ); 18 | 19 | container.rotation.y = Math.random() * 360; 20 | container.add(mesh); 21 | 22 | mesh.position.setX(this.astronomicalBody.properties.distance); 23 | return container; 24 | } 25 | 26 | onSatelliteAdded(e) { 27 | this.addChild(e.satellite); 28 | } 29 | 30 | onSatelliteRemoved(e) { 31 | this.removeChild(e.satellite); 32 | } 33 | 34 | onFrameRenderered() { 35 | super.onFrameRenderered(); 36 | 37 | if (this.astronomicalBody.isMoving) { 38 | if (this.astronomicalBody.properties.orbitalSpeed) { 39 | this.object3D.rotation.y += this.astronomicalBody.properties.orbitalSpeed / 3; 40 | } 41 | 42 | if (this.astronomicalBody.properties.rotationSpeed) { 43 | this.object3D.children[0].rotation.y += this.astronomicalBody.properties.rotationSpeed / 3; 44 | } 45 | } 46 | } 47 | } 48 | 49 | PlanetViewMediator.SphereSegments = 32; -------------------------------------------------------------------------------- /app/js/view/mediator/SolarSystemViewMediator.js: -------------------------------------------------------------------------------- 1 | import ViewMediator from './ViewMediator'; 2 | 3 | export default class SolarSystemViewMediator extends ViewMediator { 4 | constructor(solarSystem, mediatorFactory) { 5 | super(solarSystem, mediatorFactory); 6 | this.sunViewMediator = this.mediatorFactory.getMediator(solarSystem.sun); 7 | this.object3D.add(this.sunViewMediator.object3D); 8 | this.astronomicalBody.addObserver("PlanetAdded", (e) => this.onPlanetAdded(e)); 9 | this.astronomicalBody.addObserver("PlanetRemoved", (e) => this.onPlanetRemoved(e)); 10 | } 11 | 12 | onPlanetAdded(e) { 13 | this.addChild(e.planet); 14 | } 15 | 16 | onPlanetRemoved(e) { 17 | this.removeChild(e.planet); 18 | } 19 | } -------------------------------------------------------------------------------- /app/js/view/mediator/SunViewMediator.js: -------------------------------------------------------------------------------- 1 | import ViewMediator from './ViewMediator'; 2 | 3 | export default class SunViewMediator extends ViewMediator { 4 | constructor(sun, mediatorFactory) { 5 | super(sun, mediatorFactory); 6 | } 7 | 8 | makeObject3D() { 9 | const object3D = new THREE.Mesh( 10 | new THREE.SphereGeometry(this.astronomicalBody.properties.radius, SunViewMediator.SphereSegments, SunViewMediator.SphereSegments), 11 | new THREE.MeshPhongMaterial({ 12 | map : THREE.ImageUtils.loadTexture(this.astronomicalBody.properties.texture) 13 | }) 14 | ); 15 | 16 | const texture = THREE.ImageUtils.loadTexture('images/sunsprite.png'); 17 | const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ 18 | map: texture, 19 | blending: THREE.AdditiveBlending, 20 | useScreenCoordinates: false, 21 | color: 0xffffff 22 | })); 23 | sprite.scale.x = this.astronomicalBody.properties.radius * 10; 24 | sprite.scale.y = this.astronomicalBody.properties.radius * 10; 25 | sprite.scale.z = 1; 26 | 27 | const container = new THREE.Object3D(); 28 | container.add(sprite); 29 | container.add(object3D); 30 | return container; 31 | } 32 | } 33 | 34 | SunViewMediator.SphereSegments = 32; -------------------------------------------------------------------------------- /app/js/view/mediator/ViewMediator.js: -------------------------------------------------------------------------------- 1 | import Observable from '../../Observable'; 2 | 3 | export default class ViewMediator extends Observable { 4 | constructor(astronomicalBody, mediatorFactory) { 5 | super(); 6 | this.astronomicalBody = astronomicalBody; 7 | this.mediatorFactory = mediatorFactory; 8 | this.object3D = this.makeObject3D(); 9 | this.object3D.name = astronomicalBody.name; 10 | this.childMediators = new Map(); 11 | this.object3D.traverse((object3D) => { 12 | object3D.mediator = this; 13 | }); 14 | } 15 | 16 | makeObject3D() { 17 | const container = new THREE.Object3D(); 18 | 19 | container.add(new THREE.Object3D()); 20 | return container; 21 | } 22 | 23 | addChild(child) { 24 | const mediator = this.mediatorFactory.getMediator(child); 25 | 26 | this.childMediators.set(child, mediator); 27 | this.object3D.children[0].add(mediator.object3D); 28 | 29 | for (const childofChild of child) { 30 | mediator.addChild(childofChild); 31 | } 32 | } 33 | 34 | removeChild(child) { 35 | const mediator = this.childMediators.get(child); 36 | 37 | if (mediator) { 38 | this.object3D.children[0].remove(mediator.object3D); 39 | this.childMediators.delete(child); 40 | } 41 | } 42 | 43 | onFrameRenderered() { 44 | for (const childMediator of this.childMediators.values()) { 45 | childMediator.onFrameRenderered(); 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'three'; 3 | import 'bin/TrackballControls'; 4 | import APIClient from './js/APIClient'; 5 | import GalaxyController from './js/controller/GalaxyController'; 6 | import Galaxy from './js/model/Galaxy'; 7 | import SolarSystem from './js/model/SolarSystem'; 8 | import Planet from './js/model/Planet'; 9 | import Sun from './js/model/Sun'; 10 | 11 | const galaxy = new Galaxy('Milky Way'); 12 | const galaxyController = new GalaxyController(galaxy); 13 | 14 | // add solar system to galaxy 15 | const apiClient = new APIClient(); 16 | const galaxyRecord = apiClient.getRecord(); 17 | 18 | for (const solarSystemRecord of galaxyRecord.solarSystems) { 19 | const sunRecord = solarSystemRecord.sun; 20 | const sun = new Sun(sunRecord.name, sunRecord.props); 21 | const solarSystem = new SolarSystem(solarSystemRecord.name, sun, solarSystemRecord.props); 22 | 23 | galaxy.addSolarSystem(solarSystem); 24 | 25 | for (const planetRecord of solarSystemRecord.planets) { 26 | const planet = new Planet(planetRecord.name, planetRecord.props); 27 | 28 | if (planetRecord.satellites) { 29 | for (const satelliteRecord of planetRecord.satellites) { 30 | planet.addSatellite(new Planet(satelliteRecord.name, satelliteRecord.props)); 31 | } 32 | } 33 | solarSystem.addPlanet(planet); 34 | } 35 | } -------------------------------------------------------------------------------- /app/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |