├── .gitignore ├── LICENSE ├── README.md ├── app ├── css │ ├── index.css │ └── reset.css ├── images │ ├── hand-pull.png │ ├── hand-push.png │ ├── mouse-pull.png │ └── mouse-push.png ├── index.html ├── js │ ├── lib │ │ ├── Clock.js │ │ ├── CopyShader.js │ │ ├── OBJLoader.js │ │ ├── OrbitControls.js │ │ ├── Stats.js │ │ ├── TrackballControls.js │ │ ├── dat.gui.js │ │ ├── dat.gui.min.js │ │ ├── leap-0.6.4.js │ │ ├── leap-plugins-0.1.10.js │ │ ├── mousetrap.js │ │ ├── mousetrap.min.js │ │ ├── stats.min.js │ │ ├── three.min.js │ │ ├── three69.js │ │ └── three70.js │ └── src │ │ ├── App.js │ │ ├── App2.js │ │ ├── LeapManager.js │ │ ├── Mouse.js │ │ ├── ParticleEngine.js │ │ ├── ParticleSimulation.js │ │ ├── RenderContext.js │ │ ├── ShaderPass.js │ │ ├── SimulationRenderer.js │ │ ├── UVMapAnimator.js │ │ ├── UVMapper.js │ │ ├── UpdateLoop.js │ │ ├── Utils.js │ │ ├── index.js │ │ └── shaders │ │ ├── ParticleShader.js │ │ ├── SimDebugShader.js │ │ ├── SimInitShader.js │ │ ├── SimShader.js │ │ └── UVMapShader.js ├── leaptest.html ├── models │ ├── animals.json │ ├── bear.json │ ├── bison.json │ ├── chowchow.json │ ├── cow.json │ ├── deer.json │ ├── eagle.json │ ├── elk.json │ ├── flamingo.json │ ├── fox.json │ ├── horse.json │ ├── moose.json │ ├── owl.json │ ├── panther.json │ ├── parrot.json │ ├── rabbit.json │ ├── raccoon.json │ ├── raven.json │ ├── retriever.json │ ├── stork.json │ └── wolf.json ├── shaders │ ├── Basic.vs.glsl │ ├── BasicParticleShader.fs.glsl │ ├── BasicParticleShader.vs.glsl │ ├── BasicSimShader.fs.glsl │ ├── ParticleShader.fs.glsl │ ├── ParticleShader.vs.glsl │ ├── SimInitShader.fs.glsl │ ├── SimShader.fs.glsl │ ├── UVMapShader.fs.glsl │ ├── UVMapShader.vs.glsl │ └── chunks │ │ ├── Constants.glsl │ │ ├── NoiseFuncs.glsl │ │ ├── Rand.glsl │ │ ├── SimBasicShapes.glsl │ │ ├── SimGalaxy.glsl │ │ ├── SimInputPos.glsl │ │ ├── SimRoseGalaxy.glsl │ │ └── SimTextureTarget.glsl └── uvtest.html └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-project 2 | *.sublime-workspace -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 3-Clause License 2 | 3 | Copyright (c) 2015, Nop Jiarathanakul 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Particle Dream 2 | 3 | An interactive particle simulation for your mind 4 | 5 | See it [live here](http://www.iamnop.com/particles/) or as [video here](https://www.youtube.com/watch?v=i7DR_Cedbmc) 6 | 7 | Inspired by ["Particle Dreams (1988)"](http://www.karlsims.com/particle-dreams.html) by Karl Sims 8 | 9 | ![download](https://cloud.githubusercontent.com/assets/565791/6882387/2cbb2196-d540-11e4-8383-a79a8fc418d5.png) 10 | 11 | ## Credits 12 | 13 | Nop Jiarathanakul [iamnop.com](http://www.iamnop.com/) 14 | 15 | Animals by [Mirada](http://mirada.com/) from [ro.me](http://www.ro.me/) 16 | 17 | Music by [Kai Engel](https://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/Kai_Engel_-_Irsens_Tale_-_04_Moonlight_Reprise) 18 | 19 | ## License 20 | 21 | The BSD 3-Clause License 22 | 23 | Copyright © 2015 [Nop Jiarathanakul](http://iamnop.com/) 24 | -------------------------------------------------------------------------------- /app/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:'Lucida Grande',Verdana,Helvetica,sans-serif; 3 | font-size:12px; 4 | line-height:150%; 5 | 6 | overflow: hidden; 7 | 8 | -webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */ 9 | -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ 10 | } 11 | 12 | #webgl-container { 13 | overflow: hidden; 14 | position: absolute; 15 | width: 100%; 16 | top: 0; 17 | bottom: 0; 18 | } 19 | #webgl-container>canvas { 20 | float: left; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | #debug-box { 26 | position:absolute; 27 | bottom: 0; 28 | color: #ffffff; 29 | font-size: 32px; 30 | line-height:100%; 31 | } 32 | 33 | #help-box { 34 | width: 350px; 35 | height: 350px; 36 | position: absolute; 37 | top: 0; 38 | bottom: 0; 39 | left: 0; 40 | right: 0; 41 | margin: auto; 42 | z-index: 1; 43 | 44 | padding: 50px; 45 | color: white; 46 | background: rgba(0,0,0,0.5); 47 | text-align: center; 48 | } 49 | #help-box img { 50 | width: 120px; 51 | margin-top: 10px; 52 | margin-bottom: 10px; 53 | } 54 | #help-box a { 55 | color: cyan; 56 | } 57 | #help-box { 58 | transition: opacity 0.5s, visibility 0.5s; 59 | visibility: visible; 60 | opacity: 1.0; 61 | } 62 | #help-box.hidden { 63 | visibility: hidden; 64 | opacity: 0.0; 65 | } 66 | 67 | .gradient-bg-cyan-black { 68 | /* IE10 Consumer Preview */ 69 | background-image: -ms-linear-gradient(top, #005C7A 0%, #000000 100%); 70 | 71 | /* Mozilla Firefox */ 72 | background-image: -moz-linear-gradient(top, #005C7A 0%, #000000 100%); 73 | 74 | /* Opera */ 75 | background-image: -o-linear-gradient(top, #005C7A 0%, #000000 100%); 76 | 77 | /* Webkit (Safari/Chrome 10) */ 78 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #005C7A), color-stop(1, #000000)); 79 | 80 | /* Webkit (Chrome 11+) */ 81 | background-image: -webkit-linear-gradient(top, #005C7A 0%, #000000 100%); 82 | 83 | /* W3C Markup, IE10 Release Preview */ 84 | background-image: linear-gradient(to bottom, #005C7A 0%, #000000 100%); 85 | } 86 | 87 | .gradient-bg-gray-black { 88 | /* IE10 Consumer Preview */ 89 | background-image: -ms-linear-gradient(top, #666666 0%, #000000 100%); 90 | 91 | /* Mozilla Firefox */ 92 | background-image: -moz-linear-gradient(top, #666666 0%, #000000 100%); 93 | 94 | /* Opera */ 95 | background-image: -o-linear-gradient(top, #666666 0%, #000000 100%); 96 | 97 | /* Webkit (Safari/Chrome 10) */ 98 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #666666), color-stop(1, #000000)); 99 | 100 | /* Webkit (Chrome 11+) */ 101 | background-image: -webkit-linear-gradient(top, #666666 0%, #000000 100%); 102 | 103 | /* W3C Markup, IE10 Release Preview */ 104 | background-image: linear-gradient(to bottom, #666666 0%, #000000 100%); 105 | } 106 | 107 | .gradient-bg-darkgray-black { 108 | /* IE10 Consumer Preview */ 109 | background-image: -ms-linear-gradient(top, #333333 0%, #000000 100%); 110 | 111 | /* Mozilla Firefox */ 112 | background-image: -moz-linear-gradient(top, #333333 0%, #000000 100%); 113 | 114 | /* Opera */ 115 | background-image: -o-linear-gradient(top, #333333 0%, #000000 100%); 116 | 117 | /* Webkit (Safari/Chrome 10) */ 118 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #333333), color-stop(1, #000000)); 119 | 120 | /* Webkit (Chrome 11+) */ 121 | background-image: -webkit-linear-gradient(top, #333333 0%, #000000 100%); 122 | 123 | /* W3C Markup, IE10 Release Preview */ 124 | background-image: linear-gradient(to bottom, #333333 0%, #000000 100%); 125 | } -------------------------------------------------------------------------------- /app/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /app/images/hand-pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nopjia/particles/2e2f10641759c750e2ec9238e0e8534711fd1bdf/app/images/hand-pull.png -------------------------------------------------------------------------------- /app/images/hand-push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nopjia/particles/2e2f10641759c750e2ec9238e0e8534711fd1bdf/app/images/hand-push.png -------------------------------------------------------------------------------- /app/images/mouse-pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nopjia/particles/2e2f10641759c750e2ec9238e0e8534711fd1bdf/app/images/mouse-pull.png -------------------------------------------------------------------------------- /app/images/mouse-push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nopjia/particles/2e2f10641759c750e2ec9238e0e8534711fd1bdf/app/images/mouse-push.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | A Particle Dream - v1.1 7 | 8 | 9 | 10 | 11 | 25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/js/lib/Clock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | var Clock = function ( autoStart ) { 6 | 7 | this.autoStart = ( autoStart !== undefined ) ? autoStart : true; 8 | 9 | this.startTime = 0; 10 | this.oldTime = 0; 11 | this.elapsedTime = 0; 12 | 13 | this.running = false; 14 | 15 | }; 16 | 17 | Clock.prototype.start = function () { 18 | 19 | this.startTime = Date.now(); 20 | this.oldTime = this.startTime; 21 | 22 | this.running = true; 23 | 24 | }; 25 | 26 | Clock.prototype.stop = function () { 27 | 28 | this.getElapsedTime(); 29 | 30 | this.running = false; 31 | 32 | }; 33 | 34 | Clock.prototype.getElapsedTime = function () { 35 | 36 | this.elapsedTime += this.getDelta(); 37 | 38 | return this.elapsedTime; 39 | 40 | }; 41 | 42 | 43 | Clock.prototype.getDelta = function () { 44 | 45 | var diff = 0; 46 | 47 | if ( this.autoStart && ! this.running ) { 48 | 49 | this.start(); 50 | 51 | } 52 | 53 | if ( this.running ) { 54 | 55 | var newTime = Date.now(); 56 | diff = 0.001 * ( newTime - this.oldTime ); 57 | this.oldTime = newTime; 58 | 59 | this.elapsedTime += diff; 60 | 61 | } 62 | 63 | return diff; 64 | 65 | }; -------------------------------------------------------------------------------- /app/js/lib/CopyShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * 4 | * Full-screen textured quad shader 5 | */ 6 | 7 | THREE.CopyShader = { 8 | 9 | uniforms: { 10 | 11 | "tDiffuse": { type: "t", value: null }, 12 | "opacity": { type: "f", value: 1.0 } 13 | 14 | }, 15 | 16 | vertexShader: [ 17 | 18 | "varying vec2 vUv;", 19 | 20 | "void main() {", 21 | 22 | "vUv = uv;", 23 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 24 | 25 | "}" 26 | 27 | ].join("\n"), 28 | 29 | fragmentShader: [ 30 | 31 | "uniform float opacity;", 32 | 33 | "uniform sampler2D tDiffuse;", 34 | 35 | "varying vec2 vUv;", 36 | 37 | "void main() {", 38 | 39 | "vec4 texel = texture2D( tDiffuse, vUv );", 40 | "gl_FragColor = opacity * texel;", 41 | 42 | "}" 43 | 44 | ].join("\n") 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /app/js/lib/OBJLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | THREE.OBJLoader = function ( manager ) { 6 | 7 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 8 | 9 | }; 10 | 11 | THREE.OBJLoader.prototype = { 12 | 13 | constructor: THREE.OBJLoader, 14 | 15 | load: function ( url, onLoad, onProgress, onError ) { 16 | 17 | var scope = this; 18 | 19 | var loader = new THREE.XHRLoader( scope.manager ); 20 | loader.setCrossOrigin( this.crossOrigin ); 21 | loader.load( url, function ( text ) { 22 | 23 | onLoad( scope.parse( text ) ); 24 | 25 | }, onProgress, onError ); 26 | 27 | }, 28 | 29 | parse: function ( text ) { 30 | 31 | console.time( 'OBJLoader' ); 32 | 33 | var object, objects = []; 34 | var geometry, material; 35 | 36 | function parseVertexIndex( value ) { 37 | 38 | var index = parseInt( value ); 39 | 40 | return ( index >= 0 ? index - 1 : index + vertices.length / 3 ) * 3; 41 | 42 | } 43 | 44 | function parseNormalIndex( value ) { 45 | 46 | var index = parseInt( value ); 47 | 48 | return ( index >= 0 ? index - 1 : index + normals.length / 3 ) * 3; 49 | 50 | } 51 | 52 | function parseUVIndex( value ) { 53 | 54 | var index = parseInt( value ); 55 | 56 | return ( index >= 0 ? index - 1 : index + uvs.length / 2 ) * 2; 57 | 58 | } 59 | 60 | function addVertex( a, b, c ) { 61 | 62 | geometry.vertices.push( 63 | vertices[ a ], vertices[ a + 1 ], vertices[ a + 2 ], 64 | vertices[ b ], vertices[ b + 1 ], vertices[ b + 2 ], 65 | vertices[ c ], vertices[ c + 1 ], vertices[ c + 2 ] 66 | ); 67 | 68 | } 69 | 70 | function addNormal( a, b, c ) { 71 | 72 | geometry.normals.push( 73 | normals[ a ], normals[ a + 1 ], normals[ a + 2 ], 74 | normals[ b ], normals[ b + 1 ], normals[ b + 2 ], 75 | normals[ c ], normals[ c + 1 ], normals[ c + 2 ] 76 | ); 77 | 78 | } 79 | 80 | function addUV( a, b, c ) { 81 | 82 | geometry.uvs.push( 83 | uvs[ a ], uvs[ a + 1 ], 84 | uvs[ b ], uvs[ b + 1 ], 85 | uvs[ c ], uvs[ c + 1 ] 86 | ); 87 | 88 | } 89 | 90 | function addFace( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { 91 | 92 | var ia = parseVertexIndex( a ); 93 | var ib = parseVertexIndex( b ); 94 | var ic = parseVertexIndex( c ); 95 | 96 | if ( d === undefined ) { 97 | 98 | addVertex( ia, ib, ic ); 99 | 100 | } else { 101 | 102 | var id = parseVertexIndex( d ); 103 | 104 | addVertex( ia, ib, id ); 105 | addVertex( ib, ic, id ); 106 | 107 | } 108 | 109 | if ( ua !== undefined ) { 110 | 111 | var ia = parseUVIndex( ua ); 112 | var ib = parseUVIndex( ub ); 113 | var ic = parseUVIndex( uc ); 114 | 115 | if ( d === undefined ) { 116 | 117 | addUV( ia, ib, ic ); 118 | 119 | } else { 120 | 121 | var id = parseUVIndex( ud ); 122 | 123 | addUV( ia, ib, id ); 124 | addUV( ib, ic, id ); 125 | 126 | } 127 | 128 | } 129 | 130 | if ( na !== undefined ) { 131 | 132 | var ia = parseNormalIndex( na ); 133 | var ib = parseNormalIndex( nb ); 134 | var ic = parseNormalIndex( nc ); 135 | 136 | if ( d === undefined ) { 137 | 138 | addNormal( ia, ib, ic ); 139 | 140 | } else { 141 | 142 | var id = parseNormalIndex( nd ); 143 | 144 | addNormal( ia, ib, id ); 145 | addNormal( ib, ic, id ); 146 | 147 | } 148 | 149 | } 150 | 151 | } 152 | 153 | // create mesh if no objects in text 154 | 155 | if ( /^o /gm.test( text ) === false ) { 156 | 157 | geometry = { 158 | vertices: [], 159 | normals: [], 160 | uvs: [] 161 | }; 162 | 163 | material = { 164 | name: '' 165 | }; 166 | 167 | object = { 168 | name: '', 169 | geometry: geometry, 170 | material: material 171 | }; 172 | 173 | objects.push( object ); 174 | 175 | } 176 | 177 | var vertices = []; 178 | var normals = []; 179 | var uvs = []; 180 | 181 | // v float float float 182 | 183 | var vertex_pattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; 184 | 185 | // vn float float float 186 | 187 | var normal_pattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; 188 | 189 | // vt float float 190 | 191 | var uv_pattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; 192 | 193 | // f vertex vertex vertex ... 194 | 195 | var face_pattern1 = /f( +-?\d+)( +-?\d+)( +-?\d+)( +-?\d+)?/; 196 | 197 | // f vertex/uv vertex/uv vertex/uv ... 198 | 199 | var face_pattern2 = /f( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))?/; 200 | 201 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ... 202 | 203 | var face_pattern3 = /f( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))?/; 204 | 205 | // f vertex//normal vertex//normal vertex//normal ... 206 | 207 | var face_pattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/ 208 | 209 | // 210 | 211 | var lines = text.split( '\n' ); 212 | 213 | for ( var i = 0; i < lines.length; i ++ ) { 214 | 215 | var line = lines[ i ]; 216 | line = line.trim(); 217 | 218 | var result; 219 | 220 | if ( line.length === 0 || line.charAt( 0 ) === '#' ) { 221 | 222 | continue; 223 | 224 | } else if ( ( result = vertex_pattern.exec( line ) ) !== null ) { 225 | 226 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 227 | 228 | vertices.push( 229 | parseFloat( result[ 1 ] ), 230 | parseFloat( result[ 2 ] ), 231 | parseFloat( result[ 3 ] ) 232 | ); 233 | 234 | } else if ( ( result = normal_pattern.exec( line ) ) !== null ) { 235 | 236 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 237 | 238 | normals.push( 239 | parseFloat( result[ 1 ] ), 240 | parseFloat( result[ 2 ] ), 241 | parseFloat( result[ 3 ] ) 242 | ); 243 | 244 | } else if ( ( result = uv_pattern.exec( line ) ) !== null ) { 245 | 246 | // ["vt 0.1 0.2", "0.1", "0.2"] 247 | 248 | uvs.push( 249 | parseFloat( result[ 1 ] ), 250 | parseFloat( result[ 2 ] ) 251 | ); 252 | 253 | } else if ( ( result = face_pattern1.exec( line ) ) !== null ) { 254 | 255 | // ["f 1 2 3", "1", "2", "3", undefined] 256 | 257 | addFace( 258 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] 259 | ); 260 | 261 | } else if ( ( result = face_pattern2.exec( line ) ) !== null ) { 262 | 263 | // ["f 1/1 2/2 3/3", " 1/1", "1", "1", " 2/2", "2", "2", " 3/3", "3", "3", undefined, undefined, undefined] 264 | 265 | addFace( 266 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 267 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 268 | ); 269 | 270 | } else if ( ( result = face_pattern3.exec( line ) ) !== null ) { 271 | 272 | // ["f 1/1/1 2/2/2 3/3/3", " 1/1/1", "1", "1", "1", " 2/2/2", "2", "2", "2", " 3/3/3", "3", "3", "3", undefined, undefined, undefined, undefined] 273 | 274 | addFace( 275 | result[ 2 ], result[ 6 ], result[ 10 ], result[ 14 ], 276 | result[ 3 ], result[ 7 ], result[ 11 ], result[ 15 ], 277 | result[ 4 ], result[ 8 ], result[ 12 ], result[ 16 ] 278 | ); 279 | 280 | } else if ( ( result = face_pattern4.exec( line ) ) !== null ) { 281 | 282 | // ["f 1//1 2//2 3//3", " 1//1", "1", "1", " 2//2", "2", "2", " 3//3", "3", "3", undefined, undefined, undefined] 283 | 284 | addFace( 285 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 286 | undefined, undefined, undefined, undefined, 287 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 288 | ); 289 | 290 | } else if ( /^o /.test( line ) ) { 291 | 292 | geometry = { 293 | vertices: [], 294 | normals: [], 295 | uvs: [] 296 | }; 297 | 298 | material = { 299 | name: '' 300 | }; 301 | 302 | object = { 303 | name: line.substring( 2 ).trim(), 304 | geometry: geometry, 305 | material: material 306 | }; 307 | 308 | objects.push( object ) 309 | 310 | } else if ( /^g /.test( line ) ) { 311 | 312 | // group 313 | 314 | } else if ( /^usemtl /.test( line ) ) { 315 | 316 | // material 317 | 318 | material.name = line.substring( 7 ).trim(); 319 | 320 | } else if ( /^mtllib /.test( line ) ) { 321 | 322 | // mtl file 323 | 324 | } else if ( /^s /.test( line ) ) { 325 | 326 | // smooth shading 327 | 328 | } else { 329 | 330 | // console.log( "THREE.OBJLoader: Unhandled line " + line ); 331 | 332 | } 333 | 334 | } 335 | 336 | var container = new THREE.Object3D(); 337 | 338 | for ( var i = 0, l = objects.length; i < l; i ++ ) { 339 | 340 | var object = objects[ i ]; 341 | var geometry = object.geometry; 342 | 343 | var buffergeometry = new THREE.BufferGeometry(); 344 | 345 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); 346 | 347 | if ( geometry.normals.length > 0 ) { 348 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); 349 | } 350 | 351 | if ( geometry.uvs.length > 0 ) { 352 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); 353 | } 354 | 355 | var material = new THREE.MeshLambertMaterial(); 356 | material.name = object.material.name; 357 | 358 | var mesh = new THREE.Mesh( buffergeometry, material ); 359 | mesh.name = object.name; 360 | 361 | container.add( mesh ); 362 | 363 | } 364 | 365 | console.timeEnd( 'OBJLoader' ); 366 | 367 | return container; 368 | 369 | } 370 | 371 | }; -------------------------------------------------------------------------------- /app/js/lib/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | // 18 | // This is a drop-in replacement for (most) TrackballControls used in examples. 19 | // That is, include this js file and wherever you see: 20 | // controls = new THREE.TrackballControls( camera ); 21 | // controls.target.z = 150; 22 | // Simple substitute "OrbitControls" and the control should work as-is. 23 | 24 | THREE.OrbitControls = function ( object, domElement ) { 25 | 26 | this.object = object; 27 | this.domElement = ( domElement !== undefined ) ? domElement : document; 28 | 29 | // API 30 | 31 | // Set to false to disable this control 32 | this.enabled = true; 33 | 34 | // "target" sets the location of focus, where the control orbits around 35 | // and where it pans with respect to. 36 | this.target = new THREE.Vector3(); 37 | 38 | // center is old, deprecated; use "target" instead 39 | this.center = this.target; 40 | 41 | // This option actually enables dollying in and out; left as "zoom" for 42 | // backwards compatibility 43 | this.noZoom = false; 44 | this.zoomSpeed = 1.0; 45 | 46 | // Limits to how far you can dolly in and out 47 | this.minDistance = 0; 48 | this.maxDistance = Infinity; 49 | 50 | // Set to true to disable this control 51 | this.noRotate = false; 52 | this.rotateSpeed = 1.0; 53 | 54 | // Set to true to disable this control 55 | this.noPan = false; 56 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 57 | 58 | // Set to true to automatically rotate around the target 59 | this.autoRotate = false; 60 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 61 | 62 | // How far you can orbit vertically, upper and lower limits. 63 | // Range is 0 to Math.PI radians. 64 | this.minPolarAngle = 0; // radians 65 | this.maxPolarAngle = Math.PI; // radians 66 | 67 | // How far you can orbit horizontally, upper and lower limits. 68 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 69 | this.minAzimuthAngle = - Infinity; // radians 70 | this.maxAzimuthAngle = Infinity; // radians 71 | 72 | // Set to true to disable use of the keys 73 | this.noKeys = false; 74 | 75 | // The four arrow keys 76 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 77 | 78 | // Mouse buttons 79 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 80 | 81 | //////////// 82 | // internals 83 | 84 | var scope = this; 85 | 86 | var EPS = 0.000001; 87 | 88 | var rotateStart = new THREE.Vector2(); 89 | var rotateEnd = new THREE.Vector2(); 90 | var rotateDelta = new THREE.Vector2(); 91 | 92 | var panStart = new THREE.Vector2(); 93 | var panEnd = new THREE.Vector2(); 94 | var panDelta = new THREE.Vector2(); 95 | var panOffset = new THREE.Vector3(); 96 | 97 | var offset = new THREE.Vector3(); 98 | 99 | var dollyStart = new THREE.Vector2(); 100 | var dollyEnd = new THREE.Vector2(); 101 | var dollyDelta = new THREE.Vector2(); 102 | 103 | var theta; 104 | var phi; 105 | var phiDelta = 0; 106 | var thetaDelta = 0; 107 | var scale = 1; 108 | var pan = new THREE.Vector3(); 109 | 110 | var lastPosition = new THREE.Vector3(); 111 | var lastQuaternion = new THREE.Quaternion(); 112 | 113 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 114 | 115 | var state = STATE.NONE; 116 | 117 | // for reset 118 | 119 | this.target0 = this.target.clone(); 120 | this.position0 = this.object.position.clone(); 121 | 122 | // so camera.up is the orbit axis 123 | 124 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 125 | var quatInverse = quat.clone().inverse(); 126 | 127 | // events 128 | 129 | var changeEvent = { type: 'change' }; 130 | var startEvent = { type: 'start'}; 131 | var endEvent = { type: 'end'}; 132 | 133 | this.rotateLeft = function ( angle ) { 134 | 135 | if ( angle === undefined ) { 136 | 137 | angle = getAutoRotationAngle(); 138 | 139 | } 140 | 141 | thetaDelta -= angle; 142 | 143 | }; 144 | 145 | this.rotateUp = function ( angle ) { 146 | 147 | if ( angle === undefined ) { 148 | 149 | angle = getAutoRotationAngle(); 150 | 151 | } 152 | 153 | phiDelta -= angle; 154 | 155 | }; 156 | 157 | // pass in distance in world space to move left 158 | this.panLeft = function ( distance ) { 159 | 160 | var te = this.object.matrix.elements; 161 | 162 | // get X column of matrix 163 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 164 | panOffset.multiplyScalar( - distance ); 165 | 166 | pan.add( panOffset ); 167 | 168 | }; 169 | 170 | // pass in distance in world space to move up 171 | this.panUp = function ( distance ) { 172 | 173 | var te = this.object.matrix.elements; 174 | 175 | // get Y column of matrix 176 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 177 | panOffset.multiplyScalar( distance ); 178 | 179 | pan.add( panOffset ); 180 | 181 | }; 182 | 183 | // pass in x,y of change desired in pixel space, 184 | // right and down are positive 185 | this.pan = function ( deltaX, deltaY ) { 186 | 187 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 188 | 189 | if ( scope.object.fov !== undefined ) { 190 | 191 | // perspective 192 | var position = scope.object.position; 193 | var offset = position.clone().sub( scope.target ); 194 | var targetDistance = offset.length(); 195 | 196 | // half of the fov is center to top of screen 197 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 198 | 199 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 200 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 201 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 202 | 203 | } else if ( scope.object.top !== undefined ) { 204 | 205 | // orthographic 206 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 207 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 208 | 209 | } else { 210 | 211 | // camera neither orthographic or perspective 212 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 213 | 214 | } 215 | 216 | }; 217 | 218 | this.dollyIn = function ( dollyScale ) { 219 | 220 | if ( dollyScale === undefined ) { 221 | 222 | dollyScale = getZoomScale(); 223 | 224 | } 225 | 226 | scale /= dollyScale; 227 | 228 | }; 229 | 230 | this.dollyOut = function ( dollyScale ) { 231 | 232 | if ( dollyScale === undefined ) { 233 | 234 | dollyScale = getZoomScale(); 235 | 236 | } 237 | 238 | scale *= dollyScale; 239 | 240 | }; 241 | 242 | this.update = function () { 243 | 244 | var position = this.object.position; 245 | 246 | offset.copy( position ).sub( this.target ); 247 | 248 | // rotate offset to "y-axis-is-up" space 249 | offset.applyQuaternion( quat ); 250 | 251 | // angle from z-axis around y-axis 252 | 253 | theta = Math.atan2( offset.x, offset.z ); 254 | 255 | // angle from y-axis 256 | 257 | phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 258 | 259 | if ( this.autoRotate && state === STATE.NONE ) { 260 | 261 | this.rotateLeft( getAutoRotationAngle() ); 262 | 263 | } 264 | 265 | theta += thetaDelta; 266 | phi += phiDelta; 267 | 268 | // restrict theta to be between desired limits 269 | theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); 270 | 271 | // restrict phi to be between desired limits 272 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 273 | 274 | // restrict phi to be betwee EPS and PI-EPS 275 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 276 | 277 | var radius = offset.length() * scale; 278 | 279 | // restrict radius to be between desired limits 280 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 281 | 282 | // move target to panned location 283 | this.target.add( pan ); 284 | 285 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 286 | offset.y = radius * Math.cos( phi ); 287 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 288 | 289 | // rotate offset back to "camera-up-vector-is-up" space 290 | offset.applyQuaternion( quatInverse ); 291 | 292 | position.copy( this.target ).add( offset ); 293 | 294 | this.object.lookAt( this.target ); 295 | 296 | thetaDelta = 0; 297 | phiDelta = 0; 298 | scale = 1; 299 | pan.set( 0, 0, 0 ); 300 | 301 | // update condition is: 302 | // min(camera displacement, camera rotation in radians)^2 > EPS 303 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 304 | 305 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS 306 | || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) { 307 | 308 | this.dispatchEvent( changeEvent ); 309 | 310 | lastPosition.copy( this.object.position ); 311 | lastQuaternion.copy (this.object.quaternion ); 312 | 313 | } 314 | 315 | }; 316 | 317 | 318 | this.reset = function () { 319 | 320 | state = STATE.NONE; 321 | 322 | this.target.copy( this.target0 ); 323 | this.object.position.copy( this.position0 ); 324 | 325 | this.update(); 326 | 327 | }; 328 | 329 | this.getPolarAngle = function () { 330 | 331 | return phi; 332 | 333 | }; 334 | 335 | this.getAzimuthalAngle = function () { 336 | 337 | return theta 338 | 339 | }; 340 | 341 | function getAutoRotationAngle() { 342 | 343 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 344 | 345 | } 346 | 347 | function getZoomScale() { 348 | 349 | return Math.pow( 0.95, scope.zoomSpeed ); 350 | 351 | } 352 | 353 | function onMouseDown( event ) { 354 | 355 | if ( scope.enabled === false ) return; 356 | event.preventDefault(); 357 | 358 | if ( event.button === scope.mouseButtons.ORBIT ) { 359 | if ( scope.noRotate === true ) return; 360 | 361 | state = STATE.ROTATE; 362 | 363 | rotateStart.set( event.clientX, event.clientY ); 364 | 365 | } else if ( event.button === scope.mouseButtons.ZOOM ) { 366 | if ( scope.noZoom === true ) return; 367 | 368 | state = STATE.DOLLY; 369 | 370 | dollyStart.set( event.clientX, event.clientY ); 371 | 372 | } else if ( event.button === scope.mouseButtons.PAN ) { 373 | if ( scope.noPan === true ) return; 374 | 375 | state = STATE.PAN; 376 | 377 | panStart.set( event.clientX, event.clientY ); 378 | 379 | } 380 | 381 | if ( state !== STATE.NONE ) { 382 | document.addEventListener( 'mousemove', onMouseMove, false ); 383 | document.addEventListener( 'mouseup', onMouseUp, false ); 384 | scope.dispatchEvent( startEvent ); 385 | } 386 | 387 | } 388 | 389 | function onMouseMove( event ) { 390 | 391 | if ( scope.enabled === false ) return; 392 | 393 | event.preventDefault(); 394 | 395 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 396 | 397 | if ( state === STATE.ROTATE ) { 398 | 399 | if ( scope.noRotate === true ) return; 400 | 401 | rotateEnd.set( event.clientX, event.clientY ); 402 | rotateDelta.subVectors( rotateEnd, rotateStart ); 403 | 404 | // rotating across whole screen goes 360 degrees around 405 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 406 | 407 | // rotating up and down along whole screen attempts to go 360, but limited to 180 408 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 409 | 410 | rotateStart.copy( rotateEnd ); 411 | 412 | } else if ( state === STATE.DOLLY ) { 413 | 414 | if ( scope.noZoom === true ) return; 415 | 416 | dollyEnd.set( event.clientX, event.clientY ); 417 | dollyDelta.subVectors( dollyEnd, dollyStart ); 418 | 419 | if ( dollyDelta.y > 0 ) { 420 | 421 | scope.dollyIn(); 422 | 423 | } else { 424 | 425 | scope.dollyOut(); 426 | 427 | } 428 | 429 | dollyStart.copy( dollyEnd ); 430 | 431 | } else if ( state === STATE.PAN ) { 432 | 433 | if ( scope.noPan === true ) return; 434 | 435 | panEnd.set( event.clientX, event.clientY ); 436 | panDelta.subVectors( panEnd, panStart ); 437 | 438 | scope.pan( panDelta.x, panDelta.y ); 439 | 440 | panStart.copy( panEnd ); 441 | 442 | } 443 | 444 | if ( state !== STATE.NONE ) scope.update(); 445 | 446 | } 447 | 448 | function onMouseUp( /* event */ ) { 449 | 450 | if ( scope.enabled === false ) return; 451 | 452 | document.removeEventListener( 'mousemove', onMouseMove, false ); 453 | document.removeEventListener( 'mouseup', onMouseUp, false ); 454 | scope.dispatchEvent( endEvent ); 455 | state = STATE.NONE; 456 | 457 | } 458 | 459 | function onMouseWheel( event ) { 460 | 461 | if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return; 462 | 463 | event.preventDefault(); 464 | event.stopPropagation(); 465 | 466 | var delta = 0; 467 | 468 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 469 | 470 | delta = event.wheelDelta; 471 | 472 | } else if ( event.detail !== undefined ) { // Firefox 473 | 474 | delta = - event.detail; 475 | 476 | } 477 | 478 | if ( delta > 0 ) { 479 | 480 | scope.dollyOut(); 481 | 482 | } else { 483 | 484 | scope.dollyIn(); 485 | 486 | } 487 | 488 | scope.update(); 489 | scope.dispatchEvent( startEvent ); 490 | scope.dispatchEvent( endEvent ); 491 | 492 | } 493 | 494 | function onKeyDown( event ) { 495 | 496 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 497 | 498 | switch ( event.keyCode ) { 499 | 500 | case scope.keys.UP: 501 | scope.pan( 0, scope.keyPanSpeed ); 502 | scope.update(); 503 | break; 504 | 505 | case scope.keys.BOTTOM: 506 | scope.pan( 0, - scope.keyPanSpeed ); 507 | scope.update(); 508 | break; 509 | 510 | case scope.keys.LEFT: 511 | scope.pan( scope.keyPanSpeed, 0 ); 512 | scope.update(); 513 | break; 514 | 515 | case scope.keys.RIGHT: 516 | scope.pan( - scope.keyPanSpeed, 0 ); 517 | scope.update(); 518 | break; 519 | 520 | } 521 | 522 | } 523 | 524 | function touchstart( event ) { 525 | 526 | if ( scope.enabled === false ) return; 527 | 528 | switch ( event.touches.length ) { 529 | 530 | case 1: // one-fingered touch: rotate 531 | 532 | if ( scope.noRotate === true ) return; 533 | 534 | state = STATE.TOUCH_ROTATE; 535 | 536 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 537 | break; 538 | 539 | case 2: // two-fingered touch: dolly 540 | 541 | if ( scope.noZoom === true ) return; 542 | 543 | state = STATE.TOUCH_DOLLY; 544 | 545 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 546 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 547 | var distance = Math.sqrt( dx * dx + dy * dy ); 548 | dollyStart.set( 0, distance ); 549 | break; 550 | 551 | case 3: // three-fingered touch: pan 552 | 553 | if ( scope.noPan === true ) return; 554 | 555 | state = STATE.TOUCH_PAN; 556 | 557 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 558 | break; 559 | 560 | default: 561 | 562 | state = STATE.NONE; 563 | 564 | } 565 | 566 | if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent ); 567 | 568 | } 569 | 570 | function touchmove( event ) { 571 | 572 | if ( scope.enabled === false ) return; 573 | 574 | event.preventDefault(); 575 | event.stopPropagation(); 576 | 577 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 578 | 579 | switch ( event.touches.length ) { 580 | 581 | case 1: // one-fingered touch: rotate 582 | 583 | if ( scope.noRotate === true ) return; 584 | if ( state !== STATE.TOUCH_ROTATE ) return; 585 | 586 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 587 | rotateDelta.subVectors( rotateEnd, rotateStart ); 588 | 589 | // rotating across whole screen goes 360 degrees around 590 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 591 | // rotating up and down along whole screen attempts to go 360, but limited to 180 592 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 593 | 594 | rotateStart.copy( rotateEnd ); 595 | 596 | scope.update(); 597 | break; 598 | 599 | case 2: // two-fingered touch: dolly 600 | 601 | if ( scope.noZoom === true ) return; 602 | if ( state !== STATE.TOUCH_DOLLY ) return; 603 | 604 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 605 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 606 | var distance = Math.sqrt( dx * dx + dy * dy ); 607 | 608 | dollyEnd.set( 0, distance ); 609 | dollyDelta.subVectors( dollyEnd, dollyStart ); 610 | 611 | if ( dollyDelta.y > 0 ) { 612 | 613 | scope.dollyOut(); 614 | 615 | } else { 616 | 617 | scope.dollyIn(); 618 | 619 | } 620 | 621 | dollyStart.copy( dollyEnd ); 622 | 623 | scope.update(); 624 | break; 625 | 626 | case 3: // three-fingered touch: pan 627 | 628 | if ( scope.noPan === true ) return; 629 | if ( state !== STATE.TOUCH_PAN ) return; 630 | 631 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 632 | panDelta.subVectors( panEnd, panStart ); 633 | 634 | scope.pan( panDelta.x, panDelta.y ); 635 | 636 | panStart.copy( panEnd ); 637 | 638 | scope.update(); 639 | break; 640 | 641 | default: 642 | 643 | state = STATE.NONE; 644 | 645 | } 646 | 647 | } 648 | 649 | function touchend( /* event */ ) { 650 | 651 | if ( scope.enabled === false ) return; 652 | 653 | scope.dispatchEvent( endEvent ); 654 | state = STATE.NONE; 655 | 656 | } 657 | 658 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 659 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 660 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 661 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 662 | 663 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 664 | this.domElement.addEventListener( 'touchend', touchend, false ); 665 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 666 | 667 | window.addEventListener( 'keydown', onKeyDown, false ); 668 | 669 | // force an update at start 670 | this.update(); 671 | 672 | }; 673 | 674 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 675 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 676 | -------------------------------------------------------------------------------- /app/js/lib/Stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | var Stats = function () { 6 | 7 | var startTime = Date.now(), prevTime = startTime; 8 | var ms = 0, msMin = Infinity, msMax = 0; 9 | var fps = 0, fpsMin = Infinity, fpsMax = 0; 10 | var frames = 0, mode = 0; 11 | 12 | var container = document.createElement( 'div' ); 13 | container.id = 'stats'; 14 | container.addEventListener( 'mousedown', function ( event ) { event.preventDefault(); setMode( ++ mode % 2 ) }, false ); 15 | container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; 16 | 17 | var fpsDiv = document.createElement( 'div' ); 18 | fpsDiv.id = 'fps'; 19 | fpsDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#002'; 20 | container.appendChild( fpsDiv ); 21 | 22 | var fpsText = document.createElement( 'div' ); 23 | fpsText.id = 'fpsText'; 24 | fpsText.style.cssText = 'color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 25 | fpsText.innerHTML = 'FPS'; 26 | fpsDiv.appendChild( fpsText ); 27 | 28 | var fpsGraph = document.createElement( 'div' ); 29 | fpsGraph.id = 'fpsGraph'; 30 | fpsGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0ff'; 31 | fpsDiv.appendChild( fpsGraph ); 32 | 33 | while ( fpsGraph.children.length < 74 ) { 34 | 35 | var bar = document.createElement( 'span' ); 36 | bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#113'; 37 | fpsGraph.appendChild( bar ); 38 | 39 | } 40 | 41 | var msDiv = document.createElement( 'div' ); 42 | msDiv.id = 'ms'; 43 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;display:none'; 44 | container.appendChild( msDiv ); 45 | 46 | var msText = document.createElement( 'div' ); 47 | msText.id = 'msText'; 48 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 49 | msText.innerHTML = 'MS'; 50 | msDiv.appendChild( msText ); 51 | 52 | var msGraph = document.createElement( 'div' ); 53 | msGraph.id = 'msGraph'; 54 | msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0'; 55 | msDiv.appendChild( msGraph ); 56 | 57 | while ( msGraph.children.length < 74 ) { 58 | 59 | var bar = document.createElement( 'span' ); 60 | bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131'; 61 | msGraph.appendChild( bar ); 62 | 63 | } 64 | 65 | var setMode = function ( value ) { 66 | 67 | mode = value; 68 | 69 | switch ( mode ) { 70 | 71 | case 0: 72 | fpsDiv.style.display = 'block'; 73 | msDiv.style.display = 'none'; 74 | break; 75 | case 1: 76 | fpsDiv.style.display = 'none'; 77 | msDiv.style.display = 'block'; 78 | break; 79 | } 80 | 81 | }; 82 | 83 | var updateGraph = function ( dom, value ) { 84 | 85 | var child = dom.appendChild( dom.firstChild ); 86 | child.style.height = value + 'px'; 87 | 88 | }; 89 | 90 | return { 91 | 92 | REVISION: 12, 93 | 94 | domElement: container, 95 | 96 | setMode: setMode, 97 | 98 | begin: function () { 99 | 100 | startTime = Date.now(); 101 | 102 | }, 103 | 104 | end: function () { 105 | 106 | var time = Date.now(); 107 | 108 | ms = time - startTime; 109 | msMin = Math.min( msMin, ms ); 110 | msMax = Math.max( msMax, ms ); 111 | 112 | msText.textContent = ms + ' MS (' + msMin + '-' + msMax + ')'; 113 | updateGraph( msGraph, Math.min( 30, 30 - ( ms / 200 ) * 30 ) ); 114 | 115 | frames ++; 116 | 117 | if ( time > prevTime + 1000 ) { 118 | 119 | fps = Math.round( ( frames * 1000 ) / ( time - prevTime ) ); 120 | fpsMin = Math.min( fpsMin, fps ); 121 | fpsMax = Math.max( fpsMax, fps ); 122 | 123 | fpsText.textContent = fps + ' FPS (' + fpsMin + '-' + fpsMax + ')'; 124 | updateGraph( fpsGraph, Math.min( 30, 30 - ( fps / 100 ) * 30 ) ); 125 | 126 | prevTime = time; 127 | frames = 0; 128 | 129 | } 130 | 131 | return time; 132 | 133 | }, 134 | 135 | update: function () { 136 | 137 | startTime = this.end(); 138 | 139 | } 140 | 141 | } 142 | 143 | }; 144 | 145 | if ( typeof module === 'object' ) { 146 | 147 | module.exports = Stats; 148 | 149 | } -------------------------------------------------------------------------------- /app/js/lib/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | */ 5 | 6 | THREE.TrackballControls = function ( object, domElement ) { 7 | 8 | var _this = this; 9 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 10 | 11 | this.object = object; 12 | this.domElement = ( domElement !== undefined ) ? domElement : document; 13 | 14 | // API 15 | 16 | this.enabled = true; 17 | 18 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 19 | 20 | this.rotateSpeed = 1.0; 21 | this.zoomSpeed = 1.2; 22 | this.panSpeed = 0.3; 23 | 24 | this.noRotate = false; 25 | this.noZoom = false; 26 | this.noPan = false; 27 | this.noRoll = false; 28 | 29 | this.staticMoving = false; 30 | this.dynamicDampingFactor = 0.2; 31 | 32 | this.minDistance = 0; 33 | this.maxDistance = Infinity; 34 | 35 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 36 | 37 | // internals 38 | 39 | this.target = new THREE.Vector3(); 40 | 41 | var EPS = 0.000001; 42 | 43 | var lastPosition = new THREE.Vector3(); 44 | 45 | var _state = STATE.NONE, 46 | _prevState = STATE.NONE, 47 | 48 | _eye = new THREE.Vector3(), 49 | 50 | _rotateStart = new THREE.Vector3(), 51 | _rotateEnd = new THREE.Vector3(), 52 | 53 | _zoomStart = new THREE.Vector2(), 54 | _zoomEnd = new THREE.Vector2(), 55 | 56 | _touchZoomDistanceStart = 0, 57 | _touchZoomDistanceEnd = 0, 58 | 59 | _panStart = new THREE.Vector2(), 60 | _panEnd = new THREE.Vector2(); 61 | 62 | // for reset 63 | 64 | this.target0 = this.target.clone(); 65 | this.position0 = this.object.position.clone(); 66 | this.up0 = this.object.up.clone(); 67 | 68 | // events 69 | 70 | var changeEvent = { type: 'change' }; 71 | var startEvent = { type: 'start'}; 72 | var endEvent = { type: 'end'}; 73 | 74 | 75 | // methods 76 | 77 | this.handleResize = function () { 78 | 79 | if ( this.domElement === document ) { 80 | 81 | this.screen.left = 0; 82 | this.screen.top = 0; 83 | this.screen.width = window.innerWidth; 84 | this.screen.height = window.innerHeight; 85 | 86 | } else { 87 | 88 | var box = this.domElement.getBoundingClientRect(); 89 | // adjustments come from similar code in the jquery offset() function 90 | var d = this.domElement.ownerDocument.documentElement; 91 | this.screen.left = box.left + window.pageXOffset - d.clientLeft; 92 | this.screen.top = box.top + window.pageYOffset - d.clientTop; 93 | this.screen.width = box.width; 94 | this.screen.height = box.height; 95 | 96 | } 97 | 98 | }; 99 | 100 | this.handleEvent = function ( event ) { 101 | 102 | if ( typeof this[ event.type ] == 'function' ) { 103 | 104 | this[ event.type ]( event ); 105 | 106 | } 107 | 108 | }; 109 | 110 | var getMouseOnScreen = ( function () { 111 | 112 | var vector = new THREE.Vector2(); 113 | 114 | return function ( pageX, pageY ) { 115 | 116 | vector.set( 117 | ( pageX - _this.screen.left ) / _this.screen.width, 118 | ( pageY - _this.screen.top ) / _this.screen.height 119 | ); 120 | 121 | return vector; 122 | 123 | }; 124 | 125 | }() ); 126 | 127 | var getMouseProjectionOnBall = ( function () { 128 | 129 | var vector = new THREE.Vector3(); 130 | var objectUp = new THREE.Vector3(); 131 | var mouseOnBall = new THREE.Vector3(); 132 | 133 | return function ( pageX, pageY ) { 134 | 135 | mouseOnBall.set( 136 | ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / (_this.screen.width*.5), 137 | ( _this.screen.height * 0.5 + _this.screen.top - pageY ) / (_this.screen.height*.5), 138 | 0.0 139 | ); 140 | 141 | var length = mouseOnBall.length(); 142 | 143 | if ( _this.noRoll ) { 144 | 145 | if ( length < Math.SQRT1_2 ) { 146 | 147 | mouseOnBall.z = Math.sqrt( 1.0 - length*length ); 148 | 149 | } else { 150 | 151 | mouseOnBall.z = .5 / length; 152 | 153 | } 154 | 155 | } else if ( length > 1.0 ) { 156 | 157 | mouseOnBall.normalize(); 158 | 159 | } else { 160 | 161 | mouseOnBall.z = Math.sqrt( 1.0 - length * length ); 162 | 163 | } 164 | 165 | _eye.copy( _this.object.position ).sub( _this.target ); 166 | 167 | vector.copy( _this.object.up ).setLength( mouseOnBall.y ) 168 | vector.add( objectUp.copy( _this.object.up ).cross( _eye ).setLength( mouseOnBall.x ) ); 169 | vector.add( _eye.setLength( mouseOnBall.z ) ); 170 | 171 | return vector; 172 | 173 | }; 174 | 175 | }() ); 176 | 177 | this.rotateCamera = (function(){ 178 | 179 | var axis = new THREE.Vector3(), 180 | quaternion = new THREE.Quaternion(); 181 | 182 | 183 | return function () { 184 | 185 | var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() ); 186 | 187 | if ( angle ) { 188 | 189 | axis.crossVectors( _rotateStart, _rotateEnd ).normalize(); 190 | 191 | angle *= _this.rotateSpeed; 192 | 193 | quaternion.setFromAxisAngle( axis, -angle ); 194 | 195 | _eye.applyQuaternion( quaternion ); 196 | _this.object.up.applyQuaternion( quaternion ); 197 | 198 | _rotateEnd.applyQuaternion( quaternion ); 199 | 200 | if ( _this.staticMoving ) { 201 | 202 | _rotateStart.copy( _rotateEnd ); 203 | 204 | } else { 205 | 206 | quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) ); 207 | _rotateStart.applyQuaternion( quaternion ); 208 | 209 | } 210 | 211 | } 212 | } 213 | 214 | }()); 215 | 216 | this.zoomCamera = function () { 217 | 218 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 219 | 220 | var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 221 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 222 | _eye.multiplyScalar( factor ); 223 | 224 | } else { 225 | 226 | var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 227 | 228 | if ( factor !== 1.0 && factor > 0.0 ) { 229 | 230 | _eye.multiplyScalar( factor ); 231 | 232 | if ( _this.staticMoving ) { 233 | 234 | _zoomStart.copy( _zoomEnd ); 235 | 236 | } else { 237 | 238 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 239 | 240 | } 241 | 242 | } 243 | 244 | } 245 | 246 | }; 247 | 248 | this.panCamera = (function(){ 249 | 250 | var mouseChange = new THREE.Vector2(), 251 | objectUp = new THREE.Vector3(), 252 | pan = new THREE.Vector3(); 253 | 254 | return function () { 255 | 256 | mouseChange.copy( _panEnd ).sub( _panStart ); 257 | 258 | if ( mouseChange.lengthSq() ) { 259 | 260 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 261 | 262 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 263 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 264 | 265 | _this.object.position.add( pan ); 266 | _this.target.add( pan ); 267 | 268 | if ( _this.staticMoving ) { 269 | 270 | _panStart.copy( _panEnd ); 271 | 272 | } else { 273 | 274 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 275 | 276 | } 277 | 278 | } 279 | } 280 | 281 | }()); 282 | 283 | this.checkDistances = function () { 284 | 285 | if ( !_this.noZoom || !_this.noPan ) { 286 | 287 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 288 | 289 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 290 | 291 | } 292 | 293 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 294 | 295 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 296 | 297 | } 298 | 299 | } 300 | 301 | }; 302 | 303 | this.update = function () { 304 | 305 | _eye.subVectors( _this.object.position, _this.target ); 306 | 307 | if ( !_this.noRotate ) { 308 | 309 | _this.rotateCamera(); 310 | 311 | } 312 | 313 | if ( !_this.noZoom ) { 314 | 315 | _this.zoomCamera(); 316 | 317 | } 318 | 319 | if ( !_this.noPan ) { 320 | 321 | _this.panCamera(); 322 | 323 | } 324 | 325 | _this.object.position.addVectors( _this.target, _eye ); 326 | 327 | _this.checkDistances(); 328 | 329 | _this.object.lookAt( _this.target ); 330 | 331 | if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 332 | 333 | _this.dispatchEvent( changeEvent ); 334 | 335 | lastPosition.copy( _this.object.position ); 336 | 337 | } 338 | 339 | }; 340 | 341 | this.reset = function () { 342 | 343 | _state = STATE.NONE; 344 | _prevState = STATE.NONE; 345 | 346 | _this.target.copy( _this.target0 ); 347 | _this.object.position.copy( _this.position0 ); 348 | _this.object.up.copy( _this.up0 ); 349 | 350 | _eye.subVectors( _this.object.position, _this.target ); 351 | 352 | _this.object.lookAt( _this.target ); 353 | 354 | _this.dispatchEvent( changeEvent ); 355 | 356 | lastPosition.copy( _this.object.position ); 357 | 358 | }; 359 | 360 | // listeners 361 | 362 | function keydown( event ) { 363 | 364 | if ( _this.enabled === false ) return; 365 | 366 | window.removeEventListener( 'keydown', keydown ); 367 | 368 | _prevState = _state; 369 | 370 | if ( _state !== STATE.NONE ) { 371 | 372 | return; 373 | 374 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { 375 | 376 | _state = STATE.ROTATE; 377 | 378 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { 379 | 380 | _state = STATE.ZOOM; 381 | 382 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { 383 | 384 | _state = STATE.PAN; 385 | 386 | } 387 | 388 | } 389 | 390 | function keyup( event ) { 391 | 392 | if ( _this.enabled === false ) return; 393 | 394 | _state = _prevState; 395 | 396 | window.addEventListener( 'keydown', keydown, false ); 397 | 398 | } 399 | 400 | function mousedown( event ) { 401 | 402 | if ( _this.enabled === false ) return; 403 | 404 | event.preventDefault(); 405 | event.stopPropagation(); 406 | 407 | if ( _state === STATE.NONE ) { 408 | 409 | _state = event.button; 410 | 411 | } 412 | 413 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 414 | 415 | _rotateStart.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) ); 416 | _rotateEnd.copy( _rotateStart ); 417 | 418 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 419 | 420 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 421 | _zoomEnd.copy(_zoomStart); 422 | 423 | } else if ( _state === STATE.PAN && !_this.noPan ) { 424 | 425 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 426 | _panEnd.copy(_panStart) 427 | 428 | } 429 | 430 | document.addEventListener( 'mousemove', mousemove, false ); 431 | document.addEventListener( 'mouseup', mouseup, false ); 432 | 433 | _this.dispatchEvent( startEvent ); 434 | 435 | } 436 | 437 | function mousemove( event ) { 438 | 439 | if ( _this.enabled === false ) return; 440 | 441 | event.preventDefault(); 442 | event.stopPropagation(); 443 | 444 | if ( _state === STATE.ROTATE && !_this.noRotate ) { 445 | 446 | _rotateEnd.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) ); 447 | 448 | } else if ( _state === STATE.ZOOM && !_this.noZoom ) { 449 | 450 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 451 | 452 | } else if ( _state === STATE.PAN && !_this.noPan ) { 453 | 454 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 455 | 456 | } 457 | 458 | } 459 | 460 | function mouseup( event ) { 461 | 462 | if ( _this.enabled === false ) return; 463 | 464 | event.preventDefault(); 465 | event.stopPropagation(); 466 | 467 | _state = STATE.NONE; 468 | 469 | document.removeEventListener( 'mousemove', mousemove ); 470 | document.removeEventListener( 'mouseup', mouseup ); 471 | _this.dispatchEvent( endEvent ); 472 | 473 | } 474 | 475 | function mousewheel( event ) { 476 | 477 | if ( _this.enabled === false ) return; 478 | 479 | event.preventDefault(); 480 | event.stopPropagation(); 481 | 482 | var delta = 0; 483 | 484 | if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 485 | 486 | delta = event.wheelDelta / 40; 487 | 488 | } else if ( event.detail ) { // Firefox 489 | 490 | delta = - event.detail / 3; 491 | 492 | } 493 | 494 | _zoomStart.y += delta * 0.01; 495 | _this.dispatchEvent( startEvent ); 496 | _this.dispatchEvent( endEvent ); 497 | 498 | } 499 | 500 | function touchstart( event ) { 501 | 502 | if ( _this.enabled === false ) return; 503 | 504 | switch ( event.touches.length ) { 505 | 506 | case 1: 507 | _state = STATE.TOUCH_ROTATE; 508 | _rotateStart.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 509 | _rotateEnd.copy( _rotateStart ); 510 | break; 511 | 512 | case 2: 513 | _state = STATE.TOUCH_ZOOM_PAN; 514 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 515 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 516 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 517 | 518 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 519 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 520 | _panStart.copy( getMouseOnScreen( x, y ) ); 521 | _panEnd.copy( _panStart ); 522 | break; 523 | 524 | default: 525 | _state = STATE.NONE; 526 | 527 | } 528 | _this.dispatchEvent( startEvent ); 529 | 530 | 531 | } 532 | 533 | function touchmove( event ) { 534 | 535 | if ( _this.enabled === false ) return; 536 | 537 | event.preventDefault(); 538 | event.stopPropagation(); 539 | 540 | switch ( event.touches.length ) { 541 | 542 | case 1: 543 | _rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 544 | break; 545 | 546 | case 2: 547 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 548 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 549 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 550 | 551 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 552 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 553 | _panEnd.copy( getMouseOnScreen( x, y ) ); 554 | break; 555 | 556 | default: 557 | _state = STATE.NONE; 558 | 559 | } 560 | 561 | } 562 | 563 | function touchend( event ) { 564 | 565 | if ( _this.enabled === false ) return; 566 | 567 | switch ( event.touches.length ) { 568 | 569 | case 1: 570 | _rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 571 | _rotateStart.copy( _rotateEnd ); 572 | break; 573 | 574 | case 2: 575 | _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; 576 | 577 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 578 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 579 | _panEnd.copy( getMouseOnScreen( x, y ) ); 580 | _panStart.copy( _panEnd ); 581 | break; 582 | 583 | } 584 | 585 | _state = STATE.NONE; 586 | _this.dispatchEvent( endEvent ); 587 | 588 | } 589 | 590 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 591 | 592 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 593 | 594 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 595 | this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox 596 | 597 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 598 | this.domElement.addEventListener( 'touchend', touchend, false ); 599 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 600 | 601 | window.addEventListener( 'keydown', keydown, false ); 602 | window.addEventListener( 'keyup', keyup, false ); 603 | 604 | this.handleResize(); 605 | 606 | // force an update at start 607 | this.update(); 608 | 609 | }; 610 | 611 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 612 | THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; 613 | -------------------------------------------------------------------------------- /app/js/lib/mousetrap.js: -------------------------------------------------------------------------------- 1 | /*global define:false */ 2 | /** 3 | * Copyright 2013 Craig Campbell 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | * Mousetrap is a simple keyboard shortcut library for Javascript with 18 | * no external dependencies 19 | * 20 | * @version 1.4.6 21 | * @url craig.is/killing/mice 22 | */ 23 | (function(window, document, undefined) { 24 | 25 | /** 26 | * mapping of special keycodes to their corresponding keys 27 | * 28 | * everything in this dictionary cannot use keypress events 29 | * so it has to be here to map to the correct keycodes for 30 | * keyup/keydown events 31 | * 32 | * @type {Object} 33 | */ 34 | var _MAP = { 35 | 8: 'backspace', 36 | 9: 'tab', 37 | 13: 'enter', 38 | 16: 'shift', 39 | 17: 'ctrl', 40 | 18: 'alt', 41 | 20: 'capslock', 42 | 27: 'esc', 43 | 32: 'space', 44 | 33: 'pageup', 45 | 34: 'pagedown', 46 | 35: 'end', 47 | 36: 'home', 48 | 37: 'left', 49 | 38: 'up', 50 | 39: 'right', 51 | 40: 'down', 52 | 45: 'ins', 53 | 46: 'del', 54 | 91: 'meta', 55 | 93: 'meta', 56 | 224: 'meta' 57 | }, 58 | 59 | /** 60 | * mapping for special characters so they can support 61 | * 62 | * this dictionary is only used incase you want to bind a 63 | * keyup or keydown event to one of these keys 64 | * 65 | * @type {Object} 66 | */ 67 | _KEYCODE_MAP = { 68 | 106: '*', 69 | 107: '+', 70 | 109: '-', 71 | 110: '.', 72 | 111 : '/', 73 | 186: ';', 74 | 187: '=', 75 | 188: ',', 76 | 189: '-', 77 | 190: '.', 78 | 191: '/', 79 | 192: '`', 80 | 219: '[', 81 | 220: '\\', 82 | 221: ']', 83 | 222: '\'' 84 | }, 85 | 86 | /** 87 | * this is a mapping of keys that require shift on a US keypad 88 | * back to the non shift equivelents 89 | * 90 | * this is so you can use keyup events with these keys 91 | * 92 | * note that this will only work reliably on US keyboards 93 | * 94 | * @type {Object} 95 | */ 96 | _SHIFT_MAP = { 97 | '~': '`', 98 | '!': '1', 99 | '@': '2', 100 | '#': '3', 101 | '$': '4', 102 | '%': '5', 103 | '^': '6', 104 | '&': '7', 105 | '*': '8', 106 | '(': '9', 107 | ')': '0', 108 | '_': '-', 109 | '+': '=', 110 | ':': ';', 111 | '\"': '\'', 112 | '<': ',', 113 | '>': '.', 114 | '?': '/', 115 | '|': '\\' 116 | }, 117 | 118 | /** 119 | * this is a list of special strings you can use to map 120 | * to modifier keys when you specify your keyboard shortcuts 121 | * 122 | * @type {Object} 123 | */ 124 | _SPECIAL_ALIASES = { 125 | 'option': 'alt', 126 | 'command': 'meta', 127 | 'return': 'enter', 128 | 'escape': 'esc', 129 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 130 | }, 131 | 132 | /** 133 | * variable to store the flipped version of _MAP from above 134 | * needed to check if we should use keypress or not when no action 135 | * is specified 136 | * 137 | * @type {Object|undefined} 138 | */ 139 | _REVERSE_MAP, 140 | 141 | /** 142 | * a list of all the callbacks setup via Mousetrap.bind() 143 | * 144 | * @type {Object} 145 | */ 146 | _callbacks = {}, 147 | 148 | /** 149 | * direct map of string combinations to callbacks used for trigger() 150 | * 151 | * @type {Object} 152 | */ 153 | _directMap = {}, 154 | 155 | /** 156 | * keeps track of what level each sequence is at since multiple 157 | * sequences can start out with the same sequence 158 | * 159 | * @type {Object} 160 | */ 161 | _sequenceLevels = {}, 162 | 163 | /** 164 | * variable to store the setTimeout call 165 | * 166 | * @type {null|number} 167 | */ 168 | _resetTimer, 169 | 170 | /** 171 | * temporary state where we will ignore the next keyup 172 | * 173 | * @type {boolean|string} 174 | */ 175 | _ignoreNextKeyup = false, 176 | 177 | /** 178 | * temporary state where we will ignore the next keypress 179 | * 180 | * @type {boolean} 181 | */ 182 | _ignoreNextKeypress = false, 183 | 184 | /** 185 | * are we currently inside of a sequence? 186 | * type of action ("keyup" or "keydown" or "keypress") or false 187 | * 188 | * @type {boolean|string} 189 | */ 190 | _nextExpectedAction = false; 191 | 192 | /** 193 | * loop through the f keys, f1 to f19 and add them to the map 194 | * programatically 195 | */ 196 | for (var i = 1; i < 20; ++i) { 197 | _MAP[111 + i] = 'f' + i; 198 | } 199 | 200 | /** 201 | * loop through to map numbers on the numeric keypad 202 | */ 203 | for (i = 0; i <= 9; ++i) { 204 | _MAP[i + 96] = i; 205 | } 206 | 207 | /** 208 | * cross browser add event method 209 | * 210 | * @param {Element|HTMLDocument} object 211 | * @param {string} type 212 | * @param {Function} callback 213 | * @returns void 214 | */ 215 | function _addEvent(object, type, callback) { 216 | if (object.addEventListener) { 217 | object.addEventListener(type, callback, false); 218 | return; 219 | } 220 | 221 | object.attachEvent('on' + type, callback); 222 | } 223 | 224 | /** 225 | * takes the event and returns the key character 226 | * 227 | * @param {Event} e 228 | * @return {string} 229 | */ 230 | function _characterFromEvent(e) { 231 | 232 | // for keypress events we should return the character as is 233 | if (e.type == 'keypress') { 234 | var character = String.fromCharCode(e.which); 235 | 236 | // if the shift key is not pressed then it is safe to assume 237 | // that we want the character to be lowercase. this means if 238 | // you accidentally have caps lock on then your key bindings 239 | // will continue to work 240 | // 241 | // the only side effect that might not be desired is if you 242 | // bind something like 'A' cause you want to trigger an 243 | // event when capital A is pressed caps lock will no longer 244 | // trigger the event. shift+a will though. 245 | if (!e.shiftKey) { 246 | character = character.toLowerCase(); 247 | } 248 | 249 | return character; 250 | } 251 | 252 | // for non keypress events the special maps are needed 253 | if (_MAP[e.which]) { 254 | return _MAP[e.which]; 255 | } 256 | 257 | if (_KEYCODE_MAP[e.which]) { 258 | return _KEYCODE_MAP[e.which]; 259 | } 260 | 261 | // if it is not in the special map 262 | 263 | // with keydown and keyup events the character seems to always 264 | // come in as an uppercase character whether you are pressing shift 265 | // or not. we should make sure it is always lowercase for comparisons 266 | return String.fromCharCode(e.which).toLowerCase(); 267 | } 268 | 269 | /** 270 | * checks if two arrays are equal 271 | * 272 | * @param {Array} modifiers1 273 | * @param {Array} modifiers2 274 | * @returns {boolean} 275 | */ 276 | function _modifiersMatch(modifiers1, modifiers2) { 277 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 278 | } 279 | 280 | /** 281 | * resets all sequence counters except for the ones passed in 282 | * 283 | * @param {Object} doNotReset 284 | * @returns void 285 | */ 286 | function _resetSequences(doNotReset) { 287 | doNotReset = doNotReset || {}; 288 | 289 | var activeSequences = false, 290 | key; 291 | 292 | for (key in _sequenceLevels) { 293 | if (doNotReset[key]) { 294 | activeSequences = true; 295 | continue; 296 | } 297 | _sequenceLevels[key] = 0; 298 | } 299 | 300 | if (!activeSequences) { 301 | _nextExpectedAction = false; 302 | } 303 | } 304 | 305 | /** 306 | * finds all callbacks that match based on the keycode, modifiers, 307 | * and action 308 | * 309 | * @param {string} character 310 | * @param {Array} modifiers 311 | * @param {Event|Object} e 312 | * @param {string=} sequenceName - name of the sequence we are looking for 313 | * @param {string=} combination 314 | * @param {number=} level 315 | * @returns {Array} 316 | */ 317 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { 318 | var i, 319 | callback, 320 | matches = [], 321 | action = e.type; 322 | 323 | // if there are no events related to this keycode 324 | if (!_callbacks[character]) { 325 | return []; 326 | } 327 | 328 | // if a modifier key is coming up on its own we should allow it 329 | if (action == 'keyup' && _isModifier(character)) { 330 | modifiers = [character]; 331 | } 332 | 333 | // loop through all callbacks for the key that was pressed 334 | // and see if any of them match 335 | for (i = 0; i < _callbacks[character].length; ++i) { 336 | callback = _callbacks[character][i]; 337 | 338 | // if a sequence name is not specified, but this is a sequence at 339 | // the wrong level then move onto the next match 340 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { 341 | continue; 342 | } 343 | 344 | // if the action we are looking for doesn't match the action we got 345 | // then we should keep going 346 | if (action != callback.action) { 347 | continue; 348 | } 349 | 350 | // if this is a keypress event and the meta key and control key 351 | // are not pressed that means that we need to only look at the 352 | // character, otherwise check the modifiers as well 353 | // 354 | // chrome will not fire a keypress if meta or control is down 355 | // safari will fire a keypress if meta or meta+shift is down 356 | // firefox will fire a keypress if meta or control is down 357 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 358 | 359 | // when you bind a combination or sequence a second time it 360 | // should overwrite the first one. if a sequenceName or 361 | // combination is specified in this call it does just that 362 | // 363 | // @todo make deleting its own method? 364 | var deleteCombo = !sequenceName && callback.combo == combination; 365 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; 366 | if (deleteCombo || deleteSequence) { 367 | _callbacks[character].splice(i, 1); 368 | } 369 | 370 | matches.push(callback); 371 | } 372 | } 373 | 374 | return matches; 375 | } 376 | 377 | /** 378 | * takes a key event and figures out what the modifiers are 379 | * 380 | * @param {Event} e 381 | * @returns {Array} 382 | */ 383 | function _eventModifiers(e) { 384 | var modifiers = []; 385 | 386 | if (e.shiftKey) { 387 | modifiers.push('shift'); 388 | } 389 | 390 | if (e.altKey) { 391 | modifiers.push('alt'); 392 | } 393 | 394 | if (e.ctrlKey) { 395 | modifiers.push('ctrl'); 396 | } 397 | 398 | if (e.metaKey) { 399 | modifiers.push('meta'); 400 | } 401 | 402 | return modifiers; 403 | } 404 | 405 | /** 406 | * prevents default for this event 407 | * 408 | * @param {Event} e 409 | * @returns void 410 | */ 411 | function _preventDefault(e) { 412 | if (e.preventDefault) { 413 | e.preventDefault(); 414 | return; 415 | } 416 | 417 | e.returnValue = false; 418 | } 419 | 420 | /** 421 | * stops propogation for this event 422 | * 423 | * @param {Event} e 424 | * @returns void 425 | */ 426 | function _stopPropagation(e) { 427 | if (e.stopPropagation) { 428 | e.stopPropagation(); 429 | return; 430 | } 431 | 432 | e.cancelBubble = true; 433 | } 434 | 435 | /** 436 | * actually calls the callback function 437 | * 438 | * if your callback function returns false this will use the jquery 439 | * convention - prevent default and stop propogation on the event 440 | * 441 | * @param {Function} callback 442 | * @param {Event} e 443 | * @returns void 444 | */ 445 | function _fireCallback(callback, e, combo, sequence) { 446 | 447 | // if this event should not happen stop here 448 | if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo, sequence)) { 449 | return; 450 | } 451 | 452 | if (callback(e, combo) === false) { 453 | _preventDefault(e); 454 | _stopPropagation(e); 455 | } 456 | } 457 | 458 | /** 459 | * handles a character key event 460 | * 461 | * @param {string} character 462 | * @param {Array} modifiers 463 | * @param {Event} e 464 | * @returns void 465 | */ 466 | function _handleKey(character, modifiers, e) { 467 | var callbacks = _getMatches(character, modifiers, e), 468 | i, 469 | doNotReset = {}, 470 | maxLevel = 0, 471 | processedSequenceCallback = false; 472 | 473 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 474 | for (i = 0; i < callbacks.length; ++i) { 475 | if (callbacks[i].seq) { 476 | maxLevel = Math.max(maxLevel, callbacks[i].level); 477 | } 478 | } 479 | 480 | // loop through matching callbacks for this key event 481 | for (i = 0; i < callbacks.length; ++i) { 482 | 483 | // fire for all sequence callbacks 484 | // this is because if for example you have multiple sequences 485 | // bound such as "g i" and "g t" they both need to fire the 486 | // callback for matching g cause otherwise you can only ever 487 | // match the first one 488 | if (callbacks[i].seq) { 489 | 490 | // only fire callbacks for the maxLevel to prevent 491 | // subsequences from also firing 492 | // 493 | // for example 'a option b' should not cause 'option b' to fire 494 | // even though 'option b' is part of the other sequence 495 | // 496 | // any sequences that do not match here will be discarded 497 | // below by the _resetSequences call 498 | if (callbacks[i].level != maxLevel) { 499 | continue; 500 | } 501 | 502 | processedSequenceCallback = true; 503 | 504 | // keep a list of which sequences were matches for later 505 | doNotReset[callbacks[i].seq] = 1; 506 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq); 507 | continue; 508 | } 509 | 510 | // if there were no sequence matches but we are still here 511 | // that means this is a regular match so we should fire that 512 | if (!processedSequenceCallback) { 513 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 514 | } 515 | } 516 | 517 | // if the key you pressed matches the type of sequence without 518 | // being a modifier (ie "keyup" or "keypress") then we should 519 | // reset all sequences that were not matched by this event 520 | // 521 | // this is so, for example, if you have the sequence "h a t" and you 522 | // type "h e a r t" it does not match. in this case the "e" will 523 | // cause the sequence to reset 524 | // 525 | // modifier keys are ignored because you can have a sequence 526 | // that contains modifiers such as "enter ctrl+space" and in most 527 | // cases the modifier key will be pressed before the next key 528 | // 529 | // also if you have a sequence such as "ctrl+b a" then pressing the 530 | // "b" key will trigger a "keypress" and a "keydown" 531 | // 532 | // the "keydown" is expected when there is a modifier, but the 533 | // "keypress" ends up matching the _nextExpectedAction since it occurs 534 | // after and that causes the sequence to reset 535 | // 536 | // we ignore keypresses in a sequence that directly follow a keydown 537 | // for the same character 538 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; 539 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { 540 | _resetSequences(doNotReset); 541 | } 542 | 543 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; 544 | } 545 | 546 | /** 547 | * handles a keydown event 548 | * 549 | * @param {Event} e 550 | * @returns void 551 | */ 552 | function _handleKeyEvent(e) { 553 | 554 | // normalize e.which for key events 555 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 556 | if (typeof e.which !== 'number') { 557 | e.which = e.keyCode; 558 | } 559 | 560 | var character = _characterFromEvent(e); 561 | 562 | // no character found then stop 563 | if (!character) { 564 | return; 565 | } 566 | 567 | // need to use === for the character check because the character can be 0 568 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { 569 | _ignoreNextKeyup = false; 570 | return; 571 | } 572 | 573 | Mousetrap.handleKey(character, _eventModifiers(e), e); 574 | } 575 | 576 | /** 577 | * determines if the keycode specified is a modifier key or not 578 | * 579 | * @param {string} key 580 | * @returns {boolean} 581 | */ 582 | function _isModifier(key) { 583 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 584 | } 585 | 586 | /** 587 | * called to set a 1 second timeout on the specified sequence 588 | * 589 | * this is so after each key press in the sequence you have 1 second 590 | * to press the next key before you have to start over 591 | * 592 | * @returns void 593 | */ 594 | function _resetSequenceTimer() { 595 | clearTimeout(_resetTimer); 596 | _resetTimer = setTimeout(_resetSequences, 1000); 597 | } 598 | 599 | /** 600 | * reverses the map lookup so that we can look for specific keys 601 | * to see what can and can't use keypress 602 | * 603 | * @return {Object} 604 | */ 605 | function _getReverseMap() { 606 | if (!_REVERSE_MAP) { 607 | _REVERSE_MAP = {}; 608 | for (var key in _MAP) { 609 | 610 | // pull out the numeric keypad from here cause keypress should 611 | // be able to detect the keys from the character 612 | if (key > 95 && key < 112) { 613 | continue; 614 | } 615 | 616 | if (_MAP.hasOwnProperty(key)) { 617 | _REVERSE_MAP[_MAP[key]] = key; 618 | } 619 | } 620 | } 621 | return _REVERSE_MAP; 622 | } 623 | 624 | /** 625 | * picks the best action based on the key combination 626 | * 627 | * @param {string} key - character for key 628 | * @param {Array} modifiers 629 | * @param {string=} action passed in 630 | */ 631 | function _pickBestAction(key, modifiers, action) { 632 | 633 | // if no action was picked in we should try to pick the one 634 | // that we think would work best for this key 635 | if (!action) { 636 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 637 | } 638 | 639 | // modifier keys don't work as expected with keypress, 640 | // switch to keydown 641 | if (action == 'keypress' && modifiers.length) { 642 | action = 'keydown'; 643 | } 644 | 645 | return action; 646 | } 647 | 648 | /** 649 | * binds a key sequence to an event 650 | * 651 | * @param {string} combo - combo specified in bind call 652 | * @param {Array} keys 653 | * @param {Function} callback 654 | * @param {string=} action 655 | * @returns void 656 | */ 657 | function _bindSequence(combo, keys, callback, action) { 658 | 659 | // start off by adding a sequence level record for this combination 660 | // and setting the level to 0 661 | _sequenceLevels[combo] = 0; 662 | 663 | /** 664 | * callback to increase the sequence level for this sequence and reset 665 | * all other sequences that were active 666 | * 667 | * @param {string} nextAction 668 | * @returns {Function} 669 | */ 670 | function _increaseSequence(nextAction) { 671 | return function() { 672 | _nextExpectedAction = nextAction; 673 | ++_sequenceLevels[combo]; 674 | _resetSequenceTimer(); 675 | }; 676 | } 677 | 678 | /** 679 | * wraps the specified callback inside of another function in order 680 | * to reset all sequence counters as soon as this sequence is done 681 | * 682 | * @param {Event} e 683 | * @returns void 684 | */ 685 | function _callbackAndReset(e) { 686 | _fireCallback(callback, e, combo); 687 | 688 | // we should ignore the next key up if the action is key down 689 | // or keypress. this is so if you finish a sequence and 690 | // release the key the final key will not trigger a keyup 691 | if (action !== 'keyup') { 692 | _ignoreNextKeyup = _characterFromEvent(e); 693 | } 694 | 695 | // weird race condition if a sequence ends with the key 696 | // another sequence begins with 697 | setTimeout(_resetSequences, 10); 698 | } 699 | 700 | // loop through keys one at a time and bind the appropriate callback 701 | // function. for any key leading up to the final one it should 702 | // increase the sequence. after the final, it should reset all sequences 703 | // 704 | // if an action is specified in the original bind call then that will 705 | // be used throughout. otherwise we will pass the action that the 706 | // next key in the sequence should match. this allows a sequence 707 | // to mix and match keypress and keydown events depending on which 708 | // ones are better suited to the key provided 709 | for (var i = 0; i < keys.length; ++i) { 710 | var isFinal = i + 1 === keys.length; 711 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); 712 | _bindSingle(keys[i], wrappedCallback, action, combo, i); 713 | } 714 | } 715 | 716 | /** 717 | * Converts from a string key combination to an array 718 | * 719 | * @param {string} combination like "command+shift+l" 720 | * @return {Array} 721 | */ 722 | function _keysFromString(combination) { 723 | if (combination === '+') { 724 | return ['+']; 725 | } 726 | 727 | return combination.split('+'); 728 | } 729 | 730 | /** 731 | * Gets info for a specific key combination 732 | * 733 | * @param {string} combination key combination ("command+s" or "a" or "*") 734 | * @param {string=} action 735 | * @returns {Object} 736 | */ 737 | function _getKeyInfo(combination, action) { 738 | var keys, 739 | key, 740 | i, 741 | modifiers = []; 742 | 743 | // take the keys from this pattern and figure out what the actual 744 | // pattern is all about 745 | keys = _keysFromString(combination); 746 | 747 | for (i = 0; i < keys.length; ++i) { 748 | key = keys[i]; 749 | 750 | // normalize key names 751 | if (_SPECIAL_ALIASES[key]) { 752 | key = _SPECIAL_ALIASES[key]; 753 | } 754 | 755 | // if this is not a keypress event then we should 756 | // be smart about using shift keys 757 | // this will only work for US keyboards however 758 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 759 | key = _SHIFT_MAP[key]; 760 | modifiers.push('shift'); 761 | } 762 | 763 | // if this key is a modifier then add it to the list of modifiers 764 | if (_isModifier(key)) { 765 | modifiers.push(key); 766 | } 767 | } 768 | 769 | // depending on what the key combination is 770 | // we will try to pick the best event for it 771 | action = _pickBestAction(key, modifiers, action); 772 | 773 | return { 774 | key: key, 775 | modifiers: modifiers, 776 | action: action 777 | }; 778 | } 779 | 780 | /** 781 | * binds a single keyboard combination 782 | * 783 | * @param {string} combination 784 | * @param {Function} callback 785 | * @param {string=} action 786 | * @param {string=} sequenceName - name of sequence if part of sequence 787 | * @param {number=} level - what part of the sequence the command is 788 | * @returns void 789 | */ 790 | function _bindSingle(combination, callback, action, sequenceName, level) { 791 | 792 | // store a direct mapped reference for use with Mousetrap.trigger 793 | _directMap[combination + ':' + action] = callback; 794 | 795 | // make sure multiple spaces in a row become a single space 796 | combination = combination.replace(/\s+/g, ' '); 797 | 798 | var sequence = combination.split(' '), 799 | info; 800 | 801 | // if this pattern is a sequence of keys then run through this method 802 | // to reprocess each pattern one key at a time 803 | if (sequence.length > 1) { 804 | _bindSequence(combination, sequence, callback, action); 805 | return; 806 | } 807 | 808 | info = _getKeyInfo(combination, action); 809 | 810 | // make sure to initialize array if this is the first time 811 | // a callback is added for this key 812 | _callbacks[info.key] = _callbacks[info.key] || []; 813 | 814 | // remove an existing match if there is one 815 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); 816 | 817 | // add this call back to the array 818 | // if it is a sequence put it at the beginning 819 | // if not put it at the end 820 | // 821 | // this is important because the way these are processed expects 822 | // the sequence ones to come first 823 | _callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 824 | callback: callback, 825 | modifiers: info.modifiers, 826 | action: info.action, 827 | seq: sequenceName, 828 | level: level, 829 | combo: combination 830 | }); 831 | } 832 | 833 | /** 834 | * binds multiple combinations to the same callback 835 | * 836 | * @param {Array} combinations 837 | * @param {Function} callback 838 | * @param {string|undefined} action 839 | * @returns void 840 | */ 841 | function _bindMultiple(combinations, callback, action) { 842 | for (var i = 0; i < combinations.length; ++i) { 843 | _bindSingle(combinations[i], callback, action); 844 | } 845 | } 846 | 847 | // start! 848 | _addEvent(document, 'keypress', _handleKeyEvent); 849 | _addEvent(document, 'keydown', _handleKeyEvent); 850 | _addEvent(document, 'keyup', _handleKeyEvent); 851 | 852 | var Mousetrap = { 853 | 854 | /** 855 | * binds an event to mousetrap 856 | * 857 | * can be a single key, a combination of keys separated with +, 858 | * an array of keys, or a sequence of keys separated by spaces 859 | * 860 | * be sure to list the modifier keys first to make sure that the 861 | * correct key ends up getting bound (the last key in the pattern) 862 | * 863 | * @param {string|Array} keys 864 | * @param {Function} callback 865 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 866 | * @returns void 867 | */ 868 | bind: function(keys, callback, action) { 869 | keys = keys instanceof Array ? keys : [keys]; 870 | _bindMultiple(keys, callback, action); 871 | return this; 872 | }, 873 | 874 | /** 875 | * unbinds an event to mousetrap 876 | * 877 | * the unbinding sets the callback function of the specified key combo 878 | * to an empty function and deletes the corresponding key in the 879 | * _directMap dict. 880 | * 881 | * TODO: actually remove this from the _callbacks dictionary instead 882 | * of binding an empty function 883 | * 884 | * the keycombo+action has to be exactly the same as 885 | * it was defined in the bind method 886 | * 887 | * @param {string|Array} keys 888 | * @param {string} action 889 | * @returns void 890 | */ 891 | unbind: function(keys, action) { 892 | return Mousetrap.bind(keys, function() {}, action); 893 | }, 894 | 895 | /** 896 | * triggers an event that has already been bound 897 | * 898 | * @param {string} keys 899 | * @param {string=} action 900 | * @returns void 901 | */ 902 | trigger: function(keys, action) { 903 | if (_directMap[keys + ':' + action]) { 904 | _directMap[keys + ':' + action]({}, keys); 905 | } 906 | return this; 907 | }, 908 | 909 | /** 910 | * resets the library back to its initial state. this is useful 911 | * if you want to clear out the current keyboard shortcuts and bind 912 | * new ones - for example if you switch to another page 913 | * 914 | * @returns void 915 | */ 916 | reset: function() { 917 | _callbacks = {}; 918 | _directMap = {}; 919 | return this; 920 | }, 921 | 922 | /** 923 | * should we stop this event before firing off callbacks 924 | * 925 | * @param {Event} e 926 | * @param {Element} element 927 | * @return {boolean} 928 | */ 929 | stopCallback: function(e, element) { 930 | 931 | // if the element has the class "mousetrap" then no need to stop 932 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 933 | return false; 934 | } 935 | 936 | // stop for input, select, and textarea 937 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; 938 | }, 939 | 940 | /** 941 | * exposes _handleKey publicly so it can be overwritten by extensions 942 | */ 943 | handleKey: _handleKey 944 | }; 945 | 946 | // expose mousetrap to the global object 947 | window.Mousetrap = Mousetrap; 948 | 949 | // expose mousetrap as an AMD module 950 | if (typeof define === 'function' && define.amd) { 951 | define(Mousetrap); 952 | } 953 | }) (window, document); 954 | -------------------------------------------------------------------------------- /app/js/lib/mousetrap.min.js: -------------------------------------------------------------------------------- 1 | /* mousetrap v1.4.6 craig.is/killing/mice */ 2 | (function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;gg||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;cc.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:12,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()}}};"object"===typeof module&&(module.exports=Stats); 7 | -------------------------------------------------------------------------------- /app/js/src/App.js: -------------------------------------------------------------------------------- 1 | var App = function() { 2 | 3 | var _params, _gui, _guiFields, _engine; 4 | var _customUpdate; 5 | 6 | var _currShape; // to display in GUI on init 7 | 8 | 9 | // ENGINE PRESETS 10 | 11 | var BasicPreset = function() { 12 | this.size = Utils.isMobile ? 128 : 512; 13 | var _drawMat = this.drawMat = createShaderMaterial(BasicParticleShader); 14 | var _color = new THREE.Color(1.0, 0.5, 0.3); 15 | var _alpha = 0.3; 16 | this.update = function(dt, t) { 17 | _color.offsetHSL(0.1*dt, 0.0, 0.0); 18 | _drawMat.uniforms.uColor.value.set(_color.r, _color.g, _color.b, _alpha); 19 | }; 20 | }; 21 | 22 | var Preset = function() { 23 | this.size = 512; 24 | this.simMat = createShaderMaterial(SimShader); 25 | var _drawMat = this.drawMat = createShaderMaterial(ParticleShader); 26 | this.update = function(dt, t) { 27 | _drawMat.uniforms.uTime.value = t; // for ParticleShader.vs 28 | if (_customUpdate) _customUpdate(dt, t); 29 | }; 30 | }; 31 | 32 | 33 | // SHAPE PRESETS 34 | 35 | var _DEFAULT_SHAPE = "galaxy"; 36 | 37 | var _presetShapes = { 38 | "none": "SIM_NO_SHAPE", 39 | "plane": "SIM_PLANE", 40 | "cube": "SIM_CUBE", 41 | "disc": "SIM_DISC", 42 | "sphere": "SIM_SPHERE", 43 | "ball": "SIM_BALL", 44 | "petals": "SIM_ROSE_GALAXY", 45 | "galaxy": "SIM_GALAXY", 46 | "noise": "SIM_NOISE", 47 | "object": "SIM_TEXTURE", 48 | }; 49 | 50 | var _switchShape = function(name) { 51 | if (!_presetShapes[name]) return; 52 | 53 | Object.keys(_presetShapes).forEach(function(k) { 54 | delete _params.simMat.defines[_presetShapes[k]]; 55 | }); 56 | _params.simMat.defines[_presetShapes[name]] = ""; 57 | _params.simMat.needsUpdate = true; 58 | 59 | _currShape = name; 60 | }; 61 | 62 | 63 | // FUNCTIONS 64 | 65 | var _takeScreenshot = function() { 66 | _engine.renderer.getImageData(function(dataUrl) { 67 | var url = Utils.dataUrlToBlobUrl(dataUrl); 68 | Utils.openUrlInNewWindow(url, window.innerWidth, window.innerHeight); 69 | }); 70 | }; 71 | 72 | 73 | // INIT FUNCTIONS 74 | 75 | var _init = function() { 76 | // choose preset 77 | var preset = Utils.isMobile ? BasicPreset : Preset; 78 | _params = new preset(); 79 | 80 | // routing, configure params 81 | var routeName = window.location.hash.length > 0 ? 82 | window.location.hash.substr(1) : ""; 83 | console.log("route: " + routeName); 84 | 85 | if (!_presetShapes[routeName]) { 86 | window.location.hash = ""; // fix address bar 87 | routeName = _DEFAULT_SHAPE; // default shape 88 | } 89 | 90 | if (!Utils.isMobile) _switchShape(routeName); 91 | 92 | // init engine, with params 93 | _engine = new ParticleEngine(_params); 94 | }; 95 | 96 | var _initUI = function() { 97 | _gui = new dat.GUI(); 98 | _guiFields = { 99 | "color1": [_params.drawMat.uniforms.uColor1.value.x*255, _params.drawMat.uniforms.uColor1.value.y*255, _params.drawMat.uniforms.uColor1.value.z*255], 100 | "color2": [_params.drawMat.uniforms.uColor2.value.x*255, _params.drawMat.uniforms.uColor2.value.y*255, _params.drawMat.uniforms.uColor2.value.z*255], 101 | "alpha": _params.drawMat.uniforms.uAlpha.value, 102 | "color speed": _params.drawMat.uniforms.uColorSpeed.value, 103 | "color freq": _params.drawMat.uniforms.uColorFreq.value, 104 | "point size": _params.drawMat.uniforms.uPointSize.value, 105 | "user gravity": _params.simMat.uniforms.uInputAccel.value, 106 | "shape gravity": _params.simMat.uniforms.uShapeAccel.value, 107 | "shape": _currShape, 108 | "paused": false, 109 | "camera rotate": true, 110 | "camera control": false, 111 | "screenshot": _takeScreenshot, 112 | }; 113 | 114 | _gui.addColor(_guiFields, "color1").onChange(function(value) { 115 | if (value[0] === "#") value = Utils.hexToRgb(value); 116 | _params.drawMat.uniforms.uColor1.value.x = value[0] / 255.0; 117 | _params.drawMat.uniforms.uColor1.value.y = value[1] / 255.0; 118 | _params.drawMat.uniforms.uColor1.value.z = value[2] / 255.0; 119 | }); 120 | _gui.addColor(_guiFields, "color2").onChange(function(value) { 121 | if (value[0] === "#") value = Utils.hexToRgb(value); 122 | _params.drawMat.uniforms.uColor2.value.x = value[0] / 255.0; 123 | _params.drawMat.uniforms.uColor2.value.y = value[1] / 255.0; 124 | _params.drawMat.uniforms.uColor2.value.z = value[2] / 255.0; 125 | }); 126 | _gui.add(_guiFields, "alpha", 0, 1).onChange(function(value) { 127 | _params.drawMat.uniforms.uAlpha.value = value; 128 | }); 129 | _gui.add(_guiFields, "color speed", -10, 10).onChange(function(value) { 130 | _params.drawMat.uniforms.uColorSpeed.value = value; 131 | }); 132 | _gui.add(_guiFields, "color freq", 0, 5).onChange(function(value) { 133 | _params.drawMat.uniforms.uColorFreq.value = value; 134 | }); 135 | _gui.add(_guiFields, "point size", 1, 10).onChange(function(value) { 136 | _params.drawMat.uniforms.uPointSize.value = value; 137 | }); 138 | _gui.add(_guiFields, "user gravity", 0, 10).onChange(function(value) { 139 | _params.simMat.uniforms.uInputAccel.value = value; 140 | }); 141 | _gui.add(_guiFields, "shape gravity", 0, 10).onChange(function(value) { 142 | _params.simMat.uniforms.uShapeAccel.value = value; 143 | }); 144 | _gui.add(_guiFields, "shape", Object.keys(_presetShapes)) 145 | .onFinishChange(_switchShape); 146 | _gui.add(_guiFields, "paused").onChange(function(value) { 147 | _engine.pauseSimulation(value); 148 | }).listen(); 149 | _gui.add(_guiFields, "camera rotate").onChange(function(value) { 150 | _engine.enableCameraAutoRotate(value); 151 | }); 152 | _gui.add(_guiFields, "camera control").onChange(function(value) { 153 | _engine.enableCameraControl(value); 154 | }).listen(); 155 | _gui.add(_guiFields, "screenshot"); 156 | }; 157 | 158 | var _initKeyboard = function() { 159 | 160 | // pause simulation 161 | Mousetrap.bind("space", function() { 162 | _guiFields.paused = !_guiFields.paused; 163 | _engine.pauseSimulation(_guiFields.paused); 164 | return false; 165 | }); 166 | 167 | // mouse camera control 168 | Mousetrap.bind(["alt", "option"], function() { 169 | _guiFields["camera control"] = true; 170 | _engine.enableCameraControl(true); 171 | return false; 172 | }, "keydown"); 173 | Mousetrap.bind(["alt", "option"], function() { 174 | _guiFields["camera control"] = false; 175 | _engine.enableCameraControl(false); 176 | return false; 177 | }, "keyup"); 178 | 179 | }; 180 | 181 | 182 | // RUN PROGRAM 183 | 184 | _init(); 185 | 186 | if (!Utils.isMobile) { 187 | _initUI(); 188 | _initKeyboard(); 189 | 190 | // TODO_NOP TEST 191 | 192 | var uvAnim = new UVMapAnimator(_engine.renderer.getRenderer(), _params.size); 193 | _params.simMat.uniforms.tTarget = { type: "t", value: uvAnim.target }; 194 | _customUpdate = uvAnim.update; 195 | 196 | var url = "models/wolf.json"; 197 | var loader = new THREE.JSONLoader(true); 198 | loader.load(url, function(geometry) { 199 | var mesh = new THREE.MorphAnimMesh(geometry); 200 | var scale = 0.04; 201 | mesh.scale.set(scale,scale,scale); 202 | mesh.position.y -= 1; 203 | mesh.duration = 1000 / 0.1; 204 | uvAnim.setMesh(mesh); 205 | }); 206 | } 207 | 208 | _engine.start(); 209 | }; -------------------------------------------------------------------------------- /app/js/src/App2.js: -------------------------------------------------------------------------------- 1 | var App = function() { 2 | var _gui, _guiFields; 3 | var _engine; 4 | var _currPreset = Utils.getParameterByName("shape") || "galaxy"; // initial preset 5 | var _currSimMode; 6 | var _uvAnim; 7 | var _tourMode = false; 8 | var _musicElem = document.getElementById("music"); 9 | 10 | // DEFINES 11 | 12 | var _params = { 13 | size: 512, 14 | simMat: createShaderMaterial(SimShader), 15 | drawMat: createShaderMaterial(ParticleShader), 16 | update: undefined, // defined later in the file 17 | }; 18 | 19 | var _simModes = [ 20 | "SIM_PLANE", 21 | "SIM_CUBE", 22 | "SIM_DISC", 23 | "SIM_SPHERE", 24 | "SIM_BALL", 25 | "SIM_ROSE_GALAXY", 26 | "SIM_GALAXY", 27 | "SIM_NOISE", 28 | "SIM_TEXTURE" 29 | ]; 30 | 31 | // must have same name as preset, for async loading to work properly 32 | var _meshes = { 33 | bear: { scale:0.023, yOffset:-2.30, speed:0.05, url:"models/bear.json" }, 34 | bison: { scale:0.020, yOffset:-2.00, speed:0.10, url:"models/bison.json" }, 35 | // deer: { scale:0.040, yOffset:-2.00, speed:0.10, url:"models/deer.json" }, 36 | // dog: { scale:0.040, yOffset:-1.65, speed:0.10, url:"models/retriever.json" }, 37 | // fox: { scale:0.070, yOffset:-1.50, speed:0.10, url:"models/fox.json" }, 38 | horse: { scale:0.022, yOffset:-2.30, speed:0.08, url:"models/horse.json" }, 39 | panther: { scale:0.030, yOffset:-1.70, speed:0.10, url:"models/panther.json" }, 40 | // rabbit: { scale:0.040, yOffset:-1.00, speed:0.05, url:"models/rabbit.json" }, 41 | wolf: { scale:0.040, yOffset:-1.70, speed:0.10, url:"models/wolf.json" }, 42 | }; 43 | 44 | var _presets = { 45 | "none": { "user gravity":3, "shape gravity":1, _shape:"" }, 46 | // "noise": { "user gravity":3, "shape gravity":1, _shape:"SIM_NOISE" }, 47 | "plane": { "user gravity":4, "shape gravity":3, _shape:"SIM_PLANE" }, 48 | "sphere": { "user gravity":4, "shape gravity":3, _shape:"SIM_SPHERE" }, 49 | "galaxy": { "user gravity":3, "shape gravity":1, _shape:"SIM_GALAXY" }, 50 | "petals": { "user gravity":3, "shape gravity":2, _shape:"SIM_ROSE_GALAXY" }, 51 | "bear": { "user gravity":3, "shape gravity":5, _shape:_meshes.bear }, 52 | "bison": { "user gravity":3, "shape gravity":5, _shape:_meshes.bison }, 53 | // "deer": { "user gravity":3, "shape gravity":5, _shape:_meshes.deer }, 54 | // "dog": { "user gravity":3, "shape gravity":5, _shape:_meshes.dog }, 55 | // "fox": { "user gravity":3, "shape gravity":5, _shape:_meshes.fox }, 56 | "horse": { "user gravity":3, "shape gravity":5, _shape:_meshes.horse }, 57 | "panther": { "user gravity":3, "shape gravity":5, _shape:_meshes.panther }, 58 | // "rabbit": { "user gravity":3, "shape gravity":5, _shape:_meshes.rabbit }, 59 | "wolf": { "user gravity":3, "shape gravity":5, _shape:_meshes.wolf }, 60 | }; 61 | 62 | 63 | 64 | // FUNCTIONS 65 | 66 | var _setSimMode = function(name) { 67 | if (name === _currSimMode) 68 | return; 69 | _currSimMode = name; // cache mode, prevent shader recompile 70 | 71 | _simModes.forEach(function(s) { 72 | delete _params.simMat.defines[s]; 73 | }); 74 | if (name) 75 | _params.simMat.defines[name] = ""; 76 | _params.simMat.needsUpdate = true; 77 | }; 78 | 79 | var _setPreset = function(name) { 80 | var preset = _presets[name] || _presets.none; 81 | _currPreset = name; 82 | 83 | // set shape 84 | if (preset._shape.length >= 0) { 85 | _setSimMode(preset._shape); 86 | _uvAnim.setMesh(); // set no mesh 87 | } 88 | else { 89 | _setSimMode("SIM_TEXTURE"); 90 | _uvAnim.setMesh(preset._shape.mesh); 91 | } 92 | 93 | _guiFields["user gravity"] = _params.simMat.uniforms.uInputAccel.value = preset["user gravity"]; 94 | _guiFields["shape gravity"] = _params.simMat.uniforms.uShapeAccel.value = preset["shape gravity"]; 95 | }; 96 | 97 | var _takeScreenshot = function() { 98 | _engine.renderer.getImageData(function(dataUrl) { 99 | var url = Utils.dataUrlToBlobUrl(dataUrl); 100 | Utils.openUrlInNewWindow(url, window.innerWidth, window.innerHeight); 101 | }); 102 | }; 103 | 104 | var _toggleMusic = function() { 105 | if (_musicElem.paused) 106 | _musicElem.play(); 107 | else 108 | _musicElem.pause(); 109 | }; 110 | 111 | 112 | 113 | // UPDATE 114 | 115 | var _update = _params.update = function(dt,t) { 116 | _params.drawMat.uniforms.uTime.value = t; // for ParticleShader.vs 117 | _uvAnim.update(dt,t); 118 | if(_tourMode) _tourUpdate(dt,t); 119 | }; 120 | 121 | var _tourUpdate = (function() { 122 | var SHAPE_DURATION = 25.0; 123 | var BETWEEN_DURATION = 15.0; 124 | var BETWEEN_PRESET = "galaxy"; 125 | var sequence = ["wolf", "petals", "bear", "sphere", "horse", "panther", "plane", "bison"]; 126 | var timer = 0.0; 127 | var seqIdx = 0; 128 | var seqName; 129 | 130 | return function(dt,t) { 131 | if (timer <= 0.0) { 132 | // check against sequence 133 | // if user navigate to different preset 134 | // next tour trigger will go into shape instead of the between 135 | if (_currPreset === seqName) { 136 | _setPreset(BETWEEN_PRESET); 137 | _guiFields.shape = BETWEEN_PRESET; 138 | _gui.__controllers[0].updateDisplay(); // HARDCODE: controller idx 139 | timer = BETWEEN_DURATION; 140 | } 141 | else { 142 | // sequence finished 143 | if (seqIdx >= sequence.length) { 144 | seqIdx = 0; 145 | Utils.shuffle(sequence); 146 | console.log("tour shuffled: " + sequence); 147 | } 148 | seqName = sequence[seqIdx++]; 149 | _setPreset(seqName); 150 | _guiFields.shape = seqName; 151 | _gui.__controllers[0].updateDisplay(); 152 | console.log("tour: "+seqName); 153 | timer = SHAPE_DURATION; 154 | } 155 | } 156 | 157 | timer -= dt; 158 | }; 159 | })(); 160 | 161 | 162 | 163 | // INIT 164 | 165 | var _init = function() { 166 | _engine = new ParticleEngine(_params); 167 | 168 | _uvAnim = new UVMapAnimator(_engine.renderer.getRenderer(), _params.size); 169 | _params.simMat.uniforms.tTarget = { type: "t", value: _uvAnim.target }; 170 | }; 171 | 172 | var _initUI = function() { 173 | _gui = new dat.GUI(); 174 | _guiFields = { 175 | "color1": [_params.drawMat.uniforms.uColor1.value.x*255, _params.drawMat.uniforms.uColor1.value.y*255, _params.drawMat.uniforms.uColor1.value.z*255], 176 | "color2": [_params.drawMat.uniforms.uColor2.value.x*255, _params.drawMat.uniforms.uColor2.value.y*255, _params.drawMat.uniforms.uColor2.value.z*255], 177 | "alpha": _params.drawMat.uniforms.uAlpha.value, 178 | "color speed": _params.drawMat.uniforms.uColorSpeed.value, 179 | "color freq": _params.drawMat.uniforms.uColorFreq.value, 180 | "point size": _params.drawMat.uniforms.uPointSize.value, 181 | "user gravity": _params.simMat.uniforms.uInputAccel.value, 182 | "shape gravity": _params.simMat.uniforms.uShapeAccel.value, 183 | "shape": _currPreset, 184 | "paused": false, 185 | "camera rotate": true, 186 | "camera control": false, 187 | "screenshot": _takeScreenshot, 188 | "fullscreen": Utils.toggleFullscreen, 189 | "take tour!": _tourMode, 190 | "music": true, 191 | }; 192 | 193 | _gui.add(_guiFields, "shape", Object.keys(_presets)) 194 | .onFinishChange(_setPreset); 195 | _gui.add(_guiFields, "take tour!").onChange(function(value) { 196 | _tourMode = value; 197 | }); 198 | _gui.add(_guiFields, "music").onChange(function(value) { 199 | _toggleMusic(); 200 | }); 201 | 202 | var fAppearance = _gui.addFolder("Appearance"); 203 | fAppearance.addColor(_guiFields, "color1").onChange(function(value) { 204 | if (value[0] === "#") value = Utils.hexToRgb(value); 205 | _params.drawMat.uniforms.uColor1.value.x = value[0] / 255.0; 206 | _params.drawMat.uniforms.uColor1.value.y = value[1] / 255.0; 207 | _params.drawMat.uniforms.uColor1.value.z = value[2] / 255.0; 208 | }); 209 | fAppearance.addColor(_guiFields, "color2").onChange(function(value) { 210 | if (value[0] === "#") value = Utils.hexToRgb(value); 211 | _params.drawMat.uniforms.uColor2.value.x = value[0] / 255.0; 212 | _params.drawMat.uniforms.uColor2.value.y = value[1] / 255.0; 213 | _params.drawMat.uniforms.uColor2.value.z = value[2] / 255.0; 214 | }); 215 | fAppearance.add(_guiFields, "alpha", 0, 1).onChange(function(value) { 216 | _params.drawMat.uniforms.uAlpha.value = value; 217 | }); 218 | fAppearance.add(_guiFields, "color speed", -10, 10).onChange(function(value) { 219 | _params.drawMat.uniforms.uColorSpeed.value = value; 220 | }); 221 | fAppearance.add(_guiFields, "color freq", 0, 5).onChange(function(value) { 222 | _params.drawMat.uniforms.uColorFreq.value = value; 223 | }); 224 | fAppearance.add(_guiFields, "point size", 1, 10).onChange(function(value) { 225 | _params.drawMat.uniforms.uPointSize.value = value; 226 | }); 227 | 228 | var fPhysics = _gui.addFolder("Physics"); 229 | fPhysics.add(_guiFields, "user gravity", 0, 10) 230 | .listen() 231 | .onChange(function(value) { 232 | _params.simMat.uniforms.uInputAccel.value = value; 233 | }); 234 | fPhysics.add(_guiFields, "shape gravity", 0, 10) 235 | .listen() 236 | .onChange(function(value) { 237 | _params.simMat.uniforms.uShapeAccel.value = value; 238 | }); 239 | 240 | var fControls = _gui.addFolder("Controls"); 241 | fControls.add(_guiFields, "paused").onChange(function(value) { 242 | _engine.pauseSimulation(value); 243 | }).listen(); 244 | fControls.add(_guiFields, "camera rotate").onChange(function(value) { 245 | _engine.enableCameraAutoRotate(value); 246 | }); 247 | fControls.add(_guiFields, "camera control").onChange(function(value) { 248 | _engine.enableCameraControl(value); 249 | }).listen(); 250 | 251 | _gui.add(_guiFields, "screenshot"); 252 | _gui.add(_guiFields, "fullscreen"); 253 | }; 254 | 255 | var _initKeyboard = function() { 256 | 257 | // pause simulation 258 | Mousetrap.bind("space", function() { 259 | _guiFields.paused = !_guiFields.paused; 260 | _engine.pauseSimulation(_guiFields.paused); 261 | return false; 262 | }); 263 | 264 | // mouse camera control 265 | Mousetrap.bind(["alt", "option"], function() { 266 | _guiFields["camera control"] = true; 267 | _engine.enableCameraControl(true); 268 | return false; 269 | }, "keydown"); 270 | Mousetrap.bind(["alt", "option"], function() { 271 | _guiFields["camera control"] = false; 272 | _engine.enableCameraControl(false); 273 | return false; 274 | }, "keyup"); 275 | 276 | }; 277 | 278 | var _loadMeshes = function() { 279 | var loader = new THREE.JSONLoader(true); 280 | Object.keys(_meshes).forEach(function(k) { 281 | loader.load(_meshes[k].url, function(geometry) { 282 | var mesh = new THREE.MorphAnimMesh(geometry); // no material 283 | mesh.scale.set(_meshes[k].scale,_meshes[k].scale,_meshes[k].scale); 284 | mesh.position.y = _meshes[k].yOffset; 285 | mesh.duration = 1000 / _meshes[k].speed; 286 | mesh.name = k; // for debugging 287 | _meshes[k].mesh = mesh; 288 | 289 | // refresh mesh if same name as preset 290 | if (_currPreset === k) 291 | _uvAnim.setMesh(mesh); 292 | }); 293 | }); 294 | }; 295 | 296 | 297 | 298 | // RUN PROGRAM 299 | 300 | _loadMeshes(); 301 | _init(); 302 | _initUI(); 303 | _initKeyboard(); 304 | _setPreset(_currPreset); 305 | _engine.start(); 306 | 307 | }; -------------------------------------------------------------------------------- /app/js/src/LeapManager.js: -------------------------------------------------------------------------------- 1 | var LeapManager = function(renderer, camera, transform) { 2 | var _scene, _root, _controller; 3 | 4 | var MAX_HANDS = 4; // apparently leap can have more than 2 hands 5 | // more than MAX_HANDS will draw, from plugin 6 | // but will not be taken into account 7 | 8 | this.renderer = renderer; 9 | this.camera = camera; 10 | this.matrix = transform; 11 | 12 | 13 | // PUBLIC 14 | 15 | this.followCamera = true; 16 | 17 | // read-only 18 | this.frame = undefined; 19 | this.palmPositions = []; for (var i=0; i= _mouseStructs.length) 93 | _mouseStructs[idx] = new MouseStruct(); 94 | 95 | return _mouseStructs[idx]; 96 | }; 97 | 98 | 99 | // BIND EVENTS 100 | 101 | dom.addEventListener("mousemove", _onMouseMove, false); 102 | dom.addEventListener("mousedown", _onMouseDown, false); 103 | dom.addEventListener("mouseup", _onMouseUp, false); 104 | 105 | dom.addEventListener("touchmove", _onTouchMove, false); 106 | dom.addEventListener("touchstart", _onTouchDown, false); 107 | dom.addEventListener("touchend", _onTouchUp, false); 108 | dom.addEventListener("touchleave", _onTouchUp, false); 109 | dom.addEventListener("touchcancel", _onTouchUp, false); 110 | }; -------------------------------------------------------------------------------- /app/js/src/ParticleEngine.js: -------------------------------------------------------------------------------- 1 | var ParticleEngine = function(params) { 2 | 3 | var _this = this; 4 | 5 | var _canvas, _stats; 6 | var _updateLoop; 7 | var _renderer, _camera, _scene; 8 | var _sim, _simMat, _initMat, _drawMat; 9 | var _mouse; 10 | var _controls, _raycaster; 11 | var _leapMan; 12 | var _customUpdate; 13 | var _pauseSim = false; 14 | 15 | 16 | // PARAMS 17 | 18 | params = params || {}; 19 | _size = params.size || 512; 20 | _simMat = params.simMat || createShaderMaterial(BasicSimShader); 21 | _initMat = params.initMat || createShaderMaterial(SimInitShader); 22 | _drawMat = params.drawMat || createShaderMaterial(BasicParticleShader); 23 | _customUpdate = params.update; 24 | 25 | 26 | // EVENTS 27 | 28 | var _onWindowResize = function() { 29 | _renderer.setSize(window.innerWidth, window.innerHeight); 30 | }; 31 | 32 | var _onFrameUpdate = function(dt, t) { 33 | _stats.begin(); 34 | 35 | _leapUpdate(); 36 | 37 | _inputUpdate(); 38 | 39 | if (!_controls.enabled) _controls.update(); 40 | 41 | if(_customUpdate) _customUpdate(dt, t); 42 | 43 | _renderer.update(dt); 44 | _leapMan.render(); 45 | 46 | _stats.end(); 47 | }; 48 | 49 | var _onFixedUpdate = function(dt, t) { 50 | if (!_pauseSim) _sim.update(dt, t); 51 | }; 52 | 53 | 54 | // PRIVATE FUNCTIONS 55 | 56 | var _init = function() { 57 | window.addEventListener("resize", _onWindowResize, false); 58 | 59 | _stats = new Stats(); 60 | document.body.appendChild(_stats.domElement); 61 | 62 | _updateLoop = new UpdateLoop(); 63 | _updateLoop.frameCallback = _onFrameUpdate; 64 | _updateLoop.fixedCallback = _onFixedUpdate; 65 | 66 | _canvas = document.querySelector("#webgl-canvas"); 67 | 68 | _mouse = new Mouse(_canvas); 69 | 70 | _renderer = new RenderContext(_canvas); 71 | _renderer.init(); 72 | _camera = _renderer.getCamera(); 73 | _scene = _renderer.getScene(); 74 | }; 75 | 76 | var _sceneInit = function() { 77 | _sim = new ParticleSimulation(_renderer.getRenderer(), _size, { 78 | simMat: _simMat, 79 | initMat: _initMat, 80 | drawMat: _drawMat 81 | }); 82 | _scene.add(_sim.getParticleObject()); 83 | 84 | _camera.position.set(0,0,8); 85 | _controls = new THREE.OrbitControls(_camera, _canvas); 86 | _controls.rotateUp(Math.PI/6); 87 | _controls.autoRotate = true; 88 | _controls.autoRotateSpeed = 1.0; 89 | _controls.noPan = true; 90 | _controls.enabled = false; // disable user input 91 | 92 | _raycaster = new THREE.Raycaster(); 93 | 94 | var tmat = (new THREE.Matrix4()).compose( 95 | new THREE.Vector3(0.0, -3.0, -_camera.position.z), 96 | new THREE.Quaternion(), 97 | new THREE.Vector3(0.015,0.015,0.015)); 98 | _leapMan = new LeapManager(_renderer.getRenderer(), _camera, tmat); 99 | _simMat.defines.MULTIPLE_INPUT = ""; // TODO_NOP: at least even hardcode numbers for this in shader 100 | _simMat.needsUpdate = true; 101 | 102 | _debugBox = document.querySelector("#debug-box"); 103 | }; 104 | 105 | var _mouseUpdate = function() { 106 | var mIdMax = Utils.isMobile ? 4 : 1; 107 | for (var mId=0; mId"+_simMat.uniforms.uInputPosAccel.value.x.toFixed(2) 132 | // +" "+_simMat.uniforms.uInputPosAccel.value.y.toFixed(2) 133 | // +" "+_simMat.uniforms.uInputPosAccel.value.z.toFixed(2) 134 | // +" "+_simMat.uniforms.uInputPosAccel.value.w.toFixed(2); 135 | }; 136 | 137 | var _leapUpdate = function() { 138 | var K_PULL = 1.0; // in grabStrength 139 | var K_PUSH = 100.0; // in sphereRadius 140 | 141 | _leapMan.update(); 142 | 143 | for (var i=0; i<_leapMan.activeHandCount; i++) { 144 | var inputIdx = 3-i; // iterate backwards on input, so mouse can interact at same time 145 | if (_leapMan.frame.hands[i].grabStrength === K_PULL) { 146 | _simMat.uniforms.uInputPos.value[inputIdx].copy(_leapMan.palmPositions[i]); 147 | _simMat.uniforms.uInputPosAccel.value.setComponent(inputIdx, 1.0); 148 | } 149 | else if (_leapMan.frame.hands[i].sphereRadius >= K_PUSH) { 150 | _simMat.uniforms.uInputPos.value[inputIdx].copy(_leapMan.palmPositions[i]); 151 | _simMat.uniforms.uInputPosAccel.value.setComponent(inputIdx, -1.0); 152 | } 153 | } 154 | 155 | // _debugBox.innerHTML = 156 | // "hand1: " + (_leapMan.frame.hands[0] ? _leapMan.frame.hands[0].sphereRadius : "") + " " + 157 | // "hand2: " + (_leapMan.frame.hands[1] ? _leapMan.frame.hands[1].sphereRadius : "") + 158 | // ""; 159 | }; 160 | 161 | var _inputUpdate = function() { 162 | // reset input accels 163 | _simMat.uniforms.uInputPosAccel.value.set(0,0,0,0); 164 | if (!_controls.enabled) _mouseUpdate(); 165 | _leapUpdate(); 166 | }; 167 | 168 | 169 | // PUBLIC FUNCTIONS 170 | 171 | this.start = function() { 172 | _updateLoop.start(); 173 | }; 174 | 175 | this.stop = function() { 176 | _updateLoop.stop(); 177 | }; 178 | 179 | this.pauseSimulation = function(value) { 180 | _pauseSim = value; 181 | }; 182 | 183 | this.enableCameraAutoRotate = function(value) { 184 | _controls.autoRotate = value; 185 | }; 186 | 187 | this.enableCameraControl = function(value) { 188 | _controls.enabled = value; 189 | }; 190 | 191 | 192 | // INIT 193 | 194 | _init(); 195 | 196 | _sceneInit(); 197 | 198 | // expose variables 199 | this.renderer = _renderer; 200 | this.scene = _scene; 201 | this.camera = _camera; 202 | }; 203 | -------------------------------------------------------------------------------- /app/js/src/ParticleSimulation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * params = { 3 | * simMat: THREE.Material, 4 | * initMat: THREE.Material, 5 | * drawMat: THREE.Material 6 | * } 7 | */ 8 | var ParticleSimulation = function(renderer, size, params) { 9 | var _this = this; 10 | var _size = size; 11 | var _sim, _simMat, _initMat, _drawMat, _particles; 12 | 13 | params = params || {}; 14 | 15 | var _createParticleGeometry = function(size) { 16 | var ATTR_WIDTH = 3; 17 | var geo = new THREE.BufferGeometry(); 18 | var pos = new Float32Array(size*size*ATTR_WIDTH); 19 | for (var x=0; x 3) 90 | _currUpdateTarget = 1; 91 | }; 92 | 93 | this.registerUniform = function(uniform) { 94 | _registeredUniforms.push(uniform); 95 | uniform.value = _outTargetPtr; 96 | }; 97 | 98 | this.reset = function() { 99 | _initPass.render(_renderer, _target1); 100 | _initPass.render(_renderer, _target2); 101 | _initPass.render(_renderer, _target3); 102 | }; 103 | 104 | // INITIALIZATION 105 | 106 | _checkSupport(); 107 | 108 | // init targets 109 | 110 | _target1 = _createTarget(_size); 111 | _target2 = _createTarget(_size); 112 | _target3 = _createTarget(_size); 113 | 114 | _target1.name = "SimulationRenderer._target1"; // debug tags 115 | _target2.name = "SimulationRenderer._target2"; 116 | _target3.name = "SimulationRenderer._target3"; 117 | 118 | _currUpdateTarget = 1; 119 | _outTargetPtr = _target1; 120 | 121 | // init shader pass 122 | 123 | _simPass = new ShaderPass(simMat); 124 | _initPass = new ShaderPass(initMat); 125 | // _debugPass = new ShaderPass(SimDebugShader); 126 | // _debugPass.material.uniforms.tTarget1.value = _target1; 127 | // _debugPass.material.uniforms.tTarget2.value = _target2; 128 | // _debugPass.material.uniforms.tTarget3.value = _target3; 129 | 130 | this.reset(); // reset targets 131 | }; 132 | 133 | SimulationRenderer.prototype.constructor = SimulationRenderer; -------------------------------------------------------------------------------- /app/js/src/UVMapAnimator.js: -------------------------------------------------------------------------------- 1 | var UVMapAnimator = function(renderer, size) { 2 | var _this = this; 3 | var _mesh; 4 | var _mapper = new UVMapper(renderer); 5 | var _speed = 1.0; 6 | 7 | this.target = _mapper.createTarget(size); 8 | 9 | this.update = function(dt, t) { 10 | if (!_mesh) return; 11 | 12 | _mesh.updateAnimation(dt * 1000); 13 | _mapper.render(_mesh, _this.target); 14 | }; 15 | 16 | this.setMesh = function(mesh) { 17 | _mesh = mesh; 18 | }; 19 | }; -------------------------------------------------------------------------------- /app/js/src/UVMapper.js: -------------------------------------------------------------------------------- 1 | var UVMapper = function(renderer) { 2 | var _renderer = renderer; 3 | 4 | var _camera = new THREE.OrthographicCamera( -1, 1, 1, -1, 0, 1 ); // not really used 5 | var _scene = new THREE.Scene(); 6 | 7 | var _mat = createShaderMaterial(UVMapShader); 8 | _mat.side = THREE.DoubleSide; 9 | _mat.blending = THREE.NoBlending; 10 | _mat.depthTest = false; 11 | _mat.depthWrite = false; 12 | _mat.morphTargets = true; 13 | _scene.overrideMaterial = _mat; 14 | 15 | this.render = function(mesh, target) { 16 | // might need to make geo, so don't steal from its original parent 17 | _scene.add(mesh); 18 | _renderer.render(_scene, _camera, target, true); 19 | _scene.remove(mesh); 20 | }; 21 | 22 | this.createTarget = function(size) { 23 | var target = new THREE.WebGLRenderTarget(size, size, { 24 | minFilter: THREE.LinearFilter, 25 | magFilter: THREE.LinearFilter, 26 | format: THREE.RGBAFormat, 27 | type: THREE.FloatType, 28 | depthBuffer: false, 29 | stencilBuffer: false 30 | }); 31 | target.generateMipmaps = false; 32 | 33 | return target; 34 | }; 35 | 36 | this.createMap = function(mesh, size) { 37 | var target = this.createTarget(size); 38 | this.render(mesh, target); 39 | return target; 40 | }; 41 | }; -------------------------------------------------------------------------------- /app/js/src/UpdateLoop.js: -------------------------------------------------------------------------------- 1 | // TODO: timer sync issue between fixed and frame (?) 2 | 3 | var UpdateLoop = function() { 4 | 5 | var _this = this; 6 | 7 | var _timer = 0.0; 8 | var _timeScale = 1.0; 9 | var _fixedTimer = 0.0; 10 | var _fixedTimeRemainder = 0.0; 11 | var _FIXED_TIME_STEP = 0.02; 12 | var _FIXED_TIME_STEP_MAX = 0.2; 13 | 14 | var _clock = new Clock(false); 15 | 16 | var _requestId; 17 | 18 | // PRIVATE FUNCTIONS 19 | 20 | var _fixedUpdate = function() { 21 | var fixedDt = _FIXED_TIME_STEP * _timeScale; 22 | 23 | _fixedTimer += fixedDt; 24 | 25 | _this.fixedCallback(fixedDt, _fixedTimer); 26 | }; 27 | 28 | var _frameUpdate = function() { 29 | var frameDt = _clock.getDelta(); 30 | 31 | _timer += frameDt * _timeScale; 32 | _fixedTimeRemainder += frameDt; 33 | 34 | // cap remainder 35 | if (_fixedTimeRemainder > _FIXED_TIME_STEP_MAX) { 36 | _fixedTimeRemainder = _FIXED_TIME_STEP_MAX; 37 | } 38 | 39 | // loop remainder 40 | while (_fixedTimeRemainder > _FIXED_TIME_STEP) { 41 | _fixedUpdate(); 42 | _fixedTimeRemainder -= _FIXED_TIME_STEP; 43 | } 44 | 45 | _this.frameCallback(frameDt, _timer); 46 | }; 47 | 48 | var _loop = function() { 49 | _frameUpdate(); 50 | _requestId = window.requestAnimationFrame(_loop); 51 | }; 52 | 53 | // PUBLIC FUNCTIONS 54 | 55 | this.start = function() { 56 | if (!_requestId) { 57 | _clock.start(); 58 | _loop(); 59 | } 60 | }; 61 | 62 | this.stop = function() { 63 | if (_requestId) { 64 | window.cancelAnimationFrame(_requestId); 65 | _requestId = undefined; 66 | _clock.stop(); 67 | } 68 | }; 69 | 70 | this.setTimeScale = function(scale) { 71 | _timeScale = Math.max(scale, 0); 72 | }; 73 | 74 | // EMPTY FUNCTIONS TO SET 75 | 76 | this.fixedCallback = function(dt, t) {}; 77 | this.frameCallback = function(dt, t) {}; 78 | 79 | }; 80 | 81 | UpdateLoop.prototype.constructor = UpdateLoop; -------------------------------------------------------------------------------- /app/js/src/Utils.js: -------------------------------------------------------------------------------- 1 | var Utils = {}; 2 | 3 | // pre-computed on load 4 | Utils.isMobile = (function(a) { 5 | return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)); 6 | })(navigator.userAgent||navigator.vendor||window.opera); 7 | 8 | Utils.toRadians = function(degrees) { 9 | return degrees * Math.PI / 180.0; 10 | }; 11 | 12 | Utils.toDegrees = function(rads) { 13 | return rads * 180.0 / Math.PI; 14 | }; 15 | 16 | Utils.hexToRgb = function(hex) { 17 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 18 | return result ? [ 19 | parseInt(result[1], 16), 20 | parseInt(result[2], 16), 21 | parseInt(result[3], 16) 22 | ] : null; 23 | }; 24 | 25 | Utils.rgbToHex = function() { 26 | return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 27 | }; 28 | 29 | Utils.loadTextFile = function(url) { 30 | var result; 31 | 32 | var req = new XMLHttpRequest(); 33 | req.onerror = function() { 34 | console.log("Error: request error on " + url); 35 | }; 36 | req.onload = function() { 37 | result = this.responseText; 38 | }; 39 | req.open("GET", url, false); 40 | req.send(); 41 | 42 | return result; 43 | }; 44 | 45 | Utils.loadTextFileInject = function(url) { 46 | var regex = /#inject .+/g; 47 | var fileStr = Utils.loadTextFile(url); 48 | var matches = fileStr.match(regex); 49 | 50 | if (!matches) return fileStr; 51 | 52 | for (var i=0; i 2 | 3 | 4 | Leap Test 5 | 6 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 141 | 142 | -------------------------------------------------------------------------------- /app/shaders/Basic.vs.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | void main() { 3 | vUv = uv; 4 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 5 | } -------------------------------------------------------------------------------- /app/shaders/BasicParticleShader.fs.glsl: -------------------------------------------------------------------------------- 1 | uniform vec4 uColor; 2 | 3 | void main() { 4 | gl_FragColor = uColor; 5 | } -------------------------------------------------------------------------------- /app/shaders/BasicParticleShader.vs.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D tPos; 2 | 3 | void main() { 4 | gl_PointSize = POINT_SIZE; 5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(texture2D(tPos, position.xy).rgb, 1.0); 6 | } -------------------------------------------------------------------------------- /app/shaders/BasicSimShader.fs.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | uniform sampler2D tPrev; 4 | uniform sampler2D tCurr; 5 | uniform float uDeltaT; 6 | uniform float uTime; 7 | uniform vec3 uInputPos[4]; 8 | uniform vec4 uInputPosAccel; 9 | 10 | void main() { 11 | 12 | // read data 13 | vec3 prevPos = texture2D(tPrev, vUv).rgb; 14 | vec3 currPos = texture2D(tCurr, vUv).rgb; 15 | vec3 vel = (currPos - prevPos) / uDeltaT; 16 | 17 | // CALC ACCEL 18 | 19 | vec3 accel = vec3(0.0); 20 | 21 | // input pos 22 | #inject shaders/chunks/SimInputPos.glsl 23 | 24 | // state updates 25 | vel = K_VEL_DECAY * vel + accel * uDeltaT; 26 | currPos += vel * uDeltaT; 27 | 28 | // write out 29 | gl_FragColor = vec4(currPos, 1.0); 30 | } -------------------------------------------------------------------------------- /app/shaders/ParticleShader.fs.glsl: -------------------------------------------------------------------------------- 1 | #inject shaders/chunks/Constants.glsl 2 | 3 | 4 | varying vec3 vColor; 5 | 6 | uniform float uTime; 7 | uniform float uAlpha; 8 | 9 | void main() { 10 | 11 | // calc alpha for shape 12 | vec2 tmpCoord = 0.5 * cos(M_2PI*gl_PointCoord+M_PI) + 0.5; 13 | float alpha = tmpCoord.x * tmpCoord.y; 14 | 15 | gl_FragColor = vec4(vColor, uAlpha*alpha); 16 | } -------------------------------------------------------------------------------- /app/shaders/ParticleShader.vs.glsl: -------------------------------------------------------------------------------- 1 | #inject shaders/chunks/Constants.glsl 2 | 3 | #define PS_CAM_MAX_DIST 12.0 4 | 5 | varying vec3 vColor; 6 | 7 | uniform sampler2D tPos; 8 | uniform float uTime; 9 | uniform float uPointSize; 10 | uniform vec3 uColor1; 11 | uniform vec3 uColor2; 12 | uniform float uColorFreq; 13 | uniform float uColorSpeed; 14 | 15 | void main() { 16 | vColor = mix(uColor1, uColor2, sin(uColorSpeed*uTime + uColorFreq*position.z*M_2PI)/2.0+0.5); 17 | 18 | vec4 posSample = texture2D(tPos, position.xy); 19 | vec3 pos = posSample.rgb; 20 | 21 | vec3 camToPos = pos - cameraPosition; 22 | float camDist = length(camToPos); 23 | 24 | gl_PointSize = max(uPointSize * PS_CAM_MAX_DIST/camDist, 1.0); 25 | 26 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 27 | } -------------------------------------------------------------------------------- /app/shaders/SimInitShader.fs.glsl: -------------------------------------------------------------------------------- 1 | #inject shaders/chunks/Constants.glsl 2 | #inject shaders/chunks/Rand.glsl 3 | 4 | 5 | varying vec2 vUv; 6 | 7 | uniform sampler2D tDiffuse; 8 | uniform vec4 uColor; 9 | 10 | void main() { 11 | // vec3 pos = vec3(vUv.x, vUv.y, rand(vUv)); 12 | 13 | // square sheet 14 | // vec3 pos = vec3(vUv.x, vUv.y, 0.0); 15 | // vec3 center = vec3(0.0, 0.0, 0.0); 16 | // vec3 size = vec3(1.0, 1.0, 1.0); 17 | // pos = pos*size + center - size/2.0; 18 | 19 | // sphere, continuous along vUv.y 20 | // vec2 coords = vUv; 21 | // coords.x = coords.x * M_2PI - M_PI; // theta (lat) 22 | // coords.y = coords.y * M_PI; // phi (long) 23 | // vec3 sphereCoords = vec3( 24 | // sin(coords.y) * cos(coords.x), 25 | // cos(coords.y), 26 | // sin(coords.y) * sin(coords.x) 27 | // ); 28 | // vec3 pos = sphereCoords * rand(vUv); 29 | 30 | // sphere coords, rand radius, offset y+0.5 for snoise vel 31 | vec2 coords = vUv; 32 | coords.x = coords.x * M_2PI - M_PI; 33 | coords.y = mod(coords.y+0.5, 1.0) * M_PI; 34 | vec3 sphereCoords = vec3( 35 | sin(coords.y) * cos(coords.x), 36 | cos(coords.y), 37 | sin(coords.y) * sin(coords.x) 38 | ); 39 | vec3 pos = sphereCoords * rand(vUv); 40 | pos *= 5.0; 41 | 42 | gl_FragColor = vec4(pos, 1.0); 43 | } -------------------------------------------------------------------------------- /app/shaders/SimShader.fs.glsl: -------------------------------------------------------------------------------- 1 | #inject shaders/chunks/Constants.glsl 2 | #inject shaders/chunks/Rand.glsl 3 | #inject shaders/chunks/NoiseFuncs.glsl 4 | 5 | 6 | varying vec2 vUv; 7 | 8 | uniform sampler2D tPrev; 9 | uniform sampler2D tCurr; 10 | uniform float uDeltaT; 11 | uniform float uTime; 12 | uniform vec3 uInputPos[4]; 13 | uniform vec4 uInputPosAccel; 14 | uniform float uInputAccel; 15 | uniform float uShapeAccel; 16 | 17 | #ifdef SIM_TEXTURE 18 | uniform sampler2D tTarget; 19 | #endif 20 | 21 | void main() { 22 | 23 | // read data 24 | vec3 prevPos = texture2D(tPrev, vUv).rgb; 25 | vec3 currPos = texture2D(tCurr, vUv).rgb; 26 | vec3 vel = (currPos - prevPos) / uDeltaT; 27 | 28 | // CALC ACCEL 29 | 30 | vec3 accel = vec3(0.0); 31 | 32 | #inject shaders/chunks/SimBasicShapes.glsl 33 | #inject shaders/chunks/SimRoseGalaxy.glsl 34 | #inject shaders/chunks/SimGalaxy.glsl 35 | #inject shaders/chunks/SimTextureTarget.glsl 36 | 37 | #inject shaders/chunks/SimInputPos.glsl 38 | 39 | // state updates 40 | vel = K_VEL_DECAY * vel + accel * uDeltaT; 41 | currPos += vel * uDeltaT; 42 | 43 | // write out 44 | gl_FragColor = vec4(currPos, 1.0); 45 | } -------------------------------------------------------------------------------- /app/shaders/UVMapShader.fs.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vPos; 2 | 3 | void main() { 4 | gl_FragColor = vec4(vPos, 1.0); 5 | } -------------------------------------------------------------------------------- /app/shaders/UVMapShader.vs.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vPos; 2 | 3 | #ifdef USE_MORPHTARGETS 4 | uniform float morphTargetInfluences[ 4 ]; 5 | #endif 6 | 7 | void main() { 8 | 9 | #ifdef USE_MORPHTARGETS 10 | vec3 morphed = vec3( 0.0 ); 11 | morphed += ( morphTarget0 - position ) * morphTargetInfluences[ 0 ]; 12 | morphed += ( morphTarget1 - position ) * morphTargetInfluences[ 1 ]; 13 | morphed += ( morphTarget2 - position ) * morphTargetInfluences[ 2 ]; 14 | morphed += ( morphTarget3 - position ) * morphTargetInfluences[ 3 ]; 15 | morphed += position; 16 | 17 | vPos = (modelMatrix * vec4(morphed, 1.0)).xyz; 18 | #else 19 | vPos = (modelMatrix * vec4(position, 1.0)).xyz; 20 | #endif 21 | 22 | vec2 drawUV = uv * 2.0 - 1.0; 23 | gl_Position = vec4(drawUV.x, drawUV.y, 0.0, 1.0); 24 | } -------------------------------------------------------------------------------- /app/shaders/chunks/Constants.glsl: -------------------------------------------------------------------------------- 1 | #define M_PI 3.14159265358979323846264338327950 2 | #define M_2PI 6.28318530717958647692528676655900 3 | #define M_PI2 1.57079632679489661923132169163975 4 | 5 | #define EPS 0.0001 6 | 7 | #define EQUALS(A,B) ( abs((A)-(B)) < EPS ) 8 | #define EQUALSZERO(A) ( ((A)-EPS) ) 9 | -------------------------------------------------------------------------------- /app/shaders/chunks/NoiseFuncs.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | * Description : Array and textureless GLSL 2D/3D/4D simplex 3 | * noise functions. 4 | * Author : Ian McEwan, Ashima Arts. 5 | * Maintainer : ijm 6 | * Lastmod : 20110822 (ijm) 7 | * License : Copyright (C) 2011 Ashima Arts. All rights reserved. 8 | * Distributed under the MIT License. See LICENSE file. 9 | * https://github.com/ashima/webgl-noise 10 | */ 11 | 12 | vec4 _mod289(vec4 x) 13 | { 14 | return x - floor(x * (1.0 / 289.0)) * 289.0; 15 | } 16 | 17 | vec3 _mod289(vec3 x) 18 | { 19 | return x - floor(x * (1.0 / 289.0)) * 289.0; 20 | } 21 | 22 | vec2 _mod289(vec2 x) 23 | { 24 | return x - floor(x * (1.0 / 289.0)) * 289.0; 25 | } 26 | 27 | float _mod289(float x) 28 | { 29 | return x - floor(x * (1.0 / 289.0)) * 289.0; 30 | } 31 | 32 | vec4 _permute(vec4 x) 33 | { 34 | return _mod289(((x*34.0)+1.0)*x); 35 | } 36 | 37 | vec3 _permute(vec3 x) 38 | { 39 | return _mod289(((x*34.0)+1.0)*x); 40 | } 41 | 42 | float _permute(float x) 43 | { 44 | return _mod289(((x*34.0)+1.0)*x); 45 | } 46 | 47 | vec4 _taylorInvSqrt(vec4 r) 48 | { 49 | return 1.79284291400159 - 0.85373472095314 * r; 50 | } 51 | 52 | float _taylorInvSqrt(float r) 53 | { 54 | return 1.79284291400159 - 0.85373472095314 * r; 55 | } 56 | 57 | vec4 _grad4(float j, vec4 ip) 58 | { 59 | const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0); 60 | vec4 p,s; 61 | 62 | p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0; 63 | p.w = 1.5 - dot(abs(p.xyz), ones.xyz); 64 | s = vec4(lessThan(p, vec4(0.0))); 65 | p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www; 66 | 67 | return p; 68 | } 69 | 70 | /* 71 | * Implemented by Ian McEwan, Ashima Arts, and distributed under the MIT License. {@link https://github.com/ashima/webgl-noise} 72 | */ 73 | float snoise(vec2 v) 74 | { 75 | const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0 76 | 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) 77 | -0.577350269189626, // -1.0 + 2.0 * C.x 78 | 0.024390243902439); // 1.0 / 41.0 79 | // First corner 80 | vec2 i = floor(v + dot(v, C.yy) ); 81 | vec2 x0 = v - i + dot(i, C.xx); 82 | 83 | // Other corners 84 | vec2 i1; 85 | //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0 86 | //i1.y = 1.0 - i1.x; 87 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 88 | // x0 = x0 - 0.0 + 0.0 * C.xx ; 89 | // x1 = x0 - i1 + 1.0 * C.xx ; 90 | // x2 = x0 - 1.0 + 2.0 * C.xx ; 91 | vec4 x12 = x0.xyxy + C.xxzz; 92 | x12.xy -= i1; 93 | 94 | // Permutations 95 | i = _mod289(i); // Avoid truncation effects in permutation 96 | vec3 p = _permute( _permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); 97 | 98 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); 99 | m = m*m ; 100 | m = m*m ; 101 | 102 | // Gradients: 41 points uniformly over a line, mapped onto a diamond. 103 | // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287) 104 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 105 | vec3 h = abs(x) - 0.5; 106 | vec3 ox = floor(x + 0.5); 107 | vec3 a0 = x - ox; 108 | 109 | // Normalise gradients implicitly by scaling m 110 | // Approximation of: m *= inversesqrt( a0*a0 + h*h ); 111 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 112 | 113 | // Compute final noise value at P 114 | vec3 g; 115 | g.x = a0.x * x0.x + h.x * x0.y; 116 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 117 | return 130.0 * dot(m, g); 118 | } 119 | 120 | float snoise(vec3 v) 121 | { 122 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; 123 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); 124 | 125 | // First corner 126 | vec3 i = floor(v + dot(v, C.yyy) ); 127 | vec3 x0 = v - i + dot(i, C.xxx) ; 128 | 129 | // Other corners 130 | vec3 g = step(x0.yzx, x0.xyz); 131 | vec3 l = 1.0 - g; 132 | vec3 i1 = min( g.xyz, l.zxy ); 133 | vec3 i2 = max( g.xyz, l.zxy ); 134 | 135 | // x0 = x0 - 0.0 + 0.0 * C.xxx; 136 | // x1 = x0 - i1 + 1.0 * C.xxx; 137 | // x2 = x0 - i2 + 2.0 * C.xxx; 138 | // x3 = x0 - 1.0 + 3.0 * C.xxx; 139 | vec3 x1 = x0 - i1 + C.xxx; 140 | vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y 141 | vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y 142 | 143 | // Permutations 144 | i = _mod289(i); 145 | vec4 p = _permute( _permute( _permute( 146 | i.z + vec4(0.0, i1.z, i2.z, 1.0 )) 147 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) 148 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); 149 | 150 | // Gradients: 7x7 points over a square, mapped onto an octahedron. 151 | // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) 152 | float n_ = 0.142857142857; // 1.0/7.0 153 | vec3 ns = n_ * D.wyz - D.xzx; 154 | 155 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) 156 | 157 | vec4 x_ = floor(j * ns.z); 158 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N) 159 | 160 | vec4 x = x_ *ns.x + ns.yyyy; 161 | vec4 y = y_ *ns.x + ns.yyyy; 162 | vec4 h = 1.0 - abs(x) - abs(y); 163 | 164 | vec4 b0 = vec4( x.xy, y.xy ); 165 | vec4 b1 = vec4( x.zw, y.zw ); 166 | 167 | //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; 168 | //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; 169 | vec4 s0 = floor(b0)*2.0 + 1.0; 170 | vec4 s1 = floor(b1)*2.0 + 1.0; 171 | vec4 sh = -step(h, vec4(0.0)); 172 | 173 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; 174 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; 175 | 176 | vec3 p0 = vec3(a0.xy,h.x); 177 | vec3 p1 = vec3(a0.zw,h.y); 178 | vec3 p2 = vec3(a1.xy,h.z); 179 | vec3 p3 = vec3(a1.zw,h.w); 180 | 181 | //Normalise gradients 182 | vec4 norm = _taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); 183 | p0 *= norm.x; 184 | p1 *= norm.y; 185 | p2 *= norm.z; 186 | p3 *= norm.w; 187 | 188 | // Mix final noise value 189 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); 190 | m = m * m; 191 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), 192 | dot(p2,x2), dot(p3,x3) ) ); 193 | } 194 | 195 | // Using @eddietree implementation from 196 | // His brilliant demo 'Artifacts' 197 | 198 | vec3 snoiseVec3( vec3 x ){ 199 | 200 | float s = snoise(vec3( x )); 201 | float s1 = snoise(vec3( x.y - 19.1 , x.z + 33.4 , x.x + 47.2 )); 202 | float s2 = snoise(vec3( x.z + 74.2 , x.x - 124.5 , x.y + 99.4 )); 203 | vec3 c = vec3( s , s1 , s2 ); 204 | return c; 205 | 206 | } 207 | 208 | 209 | vec3 curlNoise( vec3 p ){ 210 | 211 | const float e = 1e-1; 212 | vec3 dx = vec3( e , 0.0 , 0.0 ); 213 | vec3 dy = vec3( 0.0 , e , 0.0 ); 214 | vec3 dz = vec3( 0.0 , 0.0 , e ); 215 | 216 | vec3 p_x0 = snoiseVec3( p - dx ); 217 | vec3 p_x1 = snoiseVec3( p + dx ); 218 | vec3 p_y0 = snoiseVec3( p - dy ); 219 | vec3 p_y1 = snoiseVec3( p + dy ); 220 | vec3 p_z0 = snoiseVec3( p - dz ); 221 | vec3 p_z1 = snoiseVec3( p + dz ); 222 | 223 | float x = p_y1.z - p_y0.z - p_z1.y + p_z0.y; 224 | float y = p_z1.x - p_z0.x - p_x1.z + p_x0.z; 225 | float z = p_x1.y - p_x0.y - p_y1.x + p_y0.x; 226 | 227 | const float divisor = 1.0 / ( 2.0 * e ); 228 | return normalize( vec3( x , y , z ) * divisor ); 229 | } 230 | -------------------------------------------------------------------------------- /app/shaders/chunks/Rand.glsl: -------------------------------------------------------------------------------- 1 | // source: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl 2 | float rand(vec2 seed) { 3 | return fract(sin(dot(seed.xy,vec2(12.9898,78.233))) * 43758.5453); 4 | } 5 | -------------------------------------------------------------------------------- /app/shaders/chunks/SimBasicShapes.glsl: -------------------------------------------------------------------------------- 1 | { 2 | #ifdef SIM_PLANE 3 | vec2 coords = vUv * 2.0 - 1.0; 4 | vec3 targetPos = vec3(coords.x, 0.0, coords.y); 5 | targetPos *= 3.0; 6 | #endif 7 | 8 | #ifdef SIM_CUBE 9 | vec3 targetPos = vec3(vUv.x, vUv.y, rand(vUv)) * 2.0 - 1.0; 10 | targetPos *= 2.0; 11 | #endif 12 | 13 | #ifdef SIM_DISC 14 | // cylindrical coords 15 | float radius = vUv.y; 16 | float theta = vUv.x * M_2PI; 17 | vec3 targetPos = vec3( 18 | radius * sin(theta), 19 | 0.0, 20 | radius * cos(theta) 21 | ); 22 | targetPos *= 3.0; 23 | #endif 24 | 25 | #ifdef SIM_SPHERE 26 | // sphere, continuous along vUv.y 27 | vec2 coords = vUv; 28 | coords.x = coords.x * M_2PI - M_PI; // theta (lat) 29 | coords.y = coords.y * M_PI; // phi (long) 30 | vec3 sphereCoords = vec3( 31 | sin(coords.y) * cos(coords.x), 32 | cos(coords.y), 33 | sin(coords.y) * sin(coords.x) 34 | ); 35 | 36 | float r = 1.0; 37 | vec3 targetPos = r * sphereCoords; 38 | targetPos *= 2.0; 39 | #endif 40 | 41 | #ifdef SIM_BALL 42 | // sphere coords, rand radius, offset y+0.5 for snoise vel 43 | vec2 coords = vUv; 44 | coords.x = coords.x * M_2PI - M_PI; 45 | coords.y = coords.y * M_PI; 46 | vec3 sphereCoords = vec3( 47 | sin(coords.y) * cos(coords.x), 48 | cos(coords.y), 49 | sin(coords.y) * sin(coords.x) 50 | ); 51 | vec3 targetPos = sphereCoords * rand(vUv); 52 | targetPos *= 2.0; 53 | #endif 54 | 55 | #if defined(SIM_PLANE) || defined(SIM_SPHERE) || defined(SIM_BALL) || defined(SIM_CUBE) || defined(SIM_DISC) 56 | vec3 toTarget = targetPos - currPos; 57 | float toTargetLength = length(toTarget); 58 | if (!EQUALSZERO(toTargetLength)) 59 | accel += uShapeAccel * toTarget/toTargetLength; 60 | #endif 61 | } 62 | 63 | #ifdef SIM_NOISE 64 | accel += 0.2 * uShapeAccel * curlNoise(currPos); 65 | #endif -------------------------------------------------------------------------------- /app/shaders/chunks/SimGalaxy.glsl: -------------------------------------------------------------------------------- 1 | { 2 | #ifdef SIM_GALAXY 3 | 4 | #define K_NUM_ARMS 7.0 5 | #define K_HEIGHT 0.5 6 | #define K_SPIN_SPEED 0.25 7 | 8 | #define K_NOISE_ACCEL 0.1 9 | 10 | // cylindrical coords 11 | float radius = vUv.y; 12 | float theta = vUv.x * M_2PI; 13 | 14 | float randVal = rand(vec2(theta, radius)); 15 | 16 | // jitter coords 17 | radius += randVal * 0.5; 18 | theta += randVal * 0.5; 19 | 20 | float radialArms = sin(K_NUM_ARMS * theta); 21 | 22 | float taperComponent = cos(0.6*radius*M_PI/2.0); 23 | taperComponent *= taperComponent; 24 | float heightParam = K_HEIGHT // height constant 25 | * (rand(vec2(radius, theta))-0.5) // provide unit thickness with rand 26 | * taperComponent; // taper along radius using cosine curve 27 | 28 | float spinParam = theta // angle parameter 29 | + radius*radius // twist at rate r^2 30 | - K_SPIN_SPEED * uTime; // spin at constant speed 31 | 32 | vec3 targetPos = vec3( 33 | radius * sin(spinParam), 34 | heightParam, 35 | radius * cos(spinParam) 36 | ); 37 | targetPos *= 3.0; 38 | 39 | vec3 toTarget = targetPos - currPos; 40 | float toTargetLength = length(toTarget); 41 | accel += uShapeAccel * toTarget/toTargetLength 42 | * (radialArms/2.0+0.5) // gravity stronger in arms 43 | * randVal; // randomize gravity prevents banding 44 | 45 | 46 | // noise 47 | float noiseTime = uTime; 48 | accel += K_NOISE_ACCEL * curlNoise(currPos);// + vec3(sin(noiseTime), cos(noiseTime), sin(noiseTime)*cos(noiseTime))); 49 | 50 | #endif 51 | } -------------------------------------------------------------------------------- /app/shaders/chunks/SimInputPos.glsl: -------------------------------------------------------------------------------- 1 | { 2 | #define PROCESS_INPUT_POS(ACC, POS) if ((ACC) != 0.0) { vec3 toCenter = (POS)-currPos; float toCenterLength = length(toCenter); accel += (toCenter/toCenterLength) * (ACC)*uInputAccel/toCenterLength; } 3 | 4 | PROCESS_INPUT_POS(uInputPosAccel.x, uInputPos[0]); 5 | #ifdef MULTIPLE_INPUT 6 | PROCESS_INPUT_POS(uInputPosAccel.y, uInputPos[1]); 7 | PROCESS_INPUT_POS(uInputPosAccel.z, uInputPos[2]); 8 | PROCESS_INPUT_POS(uInputPosAccel.w, uInputPos[3]); 9 | #endif 10 | } -------------------------------------------------------------------------------- /app/shaders/chunks/SimRoseGalaxy.glsl: -------------------------------------------------------------------------------- 1 | #ifdef SIM_ROSE_GALAXY 2 | { 3 | // cylindrical coords 4 | float radius = vUv.y; 5 | float theta = vUv.x * M_2PI; 6 | 7 | // outward spiral function 8 | radius *= M_PI; 9 | vec3 targetPos = vec3( 10 | radius * sin(theta), 11 | radius*radius * sin(4.0*theta + sin(3.0*M_PI*radius+uTime/2.0)) / 10.0, 12 | radius * cos(theta) 13 | ); 14 | 15 | vec3 toTarget = targetPos - currPos; 16 | float toTargetLength = length(toTarget); 17 | accel += uShapeAccel * toTarget/toTargetLength; 18 | } 19 | #endif -------------------------------------------------------------------------------- /app/shaders/chunks/SimTextureTarget.glsl: -------------------------------------------------------------------------------- 1 | { 2 | #ifdef SIM_TEXTURE 3 | 4 | #define K_NOISE_ACCEL 0.1 5 | #define K_UV_OFFSET 0.02 6 | 7 | // jitter uv 8 | vec2 uvOffset = vec2( 9 | rand(vec2(vUv.x, vUv.y+uTime)), 10 | rand(vec2(vUv.x+uTime, vUv.y)) 11 | ); 12 | 13 | vec4 targetCol = texture2D(tTarget, vUv + uvOffset*K_UV_OFFSET); 14 | 15 | if (targetCol.a > 0.0) { 16 | vec3 toTarget = targetCol.rgb - currPos; 17 | float toTargetLength = length(toTarget); 18 | if (!EQUALSZERO(toTargetLength)) 19 | accel += uShapeAccel * toTarget/toTargetLength; 20 | } 21 | else { 22 | accel += K_NOISE_ACCEL * curlNoise(currPos); 23 | } 24 | #endif 25 | } -------------------------------------------------------------------------------- /app/uvtest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 81 | 82 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | static app -a 0.0.0.0 -H "{\"Cache-Control\": \"no-cache, must-revalidate\"}" --------------------------------------------------------------------------------