├── LICENSE ├── README.md ├── assets └── title.png ├── image.jpg ├── index.html └── js ├── EquirectangularToCubemap.js ├── Kick.js ├── Maf.js ├── OBJLoader.js ├── OrbitControls.js ├── THREE.FBOHelper.js ├── isMobile.min.js ├── three.js └── three.min.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jaume Sanchez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pumpkin Jam (halloween-2016) 2 | 3 | #### A silly Halloween-themed stravaganza 4 | Pumpkin-based music visualisation using WebGL and Web Audio 5 | 6 | 7 | 3D models and textures by BitGem Halloween Pumpkins 8 | 9 | > The repo is missing the assets from BitGem. I wasn't sure about the license, so I haven't uploaded them 10 | 11 | > I know, it sucks! The project won't work without them! But the code is there, and you can buy the assets 12 | 13 | Made with three.js, THREE.FBOHelper, isMobile 14 | 15 | Curl noise from glsl-curl-noise by @cabbibo 16 | 17 | Fog equation adapted from glsl-fog by @hughskennedy 18 | 19 | Kick detection adapted from dancer.js by @jsantell 20 | 21 | GLSL Perlin noise from webgl-noise 22 | 23 | # Credits 24 | 25 | Jaume Sanchez @thespite · www.clicktorelease.com 26 | 27 | # License 28 | 29 | MIT licensed 30 | 31 | Copyright (C) 2016 Jaume Sanchez Elias, http://www.clicktorelease.com 32 | -------------------------------------------------------------------------------- /assets/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/halloween-2016/3bf81f92fd5a4e4a24f0b62bf728f677eb088d98/assets/title.png -------------------------------------------------------------------------------- /image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/halloween-2016/3bf81f92fd5a4e4a24f0b62bf728f677eb088d98/image.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pumpkin Jam - Halloween 2016 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 174 | 175 | 176 | 177 |
178 |
179 |
Loading...
180 | 223 |
224 | 225 | 232 |
Mobile and tablet are unsupported at the moment
Sorry, I coded this in two nights, 233 | I'd need more time to get mobile ready!
234 |
235 |
236 |

Click to start

