├── .gitignore ├── README.md ├── assets ├── airborne-poster.psd ├── compress.rb ├── expand.html ├── flights-original.js └── point.ai ├── index.html ├── media ├── airborne-poster.png ├── data │ └── flights-compressed.js ├── earth-bump.jpg ├── earth-specular.png ├── earth.png └── point.png ├── scripts ├── airborne.js └── vendor │ ├── Detector.js │ ├── TrackballControls.js │ ├── dat.gui.min.js │ ├── stats.min.js │ └── three.min.js └── styles └── base.css /.gitignore: -------------------------------------------------------------------------------- 1 | scripts/analytics.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Airborne 3 | 4 | Flight data visualized on a WebGL globe. 5 | Just like everyone else. 6 | See for yourself: http://stewd.io/airborne 7 | 8 | 9 | ![Airborne](https://github.com/stewdio/airborne/raw/master/media/airborne-poster.png "Airborne") 10 | 11 | -------------------------------------------------------------------------------- /assets/airborne-poster.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/assets/airborne-poster.psd -------------------------------------------------------------------------------- /assets/compress.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | 3 | 4 | The original flights.js, compiled by Callum Prentice, 5 | is a beautiful gift in itself. But it’s also 1.5 MB. 6 | Our very silly compression routine gets it under 900 KB. 7 | The original file is organized like so: 8 | 9 | 10 | var flights = [ 11 | 12 | [ originLatitude, originaLongitude, destinationLatitude, destinationLongitude ], 13 | ... 14 | ] 15 | 16 | 17 | And the value ranges for the coordinates are: 18 | latitude -90 .. +90 0 .. 180 19 | longitude -180 .. +180 0 .. 360 20 | 21 | But with 6 significat digits looks more like: 22 | 0.000000 .. 180.000000 180,000,000 23 | 0.000000 .. 360.000000 360,000,000 24 | 25 | We’re not talking “real” compression here. 26 | We just want to reduce the number of characters 27 | that JavaScript needs to pull in. 28 | So we’ll convert from Base 10 to Base 62: 29 | 30 | A-Z = 26 31 | a-z = 26 32 | 0-9 = 10 33 | -- 34 | 62 35 | 36 | Math.pow( 62, 4 ) < 180000000 < Math.pow( 62, 5 ) 37 | Math.pow( 62, 4 ) < 360000000 < Math.pow( 62, 5 ) 38 | 39 | 40 | In JavaScript Strings must be contained in quotes 41 | whereas Numbers are raw. 42 | That makes our storage comparison something like this: 43 | 44 | MOST IMPRESSIVE 45 | Original: -123.123456 11 characters long 46 | Compressed: '12345' 7 characters long 47 | 48 | LEAST IMPRESSIVE 49 | Original: 0 1 character long 50 | Compressed: '0' 3 characters long 51 | 52 | 53 | But we can reduce the drag of the required quotes 54 | by storing all four coordinates as 1 String 55 | rather than as an Array of 4 Strings. 56 | Array of Strings: ['A','B','C','D'] 57 | Single String: ['A|B|C|D'] 58 | 59 | We could in theory store the entire dataset as a single 60 | String, rather than an Array of Strings but the drag of 61 | running split() on that just doesn’t seem worth it. 62 | We’re already pushing the user’s browser pretty hard! 63 | Instead we’ll just do split('|') on a single route within 64 | a loop, reusing a temporary variable and never duplicating 65 | the entire dataset by splitting it all at once. 66 | 67 | 68 | =end 69 | 70 | 71 | 72 | 73 | def compress input, to_zero 74 | 75 | 76 | # We want to preserve the value of our input 77 | # to make debugging / comparing easier (if we want to) 78 | # so we’ll do operations on a separate variable. 79 | 80 | n = input 81 | 82 | 83 | # First let’s convert the raw input String 84 | # into a floating point number 85 | # so we can do some mathy math on it. 86 | 87 | n = n.to_f 88 | 89 | 90 | # Next we need to bump our values up to 0. 91 | # For latitude that means +90. 92 | # For longitudes that means +180. 93 | 94 | n += to_zero 95 | 96 | 97 | # We’ve been working with 6 significat digits 98 | # which means to ensure an integer we must multiply 99 | # by 10^6. 100 | 101 | n *= 1000000 102 | 103 | 104 | # No we do the dirty work: 105 | 106 | symbols = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 107 | radix = symbols.length# 62, right? 108 | output = '' 109 | 110 | while n > 0 do 111 | 112 | remainder = n % radix 113 | output = symbols[ remainder ] + output 114 | n = ( n - remainder ) / radix 115 | end 116 | 117 | return output 118 | end 119 | 120 | 121 | 122 | 123 | # Let’s go through our orginal flights.js 124 | # and pull out the lat / long coordinates 125 | # then compress them and output to the console 126 | # so we can inspect them. 127 | 128 | puts "\n\n" 129 | print 'var flights=[' 130 | routes_compressed = '' 131 | routes_total = 0 132 | File.foreach( 'flights-original.js' ) do |line| 133 | 134 | 135 | # First let’s pull out the numeric values. 136 | 137 | coords_compressed = '' 138 | line.scan( /([0-9|\-|.]+)\,([0-9|\-|.]+)\,([0-9|\-|.]+)\,([0-9|\-|.]+)/ ) do | a, b, c, d | 139 | 140 | unless a.nil? 141 | 142 | routes_total += 1 143 | if routes_total > 1 then 144 | coords_compressed = ",'" 145 | else 146 | coords_compressed = "'" 147 | end 148 | coords_compressed += 149 | compress( a, 90 ) +'|' + 150 | compress( b, 180 ) +'|' + 151 | compress( c, 90 ) +'|' + 152 | compress( d, 180 ) +"'" 153 | print coords_compressed 154 | #routes_compressed += coords_compressed 155 | end 156 | end 157 | end 158 | print ']' 159 | puts "\n\n" 160 | puts 'Total routes found: '+ routes_total.to_s 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /assets/expand.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 62 | 63 | -------------------------------------------------------------------------------- /assets/point.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/assets/point.ai -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Airborne 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 40 | 54 | 55 | 56 |
57 |
58 |
59 |

Airborne

60 | Animated flight data? 61 | Visualization globes? 62 | It’s all been done already. 63 | There was Aaron Koblin’s Flight Patterns, 64 | the Data Arts Team WebGL Globe, 65 | Google’s O3D demos, 66 | and more recently Callum Prentice’s Flight streams. 67 | Robert Gerard Pietrusko’s 68 | ORD Fly By Numbers 69 | is not only beautiful, but by far the most academically rigorous—and that’s just to name a few. 70 | I’ve pulled some inspiration from all of them to make this little demo for myself; 71 | I often pull apart others’ code and rewrite it in order to learn how it works. 72 | That’s all this is here. 73 | Feel free to pull it apart yourself—it’s on 74 | GitHub. 75 | I always try to code and comment legibly. Pay it forward. 76 |

77 | I’ve only tested this in Chrome and to be honest only really on my own laptop—so quit your bellyaching kiddo. 78 | No, the flight data does not contain actual flight times, airline codes, etc. Sorry. 79 | But the data is publicly available here which is fantastic. 80 | So have a ball. 81 |

