├── README.md ├── sahua.css ├── main.css ├── tween.module.min.js ├── CSS3DRenderer.js ├── index.html ├── TrackballControls.js └── OrbitControls.js /README.md: -------------------------------------------------------------------------------- 1 | [Demo Address 演示地址](https://zlt-1257933030.cos.ap-shanghai.myqcloud.com/3d-wall/index.html) 2 | 3 | [base three.js css3d_periodictable 基于 three.js](https://threejs.org/examples/#css3d_periodictable) 4 | 5 | [CODEPEN coloured ribbon 彩纸链接](https://codepen.io/agathaco/pen/wvMrXjz) 6 | 7 | 可异步拿到数据后,对数据进行修改,并渲染到页面上 8 | ![image](https://user-images.githubusercontent.com/22071679/142181866-054ca4fd-0aa3-4292-adab-6428fc0273fa.png) 9 | ![image](https://user-images.githubusercontent.com/22071679/142182017-bd42f828-7240-420d-bdb2-05ec0a247963.png) 10 | ![image](https://user-images.githubusercontent.com/22071679/142182054-df81ae5e-001f-4f44-8917-71aad621499b.png) 11 | -------------------------------------------------------------------------------- /sahua.css: -------------------------------------------------------------------------------- 1 | 2 | .confetti { 3 | width: 1rem; 4 | height: 1rem; 5 | display: inline-block; 6 | position: absolute; 7 | top: -1rem; 8 | left: 0; 9 | z-index: 0; 10 | } 11 | .confetti .rotate { 12 | animation: driftyRotate 1s infinite both ease-in-out; 13 | perspective: 1000; 14 | } 15 | .confetti .askew { 16 | background: currentColor; 17 | transform: skewY(10deg); 18 | width: 1rem; 19 | height: 1rem; 20 | animation: drifty 1s infinite alternate both ease-in-out; 21 | perspective:1000; 22 | } 23 | 24 | .confetti:nth-of-type(5n) { 25 | color: #F56620; 26 | } 27 | .confetti:nth-of-type(5n+1) { 28 | color: #00EAFF; 29 | } 30 | .confetti:nth-of-type(5n+2) { 31 | color: #EA8EE0; 32 | } 33 | .confetti:nth-of-type(5n+3) { 34 | color: #EBFF38; 35 | } 36 | .confetti:nth-of-type(5n+4) { 37 | color: #0582FF; 38 | } 39 | 40 | .confetti:nth-of-type(7n) .askew { 41 | animation-delay: -.6s; 42 | animation-duration: 2.25s; 43 | } 44 | .confetti:nth-of-type(7n + 1) .askew { 45 | animation-delay: -.879s; 46 | animation-duration: 3.5s; 47 | } 48 | .confetti:nth-of-type(7n + 2) .askew { 49 | animation-delay: -.11s; 50 | animation-duration: 1.95s; 51 | } 52 | .confetti:nth-of-type(7n + 3) .askew { 53 | animation-delay: -.246s; 54 | animation-duration: .85s; 55 | } 56 | .confetti:nth-of-type(7n + 4) .askew { 57 | animation-delay: -.43s; 58 | animation-duration: 2.5s; 59 | } 60 | .confetti:nth-of-type(7n + 5) .askew { 61 | animation-delay: -.56s; 62 | animation-duration: 1.75s; 63 | } 64 | .confetti:nth-of-type(7n + 6) .askew { 65 | animation-delay: -.76s; 66 | animation-duration: 1.5s; 67 | } 68 | 69 | .confetti:nth-of-type(9n) .rotate { 70 | animation-duration: 2s; 71 | } 72 | .confetti:nth-of-type(9n + 1) .rotate { 73 | animation-duration: 2.3s; 74 | } 75 | .confetti:nth-of-type(9n + 2) .rotate { 76 | animation-duration: 1.1s; 77 | } 78 | .confetti:nth-of-type(9n + 3) .rotate { 79 | animation-duration: .75s; 80 | } 81 | .confetti:nth-of-type(9n + 4) .rotate { 82 | animation-duration: 4.3s; 83 | } 84 | .confetti:nth-of-type(9n + 5) .rotate { 85 | animation-duration: 3.05s; 86 | } 87 | .confetti:nth-of-type(9n + 6) .rotate { 88 | animation-duration: 2.76s; 89 | } 90 | .confetti:nth-of-type(9n + 7) .rotate { 91 | animation-duration: 7.6s; 92 | } 93 | .confetti:nth-of-type(9n + 8) .rotate { 94 | animation-duration: 1.78s; 95 | } 96 | 97 | @keyframes drifty { 98 | 0% { 99 | transform: skewY(10deg) translate3d(-250%, 0, 0); 100 | } 101 | 100% { 102 | transform: skewY(-12deg) translate3d(250%, 0, 0); 103 | } 104 | } 105 | @keyframes driftyRotate { 106 | 0% { 107 | transform: rotateX(0); 108 | } 109 | 100% { 110 | transform: rotateX(359deg); 111 | } 112 | } -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #443fb5; 4 | color: #fff; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | overscroll-behavior: none; 9 | overflow: hidden; 10 | } 11 | 12 | a { 13 | color: #ff0; 14 | text-decoration: none; 15 | } 16 | 17 | a:hover { 18 | text-decoration: underline; 19 | } 20 | 21 | button { 22 | cursor: pointer; 23 | text-transform: uppercase; 24 | } 25 | 26 | #info { 27 | position: absolute; 28 | top: 0px; 29 | width: 100%; 30 | padding: 10px; 31 | box-sizing: border-box; 32 | text-align: center; 33 | -moz-user-select: none; 34 | -webkit-user-select: none; 35 | -ms-user-select: none; 36 | user-select: none; 37 | pointer-events: none; 38 | z-index: 1; /* TODO Solve this in HTML */ 39 | } 40 | 41 | a, 42 | button, 43 | input, 44 | select { 45 | pointer-events: auto; 46 | } 47 | 48 | .dg.ac { 49 | -moz-user-select: none; 50 | -webkit-user-select: none; 51 | -ms-user-select: none; 52 | user-select: none; 53 | z-index: 2 !important; /* TODO Solve this in HTML */ 54 | } 55 | 56 | #overlay { 57 | position: absolute; 58 | font-size: 16px; 59 | z-index: 2; 60 | top: 0; 61 | left: 0; 62 | width: 100%; 63 | height: 100%; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | flex-direction: column; 68 | background: rgba(0, 0, 0, 0.7); 69 | } 70 | 71 | #overlay button { 72 | background: transparent; 73 | border: 0; 74 | border: 1px solid rgb(255, 255, 255); 75 | border-radius: 4px; 76 | color: #ffffff; 77 | padding: 12px 18px; 78 | text-transform: uppercase; 79 | cursor: pointer; 80 | } 81 | 82 | #notSupported { 83 | width: 50%; 84 | margin: auto; 85 | background-color: #f00; 86 | margin-top: 20px; 87 | padding: 10px; 88 | } 89 | 90 | a { 91 | color: #8ff; 92 | } 93 | 94 | #menu { 95 | position: absolute; 96 | bottom: 20px; 97 | width: 100%; 98 | text-align: center; 99 | } 100 | 101 | .element { 102 | width: 120px; 103 | height: 160px; 104 | box-shadow: 0px 0px 12px rgba(0, 255, 255, 0.5); 105 | border: 1px solid rgba(127, 255, 255, 0.25); 106 | font-family: Helvetica, sans-serif; 107 | text-align: center; 108 | line-height: normal; 109 | cursor: default; 110 | } 111 | 112 | .element:hover { 113 | box-shadow: 0px 0px 12px rgba(0, 255, 255, 0.75); 114 | border: 1px solid rgba(127, 255, 255, 0.75); 115 | } 116 | 117 | .element .number { 118 | position: absolute; 119 | top: 20px; 120 | right: 20px; 121 | font-size: 12px; 122 | color: rgba(127, 255, 255, 0.75); 123 | } 124 | 125 | .element .symbol { 126 | position: absolute; 127 | left: 0px; 128 | right: 0px; 129 | font-size: 60px; 130 | font-weight: bold; 131 | color: rgba(255, 255, 255, 0.75); 132 | text-shadow: 0 0 10px rgba(0, 255, 255, 0.95); 133 | } 134 | 135 | .element .symbol { 136 | text-align: center; 137 | } 138 | 139 | .element .symbol img { 140 | position: relative; 141 | transform: scale(0); 142 | opacity: 0; 143 | transition-duration: 0.6s; 144 | width: calc(100% - 2px); 145 | height: auto; 146 | } 147 | 148 | .element .details { 149 | transition-duration: 0.6s; 150 | opacity: 0; 151 | position: absolute; 152 | bottom: 15px; 153 | left: 0px; 154 | right: 0px; 155 | font-size: 12px; 156 | color: rgba(127, 255, 255, 0.75); 157 | } 158 | 159 | button { 160 | color: rgba(127, 255, 255, 0.75); 161 | background: transparent; 162 | outline: 1px solid rgba(127, 255, 255, 0.75); 163 | border: 0px; 164 | padding: 5px 10px; 165 | cursor: pointer; 166 | } 167 | 168 | button:hover { 169 | background-color: rgba(0, 255, 255, 0.5); 170 | } 171 | 172 | button:active { 173 | color: #000000; 174 | background-color: rgba(0, 255, 255, 0.75); 175 | } 176 | -------------------------------------------------------------------------------- /tween.module.min.js: -------------------------------------------------------------------------------- 1 | // tween.js 17.3.5 - https://github.com/tweenjs/tween.js 2 | var _Group=function(){this._tweens={},this._tweensAddedDuringUpdate={}};_Group.prototype={getAll:function(){return Object.keys(this._tweens).map(function(t){return this._tweens[t]}.bind(this))},removeAll:function(){this._tweens={}},add:function(t){this._tweens[t.getId()]=t,this._tweensAddedDuringUpdate[t.getId()]=t},remove:function(t){delete this._tweens[t.getId()],delete this._tweensAddedDuringUpdate[t.getId()]},update:function(t,n){var e=Object.keys(this._tweens);if(0===e.length)return!1;for(t=void 0!==t?t:TWEEN.now();0 2 | 3 | 4 | 5 | 蒲菲特-20周年庆典🎉🎉🎉 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 18 |
19 | 25 | 26 | 27 | 28 | 29 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /TrackballControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Vector2, 6 | Vector3 7 | } from './three.module.js'; 8 | 9 | const _changeEvent = { type: 'change' }; 10 | const _startEvent = { type: 'start' }; 11 | const _endEvent = { type: 'end' }; 12 | 13 | class TrackballControls extends EventDispatcher { 14 | 15 | constructor( object, domElement ) { 16 | 17 | super(); 18 | 19 | if ( domElement === undefined ) console.warn( 'THREE.TrackballControls: The second parameter "domElement" is now mandatory.' ); 20 | if ( domElement === document ) console.error( 'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 21 | 22 | const scope = this; 23 | const STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 24 | 25 | this.object = object; 26 | this.domElement = domElement; 27 | this.domElement.style.touchAction = 'none'; // disable touch scroll 28 | 29 | // API 30 | 31 | this.enabled = true; 32 | 33 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 34 | 35 | this.rotateSpeed = 1.0; 36 | this.zoomSpeed = 1.2; 37 | this.panSpeed = 0.3; 38 | 39 | this.noRotate = false; 40 | this.noZoom = false; 41 | this.noPan = false; 42 | 43 | this.staticMoving = false; 44 | this.dynamicDampingFactor = 0.2; 45 | 46 | this.minDistance = 0; 47 | this.maxDistance = Infinity; 48 | 49 | this.keys = [ 'KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/ ]; 50 | 51 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 52 | 53 | // internals 54 | 55 | this.target = new Vector3(); 56 | 57 | const EPS = 0.000001; 58 | 59 | const lastPosition = new Vector3(); 60 | let lastZoom = 1; 61 | 62 | let _state = STATE.NONE, 63 | _keyState = STATE.NONE, 64 | 65 | _touchZoomDistanceStart = 0, 66 | _touchZoomDistanceEnd = 0, 67 | 68 | _lastAngle = 0; 69 | 70 | const _eye = new Vector3(), 71 | 72 | _movePrev = new Vector2(), 73 | _moveCurr = new Vector2(), 74 | 75 | _lastAxis = new Vector3(), 76 | 77 | _zoomStart = new Vector2(), 78 | _zoomEnd = new Vector2(), 79 | 80 | _panStart = new Vector2(), 81 | _panEnd = new Vector2(), 82 | 83 | _pointers = [], 84 | _pointerPositions = {}; 85 | 86 | // for reset 87 | 88 | this.target0 = this.target.clone(); 89 | this.position0 = this.object.position.clone(); 90 | this.up0 = this.object.up.clone(); 91 | this.zoom0 = this.object.zoom; 92 | 93 | // methods 94 | 95 | this.handleResize = function () { 96 | 97 | const box = scope.domElement.getBoundingClientRect(); 98 | // adjustments come from similar code in the jquery offset() function 99 | const d = scope.domElement.ownerDocument.documentElement; 100 | scope.screen.left = box.left + window.pageXOffset - d.clientLeft; 101 | scope.screen.top = box.top + window.pageYOffset - d.clientTop; 102 | scope.screen.width = box.width; 103 | scope.screen.height = box.height; 104 | 105 | }; 106 | 107 | const getMouseOnScreen = ( function () { 108 | 109 | const vector = new Vector2(); 110 | 111 | return function getMouseOnScreen( pageX, pageY ) { 112 | 113 | vector.set( 114 | ( pageX - scope.screen.left ) / scope.screen.width, 115 | ( pageY - scope.screen.top ) / scope.screen.height 116 | ); 117 | 118 | return vector; 119 | 120 | }; 121 | 122 | }() ); 123 | 124 | const getMouseOnCircle = ( function () { 125 | 126 | const vector = new Vector2(); 127 | 128 | return function getMouseOnCircle( pageX, pageY ) { 129 | 130 | vector.set( 131 | ( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ) ), 132 | ( ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width ) // screen.width intentional 133 | ); 134 | 135 | return vector; 136 | 137 | }; 138 | 139 | }() ); 140 | 141 | this.rotateCamera = ( function () { 142 | 143 | const axis = new Vector3(), 144 | quaternion = new Quaternion(), 145 | eyeDirection = new Vector3(), 146 | objectUpDirection = new Vector3(), 147 | objectSidewaysDirection = new Vector3(), 148 | moveDirection = new Vector3(); 149 | 150 | return function rotateCamera() { 151 | 152 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 153 | let angle = moveDirection.length(); 154 | 155 | if ( angle ) { 156 | 157 | _eye.copy( scope.object.position ).sub( scope.target ); 158 | 159 | eyeDirection.copy( _eye ).normalize(); 160 | objectUpDirection.copy( scope.object.up ).normalize(); 161 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 162 | 163 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 164 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 165 | 166 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 167 | 168 | axis.crossVectors( moveDirection, _eye ).normalize(); 169 | 170 | angle *= scope.rotateSpeed; 171 | quaternion.setFromAxisAngle( axis, angle ); 172 | 173 | _eye.applyQuaternion( quaternion ); 174 | scope.object.up.applyQuaternion( quaternion ); 175 | 176 | _lastAxis.copy( axis ); 177 | _lastAngle = angle; 178 | 179 | } else if ( ! scope.staticMoving && _lastAngle ) { 180 | 181 | _lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor ); 182 | _eye.copy( scope.object.position ).sub( scope.target ); 183 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 184 | _eye.applyQuaternion( quaternion ); 185 | scope.object.up.applyQuaternion( quaternion ); 186 | 187 | } 188 | 189 | _movePrev.copy( _moveCurr ); 190 | 191 | }; 192 | 193 | }() ); 194 | 195 | 196 | this.zoomCamera = function () { 197 | 198 | let factor; 199 | 200 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 201 | 202 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 203 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 204 | 205 | if ( scope.object.isPerspectiveCamera ) { 206 | 207 | _eye.multiplyScalar( factor ); 208 | 209 | } else if ( scope.object.isOrthographicCamera ) { 210 | 211 | scope.object.zoom /= factor; 212 | scope.object.updateProjectionMatrix(); 213 | 214 | } else { 215 | 216 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 217 | 218 | } 219 | 220 | } else { 221 | 222 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed; 223 | 224 | if ( factor !== 1.0 && factor > 0.0 ) { 225 | 226 | if ( scope.object.isPerspectiveCamera ) { 227 | 228 | _eye.multiplyScalar( factor ); 229 | 230 | } else if ( scope.object.isOrthographicCamera ) { 231 | 232 | scope.object.zoom /= factor; 233 | scope.object.updateProjectionMatrix(); 234 | 235 | } else { 236 | 237 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 238 | 239 | } 240 | 241 | } 242 | 243 | if ( scope.staticMoving ) { 244 | 245 | _zoomStart.copy( _zoomEnd ); 246 | 247 | } else { 248 | 249 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 250 | 251 | } 252 | 253 | } 254 | 255 | }; 256 | 257 | this.panCamera = ( function () { 258 | 259 | const mouseChange = new Vector2(), 260 | objectUp = new Vector3(), 261 | pan = new Vector3(); 262 | 263 | return function panCamera() { 264 | 265 | mouseChange.copy( _panEnd ).sub( _panStart ); 266 | 267 | if ( mouseChange.lengthSq() ) { 268 | 269 | if ( scope.object.isOrthographicCamera ) { 270 | 271 | const scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth; 272 | const scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth; 273 | 274 | mouseChange.x *= scale_x; 275 | mouseChange.y *= scale_y; 276 | 277 | } 278 | 279 | mouseChange.multiplyScalar( _eye.length() * scope.panSpeed ); 280 | 281 | pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x ); 282 | pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) ); 283 | 284 | scope.object.position.add( pan ); 285 | scope.target.add( pan ); 286 | 287 | if ( scope.staticMoving ) { 288 | 289 | _panStart.copy( _panEnd ); 290 | 291 | } else { 292 | 293 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) ); 294 | 295 | } 296 | 297 | } 298 | 299 | }; 300 | 301 | }() ); 302 | 303 | this.checkDistances = function () { 304 | 305 | if ( ! scope.noZoom || ! scope.noPan ) { 306 | 307 | if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) { 308 | 309 | scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) ); 310 | _zoomStart.copy( _zoomEnd ); 311 | 312 | } 313 | 314 | if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) { 315 | 316 | scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) ); 317 | _zoomStart.copy( _zoomEnd ); 318 | 319 | } 320 | 321 | } 322 | 323 | }; 324 | 325 | this.update = function () { 326 | 327 | _eye.subVectors( scope.object.position, scope.target ); 328 | 329 | if ( ! scope.noRotate ) { 330 | 331 | scope.rotateCamera(); 332 | 333 | } 334 | 335 | if ( ! scope.noZoom ) { 336 | 337 | scope.zoomCamera(); 338 | 339 | } 340 | 341 | if ( ! scope.noPan ) { 342 | 343 | scope.panCamera(); 344 | 345 | } 346 | 347 | scope.object.position.addVectors( scope.target, _eye ); 348 | 349 | if ( scope.object.isPerspectiveCamera ) { 350 | 351 | scope.checkDistances(); 352 | 353 | scope.object.lookAt( scope.target ); 354 | 355 | if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) { 356 | 357 | scope.dispatchEvent( _changeEvent ); 358 | 359 | lastPosition.copy( scope.object.position ); 360 | 361 | } 362 | 363 | } else if ( scope.object.isOrthographicCamera ) { 364 | 365 | scope.object.lookAt( scope.target ); 366 | 367 | if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) { 368 | 369 | scope.dispatchEvent( _changeEvent ); 370 | 371 | lastPosition.copy( scope.object.position ); 372 | lastZoom = scope.object.zoom; 373 | 374 | } 375 | 376 | } else { 377 | 378 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 379 | 380 | } 381 | 382 | }; 383 | 384 | this.reset = function () { 385 | 386 | _state = STATE.NONE; 387 | _keyState = STATE.NONE; 388 | 389 | scope.target.copy( scope.target0 ); 390 | scope.object.position.copy( scope.position0 ); 391 | scope.object.up.copy( scope.up0 ); 392 | scope.object.zoom = scope.zoom0; 393 | 394 | scope.object.updateProjectionMatrix(); 395 | 396 | _eye.subVectors( scope.object.position, scope.target ); 397 | 398 | scope.object.lookAt( scope.target ); 399 | 400 | scope.dispatchEvent( _changeEvent ); 401 | 402 | lastPosition.copy( scope.object.position ); 403 | lastZoom = scope.object.zoom; 404 | 405 | }; 406 | 407 | // listeners 408 | 409 | function onPointerDown( event ) { 410 | 411 | if ( scope.enabled === false ) return; 412 | 413 | if ( _pointers.length === 0 ) { 414 | 415 | scope.domElement.setPointerCapture( event.pointerId ); 416 | 417 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 418 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 419 | 420 | } 421 | 422 | // 423 | 424 | addPointer( event ); 425 | 426 | if ( event.pointerType === 'touch' ) { 427 | 428 | onTouchStart( event ); 429 | 430 | } else { 431 | 432 | onMouseDown( event ); 433 | 434 | } 435 | 436 | } 437 | 438 | function onPointerMove( event ) { 439 | 440 | if ( scope.enabled === false ) return; 441 | 442 | if ( event.pointerType === 'touch' ) { 443 | 444 | onTouchMove( event ); 445 | 446 | } else { 447 | 448 | onMouseMove( event ); 449 | 450 | } 451 | 452 | } 453 | 454 | function onPointerUp( event ) { 455 | 456 | if ( scope.enabled === false ) return; 457 | 458 | if ( event.pointerType === 'touch' ) { 459 | 460 | onTouchEnd( event ); 461 | 462 | } else { 463 | 464 | onMouseUp(); 465 | 466 | } 467 | 468 | // 469 | 470 | removePointer( event ); 471 | 472 | if ( _pointers.length === 0 ) { 473 | 474 | scope.domElement.releasePointerCapture( event.pointerId ); 475 | 476 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 477 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 478 | 479 | } 480 | 481 | 482 | } 483 | 484 | function onPointerCancel( event ) { 485 | 486 | removePointer( event ); 487 | 488 | } 489 | 490 | function keydown( event ) { 491 | 492 | if ( scope.enabled === false ) return; 493 | 494 | window.removeEventListener( 'keydown', keydown ); 495 | 496 | if ( _keyState !== STATE.NONE ) { 497 | 498 | return; 499 | 500 | } else if ( event.code === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) { 501 | 502 | _keyState = STATE.ROTATE; 503 | 504 | } else if ( event.code === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) { 505 | 506 | _keyState = STATE.ZOOM; 507 | 508 | } else if ( event.code === scope.keys[ STATE.PAN ] && ! scope.noPan ) { 509 | 510 | _keyState = STATE.PAN; 511 | 512 | } 513 | 514 | } 515 | 516 | function keyup() { 517 | 518 | if ( scope.enabled === false ) return; 519 | 520 | _keyState = STATE.NONE; 521 | 522 | window.addEventListener( 'keydown', keydown ); 523 | 524 | } 525 | 526 | function onMouseDown( event ) { 527 | 528 | if ( _state === STATE.NONE ) { 529 | 530 | switch ( event.button ) { 531 | 532 | case scope.mouseButtons.LEFT: 533 | _state = STATE.ROTATE; 534 | break; 535 | 536 | case scope.mouseButtons.MIDDLE: 537 | _state = STATE.ZOOM; 538 | break; 539 | 540 | case scope.mouseButtons.RIGHT: 541 | _state = STATE.PAN; 542 | break; 543 | 544 | default: 545 | _state = STATE.NONE; 546 | 547 | } 548 | 549 | } 550 | 551 | const state = ( _keyState !== STATE.NONE ) ? _keyState : _state; 552 | 553 | if ( state === STATE.ROTATE && ! scope.noRotate ) { 554 | 555 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 556 | _movePrev.copy( _moveCurr ); 557 | 558 | } else if ( state === STATE.ZOOM && ! scope.noZoom ) { 559 | 560 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 561 | _zoomEnd.copy( _zoomStart ); 562 | 563 | } else if ( state === STATE.PAN && ! scope.noPan ) { 564 | 565 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 566 | _panEnd.copy( _panStart ); 567 | 568 | } 569 | 570 | scope.dispatchEvent( _startEvent ); 571 | 572 | } 573 | 574 | function onMouseMove( event ) { 575 | 576 | const state = ( _keyState !== STATE.NONE ) ? _keyState : _state; 577 | 578 | if ( state === STATE.ROTATE && ! scope.noRotate ) { 579 | 580 | _movePrev.copy( _moveCurr ); 581 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 582 | 583 | } else if ( state === STATE.ZOOM && ! scope.noZoom ) { 584 | 585 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 586 | 587 | } else if ( state === STATE.PAN && ! scope.noPan ) { 588 | 589 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 590 | 591 | } 592 | 593 | } 594 | 595 | function onMouseUp() { 596 | 597 | _state = STATE.NONE; 598 | 599 | scope.dispatchEvent( _endEvent ); 600 | 601 | } 602 | 603 | function onMouseWheel( event ) { 604 | 605 | if ( scope.enabled === false ) return; 606 | 607 | if ( scope.noZoom === true ) return; 608 | 609 | event.preventDefault(); 610 | 611 | switch ( event.deltaMode ) { 612 | 613 | case 2: 614 | // Zoom in pages 615 | _zoomStart.y -= event.deltaY * 0.025; 616 | break; 617 | 618 | case 1: 619 | // Zoom in lines 620 | _zoomStart.y -= event.deltaY * 0.01; 621 | break; 622 | 623 | default: 624 | // undefined, 0, assume pixels 625 | _zoomStart.y -= event.deltaY * 0.00025; 626 | break; 627 | 628 | } 629 | 630 | scope.dispatchEvent( _startEvent ); 631 | scope.dispatchEvent( _endEvent ); 632 | 633 | } 634 | 635 | function onTouchStart( event ) { 636 | 637 | trackPointer( event ); 638 | 639 | switch ( _pointers.length ) { 640 | 641 | case 1: 642 | _state = STATE.TOUCH_ROTATE; 643 | _moveCurr.copy( getMouseOnCircle( _pointers[ 0 ].pageX, _pointers[ 0 ].pageY ) ); 644 | _movePrev.copy( _moveCurr ); 645 | break; 646 | 647 | default: // 2 or more 648 | _state = STATE.TOUCH_ZOOM_PAN; 649 | const dx = _pointers[ 0 ].pageX - _pointers[ 1 ].pageX; 650 | const dy = _pointers[ 0 ].pageY - _pointers[ 1 ].pageY; 651 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 652 | 653 | const x = ( _pointers[ 0 ].pageX + _pointers[ 1 ].pageX ) / 2; 654 | const y = ( _pointers[ 0 ].pageY + _pointers[ 1 ].pageY ) / 2; 655 | _panStart.copy( getMouseOnScreen( x, y ) ); 656 | _panEnd.copy( _panStart ); 657 | break; 658 | 659 | } 660 | 661 | scope.dispatchEvent( _startEvent ); 662 | 663 | } 664 | 665 | function onTouchMove( event ) { 666 | 667 | trackPointer( event ); 668 | 669 | switch ( _pointers.length ) { 670 | 671 | case 1: 672 | _movePrev.copy( _moveCurr ); 673 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 674 | break; 675 | 676 | default: // 2 or more 677 | 678 | const position = getSecondPointerPosition( event ); 679 | 680 | const dx = event.pageX - position.x; 681 | const dy = event.pageY - position.y; 682 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 683 | 684 | const x = ( event.pageX + position.x ) / 2; 685 | const y = ( event.pageY + position.y ) / 2; 686 | _panEnd.copy( getMouseOnScreen( x, y ) ); 687 | break; 688 | 689 | } 690 | 691 | } 692 | 693 | function onTouchEnd( event ) { 694 | 695 | switch ( _pointers.length ) { 696 | 697 | case 0: 698 | _state = STATE.NONE; 699 | break; 700 | 701 | case 1: 702 | _state = STATE.TOUCH_ROTATE; 703 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 704 | _movePrev.copy( _moveCurr ); 705 | break; 706 | 707 | case 2: 708 | _state = STATE.TOUCH_ZOOM_PAN; 709 | _moveCurr.copy( getMouseOnCircle( event.pageX - _movePrev.pageX, event.pageY - _movePrev.pageY ) ); 710 | _movePrev.copy( _moveCurr ); 711 | break; 712 | 713 | } 714 | 715 | scope.dispatchEvent( _endEvent ); 716 | 717 | } 718 | 719 | function contextmenu( event ) { 720 | 721 | if ( scope.enabled === false ) return; 722 | 723 | event.preventDefault(); 724 | 725 | } 726 | 727 | function addPointer( event ) { 728 | 729 | _pointers.push( event ); 730 | 731 | } 732 | 733 | function removePointer( event ) { 734 | 735 | delete _pointerPositions[ event.pointerId ]; 736 | 737 | for ( let i = 0; i < _pointers.length; i ++ ) { 738 | 739 | if ( _pointers[ i ].pointerId == event.pointerId ) { 740 | 741 | _pointers.splice( i, 1 ); 742 | return; 743 | 744 | } 745 | 746 | } 747 | 748 | } 749 | 750 | function trackPointer( event ) { 751 | 752 | let position = _pointerPositions[ event.pointerId ]; 753 | 754 | if ( position === undefined ) { 755 | 756 | position = new Vector2(); 757 | _pointerPositions[ event.pointerId ] = position; 758 | 759 | } 760 | 761 | position.set( event.pageX, event.pageY ); 762 | 763 | } 764 | 765 | function getSecondPointerPosition( event ) { 766 | 767 | const pointer = ( event.pointerId === _pointers[ 0 ].pointerId ) ? _pointers[ 1 ] : _pointers[ 0 ]; 768 | 769 | return _pointerPositions[ pointer.pointerId ]; 770 | 771 | } 772 | 773 | this.dispose = function () { 774 | 775 | scope.domElement.removeEventListener( 'contextmenu', contextmenu ); 776 | 777 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 778 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 779 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 780 | 781 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 782 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 783 | 784 | window.removeEventListener( 'keydown', keydown ); 785 | window.removeEventListener( 'keyup', keyup ); 786 | 787 | }; 788 | 789 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 790 | 791 | this.domElement.addEventListener( 'pointerdown', onPointerDown ); 792 | this.domElement.addEventListener( 'pointercancel', onPointerCancel ); 793 | this.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 794 | 795 | 796 | window.addEventListener( 'keydown', keydown ); 797 | window.addEventListener( 'keyup', keyup ); 798 | 799 | this.handleResize(); 800 | 801 | // force an update at start 802 | this.update(); 803 | 804 | } 805 | 806 | } 807 | 808 | export { TrackballControls }; -------------------------------------------------------------------------------- /OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from '/three.module.js'; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 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 left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor(object, domElement) { 25 | 26 | super(); 27 | 28 | if (domElement === undefined) console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.'); 29 | if (domElement === document) console.error('THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); 30 | 31 | this.object = object; 32 | this.domElement = domElement; 33 | this.domElement.style.touchAction = 'none'; // disable touch scroll 34 | 35 | // Set to false to disable this control 36 | this.enabled = true; 37 | 38 | // "target" sets the location of focus, where the object orbits around 39 | this.target = new Vector3(); 40 | 41 | // How far you can dolly in and out ( PerspectiveCamera only ) 42 | this.minDistance = 0; 43 | this.maxDistance = Infinity; 44 | 45 | // How far you can zoom in and out ( OrthographicCamera only ) 46 | this.minZoom = 0; 47 | this.maxZoom = Infinity; 48 | 49 | // How far you can orbit vertically, upper and lower limits. 50 | // Range is 0 to Math.PI radians. 51 | this.minPolarAngle = 0; // radians 52 | this.maxPolarAngle = Math.PI; // radians 53 | 54 | // How far you can orbit horizontally, upper and lower limits. 55 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 56 | this.minAzimuthAngle = - Infinity; // radians 57 | this.maxAzimuthAngle = Infinity; // radians 58 | 59 | // Set to true to enable damping (inertia) 60 | // If damping is enabled, you must call controls.update() in your animation loop 61 | this.enableDamping = false; 62 | this.dampingFactor = 0.05; 63 | 64 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 65 | // Set to false to disable zooming 66 | this.enableZoom = true; 67 | this.zoomSpeed = 1.0; 68 | 69 | // Set to false to disable rotating 70 | this.enableRotate = true; 71 | this.rotateSpeed = 1.0; 72 | 73 | // Set to false to disable panning 74 | this.enablePan = true; 75 | this.panSpeed = 1.0; 76 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 77 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 78 | 79 | // Set to true to automatically rotate around the target 80 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 81 | this.autoRotate = false; 82 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 83 | 84 | // The four arrow keys 85 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 86 | 87 | // Mouse buttons 88 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 89 | 90 | // Touch fingers 91 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 92 | 93 | // for reset 94 | this.target0 = this.target.clone(); 95 | this.position0 = this.object.position.clone(); 96 | this.zoom0 = this.object.zoom; 97 | 98 | // the target DOM element for key events 99 | this._domElementKeyEvents = null; 100 | 101 | // 102 | // public methods 103 | // 104 | 105 | this.getPolarAngle = function () { 106 | 107 | return spherical.phi; 108 | 109 | }; 110 | 111 | this.getAzimuthalAngle = function () { 112 | 113 | return spherical.theta; 114 | 115 | }; 116 | 117 | this.getDistance = function () { 118 | 119 | return this.object.position.distanceTo(this.target); 120 | 121 | }; 122 | 123 | this.listenToKeyEvents = function (domElement) { 124 | 125 | domElement.addEventListener('keydown', onKeyDown); 126 | this._domElementKeyEvents = domElement; 127 | 128 | }; 129 | 130 | this.saveState = function () { 131 | 132 | scope.target0.copy(scope.target); 133 | scope.position0.copy(scope.object.position); 134 | scope.zoom0 = scope.object.zoom; 135 | 136 | }; 137 | 138 | this.reset = function () { 139 | 140 | scope.target.copy(scope.target0); 141 | scope.object.position.copy(scope.position0); 142 | scope.object.zoom = scope.zoom0; 143 | 144 | scope.object.updateProjectionMatrix(); 145 | scope.dispatchEvent(_changeEvent); 146 | 147 | scope.update(); 148 | 149 | state = STATE.NONE; 150 | 151 | }; 152 | 153 | // this method is exposed, but perhaps it would be better if we can make it private... 154 | this.update = function () { 155 | 156 | const offset = new Vector3(); 157 | 158 | // so camera.up is the orbit axis 159 | const quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)); 160 | const quatInverse = quat.clone().invert(); 161 | 162 | const lastPosition = new Vector3(); 163 | const lastQuaternion = new Quaternion(); 164 | 165 | const twoPI = 2 * Math.PI; 166 | 167 | return function update () { 168 | 169 | const position = scope.object.position; 170 | 171 | offset.copy(position).sub(scope.target); 172 | 173 | // rotate offset to "y-axis-is-up" space 174 | offset.applyQuaternion(quat); 175 | 176 | // angle from z-axis around y-axis 177 | spherical.setFromVector3(offset); 178 | 179 | if (scope.autoRotate && state === STATE.NONE) { 180 | 181 | rotateLeft(getAutoRotationAngle()); 182 | 183 | } 184 | 185 | if (scope.enableDamping) { 186 | 187 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 188 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 189 | 190 | } else { 191 | 192 | spherical.theta += sphericalDelta.theta; 193 | spherical.phi += sphericalDelta.phi; 194 | 195 | } 196 | 197 | // restrict theta to be between desired limits 198 | 199 | let min = scope.minAzimuthAngle; 200 | let max = scope.maxAzimuthAngle; 201 | 202 | if (isFinite(min) && isFinite(max)) { 203 | 204 | if (min < - Math.PI) min += twoPI; else if (min > Math.PI) min -= twoPI; 205 | 206 | if (max < - Math.PI) max += twoPI; else if (max > Math.PI) max -= twoPI; 207 | 208 | if (min <= max) { 209 | 210 | spherical.theta = Math.max(min, Math.min(max, spherical.theta)); 211 | 212 | } else { 213 | 214 | spherical.theta = (spherical.theta > (min + max) / 2) ? 215 | Math.max(min, spherical.theta) : 216 | Math.min(max, spherical.theta); 217 | 218 | } 219 | 220 | } 221 | 222 | // restrict phi to be between desired limits 223 | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); 224 | 225 | spherical.makeSafe(); 226 | 227 | 228 | spherical.radius *= scale; 229 | 230 | // restrict radius to be between desired limits 231 | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); 232 | 233 | // move target to panned location 234 | 235 | if (scope.enableDamping === true) { 236 | 237 | scope.target.addScaledVector(panOffset, scope.dampingFactor); 238 | 239 | } else { 240 | 241 | scope.target.add(panOffset); 242 | 243 | } 244 | 245 | offset.setFromSpherical(spherical); 246 | 247 | // rotate offset back to "camera-up-vector-is-up" space 248 | offset.applyQuaternion(quatInverse); 249 | 250 | position.copy(scope.target).add(offset); 251 | 252 | scope.object.lookAt(scope.target); 253 | 254 | if (scope.enableDamping === true) { 255 | 256 | sphericalDelta.theta *= (1 - scope.dampingFactor); 257 | sphericalDelta.phi *= (1 - scope.dampingFactor); 258 | 259 | panOffset.multiplyScalar(1 - scope.dampingFactor); 260 | 261 | } else { 262 | 263 | sphericalDelta.set(0, 0, 0); 264 | 265 | panOffset.set(0, 0, 0); 266 | 267 | } 268 | 269 | scale = 1; 270 | 271 | // update condition is: 272 | // min(camera displacement, camera rotation in radians)^2 > EPS 273 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 274 | 275 | if (zoomChanged || 276 | lastPosition.distanceToSquared(scope.object.position) > EPS || 277 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { 278 | 279 | scope.dispatchEvent(_changeEvent); 280 | 281 | lastPosition.copy(scope.object.position); 282 | lastQuaternion.copy(scope.object.quaternion); 283 | zoomChanged = false; 284 | 285 | return true; 286 | 287 | } 288 | 289 | return false; 290 | 291 | }; 292 | 293 | }(); 294 | 295 | this.dispose = function () { 296 | 297 | scope.domElement.removeEventListener('contextmenu', onContextMenu); 298 | 299 | scope.domElement.removeEventListener('pointerdown', onPointerDown); 300 | scope.domElement.removeEventListener('pointercancel', onPointerCancel); 301 | scope.domElement.removeEventListener('wheel', onMouseWheel); 302 | 303 | scope.domElement.removeEventListener('pointermove', onPointerMove); 304 | scope.domElement.removeEventListener('pointerup', onPointerUp); 305 | 306 | 307 | if (scope._domElementKeyEvents !== null) { 308 | 309 | scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown); 310 | 311 | } 312 | 313 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 314 | 315 | }; 316 | 317 | // 318 | // internals 319 | // 320 | 321 | const scope = this; 322 | 323 | const STATE = { 324 | NONE: - 1, 325 | ROTATE: 0, 326 | DOLLY: 1, 327 | PAN: 2, 328 | TOUCH_ROTATE: 3, 329 | TOUCH_PAN: 4, 330 | TOUCH_DOLLY_PAN: 5, 331 | TOUCH_DOLLY_ROTATE: 6 332 | }; 333 | 334 | let state = STATE.NONE; 335 | 336 | const EPS = 0.000001; 337 | 338 | // current position in spherical coordinates 339 | const spherical = new Spherical(); 340 | const sphericalDelta = new Spherical(); 341 | 342 | let scale = 1; 343 | const panOffset = new Vector3(); 344 | let zoomChanged = false; 345 | 346 | const rotateStart = new Vector2(); 347 | const rotateEnd = new Vector2(); 348 | const rotateDelta = new Vector2(); 349 | 350 | const panStart = new Vector2(); 351 | const panEnd = new Vector2(); 352 | const panDelta = new Vector2(); 353 | 354 | const dollyStart = new Vector2(); 355 | const dollyEnd = new Vector2(); 356 | const dollyDelta = new Vector2(); 357 | 358 | const pointers = []; 359 | const pointerPositions = {}; 360 | 361 | function getAutoRotationAngle () { 362 | 363 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 364 | 365 | } 366 | 367 | function getZoomScale () { 368 | 369 | return Math.pow(0.95, scope.zoomSpeed); 370 | 371 | } 372 | 373 | function rotateLeft (angle) { 374 | 375 | sphericalDelta.theta -= angle; 376 | 377 | } 378 | 379 | function rotateUp (angle) { 380 | 381 | sphericalDelta.phi -= angle; 382 | 383 | } 384 | 385 | const panLeft = function () { 386 | 387 | const v = new Vector3(); 388 | 389 | return function panLeft (distance, objectMatrix) { 390 | 391 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 392 | v.multiplyScalar(- distance); 393 | 394 | panOffset.add(v); 395 | 396 | }; 397 | 398 | }(); 399 | 400 | const panUp = function () { 401 | 402 | const v = new Vector3(); 403 | 404 | return function panUp (distance, objectMatrix) { 405 | 406 | if (scope.screenSpacePanning === true) { 407 | 408 | v.setFromMatrixColumn(objectMatrix, 1); 409 | 410 | } else { 411 | 412 | v.setFromMatrixColumn(objectMatrix, 0); 413 | v.crossVectors(scope.object.up, v); 414 | 415 | } 416 | 417 | v.multiplyScalar(distance); 418 | 419 | panOffset.add(v); 420 | 421 | }; 422 | 423 | }(); 424 | 425 | // deltaX and deltaY are in pixels; right and down are positive 426 | const pan = function () { 427 | 428 | const offset = new Vector3(); 429 | 430 | return function pan (deltaX, deltaY) { 431 | 432 | const element = scope.domElement; 433 | 434 | if (scope.object.isPerspectiveCamera) { 435 | 436 | // perspective 437 | const position = scope.object.position; 438 | offset.copy(position).sub(scope.target); 439 | let targetDistance = offset.length(); 440 | 441 | // half of the fov is center to top of screen 442 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); 443 | 444 | // we use only clientHeight here so aspect ratio does not distort speed 445 | panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); 446 | panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); 447 | 448 | } else if (scope.object.isOrthographicCamera) { 449 | 450 | // orthographic 451 | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); 452 | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); 453 | 454 | } else { 455 | 456 | // camera neither orthographic nor perspective 457 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); 458 | scope.enablePan = false; 459 | 460 | } 461 | 462 | }; 463 | 464 | }(); 465 | 466 | function dollyOut (dollyScale) { 467 | 468 | if (scope.object.isPerspectiveCamera) { 469 | 470 | scale /= dollyScale; 471 | 472 | } else if (scope.object.isOrthographicCamera) { 473 | 474 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); 475 | scope.object.updateProjectionMatrix(); 476 | zoomChanged = true; 477 | 478 | } else { 479 | 480 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 481 | scope.enableZoom = false; 482 | 483 | } 484 | 485 | } 486 | 487 | function dollyIn (dollyScale) { 488 | 489 | if (scope.object.isPerspectiveCamera) { 490 | 491 | scale *= dollyScale; 492 | 493 | } else if (scope.object.isOrthographicCamera) { 494 | 495 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); 496 | scope.object.updateProjectionMatrix(); 497 | zoomChanged = true; 498 | 499 | } else { 500 | 501 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 502 | scope.enableZoom = false; 503 | 504 | } 505 | 506 | } 507 | 508 | // 509 | // event callbacks - update the object state 510 | // 511 | 512 | function handleMouseDownRotate (event) { 513 | 514 | rotateStart.set(event.clientX, event.clientY); 515 | 516 | } 517 | 518 | function handleMouseDownDolly (event) { 519 | 520 | dollyStart.set(event.clientX, event.clientY); 521 | 522 | } 523 | 524 | function handleMouseDownPan (event) { 525 | 526 | panStart.set(event.clientX, event.clientY); 527 | 528 | } 529 | 530 | function handleMouseMoveRotate (event) { 531 | 532 | rotateEnd.set(event.clientX, event.clientY); 533 | 534 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 535 | 536 | const element = scope.domElement; 537 | 538 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height 539 | 540 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); 541 | 542 | rotateStart.copy(rotateEnd); 543 | 544 | scope.update(); 545 | 546 | } 547 | 548 | function handleMouseMoveDolly (event) { 549 | 550 | dollyEnd.set(event.clientX, event.clientY); 551 | 552 | dollyDelta.subVectors(dollyEnd, dollyStart); 553 | 554 | if (dollyDelta.y > 0) { 555 | 556 | dollyOut(getZoomScale()); 557 | 558 | } else if (dollyDelta.y < 0) { 559 | 560 | dollyIn(getZoomScale()); 561 | 562 | } 563 | 564 | dollyStart.copy(dollyEnd); 565 | 566 | scope.update(); 567 | 568 | } 569 | 570 | function handleMouseMovePan (event) { 571 | 572 | panEnd.set(event.clientX, event.clientY); 573 | 574 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 575 | 576 | pan(panDelta.x, panDelta.y); 577 | 578 | panStart.copy(panEnd); 579 | 580 | scope.update(); 581 | 582 | } 583 | 584 | function handleMouseUp ( /*event*/) { 585 | 586 | // no-op 587 | 588 | } 589 | 590 | function handleMouseWheel (event) { 591 | 592 | if (event.deltaY < 0) { 593 | 594 | dollyIn(getZoomScale()); 595 | 596 | } else if (event.deltaY > 0) { 597 | 598 | dollyOut(getZoomScale()); 599 | 600 | } 601 | 602 | scope.update(); 603 | 604 | } 605 | 606 | function handleKeyDown (event) { 607 | 608 | let needsUpdate = false; 609 | 610 | switch (event.code) { 611 | 612 | case scope.keys.UP: 613 | pan(0, scope.keyPanSpeed); 614 | needsUpdate = true; 615 | break; 616 | 617 | case scope.keys.BOTTOM: 618 | pan(0, - scope.keyPanSpeed); 619 | needsUpdate = true; 620 | break; 621 | 622 | case scope.keys.LEFT: 623 | pan(scope.keyPanSpeed, 0); 624 | needsUpdate = true; 625 | break; 626 | 627 | case scope.keys.RIGHT: 628 | pan(- scope.keyPanSpeed, 0); 629 | needsUpdate = true; 630 | break; 631 | 632 | } 633 | 634 | if (needsUpdate) { 635 | 636 | // prevent the browser from scrolling on cursor keys 637 | event.preventDefault(); 638 | 639 | scope.update(); 640 | 641 | } 642 | 643 | 644 | } 645 | 646 | function handleTouchStartRotate () { 647 | 648 | if (pointers.length === 1) { 649 | 650 | rotateStart.set(pointers[0].pageX, pointers[0].pageY); 651 | 652 | } else { 653 | 654 | const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); 655 | const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); 656 | 657 | rotateStart.set(x, y); 658 | 659 | } 660 | 661 | } 662 | 663 | function handleTouchStartPan () { 664 | 665 | if (pointers.length === 1) { 666 | 667 | panStart.set(pointers[0].pageX, pointers[0].pageY); 668 | 669 | } else { 670 | 671 | const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); 672 | const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); 673 | 674 | panStart.set(x, y); 675 | 676 | } 677 | 678 | } 679 | 680 | function handleTouchStartDolly () { 681 | 682 | const dx = pointers[0].pageX - pointers[1].pageX; 683 | const dy = pointers[0].pageY - pointers[1].pageY; 684 | 685 | const distance = Math.sqrt(dx * dx + dy * dy); 686 | 687 | dollyStart.set(0, distance); 688 | 689 | } 690 | 691 | function handleTouchStartDollyPan () { 692 | 693 | if (scope.enableZoom) handleTouchStartDolly(); 694 | 695 | if (scope.enablePan) handleTouchStartPan(); 696 | 697 | } 698 | 699 | function handleTouchStartDollyRotate () { 700 | 701 | if (scope.enableZoom) handleTouchStartDolly(); 702 | 703 | if (scope.enableRotate) handleTouchStartRotate(); 704 | 705 | } 706 | 707 | function handleTouchMoveRotate (event) { 708 | 709 | if (pointers.length == 1) { 710 | 711 | rotateEnd.set(event.pageX, event.pageY); 712 | 713 | } else { 714 | 715 | const position = getSecondPointerPosition(event); 716 | 717 | const x = 0.5 * (event.pageX + position.x); 718 | const y = 0.5 * (event.pageY + position.y); 719 | 720 | rotateEnd.set(x, y); 721 | 722 | } 723 | 724 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 725 | 726 | const element = scope.domElement; 727 | 728 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height 729 | 730 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); 731 | 732 | rotateStart.copy(rotateEnd); 733 | 734 | } 735 | 736 | function handleTouchMovePan (event) { 737 | 738 | if (pointers.length === 1) { 739 | 740 | panEnd.set(event.pageX, event.pageY); 741 | 742 | } else { 743 | 744 | const position = getSecondPointerPosition(event); 745 | 746 | const x = 0.5 * (event.pageX + position.x); 747 | const y = 0.5 * (event.pageY + position.y); 748 | 749 | panEnd.set(x, y); 750 | 751 | } 752 | 753 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 754 | 755 | pan(panDelta.x, panDelta.y); 756 | 757 | panStart.copy(panEnd); 758 | 759 | } 760 | 761 | function handleTouchMoveDolly (event) { 762 | 763 | const position = getSecondPointerPosition(event); 764 | 765 | const dx = event.pageX - position.x; 766 | const dy = event.pageY - position.y; 767 | 768 | const distance = Math.sqrt(dx * dx + dy * dy); 769 | 770 | dollyEnd.set(0, distance); 771 | 772 | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); 773 | 774 | dollyOut(dollyDelta.y); 775 | 776 | dollyStart.copy(dollyEnd); 777 | 778 | } 779 | 780 | function handleTouchMoveDollyPan (event) { 781 | 782 | if (scope.enableZoom) handleTouchMoveDolly(event); 783 | 784 | if (scope.enablePan) handleTouchMovePan(event); 785 | 786 | } 787 | 788 | function handleTouchMoveDollyRotate (event) { 789 | 790 | if (scope.enableZoom) handleTouchMoveDolly(event); 791 | 792 | if (scope.enableRotate) handleTouchMoveRotate(event); 793 | 794 | } 795 | 796 | function handleTouchEnd ( /*event*/) { 797 | 798 | // no-op 799 | 800 | } 801 | 802 | // 803 | // event handlers - FSM: listen for events and reset state 804 | // 805 | 806 | function onPointerDown (event) { 807 | 808 | if (scope.enabled === false) return; 809 | 810 | if (pointers.length === 0) { 811 | 812 | scope.domElement.setPointerCapture(event.pointerId); 813 | 814 | scope.domElement.addEventListener('pointermove', onPointerMove); 815 | scope.domElement.addEventListener('pointerup', onPointerUp); 816 | 817 | } 818 | 819 | // 820 | 821 | addPointer(event); 822 | 823 | if (event.pointerType === 'touch') { 824 | 825 | onTouchStart(event); 826 | 827 | } else { 828 | 829 | onMouseDown(event); 830 | 831 | } 832 | 833 | } 834 | 835 | function onPointerMove (event) { 836 | 837 | if (scope.enabled === false) return; 838 | 839 | if (event.pointerType === 'touch') { 840 | 841 | onTouchMove(event); 842 | 843 | } else { 844 | 845 | onMouseMove(event); 846 | 847 | } 848 | 849 | } 850 | 851 | function onPointerUp (event) { 852 | 853 | if (scope.enabled === false) return; 854 | 855 | if (event.pointerType === 'touch') { 856 | 857 | onTouchEnd(); 858 | 859 | } else { 860 | 861 | onMouseUp(event); 862 | 863 | } 864 | 865 | removePointer(event); 866 | 867 | // 868 | 869 | if (pointers.length === 0) { 870 | 871 | scope.domElement.releasePointerCapture(event.pointerId); 872 | 873 | scope.domElement.removeEventListener('pointermove', onPointerMove); 874 | scope.domElement.removeEventListener('pointerup', onPointerUp); 875 | 876 | } 877 | 878 | } 879 | 880 | function onPointerCancel (event) { 881 | 882 | removePointer(event); 883 | 884 | } 885 | 886 | function onMouseDown (event) { 887 | 888 | let mouseAction; 889 | 890 | switch (event.button) { 891 | 892 | case 0: 893 | 894 | mouseAction = scope.mouseButtons.LEFT; 895 | break; 896 | 897 | case 1: 898 | 899 | mouseAction = scope.mouseButtons.MIDDLE; 900 | break; 901 | 902 | case 2: 903 | 904 | mouseAction = scope.mouseButtons.RIGHT; 905 | break; 906 | 907 | default: 908 | 909 | mouseAction = - 1; 910 | 911 | } 912 | 913 | switch (mouseAction) { 914 | 915 | case MOUSE.DOLLY: 916 | 917 | if (scope.enableZoom === false) return; 918 | 919 | handleMouseDownDolly(event); 920 | 921 | state = STATE.DOLLY; 922 | 923 | break; 924 | 925 | case MOUSE.ROTATE: 926 | 927 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 928 | 929 | if (scope.enablePan === false) return; 930 | 931 | handleMouseDownPan(event); 932 | 933 | state = STATE.PAN; 934 | 935 | } else { 936 | 937 | if (scope.enableRotate === false) return; 938 | 939 | handleMouseDownRotate(event); 940 | 941 | state = STATE.ROTATE; 942 | 943 | } 944 | 945 | break; 946 | 947 | case MOUSE.PAN: 948 | 949 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 950 | 951 | if (scope.enableRotate === false) return; 952 | 953 | handleMouseDownRotate(event); 954 | 955 | state = STATE.ROTATE; 956 | 957 | } else { 958 | 959 | if (scope.enablePan === false) return; 960 | 961 | handleMouseDownPan(event); 962 | 963 | state = STATE.PAN; 964 | 965 | } 966 | 967 | break; 968 | 969 | default: 970 | 971 | state = STATE.NONE; 972 | 973 | } 974 | 975 | if (state !== STATE.NONE) { 976 | 977 | scope.dispatchEvent(_startEvent); 978 | 979 | } 980 | 981 | } 982 | 983 | function onMouseMove (event) { 984 | 985 | if (scope.enabled === false) return; 986 | 987 | switch (state) { 988 | 989 | case STATE.ROTATE: 990 | 991 | if (scope.enableRotate === false) return; 992 | 993 | handleMouseMoveRotate(event); 994 | 995 | break; 996 | 997 | case STATE.DOLLY: 998 | 999 | if (scope.enableZoom === false) return; 1000 | 1001 | handleMouseMoveDolly(event); 1002 | 1003 | break; 1004 | 1005 | case STATE.PAN: 1006 | 1007 | if (scope.enablePan === false) return; 1008 | 1009 | handleMouseMovePan(event); 1010 | 1011 | break; 1012 | 1013 | } 1014 | 1015 | } 1016 | 1017 | function onMouseUp (event) { 1018 | 1019 | handleMouseUp(event); 1020 | 1021 | scope.dispatchEvent(_endEvent); 1022 | 1023 | state = STATE.NONE; 1024 | 1025 | } 1026 | 1027 | function onMouseWheel (event) { 1028 | 1029 | if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE) return; 1030 | 1031 | event.preventDefault(); 1032 | 1033 | scope.dispatchEvent(_startEvent); 1034 | 1035 | handleMouseWheel(event); 1036 | 1037 | scope.dispatchEvent(_endEvent); 1038 | 1039 | } 1040 | 1041 | function onKeyDown (event) { 1042 | 1043 | if (scope.enabled === false || scope.enablePan === false) return; 1044 | 1045 | handleKeyDown(event); 1046 | 1047 | } 1048 | 1049 | function onTouchStart (event) { 1050 | 1051 | trackPointer(event); 1052 | 1053 | switch (pointers.length) { 1054 | 1055 | case 1: 1056 | 1057 | switch (scope.touches.ONE) { 1058 | 1059 | case TOUCH.ROTATE: 1060 | 1061 | if (scope.enableRotate === false) return; 1062 | 1063 | handleTouchStartRotate(); 1064 | 1065 | state = STATE.TOUCH_ROTATE; 1066 | 1067 | break; 1068 | 1069 | case TOUCH.PAN: 1070 | 1071 | if (scope.enablePan === false) return; 1072 | 1073 | handleTouchStartPan(); 1074 | 1075 | state = STATE.TOUCH_PAN; 1076 | 1077 | break; 1078 | 1079 | default: 1080 | 1081 | state = STATE.NONE; 1082 | 1083 | } 1084 | 1085 | break; 1086 | 1087 | case 2: 1088 | 1089 | switch (scope.touches.TWO) { 1090 | 1091 | case TOUCH.DOLLY_PAN: 1092 | 1093 | if (scope.enableZoom === false && scope.enablePan === false) return; 1094 | 1095 | handleTouchStartDollyPan(); 1096 | 1097 | state = STATE.TOUCH_DOLLY_PAN; 1098 | 1099 | break; 1100 | 1101 | case TOUCH.DOLLY_ROTATE: 1102 | 1103 | if (scope.enableZoom === false && scope.enableRotate === false) return; 1104 | 1105 | handleTouchStartDollyRotate(); 1106 | 1107 | state = STATE.TOUCH_DOLLY_ROTATE; 1108 | 1109 | break; 1110 | 1111 | default: 1112 | 1113 | state = STATE.NONE; 1114 | 1115 | } 1116 | 1117 | break; 1118 | 1119 | default: 1120 | 1121 | state = STATE.NONE; 1122 | 1123 | } 1124 | 1125 | if (state !== STATE.NONE) { 1126 | 1127 | scope.dispatchEvent(_startEvent); 1128 | 1129 | } 1130 | 1131 | } 1132 | 1133 | function onTouchMove (event) { 1134 | 1135 | trackPointer(event); 1136 | 1137 | switch (state) { 1138 | 1139 | case STATE.TOUCH_ROTATE: 1140 | 1141 | if (scope.enableRotate === false) return; 1142 | 1143 | handleTouchMoveRotate(event); 1144 | 1145 | scope.update(); 1146 | 1147 | break; 1148 | 1149 | case STATE.TOUCH_PAN: 1150 | 1151 | if (scope.enablePan === false) return; 1152 | 1153 | handleTouchMovePan(event); 1154 | 1155 | scope.update(); 1156 | 1157 | break; 1158 | 1159 | case STATE.TOUCH_DOLLY_PAN: 1160 | 1161 | if (scope.enableZoom === false && scope.enablePan === false) return; 1162 | 1163 | handleTouchMoveDollyPan(event); 1164 | 1165 | scope.update(); 1166 | 1167 | break; 1168 | 1169 | case STATE.TOUCH_DOLLY_ROTATE: 1170 | 1171 | if (scope.enableZoom === false && scope.enableRotate === false) return; 1172 | 1173 | handleTouchMoveDollyRotate(event); 1174 | 1175 | scope.update(); 1176 | 1177 | break; 1178 | 1179 | default: 1180 | 1181 | state = STATE.NONE; 1182 | 1183 | } 1184 | 1185 | } 1186 | 1187 | function onTouchEnd (event) { 1188 | 1189 | handleTouchEnd(event); 1190 | 1191 | scope.dispatchEvent(_endEvent); 1192 | 1193 | state = STATE.NONE; 1194 | 1195 | } 1196 | 1197 | function onContextMenu (event) { 1198 | 1199 | if (scope.enabled === false) return; 1200 | 1201 | event.preventDefault(); 1202 | 1203 | } 1204 | 1205 | function addPointer (event) { 1206 | 1207 | pointers.push(event); 1208 | 1209 | } 1210 | 1211 | function removePointer (event) { 1212 | 1213 | delete pointerPositions[event.pointerId]; 1214 | 1215 | for (let i = 0; i < pointers.length; i++) { 1216 | 1217 | if (pointers[i].pointerId == event.pointerId) { 1218 | 1219 | pointers.splice(i, 1); 1220 | return; 1221 | 1222 | } 1223 | 1224 | } 1225 | 1226 | } 1227 | 1228 | function trackPointer (event) { 1229 | 1230 | let position = pointerPositions[event.pointerId]; 1231 | 1232 | if (position === undefined) { 1233 | 1234 | position = new Vector2(); 1235 | pointerPositions[event.pointerId] = position; 1236 | 1237 | } 1238 | 1239 | position.set(event.pageX, event.pageY); 1240 | 1241 | } 1242 | 1243 | function getSecondPointerPosition (event) { 1244 | 1245 | const pointer = (event.pointerId === pointers[0].pointerId) ? pointers[1] : pointers[0]; 1246 | 1247 | return pointerPositions[pointer.pointerId]; 1248 | 1249 | } 1250 | 1251 | // 1252 | 1253 | scope.domElement.addEventListener('contextmenu', onContextMenu); 1254 | 1255 | scope.domElement.addEventListener('pointerdown', onPointerDown); 1256 | scope.domElement.addEventListener('pointercancel', onPointerCancel); 1257 | scope.domElement.addEventListener('wheel', onMouseWheel, { passive: false }); 1258 | 1259 | // force an update at start 1260 | 1261 | this.update(); 1262 | 1263 | } 1264 | 1265 | } 1266 | 1267 | 1268 | // This set of controls performs orbiting, dollying (zooming), and panning. 1269 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1270 | // This is very similar to OrbitControls, another set of touch behavior 1271 | // 1272 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1273 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1274 | // Pan - left mouse, or arrow keys / touch: one-finger move 1275 | 1276 | class MapControls extends OrbitControls { 1277 | 1278 | constructor(object, domElement) { 1279 | 1280 | super(object, domElement); 1281 | 1282 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1283 | 1284 | this.mouseButtons.LEFT = MOUSE.PAN; 1285 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1286 | 1287 | this.touches.ONE = TOUCH.PAN; 1288 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1289 | 1290 | } 1291 | 1292 | } 1293 | 1294 | export { OrbitControls, MapControls }; --------------------------------------------------------------------------------