diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..0ed8036 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 9daa824..ff14e50 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -.DS_Store node_modules +.DS_Store +types +lib +web_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..99f0922 --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +web_modules +examples +docs +coverage +test +.github +screenshot.* +index.html +tsconfig.json +.editorconfig diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3f8fda1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +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. + +# [3.0.0-alpha.1](https://github.com/pex-gl/pex-cam/compare/v3.0.0-alpha.0...v3.0.0-alpha.1) (2022-07-04) + + +### Bug Fixes + +* export types ([8e8808e](https://github.com/pex-gl/pex-cam/commit/8e8808e8ae5db32c5db43dfcbab5119f4cb37df0)) + + + +# [3.0.0-alpha.0](https://github.com/pex-gl/pex-cam/compare/v2.7.1...v3.0.0-alpha.0) (2022-07-04) + + +### Bug Fixes + +* add missing file extension ([eca8bbf](https://github.com/pex-gl/pex-cam/commit/eca8bbf43a9c6ca33d631c5aba4b4154f8ea372f)) +* add passive false to onWheel event listener ([5030629](https://github.com/pex-gl/pex-cam/commit/503062949e2daafe79c6a7a081c3bcfba6580ea7)) +* add passive false to touchstart ([ec854e5](https://github.com/pex-gl/pex-cam/commit/ec854e546168df7c19d45f21f122d82b2f9b3d22)) +* orbiter use Object.getOwnPropertyDescriptor ([c0f1200](https://github.com/pex-gl/pex-cam/commit/c0f120078c925bc376bd4efbc134fa4da0380a8e)) +* update default min/maxDistance ([8fb81af](https://github.com/pex-gl/pex-cam/commit/8fb81afec0eb0ebfbfab2c9ecc4acbce7f81eadf)), closes [#20](https://github.com/pex-gl/pex-cam/issues/20) +* use static getters for perspective and orthographic default options ([27be007](https://github.com/pex-gl/pex-cam/commit/27be0075105ea82fc5543575de24175e85e49187)) + + +### Code Refactoring + +* use ES modules ([3374096](https://github.com/pex-gl/pex-cam/commit/3374096e968355039c9c260e19b08990bb3c6086)) + + +### Features + +* add generic camera interface ([4963ed6](https://github.com/pex-gl/pex-cam/commit/4963ed629f4ca85b4b96a15bdfb1a11c898c0382)) +* add orthographic camera ([45bc72b](https://github.com/pex-gl/pex-cam/commit/45bc72bfa41e58e3873250b759b208c0458adc9c)) +* allow autoUpdate set ([ac442fc](https://github.com/pex-gl/pex-cam/commit/ac442fcc24a820ed47c546b5e0053609510f5521)) +* handle orthographic camera in orbiter ([31caa01](https://github.com/pex-gl/pex-cam/commit/31caa01764643c74262d2a9d45ec7d1418344bd7)) +* move to pointer events ([780caa6](https://github.com/pex-gl/pex-cam/commit/780caa6bf1128b8a8c2ec7678cb5ab5db303b290)) +* rename camera frustum to view ([01b5fe5](https://github.com/pex-gl/pex-cam/commit/01b5fe5d1d102367e6f449e3a35c11823634e4a9)) + + +### BREAKING CHANGES + +* switch to type module diff --git a/README.md b/README.md old mode 100644 new mode 100755 index a9d9c6c..cb7d529 --- a/README.md +++ b/README.md @@ -1,76 +1,403 @@ # pex-cam -Camera models and controllers. +[![npm version](https://img.shields.io/npm/v/pex-cam)](https://www.npmjs.com/package/pex-cam) +[![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://www.npmjs.com/package/pex-cam) +[![npm minzipped size](https://img.shields.io/bundlephobia/minzip/pex-cam)](https://bundlephobia.com/package/pex-cam) +[![dependencies](https://img.shields.io/librariesio/release/npm/pex-cam)](https://github.com/pex-gl/pex-cam/blob/main/package.json) +[![types](https://img.shields.io/npm/types/pex-cam)](https://github.com/microsoft/TypeScript) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fa6673.svg)](https://conventionalcommits.org) +[![styled with prettier](https://img.shields.io/badge/styled_with-Prettier-f8bc45.svg?logo=prettier)](https://github.com/prettier/prettier) +[![linted with eslint](https://img.shields.io/badge/linted_with-ES_Lint-4B32C3.svg?logo=eslint)](https://github.com/eslint/eslint) +[![license](https://img.shields.io/github/license/pex-gl/pex-cam)](https://github.com/pex-gl/pex-cam/blob/main/LICENSE.md) -# API +Cameras models and controllers for 3D rendering in [PEX](https://pex.gl). -## Perspective Camera +![](https://raw.githubusercontent.com/pex-gl/pex-cam/main/screenshot.png) -```javascript -var createPerspectiveCamera = require('pex-cam/perspective') +## Installation + +```bash +npm install pex-cam ``` -### `cam = createPerspectiveCamera(opts)` +## Usage + +```js +import { + perspective as createPerspectiveCamera, + orbiter as createOrbiter, +} from "pex-cam"; -Creates new perspective camera +const perspectiveCamera = createPerspectiveCamera({ + position: [2, 2, 2], + target: [0, -0.5, 0], + aspect: window.innerWidth / window.innerHeight, +}); -- `opts:` object with one or more of the following options - - `position`: vec3 - camera position, `[0, 0, 3]` - - `target`: vec3 - camera target, `[0, 0, 0]` - - `up`: vec3 - camera up direction, `[0, 1, 0]` - - `fov`: Number - vertical field of view, `PI/3 (60 deg)` - - `aspect`: Number - aspect ratio , `1` - - `near`: Number - near clipping plane, `0.1` - - `far`: Number - far clipping plane, `100` +const perspectiveOrbiter = createOrbiter({ + camera: perspectiveCamera, +}); -### `cam.set(opts)` +console.log(perspectiveCamera.projectionMatrix); +``` -- `opts`: see `createPerspectiveCamera` +## API -### `cam.getViewRay(x, y, windowWidth, windowHeight)` + -Create picking ray in view (camera) cooridinates +## Modules -- `x`: Number - mouse x -- `y`: Number - mouse y -- `windowWidth`: Number -- `windowHeight`: Number +
+
index
+

Re-export classes and factory functions

+
+
-### `cam.getWorldRay(x, y, windowWidth, windowHeight)` +## Classes -Create picking ray in world coordinates +
+
Camera
+

An interface for cameras to extend

+
+
OrbiterControls
+

Camera controls to orbit around a target

+
+
OrthographicCameraCamera
+

A class to create an orthographic camera

+
+
PerspectiveCameraCamera
+

A class to create a perspective camera

+
+
-- `x`: Number - mouse x -- `y`: Number - mouse y -- `windowWidth`: Number -- `windowHeight`: Number +## Typedefs -## Orbiter +
+
Radians : number
+
+
Degrees : number
+
+
CameraView : Object
+
+
CameraOptions : Object
+
+
PerspectiveCameraOptions : Object
+
+
OrthographicCameraOptions : Object
+
+
OrbiterControlsOptions : Object
+
+
-Orbiter controller + -```javascript -var createOrbiter = require('pex-cam/orbiter') -``` +## index + +Re-export classes and factory functions + +- [index](#module_index) + - [.perspective](#module_index.perspective) ⇒ [PerspectiveCamera](#PerspectiveCamera) + - [.orthographic](#module_index.orthographic) ⇒ [OrthographicCamera](#OrthographicCamera) + - [.orbiter](#module_index.orbiter) ⇒ [OrbiterControls](#OrbiterControls) + + + +### index.perspective ⇒ [PerspectiveCamera](#PerspectiveCamera) + +Factory function for perspective camera + +**Kind**: static constant of [index](#module_index) + +| Param | Type | +| ----- | ------------------------------------------------------------------------------------------------------------------ | +| opts | [CameraOptions](#CameraOptions) \| [PerspectiveCameraOptions](#PerspectiveCameraOptions) | + + + +### index.orthographic ⇒ [OrthographicCamera](#OrthographicCamera) + +Factory function for orthographic camera + +**Kind**: static constant of [index](#module_index) + +| Param | Type | +| ----- | -------------------------------------------------------------------------------------------------------------------- | +| opts | [CameraOptions](#CameraOptions) \| [OrthographicCameraOptions](#OrthographicCameraOptions) | + + + +### index.orbiter ⇒ [OrbiterControls](#OrbiterControls) + +Factory function for orbiter controls + +**Kind**: static constant of [index](#module_index) + +| Param | Type | +| ----- | -------------------------------------------------------------- | +| opts | [OrbiterControlsOptions](#OrbiterControlsOptions) | + + + +## Camera + +An interface for cameras to extend + +**Kind**: global class + + +### camera.set(opts) + +Update the camera + +**Kind**: instance method of [Camera](#Camera) + +| Param | Type | +| ----- | -------------------------------------------- | +| opts | [CameraOptions](#CameraOptions) | + + + +## OrbiterControls + +Camera controls to orbit around a target + +**Kind**: global class + +- [OrbiterControls](#OrbiterControls) + - [new OrbiterControls(opts)](#new_OrbiterControls_new) + - [.set(opts)](#OrbiterControls+set) + - [.dispose()](#OrbiterControls+dispose) + + + +### new OrbiterControls(opts) + +Create an instance of OrbiterControls + +| Param | Type | +| ----- | -------------------------------------------------------------- | +| opts | [OrbiterControlsOptions](#OrbiterControlsOptions) | + + + +### orbiterControls.set(opts) + +Update the control + +**Kind**: instance method of [OrbiterControls](#OrbiterControls) + +| Param | Type | +| ----- | --------------------------- | +| opts | OrbiterOptions | + + + +### orbiterControls.dispose() + +Remove all event listeners + +**Kind**: instance method of [OrbiterControls](#OrbiterControls) + + +## OrthographicCamera ⇐ [Camera](#Camera) + +A class to create an orthographic camera + +**Kind**: global class +**Extends**: [Camera](#Camera) + +- [OrthographicCamera](#OrthographicCamera) ⇐ [Camera](#Camera) + - [new OrthographicCamera(opts)](#new_OrthographicCamera_new) + - [.set(opts)](#OrthographicCamera+set) + + + +### new OrthographicCamera(opts) + +Create an instance of PerspectiveCamera + +| Param | Type | +| ----- | -------------------------------------------------------------------------------------------------------------------- | +| opts | [CameraOptions](#CameraOptions) \| [OrthographicCameraOptions](#OrthographicCameraOptions) | + + + +### orthographicCamera.set(opts) + +Update the camera + +**Kind**: instance method of [OrthographicCamera](#OrthographicCamera) +**Overrides**: [set](#Camera+set) + +| Param | Type | +| ----- | -------------------------------------------------------------------------------------------------------------------- | +| opts | [CameraOptions](#CameraOptions) \| [OrthographicCameraOptions](#OrthographicCameraOptions) | + + + +## PerspectiveCamera ⇐ [Camera](#Camera) + +A class to create a perspective camera + +**Kind**: global class +**Extends**: [Camera](#Camera) + +- [PerspectiveCamera](#PerspectiveCamera) ⇐ [Camera](#Camera) + - [new PerspectiveCamera(opts)](#new_PerspectiveCamera_new) + - [.set(opts)](#PerspectiveCamera+set) + - [.getViewRay(x, y, windowWidth, windowHeight)](#PerspectiveCamera+getViewRay) ⇒ module:pex-geom~ray + - [.getWorldRay(x, y, windowWidth, windowHeight)](#PerspectiveCamera+getWorldRay) ⇒ module:pex-geom~ray + + + +### new PerspectiveCamera(opts) + +Create an instance of PerspectiveCamera + +| Param | Type | +| ----- | ------------------------------------------------------------------------------------------------------------------ | +| opts | [CameraOptions](#CameraOptions) \| [PerspectiveCameraOptions](#PerspectiveCameraOptions) | + + + +### perspectiveCamera.set(opts) + +Update the camera + +**Kind**: instance method of [PerspectiveCamera](#PerspectiveCamera) +**Overrides**: [set](#Camera+set) + +| Param | Type | +| ----- | ------------------------------------------------------------------------------------------------------------------ | +| opts | [CameraOptions](#CameraOptions) \| [PerspectiveCameraOptions](#PerspectiveCameraOptions) | + + + +### perspectiveCamera.getViewRay(x, y, windowWidth, windowHeight) ⇒ module:pex-geom~ray + +Create a picking ray in view (camera) coordinates + +**Kind**: instance method of [PerspectiveCamera](#PerspectiveCamera) + +| Param | Type | Description | +| ------------ | ------------------- | ----------- | +| x | number | mouse x | +| y | number | mouse y | +| windowWidth | number | | +| windowHeight | number | | + + + +### perspectiveCamera.getWorldRay(x, y, windowWidth, windowHeight) ⇒ module:pex-geom~ray + +Create a picking ray in world coordinates + +**Kind**: instance method of [PerspectiveCamera](#PerspectiveCamera) + +| Param | Type | +| ------------ | ------------------- | +| x | number | +| y | number | +| windowWidth | number | +| windowHeight | number | + + + +## Radians : number + +**Kind**: global typedef + + +## Degrees : number + +**Kind**: global typedef + + +## CameraView : Object + +**Kind**: global typedef +**Properties** + +| Name | Type | +| --------- | --------------------------------- | +| offset | module:pex-math~vec2 | +| size | module:pex-math~vec2 | +| totalSize | module:pex-math~vec2 | + + + +## CameraOptions : Object + +**Kind**: global typedef +**Properties** + +| Name | Type | Default | +| ------------------ | -------------------------------------- | -------------------------- | +| [projectionMatrix] | module:pex-math~mat4 | mat4.create() | +| [invViewMatrix] | module:pex-math~mat4 | mat4.create() | +| [viewMatrix] | module:pex-math~mat4 | mat4.create() | +| [position] | module:pex-math~vec3 | [0, 0, 3] | +| [target] | module:pex-math~vec3 | [0, 0, 0] | +| [up] | module:pex-math~vec3 | [0, 1, 0] | +| [aspect] | number | 1 | +| [near] | number | 0.1 | +| [far] | number | 100 | +| [view] | [CameraView](#CameraView) | | + + + +## PerspectiveCameraOptions : Object + +**Kind**: global typedef +**Properties** + +| Name | Type | Default | +| ----- | -------------------------------- | ------------------------ | +| [fov] | [Radians](#Radians) | Math.PI / 3 | + + + +## OrthographicCameraOptions : Object + +**Kind**: global typedef +**Properties** + +| Name | Type | Default | +| -------- | ------------------- | --------------- | +| [left] | number | -1 | +| [right] | number | 1 | +| [bottom] | number | -1 | +| [top] | number | 1 | +| [zoom] | number | 1 | -### `orbiter = createOrbiter(opts)` + -Creates new orbiter controller +## OrbiterControlsOptions : Object -- `opts`: object with one or more of the following options - - `camera`: PerspectiveCamera - camera to be controlled - - `element`: DOM Element - mouse events target, `window` - - `easing`: Number, amount of intertia, `0` - - `drag`: Boolean - enable drag rotation, `true` - - `zoom`: Boolean - enable mouse wheel zooming, `true` - - `pan`: Boolean - enable shift + drag panning, `true` - - `lat`: Number - latitude of the orbiter position, defaults to camera.position - - `lon`: Number - longitude of the orbiter position, defaults to camera.position +**Kind**: global typedef +**Properties** -### `orbiter.set(opts)` +| Name | Type | Default | +| -------------- | -------------------------------- | ---------------------- | +| camera | [Camera](#Camera) | | +| [element] | HTMLElement | document | +| [easing] | number | 0.1 | +| [zoom] | boolean | true | +| [pan] | boolean | true | +| [drag] | boolean | true | +| [minDistance] | number | 0.01 | +| [maxDistance] | number | Infinity | +| [minLat] | [Degrees](#Degrees) | -89.5 | +| [maxLat] | [Degrees](#Degrees) | 89.5 | +| [minLon] | number | -Infinity | +| [maxLon] | number | Infinity | +| [panSlowdown] | number | 4 | +| [zoomSlowdown] | number | 400 | +| [dragSlowdown] | number | 4 | +| [autoUpdate] | boolean | true | -- `opts`: see `createOrbiter` + ## License -MIT, see [LICENSE.md](http://github.com/vorg/geom-merge/blob/master/LICENSE.md) for details. +MIT. See [license file](https://github.com/pex-gl/pex-cam/blob/main/LICENSE.md). diff --git a/camera.js b/camera.js new file mode 100644 index 0000000..7aacfb0 --- /dev/null +++ b/camera.js @@ -0,0 +1,38 @@ +import { mat4 } from "pex-math"; + +/** + * An interface for cameras to extend + */ +class Camera { + // Static getter to get different mat for each instances + static get DEFAULT_OPTIONS() { + return { + projectionMatrix: mat4.create(), + invViewMatrix: mat4.create(), + viewMatrix: mat4.create(), + position: [0, 0, 3], + target: [0, 0, 0], + up: [0, 1, 0], + aspect: 1, + near: 0.1, + far: 100, + view: null, + }; + } + + /** + * Update the camera + * @param {import("./types.js").CameraOptions} opts + */ + set(opts) { + Object.assign(this, opts); + + if (opts.position || opts.target || opts.up) { + mat4.lookAt(this.viewMatrix, this.position, this.target, this.up); + mat4.set(this.invViewMatrix, this.viewMatrix); + mat4.invert(this.invViewMatrix); + } + } +} + +export default Camera; diff --git a/example/index.html b/example/index.html deleted file mode 100644 index 7683e1d..0000000 --- a/example/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - pex - - - - - - diff --git a/example/index.js b/example/index.js index 8dbfd31..57184d1 100644 --- a/example/index.js +++ b/example/index.js @@ -1,135 +1,230 @@ -'use strict' -const ctx = require('pex-context')() -const createCube = require('primitive-cube') -const glsl = require('glslify') -const createCamera = require('../perspective') -const createOrbiter = require('../orbiter') -const mat4 = require('pex-math/mat4') -const random = require('pex-random') - -const cube = createCube(0.2) - -const camera = createCamera({ - fov: Math.PI / 3, - aspect: window.innerWidth / window.innerHeight, - near: 0.1, - far: 100, - position: [3, 3, 3], - target: [0, 0, 0], - up: [0, 1, 0] -}) - -// const arcball = createArcball({ - // camera: camera, - // element: gl.canvas -// }) - -const orbiter = createOrbiter({ - camera: camera, - element: ctx.gl.canvas, - easing: 0.1 -}) +import { + perspective as createPerspectiveCamera, + orthographic as createOrthographicCamera, + orbiter as createOrbiter, +} from "../index.js"; + +import createContext from "pex-context"; +import { cube as createCube } from "primitive-geometry"; +import { mat4 } from "pex-math"; +import * as random from "pex-random"; +import createGUI from "pex-gui"; + +const canvas = document.createElement("canvas"); +document.querySelector("main").appendChild(canvas); + +const ctx = createContext({ canvas }); +const gui = createGUI(ctx); +const cube = createCube({ sx: 0.2 }); + +const State = { distance: 5, fov: Math.PI / 3 }; + +const perspectiveCamera = createPerspectiveCamera({ + position: [2, 2, 2], + target: [0, -0.5, 0], +}); + +const orthographicCamera = createOrthographicCamera({ + position: [2, 2, 2], + target: [0, -0.5, 0], +}); + +const perspectiveOrbiter = createOrbiter({ + camera: perspectiveCamera, +}); +const orthographicOrbiter = createOrbiter({ + camera: orthographicCamera, +}); const clearCmd = { pass: ctx.pass({ - clearColor: [0, 0, 0, 1], - clearDepth: 1 - }) -} + clearColor: [0.1, 0.1, 0.1, 1], + clearDepth: 1, + }), +}; + +random.seed("0"); +const offsets = Array.from({ length: 200 }, () => random.vec3()); const drawCubeCmd = { pipeline: ctx.pipeline({ - vert: glsl` - #ifdef GL_ES - #pragma glslify: transpose = require(glsl-transpose) - #endif - #pragma glslify: inverse = require(glsl-inverse) - - attribute vec3 aPosition; - attribute vec3 aNormal; - - uniform mat4 uProjectionMatrix; - uniform mat4 uViewMatrix; - uniform mat4 uModelMatrix; - uniform vec3 uPosition; - - varying vec3 vNormal; - - void main () { - mat4 modelViewMatrix = uViewMatrix * uModelMatrix; - mat3 normalMatrix = mat3(transpose(inverse(modelViewMatrix))); vNormal = normalMatrix * aNormal; - gl_Position = uProjectionMatrix * modelViewMatrix * vec4(aPosition + uPosition, 1.0); - } - `, - frag: ` - #ifdef GL_ES - precision highp float; - #endif - - varying vec3 vNormal; - - void main () { - gl_FragColor.rgb = vNormal * 0.5 + 0.5; - gl_FragColor.a = 1.0; - } - `, - depthTest: true + vert: /* glsl */ ` + attribute vec3 aPosition; + attribute vec3 aOffset; + attribute vec3 aNormal; + + uniform mat4 uProjectionMatrix; + uniform mat4 uViewMatrix; + uniform mat4 uModelMatrix; + + varying vec3 vNormal; + + mat4 transpose(mat4 m) { + return mat4(m[0][0], m[1][0], m[2][0], m[3][0], + m[0][1], m[1][1], m[2][1], m[3][1], + m[0][2], m[1][2], m[2][2], m[3][2], + m[0][3], m[1][3], m[2][3], m[3][3]); + } + + mat4 inverse(mat4 m) { + float + a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], + a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], + a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], + a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], + + b00 = a00 * a11 - a01 * a10, + b01 = a00 * a12 - a02 * a10, + b02 = a00 * a13 - a03 * a10, + b03 = a01 * a12 - a02 * a11, + b04 = a01 * a13 - a03 * a11, + b05 = a02 * a13 - a03 * a12, + b06 = a20 * a31 - a21 * a30, + b07 = a20 * a32 - a22 * a30, + b08 = a20 * a33 - a23 * a30, + b09 = a21 * a32 - a22 * a31, + b10 = a21 * a33 - a23 * a31, + b11 = a22 * a33 - a23 * a32, + + det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + + return mat4( + a11 * b11 - a12 * b10 + a13 * b09, + a02 * b10 - a01 * b11 - a03 * b09, + a31 * b05 - a32 * b04 + a33 * b03, + a22 * b04 - a21 * b05 - a23 * b03, + a12 * b08 - a10 * b11 - a13 * b07, + a00 * b11 - a02 * b08 + a03 * b07, + a32 * b02 - a30 * b05 - a33 * b01, + a20 * b05 - a22 * b02 + a23 * b01, + a10 * b10 - a11 * b08 + a13 * b06, + a01 * b08 - a00 * b10 - a03 * b06, + a30 * b04 - a31 * b02 + a33 * b00, + a21 * b02 - a20 * b04 - a23 * b00, + a11 * b07 - a10 * b09 - a12 * b06, + a00 * b09 - a01 * b07 + a02 * b06, + a31 * b01 - a30 * b03 - a32 * b00, + a20 * b03 - a21 * b01 + a22 * b00) / det; + } + + void main () { + mat4 modelViewMatrix = uViewMatrix * uModelMatrix; + mat3 normalMatrix = mat3(transpose(inverse(modelViewMatrix))); + vNormal = normalMatrix * aNormal; + gl_Position = uProjectionMatrix * modelViewMatrix * vec4(aPosition + aOffset, 1.0); + } + `, + frag: /* glsl */ ` + precision highp float; + + varying vec3 vNormal; + + void main () { + gl_FragColor.rgb = vNormal * 0.5 + 0.5; + gl_FragColor.a = 1.0; + } + `, + depthTest: true, }), - uniforms: { - uProjectionMatrix: camera.projectionMatrix, - uViewMatrix: camera.viewMatrix, - uModelMatrix: mat4.create(), - uPosition: [0, 0, 0] - }, attributes: { aPosition: ctx.vertexBuffer(cube.positions), - aNormal: ctx.vertexBuffer(cube.normals) + aNormal: ctx.vertexBuffer(cube.normals), + aOffset: { buffer: ctx.vertexBuffer(offsets), divisor: 1 }, }, - indices: ctx.indexBuffer(cube.cells) -} - -var instances = [] -for (var i = 0; i < 200; i++) { - instances.push({ - uniforms: { - uPosition: random.vec3() - } - }) -} - -window.addEventListener('resize', (e) => { - ctx.gl.canvas.width = window.innerWidth - ctx.gl.canvas.height = window.innerHeight - camera.set({ - aspect: ctx.gl.canvas.width / ctx.gl.canvas.height - }) -}) - -var zoom2 = document.createElement('a') -zoom2.innerText = 'Zoom to 2' -zoom2.style.position = 'absolute' -zoom2.style.top = '10px' -zoom2.style.left = '20px' -zoom2.style.color = 'white' -zoom2.setAttribute('href', '#') -zoom2.addEventListener('click', () => { - orbiter.set({ distance: 2 }) -}) -document.body.appendChild(zoom2) - -var zoom5 = document.createElement('a') -zoom5.innerText = 'Zoom to 5' -zoom5.style.position = 'absolute' -zoom5.style.top = '30px' -zoom5.style.left = '20px' -zoom5.style.color = 'white' -zoom5.setAttribute('href', '#') -zoom5.addEventListener('click', () => { - orbiter.set({ distance: 5 }) -}) -document.body.appendChild(zoom5) - + instances: offsets.length, + indices: ctx.indexBuffer(cube.cells), +}; + +const onResize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const aspect = canvas.width / canvas.height; + + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const viewWidth = viewportWidth * 0.5; + const viewSize = 5; + + const size = [viewportWidth, viewportHeight]; + const totalSize = [viewportWidth, viewportHeight]; + + perspectiveCamera.set({ + aspect, + view: { + offset: [viewWidth * 0.5, 0], + size, + totalSize, + }, + }); + + orthographicCamera.set({ + left: (-0.5 * viewSize * aspect) / 2, + right: (0.5 * viewSize * aspect) / 2, + top: (0.5 * viewSize) / 2, + bottom: (-0.5 * viewSize) / 2, + view: { + offset: [-viewWidth * 0.5, 0], + size, + totalSize, + }, + }); +}; +window.addEventListener("resize", onResize); +onResize(); + +// GUI +const addOrbiterGui = (orbiter) => { + gui.addParam("easing", orbiter, "easing", { min: 0, max: 1 }); + gui.addParam("zoom", orbiter, "zoom"); + gui.addParam("pan", orbiter, "pan"); + gui.addParam("drag", orbiter, "drag"); + gui.addParam("minDistance", orbiter, "minDistance", { min: 0, max: 10 }); + gui.addParam("maxDistance", orbiter, "maxDistance", { min: 10, max: 100 }); + gui.addParam("minLat", orbiter, "minLat", { min: -89.5, max: 10 }); + gui.addParam("maxLat", orbiter, "maxLat", { min: 10, max: 89.5 }); + gui.addParam("minLon", orbiter, "minLon", { min: -1000, max: 0 }); + gui.addParam("maxLon", orbiter, "maxLon", { min: 0, max: 1000 }); + gui.addParam("panSlowdown", orbiter, "panSlowdown", { min: 0, max: 10 }); + gui.addParam("zoomSlowdown", orbiter, "zoomSlowdown", { min: 0, max: 1000 }); + gui.addParam("dragSlowdown", orbiter, "dragSlowdown", { min: 0, max: 10 }); + gui.addParam("autoUpdate", orbiter, "autoUpdate", {}, (v) => + orbiter.set({ autoUpdate: v }) + ); +}; + +gui.addColumn("Perspective"); +addOrbiterGui(perspectiveOrbiter); +gui.addSeparator(); +gui.addParam("fov", State, "fov", { min: Math.PI / 8, max: Math.PI / 2 }, () => + perspectiveCamera.set({ fov: State.fov }) +); +gui.addColumn("Orthographic"); +addOrbiterGui(orthographicOrbiter); + +gui.addColumn("Shared"); +gui.addParam("Distance", State, "distance", { min: 2, max: 20 }, () => { + perspectiveOrbiter.set({ distance: State.distance }); + orthographicOrbiter.set({ distance: State.distance }); +}); + +// Frame ctx.frame(() => { - ctx.submit(clearCmd) - ctx.submit(drawCubeCmd, instances) -}) + ctx.submit(clearCmd); + ctx.submit(drawCubeCmd, { + uniforms: { + uProjectionMatrix: perspectiveCamera.projectionMatrix, + uViewMatrix: perspectiveCamera.viewMatrix, + uModelMatrix: mat4.create(), + }, + }); + ctx.submit(drawCubeCmd, { + uniforms: { + uProjectionMatrix: orthographicCamera.projectionMatrix, + uViewMatrix: orthographicCamera.viewMatrix, + uModelMatrix: mat4.create(), + }, + }); + + gui.draw(); +}); diff --git a/example/package-lock.json b/example/package-lock.json deleted file mode 100644 index 8461943..0000000 --- a/example/package-lock.json +++ /dev/null @@ -1,876 +0,0 @@ -{ - "name": "example", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "for-each": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", - "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", - "requires": { - "is-function": "~1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } - }, - "glsl-inverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glsl-inverse/-/glsl-inverse-1.0.0.tgz", - "integrity": "sha1-EsCx0GX1WERNHm/q95td34qRiuY=" - }, - "glsl-transpose": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glsl-transpose/-/glsl-transpose-1.0.0.tgz", - "integrity": "sha1-Y6RKJIJur7x4B9fGzR1ZHAGWrpA=" - }, - "glslify": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glslify/-/glslify-6.1.0.tgz", - "integrity": "sha1-zf/P0qZXFyISjT0TNWwTbebOl0I=", - "requires": { - "bl": "^1.0.0", - "concat-stream": "^1.5.2", - "duplexify": "^3.4.5", - "falafel": "^2.0.0", - "from2": "^2.3.0", - "glsl-resolve": "0.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glslify-bundle": "^5.0.0", - "glslify-deps": "^1.2.5", - "minimist": "^1.2.0", - "resolve": "^1.1.5", - "stack-trace": "0.0.9", - "static-eval": "^1.1.1", - "tape": "^4.6.0", - "through2": "^2.0.1", - "xtend": "^4.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz", - "integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==" - }, - "bl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", - "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", - "requires": { - "readable-stream": "^2.0.5" - } - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" - }, - "commander": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", - "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=" - }, - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "requires": { - "foreach": "^2.0.5", - "object-keys": "^1.0.8" - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "duplexify": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz", - "integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "es-abstract": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", - "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" - } - }, - "escodegen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz", - "integrity": "sha512-v0MYvNQ32bzwoG2OSFzWAkuahDQHK92JBN0pTAALJ4RIxEZe766QJPDR8Hqy7XNUy5K3fnVL76OqYAdc4TZEIw==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.5.6" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "falafel": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz", - "integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=", - "requires": { - "acorn": "^5.0.0", - "foreach": "^2.0.5", - "isarray": "0.0.1", - "object-keys": "^1.0.6" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "findup": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz", - "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", - "requires": { - "colors": "~0.6.0-1", - "commander": "~2.1.0" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "glsl-inject-defines": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", - "integrity": "sha1-3RqswsF/yyvT/DJBHGYz0Ne2D9Q=", - "requires": { - "glsl-token-inject-block": "^1.0.0", - "glsl-token-string": "^1.0.1", - "glsl-tokenizer": "^2.0.2" - } - }, - "glsl-resolve": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/glsl-resolve/-/glsl-resolve-0.0.1.tgz", - "integrity": "sha1-iUvvc5ENeSyBtRQxgANdCnivdtM=", - "requires": { - "resolve": "^0.6.1", - "xtend": "^2.1.2" - }, - "dependencies": { - "resolve": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", - "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=" - }, - "xtend": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", - "integrity": "sha1-7vax8ZjByN6vrYsXZaBNrUoBxak=" - } - } - }, - "glsl-token-assignments": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", - "integrity": "sha1-pdgqt4SZwuimuDy2lJXm5mXOAZ8=" - }, - "glsl-token-defines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", - "integrity": "sha1-y4kqqVmTYjFyhHDU90AySJaX+p0=", - "requires": { - "glsl-tokenizer": "^2.0.0" - } - }, - "glsl-token-depth": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", - "integrity": "sha1-I8XjDuK9JViEtKKLyFC495HpXYQ=" - }, - "glsl-token-descope": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", - "integrity": "sha1-D8kKsyYYa4L1l7LnfcniHvzTIHY=", - "requires": { - "glsl-token-assignments": "^2.0.0", - "glsl-token-depth": "^1.1.0", - "glsl-token-properties": "^1.0.0", - "glsl-token-scope": "^1.1.0" - } - }, - "glsl-token-inject-block": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", - "integrity": "sha1-4QFfWYDBCRgkraomJfHf3ovQADQ=" - }, - "glsl-token-properties": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", - "integrity": "sha1-SD3D2Dnw1LXGFx0VkfJJvlPCip4=" - }, - "glsl-token-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", - "integrity": "sha1-oXKOeN8kRE+cuT/RjvD3VQOmQ7E=" - }, - "glsl-token-string": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glsl-token-string/-/glsl-token-string-1.0.1.tgz", - "integrity": "sha1-WUQdL4V958NEnJRWZgIezjWOSOw=" - }, - "glsl-token-whitespace-trim": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", - "integrity": "sha1-RtHf6Yx1vX1QTAXX0RsbPpzJOxA=" - }, - "glsl-tokenizer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.2.tgz", - "integrity": "sha1-cgMHUi4DxXrzXABVGVDEpw7y37k=", - "requires": { - "through2": "^0.6.3" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - } - } - }, - "glslify-bundle": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-5.0.0.tgz", - "integrity": "sha1-AlKtoe+d8wtmAAbguyH9EwtIbkI=", - "requires": { - "glsl-inject-defines": "^1.0.1", - "glsl-token-defines": "^1.0.0", - "glsl-token-depth": "^1.1.1", - "glsl-token-descope": "^1.0.2", - "glsl-token-scope": "^1.1.1", - "glsl-token-string": "^1.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glsl-tokenizer": "^2.0.2", - "murmurhash-js": "^1.0.0", - "shallow-copy": "0.0.1" - } - }, - "glslify-deps": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.0.tgz", - "integrity": "sha1-CyI0yOqePT/X9rPLfwOuWea1Glk=", - "requires": { - "events": "^1.0.2", - "findup": "^0.1.5", - "glsl-resolve": "0.0.1", - "glsl-tokenizer": "^2.0.0", - "graceful-fs": "^4.1.2", - "inherits": "^2.0.1", - "map-limit": "0.0.1", - "resolve": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "requires": { - "function-bind": "^1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "^1.0.1" - } - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "map-limit": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", - "integrity": "sha1-63lhAxwPDo0AG/LVb6toXViCLzg=", - "requires": { - "once": "~1.3.0" - }, - "dependencies": { - "once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", - "requires": { - "wrappy": "1" - } - } - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "murmurhash-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", - "integrity": "sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=" - }, - "object-inspect": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.3.0.tgz", - "integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==" - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.0.3", - "util-deprecate": "~1.0.1" - } - }, - "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", - "requires": { - "path-parse": "^1.0.5" - } - }, - "resumer": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", - "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", - "requires": { - "through": "~2.3.4" - } - }, - "shallow-copy": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", - "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "optional": true - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "static-eval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-1.1.1.tgz", - "integrity": "sha1-yoEwIQNUzxPZpyK8fpI3eEV7sZI=", - "requires": { - "escodegen": "^1.8.1" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "string.prototype.trim": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", - "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.0", - "function-bind": "^1.0.2" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tape": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/tape/-/tape-4.8.0.tgz", - "integrity": "sha512-TWILfEnvO7I8mFe35d98F6T5fbLaEtbFTG/lxWvid8qDfFTxt19EBijWmB4j3+Hoh5TfHE2faWs73ua+EphuBA==", - "requires": { - "deep-equal": "~1.0.1", - "defined": "~1.0.0", - "for-each": "~0.3.2", - "function-bind": "~1.1.0", - "glob": "~7.1.2", - "has": "~1.0.1", - "inherits": "~2.0.3", - "minimist": "~1.2.0", - "object-inspect": "~1.3.0", - "resolve": "~1.4.0", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.1.2", - "through": "~2.3.8" - }, - "dependencies": { - "resolve": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", - "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", - "requires": { - "path-parse": "^1.0.5" - } - } - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - } - } - }, - "is-browser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-browser/-/is-browser-2.0.1.tgz", - "integrity": "sha1-i/C695mpxi/Z3lvO5M8zl8PnUpo=" - }, - "is-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", - "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" - }, - "is-plask": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-plask/-/is-plask-1.1.1.tgz", - "integrity": "sha1-3mOSZXYfKK939JtBI4tnp/R8sgg=", - "requires": { - "plask-wrap": "^1.0.1" - } - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pex-context": { - "version": "2.0.0-34", - "resolved": "https://registry.npmjs.org/pex-context/-/pex-context-2.0.0-34.tgz", - "integrity": "sha512-aiMZXn7d403iXSA+VWzsRVGTP3wp5uaSBtvI00hRkHaVvPgMjRKsXUE7PkNprIFz9OQXWC+Y/5LlJhhdOPitlQ==", - "requires": { - "debug": "^2.6.3", - "is-browser": "^2.0.1", - "is-plask": "^1.1.0", - "pex-gl": "^2.4.1", - "plask-wrap": "^1.0.0", - "raf": "^3.3.0", - "ramda": "^0.23.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "pex-gl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-2.4.1.tgz", - "integrity": "sha1-MqZNLqathNo31KvrcfvHq5Wn0W4=", - "requires": { - "is-plask": "^1.1.1" - } - } - } - }, - "pex-gl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-1.4.1.tgz", - "integrity": "sha1-m9Fbr1zzK3VOOf7547zkxMAjM1Y=", - "requires": { - "is-plask": "^1.1.1" - } - }, - "pex-math": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pex-math/-/pex-math-2.0.0.tgz", - "integrity": "sha512-6Ca4OSIzCg0saX2iV6jAQpZoMrmgjWw+AAOKfjDJ3NDdsK7J7c1x2hpx6vJdGWDaSkeCCzEAX6cxGSoCdnQTPg==" - }, - "pex-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pex-random/-/pex-random-1.0.1.tgz", - "integrity": "sha1-Cwp9kOLgV9V3d+2ROjm3IlUxm+U=", - "requires": { - "seedrandom": "^2.3.10", - "simplex-noise": "2.1.1" - } - }, - "plask-wrap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plask-wrap/-/plask-wrap-1.0.1.tgz", - "integrity": "sha1-xfXBtdh0wcfZtxf3chJvJauzHd8=" - }, - "primitive-cube": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/primitive-cube/-/primitive-cube-2.0.1.tgz", - "integrity": "sha1-iqW8PL/y7+6DzOdM/KuKybW0awY=" - }, - "raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", - "requires": { - "performance-now": "^2.1.0" - } - }, - "ramda": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.23.0.tgz", - "integrity": "sha1-zNE//3NJepOXTj6GMnv9h71ujis=" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "seedrandom": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" - }, - "simplex-noise": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-2.1.1.tgz", - "integrity": "sha1-kuZ4XxaptanxMdHplsM3q/X0KF0=" - } - } -} diff --git a/example/package.json b/example/package.json deleted file mode 100644 index a79f664..0000000 --- a/example/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "example", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "budo index.js:bundle.js --open --live -- -t glslify" - }, - "author": "Marcin Ignac (http://marcinignac.com/)", - "license": "MIT", - "dependencies": { - "glsl-inverse": "^1.0.0", - "glsl-transpose": "^1.0.0", - "glslify": "^6.0.1", - "pex-context": "^2.0.0-34", - "pex-gl": "^1.3.0", - "pex-math": "^2.0.0", - "pex-random": "^1.0.1", - "primitive-cube": "^2.0.0" - }, - "devDependencies": { - "budo": "^11.3.2" - } -} diff --git a/index.html b/index.html new file mode 100644 index 0000000..cb838af --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ + + + + + + + pex-cam by pex-gl (https://github.com/pex-gl) + + + +
+ + + + + + diff --git a/index.js b/index.js old mode 100644 new mode 100755 index 92f031f..55ca9bc --- a/index.js +++ b/index.js @@ -1,4 +1,29 @@ -module.exports = { - orbiter: require('./orbiter'), - perspective: require('./perspective') -} +/** + * Re-export classes and factory functions + * @module index + */ + +import PerspectiveCamera from "./perspective.js"; +import OrthographicCamera from "./orthographic.js"; +import OrbiterControls from "./orbiter.js"; + +/** + * Factory function for perspective camera + * @param {import("./types.js").CameraOptions & import("./types.js").PerspectiveCameraOptions} opts + * @returns {PerspectiveCamera} + */ +export const perspective = (opts) => new PerspectiveCamera(opts); + +/** + * Factory function for orthographic camera + * @param {import("./types.js").CameraOptions & import("./types.js").OrthographicCameraOptions} opts + * @returns {OrthographicCamera} + */ +export const orthographic = (opts) => new OrthographicCamera(opts); + +/** + * Factory function for orbiter controls + * @param {import("./types.js").OrbiterControlsOptions} opts + * @returns {OrbiterControls} + */ +export const orbiter = (opts) => new OrbiterControls(opts); diff --git a/orbiter.js b/orbiter.js index aafb3aa..24b378e 100644 --- a/orbiter.js +++ b/orbiter.js @@ -1,289 +1,362 @@ -'use strict' -const vec3 = require('pex-math/vec3') -const mat4 = require('pex-math/mat4') -const ray = require('pex-geom/ray') -const clamp = require('pex-math/utils').clamp -const raf = require('raf') -const interpolateAngle = require('interpolate-angle') -const lerp = require('pex-math/utils').lerp -const toRadians = require('pex-math/utils').toRadians -const toDegrees = require('pex-math/utils').toDegrees -const latLonToXyz = require('latlon-to-xyz') -const xyzToLatLon = require('xyz-to-latlon') -const eventOffset = require('mouse-event-offset') - -function offset (e, target) { - if (e.touches) return eventOffset(e.touches[0], target) - else return eventOffset(e, target) -} +import { vec2, vec3, utils } from "pex-math"; +import { ray } from "pex-geom"; + +import interpolateAngle from "interpolate-angle"; +import latLonToXyz from "latlon-to-xyz"; +import xyzToLatLon from "xyz-to-latlon"; +import eventOffset from "mouse-event-offset"; + +/** + * Camera controls to orbit around a target + */ +class OrbiterControls { + static get DEFAULT_OPTIONS() { + return { + element: document, + easing: 0.1, + + zoom: true, + pan: true, + drag: true, + + minDistance: 0.01, + maxDistance: Infinity, + minLat: -89.5, + maxLat: 89.5, + minLon: -Infinity, + maxLon: Infinity, + panSlowdown: 4, + zoomSlowdown: 400, + dragSlowdown: 4, -function Orbiter (opts) { - // TODO: split into internal state and public state - const initialState = { - camera: opts.camera, - invViewMatrix: mat4.create(), - dragging: false, - lat: 0, // Y - minLat: -89.5, - maxLat: 89.5, - lon: 0, // XZ - minLon: -Infinity, - maxLon: Infinity, - currentLat: 0, - currentLon: 0, - easing: 1, - element: opts.element || window, - width: 0, - height: 0, - clickPosWindow: [0, 0], - dragPos: [0, 0, 0], - dragPosWindow: [0, 0], - distance: 1, - currentDistance: 1, - minDistance: 1, - maxDistance: 1, - zoomSlowdown: 400, - zoom: true, - pan: true, - drag: true, - dragSlowdown: 4, - clickTarget: [0, 0, 0], - clickPosPlane: [0, 0, 0], - dragPosPlane: [0, 0, 0], - clickPosWorld: [0, 0, 0], - dragPosWorld: [0, 0, 0], - panPlane: null, - autoUpdate: true + autoUpdate: true, + }; } - this.set(initialState) - this.set(opts) - this.setup() -} + get domElement() { + return this.element === document ? this.element.body : this.element; + } + + /** + * Create an instance of OrbiterControls + * @param {import("./types.js").OrbiterControlsOptions} opts + */ + constructor(opts) { + // Internals + // Set initially by .set + this.lat = null; // Y + this.lon = null; // XZ + this.currentLat = null; + this.currentLon = null; + this.distance = null; + this.currentDistance = null; + + // Updated by user interaction + this.panning = false; + this.dragging = false; + this.zooming = false; + this.width = 0; + this.height = 0; -Orbiter.prototype.set = function (opts) { - if (opts.camera) { - const distance = vec3.distance(opts.camera.position, opts.camera.target) - const latLon = xyzToLatLon(vec3.normalize(vec3.sub(vec3.copy(opts.camera.position), opts.camera.target))) - this.lat = latLon[0] - this.lon = latLon[1] - this.currentLat = this.lat - this.currentLon = this.lon - this.distance = distance - this.currentDistance = this.distance - this.minDistance = opts.minDistance || distance / 10 - this.maxDistance = opts.maxDistance || distance * 10 + this.zoomTouchDistance = null; + + this.panPlane = null; + this.clickTarget = [0, 0, 0]; + this.clickPosWorld = [0, 0, 0]; + this.clickPosPlane = [0, 0, 0]; + this.dragPos = [0, 0, 0]; + this.dragPosWorld = [0, 0, 0]; + this.dragPosPlane = [0, 0, 0]; + + // TODO: add ability to set lat/lng instead of position/target + this.set({ + ...OrbiterControls.DEFAULT_OPTIONS, + ...opts, + }); + this.setup(); } - Object.assign(this, opts) -} + /** + * Update the control + * @param {import("./types.js").OrbiterOptions} opts + */ + set(opts) { + Object.assign(this, opts); -Orbiter.prototype.updateWindowSize = function () { - const width = this.element.clientWidth || this.element.innerWidth - const height = this.element.clientHeight || this.element.innerHeight - if (width !== this.width) { - this.width = width - this.height = height - this.radius = Math.min(this.width / 2, this.height / 2) + if (opts.camera) { + const latLon = xyzToLatLon( + vec3.normalize( + vec3.sub(vec3.copy(opts.camera.position), opts.camera.target) + ) + ); + const distance = + opts.distance || + vec3.distance(opts.camera.position, opts.camera.target); + + this.lat = latLon[0]; + this.lon = latLon[1]; + this.currentLat = this.lat; + this.currentLon = this.lon; + this.distance = distance; + this.currentDistance = this.distance; + } + + if (Object.getOwnPropertyDescriptor(opts, "autoUpdate")) { + if (this.autoUpdate) { + const self = this; + this.rafHandle = requestAnimationFrame(function tick() { + self.updateCamera(); + if (self.autoUpdate) self.rafHandle = requestAnimationFrame(tick); + }); + } else if (this.rafHandle) { + cancelAnimationFrame(this.rafHandle); + } + } } -} -Orbiter.prototype.updateCamera = function () { - // instad of rotating the object we want to move camera around it - // state.currRot[3] *= -1 - if (!this.camera) return + updateCamera() { + // instad of rotating the object we want to move camera around it + if (!this.camera) return; - const position = this.camera.position - const target = this.camera.target + const position = this.camera.position; + const target = this.camera.target; - this.lat = clamp(this.lat, this.minLat, this.maxLat) - this.lon = clamp(this.lon, this.minLon, this.maxLon) % 360 + this.lat = utils.clamp(this.lat, this.minLat, this.maxLat); - this.currentLat = toDegrees( - interpolateAngle( - (toRadians(this.currentLat) + 2 * Math.PI) % (2 * Math.PI), - (toRadians(this.lat) + 2 * Math.PI) % (2 * Math.PI), - this.easing - ) - ) - this.currentLon = toDegrees( - interpolateAngle( - (toRadians(this.currentLon) + 2 * Math.PI) % (2 * Math.PI), - (toRadians(this.lon) + 2 * Math.PI) % (2 * Math.PI), - this.easing - ) - ) - this.currentDistance = lerp(this.currentDistance, this.distance, this.easing) - - // set new camera position according to the current - // rotation at distance relative to target - latLonToXyz(this.currentLat, this.currentLon, position) - vec3.scale(position, this.currentDistance) - vec3.add(position, target) - - this.camera.set({ - position: position - }) -} + if (this.minLon !== -Infinity && this.maxLon !== Infinity) { + this.lon = utils.clamp(this.lon, this.minLon, this.maxLon) % 360; + } -Orbiter.prototype.setup = function () { - var orbiter = this - - function down (x, y, shift) { - orbiter.dragging = true - orbiter.dragPos[0] = x - orbiter.dragPos[1] = y - if (shift && orbiter.pan) { - orbiter.clickPosWindow[0] = x - orbiter.clickPosWindow[1] = y - vec3.set(orbiter.clickTarget, orbiter.camera.target) - const targetInViewSpace = vec3.multMat4(vec3.copy(orbiter.clickTarget), orbiter.camera.viewMatrix) - orbiter.panPlane = [targetInViewSpace, [0, 0, 1]] - ray.hitTestPlane( - orbiter.camera.getViewRay(orbiter.clickPosWindow[0], orbiter.clickPosWindow[1], orbiter.width, orbiter.height), - orbiter.panPlane[0], - orbiter.panPlane[1], - orbiter.clickPosPlane + this.currentLat = utils.toDegrees( + interpolateAngle( + (utils.toRadians(this.currentLat) + 2 * Math.PI) % (2 * Math.PI), + (utils.toRadians(this.lat) + 2 * Math.PI) % (2 * Math.PI), + this.easing ) - ray.hitTestPlane( - orbiter.camera.getViewRay(orbiter.dragPosWindow[0], orbiter.dragPosWindow[1], orbiter.width, orbiter.height), - orbiter.panPlane[0], - orbiter.panPlane[1], - orbiter.dragPosPlane - ) - } else { - orbiter.panPlane = null - } + ); + + this.currentLon += (this.lon - this.currentLon) * this.easing; + + this.currentDistance = utils.lerp( + this.currentDistance, + this.distance, + this.easing + ); + + // Set position from lat/lon + latLonToXyz(this.currentLat, this.currentLon, position); + + // Move position according to distance and target + vec3.scale(position, this.currentDistance); + vec3.add(position, target); + + if (this.camera.zoom) this.camera.set({ zoom: vec3.length(position) }); + this.camera.set({ position }); + } + + updateWindowSize() { + const width = this.domElement.clientWidth || this.domElement.innerWidth; + const height = this.domElement.clientHeight || this.domElement.innerHeight; + + if (width !== this.width) this.width = width; + if (height !== this.height) this.height = height; + } + + handleDragStart(position) { + this.dragging = true; + this.dragPos = position; } - function move (x, y, shift) { - if (!orbiter.dragging) { - return + handlePanZoomStart(touch0, touch1) { + this.dragging = false; + + if (this.zoom && touch1) { + this.zooming = true; + this.zoomTouchDistance = vec2.distance(touch1, touch0); } - if (shift && orbiter.panPlane) { - orbiter.dragPosWindow[0] = x - orbiter.dragPosWindow[1] = y - ray.hitTestPlane( - orbiter.camera.getViewRay(orbiter.clickPosWindow[0], orbiter.clickPosWindow[1], orbiter.width, orbiter.height), - orbiter.panPlane[0], - orbiter.panPlane[1], - orbiter.clickPosPlane - ) + + const camera = this.camera; + + if (this.pan && camera) { + this.panning = true; + this.updateWindowSize(); + + // TODO: use dragPos? + const clickPosWindow = touch1 + ? [(touch0[0] + touch1[0]) * 0.5, (touch0[1] + touch1[1]) * 0.5] + : touch0; + + vec3.set(this.clickTarget, camera.target); + const targetInViewSpace = vec3.multMat4( + vec3.copy(this.clickTarget), + camera.viewMatrix + ); + this.panPlane = [targetInViewSpace, [0, 0, 1]]; + ray.hitTestPlane( - orbiter.camera.getViewRay(orbiter.dragPosWindow[0], orbiter.dragPosWindow[1], orbiter.width, orbiter.height), - orbiter.panPlane[0], - orbiter.panPlane[1], - orbiter.dragPosPlane - ) - mat4.set(orbiter.invViewMatrix, orbiter.camera.viewMatrix) - mat4.invert(orbiter.invViewMatrix) - vec3.multMat4(vec3.set(orbiter.clickPosWorld, orbiter.clickPosPlane), orbiter.invViewMatrix) - vec3.multMat4(vec3.set(orbiter.dragPosWorld, orbiter.dragPosPlane), orbiter.invViewMatrix) - const diffWorld = vec3.sub(vec3.copy(orbiter.dragPosWorld), orbiter.clickPosWorld) - const target = vec3.sub(vec3.copy(orbiter.clickTarget), diffWorld) - orbiter.camera.set({ target: target }) - orbiter.updateCamera() - } else if (orbiter.drag) { - const dx = x - orbiter.dragPos[0] - const dy = y - orbiter.dragPos[1] - orbiter.dragPos[0] = x - orbiter.dragPos[1] = y - - orbiter.lat += dy / orbiter.dragSlowdown - orbiter.lon -= dx / orbiter.dragSlowdown - - // TODO: how to have resolution independed scaling? will this code behave differently with retina/pixelRatio=2? - orbiter.updateCamera() + camera.getViewRay( + clickPosWindow[0], + clickPosWindow[1], + this.width, + this.height + ), + this.panPlane, + this.clickPosPlane + ); } } - function up () { - orbiter.dragging = false - orbiter.panPlane = null + handleDragMove(position) { + const dx = position[0] - this.dragPos[0]; + const dy = position[1] - this.dragPos[1]; + + this.lat += dy / this.dragSlowdown; + this.lon -= dx / this.dragSlowdown; + + this.dragPos = position; } - function scroll (dy) { - if (!orbiter.zoom) { - return false + handlePanZoomMove(touch0, touch1) { + if (this.zoom && touch1) { + const distance = vec2.distance(touch1, touch0); + this.handleZoom(this.zoomTouchDistance - distance); + this.zoomTouchDistance = distance; } - orbiter.distance *= 1 + dy / orbiter.zoomSlowdown - orbiter.distance = clamp(orbiter.distance, orbiter.minDistance, orbiter.maxDistance) - orbiter.updateCamera() - return true - } - function onMouseDown (e) { - orbiter.updateWindowSize() - const pos = offset(e, orbiter.element) - down( - pos[0], - pos[1], - e.shiftKey || (e.touches && e.touches.length === 2) - ) - } + const camera = this.camera; - function onMouseMove (e) { - const pos = offset(e, orbiter.element) - move( - pos[0], - pos[1], - e.shiftKey || (e.touches && e.touches.length === 2) - ) - } + if (this.pan && camera && this.panPlane) { + const dragPosWindow = touch1 + ? [(touch0[0] + touch1[0]) * 0.5, (touch0[1] + touch1[1]) * 0.5] + : touch0; - function onMouseUp (e) { - up() - } + ray.hitTestPlane( + camera.getViewRay( + dragPosWindow[0], + dragPosWindow[1], + this.width, + this.height + ), + this.panPlane, + this.dragPosPlane + ); - function onWheel (e) { - if (scroll(e.deltaY) === true) { - e.preventDefault() - return false + vec3.multMat4( + vec3.set(this.clickPosWorld, this.clickPosPlane), + camera.invViewMatrix + ); + vec3.multMat4( + vec3.set(this.dragPosWorld, this.dragPosPlane), + camera.invViewMatrix + ); + + const diffWorld = vec3.sub( + vec3.copy(this.dragPosWorld), + this.clickPosWorld + ); + camera.set({ + distance: this.distance, + target: vec3.sub(vec3.copy(this.clickTarget), diffWorld), + }); } } - function onTouchStart (e) { - e.preventDefault() - onMouseDown(e) + handleZoom(dy) { + this.distance *= 1 + dy / this.zoomSlowdown; + this.distance = utils.clamp( + this.distance, + this.minDistance, + this.maxDistance + ); } - this._onMouseDown = onMouseDown - this._onTouchStart = onTouchStart - this._onMouseMove = onMouseMove - this._onMouseUp = onMouseUp - this._onWheel = onWheel - - this.element.addEventListener('mousedown', onMouseDown) - this.element.addEventListener('touchstart', onTouchStart) - this.element.addEventListener('wheel', onWheel) - window.addEventListener('mousemove', onMouseMove) - window.addEventListener('touchmove', onMouseMove, { passive: false }) - window.addEventListener('mouseup', onMouseUp) - window.addEventListener('touchend', onMouseUp) - - this.updateCamera() - - if (this.autoUpdate) { - const self = this - this._rafHandle = raf(function tick () { - orbiter.updateCamera() - self._rafHandle = raf(tick) - }) + handleEnd() { + this.dragging = false; + this.panning = false; + this.zooming = false; + this.panPlane = null; } -} -Orbiter.prototype.dispose = function () { - this.element.removeEventListener('mousedown', this._onMouseDown) - this.element.removeEventListener('touchstart', this._onTouchStart) - this.element.removeEventListener('wheel', this._onWheel) - window.removeEventListener('mousemove', this._onMouseMove) - window.removeEventListener('touchmove', this._onMouseMove) - window.removeEventListener('mouseup', this._onMouseUp) - window.removeEventListener('touchend', this._onMouseUp) - raf.cancel(this._rafHandle) - this.camera = null -} + setup() { + this.onPointerDown = (event) => { + const pan = + event.ctrlKey || + event.metaKey || + event.shiftKey || + (event.touches && event.touches.length === 2); + + const touch0 = eventOffset( + event.touches ? event.touches[0] : event, + this.domElement + ); + if (this.drag && !pan) { + this.handleDragStart(touch0); + } else if ((this.pan || this.zoom) && pan) { + const touch1 = + event.touches && eventOffset(event.touches[1], this.domElement); + this.handlePanZoomStart(touch0, touch1); + } + }; + + this.onPointerMove = (event) => { + const touch0 = eventOffset( + event.touches ? event.touches[0] : event, + this.domElement + ); + if (this.dragging) { + this.handleDragMove(touch0); + } else if (this.panning || this.zooming) { + if (event.touches && !event.touches[1]) return; + const touch1 = + event.touches && eventOffset(event.touches[1], this.domElement); + this.handlePanZoomMove(touch0, touch1); + } + }; + + this.onPointerUp = () => { + this.handleEnd(); + }; + + this.onTouchStart = (event) => { + event.preventDefault(); + + if (event.touches.length <= 2) this.onPointerDown(event); + }; + + this.onTouchMove = (event) => { + !!event.cancelable && event.preventDefault(); + + if (event.touches.length <= 2) this.onPointerMove(event); + }; + + this.onWheel = (event) => { + if (!this.zoom) return; -module.exports = function createOrbiter (opts) { - return new Orbiter(opts) + event.preventDefault(); + this.handleZoom(event.deltaY); + }; + + this.element.addEventListener("pointerdown", this.onPointerDown); + this.element.addEventListener("wheel", this.onWheel, { passive: false }); + + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + + this.domElement.style.touchAction = "none"; + } + + /** + * Remove all event listeners + */ + dispose() { + if (this.rafHandle) cancelAnimationFrame(this.rafHandle); + + this.element.removeEventListener("pointerdown", this.onPointerDown); + this.element.removeEventListener("wheel", this.onWheel); + + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } } + +export default OrbiterControls; diff --git a/orbiter.test.js b/orbiter.test.js deleted file mode 100644 index b0537e1..0000000 --- a/orbiter.test.js +++ /dev/null @@ -1,21 +0,0 @@ -const latLonToXyz = require('./orbiter').latLonToXyz -const xyzToLatLon = require('./orbiter').xyzToLatLon -const Vec3 = require('pex-math/Vec3') - -function toFixed2 (v) { - return +v.toFixed(2) -} - -console.log('0, 0', latLonToXyz(0, 0).map(toFixed2), 'expecting', [0, 0, 1]) -console.log('0, 90', latLonToXyz(0, 90 / 180 * Math.PI).map(toFixed2), 'expecting', [1, 0, 0]) -console.log('0, -90', latLonToXyz(0, -90 / 180 * Math.PI).map(toFixed2), 'expecting', [-1, 0, 0]) -console.log('90, 0', latLonToXyz(90 / 180 * Math.PI, 0).map(toFixed2), 'expecting', [0, 1, 0]) -console.log('45, 90', latLonToXyz(45 / 180 * Math.PI, 90 / 180 * Math.PI).map(toFixed2), 'expecting', Vec3.normalize([1, 1, 0])) - -const pos = Vec3.normalize([5, 5, 0]) -const latLon = xyzToLatLon(pos) -const pos2 = latLonToXyz(latLon[0], latLon[1]) -const latLon2 = xyzToLatLon(pos2) -console.log(' ') -console.log('pos', pos.map(toFixed2), '->', pos2.map(toFixed2)) -console.log('latLon', latLon.map(toFixed2), '->', latLon2.map(toFixed2)) diff --git a/orthographic.js b/orthographic.js new file mode 100644 index 0000000..2024546 --- /dev/null +++ b/orthographic.js @@ -0,0 +1,107 @@ +import { mat4, vec3 } from "pex-math"; + +import Camera from "./camera.js"; + +/** + * A class to create an orthographic camera + * @extends Camera + */ +class OrthographicCamera extends Camera { + static get DEFAULT_OPTIONS() { + return { + left: -1, + right: 1, + bottom: -1, + top: 1, + zoom: 1, + }; + } + + /** + * Create an instance of PerspectiveCamera + * @param {import("./types.js").CameraOptions & import("./types.js").OrthographicCameraOptions} opts + */ + constructor(opts = {}) { + super(); + + this.set({ + ...Camera.DEFAULT_OPTIONS, + ...OrthographicCamera.DEFAULT_OPTIONS, + ...opts, + }); + } + + /** + * Update the camera + * @param {import("./types.js").CameraOptions & import("./types.js").OrthographicCameraOptions} opts + */ + set(opts) { + super.set(opts); + + if ( + opts.left || + opts.right || + opts.bottom || + opts.top || + opts.zoom || + opts.near || + opts.far || + opts.view + ) { + const dx = (this.right - this.left) / (2 / this.zoom); + const dy = (this.top - this.bottom) / (2 / this.zoom); + const cx = (this.right + this.left) / 2; + const cy = (this.top + this.bottom) / 2; + + let left = cx - dx; + let right = cx + dx; + let top = cy + dy; + let bottom = cy - dy; + + if (this.view) { + const zoomW = + 1 / this.zoom / (this.view.size[0] / this.view.totalSize[0]); + const zoomH = + 1 / this.zoom / (this.view.size[1] / this.view.totalSize[1]); + const scaleW = (this.right - this.left) / this.view.size[0]; + const scaleH = (this.top - this.bottom) / this.view.size[1]; + + left += scaleW * (this.view.offset[0] / zoomW); + right = left + scaleW * (this.view.size[0] / zoomW); + top -= scaleH * (this.view.offset[1] / zoomH); + bottom = top - scaleH * (this.view.size[1] / zoomH); + } + + mat4.ortho( + this.projectionMatrix, + left, + right, + bottom, + top, + this.near, + this.far + ); + } + } + + getViewRay(x, y, windowWidth, windowHeight) { + if (this.view) { + x += this.view.offset[0]; + y += this.view.offset[1]; + windowWidth = this.view.totalSize[0]; + windowHeight = this.view.totalSize[1]; + } + + // [origin, direction] + return [ + [0, 0, 0], + vec3.normalize([ + (x * (this.right - this.left)) / this.zoom / windowWidth, + ((1 - y) * (this.top - this.bottom)) / this.zoom / windowHeight, + -this.near, + ]), + ]; + } +} + +export default OrthographicCamera; diff --git a/package-lock.json b/package-lock.json index 2004a80..68805ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,306 +1,269 @@ { "name": "pex-cam", - "version": "2.8.0", - "lockfileVersion": 1, + "version": "3.0.0-alpha.1", + "lockfileVersion": 2, "requires": true, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" + "packages": { + "": { + "name": "pex-cam", + "version": "3.0.0-alpha.1", + "license": "MIT", + "dependencies": { + "interpolate-angle": "^1.0.2", + "latlon-to-xyz": "^1.0.1", + "mouse-event-offset": "^3.0.2", + "pex-geom": "^3.0.0-alpha.0", + "pex-math": "^4.0.0-alpha.1", + "xyz-to-latlon": "^1.0.2" + }, + "devDependencies": { + "es-module-shims": "^1.5.8", + "pex-context": "github:pex-gl/pex-context#v3", + "pex-gui": "github:pex-gl/pex-gui#v3", + "pex-random": "github:pex-gl/pex-random#v2", + "primitive-geometry": "^2.7.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + "node_modules/es-module-shims": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.5.8.tgz", + "integrity": "sha512-HHRl0wLqpMRHKdeJAIj7rkqP72sN9QcLTNvTvtsYGs1Nt98PJ0tIPKW5ZfpCWBFawb7MBY0yIRsBeCxlwSHqeQ==", + "dev": true }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" + "node_modules/interpolate-angle": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "lerp": "^1.0.3" } }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + "node_modules/latlon-to-xyz": { + "version": "1.0.1", + "license": "MIT" }, - "es-abstract": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", - "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", - "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.1", - "is-callable": "1.1.3", - "is-regex": "1.0.4" - } + "node_modules/lerp": { + "version": "1.0.3", + "license": "MIT" }, - "es-to-primitive": { + "node_modules/mouse-event-offset": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/pex-color": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" + "resolved": "git+ssh://git@github.com/pex-gl/pex-color.git#942007b3ce164e23ad85bd59d6a04d046321445b", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, - "for-each": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", - "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", - "requires": { - "is-function": "1.0.1" + "node_modules/pex-context": { + "version": "2.11.0-2", + "resolved": "git+ssh://git@github.com/pex-gl/pex-context.git#e9b88af96bc5aca9be5cce1103aa7a45ae73bdfe", + "dev": true, + "license": "MIT", + "dependencies": { + "pex-gl": "^3.0.0-alpha.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "node_modules/pex-geom": { + "version": "3.0.0-alpha.0", + "resolved": "git+ssh://git@github.com/pex-gl/pex-geom.git#4523420f126284939e1ad42eefc01a24e62f463d", + "license": "MIT", + "dependencies": { + "pex-math": "4.0.0-alpha.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "node_modules/pex-gl": { + "version": "3.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-3.0.0-alpha.0.tgz", + "integrity": "sha512-F5xRhI2A3iVhcSmBIIsjEoAeLBa8tqTUzQy2cwZ9X5i1yKRXH8mfQd21JegAuY2aIgzHylquQmWXKzskR8reIA==", + "dev": true, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "requires": { - "function-bind": "1.1.1" + "node_modules/pex-gui": { + "version": "2.4.0", + "resolved": "git+ssh://git@github.com/pex-gl/pex-gui.git#e64c6d12d1742be7a88885559b96c6c41438b55f", + "dev": true, + "license": "MIT", + "dependencies": { + "pex-color": "github:pex-gl/pex-color#v2", + "pex-geom": "github:pex-gl/pex-geom#v3", + "pex-math": "github:pex-gl/pex-math#v4" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "node_modules/pex-math": { + "version": "4.0.0-alpha.1", + "resolved": "git+ssh://git@github.com/pex-gl/pex-math.git#a4f95e0eef7071a62bae5f1bd2a494b5a3746684", + "license": "MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "node_modules/pex-random": { + "version": "1.0.1", + "resolved": "git+ssh://git@github.com/pex-gl/pex-random.git#e43efec8c740fb64f7349f4006417c3ec1a82ddb", + "dev": true, + "license": "MIT", + "dependencies": { + "seedrandom": "^3.0.5", + "simplex-noise": "3.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } }, - "interpolate-angle": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/interpolate-angle/-/interpolate-angle-1.0.2.tgz", - "integrity": "sha1-q6mSFyJy4ucJOMlTC0HcNcV+5do=", - "requires": { - "lerp": "1.0.3" + "node_modules/primitive-geometry": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/primitive-geometry/-/primitive-geometry-2.7.0.tgz", + "integrity": "sha512-WlZ2FiNpGSytzqVGkcQu5cixyggOwdE6L77TU53h80hq0A/fGSINHmSAVM01y0TnMWUd7LYs9Bovbdi45XeJzA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paypal.me/dmnsgn" + }, + { + "type": "individual", + "url": "https://commerce.coinbase.com/checkout/56cbdf28-e323-48d8-9c98-7019e72c97f3" + } + ], + "engines": { + "node": ">=15.0.0", + "npm": ">=7.0.0" } }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "node_modules/simplex-noise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-3.0.1.tgz", + "integrity": "sha512-eww0SFiWLyOaUKQMJ7gbdvQJvULeJdM/Y4BiC3rrOQnYHo+MSPh465/qeXSZkpTdB9/HthumpnYD3DobZweBBQ==", + "dev": true }, - "is-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", - "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" + "node_modules/xyz-to-latlon": { + "version": "1.0.2", + "license": "MIT" + } + }, + "dependencies": { + "es-module-shims": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.5.8.tgz", + "integrity": "sha512-HHRl0wLqpMRHKdeJAIj7rkqP72sN9QcLTNvTvtsYGs1Nt98PJ0tIPKW5ZfpCWBFawb7MBY0yIRsBeCxlwSHqeQ==", + "dev": true }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "interpolate-angle": { + "version": "1.0.2", "requires": { - "has": "1.0.1" + "lerp": "^1.0.3" } }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" - }, "latlon-to-xyz": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/latlon-to-xyz/-/latlon-to-xyz-1.0.1.tgz", - "integrity": "sha512-OF65aE60Y9aMOibxWFC2Vl7EN2BOtMKabm0t97+VaVgR9LoStZHbNNMc8dS/LG6iE1BpEXmnoH85wWr1drp/Tw==" + "version": "1.0.1" }, "lerp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz", - "integrity": "sha1-oYyJaPkXiW3hXM/MKNVaa3Med24=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.0.3" }, "mouse-event-offset": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", - "integrity": "sha1-39hqbiSMa6jK1TuQXVA3ogY+mYQ=" - }, - "object-inspect": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.3.0.tgz", - "integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==" + "version": "3.0.2" }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + "pex-color": { + "version": "git+ssh://git@github.com/pex-gl/pex-color.git#942007b3ce164e23ad85bd59d6a04d046321445b", + "dev": true, + "from": "pex-color@github:pex-gl/pex-color#v2" }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "pex-context": { + "version": "git+ssh://git@github.com/pex-gl/pex-context.git#e9b88af96bc5aca9be5cce1103aa7a45ae73bdfe", + "dev": true, + "from": "pex-context@github:pex-gl/pex-context#v3", "requires": { - "wrappy": "1.0.2" + "pex-gl": "^3.0.0-alpha.0" } }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "pex-geom": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pex-geom/-/pex-geom-2.0.1.tgz", - "integrity": "sha512-2/Ci2rGGyqm3nQNKKsR8ZaUL+DK/2U0CIKNQLEt5K5OfFFMY4ALh9DyhL0gVADT3Znpe3BSjymqSST3kheNhIQ==", + "version": "git+ssh://git@github.com/pex-gl/pex-geom.git#4523420f126284939e1ad42eefc01a24e62f463d", + "from": "pex-geom@^3.0.0-alpha.0", "requires": { - "pex-math": "2.0.0", - "tape": "4.8.0" + "pex-math": "4.0.0-alpha.1" } }, - "pex-math": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pex-math/-/pex-math-2.0.0.tgz", - "integrity": "sha512-6Ca4OSIzCg0saX2iV6jAQpZoMrmgjWw+AAOKfjDJ3NDdsK7J7c1x2hpx6vJdGWDaSkeCCzEAX6cxGSoCdnQTPg==" - }, - "raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", - "requires": { - "performance-now": "2.1.0" - } + "pex-gl": { + "version": "3.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/pex-gl/-/pex-gl-3.0.0-alpha.0.tgz", + "integrity": "sha512-F5xRhI2A3iVhcSmBIIsjEoAeLBa8tqTUzQy2cwZ9X5i1yKRXH8mfQd21JegAuY2aIgzHylquQmWXKzskR8reIA==", + "dev": true }, - "resolve": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", - "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "pex-gui": { + "version": "git+ssh://git@github.com/pex-gl/pex-gui.git#e64c6d12d1742be7a88885559b96c6c41438b55f", + "dev": true, + "from": "pex-gui@github:pex-gl/pex-gui#v3", "requires": { - "path-parse": "1.0.5" + "pex-color": "github:pex-gl/pex-color#v2", + "pex-geom": "github:pex-gl/pex-geom#v3", + "pex-math": "github:pex-gl/pex-math#v4" } }, - "resumer": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", - "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", - "requires": { - "through": "2.3.8" - } + "pex-math": { + "version": "git+ssh://git@github.com/pex-gl/pex-math.git#a4f95e0eef7071a62bae5f1bd2a494b5a3746684", + "from": "pex-math@^4.0.0-alpha.1" }, - "string.prototype.trim": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", - "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", + "pex-random": { + "version": "git+ssh://git@github.com/pex-gl/pex-random.git#e43efec8c740fb64f7349f4006417c3ec1a82ddb", + "dev": true, + "from": "pex-random@github:pex-gl/pex-random#v2", "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.10.0", - "function-bind": "1.1.1" + "seedrandom": "^3.0.5", + "simplex-noise": "3.0.1" } }, - "tape": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/tape/-/tape-4.8.0.tgz", - "integrity": "sha512-TWILfEnvO7I8mFe35d98F6T5fbLaEtbFTG/lxWvid8qDfFTxt19EBijWmB4j3+Hoh5TfHE2faWs73ua+EphuBA==", - "requires": { - "deep-equal": "1.0.1", - "defined": "1.0.0", - "for-each": "0.3.2", - "function-bind": "1.1.1", - "glob": "7.1.2", - "has": "1.0.1", - "inherits": "2.0.3", - "minimist": "1.2.0", - "object-inspect": "1.3.0", - "resolve": "1.4.0", - "resumer": "0.0.0", - "string.prototype.trim": "1.1.2", - "through": "2.3.8" - } + "primitive-geometry": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/primitive-geometry/-/primitive-geometry-2.7.0.tgz", + "integrity": "sha512-WlZ2FiNpGSytzqVGkcQu5cixyggOwdE6L77TU53h80hq0A/fGSINHmSAVM01y0TnMWUd7LYs9Bovbdi45XeJzA==", + "dev": true }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "simplex-noise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-3.0.1.tgz", + "integrity": "sha512-eww0SFiWLyOaUKQMJ7gbdvQJvULeJdM/Y4BiC3rrOQnYHo+MSPh465/qeXSZkpTdB9/HthumpnYD3DobZweBBQ==", + "dev": true }, "xyz-to-latlon": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/xyz-to-latlon/-/xyz-to-latlon-1.0.2.tgz", - "integrity": "sha512-iTQe5HIzAE0r2L2u2abs17s/SekCCUAlIHzYnHHo10aR0B1iKRSGye33z7a52IMJoUUMTpPwNUTNIy+OhZVRIg==" + "version": "1.0.2" } } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 63d80ef..47908d2 --- a/package.json +++ b/package.json @@ -1,43 +1,55 @@ { "name": "pex-cam", - "version": "2.8.0", - "description": "Camera models and modifiers.", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "https://github.com/pex-gl/pex-cam.git" - }, + "version": "3.0.0-alpha.1", + "description": "Cameras models and controllers for 3D rendering in PEX.", "keywords": [ "pex", - "webgl" + "cam", + "webgl", + "3d", + "cameras", + "perspective", + "orthographic", + "orbiter" ], - "author": { - "name": "Henryk Wollik", - "email": "hwollik@hotmail.com", - "url": "http://henrykwollik.com" - }, + "homepage": "https://github.com/pex-gl/pex-cam", + "bugs": "https://github.com/pex-gl/pex-cam/issues", + "repository": "pex-gl/pex-cam", + "license": "MIT", + "author": "Henryk Wollik (http://henrykwollik.com)", "contributors": [ - { - "name": "Marcin Ignac", - "email": "marcin.ignac@gmail.com", - "url": "http://marcinignac.com" - } + "Marcin Ignac (http://marcinignac.com)", + "Damien Seguin (https://github.com/dmnsgn)" ], - "license": "MIT", - "bugs": { - "url": "https://github.com/pex-gl/pex-cam/issues" + "type": "module", + "exports": "./index.js", + "main": "index.js", + "types": "types/index.d.ts", + "scripts": { + "build": "snowdev build", + "dev": "snowdev dev", + "release": "snowdev release" }, - "homepage": "https://github.com/pex-gl/pex-cam", "dependencies": { "interpolate-angle": "^1.0.2", "latlon-to-xyz": "^1.0.1", "mouse-event-offset": "^3.0.2", - "pex-geom": "^2.0.1", - "pex-math": "^2.0.0", - "raf": "^3.4.0", + "pex-geom": "^3.0.0-alpha.0", + "pex-math": "^4.0.0-alpha.1", "xyz-to-latlon": "^1.0.2" + }, + "devDependencies": { + "es-module-shims": "^1.5.8", + "pex-context": "github:pex-gl/pex-context#v3", + "pex-gui": "github:pex-gl/pex-gui#v3", + "pex-random": "github:pex-gl/pex-random#v2", + "primitive-geometry": "^2.7.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "snowdev": { + "dependencies": "all" } } diff --git a/perspective.js b/perspective.js index 16def4a..4069591 100644 --- a/perspective.js +++ b/perspective.js @@ -1,124 +1,122 @@ -const vec3 = require('pex-math/vec3') -const mat4 = require('pex-math/mat4') - -function setFrustumOffset (camera, x, y, width, height, widthTotal, heightTotal) { - // console.log('frustum', x, y, width, height, widthTotal, heightTotal) - widthTotal = widthTotal === undefined ? width : widthTotal - heightTotal = heightTotal === undefined ? height : heightTotal - - var near = camera.near - var far = camera.far - var fov = camera.fov - - var aspectRatio = widthTotal / heightTotal - - var top = Math.tan(fov * 0.5) * near - var bottom = -top - var left = aspectRatio * bottom - var right = aspectRatio * top - var width_ = Math.abs(right - left) - var height_ = Math.abs(top - bottom) - var widthNormalized = width_ / widthTotal - var heightNormalized = height_ / heightTotal - - var l = left + x * widthNormalized - var r = left + (x + width) * widthNormalized - var b = top - (y + height) * heightNormalized - var t = top - y * heightNormalized - - camera.aspect = aspectRatio - mat4.frustum(camera.projectionMatrix, l, r, b, t, near, far) -} - -function PerspectiveCamera (opts) { - this.set({ - projectionMatrix: mat4.create(), - invViewMatrix: mat4.create(), - viewMatrix: mat4.create(), - position: [0, 0, 3], - target: [0, 0, 0], - up: [0, 1, 0], - fov: Math.PI / 3, - aspect: 1, - near: 0.1, - far: 100 - }) - - this.set(opts) -} - -PerspectiveCamera.prototype.set = function (opts) { - Object.assign(this, opts) - - if (opts.position || opts.target || opts.up) { - mat4.lookAt( - this.viewMatrix, - this.position, - this.target, - this.up - ) - mat4.set(this.invViewMatrix, this.viewMatrix) - mat4.invert(this.invViewMatrix) +import { vec3, mat4 } from "pex-math"; + +import Camera from "./camera.js"; + +/** + * A class to create a perspective camera + * @extends Camera + */ +class PerspectiveCamera extends Camera { + static get DEFAULT_OPTIONS() { + return { + fov: Math.PI / 3, + }; } - if (opts.fov || opts.aspect || opts.near || opts.far) { - mat4.perspective( - this.projectionMatrix, - this.fov, - this.aspect, - this.near, - this.far - ) + /** + * Create an instance of PerspectiveCamera + * @param {import("./types.js").CameraOptions & import("./types.js").PerspectiveCameraOptions} opts + */ + constructor(opts = {}) { + super(); + + this.set({ + ...Camera.DEFAULT_OPTIONS, + ...PerspectiveCamera.DEFAULT_OPTIONS, + ...opts, + }); } - if (this.frustum) { - setFrustumOffset( - this, - this.frustum.offset[0], this.frustum.offset[1], - this.frustum.size[0], this.frustum.size[1], - this.frustum.totalSize[0], this.frustum.totalSize[1] - ) + /** + * Update the camera + * @param {import("./types.js").CameraOptions & import("./types.js").PerspectiveCameraOptions} opts + */ + set(opts) { + super.set(opts); + + if (opts.fov || opts.aspect || opts.near || opts.far || opts.view) { + if (this.view) { + const aspectRatio = this.view.totalSize[0] / this.view.totalSize[1]; + + const top = Math.tan(this.fov * 0.5) * this.near; + const bottom = -top; + const left = aspectRatio * bottom; + const right = aspectRatio * top; + const width = Math.abs(right - left); + const height = Math.abs(top - bottom); + const widthNormalized = width / this.view.totalSize[0]; + const heightNormalized = height / this.view.totalSize[1]; + + const l = left + this.view.offset[0] * widthNormalized; + const r = + left + (this.view.offset[0] + this.view.size[0]) * widthNormalized; + const b = + top - (this.view.offset[1] + this.view.size[1]) * heightNormalized; + const t = top - this.view.offset[1] * heightNormalized; + + mat4.frustum(this.projectionMatrix, l, r, b, t, this.near, this.far); + } else { + mat4.perspective( + this.projectionMatrix, + this.fov, + this.aspect, + this.near, + this.far + ); + } + } } -} -PerspectiveCamera.prototype.getViewRay = function (x, y, windowWidth, windowHeight) { - if (this.frustum) { - x += this.frustum.offset[0] - y += this.frustum.offset[1] - windowWidth = this.frustum.totalSize[0] - windowHeight = this.frustum.totalSize[1] + /** + * Create a picking ray in view (camera) coordinates + * @param {number} x mouse x + * @param {number} y mouse y + * @param {number} windowWidth + * @param {number} windowHeight + * @returns {import("pex-geom").ray} + */ + getViewRay(x, y, windowWidth, windowHeight) { + if (this.view) { + x += this.view.offset[0]; + y += this.view.offset[1]; + windowWidth = this.view.totalSize[0]; + windowHeight = this.view.totalSize[1]; + } + let nx = (2 * x) / windowWidth - 1; + let ny = 1 - (2 * y) / windowHeight; + + const hNear = 2 * Math.tan(this.fov / 2) * this.near; + const wNear = hNear * this.aspect; + + nx *= wNear * 0.5; + ny *= hNear * 0.5; + + // [origin, direction] + return [[0, 0, 0], vec3.normalize([nx, ny, -this.near])]; } - let nx = 2 * x / windowWidth - 1 - let ny = 1 - 2 * y / windowHeight - - let hNear = 2 * Math.tan(this.fov / 2) * this.near - let wNear = hNear * this.aspect - - nx *= (wNear * 0.5) - ny *= (hNear * 0.5) - - let origin = [0, 0, 0] - let direction = vec3.normalize([nx, ny, -this.near]) - let ray = [origin, direction] - return ray -} - -PerspectiveCamera.prototype.getWorldRay = function (x, y, windowWidth, windowHeight) { - let ray = this.getViewRay(x, y, windowWidth, windowHeight) - let origin = ray[0] - let direction = ray[1] - - vec3.multMat4(origin, this.invViewMatrix) - // this is correct as origin is [0, 0, 0] so direction is also a point - vec3.multMat4(direction, this.invViewMatrix) - - // is this necessary? - vec3.normalize(vec3.sub(direction, origin)) - - return ray + /** + * Create a picking ray in world coordinates + * @param {number} x + * @param {number} y + * @param {number} windowWidth + * @param {number} windowHeight + * @returns {import("pex-geom").ray} + */ + getWorldRay(x, y, windowWidth, windowHeight) { + let ray = this.getViewRay(x, y, windowWidth, windowHeight); + const origin = ray[0]; + const direction = ray[1]; + + vec3.multMat4(origin, this.invViewMatrix); + // this is correct as origin is [0, 0, 0] so direction is also a point + vec3.multMat4(direction, this.invViewMatrix); + + // TODO: is this necessary? + vec3.normalize(vec3.sub(direction, origin)); + + return ray; + } } -module.exports = function createPerspectiveCamera (opts) { - return new PerspectiveCamera(opts) -} +export default PerspectiveCamera; diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..8665df9 Binary files /dev/null and b/screenshot.png differ diff --git a/types.js b/types.js new file mode 100644 index 0000000..1292af7 --- /dev/null +++ b/types.js @@ -0,0 +1,63 @@ +/** + * @typedef {number} Radians + */ +/** + * @typedef {number} Degrees + */ + +/** + * @typedef {Object} CameraView + * @property {import("pex-math").vec2} offset + * @property {import("pex-math").vec2} size + * @property {import("pex-math").vec2} totalSize + */ + +/** + * @typedef {Object} CameraOptions + * @property {import("pex-math").mat4} [projectionMatrix=mat4.create()] + * @property {import("pex-math").mat4} [invViewMatrix=mat4.create()] + * @property {import("pex-math").mat4} [viewMatrix=mat4.create()] + * @property {import("pex-math").vec3} [position=[0, 0, 3]] + * @property {import("pex-math").vec3} [target=[0, 0, 0]] + * @property {import("pex-math").vec3} [up=[0, 1, 0]] + * @property {number} [aspect=1] + * @property {number} [near=0.1] + * @property {number} [far=100] + * @property {CameraView} [view=null] + */ + +/** + * @typedef {Object} PerspectiveCameraOptions + * @property {Radians} [fov=Math.PI / 3] + */ + +/** + * @typedef {Object} OrthographicCameraOptions + * @property {number} [left=-1] + * @property {number} [right=1] + * @property {number} [bottom=-1] + * @property {number} [top=1] + * @property {number} [zoom=1] + */ + +/** + * @typedef {Object} OrbiterControlsOptions + * @property {import("./camera.js").Camera} camera + * @property {HTMLElement} [element=document] + * @property {number} [easing=0.1] + * @property {boolean} [zoom=true] + * @property {boolean} [pan=true] + * @property {boolean} [drag=true] + * @property {number} [minDistance=0.01] + * @property {number} [maxDistance=Infinity] + * @property {Degrees} [minLat=-89.5] + * @property {Degrees} [maxLat=89.5] + * @property {number} [minLon=-Infinity] + * @property {number} [maxLon=Infinity] + * @property {number} [panSlowdown=4] + * @property {number} [zoomSlowdown=400] + * @property {number} [dragSlowdown=4] + * @property {boolean} [autoUpdate=true] + */ + +export {};