82 | Cheers,
83 | Stewart Smith
84 | @stewd_io 85 |
86 |
87 | 88 | -------------------------------------------------------------------------------- /media/airborne-poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/media/airborne-poster.png -------------------------------------------------------------------------------- /media/earth-bump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/media/earth-bump.jpg -------------------------------------------------------------------------------- /media/earth-specular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/media/earth-specular.png -------------------------------------------------------------------------------- /media/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/media/earth.png -------------------------------------------------------------------------------- /media/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewdio/airborne/c7734200df6d693b0b09f26c66d236663927ac3c/media/point.png -------------------------------------------------------------------------------- /scripts/airborne.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ///////////////// 6 | // // 7 | // Globals // 8 | // // 9 | ///////////////// 10 | 11 | 12 | // Easy tweaks via DAT GUI 13 | // or through the console! 14 | 15 | var 16 | sunRotationPerFrame = 0.0023, 17 | earthRotationPerFrame = 0.001, 18 | flightSpriteSize = 0.05, 19 | flightsPathLinesOpacity = 0.04 20 | 21 | 22 | // Three.js basics. 23 | 24 | var 25 | camera, 26 | scene, 27 | renderer, 28 | controls, 29 | stats 30 | 31 | 32 | // Main stage dressing. 33 | 34 | var 35 | system, 36 | earth, 37 | sun 38 | 39 | 40 | // Flight data. 41 | 42 | var 43 | flightsTotal = flights.length, 44 | flightsPathSplines = [], 45 | flightsPointCloudGeometry, 46 | flightsPointCloud, 47 | flightPositions, 48 | flightSpriteSizes, 49 | flightsPathLines, 50 | flightsStartTimes = [], 51 | flightsEndTimes = [] 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ////////////// 61 | // // 62 | // Boot // 63 | // // 64 | ////////////// 65 | 66 | 67 | document.addEventListener( 'DOMContentLoaded', function(){ 68 | 69 | if( !Detector.webgl ) Detector.addGetWebGLMessage( document.body ) 70 | else { 71 | 72 | setupThree() 73 | setupSystem() 74 | setupSun() 75 | setupEarth() 76 | setupFlights() 77 | setupFlightsPathSplines() 78 | setupFlightsPathLines() 79 | setupFlightsPointCloud() 80 | setupGUI() 81 | 82 | earth.rotation.y -= Math.PI / 3 83 | system.rotation.z += 23.4 * Math.PI / 180 84 | system.rotation.x = Math.PI / 5 85 | animate() 86 | } 87 | }) 88 | 89 | 90 | 91 | 92 | /////////////// 93 | // // 94 | // Three // 95 | // // 96 | /////////////// 97 | 98 | 99 | function setupThree(){ 100 | 101 | var 102 | container = document.getElementById( 'three' ), 103 | angle = 30, 104 | width = container.offsetWidth || window.innerWidth, 105 | height = container.offsetHeight || window.innerHeight, 106 | aspect = width / height, 107 | near = 0.01, 108 | far = 100 109 | 110 | 111 | // Fire up the WebGL renderer. 112 | 113 | renderer = new THREE.WebGLRenderer({ antialias: true }) 114 | renderer.setClearColor( 0x000000, 1.0 ) 115 | renderer.setSize( width, height ) 116 | renderer.shadowMapEnabled = true 117 | renderer.shadowMapType = THREE.PCFSoftShadowMap 118 | container.appendChild( renderer.domElement ) 119 | window.addEventListener( 'resize', onThreeResize, false ) 120 | 121 | 122 | // Create and place the camera. 123 | 124 | camera = new THREE.PerspectiveCamera( angle, aspect, near, far ) 125 | camera.position.z = 5 126 | 127 | 128 | // Add trackball controls for panning (click/touch and drag) 129 | // and zooming (mouse wheel or gestures.) 130 | 131 | controls = new THREE.TrackballControls( camera, renderer.domElement ) 132 | controls.dynamicDampingFactor = 0.2 133 | controls.addEventListener( 'change', render ) 134 | 135 | 136 | // Create the scene tree to attach objects to. 137 | 138 | scene = new THREE.Scene() 139 | 140 | 141 | // Finally, add a performance monitoring bug 142 | // (“bug” in the video sense, not the programming sense!) 143 | // so we can see how speedy (or sluggish) our render is. 144 | 145 | stats = new Stats() 146 | stats.breakLine = function(){ 147 | 148 | [ 'fpsText', 'msText' ].forEach( function( id ){ 149 | 150 | var element = stats.domElement.querySelector( '#'+ id ) 151 | 152 | element.innerHTML = element.textContent.replace( /\(/, '
(' ) 153 | }) 154 | } 155 | document.body.appendChild( stats.domElement ) 156 | } 157 | function onThreeResize() { 158 | 159 | var 160 | container = document.getElementById( 'three' ), 161 | width = container.offsetWidth || window.innerWidth, 162 | height = container.offsetHeight || window.innerHeight 163 | 164 | camera.aspect = width / height 165 | camera.updateProjectionMatrix() 166 | renderer.setSize( width, height ) 167 | controls.handleResize() 168 | render() 169 | } 170 | 171 | 172 | 173 | 174 | //////////////// 175 | // // 176 | // System // 177 | // // 178 | //////////////// 179 | 180 | 181 | function setupSystem(){ 182 | 183 | system = new THREE.Object3D() 184 | system.name = 'system' 185 | scene.add( system ) 186 | } 187 | function setupSun(){ 188 | 189 | scene.add( new THREE.AmbientLight( 0x111111 )) 190 | 191 | sun = new THREE.DirectionalLight( 0xFFFFFF, 0.3 ) 192 | sun.name = 'sun' 193 | sun.position.set( -4, 0, 0 ) 194 | sun.castShadow = true 195 | sun.shadowCameraNear = 1 196 | sun.shadowCameraFar = 5 197 | sun.shadowCameraFov = 30 198 | sun.shadowCameraLeft = -1 199 | sun.shadowCameraRight = 1 200 | sun.shadowCameraTop = 1 201 | sun.shadowCameraBottom = -1 202 | sun.revolutionAngle = -Math.PI / 4 203 | system.add( sun ) 204 | } 205 | function setupEarth( radius ){ 206 | 207 | earth = new THREE.Mesh( 208 | 209 | new THREE.SphereGeometry( radius || 1, 64, 32 ), 210 | new THREE.MeshPhongMaterial({ 211 | 212 | map : THREE.ImageUtils.loadTexture( 'media/earth.png' ), 213 | bumpMap : THREE.ImageUtils.loadTexture( 'media/earth-bump.jpg' ), 214 | bumpScale : 0.02, 215 | specularMap : THREE.ImageUtils.loadTexture( 'media/earth-specular.png' ), 216 | specular : new THREE.Color( 0xFFFFFF ), 217 | shininess : 4 218 | }) 219 | ) 220 | earth.name = 'earth' 221 | earth.castShadow = true 222 | earth.receiveShadow = false 223 | system.add( earth ) 224 | } 225 | 226 | 227 | 228 | 229 | ///////////////// 230 | // // 231 | // Flights // 232 | // // 233 | ///////////////// 234 | 235 | 236 | // In order to reduce the size of our dataset 237 | // we’ve compressed it -- in a very dumb way ;) 238 | // To make it useful again we must expand it. 239 | 240 | function setupFlights(){ 241 | 242 | function expand( input, fromZero ){ 243 | 244 | var 245 | symbols = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 246 | i, chr, n = '', 247 | output = 0 248 | 249 | for( i = input.length - 1; i >= 0; i -- ){ 250 | 251 | chr = input.charAt( i ) 252 | n = symbols.indexOf( chr ) * Math.pow( symbols.length, input.length - 1 - i ) 253 | output += n 254 | } 255 | 256 | // We started with 6 significant digits 257 | // and multiplied appropriately to make them all integers 258 | // so now we must divide to go back. 259 | 260 | output /= 1000000 261 | 262 | 263 | // We also had to bump up our coordinates from 264 | // -90 for latitude and -180 for longitude 265 | // so it’s time to bump them back down. 266 | 267 | output += fromZero 268 | 269 | 270 | // Yea, we’re all done here. 271 | 272 | return output 273 | } 274 | flights.forEach( function( f, i ){ 275 | 276 | f = f.split( '|' ) 277 | flights[ i ] = [ 278 | 279 | expand( f[0], -90 ), 280 | expand( f[1], -180 ), 281 | expand( f[2], -90 ), 282 | expand( f[3], -180 ) 283 | ] 284 | }) 285 | } 286 | 287 | 288 | 289 | 290 | function setFlightTimes( index ){ 291 | 292 | var 293 | flight = flights[ index ], 294 | distance = latlongDistance( flight[ 0 ], flight[ 1 ], flight[ 2 ], flight[ 3 ]), 295 | startTime = Date.now() + Math.floor( Math.random() * 1000 * 20 ), 296 | duration = Math.floor( distance * 1000 * 80 ) 297 | 298 | 299 | // Just a little bit of variation in there. 300 | 301 | duration *= 0.8 + Math.random() 302 | flightsStartTimes[ index ] = startTime 303 | flightsEndTimes[ index ] = startTime + duration 304 | } 305 | 306 | 307 | 308 | 309 | // Here we’re going to compute the skeletons of our flight paths. 310 | // We can then extrapolate more detailed flight path geometry later. 311 | 312 | function setupFlightsPathSplines( radius ){ 313 | 314 | var f, 315 | originLatitude, 316 | originLongitude, 317 | destinationLatitude, 318 | destinationLongitude, 319 | distance, altitudeMax, 320 | pointsTotal, points, pointLL, pointXYZ, p, 321 | arcAngle, arcRadius, 322 | spline 323 | 324 | if( radius === undefined ) radius = 1 325 | for( f = 0; f < flightsTotal; f ++ ){ 326 | 327 | originLatitude = flights[ f ][ 0 ] 328 | originLongitude = flights[ f ][ 1 ] 329 | destinationLatitude = flights[ f ][ 2 ] 330 | destinationLongitude = flights[ f ][ 3 ] 331 | 332 | 333 | // Let’s make local flights fly lower altitudes 334 | // and long haul flights fly higher altitudes. 335 | // You dig man? You get it? You see what I mean? 336 | 337 | distance = latlongDistance( originLatitude, originLongitude, destinationLatitude, destinationLongitude ) 338 | altitudeMax = 0.02 + distance * 0.1 339 | 340 | 341 | // Aight yall. 342 | // We’re about to plot the path of this flight 343 | // using X number of points 344 | // to generate a smooth-ish curve. 345 | 346 | pointsTotal = 8 347 | points = [] 348 | for( p = 0; p < pointsTotal + 1; p ++ ){ 349 | 350 | 351 | // Is our path shooting straight up? 0 degrees. 352 | // Is our path shooting straight down? 180 degrees. 353 | // But likely we’re somewhere in between. 354 | 355 | arcAngle = p * 180 / pointsTotal 356 | 357 | 358 | // The base ‘radius‘ here is intended to be Earth’s radius. 359 | // Then we build a sine curve on top of that 360 | // with its max amplitude being ‘altitudeMax’. 361 | 362 | arcRadius = radius + ( Math.sin( arcAngle * Math.PI / 180 )) * altitudeMax 363 | 364 | 365 | // So at this point in the flight (p) 366 | // where are we between origin and destination? 367 | 368 | pointLL = latlongTween( 369 | 370 | originLatitude, 371 | originLongitude, 372 | destinationLatitude, 373 | destinationLongitude, 374 | p / pointsTotal 375 | ) 376 | 377 | 378 | // Ok. Now we know where (in latitude / longitude) 379 | // our flight is supposed to be at point ‘p’ 380 | // and we know what its altitude should be as well. 381 | // Time to convert that into an actual XYZ location 382 | // that will sit above our 3D globe. 383 | 384 | pointXYZ = ll2xyz( pointLL.latitude, pointLL.longitude, arcRadius ) 385 | points.push( new THREE.Vector3( pointXYZ.x, pointXYZ.y, pointXYZ.z )) 386 | } 387 | 388 | 389 | // Pack up this SplineCurve 390 | // then push it into our global splines array. 391 | // Also set the flight time. 392 | 393 | spline = new THREE.SplineCurve3( points ) 394 | flightsPathSplines.push( spline ) 395 | setFlightTimes( f ) 396 | } 397 | } 398 | 399 | 400 | 401 | 402 | function setupFlightsPointCloud(){ 403 | 404 | 405 | // Ah, the locals. 406 | 407 | var 408 | f, 409 | flightsColors = new Float32Array( flightsTotal * 3 ), 410 | color = new THREE.Color(), 411 | material 412 | 413 | 414 | // Globals. Yup. 415 | 416 | flightsPointCloudGeometry = new THREE.BufferGeometry() 417 | flightPositions = new Float32Array( flightsTotal * 3 ) 418 | flightSpriteSizes = new Float32Array( flightsTotal ) 419 | 420 | 421 | // For each flight we’ll need to add a Point 422 | // to our global Point Cloud. 423 | // Each point as an XYZ position and RGB color 424 | // and an image sprite size. 425 | 426 | for( f = 0; f < flightsTotal; f ++ ){ 427 | 428 | flightPositions[ 3 * f + 0 ] = 0// X 429 | flightPositions[ 3 * f + 1 ] = 0// Y 430 | flightPositions[ 3 * f + 2 ] = 0// Z 431 | 432 | 433 | // We’re going to based our flight’s Hue 434 | // on its origin longitude. 435 | // This way we can easy spot foreign flights 436 | // against a background of local flights. 437 | 438 | color.setHSL( 439 | 440 | (( flights[ f ][ 1 ] + 100 ) % 360 ) / 360, 441 | 1.0, 442 | 0.55 443 | ) 444 | flightsColors[ 3 * f + 0 ] = color.r// Red 445 | flightsColors[ 3 * f + 1 ] = color.g// Green 446 | flightsColors[ 3 * f + 2 ] = color.b// Blue 447 | 448 | flightSpriteSizes[ f ] = flightSpriteSize//@@ IN THE FUTURE SCALE BY NUMBER OF PASSENGERS ;) 449 | } 450 | flightsPointCloudGeometry.addAttribute( 'position', new THREE.BufferAttribute( flightPositions, 3 )) 451 | flightsPointCloudGeometry.addAttribute( 'customColor', new THREE.BufferAttribute( flightsColors, 3 )) 452 | flightsPointCloudGeometry.addAttribute( 'size', new THREE.BufferAttribute( flightSpriteSizes, 1 )) 453 | flightsPointCloudGeometry.computeBoundingBox() 454 | 455 | 456 | // Now that we have the basic position and color data 457 | // it’s time to finesse it with our shaders. 458 | 459 | material = new THREE.ShaderMaterial({ 460 | 461 | uniforms: { 462 | 463 | color: { type: 'c', value: new THREE.Color( 0xFFFFFF )}, 464 | texture: { type: 't', value: THREE.ImageUtils.loadTexture( 'media/point.png' )} 465 | }, 466 | attributes: { 467 | 468 | size: { type: 'f', value: null }, 469 | customColor: { type: 'c', value: null } 470 | }, 471 | vertexShader: document.getElementById( 'vertexShader' ).textContent, 472 | fragmentShader: document.getElementById( 'fragmentShader' ).textContent, 473 | blending: THREE.AdditiveBlending, 474 | depthTest: true, 475 | depthWrite: false, 476 | transparent: true 477 | }) 478 | 479 | 480 | // Finally, let’s pack this into our global variable 481 | // so we can play with it later, 482 | // and add it to the scene. 483 | 484 | flightsPointCloud = new THREE.PointCloud( flightsPointCloudGeometry, material ) 485 | earth.add( flightsPointCloud ) 486 | } 487 | 488 | 489 | 490 | 491 | // We’re going to draw arcs along the flight splines 492 | // to show entire flight paths at a glance. 493 | // These lines are 2D, in that they do not scale 494 | // according to zoom level. 495 | // This is kind of beautiful because as you zoom out 496 | // they become more visually prevalent -- like seeing 497 | // the sum of the parts rather than the individual bits. 498 | // The opposite is true when you zoom in. 499 | 500 | function setupFlightsPathLines() { 501 | 502 | var 503 | geometry = new THREE.BufferGeometry(), 504 | material = new THREE.LineBasicMaterial({ 505 | 506 | color: 0xFFFFFF, 507 | vertexColors: THREE.VertexColors, 508 | transparent: true, 509 | opacity: flightsPathLinesOpacity, 510 | depthTest: true, 511 | depthWrite: false, 512 | linewidth: 1//0.5 513 | }), 514 | segmentsTotal = 32, 515 | segments = new Float32Array( flightsTotal * 3 * 2 * segmentsTotal ), 516 | segmentBeginsAt, 517 | segmentEndsAt, 518 | colors = new Float32Array( flightsTotal * 3 * 2 * segmentsTotal ), 519 | color = new THREE.Color(), 520 | f, s, index, 521 | beginsAtNormal, 522 | endsAtNormal 523 | 524 | 525 | for( f = 0; f < flightsTotal; f ++ ){ 526 | 527 | for( s = 0; s < segmentsTotal - 1; s ++ ){ 528 | 529 | index = ( f * segmentsTotal + s ) * 6 530 | beginsAtNormal = s / ( segmentsTotal - 1 ) 531 | endsAtNormal = ( s + 1 ) / ( segmentsTotal - 1 ) 532 | 533 | 534 | // Begin this line segment. 535 | 536 | segmentBeginsAt = flightsPathSplines[ f ].getPoint( beginsAtNormal ) 537 | segments[ index + 0 ] = segmentBeginsAt.x 538 | segments[ index + 1 ] = segmentBeginsAt.y 539 | segments[ index + 2 ] = segmentBeginsAt.z 540 | color.setHSL( 541 | 542 | (( flights[ f ][ 1 ] + 100 ) % 360 ) / 360, 543 | 1, 544 | 0.3 + beginsAtNormal * 0.2 545 | ) 546 | colors[ index + 0 ] = color.r 547 | colors[ index + 1 ] = color.g 548 | colors[ index + 2 ] = color.b 549 | 550 | 551 | // End this line segment. 552 | 553 | segmentEndsAt = flightsPathSplines[ f ].getPoint( endsAtNormal ) 554 | segments[ index + 3 ] = segmentEndsAt.x 555 | segments[ index + 4 ] = segmentEndsAt.y 556 | segments[ index + 5 ] = segmentEndsAt.z 557 | color.setHSL( 558 | 559 | (( flights[ f ][ 1 ] + 100 ) % 360 ) / 360, 560 | 1, 561 | 0.3 + endsAtNormal * 0.2 562 | ) 563 | colors[ index + 3 ] = color.r 564 | colors[ index + 4 ] = color.g 565 | colors[ index + 5 ] = color.b 566 | } 567 | } 568 | geometry.addAttribute( 'position', new THREE.BufferAttribute( segments, 3 )) 569 | geometry.addAttribute( 'color', new THREE.BufferAttribute( colors, 3 )) 570 | geometry.computeBoundingSphere() 571 | geometry.dynamic = true//@@ NEEDED? 572 | 573 | 574 | // Finally, let’s pack this into our global variable 575 | // so we can play with it later, 576 | // and add it to the scene. 577 | 578 | flightsPathLines = new THREE.Line( geometry, material, THREE.LinePieces ) 579 | flightsPathLines.dynamic = true//@@ IS THIS STILL NEEDED? 580 | earth.add( flightsPathLines ) 581 | } 582 | 583 | 584 | 585 | 586 | function updateFlights(){ 587 | 588 | var f, 589 | easedValue, point, 590 | segmentsTotal = 32, 591 | s, index, 592 | //segments = flightsPathLines.geometry.attributes.position, 593 | segmentBeginsAt, 594 | segmentEndsAt 595 | 596 | 597 | for( f = 0; f < flightsTotal; f ++ ){ 598 | 599 | if( Date.now() > flightsStartTimes[ f ] ){ 600 | 601 | easedValue = easeOutQuadratic( 602 | 603 | Date.now() - flightsStartTimes[ f ], 604 | 0, 605 | 1, 606 | flightsEndTimes[ f ] - flightsStartTimes[ f ] 607 | ) 608 | if( easedValue < 0 ){ 609 | 610 | easedValue = 0 611 | setFlightTimes( f ) 612 | } 613 | 614 | 615 | // Update the Point Cloud. 616 | // Lots of cute little airplanes... 617 | 618 | point = flightsPathSplines[ f ].getPoint( easedValue ) 619 | flightPositions[ f * 3 + 0 ] = point.x 620 | flightPositions[ f * 3 + 1 ] = point.y 621 | flightPositions[ f * 3 + 2 ] = point.z 622 | 623 | 624 | // Update the flight path trails. 625 | /* 626 | for( s = 0; s < segmentsTotal - 1; s ++ ){ 627 | 628 | index = ( f * segmentsTotal + s ) * 6 629 | 630 | 631 | // Begin line segment. 632 | 633 | segmentBeginsAt = flightsPathSplines[ f ].getPoint( 634 | 635 | ( s / ( segmentsTotal - 1 )) * easedValue 636 | ) 637 | flightsPathLines.geometry.attributes.position[ index + 0 ] = 0//segmentBeginsAt.x 638 | flightsPathLines.geometry.attributes.position[ index + 1 ] = 0//segmentBeginsAt.y 639 | flightsPathLines.geometry.attributes.position[ index + 2 ] = 0//segmentBeginsAt.z 640 | 641 | 642 | // End line segment. 643 | 644 | segmentEndsAt = flightsPathSplines[ f ].getPoint( 645 | 646 | (( s + 1 ) / ( segmentsTotal - 1 )) * easedValue 647 | ) 648 | flightsPathLines.geometry.attributes.position[ index + 3 ] = 2//segmentEndsAt.x 649 | flightsPathLines.geometry.attributes.position[ index + 4 ] = 2//segmentEndsAt.y 650 | flightsPathLines.geometry.attributes.position[ index + 5 ] = 2//segmentEndsAt.z 651 | } 652 | */ 653 | } 654 | } 655 | //flightsPathLines.geometry.computeBoundingSphere() 656 | // flightsPathLines.geometry.attributes.position.needsUpdate = true 657 | // flightsPathLines.geometry.verticesNeedUpdate = true 658 | // flightsPathLines.geometry.elementsNeedUpdate = true 659 | // flightsPathLines.needsUpdate = true 660 | flightsPointCloudGeometry.attributes.position.needsUpdate = true 661 | } 662 | 663 | 664 | 665 | 666 | ///////////////// 667 | // // 668 | // DAT GUI // 669 | // // 670 | ///////////////// 671 | 672 | 673 | function setupGUI(){ 674 | 675 | var gui = new dat.GUI() 676 | 677 | gui.add( window, 'sunRotationPerFrame', 0, 0.02 ).name( 'Sun speed' ).onFinishChange( function( value ){ 678 | 679 | sunRotationPerFrame = value 680 | return false 681 | }) 682 | gui.add( window, 'earthRotationPerFrame', 0, 0.005 ).name( 'Earth speed' ).onFinishChange( function( value ){ 683 | 684 | earthRotationPerFrame = value 685 | return false 686 | }) 687 | gui.add( window, 'flightSpriteSize', 0.01, 0.2 ).name( 'Sprite size' ).onChange( function( value ){ 688 | 689 | var f 690 | 691 | for( f = 0; f < flightsTotal; f ++ ){ 692 | 693 | flightSpriteSizes[ f ] = flightSpriteSize 694 | } 695 | flightsPointCloudGeometry.attributes.size.needsUpdate = true 696 | }) 697 | gui.add( window, 'flightsPathLinesOpacity', 0, 1 ).name( 'Path opacity' ).onChange( function( value ){ 698 | 699 | flightsPathLines.material.opacity = value; 700 | }) 701 | gui.add( window, 'toggleAbout' ).name( 'Tell me more' ) 702 | } 703 | function toggleAbout(){ 704 | 705 | var 706 | element = document.getElementById( 'about' ), 707 | showing = element.classList.contains( 'show' ) 708 | 709 | if( !showing ) element.classList.add( 'show' ) 710 | else element.classList.remove( 'show' ) 711 | } 712 | 713 | 714 | 715 | 716 | /////////////// 717 | // // 718 | // Tools // 719 | // // 720 | /////////////// 721 | 722 | 723 | function ll2xyz( latitude, longitude, radius ){ 724 | 725 | var 726 | phi = ( 90 - latitude ) * Math.PI / 180, 727 | theta = ( 360 - longitude ) * Math.PI / 180 728 | 729 | return { 730 | 731 | x: radius * Math.sin( phi ) * Math.cos( theta ), 732 | y: radius * Math.cos( phi ), 733 | z: radius * Math.sin( phi ) * Math.sin( theta ) 734 | } 735 | } 736 | function latlongTween( latitudeA, longitudeA, latitudeB, longitudeB, tween ){ 737 | 738 | 739 | // First, let’s convert degrees to radians. 740 | 741 | latitudeA *= Math.PI / 180 742 | longitudeA *= Math.PI / 180 743 | latitudeB *= Math.PI / 180 744 | longitudeB *= Math.PI / 180 745 | 746 | 747 | // Now we can get seriously mathy. 748 | 749 | var 750 | d = 2 * Math.asin( Math.sqrt( 751 | 752 | Math.pow(( Math.sin(( latitudeA - latitudeB ) / 2 )), 2 ) + 753 | Math.cos( latitudeA ) * 754 | Math.cos( latitudeB ) * 755 | Math.pow( Math.sin(( longitudeA - longitudeB ) / 2 ), 2 ) 756 | )), 757 | A = Math.sin(( 1 - tween ) * d ) / Math.sin( d ), 758 | B = Math.sin( tween * d ) / Math.sin( d ) 759 | 760 | 761 | // Here’s our XYZ location for the tween Point. Sort of. 762 | // (It doesn’t take into account the sphere’s radius.) 763 | // It’s a necessary in between step that doesn’t fully 764 | // resolve to usable XYZ coordinates. 765 | 766 | var 767 | x = A * Math.cos( latitudeA ) * Math.cos( longitudeA ) + B * Math.cos( latitudeB ) * Math.cos( longitudeB ), 768 | y = A * Math.cos( latitudeA ) * Math.sin( longitudeA ) + B * Math.cos( latitudeB ) * Math.sin( longitudeB ), 769 | z = A * Math.sin( latitudeA ) + B * Math.sin( latitudeB ) 770 | 771 | 772 | // And we can convert that right back to lat / long. 773 | 774 | var 775 | latitude = Math.atan2( z, Math.sqrt( Math.pow( x, 2 ) + Math.pow( y, 2 ))) * 180 / Math.PI, 776 | longitude = Math.atan2( y, x ) * 180 / Math.PI 777 | 778 | 779 | // Return a nice package of useful values for our tween Point. 780 | 781 | return { 782 | 783 | latitude: latitude, 784 | longitude: longitude 785 | } 786 | } 787 | 788 | 789 | // This resource is fantastic (borrowed the algo from there): 790 | // http://www.movable-type.co.uk/scripts/latlong.html 791 | // Would be nice to integrate this with latlongTween() to reduce 792 | // code and bring the styles more in line with each other. 793 | 794 | function latlongDistance( latitudeA, longitudeA, latitudeB, longitudeB ){ 795 | 796 | var 797 | earthRadiusMeters = 6371000, 798 | 799 | φ1 = latitudeA * Math.PI / 180, 800 | φ2 = latitudeB * Math.PI / 180, 801 | Δφ = ( latitudeB - latitudeA ) * Math.PI / 180, 802 | Δλ = ( longitudeB - longitudeA ) * Math.PI / 180, 803 | 804 | a = Math.sin( Δφ / 2 ) * Math.sin( Δφ / 2 ) + 805 | Math.cos( φ1 ) * Math.cos( φ2 ) * 806 | Math.sin( Δλ / 2 ) * Math.sin( Δλ / 2 ), 807 | c = 2 * Math.atan2( Math.sqrt( a ), Math.sqrt( 1 - a )), 808 | 809 | distanceMeters = earthRadiusMeters * c 810 | 811 | 812 | // For this demo we don’t need actual distance in kilometers 813 | // because we’re just using a factor to scale time by 814 | // so we’ll just return the normal of earth’s circumference. 815 | 816 | return c 817 | } 818 | function easeOutQuadratic( t, b, c, d ){ 819 | 820 | if(( t /= d / 2 ) < 1 ) return c / 2 * t * t + b 821 | return -c / 2 * (( --t ) * ( t - 2 ) - 1 ) + b 822 | } 823 | 824 | 825 | 826 | 827 | ////////////// 828 | // // 829 | // Loop // 830 | // // 831 | ////////////// 832 | 833 | 834 | function animate(){ 835 | 836 | stats.begin() 837 | earth.rotation.y += earthRotationPerFrame 838 | sun.revolutionAngle += sunRotationPerFrame 839 | sun.position.x = 4 * Math.cos( sun.revolutionAngle ) 840 | sun.position.z = 4 * Math.sin( sun.revolutionAngle ) 841 | render() 842 | controls.update() 843 | updateFlights() 844 | stats.end() 845 | stats.breakLine() 846 | requestAnimationFrame( animate ) 847 | } 848 | function render(){ 849 | 850 | renderer.render( scene, camera ) 851 | } 852 | 853 | 854 | 855 | -------------------------------------------------------------------------------- /scripts/vendor/Detector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * @author mr.doob / http://mrdoob.com/ 4 | */ 5 | 6 | var Detector = { 7 | 8 | canvas: !! window.CanvasRenderingContext2D, 9 | webgl: ( function () { try { var canvas = document.createElement( 'canvas' ); return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )(), 10 | workers: !! window.Worker, 11 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 12 | 13 | getWebGLErrorMessage: function () { 14 | 15 | var element = document.createElement( 'div' ); 16 | element.className = 'webgl-error'; 17 | 18 | if ( !this.webgl ) { 19 | 20 | element.innerHTML = window.WebGLRenderingContext ? [ 21 | 'Your graphics card does not seem to support WebGL.
', 22 | 'Find out how to get it here.' 23 | ].join( '\n' ) : [ 24 | 'Your browser does not seem to support WebGL.
', 25 | 'Find out how to get it here.' 26 | ].join( '\n' ); 27 | 28 | } 29 | 30 | return element; 31 | 32 | }, 33 | 34 | addGetWebGLMessage: function (parent ) { 35 | 36 | parent.appendChild( Detector.getWebGLErrorMessage() ); 37 | 38 | } 39 | 40 | }; -------------------------------------------------------------------------------- /scripts/vendor/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 | 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 ); -------------------------------------------------------------------------------- /scripts/vendor/dat.gui.min.js: -------------------------------------------------------------------------------- 1 | var dat=dat||{};dat.gui=dat.gui||{};dat.utils=dat.utils||{};dat.controllers=dat.controllers||{};dat.dom=dat.dom||{};dat.color=dat.color||{};dat.utils.css=function(){return{load:function(e,a){a=a||document;var b=a.createElement("link");b.type="text/css";b.rel="stylesheet";b.href=e;a.getElementsByTagName("head")[0].appendChild(b)},inject:function(e,a){a=a||document;var b=document.createElement("style");b.type="text/css";b.innerHTML=e;a.getElementsByTagName("head")[0].appendChild(b)}}}(); 2 | dat.utils.common=function(){var e=Array.prototype.forEach,a=Array.prototype.slice;return{BREAK:{},extend:function(b){this.each(a.call(arguments,1),function(a){for(var f in a)this.isUndefined(a[f])||(b[f]=a[f])},this);return b},defaults:function(b){this.each(a.call(arguments,1),function(a){for(var f in a)this.isUndefined(b[f])&&(b[f]=a[f])},this);return b},compose:function(){var b=a.call(arguments);return function(){for(var d=a.call(arguments),f=b.length-1;0<=f;f--)d=[b[f].apply(this,d)];return d[0]}}, 3 | each:function(a,d,f){if(e&&a.forEach===e)a.forEach(d,f);else if(a.length===a.length+0)for(var c=0,p=a.length;cthis.__max&&(a=this.__max);void 0!==this.__step&&0!=a%this.__step&&(a=Math.round(a/this.__step)*this.__step);return b.superclass.prototype.setValue.call(this,a)},min:function(a){this.__min=a;return this},max:function(a){this.__max=a;return this},step:function(a){this.__step=a;return this}});return b}(dat.controllers.Controller,dat.utils.common); 17 | dat.controllers.NumberControllerBox=function(e,a,b){var d=function(f,c,e){function k(){var a=parseFloat(n.__input.value);b.isNaN(a)||n.setValue(a)}function l(a){var c=r-a.clientY;n.setValue(n.getValue()+c*n.__impliedStep);r=a.clientY}function q(){a.unbind(window,"mousemove",l);a.unbind(window,"mouseup",q)}this.__truncationSuspended=!1;d.superclass.call(this,f,c,e);var n=this,r;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"change",k);a.bind(this.__input, 18 | "blur",function(){k();n.__onFinishChange&&n.__onFinishChange.call(n,n.getValue())});a.bind(this.__input,"mousedown",function(c){a.bind(window,"mousemove",l);a.bind(window,"mouseup",q);r=c.clientY});a.bind(this.__input,"keydown",function(a){13===a.keyCode&&(n.__truncationSuspended=!0,this.blur(),n.__truncationSuspended=!1)});this.updateDisplay();this.domElement.appendChild(this.__input)};d.superclass=e;b.extend(d.prototype,e.prototype,{updateDisplay:function(){var a=this.__input,c;if(this.__truncationSuspended)c= 19 | this.getValue();else{c=this.getValue();var b=Math.pow(10,this.__precision);c=Math.round(c*b)/b}a.value=c;return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.NumberController,dat.dom.dom,dat.utils.common); 20 | dat.controllers.NumberControllerSlider=function(e,a,b,d,f){function c(a,c,d,b,f){return b+(a-c)/(d-c)*(f-b)}var p=function(d,b,f,e,r){function y(d){d.preventDefault();var b=a.getOffset(h.__background),f=a.getWidth(h.__background);h.setValue(c(d.clientX,b.left,b.left+f,h.__min,h.__max));return!1}function g(){a.unbind(window,"mousemove",y);a.unbind(window,"mouseup",g);h.__onFinishChange&&h.__onFinishChange.call(h,h.getValue())}p.superclass.call(this,d,b,{min:f,max:e,step:r});var h=this;this.__background= 21 | document.createElement("div");this.__foreground=document.createElement("div");a.bind(this.__background,"mousedown",function(c){a.bind(window,"mousemove",y);a.bind(window,"mouseup",g);y(c)});a.addClass(this.__background,"slider");a.addClass(this.__foreground,"slider-fg");this.updateDisplay();this.__background.appendChild(this.__foreground);this.domElement.appendChild(this.__background)};p.superclass=e;p.useDefaultStyles=function(){b.inject(f)};d.extend(p.prototype,e.prototype,{updateDisplay:function(){var a= 22 | (this.getValue()-this.__min)/(this.__max-this.__min);this.__foreground.style.width=100*a+"%";return p.superclass.prototype.updateDisplay.call(this)}});return p}(dat.controllers.NumberController,dat.dom.dom,dat.utils.css,dat.utils.common,"/**\n * dat-gui JavaScript Controller Library\n * http://code.google.com/p/dat-gui\n *\n * Copyright 2011 Data Arts Team, Google Creative Lab\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\n.slider {\n box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);\n height: 1em;\n border-radius: 1em;\n background-color: #eee;\n padding: 0 0.5em;\n overflow: hidden;\n}\n\n.slider-fg {\n padding: 1px 0 2px 0;\n background-color: #aaa;\n height: 1em;\n margin-left: -0.5em;\n padding-right: 0.5em;\n border-radius: 1em 0 0 1em;\n}\n\n.slider-fg:after {\n display: inline-block;\n border-radius: 1em;\n background-color: #fff;\n border: 1px solid #aaa;\n content: '';\n float: right;\n margin-right: -1em;\n margin-top: -1px;\n height: 0.9em;\n width: 0.9em;\n}"); 23 | dat.controllers.FunctionController=function(e,a,b){var d=function(b,c,e){d.superclass.call(this,b,c);var k=this;this.__button=document.createElement("div");this.__button.innerHTML=void 0===e?"Fire":e;a.bind(this.__button,"click",function(a){a.preventDefault();k.fire();return!1});a.addClass(this.__button,"button");this.domElement.appendChild(this.__button)};d.superclass=e;b.extend(d.prototype,e.prototype,{fire:function(){this.__onChange&&this.__onChange.call(this);this.__onFinishChange&&this.__onFinishChange.call(this, 24 | this.getValue());this.getValue().call(this.object)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common); 25 | dat.controllers.BooleanController=function(e,a,b){var d=function(b,c){d.superclass.call(this,b,c);var e=this;this.__prev=this.getValue();this.__checkbox=document.createElement("input");this.__checkbox.setAttribute("type","checkbox");a.bind(this.__checkbox,"change",function(){e.setValue(!e.__prev)},!1);this.domElement.appendChild(this.__checkbox);this.updateDisplay()};d.superclass=e;b.extend(d.prototype,e.prototype,{setValue:function(a){a=d.superclass.prototype.setValue.call(this,a);this.__onFinishChange&& 26 | this.__onFinishChange.call(this,this.getValue());this.__prev=this.getValue();return a},updateDisplay:function(){!0===this.getValue()?(this.__checkbox.setAttribute("checked","checked"),this.__checkbox.checked=!0):this.__checkbox.checked=!1;return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common); 27 | dat.color.toString=function(e){return function(a){if(1==a.a||e.isUndefined(a.a)){for(a=a.hex.toString(16);6>a.length;)a="0"+a;return"#"+a}return"rgba("+Math.round(a.r)+","+Math.round(a.g)+","+Math.round(a.b)+","+a.a+")"}}(dat.utils.common); 28 | dat.color.interpret=function(e,a){var b,d,f=[{litmus:a.isString,conversions:{THREE_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9])([A-F0-9])([A-F0-9])$/i);return null===a?!1:{space:"HEX",hex:parseInt("0x"+a[1].toString()+a[1].toString()+a[2].toString()+a[2].toString()+a[3].toString()+a[3].toString())}},write:e},SIX_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9]{6})$/i);return null===a?!1:{space:"HEX",hex:parseInt("0x"+a[1].toString())}},write:e},CSS_RGB:{read:function(a){a=a.match(/^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/); 29 | return null===a?!1:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3])}},write:e},CSS_RGBA:{read:function(a){a=a.match(/^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/);return null===a?!1:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3]),a:parseFloat(a[4])}},write:e}}},{litmus:a.isNumber,conversions:{HEX:{read:function(a){return{space:"HEX",hex:a,conversionName:"HEX"}},write:function(a){return a.hex}}}},{litmus:a.isArray,conversions:{RGB_ARRAY:{read:function(a){return 3!= 30 | a.length?!1:{space:"RGB",r:a[0],g:a[1],b:a[2]}},write:function(a){return[a.r,a.g,a.b]}},RGBA_ARRAY:{read:function(a){return 4!=a.length?!1:{space:"RGB",r:a[0],g:a[1],b:a[2],a:a[3]}},write:function(a){return[a.r,a.g,a.b,a.a]}}}},{litmus:a.isObject,conversions:{RGBA_OBJ:{read:function(c){return a.isNumber(c.r)&&a.isNumber(c.g)&&a.isNumber(c.b)&&a.isNumber(c.a)?{space:"RGB",r:c.r,g:c.g,b:c.b,a:c.a}:!1},write:function(a){return{r:a.r,g:a.g,b:a.b,a:a.a}}},RGB_OBJ:{read:function(c){return a.isNumber(c.r)&& 31 | a.isNumber(c.g)&&a.isNumber(c.b)?{space:"RGB",r:c.r,g:c.g,b:c.b}:!1},write:function(a){return{r:a.r,g:a.g,b:a.b}}},HSVA_OBJ:{read:function(c){return a.isNumber(c.h)&&a.isNumber(c.s)&&a.isNumber(c.v)&&a.isNumber(c.a)?{space:"HSV",h:c.h,s:c.s,v:c.v,a:c.a}:!1},write:function(a){return{h:a.h,s:a.s,v:a.v,a:a.a}}},HSV_OBJ:{read:function(d){return a.isNumber(d.h)&&a.isNumber(d.s)&&a.isNumber(d.v)?{space:"HSV",h:d.h,s:d.s,v:d.v}:!1},write:function(a){return{h:a.h,s:a.s,v:a.v}}}}}];return function(){d=!1; 32 | var c=1\n\n Here\'s the new load parameter for your GUI\'s constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n \n
\n \n
\n\n', 59 | ".dg {\n /** Clear list styles */\n /* Auto-place container */\n /* Auto-placed GUI's */\n /* Line items that don't contain folders. */\n /** Folder names */\n /** Hides closed items */\n /** Controller row */\n /** Name-half (left) */\n /** Controller-half (right) */\n /** Controller placement */\n /** Shorter number boxes when slider is present. */\n /** Ensure the entire boolean and function row shows a hand */ }\n .dg ul {\n list-style: none;\n margin: 0;\n padding: 0;\n width: 100%;\n clear: both; }\n .dg.ac {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 0;\n z-index: 0; }\n .dg:not(.ac) .main {\n /** Exclude mains in ac so that we don't hide close button */\n overflow: hidden; }\n .dg.main {\n -webkit-transition: opacity 0.1s linear;\n -o-transition: opacity 0.1s linear;\n -moz-transition: opacity 0.1s linear;\n transition: opacity 0.1s linear; }\n .dg.main.taller-than-window {\n overflow-y: auto; }\n .dg.main.taller-than-window .close-button {\n opacity: 1;\n /* TODO, these are style notes */\n margin-top: -1px;\n border-top: 1px solid #2c2c2c; }\n .dg.main ul.closed .close-button {\n opacity: 1 !important; }\n .dg.main:hover .close-button,\n .dg.main .close-button.drag {\n opacity: 1; }\n .dg.main .close-button {\n /*opacity: 0;*/\n -webkit-transition: opacity 0.1s linear;\n -o-transition: opacity 0.1s linear;\n -moz-transition: opacity 0.1s linear;\n transition: opacity 0.1s linear;\n border: 0;\n position: absolute;\n line-height: 19px;\n height: 20px;\n /* TODO, these are style notes */\n cursor: pointer;\n text-align: center;\n background-color: #000; }\n .dg.main .close-button:hover {\n background-color: #111; }\n .dg.a {\n float: right;\n margin-right: 15px;\n overflow-x: hidden; }\n .dg.a.has-save > ul {\n margin-top: 27px; }\n .dg.a.has-save > ul.closed {\n margin-top: 0; }\n .dg.a .save-row {\n position: fixed;\n top: 0;\n z-index: 1002; }\n .dg li {\n -webkit-transition: height 0.1s ease-out;\n -o-transition: height 0.1s ease-out;\n -moz-transition: height 0.1s ease-out;\n transition: height 0.1s ease-out; }\n .dg li:not(.folder) {\n cursor: auto;\n height: 27px;\n line-height: 27px;\n overflow: hidden;\n padding: 0 4px 0 5px; }\n .dg li.folder {\n padding: 0;\n border-left: 4px solid rgba(0, 0, 0, 0); }\n .dg li.title {\n cursor: pointer;\n margin-left: -4px; }\n .dg .closed li:not(.title),\n .dg .closed ul li,\n .dg .closed ul li > * {\n height: 0;\n overflow: hidden;\n border: 0; }\n .dg .cr {\n clear: both;\n padding-left: 3px;\n height: 27px; }\n .dg .property-name {\n cursor: default;\n float: left;\n clear: left;\n width: 40%;\n overflow: hidden;\n text-overflow: ellipsis; }\n .dg .c {\n float: left;\n width: 60%; }\n .dg .c input[type=text] {\n border: 0;\n margin-top: 4px;\n padding: 3px;\n width: 100%;\n float: right; }\n .dg .has-slider input[type=text] {\n width: 30%;\n /*display: none;*/\n margin-left: 0; }\n .dg .slider {\n float: left;\n width: 66%;\n margin-left: -5px;\n margin-right: 0;\n height: 19px;\n margin-top: 4px; }\n .dg .slider-fg {\n height: 100%; }\n .dg .c input[type=checkbox] {\n margin-top: 9px; }\n .dg .c select {\n margin-top: 5px; }\n .dg .cr.function,\n .dg .cr.function .property-name,\n .dg .cr.function *,\n .dg .cr.boolean,\n .dg .cr.boolean * {\n cursor: pointer; }\n .dg .selector {\n display: none;\n position: absolute;\n margin-left: -9px;\n margin-top: 23px;\n z-index: 10; }\n .dg .c:hover .selector,\n .dg .selector.drag {\n display: block; }\n .dg li.save-row {\n padding: 0; }\n .dg li.save-row .button {\n display: inline-block;\n padding: 0px 6px; }\n .dg.dialogue {\n background-color: #222;\n width: 460px;\n padding: 15px;\n font-size: 13px;\n line-height: 15px; }\n\n/* TODO Separate style and structure */\n#dg-new-constructor {\n padding: 10px;\n color: #222;\n font-family: Monaco, monospace;\n font-size: 10px;\n border: 0;\n resize: none;\n box-shadow: inset 1px 1px 1px #888;\n word-wrap: break-word;\n margin: 12px 0;\n display: block;\n width: 440px;\n overflow-y: scroll;\n height: 100px;\n position: relative; }\n\n#dg-local-explain {\n display: none;\n font-size: 11px;\n line-height: 17px;\n border-radius: 3px;\n background-color: #333;\n padding: 8px;\n margin-top: 10px; }\n #dg-local-explain code {\n font-size: 10px; }\n\n#dat-gui-save-locally {\n display: none; }\n\n/** Main type */\n.dg {\n color: #eee;\n font: 11px 'Lucida Grande', sans-serif;\n text-shadow: 0 -1px 0 #111;\n /** Auto place */\n /* Controller row,
  • */\n /** Controllers */ }\n .dg.main {\n /** Scrollbar */ }\n .dg.main::-webkit-scrollbar {\n width: 5px;\n background: #1a1a1a; }\n .dg.main::-webkit-scrollbar-corner {\n height: 0;\n display: none; }\n .dg.main::-webkit-scrollbar-thumb {\n border-radius: 5px;\n background: #676767; }\n .dg li:not(.folder) {\n background: #1a1a1a;\n border-bottom: 1px solid #2c2c2c; }\n .dg li.save-row {\n line-height: 25px;\n background: #dad5cb;\n border: 0; }\n .dg li.save-row select {\n margin-left: 5px;\n width: 108px; }\n .dg li.save-row .button {\n margin-left: 5px;\n margin-top: 1px;\n border-radius: 2px;\n font-size: 9px;\n line-height: 7px;\n padding: 4px 4px 5px 4px;\n background: #c5bdad;\n color: #fff;\n text-shadow: 0 1px 0 #b0a58f;\n box-shadow: 0 -1px 0 #b0a58f;\n cursor: pointer; }\n .dg li.save-row .button.gears {\n background: #c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;\n height: 7px;\n width: 8px; }\n .dg li.save-row .button:hover {\n background-color: #bab19e;\n box-shadow: 0 -1px 0 #b0a58f; }\n .dg li.folder {\n border-bottom: 0; }\n .dg li.title {\n padding-left: 16px;\n background: black url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.2); }\n .dg .closed li.title {\n background-image: url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==); }\n .dg .cr.boolean {\n border-left: 3px solid #806787; }\n .dg .cr.function {\n border-left: 3px solid #e61d5f; }\n .dg .cr.number {\n border-left: 3px solid #2fa1d6; }\n .dg .cr.number input[type=text] {\n color: #2fa1d6; }\n .dg .cr.string {\n border-left: 3px solid #1ed36f; }\n .dg .cr.string input[type=text] {\n color: #1ed36f; }\n .dg .cr.function:hover, .dg .cr.boolean:hover {\n background: #111; }\n .dg .c input[type=text] {\n background: #303030;\n outline: none; }\n .dg .c input[type=text]:hover {\n background: #3c3c3c; }\n .dg .c input[type=text]:focus {\n background: #494949;\n color: #fff; }\n .dg .c .slider {\n background: #303030;\n cursor: ew-resize; }\n .dg .c .slider-fg {\n background: #2fa1d6; }\n .dg .c .slider:hover {\n background: #3c3c3c; }\n .dg .c .slider:hover .slider-fg {\n background: #44abda; }\n", 60 | dat.controllers.factory=function(e,a,b,d,f,c,p){return function(k,l,q,n){var r=k[l];if(p.isArray(q)||p.isObject(q))return new e(k,l,q);if(p.isNumber(r))return p.isNumber(q)&&p.isNumber(n)?new b(k,l,q,n):new a(k,l,{min:q,max:n});if(p.isString(r))return new d(k,l);if(p.isFunction(r))return new f(k,l,"");if(p.isBoolean(r))return new c(k,l)}}(dat.controllers.OptionController,dat.controllers.NumberControllerBox,dat.controllers.NumberControllerSlider,dat.controllers.StringController=function(e,a,b){var d= 61 | function(b,c){function e(){k.setValue(k.__input.value)}d.superclass.call(this,b,c);var k=this;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"keyup",e);a.bind(this.__input,"change",e);a.bind(this.__input,"blur",function(){k.__onFinishChange&&k.__onFinishChange.call(k,k.getValue())});a.bind(this.__input,"keydown",function(a){13===a.keyCode&&this.blur()});this.updateDisplay();this.domElement.appendChild(this.__input)};d.superclass=e;b.extend(d.prototype, 62 | e.prototype,{updateDisplay:function(){a.isActive(this.__input)||(this.__input.value=this.getValue());return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common),dat.controllers.FunctionController,dat.controllers.BooleanController,dat.utils.common),dat.controllers.Controller,dat.controllers.BooleanController,dat.controllers.FunctionController,dat.controllers.NumberControllerBox,dat.controllers.NumberControllerSlider,dat.controllers.OptionController, 63 | dat.controllers.ColorController=function(e,a,b,d,f){function c(a,b,d,c){a.style.background="";f.each(l,function(e){a.style.cssText+="background: "+e+"linear-gradient("+b+", "+d+" 0%, "+c+" 100%); "})}function p(a){a.style.background="";a.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);";a.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"; 64 | a.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);";a.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);";a.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}var k=function(e,n){function r(b){t(b);a.bind(window,"mousemove",t);a.bind(window, 65 | "mouseup",l)}function l(){a.unbind(window,"mousemove",t);a.unbind(window,"mouseup",l)}function g(){var a=d(this.value);!1!==a?(s.__color.__state=a,s.setValue(s.__color.toOriginal())):this.value=s.__color.toString()}function h(){a.unbind(window,"mousemove",u);a.unbind(window,"mouseup",h)}function t(b){b.preventDefault();var d=a.getWidth(s.__saturation_field),c=a.getOffset(s.__saturation_field),e=(b.clientX-c.left+document.body.scrollLeft)/d;b=1-(b.clientY-c.top+document.body.scrollTop)/d;1 66 | b&&(b=0);1e&&(e=0);s.__color.v=b;s.__color.s=e;s.setValue(s.__color.toOriginal());return!1}function u(b){b.preventDefault();var d=a.getHeight(s.__hue_field),c=a.getOffset(s.__hue_field);b=1-(b.clientY-c.top+document.body.scrollTop)/d;1b&&(b=0);s.__color.h=360*b;s.setValue(s.__color.toOriginal());return!1}k.superclass.call(this,e,n);this.__color=new b(this.getValue());this.__temp=new b(0);var s=this;this.domElement=document.createElement("div");a.makeSelectable(this.domElement,!1); 67 | this.__selector=document.createElement("div");this.__selector.className="selector";this.__saturation_field=document.createElement("div");this.__saturation_field.className="saturation-field";this.__field_knob=document.createElement("div");this.__field_knob.className="field-knob";this.__field_knob_border="2px solid ";this.__hue_knob=document.createElement("div");this.__hue_knob.className="hue-knob";this.__hue_field=document.createElement("div");this.__hue_field.className="hue-field";this.__input=document.createElement("input"); 68 | this.__input.type="text";this.__input_textShadow="0 1px 1px ";a.bind(this.__input,"keydown",function(a){13===a.keyCode&&g.call(this)});a.bind(this.__input,"blur",g);a.bind(this.__selector,"mousedown",function(b){a.addClass(this,"drag").bind(window,"mouseup",function(b){a.removeClass(s.__selector,"drag")})});var v=document.createElement("div");f.extend(this.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"});f.extend(this.__field_knob.style, 69 | {position:"absolute",width:"12px",height:"12px",border:this.__field_knob_border+(0.5>this.__color.v?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1});f.extend(this.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1});f.extend(this.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"});f.extend(v.style,{width:"100%",height:"100%", 70 | background:"none"});c(v,"top","rgba(0,0,0,0)","#000");f.extend(this.__hue_field.style,{width:"15px",height:"100px",display:"inline-block",border:"1px solid #555",cursor:"ns-resize"});p(this.__hue_field);f.extend(this.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:this.__input_textShadow+"rgba(0,0,0,0.7)"});a.bind(this.__saturation_field,"mousedown",r);a.bind(this.__field_knob,"mousedown",r);a.bind(this.__hue_field,"mousedown",function(b){u(b);a.bind(window, 71 | "mousemove",u);a.bind(window,"mouseup",h)});this.__saturation_field.appendChild(v);this.__selector.appendChild(this.__field_knob);this.__selector.appendChild(this.__saturation_field);this.__selector.appendChild(this.__hue_field);this.__hue_field.appendChild(this.__hue_knob);this.domElement.appendChild(this.__input);this.domElement.appendChild(this.__selector);this.updateDisplay()};k.superclass=e;f.extend(k.prototype,e.prototype,{updateDisplay:function(){var a=d(this.getValue());if(!1!==a){var e=!1; 72 | f.each(b.COMPONENTS,function(b){if(!f.isUndefined(a[b])&&!f.isUndefined(this.__color.__state[b])&&a[b]!==this.__color.__state[b])return e=!0,{}},this);e&&f.extend(this.__color.__state,a)}f.extend(this.__temp.__state,this.__color.__state);this.__temp.a=1;var k=0.5>this.__color.v||0.5a&&(a+=1);return{h:360*a,s:e/c,v:c/255}},rgb_to_hex:function(a,b,d){a=this.hex_with_component(0,2,a);a=this.hex_with_component(a,1,b);return a=this.hex_with_component(a,0,d)},component_from_hex:function(a,b){return a>>8*b&255},hex_with_component:function(a,b,d){return d<<(e=8*b)|a&~(255<c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:11,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; 7 | -------------------------------------------------------------------------------- /styles/base.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | 4 | 5 | 6 | /***************/ 7 | /* */ 8 | /* General */ 9 | /* */ 10 | /***************/ 11 | 12 | 13 | html, body { 14 | 15 | width: 100%; 16 | height: 100%; 17 | } 18 | body { 19 | 20 | margin: 0; 21 | border: 0; 22 | padding: 0; 23 | background-color: #000; 24 | color: #888; 25 | font-family: "F37Ginger", "Helvetica Neue", Helvetica, sans-serif; 26 | font-size: 16px; 27 | line-height: 20px; 28 | overflow: hidden; 29 | -webkit-touch-callout: none; 30 | -webkit-user-select: none; 31 | -khtml-user-select: none; 32 | -moz-user-select: none; 33 | -ms-user-select: none; 34 | user-select: none; 35 | } 36 | a { 37 | 38 | color: #06C; 39 | text-decoration: none; 40 | } 41 | a:hover { 42 | 43 | border-top: 2px solid #06C; 44 | border-bottom: 2px solid #06C; 45 | background-color: #06C; 46 | color: #FFF; 47 | } 48 | 49 | 50 | 51 | 52 | /*****************/ 53 | /* */ 54 | /* Interface */ 55 | /* */ 56 | /*****************/ 57 | 58 | 59 | .dg.ac .property-name, 60 | .dg.ac input, 61 | .dg.ac .close-button { 62 | 63 | font-family: "F37Ginger", "Helvetica Neue", Helvetica, sans-serif !important; 64 | font-size: 14px; 65 | } 66 | .dg.ac .close-button { 67 | 68 | font-size: 12px; 69 | color: rgba( 255, 255, 255, 0.3 ); 70 | } 71 | .dg.ac .close-button:hover { 72 | 73 | color: #FFF; 74 | } 75 | #about { 76 | 77 | position: absolute; 78 | top: 0; 79 | left: 100%; 80 | width: 100%; 81 | height: 100%; 82 | background-color: rgba( 255, 255, 255, 0.9 ); 83 | color: #000; 84 | opacity: 0.5; 85 | font-size: 1.5vw; 86 | line-height: 2.0vw; 87 | transition-property: left, opacity; 88 | transition-duration: 0.2s; 89 | transition-timing-function: ease-in; 90 | } 91 | #about.show { 92 | 93 | left: 0; 94 | opacity: 1; 95 | transition-property: left, opacity; 96 | transition-duration: 0.3s; 97 | transition-timing-function: ease-out; 98 | } 99 | #about .page { 100 | 101 | margin: 15vh auto 0 auto; 102 | width: 40vw; 103 | height: 85vh; 104 | overflow: auto; 105 | } 106 | #about h1 { 107 | 108 | margin-bottom: 2vw; 109 | font-size: 2vw; 110 | } 111 | 112 | 113 | 114 | 115 | /*************/ 116 | /* */ 117 | /* Stats */ 118 | /* */ 119 | /*************/ 120 | 121 | 122 | #stats { 123 | 124 | position: absolute; 125 | right: 0; 126 | bottom: 0; 127 | } 128 | #fps, #ms { 129 | 130 | background-color: rgba( 0, 0, 0, 0.85 ) !important; 131 | } 132 | #fpsText, #msText { 133 | 134 | color: #FFF !important; 135 | font-size: 11px !important; 136 | font-family: Akkurat-Mono, "Courier New", monospace !important; 137 | } 138 | #fpsGraph, #msGraph { 139 | 140 | background: linear-gradient( #FFF, #444 ) !important; 141 | } 142 | #fpsGraph span, #msGraph span { 143 | 144 | background-color: #333 !important; 145 | } 146 | 147 | 148 | 149 | --------------------------------------------------------------------------------