├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .node-version ├── .npmignore ├── .releaserc.json ├── CONTRIBUTING.md ├── LICENSE ├── eslint.config.mjs ├── examples ├── PseudoElement.js ├── auto-rotate.html ├── await-transitions.html ├── basic.html ├── boundary.html ├── camera-up.html ├── click-to-set-orbit-point.html ├── collision-custom.html ├── collision.html ├── combined-gestures.html ├── config.html ├── cursor.html ├── dist ├── easing.html ├── effect-shake.html ├── env.jpg ├── event-attach.html ├── fig1.svg ├── first-person.html ├── fit-and-padding.html ├── fit-to-bounding-sphere.html ├── fit-to-rect.html ├── focal-offset.html ├── iframe-child.html ├── iframe.html ├── infinity-dolly.html ├── keyboard.html ├── look-in-direction.html ├── mouse-drag-with-modifier-keys.html ├── multiple.html ├── orthographic.html ├── padding-with-view-offset.html ├── path-animation.html ├── pointer-lock.html ├── rest-and-sleep.html ├── rubber-duck.glb ├── ruler.png ├── style.css ├── view-offset.html ├── viewport.html ├── worker.html └── worker.js ├── package-lock.json ├── package.json ├── readme.md ├── rollup.config.mjs ├── src ├── CameraControls.ts ├── EventDispatcher.ts ├── constants.ts ├── index.ts ├── types.ts └── utils │ ├── extractClientCoordFromEvent.ts │ ├── math-utils.ts │ └── notSupportedInOrthographicCamera.ts ├── tsconfig.json └── typedoc.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:18", 3 | // "hostRequirements": { 4 | // "memory": "8gb" 5 | // }, 6 | "waitFor": "onCreateCommand", 7 | "updateContentCommand": "npm ci", 8 | "postCreateCommand": "", 9 | "postAttachCommand": "npm run dev", 10 | "customizations": { 11 | "codespaces": { 12 | "openFiles": ["CONTRIBUTING.md", "src/CameraControls.ts"] 13 | }, 14 | "vscode": { 15 | "settings": {}, 16 | "extensions": ["dbaeumer.vscode-eslint"] 17 | } 18 | }, 19 | "portsAttributes": { 20 | "3000": { 21 | "label": "examples static server", 22 | "onAutoForward": "openPreview" 23 | } 24 | }, 25 | "forwardPorts": [3000] 26 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [yomotsu] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | # title: "" 3 | description: Create a report to help us improve 4 | labels: [] 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: Describe the bug 10 | description: A clear and concise description of what the bug is. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: reproduce 16 | attributes: 17 | label: To Reproduce 18 | placeholder: | 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. See error 23 | value: | 24 | Steps to reproduce the behavior: 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. See error 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: code 33 | attributes: 34 | label: Code 35 | render: js 36 | placeholder: | 37 | // code goes here 38 | 39 | - type: textarea 40 | id: live-example 41 | attributes: 42 | label: Live example 43 | description: Link URL of live examples. 44 | placeholder: | 45 | https://codesandbox.io/s/react-three-fiber-camera-controls-4jjor 46 | 47 | - type: textarea 48 | id: expected-behavior 49 | attributes: 50 | label: Expected behavior 51 | description: A clear and concise description of what you expected to happen. 52 | validations: 53 | required: true 54 | 55 | - type: textarea 56 | id: screenshots-or-video 57 | attributes: 58 | label: Screenshots or Video 59 | description: If applicable, add screenshots or videos to help explain your problem. 60 | 61 | - type: markdown 62 | attributes: 63 | value: | 64 | ## Platform 65 | 66 | - type: dropdown 67 | id: platform-device 68 | attributes: 69 | label: Device 70 | multiple: true 71 | options: 72 | - Desktop 73 | - Mobile 74 | 75 | - type: dropdown 76 | id: platform-os 77 | attributes: 78 | label: OS 79 | multiple: true 80 | options: 81 | - Windows 82 | - MacOS 83 | - Linux 84 | - Android 85 | - iOS 86 | 87 | - type: dropdown 88 | id: platform-browser 89 | attributes: 90 | label: Browser 91 | multiple: true 92 | options: 93 | - Chrome 94 | - Firefox 95 | - Safari 96 | - Edge 97 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | # title: "" 3 | description: Suggest an idea for this project 4 | labels: [] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: solution 15 | attributes: 16 | label: Describe the solution you'd like 17 | description: A clear and concise description of what you want to happen. 18 | - type: textarea 19 | id: alternatives 20 | attributes: 21 | label: Describe alternatives you've considered 22 | description: A clear and concise description of any alternative solutions or features you've considered. 23 | - type: textarea 24 | id: additional-context 25 | attributes: 26 | label: Additional context 27 | description: Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release to npm 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | 7 | # Cancel any previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | 14 | release-job: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | released: ${{ steps.main.outputs.released }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: "20" 23 | cache: "npm" 24 | - id: main 25 | run: | 26 | npm ci 27 | npm run release 28 | if [ -d "dist/" ]; then 29 | echo "released=true" >> "$GITHUB_OUTPUT" 30 | 31 | npm run typedoc 32 | 33 | # Remove any unnecessary files or directories that might contain symlinks or large files 34 | # For example, remove node_modules from 'examples' to avoid copying symlinks 35 | rm -rf examples/node_modules 36 | 37 | # Copy only the required content to the docs folder 38 | mkdir -p docs 39 | cp -r dist/* docs/ 40 | cp -r examples docs/ 41 | 42 | # Delete symbolic links under the docs directory. 43 | find docs -type l -exec rm {} \; 44 | fi 45 | env: 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | - if: steps.main.outputs.released == 'true' 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: github-pages 52 | path: ./docs 53 | 54 | # See: https://github.com/actions/deploy-pages 55 | deploy-job: 56 | needs: release-job 57 | if: needs.release-job.outputs.released == 'true' 58 | permissions: 59 | pages: write 60 | id-token: write 61 | environment: 62 | name: github-pages 63 | url: ${{ steps.deployment.outputs.page_url }} 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Deploy to GitHub Pages 67 | id: deployment 68 | uses: actions/deploy-pages@v4 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | Thumbs.db 3 | 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | *.map 9 | 10 | node_modules 11 | yarn.lock 12 | docs 13 | 14 | /dist -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*": "npx eslint src --fix" 3 | } 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.11.0 -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/* 2 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Pre-requisites: 2 | 3 | - `npm ci` 4 | 5 | ```sh 6 | $ npm run dev 7 | $ open http://localhost:3000/basic.html 8 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 @yomotsu 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 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import tsPlugin from "@typescript-eslint/eslint-plugin"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import styPlugin from "@stylistic/eslint-plugin"; 5 | 6 | // https://www.npmjs.com/package/eslint-config-mdcs 7 | const mdcs = { 8 | globals: { 9 | THREE: "readonly", 10 | console: "writable", 11 | }, 12 | 13 | rules: { 14 | "array-bracket-spacing": [ 15 | "error", 16 | "always", 17 | { singleValue: true, arraysInArrays: false } 18 | ], 19 | "block-spacing": [ "error", "always" ], 20 | "brace-style": [ "error", "1tbs", { allowSingleLine: true } ], 21 | "comma-spacing": [ "error", { before: false, after: true } ], 22 | "comma-style": [ "error", "last" ], 23 | "computed-property-spacing": [ "error", "always" ], 24 | "eol-last": [ "error", "always" ], 25 | "func-call-spacing": [ "error", "never" ], 26 | "indent": [ "error", "tab", { SwitchCase: 1 } ], 27 | "key-spacing": [ "error", { beforeColon: false } ], 28 | "new-parens": "error", 29 | "no-trailing-spaces": [ "error", { skipBlankLines: false } ], 30 | "no-whitespace-before-property": "error", 31 | "object-curly-spacing": [ "error", "always" ], 32 | "padded-blocks": [ 33 | "error", 34 | { 35 | blocks: "always", 36 | switches: "always", 37 | classes: "always" 38 | } 39 | ], 40 | "semi": [ 41 | "error", 42 | "always", 43 | { "omitLastInOneLineBlock": true } 44 | ], 45 | "semi-spacing": [ "error", { before: false, after: true } ], 46 | "space-before-blocks": [ "error", { 47 | functions: "always", 48 | keywords: "always", 49 | classes: "always" 50 | } ], 51 | "space-before-function-paren": [ 52 | "error", 53 | { 54 | anonymous: "always", 55 | named: "never", 56 | asyncArrow: "ignore" 57 | } 58 | ], 59 | "space-in-parens": [ "error", "always" ], 60 | "space-infix-ops": [ "error" ], 61 | "space-unary-ops": [ 62 | "error", 63 | { 64 | words: true, 65 | nonwords: true, 66 | overrides: {} 67 | } 68 | ], 69 | "keyword-spacing": [ "error", { before: true, after: true } ], 70 | "padding-line-between-statements": [ 71 | "error", 72 | { blankLine: "always", prev: "block-like", next: "*" } 73 | ], 74 | "no-multi-spaces": "error", 75 | "no-undef": "warn", 76 | "no-unused-vars": "warn", 77 | "no-extra-semi": "warn" 78 | } 79 | }; 80 | 81 | export default [ 82 | { 83 | files: [ "src/**/*.ts" ], 84 | // ignores: [], 85 | languageOptions: { 86 | parser: tsParser, 87 | globals: { 88 | ...globals.browser, 89 | ...globals.node, 90 | ...mdcs.globals, 91 | }, 92 | }, 93 | 94 | plugins: { 95 | "@typescript-eslint": tsPlugin, 96 | "@stylistic": styPlugin, 97 | }, 98 | 99 | rules: { 100 | ...mdcs.rules, 101 | "no-unused-vars": 0, 102 | "@typescript-eslint/no-unused-vars": 1, 103 | "indent": 0, 104 | "@stylistic/indent": [ 105 | "error", 106 | "tab", 107 | { 108 | SwitchCase: 1, 109 | flatTernaryExpressions: true, 110 | }, 111 | ], 112 | "no-multi-spaces": [ 0 ], 113 | "no-trailing-spaces": [ 114 | "error", 115 | { 116 | ignoreComments: true, 117 | }, 118 | ], 119 | "key-spacing": [ 0 ], 120 | }, 121 | }, 122 | ]; 123 | -------------------------------------------------------------------------------- /examples/PseudoElement.js: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from '../dist/camera-controls.module.js'; 2 | 3 | /** 4 | * Represents a pseudo element, works in WebWorker. 5 | * @extends EventDispatcher 6 | */ 7 | export class PseudoElement extends EventDispatcher { 8 | 9 | /** @type {PseudoDocument} */ 10 | ownerDocument = new PseudoDocument(); 11 | /** @type {any} */ 12 | style = {}; 13 | /** @private @type {DOMRect} */ 14 | _domRect = new DOMRect(); 15 | 16 | constructor() { 17 | 18 | super(); 19 | 20 | } 21 | 22 | /** 23 | * @returns {DOMRect} The DOMRect. 24 | */ 25 | getBoundingClientRect() { 26 | 27 | return DOMRect.fromRect( this._domRect ); 28 | 29 | } 30 | 31 | /** 32 | * Just for compatibility. doesn't work. 33 | * @returns {Promise} A promise that rejects. 34 | */ 35 | requestPointerLock() { 36 | 37 | return Promise.reject(); 38 | 39 | } 40 | 41 | /** 42 | * Just for compatibility. do nothing. 43 | * @param {string} _ The attribute name. 44 | * @param {string} __ The attribute value. 45 | */ 46 | setAttribute( _, __ ) {} 47 | 48 | /** 49 | * Updates the PseudoElement size based on a given bound. 50 | * @param {number} x The new bound x. 51 | * @param {number} y The new bound y. 52 | * @param {number} width The new bound width. 53 | * @param {number} height The new bound height. 54 | */ 55 | update( x, y, width, height ) { 56 | 57 | this._domRect.x = x; 58 | this._domRect.y = y; 59 | this._domRect.width = width; 60 | this._domRect.height = height; 61 | 62 | } 63 | 64 | } 65 | 66 | /** 67 | * Represents a pseudo document. 68 | * @extends EventDispatcher 69 | */ 70 | class PseudoDocument extends EventDispatcher { 71 | 72 | /** 73 | * Creates an instance of PseudoDocument. 74 | */ 75 | constructor() { 76 | 77 | super(); 78 | 79 | } 80 | 81 | /** 82 | * Exits pointer lock. 83 | */ 84 | exitPointerLock() {} 85 | 86 | /** 87 | * Just for compatibility. do nothing. 88 | * @returns {PseudoElement|null} The element locking the pointer. 89 | */ 90 | get pointerLockElement() { 91 | 92 | return null; 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /examples/auto-rotate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 33 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /examples/await-transitions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 |

