├── LICENSE ├── README.md ├── index.html ├── BVHImport.js └── TrackballControls.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 herzig 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 | # BVHImporter 2 | 3 | Note: if you're looking for a Three.js BVH Loader, there is a modified version of this one included the official Three.js examples loader collection: [http://threejs.org/examples/?q=loader#webgl_loader_bvh](http://threejs.org/examples/?q=loader#webgl_loader_bvh) 4 | 5 | JavaScript parser for BVH files and converter to Three.js Bones. 6 | 7 | `BVHImport.readBvh(lines)` parses a bvh file and creates basic tech-agnostic representation of the bvh nodes including motion data. 8 | 9 | `BVHImport.toTHREE(source)` converts the internal reperesentation to a THREE.Skeleton and a single THREE.AnimationClip which can be passed straight to a THREE.AnimationMixer 10 | 11 | 12 | ## Usage 13 | (see [http://herzig.github.io/BVHImporter/](http://herzig.github.io/BVHImporter/) for the full working example. 14 | Parsing a BVH file and create a Three.js skeleton & keyframe animation: 15 | ``` 16 | // import BVH file (from string array). 17 | var root = BVHImport.readBvh(lines); 18 | 19 | // animation contains a THREE.Skeleton and the THREE.AnimationClip 20 | var animation = BVHImport.toTHREE(root); 21 | 22 | // create a minimal empty geometry and skinned mesh to hold the bones 23 | var geometry = new THREE.Geometry(); 24 | var material = new THREE.MeshPhongMaterial({ skinning: true, }); 25 | var mesh = new THREE.SkinnedMesh(geometry, material); 26 | 27 | // bind skeleton 28 | mesh.add(animation.skeleton.bones[0]); 29 | mesh.bind(animation.skeleton); 30 | 31 | skeletonHelper = new THREE.SkeletonHelper(mesh); 32 | skeletonHelper.material.linewidth = 5; 33 | 34 | scene.add(skeletonHelper); 35 | scene.add(mesh); 36 | 37 | mixer = new THREE.AnimationMixer(mesh); 38 | mixer.clipAction(animation.clip).setEffectiveWeight(1.0).play( 39 | ```` 40 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /BVHImport.js: -------------------------------------------------------------------------------- 1 | /* 2 | Ivo Herzig, 2016 3 | MIT License 4 | 5 | A JavaScript parser for BVH files and converter to Three.js animation. 6 | */ 7 | 8 | var BVHImport = new function() { 9 | 10 | 11 | /* 12 | converts a bvh skeletal animation definition to THREE.Bones 13 | and a THREE.AnimationClip 14 | 15 | bone: bvh bone hierarchy including keyframe data (as produced by BVHImport.readBvh) 16 | 17 | returns an object containing a THREE.Skeleton and a THREE.AnimationClip 18 | ({ skeleton: THREE.Skeleton, clip: THREE.AnimationClip }) 19 | */ 20 | this.toTHREE = function (bone) { 21 | 22 | var threeBones = []; 23 | toTHREEBone(bone, threeBones); 24 | 25 | return { 26 | skeleton: new THREE.Skeleton(threeBones), 27 | clip: toTHREEAnimation(bone) 28 | } 29 | 30 | ; 31 | } 32 | 33 | /* 34 | converts the internal bvh node structure to a THREE.Bone hierarchy 35 | 36 | source: the bvh root node 37 | list: pass an empty array, will contain a flat list of all converte THREE.Bones 38 | 39 | returns the root THREE.Bone 40 | */ 41 | function toTHREEBone(source, list) { 42 | var bone = new THREE.Bone(); 43 | list.push(bone); 44 | bone.position.add(source.offset); 45 | bone.name = source.name; 46 | 47 | if (source.type != "ENDSITE") { 48 | for (var i = 0; i < source.children.length; ++i) { 49 | bone.add(toTHREEBone(source.children[i], list)); 50 | } 51 | } 52 | 53 | return bone; 54 | } 55 | 56 | /* 57 | builds a THREE.AnimationClip from the keyframe data saved in the bone. 58 | 59 | bone: bvh root node 60 | 61 | returns: a THREE.AnimationClip containing position and quaternion tracks 62 | */ 63 | function toTHREEAnimation(bone) { 64 | 65 | var bones = []; 66 | flatten(bone, bones); 67 | 68 | var tracks = []; 69 | 70 | // create a position and quaternion animation track for each node 71 | for (var i = 0; i < bones.length; ++i) { 72 | var b = bones[i]; 73 | 74 | if (b.type == "ENDSITE") 75 | continue; 76 | 77 | // track data 78 | var times = []; 79 | var positions = []; 80 | var rotations = []; 81 | 82 | for (var j = 0; j < b.frames.length; ++j) { 83 | var f = b.frames[j]; 84 | times.push(f.time); 85 | positions.push(f.position.x + b.offset.x); 86 | positions.push(f.position.y + b.offset.y); 87 | positions.push(f.position.z + b.offset.z); 88 | 89 | rotations.push(f.rotation.x); 90 | rotations.push(f.rotation.y); 91 | rotations.push(f.rotation.z); 92 | rotations.push(f.rotation.w); 93 | } 94 | 95 | tracks.push(new THREE.VectorKeyframeTrack( 96 | ".bones["+b.name+"].position", times, positions)); 97 | 98 | tracks.push(new THREE.QuaternionKeyframeTrack( 99 | ".bones["+b.name+"].quaternion", times, rotations)); 100 | } 101 | 102 | var clip = new THREE.AnimationClip("animation", -1, tracks); 103 | 104 | return clip; 105 | } 106 | 107 | 108 | /* 109 | reads a BVH file 110 | */ 111 | this.readBvh = function(lines) { 112 | 113 | // read model structure 114 | if (lines.shift().trim().toUpperCase() != "HIERARCHY") 115 | throw "HIERARCHY expected"; 116 | 117 | var list = []; 118 | var root = BVHImport.readNode(lines, lines.shift().trim(), list); 119 | 120 | // read motion data 121 | if (lines.shift().trim().toUpperCase() != "MOTION") 122 | throw "MOTION expected"; 123 | 124 | var tokens = lines.shift().trim().split(/[\s]+/); 125 | 126 | // number of frames 127 | var numFrames = parseInt(tokens[1]); 128 | if (isNaN(numFrames)) 129 | throw "Failed to read number of frames."; 130 | 131 | // frame time 132 | tokens = lines.shift().trim().split(/[\s]+/); 133 | var frameTime = parseFloat(tokens[2]); 134 | if (isNaN(frameTime)) 135 | throw "Failed to read frame time."; 136 | 137 | // read frame data line by line 138 | for (var i = 0; i < numFrames; ++i) { 139 | tokens = lines.shift().trim().split(/[\s]+/); 140 | 141 | BVHImport.readFrameData(tokens, i*frameTime, root, list); 142 | } 143 | 144 | return root; 145 | } 146 | 147 | /* 148 | Recursively parses the HIERACHY section of the BVH file 149 | 150 | - lines: all lines of the file. lines are consumed as we go along. 151 | - firstline: line containing the node type and name e.g. "JOINT hip" 152 | - list: collects a flat list of nodes 153 | 154 | returns: a BVH node including children 155 | */ 156 | this.readNode = function(lines, firstline, list) { 157 | var node = {name: "", type: "", frames: []}; 158 | list.push(node); 159 | 160 | // parse node tpye and name. 161 | var tokens = firstline.trim().split(/[\s]+/) 162 | 163 | if (tokens[0].toUpperCase() === "END" && tokens[1].toUpperCase() === "SITE") { 164 | node.type = "ENDSITE"; 165 | node.name = "ENDSITE"; // bvh end sites have no name 166 | } 167 | else { 168 | node.name = tokens[1]; 169 | node.type = tokens[0].toUpperCase(); 170 | } 171 | 172 | // opening bracket 173 | if (lines.shift().trim() != "{") 174 | throw "Expected opening { after type & name"; 175 | 176 | // parse OFFSET 177 | tokens = lines.shift().trim().split(/[\s]+/); 178 | 179 | if (tokens[0].toUpperCase() != "OFFSET") 180 | throw "Expected OFFSET, but got: " + tokens[0]; 181 | if (tokens.length != 4) 182 | throw "OFFSET: Invalid number of values"; 183 | 184 | var offset = { 185 | x: parseFloat(tokens[1]), y: parseFloat(tokens[2]), z: parseFloat(tokens[3]) }; 186 | 187 | if (isNaN(offset.x) || isNaN(offset.y) || isNaN(offset.z)) 188 | throw "OFFSET: Invalid values"; 189 | 190 | node.offset = offset; 191 | 192 | // parse CHANNELS definitions 193 | if (node.type != "ENDSITE") { 194 | tokens = lines.shift().trim().split(/[\s]+/); 195 | 196 | if (tokens[0].toUpperCase() != "CHANNELS") 197 | throw "Expected CHANNELS definition"; 198 | 199 | var numChannels = parseInt(tokens[1]); 200 | node.channels = tokens.splice(2, numChannels); 201 | node.children = []; 202 | } 203 | 204 | // read children 205 | while (true) { 206 | var line = lines.shift().trim(); 207 | 208 | if (line == "}") { 209 | return node; 210 | } 211 | else { 212 | node.children.push(BVHImport.readNode(lines, line, list)); 213 | } 214 | } 215 | } 216 | 217 | /* 218 | Recursively reads data from a single frame into the bone hierarchy. 219 | The bone hierarchy has to be structured in the same order as the BVH file. 220 | keyframe data is stored in bone.frames. 221 | 222 | - data: splitted string array (frame values), values are shift()ed so 223 | this should be empty after parsing the whole hierarchy. 224 | - frameTime: playback time for this keyframe. 225 | - bone: the bone to read frame data from. 226 | */ 227 | this.readFrameData = function(data, frameTime, bone) { 228 | 229 | if (bone.type === "ENDSITE") // end sites have no motion data 230 | return; 231 | 232 | // add keyframe 233 | var keyframe = { 234 | time: frameTime, 235 | position: { x: 0, y: 0, z: 0 }, 236 | rotation: new Quat(), 237 | }; 238 | 239 | bone.frames.push(keyframe); 240 | 241 | // parse values for each channel in node 242 | for (var i = 0; i < bone.channels.length; ++i) { 243 | 244 | switch(bone.channels[i]) { 245 | case "Xposition": 246 | keyframe.position.x = parseFloat(data.shift().trim()); 247 | break; 248 | case "Yposition": 249 | keyframe.position.y = parseFloat(data.shift().trim()); 250 | break; 251 | case "Zposition": 252 | keyframe.position.z = parseFloat(data.shift().trim()); 253 | break; 254 | case "Xrotation": 255 | var quat = new Quat(); 256 | quat.setFromAxisAngle(1, 0, 0, parseFloat(data.shift().trim()) * Math.PI / 180); 257 | 258 | keyframe.rotation.multiply(quat); 259 | break; 260 | case "Yrotation": 261 | var quat = new Quat(); 262 | quat.setFromAxisAngle(0, 1, 0, parseFloat(data.shift().trim()) * Math.PI / 180); 263 | 264 | keyframe.rotation.multiply(quat); 265 | break; 266 | case "Zrotation": 267 | var quat = new Quat(); 268 | quat.setFromAxisAngle(0, 0, 1, parseFloat(data.shift().trim()) * Math.PI / 180); 269 | 270 | keyframe.rotation.multiply(quat); 271 | break; 272 | default: 273 | throw "invalid channel type"; 274 | break; 275 | } 276 | } 277 | 278 | // parse child nodes 279 | for (var i = 0; i < bone.children.length; ++i) { 280 | BVHImport.readFrameData(data, frameTime, bone.children[i]); 281 | } 282 | } 283 | 284 | /* 285 | traverses the node hierarchy and builds a flat list of nodes 286 | */ 287 | function flatten(bone, flatList) { 288 | flatList.push(bone); 289 | 290 | if (bone.type !== "ENDSITE") 291 | { 292 | for (var i = 0; i < bone.children.length; ++i) { 293 | flatten(bone.children[i], flatList); 294 | } 295 | } 296 | } 297 | 298 | /* 299 | a minimal quaternion implementation to store joint rotations 300 | used in keyframe data 301 | */ 302 | function Quat(x, y, z, w) { 303 | this.x = x || 0; 304 | this.y = y || 0; 305 | this.z = z || 0; 306 | this.w = (w === undefined) ? 1 : w; 307 | } 308 | 309 | Quat.prototype.setFromAxisAngle = function(ax, ay, az, angle) { 310 | var angleHalf = angle * 0.5; 311 | var sin = Math.sin(angleHalf); 312 | 313 | this.x = ax * sin; 314 | this.y = ay * sin; 315 | this.z = az * sin; 316 | this.w = Math.cos(angleHalf); 317 | } 318 | 319 | Quat.prototype.multiply = function(quat) { 320 | var a = this, b = quat; 321 | 322 | var qax = a.x, qay = a.y, qaz = a.z, qaw = a.w; 323 | var qbx = b.x, qby = b.y, qbz = b.z, qbw = b.w; 324 | 325 | this.x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby; 326 | this.y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz; 327 | this.z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx; 328 | this.w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz; 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | * @author Simone Manini / http://daron1337.github.io 5 | * @author Luca Antiga / http://lantiga.github.io 6 | */ 7 | 8 | THREE.TrackballControls = function ( object, domElement ) { 9 | 10 | var _this = this; 11 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 12 | 13 | this.object = object; 14 | this.domElement = ( domElement !== undefined ) ? domElement : document; 15 | 16 | // API 17 | 18 | this.enabled = true; 19 | 20 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 21 | 22 | this.rotateSpeed = 1.0; 23 | this.zoomSpeed = 1.2; 24 | this.panSpeed = 0.3; 25 | 26 | this.noRotate = false; 27 | this.noZoom = false; 28 | this.noPan = false; 29 | 30 | this.staticMoving = false; 31 | this.dynamicDampingFactor = 0.2; 32 | 33 | this.minDistance = 0; 34 | this.maxDistance = Infinity; 35 | 36 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 37 | 38 | // internals 39 | 40 | this.target = new THREE.Vector3(); 41 | 42 | var EPS = 0.000001; 43 | 44 | var lastPosition = new THREE.Vector3(); 45 | 46 | var _state = STATE.NONE, 47 | _prevState = STATE.NONE, 48 | 49 | _eye = new THREE.Vector3(), 50 | 51 | _movePrev = new THREE.Vector2(), 52 | _moveCurr = new THREE.Vector2(), 53 | 54 | _lastAxis = new THREE.Vector3(), 55 | _lastAngle = 0, 56 | 57 | _zoomStart = new THREE.Vector2(), 58 | _zoomEnd = new THREE.Vector2(), 59 | 60 | _touchZoomDistanceStart = 0, 61 | _touchZoomDistanceEnd = 0, 62 | 63 | _panStart = new THREE.Vector2(), 64 | _panEnd = new THREE.Vector2(); 65 | 66 | // for reset 67 | 68 | this.target0 = this.target.clone(); 69 | this.position0 = this.object.position.clone(); 70 | this.up0 = this.object.up.clone(); 71 | 72 | // events 73 | 74 | var changeEvent = { type: 'change' }; 75 | var startEvent = { type: 'start' }; 76 | var endEvent = { type: 'end' }; 77 | 78 | 79 | // methods 80 | 81 | this.handleResize = function () { 82 | 83 | if ( this.domElement === document ) { 84 | 85 | this.screen.left = 0; 86 | this.screen.top = 0; 87 | this.screen.width = window.innerWidth; 88 | this.screen.height = window.innerHeight; 89 | 90 | } else { 91 | 92 | var box = this.domElement.getBoundingClientRect(); 93 | // adjustments come from similar code in the jquery offset() function 94 | var d = this.domElement.ownerDocument.documentElement; 95 | this.screen.left = box.left + window.pageXOffset - d.clientLeft; 96 | this.screen.top = box.top + window.pageYOffset - d.clientTop; 97 | this.screen.width = box.width; 98 | this.screen.height = box.height; 99 | 100 | } 101 | 102 | }; 103 | 104 | this.handleEvent = function ( event ) { 105 | 106 | if ( typeof this[ event.type ] == 'function' ) { 107 | 108 | this[ event.type ]( event ); 109 | 110 | } 111 | 112 | }; 113 | 114 | var getMouseOnScreen = ( function () { 115 | 116 | var vector = new THREE.Vector2(); 117 | 118 | return function getMouseOnScreen( pageX, pageY ) { 119 | 120 | vector.set( 121 | ( pageX - _this.screen.left ) / _this.screen.width, 122 | ( pageY - _this.screen.top ) / _this.screen.height 123 | ); 124 | 125 | return vector; 126 | 127 | }; 128 | 129 | }() ); 130 | 131 | var getMouseOnCircle = ( function () { 132 | 133 | var vector = new THREE.Vector2(); 134 | 135 | return function getMouseOnCircle( pageX, pageY ) { 136 | 137 | vector.set( 138 | ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), 139 | ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional 140 | ); 141 | 142 | return vector; 143 | 144 | }; 145 | 146 | }() ); 147 | 148 | this.rotateCamera = ( function() { 149 | 150 | var axis = new THREE.Vector3(), 151 | quaternion = new THREE.Quaternion(), 152 | eyeDirection = new THREE.Vector3(), 153 | objectUpDirection = new THREE.Vector3(), 154 | objectSidewaysDirection = new THREE.Vector3(), 155 | moveDirection = new THREE.Vector3(), 156 | angle; 157 | 158 | return function rotateCamera() { 159 | 160 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 161 | angle = moveDirection.length(); 162 | 163 | if ( angle ) { 164 | 165 | _eye.copy( _this.object.position ).sub( _this.target ); 166 | 167 | eyeDirection.copy( _eye ).normalize(); 168 | objectUpDirection.copy( _this.object.up ).normalize(); 169 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 170 | 171 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 172 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 173 | 174 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 175 | 176 | axis.crossVectors( moveDirection, _eye ).normalize(); 177 | 178 | angle *= _this.rotateSpeed; 179 | quaternion.setFromAxisAngle( axis, angle ); 180 | 181 | _eye.applyQuaternion( quaternion ); 182 | _this.object.up.applyQuaternion( quaternion ); 183 | 184 | _lastAxis.copy( axis ); 185 | _lastAngle = angle; 186 | 187 | } else if ( ! _this.staticMoving && _lastAngle ) { 188 | 189 | _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); 190 | _eye.copy( _this.object.position ).sub( _this.target ); 191 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 192 | _eye.applyQuaternion( quaternion ); 193 | _this.object.up.applyQuaternion( quaternion ); 194 | 195 | } 196 | 197 | _movePrev.copy( _moveCurr ); 198 | 199 | }; 200 | 201 | }() ); 202 | 203 | 204 | this.zoomCamera = function () { 205 | 206 | var factor; 207 | 208 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 209 | 210 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 211 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 212 | _eye.multiplyScalar( factor ); 213 | 214 | } else { 215 | 216 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 217 | 218 | if ( factor !== 1.0 && factor > 0.0 ) { 219 | 220 | _eye.multiplyScalar( factor ); 221 | 222 | if ( _this.staticMoving ) { 223 | 224 | _zoomStart.copy( _zoomEnd ); 225 | 226 | } else { 227 | 228 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 229 | 230 | } 231 | 232 | } 233 | 234 | } 235 | 236 | }; 237 | 238 | this.panCamera = ( function() { 239 | 240 | var mouseChange = new THREE.Vector2(), 241 | objectUp = new THREE.Vector3(), 242 | pan = new THREE.Vector3(); 243 | 244 | return function panCamera() { 245 | 246 | mouseChange.copy( _panEnd ).sub( _panStart ); 247 | 248 | if ( mouseChange.lengthSq() ) { 249 | 250 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 251 | 252 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 253 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 254 | 255 | _this.object.position.add( pan ); 256 | _this.target.add( pan ); 257 | 258 | if ( _this.staticMoving ) { 259 | 260 | _panStart.copy( _panEnd ); 261 | 262 | } else { 263 | 264 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 265 | 266 | } 267 | 268 | } 269 | 270 | }; 271 | 272 | }() ); 273 | 274 | this.checkDistances = function () { 275 | 276 | if ( ! _this.noZoom || ! _this.noPan ) { 277 | 278 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 279 | 280 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 281 | _zoomStart.copy( _zoomEnd ); 282 | 283 | } 284 | 285 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 286 | 287 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 288 | _zoomStart.copy( _zoomEnd ); 289 | 290 | } 291 | 292 | } 293 | 294 | }; 295 | 296 | this.update = function () { 297 | 298 | _eye.subVectors( _this.object.position, _this.target ); 299 | 300 | if ( ! _this.noRotate ) { 301 | 302 | _this.rotateCamera(); 303 | 304 | } 305 | 306 | if ( ! _this.noZoom ) { 307 | 308 | _this.zoomCamera(); 309 | 310 | } 311 | 312 | if ( ! _this.noPan ) { 313 | 314 | _this.panCamera(); 315 | 316 | } 317 | 318 | _this.object.position.addVectors( _this.target, _eye ); 319 | 320 | _this.checkDistances(); 321 | 322 | _this.object.lookAt( _this.target ); 323 | 324 | if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 325 | 326 | _this.dispatchEvent( changeEvent ); 327 | 328 | lastPosition.copy( _this.object.position ); 329 | 330 | } 331 | 332 | }; 333 | 334 | this.reset = function () { 335 | 336 | _state = STATE.NONE; 337 | _prevState = STATE.NONE; 338 | 339 | _this.target.copy( _this.target0 ); 340 | _this.object.position.copy( _this.position0 ); 341 | _this.object.up.copy( _this.up0 ); 342 | 343 | _eye.subVectors( _this.object.position, _this.target ); 344 | 345 | _this.object.lookAt( _this.target ); 346 | 347 | _this.dispatchEvent( changeEvent ); 348 | 349 | lastPosition.copy( _this.object.position ); 350 | 351 | }; 352 | 353 | // listeners 354 | 355 | function keydown( event ) { 356 | 357 | if ( _this.enabled === false ) return; 358 | 359 | window.removeEventListener( 'keydown', keydown ); 360 | 361 | _prevState = _state; 362 | 363 | if ( _state !== STATE.NONE ) { 364 | 365 | return; 366 | 367 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) { 368 | 369 | _state = STATE.ROTATE; 370 | 371 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) { 372 | 373 | _state = STATE.ZOOM; 374 | 375 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) { 376 | 377 | _state = STATE.PAN; 378 | 379 | } 380 | 381 | } 382 | 383 | function keyup( event ) { 384 | 385 | if ( _this.enabled === false ) return; 386 | 387 | _state = _prevState; 388 | 389 | window.addEventListener( 'keydown', keydown, false ); 390 | 391 | } 392 | 393 | function mousedown( event ) { 394 | 395 | if ( _this.enabled === false ) return; 396 | 397 | event.preventDefault(); 398 | event.stopPropagation(); 399 | 400 | if ( _state === STATE.NONE ) { 401 | 402 | _state = event.button; 403 | 404 | } 405 | 406 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 407 | 408 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 409 | _movePrev.copy( _moveCurr ); 410 | 411 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 412 | 413 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 414 | _zoomEnd.copy( _zoomStart ); 415 | 416 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 417 | 418 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 419 | _panEnd.copy( _panStart ); 420 | 421 | } 422 | 423 | document.addEventListener( 'mousemove', mousemove, false ); 424 | document.addEventListener( 'mouseup', mouseup, false ); 425 | 426 | _this.dispatchEvent( startEvent ); 427 | 428 | } 429 | 430 | function mousemove( event ) { 431 | 432 | if ( _this.enabled === false ) return; 433 | 434 | event.preventDefault(); 435 | event.stopPropagation(); 436 | 437 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 438 | 439 | _movePrev.copy( _moveCurr ); 440 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 441 | 442 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 443 | 444 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 445 | 446 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 447 | 448 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 449 | 450 | } 451 | 452 | } 453 | 454 | function mouseup( event ) { 455 | 456 | if ( _this.enabled === false ) return; 457 | 458 | event.preventDefault(); 459 | event.stopPropagation(); 460 | 461 | _state = STATE.NONE; 462 | 463 | document.removeEventListener( 'mousemove', mousemove ); 464 | document.removeEventListener( 'mouseup', mouseup ); 465 | _this.dispatchEvent( endEvent ); 466 | 467 | } 468 | 469 | function mousewheel( event ) { 470 | 471 | if ( _this.enabled === false ) return; 472 | 473 | event.preventDefault(); 474 | event.stopPropagation(); 475 | 476 | var delta = 0; 477 | 478 | if ( event.wheelDelta ) { 479 | 480 | // WebKit / Opera / Explorer 9 481 | 482 | delta = event.wheelDelta / 40; 483 | 484 | } else if ( event.detail ) { 485 | 486 | // Firefox 487 | 488 | delta = - event.detail / 3; 489 | 490 | } 491 | 492 | _zoomStart.y += delta * 0.01; 493 | _this.dispatchEvent( startEvent ); 494 | _this.dispatchEvent( endEvent ); 495 | 496 | } 497 | 498 | function touchstart( event ) { 499 | 500 | if ( _this.enabled === false ) return; 501 | 502 | switch ( event.touches.length ) { 503 | 504 | case 1: 505 | _state = STATE.TOUCH_ROTATE; 506 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 507 | _movePrev.copy( _moveCurr ); 508 | break; 509 | 510 | default: // 2 or more 511 | _state = STATE.TOUCH_ZOOM_PAN; 512 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 513 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 514 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 515 | 516 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 517 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 518 | _panStart.copy( getMouseOnScreen( x, y ) ); 519 | _panEnd.copy( _panStart ); 520 | break; 521 | 522 | } 523 | 524 | _this.dispatchEvent( startEvent ); 525 | 526 | } 527 | 528 | function touchmove( event ) { 529 | 530 | if ( _this.enabled === false ) return; 531 | 532 | event.preventDefault(); 533 | event.stopPropagation(); 534 | 535 | switch ( event.touches.length ) { 536 | 537 | case 1: 538 | _movePrev.copy( _moveCurr ); 539 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 540 | break; 541 | 542 | default: // 2 or more 543 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 544 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 545 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 546 | 547 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 548 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 549 | _panEnd.copy( getMouseOnScreen( x, y ) ); 550 | break; 551 | 552 | } 553 | 554 | } 555 | 556 | function touchend( event ) { 557 | 558 | if ( _this.enabled === false ) return; 559 | 560 | switch ( event.touches.length ) { 561 | 562 | case 0: 563 | _state = STATE.NONE; 564 | break; 565 | 566 | case 1: 567 | _state = STATE.TOUCH_ROTATE; 568 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 569 | _movePrev.copy( _moveCurr ); 570 | break; 571 | 572 | } 573 | 574 | _this.dispatchEvent( endEvent ); 575 | 576 | } 577 | 578 | function contextmenu( event ) { 579 | 580 | event.preventDefault(); 581 | 582 | } 583 | 584 | this.dispose = function() { 585 | 586 | this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); 587 | this.domElement.removeEventListener( 'mousedown', mousedown, false ); 588 | this.domElement.removeEventListener( 'mousewheel', mousewheel, false ); 589 | this.domElement.removeEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox 590 | 591 | this.domElement.removeEventListener( 'touchstart', touchstart, false ); 592 | this.domElement.removeEventListener( 'touchend', touchend, false ); 593 | this.domElement.removeEventListener( 'touchmove', touchmove, false ); 594 | 595 | document.removeEventListener( 'mousemove', mousemove, false ); 596 | document.removeEventListener( 'mouseup', mouseup, false ); 597 | 598 | window.removeEventListener( 'keydown', keydown, false ); 599 | window.removeEventListener( 'keyup', keyup, false ); 600 | 601 | }; 602 | 603 | this.domElement.addEventListener( 'contextmenu', contextmenu, false ); 604 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 605 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 606 | this.domElement.addEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox 607 | 608 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 609 | this.domElement.addEventListener( 'touchend', touchend, false ); 610 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 611 | 612 | window.addEventListener( 'keydown', keydown, false ); 613 | window.addEventListener( 'keyup', keyup, false ); 614 | 615 | this.handleResize(); 616 | 617 | // force an update at start 618 | this.update(); 619 | 620 | }; 621 | 622 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 623 | THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; 624 | --------------------------------------------------------------------------------