├── .editorconfig
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── docs
├── .nojekyll
├── assets
│ ├── highlight.css
│ ├── icons.css
│ ├── icons.png
│ ├── icons@2x.png
│ ├── main.js
│ ├── search.js
│ ├── style.css
│ ├── widgets.png
│ └── widgets@2x.png
├── classes
│ ├── camera.default.html
│ ├── controls.default.html
│ ├── orthographiccamera.default.html
│ ├── perspectivecamera.default.html
│ └── pointermanager.default.html
├── enums
│ ├── types.cameratype.html
│ ├── types.controlsactions.html
│ └── types.pointermanagerstate.html
├── index.html
├── interfaces
│ ├── types.cameraoptions.html
│ ├── types.cameraview.html
│ ├── types.controlsoptions.html
│ ├── types.orthographiccameraoptions.html
│ ├── types.perspectivecameraoptions.html
│ ├── types.pointermanagerconfig.html
│ ├── types.pointermanagerevent.html
│ └── types.pointermanageroptions.html
├── modules.html
└── modules
│ ├── camera.html
│ ├── controls.html
│ ├── index.html
│ ├── normalize_wheel.export_.html
│ ├── normalize_wheel.html
│ ├── orthographiccamera.html
│ ├── perspectivecamera.html
│ ├── pointermanager.html
│ └── types.html
├── index.html
├── package-lock.json
├── package.json
├── screenshot.jpg
├── src
├── Camera.ts
├── Controls.ts
├── OrthographicCamera.ts
├── PerspectiveCamera.ts
├── PointerManager.ts
├── index.ts
├── normalize-wheel.d.ts
└── types.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | web_modules
3 | .DS_Store
4 | types
5 | lib
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | web_modules
2 | examples
3 | docs
4 | coverage
5 | test
6 | .github
7 | screenshot.*
8 | index.html
9 | tsconfig.json
10 | .editorconfig
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [3.1.1](https://github.com/dmnsgn/cameras/compare/v3.1.0...v3.1.1) (2021-11-12)
6 |
7 |
8 |
9 | # [3.1.0](https://github.com/dmnsgn/cameras/compare/v3.0.3...v3.1.0) (2021-10-02)
10 |
11 |
12 | ### Features
13 |
14 | * add exports field to package.json ([c1e40e4](https://github.com/dmnsgn/cameras/commit/c1e40e4d980d26185206b1c74e779b48133a022a))
15 |
16 |
17 |
18 | ## [3.0.3](https://github.com/dmnsgn/cameras/compare/v3.0.2...v3.0.3) (2021-05-22)
19 |
20 |
21 | ### Bug Fixes
22 |
23 | * use normalized wheel event in PointerManager removeEventListener ([4ce4374](https://github.com/dmnsgn/cameras/commit/4ce43742a1e5556679671519de1b6f9f7a97679c)), closes [#5](https://github.com/dmnsgn/cameras/issues/5)
24 |
25 |
26 |
27 | ## [3.0.2](https://github.com/dmnsgn/cameras/compare/v3.0.1...v3.0.2) (2021-04-30)
28 |
29 |
30 |
31 | ## [3.0.1](https://github.com/dmnsgn/cameras/compare/v3.0.0...v3.0.1) (2021-03-26)
32 |
33 |
34 |
35 | # [3.0.0](https://github.com/dmnsgn/cameras/compare/v2.0.0...v3.0.0) (2021-03-26)
36 |
37 |
38 | ### Bug Fixes
39 |
40 | * check rotate and dolly in Controls ([17ce65b](https://github.com/dmnsgn/cameras/commit/17ce65bf3ccf0ccf1b9f092415f77d0f5045639f)), closes [#3](https://github.com/dmnsgn/cameras/issues/3)
41 |
42 |
43 | ### Code Refactoring
44 |
45 | * use ES modules ([935a1e3](https://github.com/dmnsgn/cameras/commit/935a1e31cde132d7729d2e88a865aa8356f5c646))
46 |
47 |
48 | ### BREAKING CHANGES
49 |
50 | * switch to type module
51 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (C) 2020 Damien Seguin
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cameras
2 |
3 | [](https://www.npmjs.com/package/cameras)
4 | [](https://www.npmjs.com/package/cameras)
5 | [](https://bundlephobia.com/package/cameras)
6 | [](https://github.com/dmnsgn/cameras/blob/main/package.json)
7 | [](https://github.com/microsoft/TypeScript)
8 | [](https://conventionalcommits.org)
9 | [](https://github.com/prettier/prettier)
10 | [](https://github.com/eslint/eslint)
11 | [](https://github.com/dmnsgn/cameras/blob/main/LICENSE.md)
12 |
13 | Cameras for 3D rendering.
14 |
15 | [](https://paypal.me/dmnsgn)
16 | [](https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3)
17 | [](https://twitter.com/dmnsgn)
18 |
19 | 
20 |
21 | ## Installation
22 |
23 | ```bash
24 | npm install cameras
25 | ```
26 |
27 | ## Usage
28 |
29 | See the [demo](https://dmnsgn.github.io/cameras/) and its [source](index.html).
30 |
31 | ```js
32 | import { PerspectiveCamera } from "cameras";
33 |
34 | const perspectiveCamera = new PerspectiveCamera({
35 | fov: Math.PI / 2,
36 | near: 1,
37 | far: 1000,
38 | position: [3, 3, 3],
39 | target: [0, 1, 0],
40 | });
41 |
42 | // Create controls
43 | const perspectiveCameraControls = new Controls({
44 | element: regl._gl.canvas,
45 | camera: perspectiveCamera,
46 | });
47 |
48 | // Update controls and set camera with controls position/target
49 | perspectiveCameraControls.update();
50 | perspectiveCamera.position = perspectiveCameraControls.position;
51 | perspectiveCamera.target = perspectiveCameraControls.target;
52 |
53 | // Update view matrices (call when changing position/target/up)
54 | perspectiveCamera.update();
55 |
56 | // Update projection matrix (call when changing near/far/view and other camera type specific options)
57 | perspectiveCamera.updateProjectionMatrix();
58 | ```
59 |
60 | ## API
61 |
62 | See the [documentation](https://dmnsgn.github.io/cameras/docs) and [Typescript types](src/types.ts).
63 |
64 | ## License
65 |
66 | MIT. See [license file](https://github.com/dmnsgn/cameras/blob/main/LICENSE.md).
67 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmnsgn/cameras/9f74b2cc78496feda4cb55b03e9f4e126ce2e573/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/assets/highlight.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --light-hl-0: #000000;
3 | --dark-hl-0: #D4D4D4;
4 | --light-hl-1: #AF00DB;
5 | --dark-hl-1: #C586C0;
6 | --light-hl-2: #001080;
7 | --dark-hl-2: #9CDCFE;
8 | --light-hl-3: #A31515;
9 | --dark-hl-3: #CE9178;
10 | --light-hl-4: #0000FF;
11 | --dark-hl-4: #569CD6;
12 | --light-hl-5: #0070C1;
13 | --dark-hl-5: #4FC1FF;
14 | --light-hl-6: #795E26;
15 | --dark-hl-6: #DCDCAA;
16 | --light-hl-7: #267F99;
17 | --dark-hl-7: #4EC9B0;
18 | --light-hl-8: #098658;
19 | --dark-hl-8: #B5CEA8;
20 | --light-hl-9: #008000;
21 | --dark-hl-9: #6A9955;
22 | --light-code-background: #FFFFFF;
23 | --dark-code-background: #1E1E1E;
24 | }
25 |
26 | @media (prefers-color-scheme: light) { :root {
27 | --hl-0: var(--light-hl-0);
28 | --hl-1: var(--light-hl-1);
29 | --hl-2: var(--light-hl-2);
30 | --hl-3: var(--light-hl-3);
31 | --hl-4: var(--light-hl-4);
32 | --hl-5: var(--light-hl-5);
33 | --hl-6: var(--light-hl-6);
34 | --hl-7: var(--light-hl-7);
35 | --hl-8: var(--light-hl-8);
36 | --hl-9: var(--light-hl-9);
37 | --code-background: var(--light-code-background);
38 | } }
39 |
40 | @media (prefers-color-scheme: dark) { :root {
41 | --hl-0: var(--dark-hl-0);
42 | --hl-1: var(--dark-hl-1);
43 | --hl-2: var(--dark-hl-2);
44 | --hl-3: var(--dark-hl-3);
45 | --hl-4: var(--dark-hl-4);
46 | --hl-5: var(--dark-hl-5);
47 | --hl-6: var(--dark-hl-6);
48 | --hl-7: var(--dark-hl-7);
49 | --hl-8: var(--dark-hl-8);
50 | --hl-9: var(--dark-hl-9);
51 | --code-background: var(--dark-code-background);
52 | } }
53 |
54 | body.light {
55 | --hl-0: var(--light-hl-0);
56 | --hl-1: var(--light-hl-1);
57 | --hl-2: var(--light-hl-2);
58 | --hl-3: var(--light-hl-3);
59 | --hl-4: var(--light-hl-4);
60 | --hl-5: var(--light-hl-5);
61 | --hl-6: var(--light-hl-6);
62 | --hl-7: var(--light-hl-7);
63 | --hl-8: var(--light-hl-8);
64 | --hl-9: var(--light-hl-9);
65 | --code-background: var(--light-code-background);
66 | }
67 |
68 | body.dark {
69 | --hl-0: var(--dark-hl-0);
70 | --hl-1: var(--dark-hl-1);
71 | --hl-2: var(--dark-hl-2);
72 | --hl-3: var(--dark-hl-3);
73 | --hl-4: var(--dark-hl-4);
74 | --hl-5: var(--dark-hl-5);
75 | --hl-6: var(--dark-hl-6);
76 | --hl-7: var(--dark-hl-7);
77 | --hl-8: var(--dark-hl-8);
78 | --hl-9: var(--dark-hl-9);
79 | --code-background: var(--dark-code-background);
80 | }
81 |
82 | .hl-0 { color: var(--hl-0); }
83 | .hl-1 { color: var(--hl-1); }
84 | .hl-2 { color: var(--hl-2); }
85 | .hl-3 { color: var(--hl-3); }
86 | .hl-4 { color: var(--hl-4); }
87 | .hl-5 { color: var(--hl-5); }
88 | .hl-6 { color: var(--hl-6); }
89 | .hl-7 { color: var(--hl-7); }
90 | .hl-8 { color: var(--hl-8); }
91 | .hl-9 { color: var(--hl-9); }
92 | pre, code { background: var(--code-background); }
93 |
--------------------------------------------------------------------------------
/docs/assets/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmnsgn/cameras/9f74b2cc78496feda4cb55b03e9f4e126ce2e573/docs/assets/icons.png
--------------------------------------------------------------------------------
/docs/assets/icons@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmnsgn/cameras/9f74b2cc78496feda4cb55b03e9f4e126ce2e573/docs/assets/icons@2x.png
--------------------------------------------------------------------------------
/docs/assets/widgets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmnsgn/cameras/9f74b2cc78496feda4cb55b03e9f4e126ce2e573/docs/assets/widgets.png
--------------------------------------------------------------------------------
/docs/assets/widgets@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmnsgn/cameras/9f74b2cc78496feda4cb55b03e9f4e126ce2e573/docs/assets/widgets@2x.png
--------------------------------------------------------------------------------
/docs/classes/camera.default.html:
--------------------------------------------------------------------------------
1 |
default | cameras Properties inverse View Matrix inverse View Matrix: mat4 = ...
position position: vec3 = ...
projection Matrix projection Matrix: mat4 = ...
view Matrix view Matrix: mat4 = ...
Legend Class Constructor Property Method Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/enums/types.cameratype.html:
--------------------------------------------------------------------------------
1 | CameraType | cameras Enumeration members Orthographic Orthographic = 2
Perspective Perspective = 1
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/enums/types.controlsactions.html:
--------------------------------------------------------------------------------
1 | ControlsActions | cameras Enumeration ControlsActions Enumeration members Rotate Azimuth Rotate Azimuth = "RotateAzimuth"
Rotate Polar Rotate Polar = "RotatePolar"
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/enums/types.pointermanagerstate.html:
--------------------------------------------------------------------------------
1 | PointerManagerState | cameras Enumeration PointerManagerState Enumeration members Mouse Left Mouse Left = "MouseLeft"
Mouse Middle Mouse Middle = "MouseMiddle"
Mouse Right Mouse Right = "MouseRight"
Mouse Wheel Mouse Wheel = "MouseWheel"
Touch One Touch One = "TouchOne"
Touch Three Touch Three = "TouchThree"
Touch Two Touch Two = "TouchTwo"
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 | cameras
2 |
3 | cameras
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Cameras for 3D rendering.
15 |
16 |
17 |
18 |
19 |
20 |
21 | Installation
22 |
23 |
npm install cameras
24 |
25 |
26 |
27 | Usage
28 |
29 |
See the demo and its source .
30 |
import { PerspectiveCamera } from "cameras" ; const perspectiveCamera = new PerspectiveCamera ({ fov: Math .PI / 2 , near: 1 , far: 1000 , position: [ 3 , 3 , 3 ], target: [ 0 , 1 , 0 ], }); // Create controls const perspectiveCameraControls = new Controls ({ element: regl . _gl . canvas , camera: perspectiveCamera , }); // Update controls and set camera with controls position/target perspectiveCameraControls . update (); perspectiveCamera . position = perspectiveCameraControls . position ; perspectiveCamera . target = perspectiveCameraControls . target ; // Update view matrices (call when changing position/target/up) perspectiveCamera . update (); // Update projection matrix (call when changing near/far/view and other camera type specific options) perspectiveCamera . updateProjectionMatrix ();
31 |
32 |
33 |
34 | API
35 |
36 |
See the documentation and Typescript types .
37 |
38 |
39 | License
40 |
41 |
MIT. See license file .
42 |
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.cameraoptions.html:
--------------------------------------------------------------------------------
1 | CameraOptions | cameras Properties Optional inverse View Matrixinverse View Matrix?: mat4
Optional nearnear?: number
Optional positionposition?: vec3
Optional projection Matrixprojection Matrix?: mat4
Optional targettarget?: vec3
Optional view Matrixview Matrix?: mat4
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.cameraview.html:
--------------------------------------------------------------------------------
1 | CameraView | cameras Properties offset offset: [ number , number ]
size size: [ number , number ]
total Size total Size: [ number , number ]
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.orthographiccameraoptions.html:
--------------------------------------------------------------------------------
1 | OrthographicCameraOptions | cameras Interface OrthographicCameraOptions Properties Optional inverse View Matrixinverse View Matrix?: mat4
Optional nearnear?: number
Optional positionposition?: vec3
Optional projection Matrixprojection Matrix?: mat4
Optional targettarget?: vec3
Optional view Matrixview Matrix?: mat4
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.perspectivecameraoptions.html:
--------------------------------------------------------------------------------
1 | PerspectiveCameraOptions | cameras Interface PerspectiveCameraOptions Properties Optional inverse View Matrixinverse View Matrix?: mat4
Optional nearnear?: number
Optional positionposition?: vec3
Optional projection Matrixprojection Matrix?: mat4
Optional targettarget?: vec3
Optional view Matrixview Matrix?: mat4
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.pointermanagerconfig.html:
--------------------------------------------------------------------------------
1 | PointerManagerConfig | cameras Interface PointerManagerConfig Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.pointermanagerevent.html:
--------------------------------------------------------------------------------
1 | PointerManagerEvent | cameras Interface PointerManagerEvent Properties Optional original Eventoriginal Event?: Event
Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/interfaces/types.pointermanageroptions.html:
--------------------------------------------------------------------------------
1 | PointerManagerOptions | cameras Interface PointerManagerOptions Properties element element: HTMLElement
Methods Optional on Pointer UpdateParameters Returns unknown Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules.html:
--------------------------------------------------------------------------------
1 | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/camera.html:
--------------------------------------------------------------------------------
1 | Camera | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/controls.html:
--------------------------------------------------------------------------------
1 | Controls | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/index.html:
--------------------------------------------------------------------------------
1 | index | cameras References Camera Renames and re-exports default Controls Renames and re-exports default Orthographic Camera Renames and re-exports default Perspective Camera Renames and re-exports default Pointer Manager Renames and re-exports default Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/normalize_wheel.export_.html:
--------------------------------------------------------------------------------
1 | export= | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/normalize_wheel.html:
--------------------------------------------------------------------------------
1 | normalize-wheel | cameras Functions export= export=( event: Event ) : NormalizedWheelEvent Parameters Returns NormalizedWheelEvent Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/orthographiccamera.html:
--------------------------------------------------------------------------------
1 | OrthographicCamera | cameras Module OrthographicCamera Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/perspectivecamera.html:
--------------------------------------------------------------------------------
1 | PerspectiveCamera | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/pointermanager.html:
--------------------------------------------------------------------------------
1 | PointerManager | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/types.html:
--------------------------------------------------------------------------------
1 | types | cameras Legend Namespace Function Type alias Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | cameras by Damien Seguin (https://github.com/dmnsgn)
8 |
27 |
28 |
29 |
30 | cameras
31 |
32 |
33 |
34 |
35 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cameras",
3 | "version": "3.1.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "version": "3.1.1",
9 | "funding": [
10 | {
11 | "type": "individual",
12 | "url": "https://paypal.me/dmnsgn"
13 | },
14 | {
15 | "type": "individual",
16 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3"
17 | }
18 | ],
19 | "license": "MIT",
20 | "dependencies": {
21 | "clamp": "^1.0.1",
22 | "gl-matrix": "^3.3.0",
23 | "normalize-wheel": "^1.0.1"
24 | },
25 | "devDependencies": {
26 | "core-js": "^3.11.1",
27 | "es-module-shims": "^0.10.4",
28 | "primitive-geometry": "^2.0.0",
29 | "regl": "^2.1.0",
30 | "tslib": "^2.2.0"
31 | },
32 | "engines": {
33 | "node": ">=15.0.0",
34 | "npm": ">=7.0.0"
35 | }
36 | },
37 | "node_modules/clamp": {
38 | "version": "1.0.1",
39 | "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
40 | "integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ="
41 | },
42 | "node_modules/core-js": {
43 | "version": "3.11.1",
44 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.11.1.tgz",
45 | "integrity": "sha512-k93Isqg7e4txZWMGNYwevZL9MiogLk8pd1PtwrmFmi8IBq4GXqUaVW/a33Llt6amSI36uSjd0GWwc9pTT9ALlQ==",
46 | "dev": true,
47 | "hasInstallScript": true,
48 | "funding": {
49 | "type": "opencollective",
50 | "url": "https://opencollective.com/core-js"
51 | }
52 | },
53 | "node_modules/es-module-shims": {
54 | "version": "0.10.4",
55 | "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-0.10.4.tgz",
56 | "integrity": "sha512-tkmdigmgPVUWp1+psYM5gwBVhsgynU7v8CHpg74BmXSz+sXwAE42AdJahcoIkQPIUXhErX+BISAsW3chrFSCnQ==",
57 | "dev": true
58 | },
59 | "node_modules/gl-matrix": {
60 | "version": "3.3.0",
61 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz",
62 | "integrity": "sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA=="
63 | },
64 | "node_modules/normalize-wheel": {
65 | "version": "1.0.1",
66 | "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
67 | "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
68 | },
69 | "node_modules/primitive-geometry": {
70 | "version": "2.0.0",
71 | "resolved": "https://registry.npmjs.org/primitive-geometry/-/primitive-geometry-2.0.0.tgz",
72 | "integrity": "sha512-X1Ow3JT8kYuH/GwpJ+xrCTNB45ZE7rQi4qnOjcINmzTVc5/t4bNzpMkQGTsKjFBk7Er0BlQvn2Ey3KJxSDcn1g==",
73 | "dev": true,
74 | "funding": [
75 | {
76 | "type": "individual",
77 | "url": "https://paypal.me/dmnsgn"
78 | },
79 | {
80 | "type": "individual",
81 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3"
82 | }
83 | ],
84 | "engines": {
85 | "node": ">=15.0.0",
86 | "npm": ">=7.0.0"
87 | }
88 | },
89 | "node_modules/regl": {
90 | "version": "2.1.0",
91 | "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.0.tgz",
92 | "integrity": "sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg==",
93 | "dev": true
94 | },
95 | "node_modules/tslib": {
96 | "version": "2.2.0",
97 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
98 | "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
99 | "dev": true
100 | }
101 | },
102 | "dependencies": {
103 | "clamp": {
104 | "version": "1.0.1",
105 | "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
106 | "integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ="
107 | },
108 | "core-js": {
109 | "version": "3.11.1",
110 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.11.1.tgz",
111 | "integrity": "sha512-k93Isqg7e4txZWMGNYwevZL9MiogLk8pd1PtwrmFmi8IBq4GXqUaVW/a33Llt6amSI36uSjd0GWwc9pTT9ALlQ==",
112 | "dev": true
113 | },
114 | "es-module-shims": {
115 | "version": "0.10.4",
116 | "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-0.10.4.tgz",
117 | "integrity": "sha512-tkmdigmgPVUWp1+psYM5gwBVhsgynU7v8CHpg74BmXSz+sXwAE42AdJahcoIkQPIUXhErX+BISAsW3chrFSCnQ==",
118 | "dev": true
119 | },
120 | "gl-matrix": {
121 | "version": "3.3.0",
122 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz",
123 | "integrity": "sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA=="
124 | },
125 | "normalize-wheel": {
126 | "version": "1.0.1",
127 | "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
128 | "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
129 | },
130 | "primitive-geometry": {
131 | "version": "2.0.0",
132 | "resolved": "https://registry.npmjs.org/primitive-geometry/-/primitive-geometry-2.0.0.tgz",
133 | "integrity": "sha512-X1Ow3JT8kYuH/GwpJ+xrCTNB45ZE7rQi4qnOjcINmzTVc5/t4bNzpMkQGTsKjFBk7Er0BlQvn2Ey3KJxSDcn1g==",
134 | "dev": true
135 | },
136 | "regl": {
137 | "version": "2.1.0",
138 | "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.0.tgz",
139 | "integrity": "sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg==",
140 | "dev": true
141 | },
142 | "tslib": {
143 | "version": "2.2.0",
144 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
145 | "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
146 | "dev": true
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cameras",
3 | "version": "3.1.1",
4 | "description": "Cameras for 3D rendering.",
5 | "keywords": [
6 | "cameras",
7 | "perspective",
8 | "orthographic",
9 | "3d",
10 | "webgl"
11 | ],
12 | "homepage": "https://github.com/dmnsgn/cameras",
13 | "bugs": "https://github.com/dmnsgn/cameras/issues",
14 | "repository": "dmnsgn/cameras",
15 | "funding": [
16 | {
17 | "type": "individual",
18 | "url": "https://paypal.me/dmnsgn"
19 | },
20 | {
21 | "type": "individual",
22 | "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3"
23 | }
24 | ],
25 | "license": "MIT",
26 | "author": "Damien Seguin (https://github.com/dmnsgn)",
27 | "type": "module",
28 | "exports": "./lib/index.js",
29 | "main": "lib/index.js",
30 | "types": "types/index.d.ts",
31 | "dependencies": {
32 | "clamp": "^1.0.1",
33 | "gl-matrix": "^3.3.0",
34 | "normalize-wheel": "^1.0.1"
35 | },
36 | "devDependencies": {
37 | "core-js": "^3.11.1",
38 | "es-module-shims": "^0.10.4",
39 | "primitive-geometry": "^2.0.0",
40 | "regl": "^2.1.0",
41 | "tslib": "^2.2.0"
42 | },
43 | "engines": {
44 | "node": ">=15.0.0",
45 | "npm": ">=7.0.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dmnsgn/cameras/9f74b2cc78496feda4cb55b03e9f4e126ce2e573/screenshot.jpg
--------------------------------------------------------------------------------
/src/Camera.ts:
--------------------------------------------------------------------------------
1 | import { mat4, vec3 } from "gl-matrix";
2 |
3 | import { CameraType, CameraOptions, CameraView } from "./types.js";
4 |
5 | export default class Camera {
6 | public readonly type: CameraType = CameraType.Camera;
7 |
8 | public near = 0.1;
9 | public far = 100;
10 |
11 | public up: vec3 = vec3.fromValues(0, 1, 0);
12 | public position: vec3 = vec3.fromValues(0, 0, 1);
13 | public target: vec3 = vec3.create();
14 |
15 | public projectionMatrix: mat4 = mat4.create();
16 |
17 | public viewMatrix: mat4 = mat4.create();
18 | public inverseViewMatrix: mat4 = mat4.create();
19 |
20 | public view?: CameraView;
21 |
22 | constructor(options?: CameraOptions) {
23 | Object.assign(this, options);
24 | }
25 |
26 | public update(): void {
27 | mat4.lookAt(this.viewMatrix, this.position, this.target, this.up);
28 | mat4.copy(this.inverseViewMatrix, this.viewMatrix);
29 | mat4.invert(this.inverseViewMatrix, this.inverseViewMatrix);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Controls.ts:
--------------------------------------------------------------------------------
1 | import { vec3, quat, glMatrix } from "gl-matrix";
2 | import clamp from "clamp";
3 |
4 | import PointerManager from "./PointerManager.js";
5 | import PerspectiveCamera from "./PerspectiveCamera.js";
6 | import OrthographicCamera from "./OrthographicCamera.js";
7 |
8 | import {
9 | ControlsOptions,
10 | ControlsActions,
11 | Radian,
12 | ControlsConfig,
13 | PointerManagerState,
14 | PointerManagerEvent,
15 | } from "./types.js";
16 |
17 | const { EPSILON } = glMatrix;
18 | const PI2 = Math.PI * 2;
19 |
20 | const TEMP = vec3.create();
21 |
22 | export default class Controls {
23 | private static isNegligeable(number: number): boolean {
24 | return Math.abs(number) < EPSILON;
25 | }
26 |
27 | private static Y_UP = vec3.fromValues(0, 1, 0);
28 |
29 | public element: HTMLElement;
30 | public camera: PerspectiveCamera | OrthographicCamera;
31 | public config: ControlsConfig = {
32 | [PointerManagerState.MouseLeft]: ControlsActions.Rotate,
33 | [PointerManagerState.MouseMiddle]: ControlsActions.Dolly,
34 | [PointerManagerState.MouseRight]: ControlsActions.RotatePolar,
35 | [PointerManagerState.MouseWheel]: ControlsActions.Dolly,
36 | [PointerManagerState.TouchOne]: ControlsActions.Rotate,
37 | [PointerManagerState.TouchTwo]: ControlsActions.Dolly,
38 | [PointerManagerState.TouchThree]: ControlsActions.RotatePolar,
39 | };
40 |
41 | public position: vec3 = vec3.fromValues(0, 0, 1);
42 | public target: vec3 = vec3.create();
43 | public phi: Radian = Math.PI / 2;
44 | public theta: Radian = 0;
45 | public distance: number;
46 |
47 | public damping = 0.9;
48 |
49 | public dolly = true;
50 | public dollySpeed = 1;
51 | public dollyMaxDelta = Infinity;
52 |
53 | public rotate = true;
54 | public rotateSpeed = 1;
55 | public rotateMaxThetaDelta = Infinity;
56 | public rotateMaxPhiDelta = Infinity;
57 |
58 | public distanceBounds: number[] = [EPSILON, Infinity];
59 | public phiBounds: Radian[] = [0, Math.PI];
60 | public thetaBounds: Radian[] = [-Infinity, Infinity];
61 |
62 | private pointerManager: PointerManager;
63 | private sphericalTarget: vec3 = vec3.create();
64 | private targetTarget: vec3 = vec3.create();
65 | private upQuat: quat = quat.create();
66 | private upQuatInverse: quat = quat.create();
67 |
68 | constructor(options?: ControlsOptions) {
69 | Object.assign(this, options);
70 |
71 | // Set by spherical angle and optional distance
72 | if (options.theta || options.phi) {
73 | this.updatePosition();
74 | }
75 | // Set by position and optional target
76 | else {
77 | if (!options.position) vec3.copy(this.position, options.camera.position);
78 | vec3.subtract(TEMP, this.position, this.target);
79 | this.distance = vec3.length(TEMP);
80 | this.theta = Math.atan2(this.position[0], this.position[2]);
81 | this.phi = Math.acos(clamp(this.position[1] / this.distance, -1, 1));
82 | }
83 |
84 | // Init private targets
85 | this.sphericalTarget[0] = this.theta;
86 | this.sphericalTarget[1] = this.phi;
87 | this.sphericalTarget[2] = this.distance;
88 | vec3.copy(this.targetTarget, this.target);
89 |
90 | this.update();
91 |
92 | this.onPointerUpdate = this.onPointerUpdate.bind(this);
93 |
94 | this.pointerManager = new PointerManager({
95 | element: this.element,
96 | config: { wheel: true, drag: true },
97 | onPointerUpdate: this.onPointerUpdate,
98 | });
99 | this.pointerManager.enable();
100 | }
101 |
102 | // Actions
103 | private handleDolly(event: PointerManagerEvent): void {
104 | if (!this.dolly) return;
105 |
106 | let delta = event.dy;
107 | switch (event.state) {
108 | case PointerManagerState.MouseLeft:
109 | case PointerManagerState.MouseRight:
110 | case PointerManagerState.MouseMiddle: {
111 | delta *= 20;
112 | break;
113 | }
114 |
115 | case PointerManagerState.TouchTwo: {
116 | delta /= 20;
117 | break;
118 | }
119 |
120 | default:
121 | break;
122 | }
123 |
124 | this.sphericalTarget[2] += clamp(
125 | delta * this.dollySpeed,
126 | -this.dollyMaxDelta,
127 | this.dollyMaxDelta
128 | );
129 | }
130 |
131 | private handleRotateAzimuth(event: PointerManagerEvent): void {
132 | this.sphericalTarget[0] -= clamp(
133 | PI2 * event.dx * this.rotateSpeed,
134 | -this.rotateMaxThetaDelta,
135 | this.rotateMaxThetaDelta
136 | );
137 | }
138 |
139 | private handleRotatePolar(event: PointerManagerEvent): void {
140 | this.sphericalTarget[1] -= clamp(
141 | PI2 * event.dy * this.rotateSpeed,
142 | -this.rotateMaxPhiDelta,
143 | this.rotateMaxPhiDelta
144 | );
145 | }
146 |
147 | private handleRotate(event: PointerManagerEvent): void {
148 | if (!this.rotate) return;
149 |
150 | this.handleRotateAzimuth(event);
151 | this.handleRotatePolar(event);
152 | }
153 |
154 | // Pointer Event handlers
155 | private onPointerUpdate(event: PointerManagerEvent): void {
156 | this[
157 | `handle${this.config[event.state]}` as
158 | | "handleDolly"
159 | | "handleRotateAzimuth"
160 | | "handleRotatePolar"
161 | | "handleRotate"
162 | ](event);
163 | }
164 |
165 | // Update
166 | private updatePosition(): void {
167 | this.distance = Math.max(EPSILON, this.distance);
168 |
169 | this.position[0] =
170 | this.distance * Math.sin(this.phi) * Math.sin(this.theta);
171 | this.position[1] = this.distance * Math.cos(this.phi);
172 | this.position[2] =
173 | this.distance * Math.sin(this.phi) * Math.cos(this.theta);
174 | }
175 |
176 | public update(): void {
177 | const dampRatio = 1 - this.damping;
178 | const deltaTheta = this.sphericalTarget[0] - this.theta;
179 | const deltaPhi = this.sphericalTarget[1] - this.phi;
180 | const deltaDistance = this.sphericalTarget[2] - this.distance;
181 | const deltaTarget = vec3.create();
182 | vec3.sub(deltaTarget, this.targetTarget, this.target);
183 |
184 | if (
185 | !Controls.isNegligeable(deltaTheta) ||
186 | !Controls.isNegligeable(deltaPhi) ||
187 | !Controls.isNegligeable(deltaDistance) ||
188 | !Controls.isNegligeable(deltaTarget[0]) ||
189 | !Controls.isNegligeable(deltaTarget[1]) ||
190 | !Controls.isNegligeable(deltaTarget[2])
191 | ) {
192 | this.theta = this.theta + deltaTheta * dampRatio;
193 | this.phi = this.phi + deltaPhi * dampRatio;
194 | this.distance = this.distance + deltaDistance * dampRatio;
195 |
196 | vec3.add(
197 | this.target,
198 | this.target,
199 | vec3.scale(deltaTarget, deltaTarget, dampRatio)
200 | );
201 | } else {
202 | this.theta = this.sphericalTarget[0];
203 | this.phi = this.sphericalTarget[1];
204 | this.distance = this.sphericalTarget[2];
205 |
206 | vec3.copy(this.targetTarget, this.target);
207 | vec3.copy(this.target, deltaTarget);
208 | }
209 |
210 | vec3.subtract(this.position, this.position, this.target);
211 | vec3.transformQuat(this.position, this.position, this.upQuat);
212 |
213 | this.phi = clamp(this.phi, EPSILON, Math.PI - EPSILON);
214 | this.distance = clamp(
215 | this.distance,
216 | this.distanceBounds[0],
217 | this.distanceBounds[1]
218 | );
219 |
220 | quat.rotationTo(this.upQuat, this.camera.up, Controls.Y_UP);
221 | quat.invert(this.upQuatInverse, this.upQuat);
222 |
223 | this.updatePosition();
224 |
225 | // TODO: copy directly into camera as an option
226 | vec3.transformQuat(this.position, this.position, this.upQuatInverse);
227 | vec3.add(this.position, this.target, this.position);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/OrthographicCamera.ts:
--------------------------------------------------------------------------------
1 | import { mat4 } from "gl-matrix";
2 |
3 | import Camera from "./Camera.js";
4 |
5 | import { OrthographicCameraOptions, CameraType } from "./types.js";
6 |
7 | export default class OrthographicCamera extends Camera {
8 | public readonly type: CameraType = CameraType.Orthographic;
9 |
10 | public left = -1;
11 | public right = 1;
12 | public top = 1;
13 | public bottom = -1;
14 |
15 | public zoom = 1;
16 |
17 | constructor(options?: OrthographicCameraOptions) {
18 | super(options);
19 |
20 | Object.assign(this, options);
21 |
22 | this.updateProjectionMatrix();
23 | }
24 |
25 | public updateProjectionMatrix(): void {
26 | const dx = (this.right - this.left) / (2 / this.zoom);
27 | const dy = (this.top - this.bottom) / (2 / this.zoom);
28 | const cx = (this.right + this.left) / 2;
29 | const cy = (this.top + this.bottom) / 2;
30 |
31 | let left = cx - dx;
32 | let right = cx + dx;
33 | let top = cy + dy;
34 | let bottom = cy - dy;
35 |
36 | if (this.view) {
37 | const zoomW =
38 | 1 / this.zoom / (this.view.size[0] / this.view.totalSize[0]);
39 | const zoomH =
40 | 1 / this.zoom / (this.view.size[1] / this.view.totalSize[1]);
41 | const scaleW = (this.right - this.left) / this.view.size[0];
42 | const scaleH = (this.top - this.bottom) / this.view.size[1];
43 |
44 | left += scaleW * (this.view.offset[0] / zoomW);
45 | right = left + scaleW * (this.view.size[0] / zoomW);
46 | top -= scaleH * (this.view.offset[1] / zoomH);
47 | bottom = top - scaleH * (this.view.size[1] / zoomH);
48 | }
49 |
50 | mat4.ortho(
51 | this.projectionMatrix,
52 | left,
53 | right,
54 | bottom,
55 | top,
56 | this.near,
57 | this.far
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/PerspectiveCamera.ts:
--------------------------------------------------------------------------------
1 | import { mat4 } from "gl-matrix";
2 |
3 | import Camera from "./Camera.js";
4 |
5 | import { PerspectiveCameraOptions, Radian, CameraType } from "./types.js";
6 |
7 | export default class PerspectiveCamera extends Camera {
8 | public readonly type: CameraType = CameraType.Perspective;
9 |
10 | public fov: Radian = Math.PI / 4;
11 | public aspect?: number = 1;
12 |
13 | constructor(options?: PerspectiveCameraOptions) {
14 | super(options);
15 |
16 | Object.assign(this, options);
17 |
18 | this.updateProjectionMatrix();
19 | }
20 |
21 | public updateProjectionMatrix(): void {
22 | if (this.view) {
23 | const aspectRatio = this.view.totalSize[0] / this.view.totalSize[1];
24 |
25 | const top = Math.tan(this.fov * 0.5) * this.near;
26 | const bottom = -top;
27 | const left = aspectRatio * bottom;
28 | const right = aspectRatio * top;
29 | const width = Math.abs(right - left);
30 | const height = Math.abs(top - bottom);
31 | const widthNormalized = width / this.view.totalSize[0];
32 | const heightNormalized = height / this.view.totalSize[1];
33 |
34 | const l = left + this.view.offset[0] * widthNormalized;
35 | const r =
36 | left + (this.view.offset[0] + this.view.size[0]) * widthNormalized;
37 | const b =
38 | top - (this.view.offset[1] + this.view.size[1]) * heightNormalized;
39 | const t = top - this.view.offset[1] * heightNormalized;
40 |
41 | mat4.frustum(this.projectionMatrix, l, r, b, t, this.near, this.far);
42 | } else {
43 | mat4.perspective(
44 | this.projectionMatrix,
45 | this.fov,
46 | this.aspect,
47 | this.near,
48 | this.far
49 | );
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/PointerManager.ts:
--------------------------------------------------------------------------------
1 | import { vec2 } from "gl-matrix";
2 | import normalizeWheel from "normalize-wheel";
3 |
4 | import {
5 | PointerManagerOptions,
6 | PointerManagerState,
7 | PointerManagerEvent,
8 | PointerManagerConfig,
9 | } from "./types.js";
10 |
11 | const HAS_TOUCH_EVENTS = "TouchEvent" in window;
12 | const WHEEL_EVENT = normalizeWheel.getEventType();
13 | const EVENT_LISTENER_OPTIONS = {
14 | passive: false,
15 | };
16 | const VEC2_IDENTITY = vec2.create();
17 | const tempElement = vec2.create();
18 | const tempPointer = vec2.create();
19 |
20 | export default class PointerManager {
21 | private static isTouchEvent(event: Event): boolean {
22 | return HAS_TOUCH_EVENTS && event instanceof TouchEvent;
23 | }
24 |
25 | private static BUTTONS = [
26 | PointerManagerState.MouseLeft,
27 | PointerManagerState.MouseMiddle,
28 | PointerManagerState.MouseRight,
29 | ];
30 |
31 | private static TOUCHES = [
32 | PointerManagerState.Idle,
33 | PointerManagerState.TouchOne,
34 | PointerManagerState.TouchTwo,
35 | PointerManagerState.TouchThree,
36 | ];
37 |
38 | public element: HTMLElement;
39 | public config: PointerManagerConfig;
40 |
41 | public onPointerUpdate: (event: PointerManagerEvent) => unknown;
42 |
43 | private state: PointerManagerState;
44 | private initialTouchDistance = 0;
45 | private initialPosition: vec2 = vec2.create();
46 | private lastPosition: vec2 = vec2.create();
47 | private movePosition: vec2 = vec2.create();
48 | private clientSize: vec2 = vec2.create();
49 | private isElementRoot: boolean;
50 |
51 | constructor(options: PointerManagerOptions) {
52 | Object.assign(this, options);
53 |
54 | this.onMouseWheel = this.onMouseWheel.bind(this);
55 | this.onMouseDown = this.onMouseDown.bind(this);
56 | this.onTouchStart = this.onTouchStart.bind(this);
57 | this.handleDragging = this.handleDragging.bind(this);
58 | this.onPointerUp = this.onPointerUp.bind(this);
59 | }
60 |
61 | public enable(): void {
62 | this.isElementRoot = this.element === document.body;
63 |
64 | if (this.config.wheel) {
65 | this.element.addEventListener(WHEEL_EVENT, this.onMouseWheel);
66 | }
67 |
68 | if (this.config.drag) {
69 | this.element.addEventListener("mousedown", this.onMouseDown);
70 | this.element.addEventListener("touchstart", this.onTouchStart);
71 | }
72 | }
73 |
74 | public disable(): void {
75 | if (this.config.wheel) {
76 | this.element.removeEventListener(WHEEL_EVENT, this.onMouseWheel);
77 | }
78 |
79 | if (this.config.drag) {
80 | this.element.removeEventListener("mousedown", this.onMouseDown);
81 | this.element.removeEventListener("touchstart", this.onTouchStart);
82 | }
83 | }
84 |
85 | // Compute position helpers
86 | private setClientSize(out: vec2): void {
87 | const element = this.isElementRoot
88 | ? document.documentElement
89 | : this.element;
90 | out[0] = element.clientWidth;
91 | out[1] = element.clientHeight;
92 | }
93 |
94 | private setTouchBaryCenter(out: vec2, event: TouchEvent): void {
95 | for (let i = 0; i < event.touches.length; i++) {
96 | out[0] += event.touches[i].clientX;
97 | out[1] += event.touches[i].clientY;
98 | }
99 |
100 | out[0] /= event.touches.length;
101 | out[1] /= event.touches.length;
102 | }
103 |
104 | private getPointerPosition(event: Event): vec2 {
105 | if (PointerManager.isTouchEvent(event)) {
106 | vec2.zero(tempPointer);
107 | this.setTouchBaryCenter(tempPointer, event as TouchEvent);
108 | } else {
109 | tempPointer[0] = (event as MouseEvent).clientX;
110 | tempPointer[1] = (event as MouseEvent).clientY;
111 | }
112 | return tempPointer;
113 | }
114 |
115 | private getElementPosition(): vec2 {
116 | return this.isElementRoot
117 | ? VEC2_IDENTITY
118 | : (() => {
119 | const { left, top } = this.element.getBoundingClientRect();
120 | tempElement[0] = left;
121 | tempElement[1] = top;
122 | return tempElement;
123 | })();
124 | }
125 |
126 | private setRelativePosition(out: vec2, event: Event): void {
127 | vec2.subtract(
128 | out,
129 | this.getPointerPosition(event),
130 | this.getElementPosition()
131 | );
132 | }
133 |
134 | // Dragging
135 | private initDragging(event: Event): void {
136 | this.setRelativePosition(this.initialPosition, event);
137 | this.setClientSize(this.clientSize);
138 |
139 | if (
140 | PointerManager.isTouchEvent(event) &&
141 | (event as TouchEvent).touches.length >= 2
142 | ) {
143 | const { clientX, clientY } = (event as TouchEvent).touches[1];
144 |
145 | // Get finger distance
146 | this.initialTouchDistance = vec2.distance(
147 | [clientX, clientY],
148 | this.initialPosition
149 | );
150 |
151 | // Set position to center
152 | vec2.set(
153 | this.lastPosition,
154 | ((event as TouchEvent).touches[0].clientX + clientX) * 0.5,
155 | ((event as TouchEvent).touches[0].clientY + clientY) * 0.5
156 | );
157 | } else {
158 | vec2.copy(this.lastPosition, this.initialPosition);
159 | }
160 |
161 | document.addEventListener("mousemove", this.handleDragging);
162 | document.addEventListener(
163 | "touchmove",
164 | this.handleDragging,
165 | EVENT_LISTENER_OPTIONS
166 | );
167 | document.addEventListener("mouseup", this.onPointerUp);
168 | document.addEventListener("touchend", this.onPointerUp);
169 | }
170 |
171 | private handleDragging(event: Event): void {
172 | event.preventDefault();
173 |
174 | this.setRelativePosition(this.movePosition, event);
175 |
176 | let dx = 0;
177 | let dy = 0;
178 | if (
179 | PointerManager.isTouchEvent(event) &&
180 | (event as TouchEvent).touches.length >= 2
181 | ) {
182 | dy =
183 | this.initialTouchDistance -
184 | vec2.distance(
185 | [
186 | (event as TouchEvent).touches[1].clientX,
187 | (event as TouchEvent).touches[1].clientY,
188 | ],
189 | this.movePosition
190 | );
191 | } else {
192 | dx = (this.movePosition[0] - this.lastPosition[0]) / this.clientSize[1];
193 | dy = (this.movePosition[1] - this.lastPosition[1]) / this.clientSize[1];
194 | }
195 | vec2.copy(this.lastPosition, this.movePosition);
196 |
197 | this.onPointerUpdate({
198 | state: this.state,
199 | dx,
200 | dy,
201 | originalEvent: event,
202 | });
203 | }
204 |
205 | // Event handlers
206 | private onMouseWheel(event: Event): void {
207 | this.state = PointerManagerState.MouseWheel;
208 |
209 | this.onPointerUpdate({
210 | state: this.state,
211 | // Try normalising with drag offset
212 | dx: normalizeWheel(event).pixelX / 100,
213 | dy: normalizeWheel(event).pixelY / 100,
214 | });
215 | }
216 |
217 | private onMouseDown(event: MouseEvent): void {
218 | const prevState = this.state;
219 |
220 | this.state = PointerManager.BUTTONS[event.button];
221 |
222 | if (prevState !== this.state) this.initDragging(event);
223 | }
224 |
225 | private onTouchStart(event: TouchEvent): void {
226 | event.preventDefault();
227 |
228 | const prevState = this.state;
229 |
230 | this.state = PointerManager.TOUCHES[event.touches.length];
231 |
232 | if (prevState !== this.state) this.initDragging(event);
233 | }
234 |
235 | private onPointerUp(): void {
236 | this.state = PointerManagerState.Idle;
237 |
238 | document.removeEventListener("mousemove", this.handleDragging);
239 | document.removeEventListener(
240 | "touchmove",
241 | this.handleDragging,
242 | EVENT_LISTENER_OPTIONS as AddEventListenerOptions
243 | );
244 | document.removeEventListener("mouseup", this.onPointerUp);
245 | document.removeEventListener("touchend", this.onPointerUp);
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Camera } from "./Camera.js";
2 | export { default as PerspectiveCamera } from "./PerspectiveCamera.js";
3 | export { default as OrthographicCamera } from "./OrthographicCamera.js";
4 | export { default as Controls } from "./Controls.js";
5 | export { default as PointerManager } from "./PointerManager.js";
6 |
--------------------------------------------------------------------------------
/src/normalize-wheel.d.ts:
--------------------------------------------------------------------------------
1 | declare module "normalize-wheel" {
2 | interface NormalizedWheelEvent {
3 | spinX: number;
4 | spinY: number;
5 | pixelX: number;
6 | pixelY: number;
7 | }
8 |
9 | function normalizeWheel(event: Event): NormalizedWheelEvent;
10 |
11 | namespace normalizeWheel {
12 | export let getEventType: () => string;
13 | }
14 |
15 | export = normalizeWheel;
16 | }
17 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { mat4, vec3 } from "gl-matrix";
2 | import PerspectiveCamera from "./PerspectiveCamera.js";
3 | import OrthographicCamera from "./OrthographicCamera.js";
4 |
5 | // General
6 | export type Radian = number;
7 | export type Degree = number;
8 | export type Pixel = number;
9 |
10 | // Camera
11 | export enum CameraType {
12 | Camera,
13 | Perspective,
14 | Orthographic,
15 | }
16 |
17 | export interface CameraOptions {
18 | near?: number;
19 | far?: number;
20 | up?: vec3;
21 |
22 | projectionMatrix?: mat4;
23 |
24 | viewMatrix?: mat4;
25 | inverseViewMatrix?: mat4;
26 |
27 | position?: vec3;
28 | target?: vec3;
29 |
30 | view?: CameraView;
31 | }
32 |
33 | export interface PerspectiveCameraOptions extends CameraOptions {
34 | fov: number;
35 | aspect: number;
36 | }
37 |
38 | export interface OrthographicCameraOptions extends CameraOptions {
39 | left: number;
40 | right: number;
41 | top: number;
42 | bottom: number;
43 | zoom: 1;
44 | }
45 |
46 | export interface CameraView {
47 | totalSize: [number, number];
48 | size: [number, number];
49 | offset: [number, number];
50 | }
51 |
52 | // Controls
53 | export type ControlsConfig = {
54 | [key in PointerManagerState]?: ControlsActions;
55 | };
56 |
57 | export interface ControlsOptions {
58 | element: HTMLElement;
59 | camera: PerspectiveCamera | OrthographicCamera;
60 | config: ControlsConfig;
61 |
62 | position: vec3;
63 | target: vec3;
64 | distance: number;
65 |
66 | damping: number;
67 |
68 | dolly: boolean;
69 | dollySpeed: number;
70 | dollyMaxDelta: number;
71 |
72 | rotate: boolean;
73 | rotateSpeed: number;
74 | rotateMaxThetaDelta: number;
75 | rotateMaxPhiDelta: number;
76 |
77 | phiBounds: Radian[];
78 | thetaBounds: Radian[];
79 | distanceBounds: number[];
80 |
81 | phi: Radian;
82 | theta: Radian;
83 | }
84 |
85 | export enum ControlsActions {
86 | Rotate = "Rotate",
87 | RotatePolar = "RotatePolar",
88 | RotateAzimuth = "RotateAzimuth",
89 | Dolly = "Dolly",
90 | Zoom = "Zoom",
91 | }
92 |
93 | // PointerManager
94 | export interface PointerManagerConfig {
95 | wheel: boolean;
96 | drag: boolean;
97 | }
98 |
99 | export enum PointerManagerState {
100 | Idle = "Idle",
101 | MouseWheel = "MouseWheel",
102 | MouseLeft = "MouseLeft",
103 | MouseMiddle = "MouseMiddle",
104 | MouseRight = "MouseRight",
105 | TouchOne = "TouchOne",
106 | TouchTwo = "TouchTwo",
107 | TouchThree = "TouchThree",
108 | }
109 |
110 | export interface PointerManagerEvent {
111 | state: PointerManagerState;
112 | originalEvent?: Event;
113 | dx?: Pixel;
114 | dy?: Pixel;
115 | }
116 |
117 | export interface PointerManagerOptions {
118 | element: HTMLElement;
119 | config: PointerManagerConfig;
120 | onPointerUpdate?: (event: PointerManagerEvent) => unknown;
121 | }
122 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "declaration": true,
6 | "declarationDir": "types",
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "importHelpers": true,
10 | "lib": ["DOM", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"],
11 | "module": "ES2020",
12 | "moduleResolution": "node",
13 | "outDir": "lib",
14 | "sourceMap": true,
15 | "strictFunctionTypes": true,
16 | "target": "ES2020"
17 | },
18 | "include": [
19 | "src/**/*"
20 | ],
21 | "exclude": [
22 | "node_modules",
23 | "web_modules",
24 | "**/*.spec.ts"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------