├── .npmignore ├── .gitignore ├── LICENSE ├── docs ├── example.js └── index.html ├── package.json ├── README.md └── regl-camera.js /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mikola Lysenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var regl = require('regl')() 4 | var camera = window.camera = require('../regl-camera')(regl, { 5 | center: [0, 2.5, 0], 6 | damping: 0, 7 | noScroll: true, 8 | renderOnDirty: true 9 | }) 10 | 11 | var bunny = require('bunny') 12 | var normals = require('angle-normals') 13 | 14 | window.addEventListener('resize', function () { 15 | camera.dirty = true; 16 | }); 17 | 18 | var drawBunny = regl({ 19 | frag: ` 20 | precision mediump float; 21 | varying vec3 vnormal; 22 | void main () { 23 | gl_FragColor = vec4(abs(vnormal), 1.0); 24 | }`, 25 | vert: ` 26 | precision mediump float; 27 | uniform mat4 projection, view; 28 | attribute vec3 position, normal; 29 | varying vec3 vnormal; 30 | void main () { 31 | vnormal = normal; 32 | gl_Position = projection * view * vec4(position, 1.0); 33 | }`, 34 | attributes: { 35 | position: bunny.positions, 36 | normal: normals(bunny.cells, bunny.positions) 37 | }, 38 | elements: bunny.cells 39 | }) 40 | 41 | regl.frame(function () { 42 | camera(function () { 43 | regl.clear({color: [0, 0, 0, 1]}) 44 | drawBunny() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regl-camera", 3 | "version": "2.1.1", 4 | "description": "Camera for regl", 5 | "main": "regl-camera.js", 6 | "dependencies": { 7 | "gl-mat4": "^1.1.4", 8 | "mouse-wheel": "^1.2.0", 9 | "mouse-change": "^1.3.0" 10 | }, 11 | "devDependencies": { 12 | "angle-normals": "^1.0.0", 13 | "browserify": "^13.1.0", 14 | "bunny": "^1.0.1", 15 | "es2040": "^1.2.5", 16 | "github-cornerify": "^1.0.7", 17 | "indexhtmlify": "^1.3.1", 18 | "metadataify": "^1.0.1", 19 | "regl": "^1.2.1", 20 | "standard": "^10.0.1", 21 | "uglify-js": "^2.8.22" 22 | }, 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1", 25 | "start": "budo docs/example.js --live --open", 26 | "build": "browserify docs/example.js -t es2040 | uglifyjs -c -m | indexhtmlify | metadataify | github-cornerify > docs/index.html", 27 | "lint": "standard", 28 | "lint-fix": "standard --fix" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/mikolalysenko/regl-camera.git" 33 | }, 34 | "keywords": [ 35 | "regl", 36 | "camera", 37 | "3d", 38 | "orbit", 39 | "turntable" 40 | ], 41 | "author": "Mikola Lysenko", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/mikolalysenko/regl-camera/issues" 45 | }, 46 | "homepage": "https://github.com/mikolalysenko/regl-camera#readme", 47 | "github-corner": { 48 | "fg": "#333", 49 | "bg" : "#fff" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # regl-camera 2 | A basic reusable "turntable" camera component for [regl](http://regl.party). (Secretly just [spherical coordinates](https://en.wikipedia.org/wiki/Spherical_coordinate_system).) 3 | 4 | ## Example 5 | 6 | ```javascript 7 | const regl = require('regl')() 8 | const camera = require('regl-camera')(regl, { 9 | center: [0, 2.5, 0] 10 | }) 11 | 12 | const bunny = require('bunny') 13 | const normals = require('angle-normals') 14 | 15 | const drawBunny = regl({ 16 | frag: ` 17 | precision mediump float; 18 | varying vec3 vnormal; 19 | void main () { 20 | gl_FragColor = vec4(abs(vnormal), 1.0); 21 | }`, 22 | vert: ` 23 | precision mediump float; 24 | uniform mat4 projection, view; 25 | attribute vec3 position, normal; 26 | varying vec3 vnormal; 27 | void main () { 28 | vnormal = normal; 29 | gl_Position = projection * view * vec4(position, 1.0); 30 | }`, 31 | attributes: { 32 | position: bunny.positions, 33 | normal: normals(bunny.cells, bunny.positions) 34 | }, 35 | elements: bunny.cells 36 | }) 37 | 38 | regl.frame(() => { 39 | camera((state) => { 40 | if (!state.dirty) return; 41 | regl.clear({color: [0, 0, 0, 1]}) 42 | drawBunny() 43 | }) 44 | }) 45 | ``` 46 | 47 | ## Install 48 | 49 | ``` 50 | npm i regl-camera 51 | ``` 52 | 53 | ## API 54 | 55 | ### Constructor 56 | 57 | #### `var camera = require('regl-camera')(regl[, options])` 58 | `module.exports` of `regl-camera` is a constructor for the camera. It takes the following arguments: 59 | 60 | * `regl` is a handle to the regl instance 61 | * `options` is an object with the following optional properties: 62 | + `center` which is the center of the camera 63 | + `theta` the theta angle for the camera 64 | + `phi` the phi angle for the camera 65 | + `distance` the distance from the camera eye to the center 66 | + `up` is the up vector for the camera 67 | + `fovy` is the field of view angle in y direction (defaults to `Math.PI / 4`) 68 | + `near` is the near clipping plane in z (defaults to `0.01`) 69 | + `far` is the far clipping plane in z (defaults to `1000.0`) 70 | + `mouse` set to `false` to turn off mouse events 71 | + `damping` multiplier for inertial damping (default 0.9). Set to 0 to disable inertia. 72 | + `noScroll` boolean flag to prevent mouse wheel from scrolling the whole window. Default is false. 73 | + `element` is an optional DOM element for mouse events (defaults to regl canvas element) 74 | + `rotationSpeed` the rotation interactions (default: `1`) 75 | + `zoomSpeed` the zoom interactions (default: `1`) 76 | + `renderOnDirty` boolean flag to control whether scene is only rendered when the camera state has changed. If true, render can be triggerd at any time by setting `camer.dirty = true`. If false, dirty state can still be detected and used through `context.dirty`. 77 | 78 | ### Command usage 79 | 80 | #### `camera(block)` 81 | `regl-camera` sets up an environment with the following variables in both the context and uniform blocks: 82 | 83 | | Variable | Type | Description | 84 | |----------|------|-------------| 85 | | `view` | `mat4` | The view matrix for the camera | 86 | | `projection` | `mat4` | The projection matrix for the camera | 87 | | `center` | `vec3` | The center of the camera | 88 | | `eye` | `vec3` | The eye coordinates of the camera | 89 | | `up` | `vec3` | The up vector for the camera matrix | 90 | | `theta` | `float` | Latitude angle parameter in radians | 91 | | `phi` | `float` | Longitude angle parameter in radians | 92 | | `distance` | `float` | Distance from camera to center of objective | 93 | | `dirty` | `boolean` | Flag set to true when camera state has changed | 94 | 95 | **Note** 96 | These properties can also be accessed and modified directly by accessing the object, though at the moment you will need to manually set `camera.dirty = true` if relying upon `renderOnDirty` 97 | 98 | ## License 99 | (c) 2016 Mikola Lysenko. MIT License 100 | -------------------------------------------------------------------------------- /regl-camera.js: -------------------------------------------------------------------------------- 1 | var mouseChange = require('mouse-change') 2 | var mouseWheel = require('mouse-wheel') 3 | var identity = require('gl-mat4/identity') 4 | var perspective = require('gl-mat4/perspective') 5 | var lookAt = require('gl-mat4/lookAt') 6 | 7 | module.exports = createCamera 8 | 9 | var isBrowser = typeof window !== 'undefined' 10 | 11 | function createCamera (regl, props_) { 12 | var props = props_ || {} 13 | 14 | // Preserve backward-compatibilty while renaming preventDefault -> noScroll 15 | if (typeof props.noScroll === 'undefined') { 16 | props.noScroll = props.preventDefault; 17 | } 18 | 19 | var cameraState = { 20 | view: identity(new Float32Array(16)), 21 | projection: identity(new Float32Array(16)), 22 | center: new Float32Array(props.center || 3), 23 | theta: props.theta || 0, 24 | phi: props.phi || 0, 25 | distance: Math.log(props.distance || 10.0), 26 | eye: new Float32Array(3), 27 | up: new Float32Array(props.up || [0, 1, 0]), 28 | fovy: props.fovy || Math.PI / 4.0, 29 | near: typeof props.near !== 'undefined' ? props.near : 0.01, 30 | far: typeof props.far !== 'undefined' ? props.far : 1000.0, 31 | noScroll: typeof props.noScroll !== 'undefined' ? props.noScroll : false, 32 | flipY: !!props.flipY, 33 | dtheta: 0, 34 | dphi: 0, 35 | rotationSpeed: typeof props.rotationSpeed !== 'undefined' ? props.rotationSpeed : 1, 36 | zoomSpeed: typeof props.zoomSpeed !== 'undefined' ? props.zoomSpeed : 1, 37 | renderOnDirty: typeof props.renderOnDirty !== undefined ? !!props.renderOnDirty : false 38 | } 39 | 40 | var element = props.element 41 | var damping = typeof props.damping !== 'undefined' ? props.damping : 0.9 42 | 43 | var right = new Float32Array([1, 0, 0]) 44 | var front = new Float32Array([0, 0, 1]) 45 | 46 | var minDistance = Math.log('minDistance' in props ? props.minDistance : 0.1) 47 | var maxDistance = Math.log('maxDistance' in props ? props.maxDistance : 1000) 48 | 49 | var ddistance = 0 50 | 51 | var prevX = 0 52 | var prevY = 0 53 | 54 | if (isBrowser && props.mouse !== false) { 55 | var source = element || regl._gl.canvas 56 | 57 | function getWidth () { 58 | return element ? element.offsetWidth : window.innerWidth 59 | } 60 | 61 | function getHeight () { 62 | return element ? element.offsetHeight : window.innerHeight 63 | } 64 | 65 | mouseChange(source, function (buttons, x, y) { 66 | if (buttons & 1) { 67 | var dx = (x - prevX) / getWidth() 68 | var dy = (y - prevY) / getHeight() 69 | 70 | cameraState.dtheta += cameraState.rotationSpeed * 4.0 * dx 71 | cameraState.dphi += cameraState.rotationSpeed * 4.0 * dy 72 | cameraState.dirty = true; 73 | } 74 | prevX = x 75 | prevY = y 76 | }) 77 | 78 | mouseWheel(source, function (dx, dy) { 79 | ddistance += dy / getHeight() * cameraState.zoomSpeed 80 | cameraState.dirty = true; 81 | }, props.noScroll) 82 | } 83 | 84 | function damp (x) { 85 | var xd = x * damping 86 | if (Math.abs(xd) < 0.1) { 87 | return 0 88 | } 89 | cameraState.dirty = true; 90 | return xd 91 | } 92 | 93 | function clamp (x, lo, hi) { 94 | return Math.min(Math.max(x, lo), hi) 95 | } 96 | 97 | function updateCamera (props) { 98 | Object.keys(props).forEach(function (prop) { 99 | cameraState[prop] = props[prop] 100 | }) 101 | 102 | var center = cameraState.center 103 | var eye = cameraState.eye 104 | var up = cameraState.up 105 | var dtheta = cameraState.dtheta 106 | var dphi = cameraState.dphi 107 | 108 | cameraState.theta += dtheta 109 | cameraState.phi = clamp( 110 | cameraState.phi + dphi, 111 | -Math.PI / 2.0, 112 | Math.PI / 2.0) 113 | cameraState.distance = clamp( 114 | cameraState.distance + ddistance, 115 | minDistance, 116 | maxDistance) 117 | 118 | cameraState.dtheta = damp(dtheta) 119 | cameraState.dphi = damp(dphi) 120 | ddistance = damp(ddistance) 121 | 122 | var theta = cameraState.theta 123 | var phi = cameraState.phi 124 | var r = Math.exp(cameraState.distance) 125 | 126 | var vf = r * Math.sin(theta) * Math.cos(phi) 127 | var vr = r * Math.cos(theta) * Math.cos(phi) 128 | var vu = r * Math.sin(phi) 129 | 130 | for (var i = 0; i < 3; ++i) { 131 | eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i] 132 | } 133 | 134 | lookAt(cameraState.view, eye, center, up) 135 | } 136 | 137 | cameraState.dirty = true; 138 | 139 | var injectContext = regl({ 140 | context: Object.assign({}, cameraState, { 141 | dirty: function () { 142 | return cameraState.dirty; 143 | }, 144 | projection: function (context) { 145 | perspective(cameraState.projection, 146 | cameraState.fovy, 147 | context.viewportWidth / context.viewportHeight, 148 | cameraState.near, 149 | cameraState.far) 150 | if (cameraState.flipY) { cameraState.projection[5] *= -1 } 151 | return cameraState.projection 152 | } 153 | }), 154 | uniforms: Object.keys(cameraState).reduce(function (uniforms, name) { 155 | uniforms[name] = regl.context(name) 156 | return uniforms 157 | }, {}) 158 | }) 159 | 160 | function setupCamera (props, block) { 161 | if (typeof setupCamera.dirty !== 'undefined') { 162 | cameraState.dirty = setupCamera.dirty || cameraState.dirty 163 | setupCamera.dirty = undefined; 164 | } 165 | 166 | if (props && block) { 167 | cameraState.dirty = true; 168 | } 169 | 170 | if (cameraState.renderOnDirty && !cameraState.dirty) return; 171 | 172 | if (!block) { 173 | block = props 174 | props = {} 175 | } 176 | 177 | updateCamera(props) 178 | injectContext(block) 179 | cameraState.dirty = false; 180 | } 181 | 182 | Object.keys(cameraState).forEach(function (name) { 183 | setupCamera[name] = cameraState[name] 184 | }) 185 | 186 | return setupCamera 187 | } 188 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |