├── .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 | [![npm version](https://img.shields.io/npm/v/cameras)](https://www.npmjs.com/package/cameras) 4 | [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://www.npmjs.com/package/cameras) 5 | [![npm minzipped size](https://img.shields.io/bundlephobia/minzip/cameras)](https://bundlephobia.com/package/cameras) 6 | [![dependencies](https://img.shields.io/librariesio/release/npm/cameras)](https://github.com/dmnsgn/cameras/blob/main/package.json) 7 | [![types](https://img.shields.io/npm/types/cameras)](https://github.com/microsoft/TypeScript) 8 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fa6673.svg)](https://conventionalcommits.org) 9 | [![styled with prettier](https://img.shields.io/badge/styled_with-Prettier-f8bc45.svg?logo=prettier)](https://github.com/prettier/prettier) 10 | [![linted with eslint](https://img.shields.io/badge/linted_with-ES_Lint-4B32C3.svg?logo=eslint)](https://github.com/eslint/eslint) 11 | [![license](https://img.shields.io/github/license/dmnsgn/cameras)](https://github.com/dmnsgn/cameras/blob/main/LICENSE.md) 12 | 13 | Cameras for 3D rendering. 14 | 15 | [![paypal](https://img.shields.io/badge/donate-paypal-informational?logo=paypal)](https://paypal.me/dmnsgn) 16 | [![coinbase](https://img.shields.io/badge/donate-coinbase-informational?logo=coinbase)](https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3) 17 | [![twitter](https://img.shields.io/twitter/follow/dmnsgn?style=social)](https://twitter.com/dmnsgn) 18 | 19 | ![](https://raw.githubusercontent.com/dmnsgn/cameras/master/screenshot.jpg) 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
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Constructors

constructor

Properties

far

far: number = 100

inverseViewMatrix

inverseViewMatrix: mat4 = ...

near

near: number = 0.1

position

position: vec3 = ...

projectionMatrix

projectionMatrix: mat4 = ...

target

target: vec3 = ...

Readonly type

type: CameraType = CameraType.Camera

up

up: vec3 = ...

Optional view

view?: CameraView

viewMatrix

viewMatrix: mat4 = ...

Methods

update

  • update(): void

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/enums/types.cameratype.html: -------------------------------------------------------------------------------- 1 | CameraType | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration CameraType

Index

Enumeration members

Camera

Camera = 0

Orthographic

Orthographic = 2

Perspective

Perspective = 1

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/enums/types.controlsactions.html: -------------------------------------------------------------------------------- 1 | ControlsActions | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration ControlsActions

Index

Enumeration members

Dolly

Dolly = "Dolly"

Rotate

Rotate = "Rotate"

RotateAzimuth

RotateAzimuth = "RotateAzimuth"

RotatePolar

RotatePolar = "RotatePolar"

Zoom

Zoom = "Zoom"

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/enums/types.pointermanagerstate.html: -------------------------------------------------------------------------------- 1 | PointerManagerState | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Enumeration PointerManagerState

Index

Enumeration members

Idle

Idle = "Idle"

MouseLeft

MouseLeft = "MouseLeft"

MouseMiddle

MouseMiddle = "MouseMiddle"

MouseRight

MouseRight = "MouseRight"

MouseWheel

MouseWheel = "MouseWheel"

TouchOne

TouchOne = "TouchOne"

TouchThree

TouchThree = "TouchThree"

TouchTwo

TouchTwo = "TouchTwo"

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

cameras

2 | 3 |

cameras

4 |
5 |

npm version 6 | stability-stable 7 | npm minzipped size 8 | dependencies 9 | types 10 | Conventional Commits 11 | styled with prettier 12 | linted with eslint 13 | license

14 |

Cameras for 3D rendering.

15 |

paypal 16 | coinbase 17 | twitter

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 |

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.cameraoptions.html: -------------------------------------------------------------------------------- 1 | CameraOptions | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface CameraOptions

Hierarchy

Index

Properties

Optional far

far?: number

Optional inverseViewMatrix

inverseViewMatrix?: mat4

Optional near

near?: number

Optional position

position?: vec3

Optional projectionMatrix

projectionMatrix?: mat4

Optional target

target?: vec3

Optional up

up?: vec3

Optional view

view?: CameraView

Optional viewMatrix

viewMatrix?: mat4

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.cameraview.html: -------------------------------------------------------------------------------- 1 | CameraView | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface CameraView

Hierarchy

  • CameraView

Index

Properties

offset

offset: [number, number]

size

size: [number, number]

totalSize

totalSize: [number, number]

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.orthographiccameraoptions.html: -------------------------------------------------------------------------------- 1 | OrthographicCameraOptions | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface OrthographicCameraOptions

Hierarchy

Index

Properties

bottom

bottom: number

Optional far

far?: number

Optional inverseViewMatrix

inverseViewMatrix?: mat4

left

left: number

Optional near

near?: number

Optional position

position?: vec3

Optional projectionMatrix

projectionMatrix?: mat4

right

right: number

Optional target

target?: vec3

top

top: number

Optional up

up?: vec3

Optional view

view?: CameraView

Optional viewMatrix

viewMatrix?: mat4

zoom

zoom: 1

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.perspectivecameraoptions.html: -------------------------------------------------------------------------------- 1 | PerspectiveCameraOptions | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface PerspectiveCameraOptions

Hierarchy

Index

Properties

aspect

aspect: number

Optional far

far?: number

fov

fov: number

Optional inverseViewMatrix

inverseViewMatrix?: mat4

Optional near

near?: number

Optional position

position?: vec3

Optional projectionMatrix

projectionMatrix?: mat4

Optional target

target?: vec3

Optional up

up?: vec3

Optional view

view?: CameraView

Optional viewMatrix

viewMatrix?: mat4

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.pointermanagerconfig.html: -------------------------------------------------------------------------------- 1 | PointerManagerConfig | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface PointerManagerConfig

Hierarchy

  • PointerManagerConfig

Index

Properties

Properties

drag

drag: boolean

wheel

wheel: boolean

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.pointermanagerevent.html: -------------------------------------------------------------------------------- 1 | PointerManagerEvent | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface PointerManagerEvent

Hierarchy

  • PointerManagerEvent

Index

Properties

Optional dx

dx?: number

Optional dy

dy?: number

Optional originalEvent

originalEvent?: Event

state

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/types.pointermanageroptions.html: -------------------------------------------------------------------------------- 1 | PointerManagerOptions | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface PointerManagerOptions

Hierarchy

  • PointerManagerOptions

Index

Properties

config

element

element: HTMLElement

Methods

Optional onPointerUpdate

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

cameras

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/camera.html: -------------------------------------------------------------------------------- 1 | Camera | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module Camera

Index

Classes

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/controls.html: -------------------------------------------------------------------------------- 1 | Controls | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module Controls

Index

Classes

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/index.html: -------------------------------------------------------------------------------- 1 | index | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module index

Index

References

Camera

Renames and re-exports default

Controls

Renames and re-exports default

OrthographicCamera

Renames and re-exports default

PerspectiveCamera

Renames and re-exports default

PointerManager

Renames and re-exports default

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/normalize_wheel.export_.html: -------------------------------------------------------------------------------- 1 | export= | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Index

Functions

Functions

getEventType

  • getEventType(): string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/normalize_wheel.html: -------------------------------------------------------------------------------- 1 | normalize-wheel | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module normalize-wheel

Index

Namespaces

Functions

Functions

export=

  • export=(event: Event): NormalizedWheelEvent

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/orthographiccamera.html: -------------------------------------------------------------------------------- 1 | OrthographicCamera | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module OrthographicCamera

Index

Classes

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/perspectivecamera.html: -------------------------------------------------------------------------------- 1 | PerspectiveCamera | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module PerspectiveCamera

Index

Classes

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/pointermanager.html: -------------------------------------------------------------------------------- 1 | PointerManager | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module PointerManager

Index

Classes

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/types.html: -------------------------------------------------------------------------------- 1 | types | cameras
Options
All
  • Public
  • Public/Protected
  • All
Menu

Module types

Index

Type aliases

ControlsConfig

ControlsConfig: { [ key in PointerManagerState]?: ControlsActions }

Degree

Degree: number

Pixel

Pixel: number

Radian

Radian: number

Generated using TypeDoc

-------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------