Transition promises will resolve when the camera slows to less than controls.restThreshold units in a single frame. You can set this value here

16 |
17 | 18 | 19 | 27 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 |
37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 | 45 |
46 | 47 | 48 |
49 | 50 | 51 | 59 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /examples/boundary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 |
13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | 42 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /examples/camera-up.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 | 41 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /examples/click-to-set-orbit-point.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 |
15 | 16 | 17 | 25 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/collision-custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 33 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /examples/collision.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 33 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /examples/combined-gestures.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 17 |
18 | 19 | 20 | 28 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

12 | GitHub repo
13 | see also mouse-drag-with-modifier-keys 14 |

15 | 16 |
17 | cameraControls.mouseButtons.left = 18 | 28 |
29 | cameraControls.mouseButtons.middle = 30 | 40 |
41 | cameraControls.mouseButtons.right = 42 | 52 |
53 | cameraControls.mouseButtons.wheel = 54 | 64 |
65 | 66 | cameraControls.touches.one = 67 | 76 |
77 | cameraControls.touches.two = 78 | 94 |
95 | cameraControls.touches.three = 96 | 112 |
113 | 114 | 115 | 123 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /examples/cursor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 22 | 23 | 24 |
25 |

GitHub repo

26 | 27 |
28 | 29 |
30 | 31 | 32 | 40 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /examples/dist: -------------------------------------------------------------------------------- 1 | ../dist -------------------------------------------------------------------------------- /examples/easing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 | GitHub repo
12 |
13 |
14 |
15 |
16 | 17 | 18 | 26 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /examples/effect-shake.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 |
16 | 17 | 18 | 26 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /examples/env.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomotsu/camera-controls/dddd5608c4ea99dcad8813a24a8572542af3910c/examples/env.jpg -------------------------------------------------------------------------------- /examples/event-attach.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 |
16 | 17 | 18 | 26 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/fig1.svg: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Azimuth angle 50 | (theta) 51 | 52 | 53 | Polar- 54 | angle 55 | (phi) 56 | 57 | z 58 | y 59 | x 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/first-person.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 | 33 |
34 | cameraControls.mouseButtons.wheel = 35 | 39 |
40 | 41 | 42 | 50 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /examples/fit-and-padding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 | 12 |
13 | GitHub repo
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 33 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /examples/fit-to-bounding-sphere.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 | 12 |
13 | GitHub repo
14 | 15 |
16 |
17 | 18 | 19 | 27 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /examples/fit-to-rect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 | 11 |
12 | GitHub repo
13 | 14 | 15 |
16 |
17 | 18 | 19 | 27 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/focal-offset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 | 43 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /examples/iframe-child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 | 11 | 12 | 20 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /examples/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/infinity-dolly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 |

If you enable infinityDolly, you must set min and max distance as well.

13 |
14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 | 36 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /examples/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 |

13 | W, A, S, D keys to move
14 | arrow keys (left, right, up, down) to rotate 15 |

16 | 17 |
18 | 19 | 20 | 28 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /examples/look-in-direction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | look in the direction: 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 28 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /examples/mouse-drag-with-modifier-keys.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 18 |
19 | 20 | 21 | 29 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /examples/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 |
15 | 16 | 17 | 25 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /examples/orthographic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 | 48 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /examples/padding-with-view-offset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 27 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /examples/path-animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 | 28 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /examples/pointer-lock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 |
13 |
14 | 15 |
16 | 17 | 18 | 26 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/rest-and-sleep.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 | 14 | 15 |

These buttons will be disabled whenever a transition is occuring and then re-enabled when the transition is complete.

16 |
17 | 18 |

The rest event will fire when the camera slows to less than controls.restThreshold units in a single frame. You can set this value here

19 | 20 |

The sleep event will fire once the camera completely stops. Due to damping, this is often long after it appears to have stopped.

21 | 22 |
23 | 24 | 25 | 33 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /examples/rubber-duck.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomotsu/camera-controls/dddd5608c4ea99dcad8813a24a8572542af3910c/examples/rubber-duck.glb -------------------------------------------------------------------------------- /examples/ruler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomotsu/camera-controls/dddd5608c4ea99dcad8813a24a8572542af3910c/examples/ruler.png -------------------------------------------------------------------------------- /examples/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | canvas { 6 | display: block; 7 | } 8 | 9 | .info { 10 | color: #FFF; 11 | position: absolute; 12 | padding: 20px 0 0 20px; 13 | } 14 | 15 | a { 16 | color: inherit; 17 | } 18 | 19 | label { 20 | cursor: pointer; 21 | } 22 | 23 | .ruler { 24 | pointer-events: none; 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | right: 0; 29 | bottom: 0; 30 | background: url( "./ruler.png" ) no-repeat 0 0; 31 | } 32 | -------------------------------------------------------------------------------- /examples/view-offset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | right button drag to translate the view offset. 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 31 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /examples/viewport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | viewport example - camera-controls 7 | 8 | 9 | 10 | 11 |
12 |

13 | viewport example - camera-controls
14 |

15 |

16 |
17 |
18 |
19 | 20 |

21 |
22 | 23 | 24 | 32 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /examples/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | =^.^= 7 | 8 | 9 | 10 |
11 |

GitHub repo

12 | 13 |
14 | 15 | 16 | 17 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /examples/worker.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://unpkg.com/three@0.161.0/build/three.module.js'; 2 | import CameraControls from '../dist/camera-controls.module.js'; 3 | import { PseudoElement } from './PseudoElement.js'; 4 | 5 | CameraControls.install( { THREE } ); 6 | 7 | // DOM element doesn't exist in WebWorker. use a virtual element in CameraControls instead. 8 | const pseudoElement = new PseudoElement(); 9 | let scene; 10 | let camera; 11 | let renderer; 12 | let cameraControls; 13 | 14 | self.onmessage = ( { data } ) => { 15 | 16 | const { action, payload } = data; 17 | 18 | switch ( action ) { 19 | 20 | case 'init': { 21 | 22 | const { canvas, x, y, width, height } = payload; 23 | canvas.style = { width: '', height: '' }; 24 | 25 | const clock = new THREE.Clock(); 26 | scene = new THREE.Scene(); 27 | camera = new THREE.PerspectiveCamera( 60, width / height, 0.01, 100 ); 28 | camera.position.set( 0, 0, 5 ); 29 | renderer = new THREE.WebGLRenderer( { canvas } ); 30 | renderer.setSize( width, height ); 31 | 32 | pseudoElement.update( x, y, width, height ); 33 | cameraControls = new CameraControls( camera, pseudoElement, self ); 34 | cameraControls.addEventListener( 'controlend', () => self.postMessage( { action: 'controlend' } ) ); 35 | 36 | const mesh = new THREE.Mesh( 37 | new THREE.BoxGeometry( 1, 1, 1 ), 38 | new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } ) 39 | ); 40 | scene.add( mesh ); 41 | 42 | const gridHelper = new THREE.GridHelper( 50, 50 ); 43 | gridHelper.position.y = - 1; 44 | scene.add( gridHelper ); 45 | 46 | renderer.render( scene, camera ); 47 | 48 | ( function anim() { 49 | 50 | const delta = clock.getDelta(); 51 | // const elapsed = clock.getElapsedTime(); 52 | const updated = cameraControls.update( delta ); 53 | 54 | // if ( elapsed > 30 ) return; 55 | 56 | requestAnimationFrame( anim ); 57 | 58 | if ( updated ) { 59 | 60 | renderer.render( scene, camera ); 61 | self.postMessage( 'rendered' ); 62 | 63 | } 64 | 65 | } )(); 66 | 67 | break; 68 | 69 | } 70 | 71 | case 'resize': { 72 | 73 | const { x, y, width, height } = payload; 74 | pseudoElement.update( x, y, width, height ); 75 | renderer.setSize( width, height ); 76 | camera.aspect = width / height; 77 | camera.updateProjectionMatrix(); 78 | renderer.render( scene, camera ); 79 | break; 80 | 81 | } 82 | 83 | case 'pointerdown': { 84 | 85 | const { event } = payload; 86 | pseudoElement.dispatchEvent( { type: action, ...event } ); 87 | break; 88 | 89 | } 90 | 91 | case 'pointermove': 92 | case 'pointerup': { 93 | 94 | const { event } = payload; 95 | pseudoElement.ownerDocument.dispatchEvent( { type: action, ...event } ); 96 | break; 97 | 98 | } 99 | 100 | case 'reset': { 101 | 102 | cameraControls.reset( true ); 103 | break; 104 | 105 | } 106 | 107 | } 108 | 109 | // debug log 110 | self.postMessage( data.action ); 111 | 112 | }; 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camera-controls", 3 | "version": "0.0.0-semantic-release", 4 | "author": "Yomotsu", 5 | "license": "MIT", 6 | "main": "dist/camera-controls.cjs", 7 | "module": "dist/camera-controls.module.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "repository": "yomotsu/camera-controls", 13 | "devDependencies": { 14 | "@rollup/plugin-replace": "^6.0.2", 15 | "@rollup/plugin-typescript": "^12.1.2", 16 | "@stylistic/eslint-plugin": "^4.2.0", 17 | "@types/three": "^0.174.0", 18 | "@typescript-eslint/eslint-plugin": "^8.26.1", 19 | "@typescript-eslint/parser": "^8.26.1", 20 | "downlevel-dts": "^0.11.0", 21 | "eslint": "^9.22.0", 22 | "globals": "^16.0.0", 23 | "husky": "^9.1.7", 24 | "lint-staged": "^15.4.3", 25 | "npm-run-all": "^4.1.5", 26 | "open-cli": "^8.0.0", 27 | "rollup": "^4.35.0", 28 | "semantic-release": "^23.1.1", 29 | "serve": "^14.2.4", 30 | "terser": "^5.39.0", 31 | "three": "^0.174.0", 32 | "tslib": "^2.8.1", 33 | "typedoc": "^0.27.9", 34 | "typescript": "^5.8.2" 35 | }, 36 | "peerDependencies": { 37 | "three": ">=0.126.1" 38 | }, 39 | "scripts": { 40 | "prepare": "husky install", 41 | "dev": "npm-run-all -p dev:*", 42 | "dev:rollup": "rollup --config --watch", 43 | "dev:serve": "serve -S -p 3000 ./ && kill $!", 44 | "dev:open": "open-cli http://localhost:3000/examples/", 45 | "build": "rollup --config && terser dist/camera-controls.module.js -o dist/camera-controls.module.min.js --comments '/^!/' && downlevel-dts . .", 46 | "prepack": "npm run build", 47 | "lint": "eslint src", 48 | "typedoc": "typedoc", 49 | "release": "semantic-release" 50 | }, 51 | "browserslist": [ 52 | "> 1%", 53 | "last 3 versions", 54 | "not dead" 55 | ], 56 | "typesVersions": { 57 | "<=3.4.0-0": { 58 | "*": [ 59 | "./" 60 | ] 61 | } 62 | }, 63 | "keywords": [ 64 | "three", 65 | "three.js", 66 | "orbit", 67 | "controls", 68 | "OrbitControls", 69 | "camera" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # camera-controls 2 | 3 | A camera control for three.js, similar to THREE.OrbitControls yet supports smooth transitions and more features. 4 | 5 | [![Latest NPM release](https://img.shields.io/npm/v/camera-controls.svg)](https://www.npmjs.com/package/camera-controls) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?label=GitHub&message=Open%20in%20%20Codespaces&color=24292f)](https://github.com/codespaces/new?template_repository=yomotsu%2Fcamera-controls) 6 | 7 | [documentation](https://yomotsu.github.io/camera-controls/classes/CameraControls) 8 | 9 | ## Examples 10 | 11 | | camera move | default user input (Configurable) | 12 | | --- | --- | 13 | | Orbit rotation | left mouse drag / touch: one-finger move | 14 | | Dolly | middle mouse drag, or mousewheel / touch: two-finger pinch-in or out | 15 | | Truck (Pan) | right mouse drag / touch: two-finger move or three-finger move | 16 | 17 | - [basic](https://yomotsu.github.io/camera-controls/examples/basic.html) 18 | - [fit-and-padding](https://yomotsu.github.io/camera-controls/examples/fit-and-padding.html) 19 | - [fit-to-rect](https://yomotsu.github.io/camera-controls/examples/fit-to-rect.html) 20 | - [fit-to-bounding-sphere](https://yomotsu.github.io/camera-controls/examples/fit-to-bounding-sphere.html) 21 | - [infinity dolly](https://yomotsu.github.io/camera-controls/examples/infinity-dolly.html) 22 | - [boundary](https://yomotsu.github.io/camera-controls/examples/boundary.html) 23 | - [focal offset](https://yomotsu.github.io/camera-controls/examples/focal-offset.html) 24 | - [click to set orbit point](https://yomotsu.github.io/camera-controls/examples/click-to-set-orbit-point.html) 25 | - [look in the point direction](https://yomotsu.github.io/camera-controls/examples/look-in-direction.html) 26 | - [viewport within the canvas](https://yomotsu.github.io/camera-controls/examples/viewport.html) 27 | - [multiple camera-controls and viewport](https://yomotsu.github.io/camera-controls/examples/multiple.html) 28 | - [z-up camera](https://yomotsu.github.io/camera-controls/examples/camera-up.html) 29 | - [orthographic](https://yomotsu.github.io/camera-controls/examples/orthographic.html) 30 | - [event attach / detach](https://yomotsu.github.io/camera-controls/examples/event-attach.html) 31 | - [user input config](https://yomotsu.github.io/camera-controls/examples/config.html) 32 | - [mouse drag with modifier keys](https://yomotsu.github.io/camera-controls/examples/mouse-drag-with-modifier-keys.html) 33 | - [combined gestures](https://yomotsu.github.io/camera-controls/examples/combined-gestures.html) 34 | - [keyboard events](https://yomotsu.github.io/camera-controls/examples/keyboard.html) 35 | - [rest and sleep events](https://yomotsu.github.io/camera-controls/examples/rest-and-sleep.html) 36 | - [changing the cursor](https://yomotsu.github.io/camera-controls/examples/cursor.html) 37 | - [collision](https://yomotsu.github.io/camera-controls/examples/collision.html) 38 | - [collision (custom)](https://yomotsu.github.io/camera-controls/examples/collision-custom.html) 39 | - [first-person](https://yomotsu.github.io/camera-controls/examples/first-person.html) 40 | - [third-person](https://yomotsu.github.io/meshwalk/examples/5_terrain.html) (with [meshwalk](https://github.com/yomotsu/meshwalk)) 41 | - [pointer lock](https://yomotsu.github.io/camera-controls/examples/pointer-lock.html) 42 | - [auto rotate](https://yomotsu.github.io/camera-controls/examples/auto-rotate.html) 43 | - [view offset translate](https://yomotsu.github.io/camera-controls/examples/view-offset.html) 44 | - [camera shake effect](https://yomotsu.github.io/camera-controls/examples/effect-shake.html) 45 | - [rotate with time duration and easing](https://yomotsu.github.io/camera-controls/examples/easing.html) (with [gsap](https://www.npmjs.com/package/gsap)) 46 | - [path animation](https://yomotsu.github.io/camera-controls/examples/path-animation.html) (with [gsap](https://www.npmjs.com/package/gsap)) 47 | - [complex transitions with `await`](https://yomotsu.github.io/camera-controls/examples/await-transitions.html) 48 | - [set view padding](https://yomotsu.github.io/camera-controls/examples/padding-with-view-offset.html) 49 | - [WebWorker (OffscreenCanvas)](https://yomotsu.github.io/camera-controls/examples/worker.html) 50 | - [outside of iframe dragging](https://yomotsu.github.io/camera-controls/examples/iframe.html) 51 | - [in react-three-fiber (simplest)](https://codesandbox.io/s/react-three-fiber-camera-controls-4jjor?file=/src/App.tsx) 52 | - [in react-three-fiber (drei official)](https://codesandbox.io/s/sew669) (see [doc](https://drei.docs.pmnd.rs/controls/camera-controls)) 53 | 54 | ## Usage 55 | 56 | (The below code is for three.js users. If you use react-three-fiber (aka R3F), r3f-ready camera-controls is available on [@react-three/drei](https://github.com/pmndrs/drei#cameracontrols) 57 | 58 | ```javascript 59 | import * as THREE from 'three'; 60 | import CameraControls from 'camera-controls'; 61 | 62 | CameraControls.install( { THREE: THREE } ); 63 | 64 | // snip ( init three scene... ) 65 | const clock = new THREE.Clock(); 66 | const camera = new THREE.PerspectiveCamera( 60, width / height, 0.01, 1000 ); 67 | const cameraControls = new CameraControls( camera, renderer.domElement ); 68 | 69 | ( function anim () { 70 | 71 | // snip 72 | const delta = clock.getDelta(); 73 | const hasControlsUpdated = cameraControls.update( delta ); 74 | 75 | requestAnimationFrame( anim ); 76 | 77 | // you can skip this condition to render though 78 | if ( hasControlsUpdated ) { 79 | 80 | renderer.render( scene, camera ); 81 | 82 | } 83 | 84 | } )(); 85 | ``` 86 | 87 | ### Important! 88 | 89 | You *must install* three.js before using camera-controls. Not doing so will lead to runtime errors (undefined references to THREE). 90 | 91 | **Before creating a new CameraControls instance, call**: 92 | ```javascript 93 | CameraControls.install( { THREE: THREE } ); 94 | ``` 95 | 96 | You can then proceed to use CameraControls. 97 | 98 | Note: If you do not wish to use the entire three.js to reduce file size(tree-shaking for example), make a subset to install. 99 | 100 | ```js 101 | import { 102 | Vector2, 103 | Vector3, 104 | Vector4, 105 | Quaternion, 106 | Matrix4, 107 | Spherical, 108 | Box3, 109 | Sphere, 110 | Raycaster, 111 | } from 'three'; 112 | 113 | const subsetOfTHREE = { 114 | Vector2 : Vector2, 115 | Vector3 : Vector3, 116 | Vector4 : Vector4, 117 | Quaternion: Quaternion, 118 | Matrix4 : Matrix4, 119 | Spherical : Spherical, 120 | Box3 : Box3, 121 | Sphere : Sphere, 122 | Raycaster : Raycaster, 123 | }; 124 | 125 | CameraControls.install( { THREE: subsetOfTHREE } ); 126 | ``` 127 | 128 | ## Constructor 129 | 130 | `CameraControls( camera, domElement )` 131 | 132 | - `camera` is a `THREE.PerspectiveCamera` or `THREE.OrthographicCamera` to be controlled. 133 | - `domElement` is a `HTMLElement` for draggable area. (optional. if domElement is omitted here, can be connect later with `.connect()`) 134 | 135 | ## Terms 136 | 137 | ### Orbit rotations 138 | 139 | CameraControls uses Spherical Coordinates for orbit rotations. 140 | 141 | If your camera is Y-up, the Azimuthal angle will be the angle for y-axis rotation and the Polar angle will be the angle for vertical position. 142 | 143 | ![](https://raw.githubusercontent.com/yomotsu/camera-controls/dev/examples/fig1.svg) 144 | 145 | 146 | ### Dolly vs Zoom 147 | 148 | - A Zoom involves changing the lens focal length. In three.js, zooming is actually changing the camera FOV, and the camera is stationary (doesn't move). 149 | - A Dolly involves physically moving the camera to change the composition of the image in the frame. 150 | 151 | See [the demo](https://github.com/yomotsu/camera-movement-comparison#dolly-vs-zoom) 152 | 153 | ## Properties 154 | 155 | | Name | Type | Default | Description | 156 | | ------------------------- | --------- | ----------- | ----------- | 157 | | `.camera` | `THREE.Perspective \| THREE.Orthographic` | N/A | The camera to be controlled | 158 | | `.enabled` | `boolean` | `true` | Whether or not the controls are enabled. | 159 | | `.active` | `boolean` | `false` | Returns `true` if the controls are active updating. | 160 | | `.currentAction` | `ACTION` | N/A | Getter for the current `ACTION`. | 161 | | `.distance` | `number` | N/A | Current distance. | 162 | | `.minDistance` | `number` | `Number.EPSILON` | Minimum distance for dolly. The value must be higher than `0` | 163 | | `.maxDistance` | `number` | `Infinity` | Maximum distance for dolly. | 164 | | `.minZoom` | `number` | `0.01` | Minimum camera zoom. | 165 | | `.maxZoom` | `number` | `Infinity` | Maximum camera zoom. | 166 | | `.polarAngle` | `number` | N/A | Current polarAngle in radians. | 167 | | `.minPolarAngle` | `number` | `0` | In radians. | 168 | | `.maxPolarAngle` | `number` | `Math.PI` | In radians. | 169 | | `.azimuthAngle` | `number` | N/A | current azimuthAngle in radians ¹. | 170 | | `.minAzimuthAngle` | `number` | `-Infinity` | In radians. | 171 | | `.maxAzimuthAngle` | `number` | `Infinity` | In radians. | 172 | | `.boundaryFriction` | `number` | `0.0` | Friction ratio of the boundary. | 173 | | `.boundaryEnclosesCamera` | `boolean` | `false` | Whether camera position should be enclosed in the boundary or not. | 174 | | `.smoothTime` | `number` | `0.25` | Approximate time in seconds to reach the target. A smaller value will reach the target faster. | 175 | | `.draggingSmoothTime` | `number` | `0.125` | The smoothTime while dragging. | 176 | | `.azimuthRotateSpeed` | `number` | `1.0` | Speed of azimuth rotation. | 177 | | `.polarRotateSpeed` | `number` | `1.0` | Speed of polar rotation. | 178 | | `.dollySpeed` | `number` | `1.0` | Speed of mouse-wheel dollying. | 179 | | `.truckSpeed` | `number` | `2.0` | Speed of drag for truck and pedestal. | 180 | | `.dollyToCursor` | `boolean` | `false` | `true` to enable Dolly-in to the mouse cursor coords. | 181 | | `.dollyDragInverted` | `boolean` | `false` | `true` to invert direction when dollying or zooming via drag. | 182 | | `.interactiveArea` | `DOMRect` | N/A | Set drag-start, touches and wheel enable area in the domElement. each values are between `0` and `1` inclusive, where `0` is left/top and `1` is right/bottom of the screen. | 183 | | `.colliderMeshes` | `array` | `[]` | An array of Meshes to collide with camera ². | 184 | | `.infinityDolly` | `boolean` | `false` | `true` to enable Infinity Dolly for wheel and pinch. Use this with `minDistance` and `maxDistance` ³. | 185 | | `.restThreshold` | `number` | `0.0025` | Controls how soon the `rest` event fires as the camera slows | 186 | 187 | 1. Every 360 degrees turn is added to `.azimuthAngle` value, which is accumulative. 188 | `360º = 360 * THREE.MathUtils.DEG2RAD = Math.PI * 2`, `720º = Math.PI * 4`. 189 | **Tip**: [How to normalize accumulated azimuthAngle?](#tips) 190 | 2. Be aware colliderMeshes may decrease performance. The collision test uses 4 raycasters from the camera since the near plane has 4 corners. 191 | 3. If the Dolly distance is less (or over) than the `minDistance` (or `maxDistance`), `infinityDolly` will keep the distance and pushes the target position instead. 192 | 193 | ## Events 194 | 195 | CameraControls instance emits the following events. 196 | To subscribe, use `cameraControl.addEventListener( 'eventname', function )`. 197 | To unsubscribe, use `cameraControl.removeEventListener( 'eventname', function )`. 198 | 199 | | Event name | Timing | 200 | | ------------------- | ------ | 201 | | `'controlstart'` | When the user starts to control the camera via mouse / touches. ¹ | 202 | | `'control'` | When the user controls the camera (dragging). | 203 | | `'controlend'` | When the user ends to control the camera. ¹ | 204 | | `'transitionstart'` | When any kind of transition starts, either user control or using a method with `enableTransition = true` | 205 | | `'update'` | When the camera position is updated. | 206 | | `'wake'` | When the camera starts moving. | 207 | | `'rest'` | When the camera movement is below `.restThreshold` ². | 208 | | `'sleep'` | When the camera end moving. | 209 | 210 | 1. `mouseButtons.wheel` (Mouse wheel control) does not emit `'controlstart'` and `'controlend'`. `mouseButtons.wheel` uses scroll-event internally, and scroll-event happens intermittently. That means "start" and "end" cannot be detected. 211 | 2. Due to damping, `sleep` will usually fire a few seconds after the camera _appears_ to have stopped moving. If you want to do something (e.g. enable UI, perform another transition) at the point when the camera has stopped, you probably want the `rest` event. This can be fine tuned using the `.restThreshold` parameter. See the [Rest and Sleep Example](https://yomotsu.github.io/camera-controls/examples/rest-and-sleep.html). 212 | 213 | ## User input config 214 | 215 | Working example: [user input config](https://yomotsu.github.io/camera-controls/examples/config.html) 216 | 217 | | button to assign | behavior | 218 | | --------------------- | -------- | 219 | | `mouseButtons.left` | `CameraControls.ACTION.ROTATE`* \| `CameraControls.ACTION.TRUCK` \| `CameraControls.ACTION.SCREEN_PAN` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY` \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 220 | | `mouseButtons.right` | `CameraControls.ACTION.ROTATE` \| `CameraControls.ACTION.TRUCK`* \| `CameraControls.ACTION.SCREEN_PAN` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY` \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 221 | | `mouseButtons.wheel` ¹ | `CameraControls.ACTION.ROTATE` \| `CameraControls.ACTION.TRUCK` \| `CameraControls.ACTION.SCREEN_PAN` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY` \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 222 | | `mouseButtons.middle` ² | `CameraControls.ACTION.ROTATE` \| `CameraControls.ACTION.TRUCK` \| `CameraControls.ACTION.SCREEN_PAN` \| `CameraControls.ACTION.OFFSET` \| `CameraControls.ACTION.DOLLY`* \| `CameraControls.ACTION.ZOOM` \| `CameraControls.ACTION.NONE` | 223 | 224 | 1. Mouse wheel event for scroll "up/down" on mac "up/down/left/right" 225 | 2. Mouse click on wheel event "button" 226 | 227 | - \* is the default. 228 | - The default of `mouseButtons.wheel` is: 229 | - `DOLLY` for Perspective camera. 230 | - `ZOOM` for Orthographic camera, and can't set `DOLLY`. 231 | 232 | | fingers to assign | behavior | 233 | | --------------------- | -------- | 234 | | `touches.one` | `CameraControls.ACTION.TOUCH_ROTATE`* \| `CameraControls.ACTION.TOUCH_TRUCK` \| `CameraControls.ACTION.TOUCH_SCREEN_PAN` \| `CameraControls.ACTION.TOUCH_OFFSET` \| `CameraControls.ACTION.DOLLY` | `CameraControls.ACTION.ZOOM` | `CameraControls.ACTION.NONE` | 235 | | `touches.two` | `ACTION.TOUCH_DOLLY_TRUCK` \| `ACTION.TOUCH_DOLLY_SCREEN_PAN` \| `ACTION.TOUCH_DOLLY_OFFSET` \| `ACTION.TOUCH_DOLLY_ROTATE` \| `ACTION.TOUCH_ZOOM_TRUCK` \| `ACTION.TOUCH_ZOOM_SCREEN_PAN` \| `ACTION.TOUCH_ZOOM_OFFSET` \| `ACTION.TOUCH_ZOOM_ROTATE` \| `ACTION.TOUCH_DOLLY` \| `ACTION.TOUCH_ZOOM` \| `CameraControls.ACTION.TOUCH_ROTATE` \| `CameraControls.ACTION.TOUCH_TRUCK` \| `CameraControls.ACTION.TOUCH_SCREEN_PAN` \| `CameraControls.ACTION.TOUCH_OFFSET` \| `CameraControls.ACTION.NONE` | 236 | | `touches.three` | `ACTION.TOUCH_DOLLY_TRUCK` \| `ACTION.TOUCH_DOLLY_SCREEN_PAN` \| `ACTION.TOUCH_DOLLY_OFFSET` \| `ACTION.TOUCH_DOLLY_ROTATE` \| `ACTION.TOUCH_ZOOM_TRUCK` \| `ACTION.TOUCH_ZOOM_SCREEN_PAN` \| `ACTION.TOUCH_ZOOM_OFFSET` \| `ACTION.TOUCH_ZOOM_ROTATE` \| `CameraControls.ACTION.TOUCH_ROTATE` \| `CameraControls.ACTION.TOUCH_TRUCK` \| `CameraControls.ACTION.TOUCH_SCREEN_PAN` \| `CameraControls.ACTION.TOUCH_OFFSET` \| `CameraControls.ACTION.NONE` | 237 | 238 | - \* is the default. 239 | - The default of `touches.two` and `touches.three` is: 240 | - `TOUCH_DOLLY_TRUCK` for Perspective camera. 241 | - `TOUCH_ZOOM_TRUCK` for Orthographic camera, and can't set `TOUCH_DOLLY_TRUCK` and `TOUCH_DOLLY`. 242 | 243 | ## Methods 244 | 245 | #### `rotate( azimuthAngle, polarAngle, enableTransition )` 246 | 247 | Rotate azimuthal angle(horizontal) and polar angle(vertical). 248 | Every value is added to the current value. 249 | 250 | | Name | Type | Description | 251 | | ------------------ | --------- | ----------- | 252 | | `azimuthAngle` | `number` | Azimuth rotate angle. In radian. | 253 | | `polarAngle` | `number` | Polar rotate angle. In radian. | 254 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 255 | 256 | If you want to rotate only one axis, put a angle for the axis to rotate, and `0` for another. 257 | ``` js 258 | rotate( 20 * THREE.MathUtils.DEG2RAD, 0, true ); 259 | ``` 260 | 261 | --- 262 | 263 | #### `rotateAzimuthTo( azimuthAngle, enableTransition )` 264 | 265 | Rotate azimuthal angle(horizontal) to the given angle and keep the same polar angle(vertical) target. 266 | 267 | | Name | Type | Description | 268 | | ------------------ | --------- | ----------- | 269 | | `azimuthAngle` | `number` | Azimuth rotate angle. In radian. | 270 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 271 | 272 | --- 273 | 274 | #### `rotatePolarTo( polarAngle, enableTransition )` 275 | 276 | Rotate polar angle(vertical) to the given angle and keep the same azimuthal angle(horizontal) target. 277 | 278 | | Name | Type | Description | 279 | | ------------------ | --------- | ----------- | 280 | | `polarAngle` | `number` | Polar rotate angle. In radian. | 281 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 282 | 283 | --- 284 | 285 | #### `rotateTo( azimuthAngle, polarAngle, enableTransition )` 286 | 287 | Rotate azimuthal angle(horizontal) and polar angle(vertical) to the given angle. 288 | Camera view will rotate over the orbit pivot absolutely: 289 | 290 | Azimuth angle 291 | ``` 292 | 0º 293 | \ 294 | 90º -----+----- -90º 295 | \ 296 | 180º 297 | ``` 298 | 0º front, 90º (`Math.PI / 2`) left, -90º (`- Math.PI / 2`) right, 180º (`Math.PI`) back 299 | 300 | ----- 301 | 302 | Polar angle 303 | ``` 304 | 180º 305 | | 306 | 90º 307 | | 308 | 0º 309 | ``` 310 | 311 | 180º (`Math.PI`) top/sky, 90º (`Math.PI / 2`) horizontal from view, 0º bottom/floor 312 | 313 | | Name | Type | Description | 314 | | ------------------ | --------- | ----------- | 315 | | `azimuthAngle` | `number` | Azimuth rotate angle to. In radian. | 316 | | `polarAngle` | `number` | Polar rotate angle to. In radian. | 317 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 318 | 319 | --- 320 | 321 | #### `dolly( distance, enableTransition )` 322 | 323 | Dolly in/out camera position. 324 | 325 | | Name | Type | Description | 326 | | ------------------ | --------- | ----------- | 327 | | `distance` | `number` | Distance of dollyIn | 328 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 329 | 330 | --- 331 | 332 | #### `dollyTo( distance, enableTransition )` 333 | 334 | Dolly in/out camera position to given distance. 335 | 336 | | Name | Type | Description | 337 | | ------------------ | --------- | ----------- | 338 | | `distance` | `number` | Distance of dollyIn | 339 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 340 | 341 | --- 342 | 343 | #### `dollyInFixed( distance, enableTransition )` 344 | 345 | Dolly in, but does not change the distance between the target and the camera, and moves the target position instead. 346 | Specify a negative value for dolly out. 347 | 348 | | Name | Type | Description | 349 | | ------------------ | --------- | ----------- | 350 | | `distance` | `number` | Distance of dollyIn | 351 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 352 | 353 | --- 354 | 355 | #### `zoom( zoomStep, enableTransition )` 356 | 357 | Zoom in/out camera. The value is added to camera zoom. 358 | Limits set with `.minZoom` and `.maxZoom` 359 | 360 | | Name | Type | Description | 361 | | ------------------ | --------- | ----------- | 362 | | `zoomStep` | `number` | zoom scale | 363 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 364 | 365 | You can also make zoomIn function using `camera.zoom` property. 366 | e.g. 367 | ``` js 368 | const zoomIn = () => cameraControls.zoom( camera.zoom / 2, true ); 369 | const zoomOut = () => cameraControls.zoom( - camera.zoom / 2, true ); 370 | ``` 371 | 372 | --- 373 | 374 | #### `zoomTo( zoom, enableTransition )` 375 | 376 | Zoom in/out camera to given scale. The value overwrites camera zoom. 377 | Limits set with `.minZoom` and `.maxZoom` 378 | 379 | | Name | Type | Description | 380 | | ------------------ | --------- | ----------- | 381 | | `zoom` | `number` | zoom scale | 382 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 383 | 384 | 385 | --- 386 | 387 | #### `truck( x, y, enableTransition )` 388 | 389 | Truck and pedestal camera using current azimuthal angle. 390 | 391 | | Name | Type | Description | 392 | | ------------------ | --------- | ----------- | 393 | | `x` | `number` | Horizontal translate amount | 394 | | `y` | `number` | Vertical translate amount | 395 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 396 | 397 | --- 398 | 399 | #### `lookInDirectionOf( x, y, z, enableTransition )` 400 | 401 | Look in the given point direction. 402 | 403 | | Name | Type | Description | 404 | | ------------------ | --------- | ----------- | 405 | | `x` | `number` | point x | 406 | | `y` | `number` | point y | 407 | | `z` | `number` | point z | 408 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 409 | 410 | #### `setFocalOffset( x, y, z, enableTransition )` 411 | 412 | Set focal offset using the screen parallel coordinates. 413 | `z` doesn't affect in Orthographic as with Dolly. 414 | 415 | | Name | Type | Description | 416 | | ------------------ | --------- | ----------- | 417 | | `x` | `number` | Horizontal offset amount | 418 | | `y` | `number` | Vertical offset amount | 419 | | `z` | `number` | Depth offset amount. The result is the same as Dolly but unaffected by `minDistance` and `maxDistance` | 420 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 421 | 422 | --- 423 | 424 | #### `setOrbitPoint( targetX, targetY, targetZ )` 425 | 426 | Set orbit point without moving the camera. 427 | 428 | | Name | Type | Description | 429 | | ------------------ | --------- | ----------- | 430 | | `targetX` | `number` | Orbit center position x | 431 | | `targetY` | `number` | Orbit center position y | 432 | | `targetZ` | `number` | Orbit center position z | 433 | 434 | --- 435 | 436 | #### `forward( distance, enableTransition )` 437 | 438 | Move forward / backward. 439 | 440 | | Name | Type | Description | 441 | | ------------------ | --------- | ----------- | 442 | | `distance` | `number` | Amount to move forward / backward. Negative value to move backward | 443 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 444 | 445 | --- 446 | 447 | #### `moveTo( x, y, z, enableTransition )` 448 | 449 | Move `target` position to given point. 450 | 451 | | Name | Type | Description | 452 | | ------------------ | --------- | ----------- | 453 | | `x` | `number` | x coord to move center position | 454 | | `y` | `number` | y coord to move center position | 455 | | `z` | `number` | z coord to move center position | 456 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 457 | 458 | --- 459 | 460 | #### `elevate( height, enableTransition )` 461 | 462 | Move up / down. 463 | 464 | | Name | Type | Description | 465 | | ------------------ | --------- | ----------- | 466 | | `height` | `number` | Amount to move up / down. Negative value to move down | 467 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 468 | 469 | --- 470 | 471 | #### `fitToBox( box3OrMesh, enableTransition, { paddingTop, paddingLeft, paddingBottom, paddingRight } )` 472 | 473 | Fit the viewport to the box or the bounding box of the object, using the nearest axis. paddings are in unit. 474 | set `cover: true` to fill enter screen. 475 | 476 | | Name | Type | Description | 477 | | ----------------------- | ---------------------------- | ----------- | 478 | | `box3OrMesh` | `THREE.Box3` \| `THREE.Mesh` | Axis aligned bounding box to fit the view. | 479 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 480 | | `options` | `object` | Options | 481 | | `options.cover` | `boolean` | Whether fill enter screen or not. Default is `false` | 482 | | `options.paddingTop` | `number` | Padding top. Default is `0` | 483 | | `options.paddingRight` | `number` | Padding right. Default is `0` | 484 | | `options.paddingBottom` | `number` | Padding bottom. Default is `0` | 485 | | `options.paddingLeft` | `number` | Padding left. Default is `0` | 486 | 487 | --- 488 | 489 | #### `fitToSphere( sphereOrMesh, enableTransition )` 490 | 491 | Fit the viewport to the sphere or the bounding sphere of the object. 492 | 493 | | Name | Type | Description | 494 | | ------------------ | ------------------------------ | ----------- | 495 | | `sphereOrMesh` | `THREE.Sphere` \| `THREE.Mesh` | bounding sphere to fit the view. | 496 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 497 | 498 | --- 499 | 500 | #### `setLookAt( positionX, positionY, positionZ, targetX, targetY, targetZ, enableTransition )` 501 | 502 | Look at the `target` from the `position`. 503 | 504 | | Name | Type | Description | 505 | | ------------------ | --------- | ----------- | 506 | | `positionX` | `number` | Camera position x. | 507 | | `positionY` | `number` | Camera position y. | 508 | | `positionZ` | `number` | Camera position z. | 509 | | `targetX` | `number` | Orbit center position x. | 510 | | `targetY` | `number` | Orbit center position y. | 511 | | `targetZ` | `number` | Orbit center position z. | 512 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 513 | 514 | --- 515 | 516 | #### `lerpLookAt( positionAX, positionAY, positionAZ, targetAX, targetAY, targetAZ, positionBX, positionBY, positionBZ, targetBX, targetBY, targetBZ, t, enableTransition )` 517 | 518 | Similar to `setLookAt`, but it interpolates between two states. 519 | 520 | | Name | Type | Description | 521 | | ------------------ | --------- | ----------- | 522 | | `positionAX` | `number` | The starting position x of look at from. | 523 | | `positionAY` | `number` | The starting position y of look at from. | 524 | | `positionAZ` | `number` | The starting position z of look at from. | 525 | | `targetAX` | `number` | The starting position x of look at. | 526 | | `targetAY` | `number` | The starting position y of look at. | 527 | | `targetAZ` | `number` | The starting position z of look at. | 528 | | `positionBX` | `number` | Look at from position x to interpolate towards. | 529 | | `positionBY` | `number` | Look at from position y to interpolate towards. | 530 | | `positionBZ` | `number` | Look at from position z to interpolate towards. | 531 | | `targetBX` | `number` | look at position x to interpolate towards. | 532 | | `targetBY` | `number` | look at position y to interpolate towards. | 533 | | `targetBZ` | `number` | look at position z to interpolate towards. | 534 | | `t` | `number` | Interpolation factor in the closed interval. The value must be a number between `0` to `1` inclusive, where `1` is 100% | 535 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 536 | 537 | --- 538 | 539 | #### `setPosition( positionX, positionY, positionZ, enableTransition )` 540 | 541 | Set angle and distance by given position. 542 | An alias of `setLookAt()`, without target change. Thus keep gazing at the current target 543 | 544 | | Name | Type | Description | 545 | | ------------------ | --------- | ----------- | 546 | | `positionX` | `number` | Position x of look at from. | 547 | | `positionY` | `number` | Position y of look at from. | 548 | | `positionZ` | `number` | Position z of look at from. | 549 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 550 | 551 | --- 552 | 553 | #### `setTarget( targetX, targetY, targetZ, enableTransition )` 554 | 555 | Set the target position where gaze at. 556 | An alias of `setLookAt()`, without position change. Thus keep the same position. 557 | 558 | | Name | Type | Description | 559 | | ------------------ | --------- | ----------- | 560 | | `targetX` | `number` | Position x of look at. | 561 | | `targetY` | `number` | Position y of look at. | 562 | | `targetZ` | `number` | Position z of look at. | 563 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 564 | 565 | --- 566 | 567 | #### `setBoundary( box3? )` 568 | 569 | Set the boundary box that encloses the target of the camera. `box3` is in `THREE.Box3` 570 | 571 | | Name | Type | Description | 572 | | ------ | ------------- | ----------- | 573 | | `box3` | `THREE.Box3?` | Boundary area. No argument to remove the boundary. | 574 | 575 | --- 576 | 577 | #### `setViewport( vector4? )` 578 | 579 | Set (or unset) the current viewport. 580 | Set this when you want to use renderer viewport and [`.dollyToCursor`](#properties) feature at the same time. 581 | 582 | See: [THREE.WebGLRenderer.setViewport()](https://threejs.org/docs/#api/en/renderers/WebGLRenderer.setViewport) 583 | 584 | | Name | Type | Description | 585 | | --------- | ---------------- | ----------- | 586 | | `vector4` | `THREE.Vector4?` | Vector4 that represents the viewport, or `undefined` for unsetting this. | 587 | 588 | #### `setViewport( x, y, width, height )` 589 | 590 | Same as [`setViewport( vector4 )`](#setviewport-vector4-|-null-), but you can give it four numbers that represents a viewport instead: 591 | 592 | | Name | Type | Description | 593 | | -------- | -------- | ----------- | 594 | | `x` | `number` | Leftmost of the viewport. | 595 | | `y` | `number` | Bottommost of the viewport. | 596 | | `width` | `number` | Width of the viewport. | 597 | | `height` | `number` | Height of the viewport. | 598 | 599 | --- 600 | 601 | #### `getTarget( out, receiveEndValue )` 602 | 603 | Returns the orbit center position, where the camera looking at. 604 | 605 | | Name | Type | Description | 606 | | ----------------- | --------------- | ----------- | 607 | | `out` | `THREE.Vector3` | The receiving Vector3 instance to copy the result | 608 | | `receiveEndValue` | `boolean` | Whether receive the transition end coords or current. default is `true` | 609 | 610 | --- 611 | 612 | #### `getPosition( out, receiveEndValue )` 613 | 614 | Returns the camera position. 615 | 616 | | Name | Type | Description | 617 | | ----------------- | --------------- | ----------- | 618 | | `out` | `THREE.Vector3` | The receiving Vector3 instance to copy the result | 619 | | `receiveEndValue` | `boolean` | Whether receive the transition end coords or current. default is `true` | 620 | 621 | --- 622 | 623 | #### `getSpherical( out, receiveEndValue )` 624 | 625 | Returns the spherical coordinates of the orbit. 626 | 627 | | Name | Type | Description | 628 | | ----------------- | --------------- | ----------- | 629 | | `out` | `THREE.Spherical` | The receiving Spherical instance to copy the result | 630 | | `receiveEndValue` | `boolean` | Whether receive the transition end coords or current. default is `true` | 631 | 632 | --- 633 | 634 | #### `getFocalOffset( out, receiveEndValue )` 635 | 636 | Returns the focal offset, which is how much the camera appears to be translated in screen parallel coordinates. 637 | 638 | | Name | Type | Description | 639 | | ----------------- | --------------- | ----------- | 640 | | `out` | `THREE.Vector3` | The receiving Vector3 instance to copy the result | 641 | | `receiveEndValue` | `boolean` | Whether receive the transition end coords or current. default is `true` | 642 | 643 | --- 644 | 645 | #### `stop()` 646 | 647 | stop all transitions. 648 | 649 | --- 650 | 651 | #### `saveState()` 652 | 653 | Set current camera position as the default position 654 | 655 | --- 656 | 657 | #### `normalizeRotations()` 658 | 659 | Normalize camera azimuth angle rotation between 0 and 360 degrees. 660 | 661 | #### `reset( enableTransition )` 662 | 663 | Reset all rotation and position to default. 664 | 665 | | Name | Type | Description | 666 | | ------------------ | --------- | ----------- | 667 | | `enableTransition` | `boolean` | Whether to move smoothly or immediately | 668 | 669 | --- 670 | 671 | #### `update( delta ): boolean` 672 | 673 | Update camera position and directions. This should be called in your tick loop and returns `true` if re-rendering is needed. 674 | 675 | | Name | Type | Description | 676 | | ------- | -------- | ----------- | 677 | | `delta` | `number` | Delta time between previous update call | 678 | 679 | --- 680 | 681 | #### `updateCameraUp()` 682 | 683 | When you change camera-up vector, run `.updateCameraUp()` to sync. 684 | 685 | --- 686 | 687 | #### `applyCameraUp()` 688 | 689 | Apply current camera-up direction to the camera. 690 | The orbit system will be re-initialized with the current position. 691 | 692 | --- 693 | 694 | #### `connect()` 695 | 696 | Attach all internal event handlers to enable drag control. 697 | 698 | --- 699 | 700 | #### `disconnect()` 701 | 702 | Detach all internal event handlers to disable drag control. 703 | 704 | --- 705 | 706 | #### `dispose()` 707 | 708 | Dispose the cameraControls instance itself, remove all eventListeners. 709 | 710 | --- 711 | 712 | #### `addEventListener( type: string, listener: function )` 713 | 714 | Adds the specified event listener. 715 | 716 | --- 717 | 718 | #### `removeEventListener( type: string, listener: function )` 719 | 720 | Removes the specified event listener. 721 | 722 | --- 723 | 724 | #### `removeAllEventListeners( type: string )` 725 | 726 | Removes all listeners for the specified type. 727 | 728 | --- 729 | 730 | #### `toJSON()` 731 | 732 | Get all state in JSON string 733 | 734 | --- 735 | 736 | #### `fromJSON( json, enableTransition )` 737 | 738 | Reproduce the control state with JSON. `enableTransition` is where anim or not in a boolean. 739 | 740 | --- 741 | 742 | ## Tips 743 | 744 | ### Normalize accumulated azimuth angle: 745 | If you need a normalized accumulated azimuth angle (between 0 and 360 deg), compute with [THREE.MathUtils.euclideanModulo](https://threejs.org/docs/#api/en/math/MathUtils) 746 | e.g.: 747 | ``` js 748 | const TAU = Math.PI * 2; 749 | 750 | function normalizeAngle( angle ) { 751 | 752 | return THREE.MathUtils.euclideanModulo( angle, TAU ); 753 | 754 | } 755 | 756 | const normalizedAzimuthAngle = normalizeAngle( cameraControls.azimuthAngle ); 757 | ``` 758 | 759 | --- 760 | ### Find the absolute angle to shortest azimuth rotatation: 761 | You may rotate 380deg but actually, you expect to rotate -20deg. 762 | To get the absolute angle, use the below: 763 | 764 | ```js 765 | const TAU = Math.PI * 2; 766 | 767 | function absoluteAngle( targetAngle, sourceAngle ){ 768 | 769 | const angle = targetAngle - sourceAngle 770 | return THREE.MathUtils.euclideanModulo( angle + Math.PI, TAU ) - Math.PI; 771 | 772 | } 773 | 774 | console.log( absoluteAngle( 380 * THREE.MathUtils.DEG2RAD, 0 ) * THREE.MathUtils.RAD2DEG ); // -20deg 775 | console.log( absoluteAngle( -1000 * THREE.MathUtils.DEG2RAD, 0 ) * THREE.MathUtils.RAD2DEG ); // 80deg 776 | ``` 777 | 778 | --- 779 | ### Creating Complex Transitions 780 | 781 | All methods that take the `enableTransition` parameter return a `Promise` can be used to create complex animations, for example: 782 | 783 | ``` js 784 | async function complexTransition() { 785 | await cameraControls.rotateTo( Math.PI / 2, Math.PI / 4, true ); 786 | await cameraControls.dollyTo( 3, true ); 787 | await cameraControls.fitToSphere( mesh, true ); 788 | } 789 | ``` 790 | 791 | This will rotate the camera, then dolly, and finally fit to the bounding sphere of the `mesh`. 792 | 793 | The speed and timing of transitions can be tuned using `.restThreshold` and `.smoothTime`. 794 | 795 | If `enableTransition` is `false`, the promise will resolve immediately: 796 | 797 | ``` js 798 | // will resolve immediately 799 | await cameraControls.dollyTo( 3, false ); 800 | ``` 801 | 802 | --- 803 | 804 | ## V2 Migration Guide 805 | 806 | camera-controls used to use simple damping for its smooth transition. camera-controls v2 now uses [SmoothDamp](https://docs.unity3d.com/ScriptReference/Mathf.SmoothDamp.html). 807 | one of the benefits of using SmoothDamp is, SmoothDamp transition can be controlled with smoothTime which is approximately the time it will take to reach the end position. 808 | Also, the Maximum speed of the transition can be set with `max speed`. 809 | 810 | Due to the change, the following are needed. 811 | (if you haven't changed `dampingFactor` and `draggingDampingFactor` in v1.x, nothing is needed) 812 | 813 | deprecated 814 | - `dampingFactor` (use smoothTime instead) 815 | - `draggingDampingFactor` (use draggingSmoothTime instead) 816 | 817 | added 818 | - `smoothTime` 819 | - `draggingSmoothTime` 820 | - `maxSpeed` 821 | 822 | ...That's it! 823 | 824 | ## Contributors 825 | 826 | This project exists thanks to all the people who contribute. 827 | 828 | ![](https://contributors-img.web.app/image?repo=yomotsu/camera-controls) 829 | 830 | 831 | ## Release 832 | 833 | Pre-requisites: 834 | 1. a npm registry up and running with a [`NPM_TOKEN`](https://docs.npmjs.com/creating-and-viewing-access-tokens) 835 | ```sh 836 | $ export NPM_TOKEN=npm_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 837 | ``` 838 | 2. a Github [PAT](https://github.com/semantic-release/github#github-authentication) 839 | ```sh 840 | $ export GITHUB_TOKEN=github_pat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 841 | ``` 842 | 843 | ```sh 844 | $ npm run release -- --dry-run 845 | ``` 846 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' assert { type: 'json' }; 2 | import rollupReplace from '@rollup/plugin-replace'; 3 | import rollupTypescript from '@rollup/plugin-typescript'; 4 | import typescript from 'typescript'; 5 | 6 | const license = `/*! 7 | * ${ pkg.name } 8 | * https://github.com/${ pkg.repository } 9 | * (c) 2017 @yomotsu 10 | * Released under the MIT License. 11 | */`; 12 | 13 | export default { 14 | input: 'src/index.ts', 15 | output: [ 16 | { 17 | format: 'es', 18 | file: pkg.module, 19 | banner: license, 20 | indent: '\t', 21 | exports: 'named', 22 | }, 23 | { 24 | format: 'cjs', 25 | file: pkg.main, 26 | banner: license, 27 | indent: '\t', 28 | exports: 'named', 29 | } 30 | ], 31 | plugins: [ 32 | rollupReplace( { preventAssignment: true, __VERSION: pkg.version } ), 33 | rollupTypescript( { typescript } ), 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /src/EventDispatcher.ts: -------------------------------------------------------------------------------- 1 | export type Listener = ( event?: DispatcherEvent ) => void; 2 | 3 | export interface DispatcherEvent { 4 | type: string; 5 | [ key: string ]: any; 6 | } 7 | 8 | export class EventDispatcher { 9 | 10 | private _listeners: { [ type: string ]: Listener[] } = {}; 11 | 12 | /** 13 | * Adds the specified event listener. 14 | * @param type event name 15 | * @param listener handler function 16 | * @category Methods 17 | */ 18 | addEventListener( type: string, listener: Listener ): void { 19 | 20 | const listeners = this._listeners; 21 | 22 | if ( listeners[ type ] === undefined ) listeners[ type ] = []; 23 | 24 | if ( listeners[ type ].indexOf( listener ) === - 1 ) listeners[ type ].push( listener ); 25 | 26 | } 27 | 28 | /** 29 | * Presence of the specified event listener. 30 | * @param type event name 31 | * @param listener handler function 32 | * @category Methods 33 | */ 34 | hasEventListener( type: string, listener: Listener ): boolean { 35 | 36 | const listeners = this._listeners; 37 | 38 | return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1; 39 | 40 | } 41 | 42 | /** 43 | * Removes the specified event listener 44 | * @param type event name 45 | * @param listener handler function 46 | * @category Methods 47 | */ 48 | removeEventListener( type: string, listener: Listener ): void { 49 | 50 | const listeners = this._listeners; 51 | const listenerArray = listeners[ type ]; 52 | 53 | if ( listenerArray !== undefined ) { 54 | 55 | const index = listenerArray.indexOf( listener ); 56 | 57 | if ( index !== - 1 ) listenerArray.splice( index, 1 ); 58 | 59 | } 60 | 61 | } 62 | 63 | /** 64 | * Removes all event listeners 65 | * @param type event name 66 | * @category Methods 67 | */ 68 | removeAllEventListeners( type?: string ): void { 69 | 70 | if ( ! type ) { 71 | 72 | this._listeners = {}; 73 | return; 74 | 75 | } 76 | 77 | if ( Array.isArray( this._listeners[ type ] ) ) this._listeners[ type ].length = 0; 78 | 79 | } 80 | 81 | /** 82 | * Fire an event type. 83 | * @param event DispatcherEvent 84 | * @category Methods 85 | */ 86 | dispatchEvent( event: DispatcherEvent ): void { 87 | 88 | const listeners = this._listeners; 89 | const listenerArray = listeners[ event.type ]; 90 | 91 | if ( listenerArray !== undefined ) { 92 | 93 | event.target = this; 94 | const array = listenerArray.slice( 0 ); 95 | 96 | for ( let i = 0, l = array.length; i < l; i ++ ) { 97 | 98 | array[ i ].call( this, event ); 99 | 100 | } 101 | 102 | } 103 | 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const PI_2 = Math.PI * 2; 2 | export const PI_HALF = Math.PI / 2; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { CameraControls as default } from './CameraControls'; 2 | export { EventDispatcher } from './EventDispatcher'; 3 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type * as _THREE from 'three'; 2 | 3 | // Is this suppose to be `Pick`? 4 | export interface THREESubset { 5 | Vector2 : typeof _THREE.Vector2; 6 | Vector3 : typeof _THREE.Vector3; 7 | Vector4 : typeof _THREE.Vector4; 8 | Quaternion: typeof _THREE.Quaternion; 9 | Matrix4 : typeof _THREE.Matrix4; 10 | Spherical : typeof _THREE.Spherical; 11 | Box3 : typeof _THREE.Box3; 12 | Sphere : typeof _THREE.Sphere; 13 | Raycaster : typeof _THREE.Raycaster; 14 | [ key: string ]: any; 15 | } 16 | 17 | export type Ref = { 18 | value: number; 19 | } 20 | 21 | // see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value 22 | export const MOUSE_BUTTON = { 23 | LEFT: 1, 24 | RIGHT: 2, 25 | MIDDLE: 4, 26 | } as const; 27 | export type MOUSE_BUTTON = typeof MOUSE_BUTTON[ keyof typeof MOUSE_BUTTON ]; 28 | 29 | export const ACTION = Object.freeze( { 30 | NONE: 0b0, 31 | ROTATE: 0b1, 32 | TRUCK: 0b10, 33 | SCREEN_PAN: 0b100, 34 | OFFSET: 0b1000, 35 | DOLLY: 0b10000, 36 | ZOOM: 0b100000, 37 | TOUCH_ROTATE: 0b1000000, 38 | TOUCH_TRUCK: 0b10000000, 39 | TOUCH_SCREEN_PAN: 0b100000000, 40 | TOUCH_OFFSET: 0b1000000000, 41 | TOUCH_DOLLY: 0b10000000000, 42 | TOUCH_ZOOM: 0b100000000000, 43 | TOUCH_DOLLY_TRUCK: 0b1000000000000, 44 | TOUCH_DOLLY_SCREEN_PAN: 0b10000000000000, 45 | TOUCH_DOLLY_OFFSET: 0b100000000000000, 46 | TOUCH_DOLLY_ROTATE: 0b1000000000000000, 47 | TOUCH_ZOOM_TRUCK: 0b10000000000000000, 48 | TOUCH_ZOOM_OFFSET: 0b100000000000000000, 49 | TOUCH_ZOOM_SCREEN_PAN: 0b1000000000000000000, 50 | TOUCH_ZOOM_ROTATE: 0b10000000000000000000, 51 | } as const ); 52 | 53 | // Bit OR of Action 54 | export type ACTION = number; 55 | 56 | export interface PointerInput { 57 | pointerId: number; 58 | clientX: number; 59 | clientY: number; 60 | deltaX: number; 61 | deltaY: number; 62 | mouseButton: MOUSE_BUTTON | null; 63 | } 64 | 65 | type mouseButtonAction = typeof ACTION.ROTATE | typeof ACTION.TRUCK | typeof ACTION.SCREEN_PAN | typeof ACTION.OFFSET | typeof ACTION.DOLLY | typeof ACTION.ZOOM | typeof ACTION.NONE; 66 | type mouseWheelAction = typeof ACTION.ROTATE | typeof ACTION.TRUCK | typeof ACTION.SCREEN_PAN | typeof ACTION.OFFSET | typeof ACTION.DOLLY | typeof ACTION.ZOOM | typeof ACTION.NONE; 67 | type singleTouchAction = 68 | typeof ACTION.TOUCH_ROTATE | 69 | typeof ACTION.TOUCH_TRUCK | 70 | typeof ACTION.TOUCH_SCREEN_PAN | 71 | typeof ACTION.TOUCH_OFFSET | 72 | typeof ACTION.DOLLY | 73 | typeof ACTION.ZOOM | 74 | typeof ACTION.NONE; 75 | type multiTouchAction = 76 | typeof ACTION.TOUCH_DOLLY_ROTATE | 77 | typeof ACTION.TOUCH_DOLLY_TRUCK | 78 | typeof ACTION.TOUCH_DOLLY_OFFSET | 79 | typeof ACTION.TOUCH_ZOOM_ROTATE | 80 | typeof ACTION.TOUCH_ZOOM_TRUCK | 81 | typeof ACTION.TOUCH_ZOOM_OFFSET | 82 | typeof ACTION.TOUCH_DOLLY | 83 | typeof ACTION.TOUCH_ZOOM | 84 | typeof ACTION.TOUCH_ROTATE | 85 | typeof ACTION.TOUCH_TRUCK | 86 | typeof ACTION.TOUCH_SCREEN_PAN | 87 | typeof ACTION.TOUCH_OFFSET | 88 | typeof ACTION.NONE; 89 | 90 | export interface MouseButtons { 91 | left : mouseButtonAction; 92 | middle : mouseButtonAction; 93 | right : mouseButtonAction; 94 | wheel : mouseWheelAction; 95 | } 96 | 97 | export interface Touches { 98 | one : singleTouchAction; 99 | two : multiTouchAction; 100 | three: multiTouchAction; 101 | } 102 | 103 | export const DOLLY_DIRECTION = { 104 | NONE: 0, 105 | IN: 1, 106 | OUT: - 1, 107 | } as const; 108 | export type DOLLY_DIRECTION = typeof DOLLY_DIRECTION[ keyof typeof DOLLY_DIRECTION ]; 109 | 110 | export interface FitToOptions { 111 | cover: boolean; 112 | paddingLeft : number; 113 | paddingRight : number; 114 | paddingBottom: number; 115 | paddingTop : number; 116 | } 117 | 118 | export interface CameraControlsEventMap { 119 | update : { type: 'update' }; 120 | wake : { type: 'wake' }; 121 | rest : { type: 'rest' }; 122 | sleep : { type: 'sleep' }; 123 | transitionstart: { type: 'transitionstart' }; 124 | controlstart : { type: 'controlstart' }; 125 | control : { type: 'control' }; 126 | controlend : { type: 'controlend' }; 127 | } 128 | 129 | export function isPerspectiveCamera( camera: _THREE.Camera ): camera is _THREE.PerspectiveCamera { 130 | 131 | return ( camera as _THREE.PerspectiveCamera ).isPerspectiveCamera; 132 | 133 | } 134 | 135 | export function isOrthographicCamera( camera: _THREE.Camera ): camera is _THREE.OrthographicCamera { 136 | 137 | return ( camera as _THREE.OrthographicCamera ).isOrthographicCamera; 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/utils/extractClientCoordFromEvent.ts: -------------------------------------------------------------------------------- 1 | import type * as _THREE from 'three'; 2 | import type { PointerInput } from '../types'; 3 | 4 | export function extractClientCoordFromEvent( pointers: PointerInput[], out: _THREE.Vector2 ) { 5 | 6 | out.set( 0, 0 ); 7 | 8 | pointers.forEach( ( pointer ) => { 9 | 10 | out.x += pointer.clientX; 11 | out.y += pointer.clientY; 12 | 13 | } ); 14 | 15 | out.x /= pointers.length; 16 | out.y /= pointers.length; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/math-utils.ts: -------------------------------------------------------------------------------- 1 | import type * as _THREE from 'three'; 2 | import type { Ref } from '../types'; 3 | 4 | const EPSILON = 1e-5; 5 | export const DEG2RAD = Math.PI / 180; 6 | 7 | export function clamp( value: number, min: number, max: number ) { 8 | 9 | return Math.max( min, Math.min( max, value ) ); 10 | 11 | } 12 | 13 | export function approxZero( number: number, error: number = EPSILON ): boolean { 14 | 15 | return Math.abs( number ) < error; 16 | 17 | } 18 | 19 | export function approxEquals( a: number, b: number, error: number = EPSILON ): boolean { 20 | 21 | return approxZero( a - b, error ); 22 | 23 | } 24 | 25 | export function roundToStep( value: number, step: number ): number { 26 | 27 | return Math.round( value / step ) * step; 28 | 29 | } 30 | 31 | export function infinityToMaxNumber( value: number ): number { 32 | 33 | if ( isFinite( value ) ) return value; 34 | 35 | if ( value < 0 ) return - Number.MAX_VALUE; 36 | 37 | return Number.MAX_VALUE; 38 | 39 | } 40 | 41 | export function maxNumberToInfinity( value: number ): number { 42 | 43 | if ( Math.abs( value ) < Number.MAX_VALUE ) return value; 44 | 45 | return value * Infinity; 46 | 47 | } 48 | 49 | // https://docs.unity3d.com/ScriptReference/Mathf.SmoothDamp.html 50 | // https://github.com/Unity-Technologies/UnityCsReference/blob/a2bdfe9b3c4cd4476f44bf52f848063bfaf7b6b9/Runtime/Export/Math/Mathf.cs#L308 51 | export function smoothDamp( 52 | current: number, 53 | target: number, 54 | currentVelocityRef: Ref, 55 | smoothTime: number, 56 | maxSpeed: number = Infinity, 57 | deltaTime: number, 58 | ): number { 59 | 60 | // Based on Game Programming Gems 4 Chapter 1.10 61 | smoothTime = Math.max( 0.0001, smoothTime ); 62 | const omega = 2 / smoothTime; 63 | 64 | const x = omega * deltaTime; 65 | const exp = 1 / ( 1 + x + 0.48 * x * x + 0.235 * x * x * x ); 66 | let change = current - target; 67 | const originalTo = target; 68 | 69 | // Clamp maximum speed 70 | const maxChange = maxSpeed * smoothTime; 71 | change = clamp( change, - maxChange, maxChange ); 72 | target = current - change; 73 | 74 | const temp = ( currentVelocityRef.value + omega * change ) * deltaTime; 75 | currentVelocityRef.value = ( currentVelocityRef.value - omega * temp ) * exp; 76 | let output = target + ( change + temp ) * exp; 77 | 78 | // Prevent overshooting 79 | if ( originalTo - current > 0.0 === output > originalTo ) { 80 | 81 | output = originalTo; 82 | currentVelocityRef.value = ( output - originalTo ) / deltaTime; 83 | 84 | } 85 | 86 | return output; 87 | 88 | } 89 | 90 | // https://docs.unity3d.com/ScriptReference/Vector3.SmoothDamp.html 91 | // https://github.com/Unity-Technologies/UnityCsReference/blob/a2bdfe9b3c4cd4476f44bf52f848063bfaf7b6b9/Runtime/Export/Math/Vector3.cs#L97 92 | export function smoothDampVec3( 93 | current: _THREE.Vector3, 94 | target: _THREE.Vector3, 95 | currentVelocityRef: _THREE.Vector3, 96 | smoothTime: number, 97 | maxSpeed: number = Infinity, 98 | deltaTime: number, 99 | out: _THREE.Vector3 100 | ) { 101 | 102 | // Based on Game Programming Gems 4 Chapter 1.10 103 | smoothTime = Math.max( 0.0001, smoothTime ); 104 | const omega = 2 / smoothTime; 105 | 106 | const x = omega * deltaTime; 107 | const exp = 1 / ( 1 + x + 0.48 * x * x + 0.235 * x * x * x ); 108 | 109 | let targetX = target.x; 110 | let targetY = target.y; 111 | let targetZ = target.z; 112 | 113 | let changeX = current.x - targetX; 114 | let changeY = current.y - targetY; 115 | let changeZ = current.z - targetZ; 116 | 117 | const originalToX = targetX; 118 | const originalToY = targetY; 119 | const originalToZ = targetZ; 120 | 121 | // Clamp maximum speed 122 | const maxChange = maxSpeed * smoothTime; 123 | 124 | const maxChangeSq = maxChange * maxChange; 125 | const magnitudeSq = changeX * changeX + changeY * changeY + changeZ * changeZ; 126 | 127 | if ( magnitudeSq > maxChangeSq ) { 128 | 129 | const magnitude = Math.sqrt( magnitudeSq ); 130 | changeX = changeX / magnitude * maxChange; 131 | changeY = changeY / magnitude * maxChange; 132 | changeZ = changeZ / magnitude * maxChange; 133 | 134 | } 135 | 136 | targetX = current.x - changeX; 137 | targetY = current.y - changeY; 138 | targetZ = current.z - changeZ; 139 | 140 | const tempX = ( currentVelocityRef.x + omega * changeX ) * deltaTime; 141 | const tempY = ( currentVelocityRef.y + omega * changeY ) * deltaTime; 142 | const tempZ = ( currentVelocityRef.z + omega * changeZ ) * deltaTime; 143 | 144 | currentVelocityRef.x = ( currentVelocityRef.x - omega * tempX ) * exp; 145 | currentVelocityRef.y = ( currentVelocityRef.y - omega * tempY ) * exp; 146 | currentVelocityRef.z = ( currentVelocityRef.z - omega * tempZ ) * exp; 147 | 148 | out.x = targetX + ( changeX + tempX ) * exp; 149 | out.y = targetY + ( changeY + tempY ) * exp; 150 | out.z = targetZ + ( changeZ + tempZ ) * exp; 151 | 152 | // Prevent overshooting 153 | const origMinusCurrentX = originalToX - current.x; 154 | const origMinusCurrentY = originalToY - current.y; 155 | const origMinusCurrentZ = originalToZ - current.z; 156 | const outMinusOrigX = out.x - originalToX; 157 | const outMinusOrigY = out.y - originalToY; 158 | const outMinusOrigZ = out.z - originalToZ; 159 | 160 | if ( origMinusCurrentX * outMinusOrigX + origMinusCurrentY * outMinusOrigY + origMinusCurrentZ * outMinusOrigZ > 0 ) { 161 | 162 | out.x = originalToX; 163 | out.y = originalToY; 164 | out.z = originalToZ; 165 | 166 | currentVelocityRef.x = ( out.x - originalToX ) / deltaTime; 167 | currentVelocityRef.y = ( out.y - originalToY ) / deltaTime; 168 | currentVelocityRef.z = ( out.z - originalToZ ) / deltaTime; 169 | 170 | } 171 | 172 | return out; 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/utils/notSupportedInOrthographicCamera.ts: -------------------------------------------------------------------------------- 1 | import * as _THREE from 'three'; 2 | import { isOrthographicCamera } from '../types'; 3 | 4 | export function notSupportedInOrthographicCamera( 5 | camera: _THREE.OrthographicCamera | _THREE.PerspectiveCamera, 6 | message: string 7 | ): camera is _THREE.OrthographicCamera { 8 | 9 | if ( isOrthographicCamera( camera ) ) { 10 | 11 | console.warn( `${ message } is not supported in OrthographicCamera` ); 12 | return true; 13 | 14 | } 15 | 16 | return false; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "outDir": "./dist", 5 | "declaration": true, 6 | "target": "es2017", 7 | "lib": ["es2017", "dom"], 8 | "strict": true, 9 | "removeComments": false, 10 | "experimentalDecorators": true, 11 | "noImplicitAny": true, 12 | "noImplicitThis": true, 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeVersion": true, 3 | "markdownItOptions": { 4 | "breaks": true 5 | }, 6 | "entryPoints": ["./src/CameraControls.ts"], 7 | "out": "docs", 8 | "excludePrivate": true, 9 | "excludeProtected": true, 10 | "excludeInternal": true, 11 | "excludeExternals": true, 12 | "sort": [ "source-order" ], 13 | "categorizeByGroup": false, 14 | "categoryOrder": [ 15 | "Statics", 16 | "Constructor", 17 | "Properties", 18 | "Methods", 19 | "*" 20 | ], 21 | "tsconfig": "tsconfig.json" 22 | } 23 | --------------------------------------------------------------------------------