├── .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 | 
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 |
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 |
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;c
this.__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