├── .gitignore ├── LICENSE ├── README.md ├── camera.js ├── example └── simple.js └── package.json /.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 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Mikola Lysenko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3d-view-controls 2 | An easy to use 3D camera with input binding. 3 | 4 | Default controls: 5 | 6 | Button | Interaction 7 | -------|------------ 8 | Left mouse | Rotate 9 | Shift + left mouse *or* scroll horizontally | Roll 10 | Right mouse | Pan 11 | Middle mouse *or* scroll vertically | Zoom 12 | 13 | # Example 14 | 15 | Here is a complete working example of how to use this module in an application: 16 | 17 | ```javascript 18 | var createCamera = require('3d-view-controls') 19 | var bunny = require('bunny') 20 | var perspective = require('gl-mat4/perspective') 21 | var createMesh = require('gl-simplicial-complex') 22 | 23 | var canvas = document.createElement('canvas') 24 | document.body.appendChild(canvas) 25 | window.addEventListener('resize', require('canvas-fit')(canvas)) 26 | 27 | var gl = canvas.getContext('webgl') 28 | 29 | var camera = createCamera(canvas, { 30 | eye: [50,0,0], 31 | center: [0,0,0], 32 | zoomMax: 500 33 | }) 34 | 35 | var mesh = createMesh(gl, { 36 | cells: bunny.cells, 37 | positions: bunny.positions, 38 | colormap: 'jet' 39 | }) 40 | 41 | function render() { 42 | requestAnimationFrame(render) 43 | if(camera.tick()) { 44 | gl.viewport(0, 0, canvas.width, canvas.height) 45 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 46 | gl.enable(gl.DEPTH_TEST) 47 | mesh.draw({ 48 | projection: perspective([], Math.PI/4, canvas.width/canvas.height, 0.01, 1000), 49 | view: camera.matrix 50 | }) 51 | } 52 | } 53 | render() 54 | ``` 55 | 56 | [You can try it out in your browser right now](https://mikolalysenko.github.io/3d-view-controls). 57 | 58 | # Install 59 | 60 | ``` 61 | npm i 3d-view-controls 62 | ``` 63 | 64 | # API 65 | 66 | ## Constructor 67 | 68 | #### `var camera = require('3d-view-controls')(element[, options])` 69 | Creates a new camera object. 70 | 71 | * `element` is a DOM node onto which this 72 | * `options` is an object with the following optional properties: 73 | + `eye` - the position of the camera in world coordinates (Default `[0,0,10]`) 74 | + `center` - the target of the camera in world coordinates (Default `[0,0,0]`) 75 | + `up` - the up vector of the camera (Default `[0,1,0]`) 76 | + `mode` - the interaction mode for the camera (Default `'orbit'`) 77 | + `delay` - amount to delay interactions by for interpolation in ms (Default `16`) 78 | + `rotateSpeed` - rotation scaling factor (Default `1`) 79 | + `zoomSpeed` - zoom scaling factor (Default `1`) 80 | + `translateSpeed` - translation/panning scale factor (Default `1`) 81 | + `flipX` - flip X axis for rotations (Default `false`) 82 | + `flipY` - flip Y axis for rotations (Default `false`) 83 | + `zoomMin` - minimum zoom distance (Default `0.01`) 84 | + `zoomMax` - maximum zoom distance (Default `Infinity`) 85 | 86 | ## Geometric properties 87 | 88 | Note that you can update any property by assigning to it. For example: 89 | 90 | ```javascript 91 | camera.eye = [100, 100, 100] 92 | 93 | camera.matrix = [ 94 | 1, 0, 0, 0, 95 | 0, 1, 0, 0, 96 | 0, 0, 1, 0, 97 | 0, 0, 0, 1] 98 | ``` 99 | 100 | #### `camera.matrix` 101 | A 4x4 matrix encoded as a length 16 array representing the homogeneous transformation from world coordinates to view (camera) coordinates. 102 | 103 | #### `camera.mode` 104 | The current interaction mode for the camera. Possible values include: 105 | 106 | * `orbit` - free orbiting mode 107 | * `turntable` - behaves like a turntable/gimbal 108 | * `matrix` - manual matrix control 109 | 110 | #### `camera.eye` 111 | The position of the camera in world coordinates 112 | 113 | #### `camera.up` 114 | A vector pointing up in world coordinates 115 | 116 | #### `camera.center` 117 | The target of the camera in world coordinates 118 | 119 | #### `camera.distance` 120 | Euclidean distance from `eye` to `center` 121 | 122 | ## Methods 123 | 124 | #### `camera.tick()` 125 | Updates the camera state. Call this before each frame is rendered to compute the current state of the camera. 126 | 127 | **Returns** `true` if the state of the camera has changed since the last call to `tick` 128 | 129 | #### `camera.lookAt(center, eye, up)` 130 | Sets the camera center/eye/up vector to look at a fixed target 131 | 132 | * `center` is the new center/target for the camera 133 | * `eye` is the position of the camera in world coordinates 134 | * `up` is a vector pointing up 135 | 136 | #### `camera.rotate(yaw, pitch, roll)` 137 | Applies an incremental rotation to the camera 138 | 139 | * `yaw` is the amount to rotate about the y-axis (in xz plane of camera) 140 | * `pitch` is the amount to rotate about the x-axis (in yz plane of camera) 141 | * `roll` is the amount to rotate about the forward axis (in xy plane of camera) 142 | 143 | #### `camera.pan(dx, dy, dz)` 144 | Applies a relative motion to the camera, moving in view coordinates 145 | 146 | * `dx,dy,dz` are the components of the camera motion vector 147 | 148 | #### `camera.translate(dx, dy, dz)` 149 | Translates the camera in world coordinates 150 | 151 | * `dx,dy,dz` are the components of the translation vector 152 | 153 | ## Tuning parameters 154 | 155 | #### `camera.distanceLimits` 156 | A 2D array representing the `[lo,hi]` bounds on the zoom distance. Note that `0 < lo < hi`. 157 | 158 | #### `camera.flipX` 159 | A flag controlling whether the camera rotation is flipped along the x-axis 160 | 161 | #### `camera.flipY` 162 | A flag controlling whether the camera rotation is flipped along the y-axis 163 | 164 | #### `camera.delay` 165 | The amount of delay on the interpolation of the camera state in ms 166 | 167 | #### `camera.rotateSpeed` 168 | Camera rotation speed scaling factor 169 | 170 | #### `camera.zoomSpeed` 171 | Camera zoom speed scaling factor 172 | 173 | #### `camera.translateSpeed` 174 | Camera translation speed scaling factor 175 | 176 | #### `camera.element` 177 | The DOM element the camera is attached to 178 | 179 | # Future 180 | 181 | Expand to support more input types: 182 | 183 | * Touch 184 | * Keyboard 185 | * GamePad 186 | * VR? 187 | 188 | # License 189 | (c) 2015 Mikola Lysenko. MIT License -------------------------------------------------------------------------------- /camera.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = createCamera 4 | 5 | var now = require('right-now') 6 | var createView = require('3d-view') 7 | var mouseChange = require('mouse-change') 8 | var mouseWheel = require('mouse-wheel') 9 | var mouseOffset = require('mouse-event-offset') 10 | var hasPassive = require('has-passive-events') 11 | 12 | function createCamera(element, options) { 13 | element = element || document.body 14 | options = options || {} 15 | 16 | var limits = [ 0.01, Infinity ] 17 | if('distanceLimits' in options) { 18 | limits[0] = options.distanceLimits[0] 19 | limits[1] = options.distanceLimits[1] 20 | } 21 | if('zoomMin' in options) { 22 | limits[0] = options.zoomMin 23 | } 24 | if('zoomMax' in options) { 25 | limits[1] = options.zoomMax 26 | } 27 | 28 | var view = createView({ 29 | center: options.center || [0,0,0], 30 | up: options.up || [0,1,0], 31 | eye: options.eye || [0,0,10], 32 | mode: options.mode || 'orbit', 33 | distanceLimits: limits 34 | }) 35 | 36 | var pmatrix = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 37 | var distance = 0.0 38 | var width = element.clientWidth 39 | var height = element.clientHeight 40 | 41 | var camera = { 42 | view: view, 43 | element: element, 44 | delay: options.delay || 16, 45 | rotateSpeed: options.rotateSpeed || 1, 46 | zoomSpeed: options.zoomSpeed || 1, 47 | translateSpeed: options.translateSpeed || 1, 48 | flipX: !!options.flipX, 49 | flipY: !!options.flipY, 50 | modes: view.modes, 51 | tick: function() { 52 | var t = now() 53 | var delay = this.delay 54 | view.idle(t-delay) 55 | view.flush(t-(100+delay*2)) 56 | var ctime = t - 2 * delay 57 | view.recalcMatrix(ctime) 58 | var allEqual = true 59 | var matrix = view.computedMatrix 60 | for(var i=0; i<16; ++i) { 61 | allEqual = allEqual && (pmatrix[i] === matrix[i]) 62 | pmatrix[i] = matrix[i] 63 | } 64 | var sizeChanged = 65 | element.clientWidth === width && 66 | element.clientHeight === height 67 | width = element.clientWidth 68 | height = element.clientHeight 69 | if(allEqual) { 70 | return !sizeChanged 71 | } 72 | distance = Math.exp(view.computedRadius[0]) 73 | return true 74 | }, 75 | lookAt: function(center, eye, up) { 76 | view.lookAt(view.lastT(), center, eye, up) 77 | }, 78 | rotate: function(pitch, yaw, roll) { 79 | view.rotate(view.lastT(), pitch, yaw, roll) 80 | }, 81 | pan: function(dx, dy, dz) { 82 | view.pan(view.lastT(), dx, dy, dz) 83 | }, 84 | translate: function(dx, dy, dz) { 85 | view.translate(view.lastT(), dx, dy, dz) 86 | } 87 | } 88 | 89 | Object.defineProperties(camera, { 90 | matrix: { 91 | get: function() { 92 | return view.computedMatrix 93 | }, 94 | set: function(mat) { 95 | view.setMatrix(view.lastT(), mat) 96 | return view.computedMatrix 97 | }, 98 | enumerable: true 99 | }, 100 | mode: { 101 | get: function() { 102 | return view.getMode() 103 | }, 104 | set: function(mode) { 105 | view.setMode(mode) 106 | return view.getMode() 107 | }, 108 | enumerable: true 109 | }, 110 | center: { 111 | get: function() { 112 | return view.computedCenter 113 | }, 114 | set: function(ncenter) { 115 | view.lookAt(view.lastT(), ncenter) 116 | return view.computedCenter 117 | }, 118 | enumerable: true 119 | }, 120 | eye: { 121 | get: function() { 122 | return view.computedEye 123 | }, 124 | set: function(neye) { 125 | view.lookAt(view.lastT(), null, neye) 126 | return view.computedEye 127 | }, 128 | enumerable: true 129 | }, 130 | up: { 131 | get: function() { 132 | return view.computedUp 133 | }, 134 | set: function(nup) { 135 | view.lookAt(view.lastT(), null, null, nup) 136 | return view.computedUp 137 | }, 138 | enumerable: true 139 | }, 140 | distance: { 141 | get: function() { 142 | return distance 143 | }, 144 | set: function(d) { 145 | view.setDistance(view.lastT(), d) 146 | return d 147 | }, 148 | enumerable: true 149 | }, 150 | distanceLimits: { 151 | get: function() { 152 | return view.getDistanceLimits(limits) 153 | }, 154 | set: function(v) { 155 | view.setDistanceLimits(v) 156 | return v 157 | }, 158 | enumerable: true 159 | } 160 | }) 161 | 162 | element.addEventListener('contextmenu', function(ev) { 163 | ev.preventDefault() 164 | return false 165 | }) 166 | 167 | var lastX = 0, lastY = 0, lastMods = {shift: false, control: false, alt: false, meta: false} 168 | mouseChange(element, handleInteraction) 169 | 170 | //enable simple touch interactions 171 | element.addEventListener('touchstart', function (ev) { 172 | var xy = mouseOffset(ev.changedTouches[0], element) 173 | handleInteraction(0, xy[0], xy[1], lastMods) 174 | handleInteraction(1, xy[0], xy[1], lastMods) 175 | 176 | ev.preventDefault() 177 | }, hasPassive ? {passive: false} : false) 178 | 179 | element.addEventListener('touchmove', function (ev) { 180 | var xy = mouseOffset(ev.changedTouches[0], element) 181 | handleInteraction(1, xy[0], xy[1], lastMods) 182 | 183 | ev.preventDefault() 184 | }, hasPassive ? {passive: false} : false) 185 | 186 | element.addEventListener('touchend', function (ev) { 187 | var xy = mouseOffset(ev.changedTouches[0], element) 188 | handleInteraction(0, lastX, lastY, lastMods) 189 | 190 | ev.preventDefault() 191 | }, hasPassive ? {passive: false} : false) 192 | 193 | function handleInteraction (buttons, x, y, mods) { 194 | var scale = 1.0 / element.clientHeight 195 | var dx = scale * (x - lastX) 196 | var dy = scale * (y - lastY) 197 | 198 | var flipX = camera.flipX ? 1 : -1 199 | var flipY = camera.flipY ? 1 : -1 200 | 201 | var drot = Math.PI * camera.rotateSpeed 202 | 203 | var t = now() 204 | 205 | if(buttons & 1) { 206 | if(mods.shift) { 207 | view.rotate(t, 0, 0, -dx * drot) 208 | } else { 209 | view.rotate(t, flipX * drot * dx, -flipY * drot * dy, 0) 210 | } 211 | } else if(buttons & 2) { 212 | view.pan(t, -camera.translateSpeed * dx * distance, camera.translateSpeed * dy * distance, 0) 213 | } else if(buttons & 4) { 214 | var kzoom = camera.zoomSpeed * dy / window.innerHeight * (t - view.lastT()) * 50.0 215 | view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1)) 216 | } 217 | 218 | lastX = x 219 | lastY = y 220 | lastMods = mods 221 | } 222 | 223 | mouseWheel(element, function(dx, dy, dz) { 224 | var flipX = camera.flipX ? 1 : -1 225 | var flipY = camera.flipY ? 1 : -1 226 | var t = now() 227 | if(Math.abs(dx) > Math.abs(dy)) { 228 | view.rotate(t, 0, 0, -dx * flipX * Math.PI * camera.rotateSpeed / window.innerWidth) 229 | } else { 230 | var kzoom = camera.zoomSpeed * flipY * dy / window.innerHeight * (t - view.lastT()) / 100.0 231 | view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1)) 232 | } 233 | }, true) 234 | 235 | return camera 236 | } 237 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | var createCamera = require('../camera') 2 | var bunny = require('bunny') 3 | var perspective = require('gl-mat4/perspective') 4 | var createMesh = require('gl-mesh3d') 5 | 6 | var canvas = document.createElement('canvas') 7 | document.body.appendChild(canvas) 8 | window.addEventListener('resize', require('canvas-fit')(canvas)) 9 | 10 | var gl = canvas.getContext('webgl') 11 | 12 | var camera = createCamera(canvas, { 13 | eye: [50,0,0], 14 | center: [0,0,0], 15 | zoomMax: 500 16 | }) 17 | 18 | var mesh = createMesh(gl, { 19 | cells: bunny.cells, 20 | positions: bunny.positions, 21 | colormap: 'jet' 22 | }) 23 | 24 | function render() { 25 | requestAnimationFrame(render) 26 | if(camera.tick()) { 27 | gl.viewport(0, 0, canvas.width, canvas.height) 28 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 29 | gl.enable(gl.DEPTH_TEST) 30 | mesh.draw({ 31 | projection: perspective([], Math.PI/4, canvas.width/canvas.height, 0.01, 1000), 32 | view: camera.matrix 33 | }) 34 | } 35 | } 36 | render() 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3d-view-controls", 3 | "version": "2.2.2", 4 | "description": "A 3D camera with hooks for input handling", 5 | "main": "camera.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "dependencies": { 10 | "3d-view": "^2.0.0", 11 | "has-passive-events": "^1.0.0", 12 | "mouse-change": "^1.1.1", 13 | "mouse-event-offset": "^3.0.2", 14 | "mouse-wheel": "^1.0.2", 15 | "right-now": "^1.0.0" 16 | }, 17 | "devDependencies": { 18 | "bunny": "^1.0.1", 19 | "canvas-fit": "^1.2.0", 20 | "gl-mesh3d": "^1.2.0", 21 | "gl-mat4": "^1.1.2" 22 | }, 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/mikolalysenko/3d-view-controls.git" 29 | }, 30 | "keywords": [ 31 | "camera", 32 | "webgl", 33 | "3d", 34 | "input", 35 | "handling", 36 | "view", 37 | "orbit", 38 | "turntable", 39 | "matrix", 40 | "graphics", 41 | "stackgl", 42 | "gl" 43 | ], 44 | "author": "Mikola Lysenko", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/mikolalysenko/3d-view-controls/issues" 48 | }, 49 | "homepage": "https://github.com/mikolalysenko/3d-view-controls" 50 | } 51 | --------------------------------------------------------------------------------