Skip to content

Instantly share code, notes, and snippets.

@nkint
Created May 31, 2017 13:58
Show Gist options
  • Save nkint/77be542bf32cba2ff9b548701b4084d6 to your computer and use it in GitHub Desktop.
Save nkint/77be542bf32cba2ff9b548701b4084d6 to your computer and use it in GitHub Desktop.
Sketch of new regl camera
import { projectionMatrix, viewMatrix } from './turntable-camera'
import mouseChange from 'mouse-change'
import mouseWheel from 'mouse-wheel'
import identity from 'gl-mat4/identity'
import key from 'key-pressed'
// const isBrowser = typeof window !== 'undefined'
const getStartingState = (initialStatePlusOptions) => ({
view: identity(new Float32Array(16)),
projection: identity(new Float32Array(16)),
theta: initialStatePlusOptions.theta || 0,
phi: initialStatePlusOptions.phi || 0,
distance: Math.log(initialStatePlusOptions.distance || 10.0),
center: new Float32Array(initialStatePlusOptions.center || [0, 0, 1]),
eye: new Float32Array(3),
dtheta: 0,
dphi: 0,
ddistance: 0,
dcenter: [0, 0, 0],
mouse: Boolean(initialStatePlusOptions.mouse) || true,
})
const getStartingOptions = (initialStatePlusOptions) => ({
fovy: initialStatePlusOptions.fovy || Math.PI / 4.0,
near: typeof initialStatePlusOptions.near !== 'undefined' ? initialStatePlusOptions.near : 0.01,
far: typeof initialStatePlusOptions.far !== 'undefined' ? initialStatePlusOptions.far : 1000.0,
flipY: Boolean(initialStatePlusOptions.flipY),
damping: typeof initialStatePlusOptions.damping !== 'undefined' ? initialStatePlusOptions.damping : 0.6,
minDistance: Math.log('minDistance' in initialStatePlusOptions ? initialStatePlusOptions.minDistance : 0.1),
maxDistance: Math.log('maxDistance' in initialStatePlusOptions ? initialStatePlusOptions.maxDistance : 1000),
preventDefault: Boolean(initialStatePlusOptions.preventDefault : false),
})
const noop = function () {}
export default function createCamera(regl, initialStatePlusOptions, drawFunction = noop, callbacks = {}) {
const { onPan = noop, onZoom = noop, onRotate = noop } = callbacks
const cameraState = getStartingState(initialStatePlusOptions)
const options = getStartingOptions(initialStatePlusOptions)
const element = regl._gl.canvas
element.addEventListener('mousewheel', function (e) {
if (cameraState.preventDefault) e.preventDefault()
})
element.addEventListener('contextmenu', function (event) {
event.preventDefault()
})
document.addEventListener('mousedown', function (event) {
if (event.ctrlKey || event.button === 2) {
event.preventDefault()
}
})
let prevX = 0
let prevY = 0
mouseChange(element, function (buttons, x, y) {
const ctrl = key('<control>')
if (buttons === 1 && ctrl) {
// rotation
const dx = (x - prevX) / window.innerWidth
const dy = (y - prevY) / window.innerHeight
// costants from here: https://github.com/mapbox/mapbox-gl-js/blob/b9e10b939c6a3fe5d7ecac209f751b4871970ede/src/ui/handler/drag_rotate.js#L106
cameraState.dtheta -= dy * 0.8
cameraState.dphi += dx * 1.5
onRotate(cameraState.theta, cameraState.phi)
drawFunction()
} else if (buttons === 1 && !ctrl) {
// pan
const exp = Math.exp(cameraState.distance) * 0.001
cameraState.dcenter[0] = -(x - prevX) * exp
cameraState.dcenter[1] = (y - prevY) * exp
onPan(cameraState.center)
drawFunction()
}
prevX = x
prevY = y
})
mouseWheel(element, function (dx, dy) {
cameraState.ddistance += dy / window.innerHeight
onZoom(cameraState.distance)
drawFunction()
})
const projection = function (context) {
return projectionMatrix(cameraState.projection,
options.fovy,
context.viewportWidth / context.viewportHeight,
options.near,
options.far)
}
const view = function (context, props) {
Object.assign(cameraState, props)
viewMatrix(cameraState.view, cameraState, options)
return cameraState.view
}
const injectContextCamera = regl({
context: {
projection,
view,
// this is a dirty workaround on DynamicProperties to have access at updated cameraState in the context
cameraState: () => cameraState,
},
uniforms: {
projection: regl.context('projection'),
view: regl.context('view'),
},
})
injectContextCamera.projection = projection
injectContextCamera.view = view
return injectContextCamera
}
import perspective from 'gl-mat4/perspective'
import lookAt from 'gl-mat4/lookAt'
export function projectionMatrix(out, fovy, aspectRatio, near, far, flipY) {
perspective(out, fovy, aspectRatio, near, far)
if (flipY) { out[5] *= -1 }
return out
}
function damp(damping, x) {
const xd = x * damping
if (Math.abs(xd) < 0.0001) {
return 0
}
return xd
}
function clamp(x, lo, hi) {
return Math.min(Math.max(x, lo), hi)
}
const front = new Float32Array([1, 0, 0])
const right = new Float32Array([0, 1, 0])
const up = new Float32Array([0, 0, 1])
function updateCameraWidthDeltas(cameraState, options) {
cameraState.center[0] += cameraState.dcenter[0]
cameraState.center[1] += cameraState.dcenter[1]
cameraState.phi += cameraState.dphi
cameraState.theta += cameraState.dtheta
cameraState.theta = clamp(
cameraState.theta,
Math.PI / 2 + 0.0001,
Math.PI)
cameraState.distance = clamp(
cameraState.distance + cameraState.ddistance,
options.minDistance,
options.maxDistance)
cameraState.dtheta = damp(options.damping, cameraState.dtheta)
cameraState.dphi = damp(options.damping, cameraState.dphi)
cameraState.ddistance = damp(options.damping, cameraState.ddistance)
cameraState.dcenter[0] = damp(options.damping, cameraState.dcenter[0])
cameraState.dcenter[1] = damp(options.damping, cameraState.dcenter[1])
}
export function viewMatrix(out, cameraState, options) {
if (cameraState.mouse) {
updateCameraWidthDeltas(cameraState, options)
} else {
}
const theta = cameraState.theta
const phi = cameraState.phi
const r = Math.exp(cameraState.distance)
const vf = r * Math.sin(phi) * Math.cos(theta)
const vr = r * Math.cos(phi) * Math.cos(theta)
const vu = r * Math.sin(theta)
const center = cameraState.center
const eye = cameraState.eye
for (let i = 0; i < 3; ++i) {
eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i]
}
lookAt(cameraState.view, eye, center, up)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment