diff --git a/src/mol-gl/camera.ts b/src/mol-gl/camera.ts new file mode 100644 index 0000000000000000000000000000000000000000..468289b0babb32369a68ce17fc7cb132374d4a1f --- /dev/null +++ b/src/mol-gl/camera.ts @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +/* + * This code has been modified from https://github.com/regl-project/regl-camera, + * copyright (c) 2016 Mikola Lysenko. MIT License + */ + +const isBrowser = typeof window !== 'undefined' + +import REGL = require('regl'); + +import mouseChange, { MouseModifiers } from 'mol-util/mouse-change' +import mouseWheel from 'mol-util/mouse-wheel' +import { Mat4, Vec3 } from 'mol-math/linear-algebra/3d' +import { clamp, damp } from 'mol-math/interpolate' + +export interface CameraUniforms { + projection: Mat4, +} + +export interface CameraState { + center: Vec3, + theta: number, + phi: number, + distance: number, + eye: Vec3, + up: Vec3, + fovy: number, + near: number, + far: number, + noScroll: boolean, + flipY: boolean, + dtheta: number, + dphi: number, + rotationSpeed: number, + zoomSpeed: number, + renderOnDirty: boolean, + damping: number, + minDistance: number, + maxDistance: number, +} + +export interface Camera { + update: (props: any, block: any) => void, + setState: (newState: CameraState) => void, + isDirty: () => boolean +} + +export namespace Camera { + export function create (regl: REGL.Regl, element: HTMLElement, props: Partial<CameraState> = {}): Camera { + const state: CameraState = { + center: props.center || Vec3.zero(), + theta: props.theta || 0, + phi: props.phi || 0, + distance: Math.log(props.distance || 10.0), + eye: Vec3.zero(), + up: props.up || Vec3.create(0, 1, 0), + fovy: props.fovy || Math.PI / 4.0, + near: typeof props.near !== 'undefined' ? props.near : 0.01, + far: typeof props.far !== 'undefined' ? props.far : 1000.0, + noScroll: typeof props.noScroll !== 'undefined' ? props.noScroll : false, + flipY: !!props.flipY, + dtheta: 0, + dphi: 0, + rotationSpeed: typeof props.rotationSpeed !== 'undefined' ? props.rotationSpeed : 1, + zoomSpeed: typeof props.zoomSpeed !== 'undefined' ? props.zoomSpeed : 1, + renderOnDirty: typeof props.renderOnDirty !== undefined ? !!props.renderOnDirty : false, + damping: typeof props.damping !== 'undefined' ? props.damping : 0.9, + minDistance: Math.log(typeof props.minDistance !== 'undefined' ? props.minDistance : 0.1), + maxDistance: Math.log(typeof props.maxDistance !== 'undefined' ? props.maxDistance : 1000) + } + + const view = Mat4.identity() + const projection = Mat4.identity() + + const right = Vec3.create(1, 0, 0) + const front = Vec3.create(0, 0, 1) + + let dirty = false + let ddistance = 0 + + let prevX = 0 + let prevY = 0 + + if (isBrowser) { + const source = element || regl._gl.canvas + + const getWidth = function () { + return element ? element.offsetWidth : window.innerWidth + } + + const getHeight = function () { + return element ? element.offsetHeight : window.innerHeight + } + + mouseChange(source, function (buttons: number, x: number, y: number, mods: MouseModifiers) { + if (buttons & 1) { + const dx = (x - prevX) / getWidth() + const dy = (y - prevY) / getHeight() + + state.dtheta += state.rotationSpeed * 4.0 * dx + state.dphi += state.rotationSpeed * 4.0 * dy + dirty = true; + } + prevX = x + prevY = y + }) + + mouseWheel(source, function (dx: number, dy: number) { + ddistance += dy / getHeight() * state.zoomSpeed + dirty = true; + }, state.noScroll) + } + + function dampAndMarkDirty (x: number) { + const xd = damp(x, state.damping) + if (Math.abs(xd) < 0.1) return 0 + dirty = true; + return xd + } + + function setState (newState: Partial<CameraState> = {}) { + Object.assign(state, newState) + + const { center, eye, up, dtheta, dphi } = state + + state.theta += dtheta + state.phi = clamp(state.phi + dphi, -Math.PI / 2.0, Math.PI / 2.0) + state.distance = clamp(state.distance + ddistance, state.minDistance, state.maxDistance) + + state.dtheta = dampAndMarkDirty(dtheta) + state.dphi = dampAndMarkDirty(dphi) + ddistance = dampAndMarkDirty(ddistance) + + const theta = state.theta + const phi = state.phi + const r = Math.exp(state.distance) + + const vf = r * Math.sin(theta) * Math.cos(phi) + const vr = r * Math.cos(theta) * Math.cos(phi) + const vu = r * Math.sin(phi) + + for (let i = 0; i < 3; ++i) { + eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i] + } + + Mat4.lookAt(view, eye, center, up) + } + + const injectContext = regl({ + context: { + view: () => view, + dirty: () => dirty, + projection: (context: REGL.DefaultContext) => { + Mat4.perspective( + projection, + state.fovy, + context.viewportWidth / context.viewportHeight, + state.near, + state.far + ) + if (state.flipY) { projection[5] *= -1 } + return projection + } + }, + uniforms: { // TODO + view: regl.context('view' as any), + projection: regl.context('projection' as any) + } + }) + + function update (props: any, block: any) { + setState() + injectContext(props, block) + if (dirty) { + console.log(view) + } + dirty = false + } + + return { + update, + setState, + isDirty: () => dirty + } + } +}