237 |
238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 270 | 491 | 579 | 620 | 638 | 655 | 670 | 699 | 733 | 816 | 1664 | 1665 | 1666 | 1667 | 1668 | -------------------------------------------------------------------------------- /js/EquirectangularToCubemap.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | 3 | "use strict"; 4 | 5 | var root = this 6 | 7 | var has_require = typeof require !== 'undefined' 8 | 9 | var THREE = root.THREE || has_require && require('three') 10 | if( !THREE ) 11 | throw new Error( 'EquirectangularToCubemap requires three.js' ) 12 | 13 | function EquirectangularToCubemap( renderer ) { 14 | 15 | this.renderer = renderer; 16 | this.scene = new THREE.Scene(); 17 | 18 | var gl = this.renderer.getContext(); 19 | this.maxSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE ) 20 | 21 | this.camera = new THREE.CubeCamera( 1, 100000, 1 ); 22 | 23 | this.material = new THREE.MeshBasicMaterial( { 24 | map: null, 25 | side: THREE.BackSide 26 | } ); 27 | 28 | this.mesh = new THREE.Mesh( 29 | new THREE.IcosahedronGeometry( 100, 4 ), 30 | this.material 31 | ); 32 | this.scene.add( this.mesh ); 33 | 34 | } 35 | 36 | EquirectangularToCubemap.prototype.convert = function( source, size ) { 37 | 38 | var mapSize = Math.min( size, this.maxSize ); 39 | this.camera = new THREE.CubeCamera( 1, 100000, mapSize ); 40 | this.material.map = source; 41 | 42 | this.camera.updateCubeMap( this.renderer, this.scene ); 43 | 44 | return this.camera.renderTarget.texture; 45 | 46 | } 47 | 48 | if( typeof exports !== 'undefined' ) { 49 | if( typeof module !== 'undefined' && module.exports ) { 50 | exports = module.exports = EquirectangularToCubemap 51 | } 52 | exports.EquirectangularToCubemap = EquirectangularToCubemap 53 | } 54 | else { 55 | root.EquirectangularToCubemap = EquirectangularToCubemap 56 | } 57 | 58 | }).call(this); 59 | -------------------------------------------------------------------------------- /js/Kick.js: -------------------------------------------------------------------------------- 1 | var Kick = function ( o ) { 2 | o = o || {}; 3 | this.frequency = o.frequency !== undefined ? o.frequency : [ 0, 10 ]; 4 | this.threshold = o.threshold !== undefined ? o.threshold : 0.3; 5 | this.decay = o.decay !== undefined ? o.decay : 0.02; 6 | this.onKick = o.onKick; 7 | this.offKick = o.offKick; 8 | this.isOn = false; 9 | this.currentThreshold = this.threshold; 10 | }; 11 | 12 | Kick.prototype = { 13 | on : function () { 14 | this.isOn = true; 15 | return this; 16 | }, 17 | off : function () { 18 | this.isOn = false; 19 | return this; 20 | }, 21 | 22 | set : function ( o ) { 23 | o = o || {}; 24 | this.frequency = o.frequency !== undefined ? o.frequency : this.frequency; 25 | this.threshold = o.threshold !== undefined ? o.threshold : this.threshold; 26 | this.decay = o.decay !== undefined ? o.decay : this.decay; 27 | this.onKick = o.onKick || this.onKick; 28 | this.offKick = o.offKick || this.offKick; 29 | }, 30 | 31 | onUpdate : function () { 32 | if ( !this.isOn ) { return; } 33 | var magnitude = this.maxAmplitude( this.frequency ); 34 | if ( magnitude >= this.currentThreshold && 35 | magnitude >= this.threshold ) { 36 | this.currentThreshold = magnitude; 37 | this.onKick && this.onKick.call( this.dancer, magnitude ); 38 | } else { 39 | this.offKick && this.offKick.call( this.dancer, magnitude ); 40 | this.currentThreshold -= this.decay; 41 | } 42 | }, 43 | maxAmplitude : function ( frequency ) { 44 | var 45 | max = 0, 46 | fft = frequencyData; 47 | 48 | // Sloppy array check 49 | if ( !frequency.length ) { 50 | return frequency < fft.length ? 51 | fft[ ~~frequency ] : 52 | null; 53 | } 54 | 55 | for ( var i = frequency[ 0 ], l = frequency[ 1 ]; i <= l; i++ ) { 56 | if ( fft[ i ] > max ) { max = fft[ i ]; } 57 | } 58 | return max; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /js/Maf.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // Module code from underscore.js 4 | 5 | // Establish the root object, `window` (`self`) in the browser, `global` 6 | // on the server, or `this` in some virtual machines. We use `self` 7 | // instead of `window` for `WebWorker` support. 8 | var root = typeof self == 'object' && self.self === self && self || 9 | typeof global == 'object' && global.global === global && global || 10 | this; 11 | 12 | var Maf = function(obj) { 13 | if (obj instanceof Maf ) return obj; 14 | if (!(this instanceof Maf )) return new Maf(obj); 15 | this._wrapped = obj; 16 | }; 17 | 18 | // Export the Underscore object for **Node.js**, with 19 | // backwards-compatibility for their old module API. If we're in 20 | // the browser, add `Maf` as a global object. 21 | // (`nodeType` is checked to ensure that `module` 22 | // and `exports` are not HTML elements.) 23 | if (typeof exports != 'undefined' && !exports.nodeType) { 24 | if (typeof module != 'undefined' && !module.nodeType && module.exports) { 25 | exports = module.exports = Maf; 26 | } 27 | exports.Maf = Maf; 28 | } else { 29 | root.Maf = Maf; 30 | } 31 | 32 | // Current version. 33 | Maf.VERSION = '1.0.0'; 34 | 35 | Maf.PI = Math.PI; 36 | 37 | // https://www.opengl.org/sdk/docs/man/html/clamp.xhtml 38 | 39 | Maf.clamp = function( v, minVal, maxVal ) { 40 | return Math.min( maxVal, Math.max( minVal, v ) ); 41 | }; 42 | 43 | // https://www.opengl.org/sdk/docs/man/html/step.xhtml 44 | 45 | Maf.step = function( edge, v ) { 46 | return ( v < edge ) ? 0 : 1; 47 | } 48 | 49 | // https://www.opengl.org/sdk/docs/man/html/smoothstep.xhtml 50 | 51 | Maf.smoothStep = function ( edge0, edge1, v ) { 52 | var t = Maf.clamp( ( v - edge0 ) / ( edge1 - edge0 ), 0.0, 1.0 ); 53 | return t * t * ( 3.0 - 2.0 * t ); 54 | }; 55 | 56 | // http://docs.unity3d.com/ScriptReference/Mathf.html 57 | // http://www.shaderific.com/glsl-functions/ 58 | // https://www.opengl.org/sdk/docs/man4/html/ 59 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ff471376(v=vs.85).aspx 60 | // http://moutjs.com/docs/v0.11/math.html#map 61 | // https://code.google.com/p/kuda/source/browse/public/js/hemi/utils/mathUtils.js?r=8d581c02651077c4ac3f5fc4725323210b6b13cc 62 | 63 | // Converts from degrees to radians. 64 | Maf.deg2Rad = function( degrees ) { 65 |   return degrees * Math.PI / 180; 66 | }; 67 | 68 | Maf.toRadians = Maf.deg2Rad; 69 | 70 | // Converts from radians to degrees. 71 | Maf.rad2Deg = function(radians) { 72 |   return radians * 180 / Math.PI; 73 | }; 74 | 75 | Maf.toDegrees = Maf.rad2Deg; 76 | 77 | Maf.clamp01 = function( v ) { 78 | return Maf.clamp( v, 0, 1 ); 79 | }; 80 | 81 | // https://www.opengl.org/sdk/docs/man/html/mix.xhtml 82 | 83 | Maf.mix = function( x, y, a ) { 84 | if( a <= 0 ) return x; 85 | if( a >= 1 ) return y; 86 | return x + a * (y - x) 87 | }; 88 | 89 | Maf.lerp = Maf.mix; 90 | 91 | Maf.inverseMix = function( a, b, v ) { 92 | return ( v - a ) / ( b - a ); 93 | }; 94 | 95 | Maf.inverseLerp = Maf.inverseMix; 96 | 97 | Maf.mixUnclamped = function( x, y, a ) { 98 | if( a <= 0 ) return x; 99 | if( a >= 1 ) return y; 100 | return x + a * (y - x) 101 | }; 102 | 103 | Maf.lerpUnclamped = Maf.mixUnclamped; 104 | 105 | // https://www.opengl.org/sdk/docs/man/html/fract.xhtml 106 | 107 | Maf.fract = function( v ) { 108 | return v - Math.floor( v ); 109 | }; 110 | 111 | Maf.frac = Maf.fract; 112 | 113 | // http://stackoverflow.com/questions/4965301/finding-if-a-number-is-a-power-of-2 114 | 115 | Maf.isPowerOfTwo = function( v ) { 116 | return ( ( ( v - 1) & v ) == 0 ); 117 | }; 118 | 119 | // https://bocoup.com/weblog/find-the-closest-power-of-2-with-javascript 120 | 121 | Maf.closestPowerOfTwo = function( v ) { 122 | return Math.pow( 2, Math.round( Math.log( v ) / Math.log( 2 ) ) ); 123 | }; 124 | 125 | Maf.nextPowerOfTwo = function( v ) { 126 | return Math.pow( 2, Math.ceil( Math.log( v ) / Math.log( 2 ) ) ); 127 | } 128 | 129 | // http://stackoverflow.com/questions/1878907/the-smallest-difference-between-2-angles 130 | 131 | //function mod(a, n) { return a - Math.floor(a/n) * n; } 132 | Maf.mod = function(a, n) { return (a % n + n) % n; } 133 | 134 | Maf.deltaAngle = function( a, b ) { 135 | var d = Maf.mod( b - a, 360 ); 136 | if( d > 180 ) d = Math.abs( d - 360 ); 137 | return d; 138 | }; 139 | 140 | Maf.deltaAngleDeg = Maf.deltaAngle; 141 | 142 | Maf.deltaAngleRad = function( a, b ) { 143 | return Maf.toRadians( Maf.deltaAngle( Maf.toDegrees( a ), Maf.toDegrees( b ) ) ); 144 | }; 145 | 146 | Maf.lerpAngle = function( a, b, t ) { 147 | var angle = Maf.deltaAngle( a, b ); 148 | return Maf.mod( a + Maf.lerp( 0, angle, t ), 360 ); 149 | }; 150 | 151 | Maf.lerpAngleDeg = Maf.lerpAngle; 152 | 153 | Maf.lerpAngleRad = function( a, b, t ) { 154 | return Maf.toRadians( Maf.lerpAngleDeg( Maf.toDegrees( a ), Maf.toDegrees( b ), t ) ); 155 | }; 156 | 157 | // http://gamedev.stackexchange.com/questions/74324/gamma-space-and-linear-space-with-shader 158 | 159 | Maf.gammaToLinearSpace = function( v ) { 160 | return Math.pow( v, 2.2 ); 161 | }; 162 | 163 | Maf.linearToGammaSpace = function( v ) { 164 | return Math.pow( v, 1 / 2.2 ); 165 | }; 166 | 167 | Maf.map = function( from1, to1, from2, to2, v ) { 168 | return from2 + ( v - from1 ) * ( to2 - from2 ) / ( to1 - from1 ); 169 | } 170 | 171 | Maf.scale = Maf.map; 172 | 173 | // http://www.iquilezles.org/www/articles/functions/functions.htm 174 | 175 | Maf.almostIdentity = function( x, m, n ) { 176 | 177 | if( x > m ) return x; 178 | 179 | var a = 2 * n - m; 180 | var b = 2 * m - 3 * n; 181 | var t = x / m; 182 | 183 | return ( a * t + b) * t * t + n; 184 | } 185 | 186 | Maf.impulse = function( k, x ) { 187 | var h = k * x; 188 | return h * Math.exp( 1 - h ); 189 | }; 190 | 191 | Maf.cubicPulse = function( c, w, x ) { 192 | x = Math.abs( x - c ); 193 | if( x > w ) return 0; 194 | x /= w; 195 | return 1 - x * x * ( 3 - 2 * x ); 196 | } 197 | 198 | Maf.expStep = function( x, k, n ) { 199 | return Math.exp( -k * Math.pow( x, n ) ); 200 | } 201 | 202 | Maf.parabola = function( x, k ) { 203 | return Math.pow( 4 * x * ( 1 - x ), k ); 204 | } 205 | 206 | Maf.powerCurve = function( x, a, b ) { 207 | var k = Math.pow( a + b, a + b ) / ( Math.pow( a, a ) * Math.pow( b, b ) ); 208 | return k * Math.pow( x, a ) * Math.pow( 1 - x, b ); 209 | } 210 | 211 | // http://iquilezles.org/www/articles/smin/smin.htm ? 212 | 213 | Maf.latLonToCartesian = function( lat, lon ) { 214 | 215 | lon += 180; 216 | lat = Maf.clamp( lat, -85, 85 ); 217 | var phi = Maf.toRadians( 90 - lat ); 218 | var theta = Maf.toRadians( 180 - lon ); 219 | var x = Math.sin( phi ) * Math.cos( theta ); 220 | var y = Math.cos( phi ); 221 | var z = Math.sin( phi ) * Math.sin( theta ); 222 | 223 | return { x: x, y: y, z: z } 224 | 225 | } 226 | 227 | Maf.cartesianToLatLon = function( x, y, z ) { 228 | var n = Math.sqrt( x * x + y * y + z * z ); 229 | return{ lat: Math.asin( z / n ), lon: Math.atan2( y, x ) }; 230 | } 231 | 232 | Maf.randomInRange = function( min, max ) { 233 | return min + Math.random() * ( max - min ); 234 | } 235 | 236 | Maf.norm = function( v, minVal, maxVal ) { 237 | return ( v - minVal ) / ( maxVal - minVal ); 238 | } 239 | 240 | Maf.hash = function( n ) { 241 | return Maf.fract( (1.0 + Math.cos(n)) * 415.92653); 242 | } 243 | 244 | Maf.noise2d = function( x, y ) { 245 | var xhash = Maf.hash( x * 37.0 ); 246 | var yhash = Maf.hash( y * 57.0 ); 247 | return Maf.fract( xhash + yhash ); 248 | } 249 | 250 | // http://iquilezles.org/www/articles/smin/smin.htm 251 | 252 | Maf.smoothMin = function( a, b, k ) { 253 | var res = Math.exp( -k*a ) + Math.exp( -k*b ); 254 | return - Math.log( res )/k; 255 | } 256 | 257 | Maf.smoothMax = function( a, b, k ){ 258 | return Math.log( Math.exp(a) + Math.exp(b) )/k; 259 | } 260 | 261 | Maf.almost = function( a, b ) { 262 | return ( Math.abs( a - b ) < .0001 ); 263 | } 264 | 265 | }()); -------------------------------------------------------------------------------- /js/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 | this.materials = null; 10 | 11 | }; 12 | 13 | THREE.OBJLoader.prototype = { 14 | 15 | constructor: THREE.OBJLoader, 16 | 17 | load: function ( url, onLoad, onProgress, onError ) { 18 | 19 | var scope = this; 20 | 21 | var loader = new THREE.XHRLoader( scope.manager ); 22 | loader.setPath( this.path ); 23 | loader.load( url, function ( text ) { 24 | 25 | onLoad( scope.parse( text ) ); 26 | 27 | }, onProgress, onError ); 28 | 29 | }, 30 | 31 | setPath: function ( value ) { 32 | 33 | this.path = value; 34 | 35 | }, 36 | 37 | setMaterials: function ( materials ) { 38 | 39 | this.materials = materials; 40 | 41 | }, 42 | 43 | parse: function ( text ) { 44 | 45 | console.time( 'OBJLoader' ); 46 | 47 | var objects = []; 48 | var object; 49 | var foundObjects = false; 50 | var vertices = []; 51 | var normals = []; 52 | var uvs = []; 53 | 54 | function addObject( name ) { 55 | 56 | var geometry = { 57 | vertices: [], 58 | normals: [], 59 | uvs: [] 60 | }; 61 | 62 | var material = { 63 | name: '', 64 | smooth: true 65 | }; 66 | 67 | object = { 68 | name: name, 69 | geometry: geometry, 70 | material: material 71 | }; 72 | 73 | objects.push( object ); 74 | 75 | } 76 | 77 | function parseVertexIndex( value ) { 78 | 79 | var index = parseInt( value ); 80 | 81 | return ( index >= 0 ? index - 1 : index + vertices.length / 3 ) * 3; 82 | 83 | } 84 | 85 | function parseNormalIndex( value ) { 86 | 87 | var index = parseInt( value ); 88 | 89 | return ( index >= 0 ? index - 1 : index + normals.length / 3 ) * 3; 90 | 91 | } 92 | 93 | function parseUVIndex( value ) { 94 | 95 | var index = parseInt( value ); 96 | 97 | return ( index >= 0 ? index - 1 : index + uvs.length / 2 ) * 2; 98 | 99 | } 100 | 101 | function addVertex( a, b, c ) { 102 | 103 | object.geometry.vertices.push( 104 | vertices[ a ], vertices[ a + 1 ], vertices[ a + 2 ], 105 | vertices[ b ], vertices[ b + 1 ], vertices[ b + 2 ], 106 | vertices[ c ], vertices[ c + 1 ], vertices[ c + 2 ] 107 | ); 108 | 109 | } 110 | 111 | function addNormal( a, b, c ) { 112 | 113 | object.geometry.normals.push( 114 | normals[ a ], normals[ a + 1 ], normals[ a + 2 ], 115 | normals[ b ], normals[ b + 1 ], normals[ b + 2 ], 116 | normals[ c ], normals[ c + 1 ], normals[ c + 2 ] 117 | ); 118 | 119 | } 120 | 121 | function addUV( a, b, c ) { 122 | 123 | object.geometry.uvs.push( 124 | uvs[ a ], uvs[ a + 1 ], 125 | uvs[ b ], uvs[ b + 1 ], 126 | uvs[ c ], uvs[ c + 1 ] 127 | ); 128 | 129 | } 130 | 131 | function addFace( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { 132 | 133 | var ia = parseVertexIndex( a ); 134 | var ib = parseVertexIndex( b ); 135 | var ic = parseVertexIndex( c ); 136 | var id; 137 | 138 | if ( d === undefined ) { 139 | 140 | addVertex( ia, ib, ic ); 141 | 142 | } else { 143 | 144 | id = parseVertexIndex( d ); 145 | 146 | addVertex( ia, ib, id ); 147 | addVertex( ib, ic, id ); 148 | 149 | } 150 | 151 | if ( ua !== undefined ) { 152 | 153 | ia = parseUVIndex( ua ); 154 | ib = parseUVIndex( ub ); 155 | ic = parseUVIndex( uc ); 156 | 157 | if ( d === undefined ) { 158 | 159 | addUV( ia, ib, ic ); 160 | 161 | } else { 162 | 163 | id = parseUVIndex( ud ); 164 | 165 | addUV( ia, ib, id ); 166 | addUV( ib, ic, id ); 167 | 168 | } 169 | 170 | } 171 | 172 | if ( na !== undefined ) { 173 | 174 | ia = parseNormalIndex( na ); 175 | ib = parseNormalIndex( nb ); 176 | ic = parseNormalIndex( nc ); 177 | 178 | if ( d === undefined ) { 179 | 180 | addNormal( ia, ib, ic ); 181 | 182 | } else { 183 | 184 | id = parseNormalIndex( nd ); 185 | 186 | addNormal( ia, ib, id ); 187 | addNormal( ib, ic, id ); 188 | 189 | } 190 | 191 | } 192 | 193 | } 194 | 195 | addObject( '' ); 196 | 197 | // v float float float 198 | var vertex_pattern = /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/; 199 | 200 | // vn float float float 201 | var normal_pattern = /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/; 202 | 203 | // vt float float 204 | var uv_pattern = /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/; 205 | 206 | // f vertex vertex vertex ... 207 | var face_pattern1 = /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/; 208 | 209 | // f vertex/uv vertex/uv vertex/uv ... 210 | var face_pattern2 = /^f\s+((-?\d+)\/(-?\d+))\s+((-?\d+)\/(-?\d+))\s+((-?\d+)\/(-?\d+))(?:\s+((-?\d+)\/(-?\d+)))?/; 211 | 212 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ... 213 | var face_pattern3 = /^f\s+((-?\d+)\/(-?\d+)\/(-?\d+))\s+((-?\d+)\/(-?\d+)\/(-?\d+))\s+((-?\d+)\/(-?\d+)\/(-?\d+))(?:\s+((-?\d+)\/(-?\d+)\/(-?\d+)))?/; 214 | 215 | // f vertex//normal vertex//normal vertex//normal ... 216 | var face_pattern4 = /^f\s+((-?\d+)\/\/(-?\d+))\s+((-?\d+)\/\/(-?\d+))\s+((-?\d+)\/\/(-?\d+))(?:\s+((-?\d+)\/\/(-?\d+)))?/; 217 | 218 | var object_pattern = /^[og]\s+(.+)/; 219 | 220 | var smoothing_pattern = /^s\s+(\d+|on|off)/; 221 | 222 | // 223 | 224 | var lines = text.split( '\n' ); 225 | 226 | for ( var i = 0; i < lines.length; i ++ ) { 227 | 228 | var line = lines[ i ]; 229 | line = line.trim(); 230 | 231 | var result; 232 | 233 | if ( line.length === 0 || line.charAt( 0 ) === '#' ) { 234 | 235 | continue; 236 | 237 | } else if ( ( result = vertex_pattern.exec( line ) ) !== null ) { 238 | 239 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 240 | 241 | vertices.push( 242 | parseFloat( result[ 1 ] ), 243 | parseFloat( result[ 2 ] ), 244 | parseFloat( result[ 3 ] ) 245 | ); 246 | 247 | } else if ( ( result = normal_pattern.exec( line ) ) !== null ) { 248 | 249 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 250 | 251 | normals.push( 252 | parseFloat( result[ 1 ] ), 253 | parseFloat( result[ 2 ] ), 254 | parseFloat( result[ 3 ] ) 255 | ); 256 | 257 | } else if ( ( result = uv_pattern.exec( line ) ) !== null ) { 258 | 259 | // ["vt 0.1 0.2", "0.1", "0.2"] 260 | 261 | uvs.push( 262 | parseFloat( result[ 1 ] ), 263 | parseFloat( result[ 2 ] ) 264 | ); 265 | 266 | } else if ( ( result = face_pattern1.exec( line ) ) !== null ) { 267 | 268 | // ["f 1 2 3", "1", "2", "3", undefined] 269 | 270 | addFace( 271 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] 272 | ); 273 | 274 | } else if ( ( result = face_pattern2.exec( line ) ) !== null ) { 275 | 276 | // ["f 1/1 2/2 3/3", " 1/1", "1", "1", " 2/2", "2", "2", " 3/3", "3", "3", undefined, undefined, undefined] 277 | 278 | addFace( 279 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 280 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 281 | ); 282 | 283 | } else if ( ( result = face_pattern3.exec( line ) ) !== null ) { 284 | 285 | // ["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] 286 | 287 | addFace( 288 | result[ 2 ], result[ 6 ], result[ 10 ], result[ 14 ], 289 | result[ 3 ], result[ 7 ], result[ 11 ], result[ 15 ], 290 | result[ 4 ], result[ 8 ], result[ 12 ], result[ 16 ] 291 | ); 292 | 293 | } else if ( ( result = face_pattern4.exec( line ) ) !== null ) { 294 | 295 | // ["f 1//1 2//2 3//3", " 1//1", "1", "1", " 2//2", "2", "2", " 3//3", "3", "3", undefined, undefined, undefined] 296 | 297 | addFace( 298 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 299 | undefined, undefined, undefined, undefined, 300 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 301 | ); 302 | 303 | } else if ( ( result = object_pattern.exec( line ) ) !== null ) { 304 | 305 | // o object_name 306 | // or 307 | // g group_name 308 | 309 | var name = result[ 1 ].trim(); 310 | 311 | if ( foundObjects === false ) { 312 | 313 | foundObjects = true; 314 | object.name = name; 315 | 316 | } else { 317 | 318 | addObject( name ); 319 | 320 | } 321 | 322 | } else if ( /^usemtl /.test( line ) ) { 323 | 324 | // material 325 | 326 | object.material.name = line.substring( 7 ).trim(); 327 | 328 | } else if ( /^mtllib /.test( line ) ) { 329 | 330 | // mtl file 331 | 332 | } else if ( ( result = smoothing_pattern.exec( line ) ) !== null ) { 333 | 334 | // smooth shading 335 | 336 | object.material.smooth = result[ 1 ] === "1" || result[ 1 ] === "on"; 337 | 338 | } else { 339 | 340 | throw new Error( "Unexpected line: " + line ); 341 | 342 | } 343 | 344 | } 345 | 346 | var container = new THREE.Group(); 347 | 348 | for ( var i = 0, l = objects.length; i < l; i ++ ) { 349 | 350 | object = objects[ i ]; 351 | var geometry = object.geometry; 352 | 353 | var buffergeometry = new THREE.BufferGeometry(); 354 | 355 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); 356 | 357 | if ( geometry.normals.length > 0 ) { 358 | 359 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); 360 | 361 | } else { 362 | 363 | buffergeometry.computeVertexNormals(); 364 | 365 | } 366 | 367 | if ( geometry.uvs.length > 0 ) { 368 | 369 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); 370 | 371 | } 372 | 373 | var material; 374 | 375 | if ( this.materials !== null ) { 376 | 377 | material = this.materials.create( object.material.name ); 378 | 379 | } 380 | 381 | if ( !material ) { 382 | 383 | material = new THREE.MeshPhongMaterial(); 384 | material.name = object.material.name; 385 | 386 | } 387 | 388 | material.shading = object.material.smooth ? THREE.SmoothShading : THREE.FlatShading; 389 | 390 | var mesh = new THREE.Mesh( buffergeometry, material ); 391 | mesh.name = object.name; 392 | 393 | container.add( mesh ); 394 | 395 | } 396 | 397 | console.timeEnd( 'OBJLoader' ); 398 | 399 | return container; 400 | 401 | } 402 | 403 | }; -------------------------------------------------------------------------------- /js/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 | // Set to true to disable use of the keys 68 | this.noKeys = false; 69 | 70 | // The four arrow keys 71 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 72 | 73 | //////////// 74 | // internals 75 | 76 | var scope = this; 77 | 78 | var EPS = 0.000001; 79 | 80 | var rotateStart = new THREE.Vector2(); 81 | var rotateEnd = new THREE.Vector2(); 82 | var rotateDelta = new THREE.Vector2(); 83 | 84 | var panStart = new THREE.Vector2(); 85 | var panEnd = new THREE.Vector2(); 86 | var panDelta = new THREE.Vector2(); 87 | var panOffset = new THREE.Vector3(); 88 | 89 | var offset = new THREE.Vector3(); 90 | 91 | var dollyStart = new THREE.Vector2(); 92 | var dollyEnd = new THREE.Vector2(); 93 | var dollyDelta = new THREE.Vector2(); 94 | 95 | var phiDelta = 0; 96 | var thetaDelta = 0; 97 | var scale = 1; 98 | var pan = new THREE.Vector3(); 99 | 100 | var lastPosition = new THREE.Vector3(); 101 | 102 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 103 | 104 | var state = STATE.NONE; 105 | 106 | // for reset 107 | 108 | this.target0 = this.target.clone(); 109 | this.position0 = this.object.position.clone(); 110 | 111 | // so camera.up is the orbit axis 112 | 113 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 114 | var quatInverse = quat.clone().inverse(); 115 | 116 | // events 117 | 118 | var changeEvent = { type: 'change' }; 119 | var startEvent = { type: 'start'}; 120 | var endEvent = { type: 'end'}; 121 | 122 | this.rotateLeft = function ( angle ) { 123 | 124 | if ( angle === undefined ) { 125 | 126 | angle = getAutoRotationAngle(); 127 | 128 | } 129 | 130 | thetaDelta -= angle; 131 | 132 | }; 133 | 134 | this.rotateUp = function ( angle ) { 135 | 136 | if ( angle === undefined ) { 137 | 138 | angle = getAutoRotationAngle(); 139 | 140 | } 141 | 142 | phiDelta -= angle; 143 | 144 | }; 145 | 146 | // pass in distance in world space to move left 147 | this.panLeft = function ( distance ) { 148 | 149 | var te = this.object.matrix.elements; 150 | 151 | // get X column of matrix 152 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 153 | panOffset.multiplyScalar( - distance ); 154 | 155 | pan.add( panOffset ); 156 | 157 | }; 158 | 159 | // pass in distance in world space to move up 160 | this.panUp = function ( distance ) { 161 | 162 | var te = this.object.matrix.elements; 163 | 164 | // get Y column of matrix 165 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 166 | panOffset.multiplyScalar( distance ); 167 | 168 | pan.add( panOffset ); 169 | 170 | }; 171 | 172 | // pass in x,y of change desired in pixel space, 173 | // right and down are positive 174 | this.pan = function ( deltaX, deltaY ) { 175 | 176 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 177 | 178 | if ( scope.object.fov !== undefined ) { 179 | 180 | // perspective 181 | var position = scope.object.position; 182 | var offset = position.clone().sub( scope.target ); 183 | var targetDistance = offset.length(); 184 | 185 | // half of the fov is center to top of screen 186 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 187 | 188 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 189 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 190 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 191 | 192 | } else if ( scope.object.top !== undefined ) { 193 | 194 | // orthographic 195 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 196 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 197 | 198 | } else { 199 | 200 | // camera neither orthographic or perspective 201 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 202 | 203 | } 204 | 205 | }; 206 | 207 | this.dollyIn = function ( dollyScale ) { 208 | 209 | if ( dollyScale === undefined ) { 210 | 211 | dollyScale = getZoomScale(); 212 | 213 | } 214 | 215 | scale /= dollyScale; 216 | 217 | }; 218 | 219 | this.dollyOut = function ( dollyScale ) { 220 | 221 | if ( dollyScale === undefined ) { 222 | 223 | dollyScale = getZoomScale(); 224 | 225 | } 226 | 227 | scale *= dollyScale; 228 | 229 | }; 230 | 231 | this.update = function () { 232 | 233 | var position = this.object.position; 234 | 235 | offset.copy( position ).sub( this.target ); 236 | 237 | // rotate offset to "y-axis-is-up" space 238 | offset.applyQuaternion( quat ); 239 | 240 | // angle from z-axis around y-axis 241 | 242 | var theta = Math.atan2( offset.x, offset.z ); 243 | 244 | // angle from y-axis 245 | 246 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 247 | 248 | if ( this.autoRotate ) { 249 | 250 | this.rotateLeft( getAutoRotationAngle() ); 251 | 252 | } 253 | 254 | theta += thetaDelta; 255 | phi += phiDelta; 256 | 257 | // restrict phi to be between desired limits 258 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 259 | 260 | // restrict phi to be betwee EPS and PI-EPS 261 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 262 | 263 | var radius = offset.length() * scale; 264 | 265 | // restrict radius to be between desired limits 266 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 267 | 268 | // move target to panned location 269 | this.target.add( pan ); 270 | 271 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 272 | offset.y = radius * Math.cos( phi ); 273 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 274 | 275 | // rotate offset back to "camera-up-vector-is-up" space 276 | offset.applyQuaternion( quatInverse ); 277 | 278 | position.copy( this.target ).add( offset ); 279 | 280 | this.object.lookAt( this.target ); 281 | 282 | thetaDelta = 0; 283 | phiDelta = 0; 284 | scale = 1; 285 | pan.set( 0, 0, 0 ); 286 | 287 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS ) { 288 | 289 | this.dispatchEvent( changeEvent ); 290 | 291 | lastPosition.copy( this.object.position ); 292 | 293 | } 294 | 295 | }; 296 | 297 | 298 | this.reset = function () { 299 | 300 | state = STATE.NONE; 301 | 302 | this.target.copy( this.target0 ); 303 | this.object.position.copy( this.position0 ); 304 | 305 | this.update(); 306 | 307 | }; 308 | 309 | function getAutoRotationAngle() { 310 | 311 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 312 | 313 | } 314 | 315 | function getZoomScale() { 316 | 317 | return Math.pow( 0.95, scope.zoomSpeed ); 318 | 319 | } 320 | 321 | function onMouseDown( event ) { 322 | 323 | if ( scope.enabled === false ) return; 324 | event.preventDefault(); 325 | 326 | if ( event.button === 0 ) { 327 | if ( scope.noRotate === true ) return; 328 | 329 | state = STATE.ROTATE; 330 | 331 | rotateStart.set( event.clientX, event.clientY ); 332 | 333 | } else if ( event.button === 1 ) { 334 | if ( scope.noZoom === true ) return; 335 | 336 | state = STATE.DOLLY; 337 | 338 | dollyStart.set( event.clientX, event.clientY ); 339 | 340 | } else if ( event.button === 2 ) { 341 | if ( scope.noPan === true ) return; 342 | 343 | state = STATE.PAN; 344 | 345 | panStart.set( event.clientX, event.clientY ); 346 | 347 | } 348 | 349 | scope.domElement.addEventListener( 'mousemove', onMouseMove, false ); 350 | scope.domElement.addEventListener( 'mouseup', onMouseUp, false ); 351 | scope.dispatchEvent( startEvent ); 352 | 353 | } 354 | 355 | function onMouseMove( event ) { 356 | 357 | if ( scope.enabled === false ) return; 358 | 359 | event.preventDefault(); 360 | 361 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 362 | 363 | if ( state === STATE.ROTATE ) { 364 | 365 | if ( scope.noRotate === true ) return; 366 | 367 | rotateEnd.set( event.clientX, event.clientY ); 368 | rotateDelta.subVectors( rotateEnd, rotateStart ); 369 | 370 | // rotating across whole screen goes 360 degrees around 371 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 372 | 373 | // rotating up and down along whole screen attempts to go 360, but limited to 180 374 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 375 | 376 | rotateStart.copy( rotateEnd ); 377 | 378 | } else if ( state === STATE.DOLLY ) { 379 | 380 | if ( scope.noZoom === true ) return; 381 | 382 | dollyEnd.set( event.clientX, event.clientY ); 383 | dollyDelta.subVectors( dollyEnd, dollyStart ); 384 | 385 | if ( dollyDelta.y > 0 ) { 386 | 387 | scope.dollyIn(); 388 | 389 | } else { 390 | 391 | scope.dollyOut(); 392 | 393 | } 394 | 395 | dollyStart.copy( dollyEnd ); 396 | 397 | } else if ( state === STATE.PAN ) { 398 | 399 | if ( scope.noPan === true ) return; 400 | 401 | panEnd.set( event.clientX, event.clientY ); 402 | panDelta.subVectors( panEnd, panStart ); 403 | 404 | scope.pan( panDelta.x, panDelta.y ); 405 | 406 | panStart.copy( panEnd ); 407 | 408 | } 409 | 410 | scope.update(); 411 | 412 | } 413 | 414 | function onMouseUp( /* event */ ) { 415 | 416 | if ( scope.enabled === false ) return; 417 | 418 | scope.domElement.removeEventListener( 'mousemove', onMouseMove, false ); 419 | scope.domElement.removeEventListener( 'mouseup', onMouseUp, false ); 420 | scope.dispatchEvent( endEvent ); 421 | state = STATE.NONE; 422 | 423 | } 424 | 425 | function onMouseWheel( event ) { 426 | 427 | if ( scope.enabled === false || scope.noZoom === true ) return; 428 | 429 | event.preventDefault(); 430 | event.stopPropagation(); 431 | 432 | var delta = 0; 433 | 434 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 435 | 436 | delta = event.wheelDelta; 437 | 438 | } else if ( event.detail !== undefined ) { // Firefox 439 | 440 | delta = - event.detail; 441 | 442 | } 443 | 444 | if ( delta > 0 ) { 445 | 446 | scope.dollyOut(); 447 | 448 | } else { 449 | 450 | scope.dollyIn(); 451 | 452 | } 453 | 454 | scope.update(); 455 | scope.dispatchEvent( startEvent ); 456 | scope.dispatchEvent( changeEvent ); 457 | scope.dispatchEvent( endEvent ); 458 | 459 | } 460 | 461 | function onKeyDown( event ) { 462 | 463 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 464 | 465 | switch ( event.keyCode ) { 466 | 467 | case scope.keys.UP: 468 | scope.pan( 0, scope.keyPanSpeed ); 469 | scope.update(); 470 | break; 471 | 472 | case scope.keys.BOTTOM: 473 | scope.pan( 0, - scope.keyPanSpeed ); 474 | scope.update(); 475 | break; 476 | 477 | case scope.keys.LEFT: 478 | scope.pan( scope.keyPanSpeed, 0 ); 479 | scope.update(); 480 | break; 481 | 482 | case scope.keys.RIGHT: 483 | scope.pan( - scope.keyPanSpeed, 0 ); 484 | scope.update(); 485 | break; 486 | 487 | } 488 | 489 | } 490 | 491 | function touchstart( event ) { 492 | 493 | if ( scope.enabled === false ) return; 494 | 495 | switch ( event.touches.length ) { 496 | 497 | case 1: // one-fingered touch: rotate 498 | 499 | if ( scope.noRotate === true ) return; 500 | 501 | state = STATE.TOUCH_ROTATE; 502 | 503 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 504 | break; 505 | 506 | case 2: // two-fingered touch: dolly 507 | 508 | if ( scope.noZoom === true ) return; 509 | 510 | state = STATE.TOUCH_DOLLY; 511 | 512 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 513 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 514 | var distance = Math.sqrt( dx * dx + dy * dy ); 515 | dollyStart.set( 0, distance ); 516 | break; 517 | 518 | case 3: // three-fingered touch: pan 519 | 520 | if ( scope.noPan === true ) return; 521 | 522 | state = STATE.TOUCH_PAN; 523 | 524 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 525 | break; 526 | 527 | default: 528 | 529 | state = STATE.NONE; 530 | 531 | } 532 | 533 | scope.dispatchEvent( startEvent ); 534 | 535 | } 536 | 537 | function touchmove( event ) { 538 | 539 | if ( scope.enabled === false ) return; 540 | 541 | event.preventDefault(); 542 | event.stopPropagation(); 543 | 544 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 545 | 546 | switch ( event.touches.length ) { 547 | 548 | case 1: // one-fingered touch: rotate 549 | 550 | if ( scope.noRotate === true ) return; 551 | if ( state !== STATE.TOUCH_ROTATE ) return; 552 | 553 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 554 | rotateDelta.subVectors( rotateEnd, rotateStart ); 555 | 556 | // rotating across whole screen goes 360 degrees around 557 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 558 | // rotating up and down along whole screen attempts to go 360, but limited to 180 559 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 560 | 561 | rotateStart.copy( rotateEnd ); 562 | 563 | scope.update(); 564 | break; 565 | 566 | case 2: // two-fingered touch: dolly 567 | 568 | if ( scope.noZoom === true ) return; 569 | if ( state !== STATE.TOUCH_DOLLY ) return; 570 | 571 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 572 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 573 | var distance = Math.sqrt( dx * dx + dy * dy ); 574 | 575 | dollyEnd.set( 0, distance ); 576 | dollyDelta.subVectors( dollyEnd, dollyStart ); 577 | 578 | if ( dollyDelta.y > 0 ) { 579 | 580 | scope.dollyOut(); 581 | 582 | } else { 583 | 584 | scope.dollyIn(); 585 | 586 | } 587 | 588 | dollyStart.copy( dollyEnd ); 589 | 590 | scope.update(); 591 | break; 592 | 593 | case 3: // three-fingered touch: pan 594 | 595 | if ( scope.noPan === true ) return; 596 | if ( state !== STATE.TOUCH_PAN ) return; 597 | 598 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 599 | panDelta.subVectors( panEnd, panStart ); 600 | 601 | scope.pan( panDelta.x, panDelta.y ); 602 | 603 | panStart.copy( panEnd ); 604 | 605 | scope.update(); 606 | break; 607 | 608 | default: 609 | 610 | state = STATE.NONE; 611 | 612 | } 613 | 614 | } 615 | 616 | function touchend( /* event */ ) { 617 | 618 | if ( scope.enabled === false ) return; 619 | 620 | scope.dispatchEvent( endEvent ); 621 | state = STATE.NONE; 622 | 623 | } 624 | 625 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 626 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 627 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 628 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 629 | 630 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 631 | this.domElement.addEventListener( 'touchend', touchend, false ); 632 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 633 | 634 | window.addEventListener( 'keydown', onKeyDown, false ); 635 | 636 | // force an update at start 637 | this.update(); 638 | 639 | }; 640 | 641 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); -------------------------------------------------------------------------------- /js/THREE.FBOHelper.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | 3 | "use strict"; 4 | 5 | var root = this 6 | 7 | var has_require = typeof require !== 'undefined' 8 | 9 | var THREE = root.THREE || has_require && require('three') 10 | if( !THREE ) 11 | throw new Error( 'FBOHelper requires three.js' ) 12 | 13 | "use strict"; 14 | 15 | var layerCSS = ` 16 | #fboh-fbos-list{ 17 | all: unset; 18 | position: fixed; 19 | left: 0; 20 | top: 0; 21 | z-index: 1000000; 22 | width: 150px; 23 | } 24 | #fboh-fbos-list, #fboh-fbos-list *, #fboh-hotspot, #fboh-label, #fboh-info{ 25 | box-sizing: border-box; 26 | font-family: 'Roboto Mono', 'courier new', courier, monospace; 27 | font-size: 11px; 28 | line-height: 1.4em; 29 | } 30 | #fboh-fbos-list li{ 31 | cursor: pointer; 32 | color: white; 33 | width: 100%; 34 | padding: 4px 0; 35 | border-top: 1px solid #888; 36 | border-bottom: 1px solid black; 37 | background-color: #444; 38 | text-align: center; 39 | text-shadow: 0 -1px black; 40 | } 41 | #fboh-fbos-list li:hover{ 42 | background-color: rgba( 158, 253, 56, .5 ); 43 | } 44 | #fboh-fbos-list li.active{ 45 | background-color: rgba( 158, 253, 56, .5 ); 46 | color: white; 47 | text-shadow: 0 1px black; 48 | } 49 | #fboh-hotspot{ 50 | position: absolute; 51 | left: 0; 52 | top: 0; 53 | background-color: rgba( 158, 253, 56,.5); 54 | pointer-events: none; 55 | } 56 | #fboh-label{ 57 | position: absolute; 58 | left: 0; 59 | bottom: 0; 60 | transform-origin: bottom left; 61 | pointer-events: none; 62 | } 63 | #fboh-info{ 64 | display: none; 65 | position: absolute; 66 | left: 160px; 67 | top: 10px; 68 | pointer-events: none; 69 | } 70 | .fboh-card{ 71 | display: block; 72 | white-space: nowrap; 73 | color: black; 74 | padding: 10px; 75 | background-color: white; 76 | border: 1px solid black; 77 | } 78 | `; 79 | 80 | let formats = {} 81 | formats[ THREE.AlphaFormat ] = 'THREE.AlphaFormat'; 82 | formats[ THREE.RGBFormat ] = 'THREE.RGBFormat'; 83 | formats[ THREE.RGBAFormat ] = 'THREE.RGBAFormat'; 84 | formats[ THREE.LuminanceFormat ] = 'THREE.LuminanceFormat'; 85 | formats[ THREE.LuminanceAlphaFormat ] = 'THREE.LuminanceAlphaFormat'; 86 | //formats[ THREE.RGBEFormat ] = 'THREE.RGBEFormat'; 87 | 88 | let types = {} 89 | types[ THREE.UnsignedByteType ] = 'THREE.UnsignedByteType'; 90 | types[ THREE.ByteType ] = 'THREE.ByteType'; 91 | types[ THREE.ShortType ] = 'THREE.ShortType'; 92 | types[ THREE.UnsignedShortType ] = 'THREE.UnsignedShortType'; 93 | types[ THREE.IntType ] = 'THREE.IntType'; 94 | types[ THREE.UnsignedIntType ] = 'THREE.UnsignedIntType'; 95 | types[ THREE.FloatType ] = 'THREE.FloatType'; 96 | types[ THREE.HalfFloatType ] = 'THREE.HalfFloatType'; 97 | types[ THREE.UnsignedShort4444Type ] = 'THREE.UnsignedShort4444Type'; 98 | types[ THREE.UnsignedShort5551Type ] = 'THREE.UnsignedShort5551Type'; 99 | types[ THREE.UnsignedShort565Type ] = 'THREE.UnsignedShort565Type'; 100 | 101 | class FBOHelper { 102 | 103 | constructor( renderer ) { 104 | 105 | this.renderer = renderer; 106 | this.autoUpdate = false; 107 | this.fbos = [] 108 | this.list = document.createElement( 'ul' ); 109 | this.list.setAttribute( 'id', 'fboh-fbos-list' ); 110 | document.body.appendChild( this.list ); 111 | 112 | this.scene = new THREE.Scene(); 113 | this.camera = new THREE.OrthographicCamera( -1, 1, 1, -1, .000001, 1000 ); 114 | 115 | this.raycaster = new THREE.Raycaster(); 116 | this.mouse = new THREE.Vector2(); 117 | 118 | this.grid = document.createElement( 'div' ); 119 | this.grid.setAttribute( 'style', 'position: fixed; left: 50%; top: 50%; border: 1px solid #000000; transform: translate3d(-50%, -50%, 0 ); box-shadow: 0 0 50px black; display: none' ); 120 | this.grid.setAttribute( 'id', 'bfoh-grid' ); 121 | document.body.appendChild( this.grid ); 122 | 123 | this.hotspot = document.createElement( 'div' ); 124 | this.hotspot.setAttribute( 'id', 'fboh-hotspot' ); 125 | this.grid.appendChild( this.hotspot ); 126 | 127 | this.label = document.createElement( 'div' ); 128 | this.label.setAttribute( 'id', 'fboh-label' ); 129 | this.label.className = 'fboh-card'; 130 | this.hotspot.appendChild( this.label ); 131 | 132 | this.info = document.createElement( 'div' ); 133 | this.info.setAttribute( 'id', 'fboh-info' ); 134 | this.info.className = 'fboh-card'; 135 | document.body.appendChild( this.info ); 136 | 137 | this.currentObj = null; 138 | this.currentU = 0; 139 | this.currentV = 0; 140 | 141 | this.fboMap = new Map(); 142 | 143 | this.offsetX = 0; 144 | this.offsetY = 0; 145 | 146 | this.grid.appendChild( this.hotspot ); 147 | 148 | const head = window.document.head || window.document.getElementsByTagName('head')[0]; 149 | const style = window.document.createElement('style'); 150 | 151 | style.type = 'text/css'; 152 | if (style.styleSheet){ 153 | style.styleSheet.cssText = layerCSS; 154 | } else { 155 | style.appendChild(document.createTextNode(layerCSS)); 156 | } 157 | 158 | head.appendChild(style); 159 | 160 | const ss = document.createElement( 'link' ); 161 | ss.type = 'text/css'; 162 | ss.rel = 'stylesheet'; 163 | ss.href = 'https://fonts.googleapis.com/css?family=Roboto+Mono'; 164 | 165 | head.appendChild( ss ); 166 | 167 | this.grid.addEventListener( 'wheel', e => { 168 | 169 | var direction = ( e.deltaY < 0 ) ? 1 : -1; 170 | 171 | this.camera.zoom += direction / 50; 172 | this.camera.updateProjectionMatrix(); 173 | this.grid.style.transform = `translate3d(-50%, -50%, 0 ) scale(${this.camera.zoom},${this.camera.zoom}) translate3d(${this.offsetX}px,${this.offsetY}px,0) `; 174 | this.label.style.transform = `scale(${1/this.camera.zoom},${1/this.camera.zoom})`; 175 | this.hotspot.style.transform = `scale(${1/this.camera.zoom},${1/this.camera.zoom})`; 176 | this.hotspot.style.borderWidth = `${1/this.camera.zoom}px`; 177 | this.readPixel( this.currentObj, this.currentU, this.currentV ); 178 | 179 | } ); 180 | 181 | let dragging = false; 182 | let mouseStart = { x: 0, y: 0 }; 183 | let offsetStart = { x: 0, y: 0 }; 184 | 185 | this.grid.addEventListener( 'mousedown', e => { 186 | 187 | dragging = true; 188 | mouseStart.x = e.clientX; 189 | mouseStart.y = e.clientY; 190 | offsetStart.x = this.offsetX; 191 | offsetStart.y = this.offsetY; 192 | 193 | } ); 194 | 195 | this.grid.addEventListener( 'mouseup', e => { 196 | 197 | dragging = false; 198 | 199 | } ); 200 | 201 | this.grid.addEventListener( 'mouseout', e => { 202 | 203 | this.label.style.display = 'none'; 204 | dragging = false; 205 | 206 | } ); 207 | 208 | this.grid.addEventListener( 'mouseover', e => { 209 | 210 | this.label.style.display = 'block'; 211 | 212 | } ); 213 | 214 | this.grid.addEventListener( 'mousemove', e => { 215 | 216 | if( dragging ) { 217 | 218 | this.offsetX = offsetStart.x + ( e.clientX - mouseStart.x ) / this.camera.zoom; 219 | this.offsetY = offsetStart.y + ( e.clientY - mouseStart.y ) / this.camera.zoom; 220 | this.camera.position.x = -this.offsetX; 221 | this.camera.position.y = this.offsetY; 222 | 223 | this.grid.style.transform = `translate3d(-50%, -50%, 0 ) scale(${this.camera.zoom},${this.camera.zoom}) translate3d(${this.offsetX}px,${this.offsetY}px,0)`; 224 | 225 | } else { 226 | 227 | this.mouse.x = ( e.clientX / renderer.domElement.clientWidth ) * 2 - 1; 228 | this.mouse.y = - ( e.clientY / renderer.domElement.clientHeight ) * 2 + 1; 229 | this.raycaster.setFromCamera( this.mouse, this.camera ); 230 | 231 | const intersects = this.raycaster.intersectObject( this.currentObj.quad, true ); 232 | 233 | if ( intersects.length > 0 ) { 234 | 235 | this.readPixel( this.fboMap.get( intersects[ 0 ].object ), intersects[ 0 ].uv.x, intersects[ 0 ].uv.y ); 236 | this.label.style.display = 'block'; 237 | 238 | } else { 239 | 240 | this.label.style.display = 'none'; 241 | 242 | } 243 | 244 | } 245 | 246 | } ); 247 | 248 | window.addEventListener( 'keydown', e => { 249 | if( e.keyCode === 27 ) { 250 | this.hide(); 251 | } 252 | } ); 253 | 254 | this.grid.addEventListener( 'keydown', e => { 255 | if( e.keyCode === 27 ) { 256 | this.hide(); 257 | } 258 | } ); 259 | 260 | } 261 | 262 | hide() { 263 | 264 | this.hideAll(); 265 | this.info.style.display = 'none'; 266 | this.grid.style.display = 'none'; 267 | this.currentObj = null; 268 | 269 | } 270 | 271 | attach( fbo, name, formatter ) { 272 | 273 | var li = document.createElement( 'li' ); 274 | 275 | li.textContent = name; 276 | 277 | if( fbo.image ) { 278 | fbo.width = fbo.image.width; 279 | fbo.height = fbo.image.height; 280 | } 281 | 282 | const width = 600; 283 | const height = fbo.height * width / fbo.width; 284 | 285 | const material = new THREE.MeshBasicMaterial( { map: fbo, side: THREE.DoubleSide } ); 286 | const quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 1, 1 ), material ); 287 | if( !fbo.flipY ) quad.rotation.x = Math.PI; 288 | quad.visible = false; 289 | quad.width = width; 290 | quad.height = height; 291 | quad.scale.set( width, height, 1. ); 292 | this.scene.add( quad ); 293 | 294 | var fboData = { 295 | width: width, 296 | height: height, 297 | name: name, 298 | fbo: fbo, 299 | flipY: fbo.flipY, 300 | li: li, 301 | visible: false, 302 | quad: quad, 303 | material: material, 304 | formatter: formatter 305 | }; 306 | this.fbos.push( fboData ); 307 | this.fboMap.set( quad, fboData ); 308 | 309 | li.addEventListener( 'click', e => { 310 | quad.visible = !quad.visible; 311 | if( quad.visible ) { 312 | this.hideAll(); 313 | quad.visible = true; 314 | li.classList.add( 'active' ); 315 | this.info.style.display = 'block'; 316 | this.grid.style.display = 'block'; 317 | this.grid.style.width = ( fboData.width + 2 ) + 'px'; 318 | this.grid.style.height = ( fboData.height + 2 ) + 'px'; 319 | this.currentObj = fboData; 320 | this.info.innerHTML = `Width: ${fbo.width} Height: ${fbo.height}
Format: ${formats[fbo.texture?fbo.texture.format:fbo.format]} Type: ${types[fbo.texture?fbo.texture.type:fbo.type]}`; 321 | } else { 322 | this.info.style.display = 'none'; 323 | li.classList.remove( 'active' ); 324 | this.grid.style.display = 'none'; 325 | this.currentObj = null; 326 | } 327 | } ); 328 | 329 | this.buildList(); 330 | 331 | } 332 | 333 | detach( f ) { 334 | 335 | var p = 0; 336 | for( var fbo of this.fbos ) { 337 | if( fbo.fbo === f ) { 338 | this.fbos.splice( p, 1 ) 339 | } 340 | p++; 341 | } 342 | 343 | this.buildList(); 344 | 345 | } 346 | 347 | refreshFBO( f ) { 348 | 349 | for( var fbo of this.fbos ) { 350 | if( fbo.fbo === f ) { 351 | const width = 600; 352 | const height = f.height * width / f.width; 353 | fbo.width = width; 354 | fbo.height = height; 355 | fbo.quad.width = width; 356 | fbo.quad.height = height; 357 | fbo.quad.scale.set( width, height, 1. ); 358 | } 359 | } 360 | 361 | } 362 | 363 | hideAll() { 364 | 365 | this.fbos.forEach( fbo => { 366 | fbo.quad.visible = false; 367 | fbo.li.classList.remove( 'active' ); 368 | } ); 369 | 370 | } 371 | 372 | buildList() { 373 | 374 | while( this.list.firstChild ) this.list.removeChild( this.list.firstChild ); 375 | 376 | for( var fbo of this.fbos ) { 377 | this.list.appendChild( fbo.li ); 378 | } 379 | 380 | } 381 | 382 | setSize( w, h ) { 383 | 384 | this.camera.left = w / - 2; 385 | this.camera.right = w / 2; 386 | this.camera.top = h / 2; 387 | this.camera.bottom = h / - 2; 388 | 389 | this.camera.updateProjectionMatrix(); 390 | 391 | } 392 | 393 | readPixel( obj, u, v ) { 394 | 395 | this.currentU = u; 396 | this.currentV = v; 397 | 398 | if( this.currentObj === null ) return; 399 | 400 | const fbo = obj.fbo; 401 | 402 | const x = ~~( fbo.width * u ); 403 | const y = ~~( fbo.height * v ); 404 | 405 | let types = {} 406 | types[ THREE.UnsignedByteType ] = Uint8Array; 407 | types[ THREE.ByteType ] = Int8Array; 408 | types[ THREE.ShortType ] = Int16Array; 409 | types[ THREE.UnsignedShortType ] = Uint16Array; 410 | types[ THREE.IntType ] = Int32Array; 411 | types[ THREE.UnsignedIntType ] = Uint32Array; 412 | types[ THREE.FloatType ] = Float32Array; 413 | types[ THREE.HalfFloatType ] = null; 414 | types[ THREE.UnsignedShort4444Type ] = Uint16Array; 415 | types[ THREE.UnsignedShort5551Type ] = Uint16Array; 416 | types[ THREE.UnsignedShort565Type ] = Uint16Array; 417 | 418 | var type = types[ fbo.texture ? fbo.texture.type : fbo.type ]; 419 | if( type === null ) { 420 | console.warning( fbo.texture ? fbo.texture.type : fbo.type + ' not supported' ); 421 | return; 422 | } 423 | 424 | const pixelBuffer = new ( type )( 4 ); 425 | 426 | this.renderer.readRenderTargetPixels( fbo, x, y, 1, 1, pixelBuffer ); 427 | const posTxt = `X : ${x} Y: ${y} u: ${u} v: ${v}`; 428 | const dataTxt = obj.formatter ? 429 | obj.formatter( { 430 | x: x, 431 | y: y, 432 | u: u, 433 | v: v, 434 | r: pixelBuffer[ 0 ], 435 | g: pixelBuffer[ 1 ], 436 | b: pixelBuffer[ 2 ], 437 | a: pixelBuffer[ 3 ] 438 | } ) 439 | : 440 | `R: ${pixelBuffer[ 0 ]} G: ${pixelBuffer[ 1 ]} B: ${pixelBuffer[ 2 ]} A: ${pixelBuffer[ 3 ]}`; 441 | this.label.innerHTML = `${posTxt}
${dataTxt}`; 442 | 443 | const ox = ~~( u * fbo.width ) * obj.quad.width / fbo.width; 444 | const oy = ~~( obj.flipY ? ( 1 - v ) * fbo.height : v * fbo.height ) * obj.quad.height / fbo.height; 445 | this.hotspot.style.width = `${obj.quad.width / fbo.width}px`; 446 | this.hotspot.style.height = `${obj.quad.height / fbo.height}px`; 447 | this.hotspot.style.transform = `translate3d(${ox}px,${oy}px,0)`; 448 | this.label.style.bottom = ( obj.quad.height / fbo.height ) + 'px'; 449 | 450 | } 451 | 452 | update() { 453 | 454 | this.renderer.autoClear = false; 455 | this.renderer.render( this.scene, this.camera ); 456 | this.renderer.autoClear = true; 457 | if( this.autoUpdate ) this.readPixel( this.currentObj, this.currentU, this.currentV ); 458 | 459 | } 460 | 461 | } 462 | 463 | if( typeof exports !== 'undefined' ) { 464 | if( typeof module !== 'undefined' && module.exports ) { 465 | exports = module.exports = FBOHelper 466 | } 467 | exports.FBOHelper = FBOHelper 468 | } 469 | else { 470 | root.FBOHelper = FBOHelper 471 | } 472 | 473 | }).call(this); 474 | 475 | -------------------------------------------------------------------------------- /js/isMobile.min.js: -------------------------------------------------------------------------------- 1 | !function(a){var b=/iPhone/i,c=/iPod/i,d=/iPad/i,e=/(?=.*\bAndroid\b)(?=.*\bMobile\b)/i,f=/Android/i,g=/(?=.*\bAndroid\b)(?=.*\bSD4930UR\b)/i,h=/(?=.*\bAndroid\b)(?=.*\b(?:KFOT|KFTT|KFJWI|KFJWA|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|KFARWI|KFASWI|KFSAWI|KFSAWA)\b)/i,i=/Windows Phone/i,j=/(?=.*\bWindows\b)(?=.*\bARM\b)/i,k=/BlackBerry/i,l=/BB10/i,m=/Opera Mini/i,n=/(CriOS|Chrome)(?=.*\bMobile\b)/i,o=/(?=.*\bFirefox\b)(?=.*\bMobile\b)/i,p=new RegExp("(?:Nexus 7|BNTV250|Kindle Fire|Silk|GT-P1000)","i"),q=function(a,b){return a.test(b)},r=function(a){var r=a||navigator.userAgent,s=r.split("[FBAN");if("undefined"!=typeof s[1]&&(r=s[0]),s=r.split("Twitter"),"undefined"!=typeof s[1]&&(r=s[0]),this.apple={phone:q(b,r),ipod:q(c,r),tablet:!q(b,r)&&q(d,r),device:q(b,r)||q(c,r)||q(d,r)},this.amazon={phone:q(g,r),tablet:!q(g,r)&&q(h,r),device:q(g,r)||q(h,r)},this.android={phone:q(g,r)||q(e,r),tablet:!q(g,r)&&!q(e,r)&&(q(h,r)||q(f,r)),device:q(g,r)||q(h,r)||q(e,r)||q(f,r)},this.windows={phone:q(i,r),tablet:q(j,r),device:q(i,r)||q(j,r)},this.other={blackberry:q(k,r),blackberry10:q(l,r),opera:q(m,r),firefox:q(o,r),chrome:q(n,r),device:q(k,r)||q(l,r)||q(m,r)||q(o,r)||q(n,r)},this.seven_inch=q(p,r),this.any=this.apple.device||this.android.device||this.windows.device||this.other.device||this.seven_inch,this.phone=this.apple.phone||this.android.phone||this.windows.phone,this.tablet=this.apple.tablet||this.android.tablet||this.windows.tablet,"undefined"==typeof window)return this},s=function(){var a=new r;return a.Class=r,a};"undefined"!=typeof module&&module.exports&&"undefined"==typeof window?module.exports=r:"undefined"!=typeof module&&module.exports&&"undefined"!=typeof window?module.exports=s():"function"==typeof define&&define.amd?define("isMobile",[],a.isMobile=s()):a.isMobile=s()}(this); --------------------------------------------------------------------------------