From 158f3de52b2a6d6b148b3da2a309f9e1f6d6073b Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Fri, 13 Apr 2018 18:54:29 -0700
Subject: [PATCH] wip, camera & controls

---
 package-lock.json                       | Bin 441292 -> 453562 bytes
 package.json                            |   1 +
 src/apps/render-test/state.ts           |   6 +-
 src/mol-gl/camera.ts                    | 192 -----------------
 src/mol-gl/camera/base.ts               | 100 +++++++++
 src/mol-gl/camera/orthographic.ts       |   5 +
 src/mol-gl/camera/perspective.ts        |  61 ++++++
 src/mol-gl/camera/util.ts               |  96 +++++++++
 src/mol-gl/controls/orbit.ts            | 237 ++++++++++++++++++++
 src/mol-gl/controls/trackball.ts        |   0
 src/mol-gl/renderable.ts                |   2 +
 src/mol-gl/renderable/mesh.ts           |   7 +-
 src/mol-gl/renderable/point.ts          |   4 +
 src/mol-gl/renderer.ts                  | 244 +++++++++++++--------
 src/mol-gl/stats.ts                     |  67 ++++++
 src/mol-math/linear-algebra/3d.ts       |   4 +-
 src/mol-math/linear-algebra/3d/quat.ts  |  34 +++
 src/mol-math/linear-algebra/3d/vec2.ts  |  85 ++++++++
 src/mol-math/linear-algebra/3d/vec3.ts  |  25 +++
 src/mol-math/linear-algebra/3d/vec4.ts  |   5 +-
 src/mol-util/input/event-offset.ts      |  31 +++
 src/mol-util/input/input-observer.ts    | 273 ++++++++++++++++++++++++
 src/mol-util/input/mouse-change.ts      | 184 ++++++++++++++++
 src/mol-util/{ => input}/mouse-event.ts |   2 +-
 src/mol-util/input/mouse-wheel.ts       |  67 ++++++
 src/mol-util/input/touch-pinch.ts       | 188 ++++++++++++++++
 src/mol-util/mouse-change.ts            | 194 -----------------
 src/mol-util/mouse-wheel.ts             |  40 ----
 28 files changed, 1627 insertions(+), 527 deletions(-)
 delete mode 100644 src/mol-gl/camera.ts
 create mode 100644 src/mol-gl/camera/base.ts
 create mode 100644 src/mol-gl/camera/orthographic.ts
 create mode 100644 src/mol-gl/camera/perspective.ts
 create mode 100644 src/mol-gl/camera/util.ts
 create mode 100644 src/mol-gl/controls/orbit.ts
 create mode 100644 src/mol-gl/controls/trackball.ts
 create mode 100644 src/mol-gl/stats.ts
 create mode 100644 src/mol-math/linear-algebra/3d/vec2.ts
 create mode 100644 src/mol-util/input/event-offset.ts
 create mode 100644 src/mol-util/input/input-observer.ts
 create mode 100644 src/mol-util/input/mouse-change.ts
 rename src/mol-util/{ => input}/mouse-event.ts (96%)
 create mode 100644 src/mol-util/input/mouse-wheel.ts
 create mode 100644 src/mol-util/input/touch-pinch.ts
 delete mode 100644 src/mol-util/mouse-change.ts
 delete mode 100644 src/mol-util/mouse-wheel.ts

diff --git a/package-lock.json b/package-lock.json
index 4ce0ce891139fdffd9528576f7e5180db3a8fb39..e0ce8a7104c0c2831c0d2f0726b4aecad7820596 100644
GIT binary patch
delta 6015
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
GxBNHVru2&d

delta 1520
zcmZ9MeN5DK9LM*4^zP*DfIB$B1AZXP4pUA_Dm>FB>cm-2No2z?#g;9_2Qt@_XrnS8
zVEYB1oN%IK>C}8U=p&R!<ik>gD=d=`a9YhEgmh}tq~HC{SnJR4_Wgby-tX7@TYqt6
z^PT9!AyK~jQA-6J+|H7H>*8h!$ggJ#lpA=Y+At|Z^<GQ^_ZA_bvr<O+rYE@tM5Hps
zcVtYf8SEFhO>LZP^_4s|o<ZVTqWI2xnkD2%OU3u;)cr(FthK5A2^KYPnin#*F-3i_
zNcuvjw{L<PBsO(Xq1E?St3|2{=iA`tx0x0EBd9?wO?UVv{FuXi(>iu>-_<LJxN6?x
zQuqGat9HyC<2&%%`|)t0M<@`PLag|lk$6l{vx6%5v5nXsf#Nr*0>>tjdc6tsXKI%=
zct{^t$JV;A_!;swhm(0s!Fkh3JB24Gw?Si@utR7UOT&Up;tLykX+d``dETHNEKkOY
zd@_zeX%rc+jS>M9?bNECI1_;}FA=W+JRu|*a+|0fzG~+Ycy9%XHK>2@a6!uJ<ch8;
z#;hZS!*q2pr-*FipH&o-(17&+TOQAr5vx)43KJZz77F%z$?h1eJWd=2$fzOlFgjh1
zfXY=QM4gh!%iBdNR3&NmJf}&uQTG9RTZx0|UDcy;F)+4`oK)Y<Q1C$;x$e-#f?GWV
z;b0$eLBVAy@N_TnYr|>oWA!aEEgMs2(-8!RXVDY}llD;)%(*P%VPO-s1vh*nn?7m8
z%`eh$0t@o#HGSTyZJI3|A=HeEmeR$}VEvyq&;c-iuMNJkm6@S?BfSJg*(4PUHq*Nl
zz3<Vvwg4(nwVGP+!FF113JeRoKB3=eXt<!;A%@|GJ#>x4+5@!86lkS}&Q|bMr2DCw
zVN@`&p2iyjn82KD8i6aD=(*v+?l-)|f_W{piRkF!I6s}83Xkv8c=>>`^)#r4U9jD-
zrjsTBT4WeBG!QfR&(Sp48Bc65&ML#zQjZ;Stuhe~^pRw&lB|sBWuPmJ8F7}0Ez8sl
z*S&P2Ka2ffR&VVH@Nb4~OPGXXi`lnOo68l{j<-pG!9*-u$~<fco;avCY1-MBv5(g3
zfx+4?*2&-xhi22rK@tw`Br2fsDr-`+=Igbw<Qj8IowSw|IUJjRXB7gjIfyfmC?~$w
z&t5j7=^ooe@bDlT2!$1q+yKgTX~o=dUMpc=G_M<~RxpwuCqr{Toy;Xpj^SB0UD{V#
zXcD%#`JnaR8`wUD$I}3n3O<{`4>^MW$DsvWpf#6|l6um}N+1Iw8v6GNjxQ|cHzm54
z^Hs*7Dxa_A9R@g|NEcSE=Y26)x05H>b)t%h#!Yi6iG_+8Bo<R@_zWl9DC8!!Xo7-U
z{d{Ja`cAqF8s~A2{a5&R>2T_f7KRq5I3nPe+ZxYX5(HR=HeCIf@J2wx015mkqj2U-
zQ5Xv6^VmbcVG@nRo*dDbqEl8hAr3B=@nq=PB=F&XW(G@%kkD~TTA{L=hM@mLVHWsB
zskkU`aJMjcV7Wtk65Azi!dAi4(A_PTgzG5lwc7p^b#VNxVD6o`_ok4K2BY_7oOClC
z=(`Eh#s34w*;1lFok<Hw@o1S`sxJViP~OL*4U!pyx=+o~9Pd0Mt2q>35;mA}URtoE
IS&oVO2kCZ3(EtDd

diff --git a/package.json b/package.json
index 70f4cd415..37d12c735 100644
--- a/package.json
+++ b/package.json
@@ -80,6 +80,7 @@
   "dependencies": {
     "argparse": "^1.0.10",
     "express": "^4.16.3",
+    "gl": "^4.0.4",
     "material-ui": "^1.0.0-beta.41",
     "node-fetch": "^2.1.2",
     "react": "^16.3.1",
diff --git a/src/apps/render-test/state.ts b/src/apps/render-test/state.ts
index abb3245e4..e787d8c55 100644
--- a/src/apps/render-test/state.ts
+++ b/src/apps/render-test/state.ts
@@ -9,7 +9,7 @@ import { BehaviorSubject } from 'rxjs';
 // import { ValueCell } from 'mol-util/value-cell'
 
 // import { Vec3, Mat4 } from 'mol-math/linear-algebra'
-import { createRenderer, Renderer } from 'mol-gl/renderer'
+import Renderer from 'mol-gl/renderer'
 // import { createColorTexture } from 'mol-gl/util';
 // import Icosahedron from 'mol-geo/primitive/icosahedron'
 // import Box from 'mol-geo/primitive/box'
@@ -31,7 +31,7 @@ export default class State {
     loading = new BehaviorSubject<boolean>(false)
 
     async initRenderer (container: HTMLDivElement) {
-        this.renderer = createRenderer(container)
+        this.renderer = Renderer.fromElement(container)
         this.initialized.next(true)
         this.loadPdbId()
         this.renderer.frame()
@@ -55,8 +55,6 @@ export default class State {
         await Run(structSpacefillRepr.create(struct))
         structSpacefillRepr.renderObjects.forEach(renderer.add)
 
-        renderer.draw(true)
-
         this.loading.next(false)
     }
 }
diff --git a/src/mol-gl/camera.ts b/src/mol-gl/camera.ts
deleted file mode 100644
index 623e24672..000000000
--- a/src/mol-gl/camera.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * 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 { defaults } from 'mol-util'
-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,
-    getState: () => CameraState,
-    dirty: boolean
-}
-
-export namespace Camera {
-    export function create (regl: REGL.Regl, element: HTMLElement, initialState: Partial<CameraState> = {}): Camera {
-        const state: CameraState = {
-            center: defaults(initialState.center, Vec3.zero()),
-            theta: defaults(initialState.theta, 0),
-            phi: defaults(initialState.phi, 0),
-            distance: Math.log(defaults(initialState.distance, 10.0)),
-            eye: Vec3.zero(),
-            up: defaults(initialState.up, Vec3.create(0, 1, 0)),
-            fovy: defaults(initialState.fovy, Math.PI / 4.0),
-            near: defaults(initialState.near, 0.01),
-            far: defaults(initialState.far, 1000.0),
-            noScroll: defaults(initialState.noScroll, false),
-            flipY: defaults(initialState.flipY, false),
-            dtheta: 0,
-            dphi: 0,
-            rotationSpeed: defaults(initialState.rotationSpeed, 1),
-            zoomSpeed: defaults(initialState.zoomSpeed, 1),
-            renderOnDirty: defaults(initialState.renderOnDirty, false),
-            damping: defaults(initialState.damping, 0.9),
-            minDistance: Math.log(defaults(initialState.minDistance, 0.1)),
-            maxDistance: Math.log(defaults(initialState.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 = true
-        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)
-            dirty = false
-        }
-
-        return {
-            update,
-            setState,
-            getState: () => Object.assign({}, state),
-            get dirty() { return dirty },
-            set dirty(value: boolean) { dirty = value }
-        }
-    }
-}
diff --git a/src/mol-gl/camera/base.ts b/src/mol-gl/camera/base.ts
new file mode 100644
index 000000000..3b9b78fec
--- /dev/null
+++ b/src/mol-gl/camera/base.ts
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Mat4, Vec3, Vec4 } from 'mol-math/linear-algebra'
+import { cameraProject, cameraUnproject, cameraLookAt, Viewport } from './util';
+
+export interface Camera {
+    view: Mat4,
+    projection: Mat4,
+    projectionView: Mat4,
+    inverseProjectionView: Mat4,
+
+    viewport: Viewport,
+    position: Vec3,
+    direction: Vec3,
+    up: Vec3,
+
+    translate: (v: Vec3) => void,
+    reset: () => void,
+    lookAt: (target: Vec3) => void,
+    update: () => void,
+    project: (out: Vec4, point: Vec3) => Vec4,
+    unproject: (out: Vec3, point: Vec3) => Vec3
+}
+
+export const DefaultCameraProps = {
+    position: Vec3.zero(),
+    direction: Vec3.create(0, 0, -1),
+    up: Vec3.create(0, 1, 0),
+    viewport: Viewport.create(-1, -1, 1, 1)
+}
+export type CameraProps = Partial<typeof DefaultCameraProps>
+
+export namespace Camera {
+    export function create(props?: CameraProps): Camera {
+        const p = { ...DefaultCameraProps, ...props };
+
+        const projection = Mat4.identity()
+        const view = Mat4.identity()
+        const position = Vec3.clone(p.position)
+        const direction = Vec3.clone(p.direction)
+        const up = Vec3.clone(p.up)
+        const viewport = Viewport.clone(p.viewport)
+        const projectionView = Mat4.identity()
+        const inverseProjectionView = Mat4.identity()
+
+        function update () {
+            Mat4.mul(projectionView, projection, view)
+            Mat4.invert(inverseProjectionView, projectionView)
+        }
+
+        function lookAt (target: Vec3) {
+            cameraLookAt(direction, up, position, target)
+        }
+
+        function reset () {
+            Vec3.copy(position, p.position)
+            Vec3.copy(direction, p.direction)
+            Vec3.copy(up, p.up)
+            Mat4.setIdentity(view)
+            Mat4.setIdentity(projection)
+            Mat4.setIdentity(projectionView)
+            Mat4.setIdentity(inverseProjectionView)
+        }
+
+        function translate (v: Vec3) {
+            Vec3.add(position, position, v)
+        }
+
+        function project (out: Vec4, point: Vec3) {
+            return cameraProject(out, point, viewport, projectionView)
+        }
+
+        function unproject (out: Vec3, point: Vec3) {
+            return cameraUnproject(out, point, viewport, inverseProjectionView)
+        }
+
+        return {
+            view,
+            projection,
+            projectionView,
+            inverseProjectionView,
+
+            viewport,
+            position,
+            direction,
+            up,
+
+            translate,
+            reset,
+            lookAt,
+            update,
+            project,
+            unproject
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-gl/camera/orthographic.ts b/src/mol-gl/camera/orthographic.ts
new file mode 100644
index 000000000..e94778a50
--- /dev/null
+++ b/src/mol-gl/camera/orthographic.ts
@@ -0,0 +1,5 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
\ No newline at end of file
diff --git a/src/mol-gl/camera/perspective.ts b/src/mol-gl/camera/perspective.ts
new file mode 100644
index 000000000..88745f754
--- /dev/null
+++ b/src/mol-gl/camera/perspective.ts
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Mat4, Vec3 } from 'mol-math/linear-algebra'
+import { DefaultCameraProps, Camera } from './base'
+
+export interface PerspectiveCamera extends Camera {
+    fov: number,
+    near: number,
+    far: number
+}
+
+export const DefaultPerspectiveCameraProps = {
+    fov: Math.PI / 4,
+    near: 0.1,
+    far: 10000,
+    ...DefaultCameraProps
+}
+export type PerspectiveCameraProps = Partial<typeof DefaultPerspectiveCameraProps>
+
+export namespace PerspectiveCamera {
+    export function create(props: PerspectiveCameraProps = {}): PerspectiveCamera {
+        let { fov, near, far } = { ...DefaultPerspectiveCameraProps, ...props };
+
+        const camera = Camera.create(props)
+        const center = Vec3.zero()
+
+        function update () {
+            const aspect = camera.viewport.width / camera.viewport.height
+
+            // build projection matrix
+            Mat4.perspective(camera.projection, fov, aspect, Math.abs(near), Math.abs(far))
+
+            // build view matrix
+            Vec3.add(center, camera.position, camera.direction)
+            Mat4.lookAt(camera.view, camera.position, center, camera.up)
+
+            // update projection * view and invert
+            camera.update()
+        }
+
+        update()
+
+        return {
+            ...camera,
+            update,
+
+            get far() { return far },
+            set far(value: number) { far = value },
+
+            get near() { return near },
+            set near(value: number) { near = value },
+
+            get fov() { return fov },
+            set fov(value: number) { fov = value },
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-gl/camera/util.ts b/src/mol-gl/camera/util.ts
new file mode 100644
index 000000000..4f23d6ee6
--- /dev/null
+++ b/src/mol-gl/camera/util.ts
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Mat4, Vec3, Vec4, EPSILON } from 'mol-math/linear-algebra'
+
+export type Viewport = {
+    x: number
+    y: number
+    width: number
+    height: number
+}
+
+export namespace Viewport {
+    export function create(x: number, y: number, width: number, height: number): Viewport {
+        return { x, y, width, height }
+    }
+    export function clone(viewport: Viewport): Viewport {
+        return { ...viewport }
+    }
+}
+
+const tmpVec3 = Vec3.zero()
+
+/** Modifies the direction & up vectors in place */
+export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target: Vec3) {
+    Vec3.sub(tmpVec3, target, position)
+    Vec3.normalize(tmpVec3, tmpVec3)
+
+    if (!Vec3.isZero(tmpVec3)) {
+        // change direction vector to look at target
+        const d = Vec3.dot(tmpVec3, up)
+        if (Math.abs(d - 1) < EPSILON.Value) { // parallel
+            Vec3.scale(up, direction, -1)
+        } else if (Math.abs(d + 1) < EPSILON.Value) { // anti parallel
+            Vec3.copy(up, direction)
+        }
+        Vec3.copy(direction, tmpVec3)
+
+        // normalize up vector
+        Vec3.cross(tmpVec3, direction, up)
+        Vec3.normalize(tmpVec3, tmpVec3)
+        Vec3.cross(up, tmpVec3, direction)
+        Vec3.normalize(up, up)
+    }
+}
+
+const NEAR_RANGE = 0
+const FAR_RANGE = 1
+
+const tmpVec4 = Vec4.zero()
+
+/** Transform point into 2D window coordinates. */
+export function cameraProject (out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) {
+    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport
+
+    // clip space -> NDC -> window coordinates, implicit 1.0 for w component
+    Vec4.set(tmpVec4, point[0], point[1], point[2], 1.0)
+
+    // transform into clip space
+    Vec4.transformMat4(tmpVec4, tmpVec4, projectionView)
+
+    // transform into NDC
+    const w = tmpVec4[3]
+    if (w !== 0) {
+        tmpVec4[0] /= w
+        tmpVec4[1] /= w
+        tmpVec4[2] /= w
+    }
+
+    // transform into window coordinates, set fourth component is (1/clip.w) as in gl_FragCoord.w
+    out[0] = vX + vWidth / 2 * tmpVec4[0] + (0 + vWidth / 2)
+    out[1] = vY + vHeight / 2 * tmpVec4[1] + (0 + vHeight / 2)
+    out[2] = (FAR_RANGE - NEAR_RANGE) / 2 * tmpVec4[2] + (FAR_RANGE + NEAR_RANGE) / 2
+    out[3] = w === 0 ? 0 : 1 / w
+    return out
+}
+
+/**
+ * Transform point from screen space to 3D coordinates.
+ * The point must have x and y set to 2D window coordinates and z between 0 (near) and 1 (far).
+ */
+export function cameraUnproject (out: Vec3, point: Vec3, viewport: Viewport, inverseProjectionView: Mat4) {
+    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport
+
+    const x = point[0] - vX
+    const y = (vHeight - point[1] - 1) - vY
+    const z = point[2]
+
+    out[0] = (2 * x) / vWidth - 1
+    out[1] = (2 * y) / vHeight - 1
+    out[2] = 2 * z - 1
+    return Vec3.transformMat4(out, out, inverseProjectionView)
+}
\ No newline at end of file
diff --git a/src/mol-gl/controls/orbit.ts b/src/mol-gl/controls/orbit.ts
new file mode 100644
index 000000000..aef8fed0c
--- /dev/null
+++ b/src/mol-gl/controls/orbit.ts
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { map, filter, scan } from 'rxjs/operators';
+
+import { Quat, Vec2, Vec3, EPSILON } from 'mol-math/linear-algebra';
+import { clamp } from 'mol-math/interpolate';
+import InputObserver from 'mol-util/input/input-observer';
+
+const Y_UP = Vec3.create(0, 1, 0)
+const tmpVec3 = Vec3.zero()
+
+function cameraLookAt (direction: Vec3, up: Vec3, position: Vec3, target: Vec3) {
+    Vec3.copy(direction, target)
+    Vec3.sub(direction, direction, position)
+    Vec3.normalize(direction, direction)
+}
+
+export const DefaultOrbitControlsProps = {
+    parent: window as Window | Element,
+    noScroll: true,
+
+    phi: Math.PI / 2,
+    theta: 0,
+
+    position: Vec3.zero(),
+    up: Vec3.create(0, 1, 0),
+    target: Vec3.zero(),
+
+    distance: undefined as (number|undefined),
+    damping: 0.25,
+    rotateSpeed: 0.28,
+    zoomSpeed: 0.0075,
+    pinchSpeed: 0.0075,
+    translateSpeed: 1.0,
+}
+export type OrbitControlsProps = Partial<typeof DefaultOrbitControlsProps>
+
+interface OrbitControls {
+    update: () => void
+    copyInto: (positionOut: Vec3, directionOut: Vec3, upOut: Vec3) => void
+
+    position: Vec3
+    direction: Vec3
+    up: Vec3
+    target: Vec3
+
+    distance: number
+    damping: number
+    rotateSpeed: number
+    zoomSpeed: number
+    pinchSpeed: number
+    translateSpeed: number
+
+    phi: number
+    theta: number
+}
+
+namespace OrbitControls {
+    export function create (element: Element, props: OrbitControlsProps = {}): OrbitControls {
+        const p = { ...DefaultOrbitControlsProps, ...props }
+
+        const inputDelta = Vec3.zero() // x, y, zoom
+        const offset = Vec3.zero()
+
+        const upQuat = Quat.identity()
+        const upQuatInverse = Quat.identity()
+        const translateVec3 = Vec3.zero()
+
+        const position = Vec3.clone(p.position)
+        const direction = Vec3.zero()
+        const up = Vec3.clone(p.up)
+        const target = Vec3.clone(p.target)
+
+        // const phiBounds = Vec2.create(0, Math.PI)
+        const phiBounds = Vec2.create(-Infinity, Infinity)
+        const thetaBounds = Vec2.create(-Infinity, Infinity)
+        const distanceBounds = Vec2.create(0, Infinity)
+
+        let { damping, rotateSpeed, zoomSpeed, pinchSpeed, translateSpeed, phi, theta } = p
+        let distance = 0
+
+        // Compute distance if not defined in user options
+        if (p.distance === undefined) {
+            Vec3.sub(tmpVec3, position, target)
+            distance = Vec3.magnitude(tmpVec3)
+        }
+
+        const input = InputObserver.create(element, {
+            parent: p.parent,
+            noScroll: p.noScroll
+        })
+        input.drag.pipe(filter(v => v.buttons === 1)).subscribe(inputRotate)
+        input.drag.pipe(filter(v => v.buttons === 4)).subscribe(inputTranslate)
+        input.wheel.subscribe(inputZoom)
+        input.pinch.subscribe(inputPinch)
+
+        // Apply an initial phi and theta
+        applyPhiTheta()
+
+        return {
+            update,
+            copyInto,
+
+            position,
+            direction,
+            up,
+            target,
+
+            get distance() { return distance },
+            set distance(value: number ) { distance = value },
+            get damping() { return damping },
+            set damping(value: number ) { damping = value },
+            get rotateSpeed() { return rotateSpeed },
+            set rotateSpeed(value: number ) { rotateSpeed = value },
+            get zoomSpeed() { return zoomSpeed },
+            set zoomSpeed(value: number ) { zoomSpeed = value },
+            get pinchSpeed() { return pinchSpeed },
+            set pinchSpeed(value: number ) { pinchSpeed = value },
+            get translateSpeed() { return translateSpeed },
+            set translateSpeed(value: number ) { translateSpeed = value },
+
+            get phi() { return phi },
+            set phi(value: number ) { phi = value; applyPhiTheta() },
+            get theta() { return theta },
+            set theta(value: number ) { theta = value; applyPhiTheta() },
+        }
+
+        function copyInto(positionOut: Vec3, directionOut: Vec3, upOut: Vec3) {
+            Vec3.copy(positionOut, position)
+            Vec3.copy(directionOut, direction)
+            Vec3.copy(upOut, up)
+        }
+
+        function inputRotate ({ dx, dy }: { dx: number, dy: number }) {
+            const PI2 = Math.PI * 2
+            inputDelta[0] -= PI2 * dx * rotateSpeed
+            inputDelta[1] -= PI2 * dy * rotateSpeed
+        }
+
+        function inputZoom ({ dy }: { dy: number }) {
+            inputDelta[2] += dy * zoomSpeed
+        }
+
+        function inputPinch (delta: number) {
+            inputDelta[2] -= delta * pinchSpeed
+        }
+
+        function inputTranslate ({ dx, dy }: { dx: number, dy: number }) {
+            // TODO
+            console.log('translate', { dx, dy })
+            const x = dx * translateSpeed * distance
+            const y = dy * translateSpeed * distance
+            // Vec3.set(translateVec3, x, y, 0)
+            // Vec3.transformQuat(translateVec3, translateVec3, upQuat)
+
+            // pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
+            // pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
+
+            Vec3.copy(translateVec3, position)
+            Vec3.cross(translateVec3, translateVec3, up)
+            Vec3.normalize(translateVec3, translateVec3)
+            Vec3.scale(translateVec3, translateVec3, x )
+
+            const up2 = Vec3.clone(up)
+            Vec3.normalize(up2, up2)
+            Vec3.scale(up2, up2, y )
+            Vec3.add(translateVec3, translateVec3, up2)
+
+            Vec3.add(target, target, translateVec3)
+            Vec3.add(position, position, translateVec3)
+        }
+
+        function updateDirection () {
+            Quat.fromUnitVec3(upQuat, up, Y_UP)
+            Quat.invert(upQuatInverse, upQuat)
+
+            Vec3.sub(offset, position, target)
+            Vec3.transformQuat(offset, offset, upQuat)
+
+            let _distance = distance
+            let _theta = Math.atan2(offset[0], offset[2])
+            let _phi = Math.atan2(Math.sqrt(offset[0] * offset[0] + offset[2] * offset[2]), offset[1])
+
+            _theta += inputDelta[0]
+            _phi += inputDelta[1]
+
+            _theta = clamp(_theta, thetaBounds[0], thetaBounds[1])
+            _phi = clamp(_phi, phiBounds[0], phiBounds[1])
+            _phi = clamp(_phi, EPSILON.Value, Math.PI - EPSILON.Value)
+
+            _distance += inputDelta[2]
+            _distance = clamp(_distance, distanceBounds[0], distanceBounds[1])
+
+            const radius = Math.abs(_distance) <= EPSILON.Value ? EPSILON.Value : _distance
+            offset[0] = radius * Math.sin(_phi) * Math.sin(_theta)
+            offset[1] = radius * Math.cos(_phi)
+            offset[2] = radius * Math.sin(_phi) * Math.cos(_theta)
+
+            phi = _phi
+            theta = _theta
+            distance = _distance
+
+            Vec3.transformQuat(offset, offset, upQuatInverse)
+            Vec3.add(position, target, offset)
+            cameraLookAt(direction, up, position, target)
+        }
+
+        function update () {
+            updateDirection()
+            for (let i = 0; i < inputDelta.length; i++) {
+                inputDelta[i] *= 1 - damping
+            }
+        }
+
+        function applyPhiTheta () {
+            let _phi = phi
+            let _theta = theta
+            _theta = clamp(_theta, thetaBounds[0], thetaBounds[1])
+            _phi = clamp(_phi, phiBounds[0], phiBounds[1])
+            _phi = clamp(_phi, EPSILON.Value, Math.PI - EPSILON.Value)
+
+            const dist = Math.max(EPSILON.Value, distance)
+            position[0] = dist * Math.sin(_phi) * Math.sin(_theta)
+            position[1] = dist * Math.cos(_phi)
+            position[2] = dist * Math.sin(_phi) * Math.cos(_theta)
+            Vec3.add(position, position, target)
+
+            updateDirection()
+        }
+    }
+}
+
+export default OrbitControls
\ No newline at end of file
diff --git a/src/mol-gl/controls/trackball.ts b/src/mol-gl/controls/trackball.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/mol-gl/renderable.ts b/src/mol-gl/renderable.ts
index 1211d2e43..8a7d3f86d 100644
--- a/src/mol-gl/renderable.ts
+++ b/src/mol-gl/renderable.ts
@@ -16,6 +16,8 @@ export type AttributesBuffers<T extends AttributesData> = { [K in keyof T]: REGL
 
 export interface Renderable {
     draw(): void
+    stats: REGL.CommandStats
+    name: string
     // isPicking: () => boolean
     // isVisible: () => boolean
     // isTransparent: () => boolean
diff --git a/src/mol-gl/renderable/mesh.ts b/src/mol-gl/renderable/mesh.ts
index 52c2244c7..df2dce63f 100644
--- a/src/mol-gl/renderable/mesh.ts
+++ b/src/mol-gl/renderable/mesh.ts
@@ -58,8 +58,11 @@ namespace Mesh {
         return {
             draw: () => {
                 command()
-                console.log(command.stats)
-            }
+            },
+            get stats() {
+                return command.stats
+            },
+            name: 'mesh'
         }
     }
 }
diff --git a/src/mol-gl/renderable/point.ts b/src/mol-gl/renderable/point.ts
index 5bc8aed30..ab3fd0bf4 100644
--- a/src/mol-gl/renderable/point.ts
+++ b/src/mol-gl/renderable/point.ts
@@ -38,6 +38,10 @@ namespace Point {
         })
         return {
             draw: () => command(),
+            get stats() {
+                return command.stats
+            },
+            name: 'point'
         }
     }
 }
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index eef94cf27..472c27f99 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -6,11 +6,14 @@
 
 import REGL = require('regl');
 import * as glContext from './context'
-import { Camera } from './camera'
+import { PerspectiveCamera } from './camera/perspective'
 import { PointRenderable, MeshRenderable, Renderable } from './renderable'
+import Stats from './stats'
 
 import { Vec3, Mat4 } from 'mol-math/linear-algebra'
 import { ValueCell } from 'mol-util';
+import { isNull } from 'util';
+import OrbitControls from './controls/orbit';
 
 let _renderObjectId = 0;
 function getNextId() {
@@ -36,116 +39,177 @@ export function createRenderObject(type: 'mesh' | 'point', data: PointRenderable
     return { id: getNextId(), type, data, uniforms }
 }
 
-export interface Renderer {
+export function createRenderable(regl: REGL.Regl, o: RenderObject) {
+    switch (o.type) {
+        case 'mesh': return MeshRenderable.create(regl, o.data as MeshRenderable.Data, o.uniforms || {})
+        case 'point': return PointRenderable.create(regl, o.data as PointRenderable.Data)
+    }
+}
+
+interface Renderer {
+    camera: PerspectiveCamera
+    controls: any // OrbitControls
+
     add: (o: RenderObject) => void
     remove: (o: RenderObject) => void
     clear: () => void
-    draw: (force: boolean) => void
+    draw: () => void
     frame: () => void
 }
 
-export function createRenderable(regl: REGL.Regl, o: RenderObject) {
-    switch (o.type) {
-        case 'mesh': return MeshRenderable.create(regl, o.data as MeshRenderable.Data, o.uniforms || {})
-        case 'point': return PointRenderable.create(regl, o.data as PointRenderable.Data)
+function resizeCanvas (canvas: HTMLCanvasElement, element: HTMLElement) {
+    let w = window.innerWidth
+    let h = window.innerHeight
+    if (element !== document.body) {
+        let bounds = element.getBoundingClientRect()
+        w = bounds.right - bounds.left
+        h = bounds.bottom - bounds.top
     }
+    canvas.width = window.devicePixelRatio * w
+    canvas.height = window.devicePixelRatio * h
+    Object.assign(canvas.style, { width: w + 'px', height: h + 'px' })
 }
 
-export function createRenderer(container: HTMLDivElement): Renderer {
-    const renderableList: Renderable[] = []
-    const objectIdRenderableMap: { [k: number]: Renderable } = {}
-
-    let regl: REGL.Regl
-    try {
-        regl = glContext.create({
-            container,
-            extensions: [
-                'OES_texture_float',
-                'OES_texture_float_linear',
-                'OES_element_index_uint',
-                'EXT_disjoint_timer_query',
-                'EXT_blend_minmax',
-                'ANGLE_instanced_arrays'
-            ],
-            profile: true
+namespace Renderer {
+    export function fromElement(element: HTMLElement, contexAttributes?: WebGLContextAttributes) {
+        const canvas = document.createElement('canvas')
+        Object.assign(canvas.style, { border: 0, margin: 0, padding: 0, top: 0, left: 0 })
+        element.appendChild(canvas)
+
+        if (element === document.body) {
+            canvas.style.position = 'absolute'
+            Object.assign(element.style, { margin: 0, padding: 0 })
+        }
+
+        function resize () {
+            resizeCanvas(canvas, element)
+        }
+
+        window.addEventListener('resize', resize, false)
+
+        // function onDestroy () {
+        //     window.removeEventListener('resize', resize)
+        //     element.removeChild(canvas)
+        // }
+
+        resize()
+
+        return fromCanvas(canvas, contexAttributes)
+    }
+
+    export function fromCanvas(canvas: HTMLCanvasElement, contexAttributes?: WebGLContextAttributes) {
+        function get (name: 'webgl' | 'experimental-webgl') {
+            try {
+                return canvas.getContext(name, contexAttributes)
+            } catch (e) {
+                return null
+            }
+        }
+        const gl = get('webgl') || get('experimental-webgl')
+        if (isNull(gl)) throw new Error('unable to create webgl context')
+        return create(gl, canvas)
+    }
+
+    export function create(gl: WebGLRenderingContext, element: Element): Renderer {
+        const renderableList: Renderable[] = []
+        const objectIdRenderableMap: { [k: number]: Renderable } = {}
+
+        const camera = PerspectiveCamera.create({
+            near: 0.01,
+            far: 1000,
+            position: Vec3.create(0, 0, 50)
         })
-    } catch (e) {
-        regl = glContext.create({
-            container,
-            extensions: [
-                'OES_texture_float',
-                'OES_texture_float_linear',
-                'OES_element_index_uint',
-                'EXT_blend_minmax',
-                'ANGLE_instanced_arrays'
-            ],
-            profile: true
+
+        const controls = OrbitControls.create(element, {
+            position: Vec3.create(0, 0, 50)
         })
-    }
 
-    const camera = Camera.create(regl, container, {
-        center: Vec3.create(0, 0, 0),
-        near: 0.01,
-        far: 10000,
-        minDistance: 0.01,
-        maxDistance: 10000
-    })
-
-    const baseContext = regl({
-        context: {
-            model: Mat4.identity(),
-            transform: Mat4.setTranslation(Mat4.identity(), Vec3.create(6, 0, 0))
-        },
-        uniforms: {
-            model: regl.context('model' as any),
-            transform: regl.context('transform' as any),
-            'light.position': Vec3.create(0, 0, -100),
-            'light.color': Vec3.create(1.0, 1.0, 1.0),
-            'light.ambient': Vec3.create(0.5, 0.5, 0.5),
-            'light.falloff': 0,
-            'light.radius': 500
+        const extensions = [
+            'OES_texture_float',
+            'OES_texture_float_linear',
+            'OES_element_index_uint',
+            'EXT_blend_minmax',
+            'ANGLE_instanced_arrays'
+        ]
+        if (gl.getExtension('EXT_disjoint_timer_query') !== null) {
+            extensions.push('EXT_disjoint_timer_query')
         }
-    })
 
-    const draw = (force = false) => {
-        camera.update((state: any) => {
-            if (!force && !camera.dirty) return;
-            baseContext(() => {
-                // console.log(ctx)
+        const regl = glContext.create({ gl, extensions, profile: true })
+
+        const baseContext = regl({
+            context: {
+                model: Mat4.identity(),
+                transform: Mat4.identity(),
+                view: camera.view,
+                projection: camera.projection
+            },
+            uniforms: {
+                model: regl.context('model' as any),
+                transform: regl.context('transform' as any),
+                view: regl.context('view' as any),
+                projection: regl.context('projection' as any),
+                'light.position': Vec3.create(0, 0, -100),
+                'light.color': Vec3.create(1.0, 1.0, 1.0),
+                'light.ambient': Vec3.create(0.5, 0.5, 0.5),
+                'light.falloff': 0,
+                'light.radius': 500
+            }
+        })
+
+        const stats = Stats([])
+        let prevTime = regl.now()
+
+        const draw = () => {
+            controls.update()
+            controls.copyInto(camera.position, camera.direction, camera.up)
+            camera.update()
+            baseContext(state => {
                 regl.clear({ color: [0, 0, 0, 1] })
                 // TODO painters sort, filter visible, filter picking, visibility culling?
                 renderableList.forEach(r => {
                     r.draw()
                 })
+                stats.update(state.time - prevTime)
+                prevTime = state.time
             })
-        }, undefined)
-    }
+        }
 
-    return {
-        add: (o: RenderObject) => {
-            const renderable = createRenderable(regl, o)
-            renderableList.push(renderable)
-            objectIdRenderableMap[o.id] = renderable
-        },
-        remove: (o: RenderObject) => {
-            if (o.id in objectIdRenderableMap) {
-                // TODO
-                // objectIdRenderableMap[o.id].destroy()
-                delete objectIdRenderableMap[o.id]
-            }
-        },
-        clear: () => {
-            for (const id in objectIdRenderableMap) {
-                // TODO
-                // objectIdRenderableMap[id].destroy()
-                delete objectIdRenderableMap[id]
+        // TODO animate, draw, requestDraw
+        return {
+            camera,
+            controls,
+
+            add: (o: RenderObject) => {
+                const renderable = createRenderable(regl, o)
+                renderableList.push(renderable)
+                objectIdRenderableMap[o.id] = renderable
+                stats.add(renderable)
+                draw()
+            },
+            remove: (o: RenderObject) => {
+                if (o.id in objectIdRenderableMap) {
+                    // TODO
+                    // objectIdRenderableMap[o.id].destroy()
+                    delete objectIdRenderableMap[o.id]
+                    draw()
+                }
+            },
+            clear: () => {
+                for (const id in objectIdRenderableMap) {
+                    // TODO
+                    // objectIdRenderableMap[id].destroy()
+                    delete objectIdRenderableMap[id]
+                }
+                renderableList.length = 0
+                draw()
+            },
+            draw,
+            frame: () => {
+                regl.frame((ctx) => draw())
             }
-            renderableList.length = 0
-            camera.dirty = true
-        },
-        draw,
-        frame: () => {
-            regl.frame((ctx) => draw())
         }
     }
-}
\ No newline at end of file
+}
+
+export default Renderer
\ No newline at end of file
diff --git a/src/mol-gl/stats.ts b/src/mol-gl/stats.ts
new file mode 100644
index 000000000..206e58525
--- /dev/null
+++ b/src/mol-gl/stats.ts
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Renderable } from './renderable';
+
+export default function createStats (renderables: Renderable[]) {
+    const prevGpuTimes: number[] = []
+    for (let i = 0; i < renderables.length; i++) {
+        prevGpuTimes[i] = 0
+    }
+
+    let frameTimeCount = 0
+    let totalTime = 1.1
+    let N = 50
+
+    const totalFrameTime: number[] = []
+    const avgFrameTime: number[] = []
+    for (let i = 0; i < renderables.length; ++i) {
+        totalFrameTime[i] = 0.0
+        avgFrameTime[i] = 0.0
+    }
+
+    return {
+        add: (renderable: Renderable) => {
+            renderables.push(renderable)
+            prevGpuTimes.push(0)
+            totalFrameTime.push(0)
+            avgFrameTime.push(0)
+        },
+        update: (deltaTime: number) => {
+            totalTime += deltaTime
+            if (totalTime > 1.0) {
+                totalTime = 0
+
+                // for (let i = 0; i < renderables.length; i++) {
+                //     const renderable = renderables[i]
+                //     const str = `${renderable.name}: ${Math.round(100.0 * avgFrameTime[i]) / 100.0}ms`
+                //     console.log(str)
+                // }
+
+                const sumFrameTime = avgFrameTime.reduce((x: number, y: number) => x + y, 0)
+                const str = `${Math.round(100.0 * sumFrameTime) / 100.0}ms`
+                console.log(str)
+            }
+
+            frameTimeCount++
+
+            for (let i = 0; i < renderables.length; i++) {
+                const renderable = renderables[i]
+                const frameTime = renderable.stats.gpuTime - prevGpuTimes[i]
+                totalFrameTime[i] += frameTime
+
+                if (frameTimeCount === N) {
+                    avgFrameTime[i] = totalFrameTime[i] / N
+                    totalFrameTime[i] = 0.0
+                }
+
+                prevGpuTimes[i] = renderable.stats.gpuTime
+            }
+
+            if (frameTimeCount === N) frameTimeCount = 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-math/linear-algebra/3d.ts b/src/mol-math/linear-algebra/3d.ts
index 3a0f4219e..3a98d5ba7 100644
--- a/src/mol-math/linear-algebra/3d.ts
+++ b/src/mol-math/linear-algebra/3d.ts
@@ -19,8 +19,10 @@
 
 import Mat4 from './3d/mat4'
 import Mat3 from './3d/mat3'
+import Vec2 from './3d/vec2'
 import Vec3 from './3d/vec3'
 import Vec4 from './3d/vec4'
 import Quat from './3d/quat'
+import { EPSILON } from './3d/common'
 
-export { Mat4, Mat3, Vec3, Vec4, Quat }
\ No newline at end of file
+export { Mat4, Mat3, Vec2, Vec3, Vec4, Quat, EPSILON }
\ No newline at end of file
diff --git a/src/mol-math/linear-algebra/3d/quat.ts b/src/mol-math/linear-algebra/3d/quat.ts
index a8253363c..a758c1759 100644
--- a/src/mol-math/linear-algebra/3d/quat.ts
+++ b/src/mol-math/linear-algebra/3d/quat.ts
@@ -17,8 +17,14 @@
  * furnished to do so, subject to the following conditions:
  */
 
+/*
+ * Quat.fromUnitVec3 has been modified from https://github.com/Jam3/quat-from-unit-vec3,
+ * copyright (c) 2015 Jam3. MIT License
+ */
+
 import Mat3 from './mat3';
 import Vec3 from './vec3';
+import { EPSILON } from './common';
 
 interface Quat extends Array<number> { [d: number]: number, '@type': 'quat', length: 4 }
 
@@ -258,6 +264,34 @@ namespace Quat {
         return out;
     }
 
+    const fromUnitVec3Temp = Vec3.zero()
+    /** Quaternion from two normalized unit vectors. */
+    export function fromUnitVec3 (out: Quat, a: Vec3, b: Vec3) {
+        // assumes a and b are normalized
+        let r = Vec3.dot(a, b) + 1
+        if (r < EPSILON.Value) {
+            // If u and v are exactly opposite, rotate 180 degrees
+            // around an arbitrary orthogonal axis. Axis normalisation
+            // can happen later, when we normalise the quaternion.
+            r = 0
+            if (Math.abs(a[0]) > Math.abs(a[2])) {
+                Vec3.set(fromUnitVec3Temp, -a[1], a[0], 0)
+            } else {
+                Vec3.set(fromUnitVec3Temp, 0, -a[2], a[1])
+            }
+        } else {
+            // Otherwise, build quaternion the standard way.
+            Vec3.cross(fromUnitVec3Temp, a, b)
+        }
+
+        out[0] = fromUnitVec3Temp[0]
+        out[1] = fromUnitVec3Temp[1]
+        out[2] = fromUnitVec3Temp[2]
+        out[3] = r
+        normalize(out, out)
+        return out
+    }
+
     export function clone(a: Quat) {
         const out = zero();
         out[0] = a[0];
diff --git a/src/mol-math/linear-algebra/3d/vec2.ts b/src/mol-math/linear-algebra/3d/vec2.ts
new file mode 100644
index 000000000..200d3ea6e
--- /dev/null
+++ b/src/mol-math/linear-algebra/3d/vec2.ts
@@ -0,0 +1,85 @@
+/**
+ * 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/toji/gl-matrix/,
+ * copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ */
+
+interface Vec2 extends Array<number> { [d: number]: number, '@type': 'vec2', length: 2 }
+
+namespace Vec2 {
+    export function zero(): Vec2 {
+        // force double backing array by 0.1.
+        const ret = [0.1, 0];
+        ret[0] = 0.0;
+        return ret as any;
+    }
+
+    export function clone(a: Vec2) {
+        const out = zero();
+        out[0] = a[0];
+        out[1] = a[1];
+        return out;
+    }
+
+    export function create(x: number, y: number) {
+        const out = zero();
+        out[0] = x;
+        out[1] = y;
+        return out;
+    }
+
+    export function toArray(a: Vec2, out: Helpers.NumberArray, offset: number) {
+        out[offset + 0] = a[0];
+        out[offset + 1] = a[1];
+    }
+
+    export function fromArray(a: Vec2, array: Helpers.NumberArray, offset: number) {
+        a[0] = array[offset + 0]
+        a[1] = array[offset + 1]
+        return a
+    }
+
+    export function copy(out: Vec2, a: Vec2) {
+        out[0] = a[0];
+        out[1] = a[1];
+        return out;
+    }
+
+    export function set(out: Vec2, x: number, y: number) {
+        out[0] = x;
+        out[1] = y;
+        return out;
+    }
+
+    export function add(out: Vec2, a: Vec2, b: Vec2) {
+        out[0] = a[0] + b[0];
+        out[1] = a[1] + b[1];
+        return out;
+    }
+
+    export function distance(a: Vec2, b: Vec2) {
+        const x = b[0] - a[0],
+            y = b[1] - a[1];
+        return Math.sqrt(x * x + y * y);
+    }
+
+    export function squaredDistance(a: Vec2, b: Vec2) {
+        const x = b[0] - a[0],
+            y = b[1] - a[1];
+        return x * x + y * y;
+    }
+}
+
+export default Vec2
\ No newline at end of file
diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts
index bbf28f3ac..aa5357d14 100644
--- a/src/mol-math/linear-algebra/3d/vec3.ts
+++ b/src/mol-math/linear-algebra/3d/vec3.ts
@@ -18,6 +18,7 @@
  */
 
 import Mat4 from './mat4';
+import { Quat } from '../3d';
 
 interface Vec3 extends Array<number> { [d: number]: number, '@type': 'vec3', length: 3 }
 
@@ -238,6 +239,26 @@ namespace Vec3 {
         return out;
     }
 
+    /** Transforms the vec3 with a quat */
+    export function transformQuat(out: Vec3, a: Vec3, q: Quat) {
+        // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations
+
+        const x = a[0], y = a[1], z = a[2];
+        const qx = q[0], qy = q[1], qz = q[2], qw = q[3];
+
+        // calculate quat * vec
+        const ix = qw * x + qy * z - qz * y;
+        const iy = qw * y + qz * x - qx * z;
+        const iz = qw * z + qx * y - qy * x;
+        const iw = -qx * x - qy * y - qz * z;
+
+        // calculate result * inverse quat
+        out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+        out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+        out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+        return out;
+    }
+
     const angleTempA = zero(), angleTempB = zero();
     export function angle(a: Vec3, b: Vec3) {
         copy(angleTempA, a);
@@ -265,6 +286,10 @@ namespace Vec3 {
         const axis = cross(rotTemp, a, b);
         return Mat4.fromRotation(mat, by, axis);
     }
+
+    export function isZero(v: Vec3) {
+        return v[0] === 0 && v[1] === 0 && v[2] === 0
+    }
 }
 
 export default Vec3
\ No newline at end of file
diff --git a/src/mol-math/linear-algebra/3d/vec4.ts b/src/mol-math/linear-algebra/3d/vec4.ts
index 582058a6a..f481cf99d 100644
--- a/src/mol-math/linear-algebra/3d/vec4.ts
+++ b/src/mol-math/linear-algebra/3d/vec4.ts
@@ -17,7 +17,6 @@
  * furnished to do so, subject to the following conditions:
  */
 
-import Quat from './quat';
 import Mat4 from './mat4';
 
 interface Vec4 extends Array<number> { [d: number]: number, '@type': 'vec4', length: 4 }
@@ -79,7 +78,7 @@ namespace Vec4 {
         return out;
     }
 
-    export function add(out: Quat, a: Quat, b: Quat) {
+    export function add(out: Vec4, a: Vec4, b: Vec4) {
         out[0] = a[0] + b[0];
         out[1] = a[1] + b[1];
         out[2] = a[2] + b[2];
@@ -119,7 +118,7 @@ namespace Vec4 {
         return x * x + y * y + z * z + w * w;
     }
 
-    export function transform(out: Vec4, a: Vec4, m: Mat4) {
+    export function transformMat4(out: Vec4, a: Vec4, m: Mat4) {
         const x = a[0], y = a[1], z = a[2], w = a[3];
         out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
         out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
diff --git a/src/mol-util/input/event-offset.ts b/src/mol-util/input/event-offset.ts
new file mode 100644
index 000000000..12a14c21b
--- /dev/null
+++ b/src/mol-util/input/event-offset.ts
@@ -0,0 +1,31 @@
+/**
+ * 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/mattdesl/mouse-event-offset,
+ * copyright (c) 2014 Matt DesLauriers. MIT License
+ */
+
+import { Vec2 } from 'mol-math/linear-algebra'
+
+const rootPosition = { left: 0, top: 0 }
+
+export function eventOffset (out: Vec2, ev: MouseEvent | Touch, target: Element) {
+    const cx = ev.clientX || 0
+    const cy = ev.clientY || 0
+    const rect = getBoundingClientOffset(target)
+    out[0] = cx - rect.left
+    out[1] = cy - rect.top
+    return out
+}
+
+function getBoundingClientOffset (element: Element | Window | Document) {
+    if (element !== window && element !== document && element !== document.body) {
+        return rootPosition
+    } else {
+        return (element as Element).getBoundingClientRect()
+    }
+}
\ No newline at end of file
diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts
new file mode 100644
index 000000000..73825edd9
--- /dev/null
+++ b/src/mol-util/input/input-observer.ts
@@ -0,0 +1,273 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Subject } from 'rxjs';
+
+import { Vec2 } from 'mol-math/linear-algebra';
+
+import MouseWheel from './mouse-wheel'
+import TouchPinch from './touch-pinch'
+import { eventOffset } from './event-offset'
+
+export function getButtons(event: MouseEvent | Touch) {
+    if (typeof event === 'object') {
+        if ('buttons' in event) {
+            return event.buttons
+        } else if ('which' in event) {
+            const b = (event as any).which  // 'any' to support older browsers
+            if (b === 2) {
+                return 4
+            } else if (b === 3) {
+                return 2
+            } else if (b > 0) {
+                return 1<<(b-1)
+            }
+        } else if ('button' in event) {
+            const b = (event as any).button  // 'any' to support older browsers
+            if (b === 1) {
+                return 4
+            } else if (b === 2) {
+                return 2
+            } else if (b >= 0) {
+                return 1<<b
+            }
+        }
+    }
+    return 0
+}
+
+export const DefaultInputObserverProps = {
+    parent: window as Window | Element,
+    noScroll: true
+}
+export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
+
+export type MouseModifiers = {
+    shift: boolean,
+    alt: boolean,
+    control: boolean,
+    meta: boolean
+}
+
+interface InputObserver {
+    noScroll: boolean
+    isDragging: () => boolean
+    isPinching: () => boolean
+
+    drag: Subject<{ dx: number, dy: number, buttons: number, modifiers: MouseModifiers }>,
+    wheel: Subject<{ dx: number, dy: number, dz: number, event: WheelEvent }>,
+    pinch: Subject<number>,
+    // click: Subject<{ x: number, y: number, buttons: number, modifiers: MouseModifiers }>,
+
+    dispose: () => void
+}
+
+namespace InputObserver {
+    export function create (element: Element, props: InputObserverProps = {}): InputObserver {
+        const { parent, noScroll } = { ...DefaultInputObserverProps, ...props }
+
+        const mouseStart = Vec2.zero()
+        const tmp = Vec2.zero()
+        const tmp2 = Vec2.zero()
+        const modifiers: MouseModifiers = {
+            shift: false,
+            alt: false,
+            control: false,
+            meta: false
+        }
+
+        const touchPinch = TouchPinch.create(element)
+        const mouseWheel = MouseWheel.create(element, noScroll)
+
+        let dragging = false
+        let disposed = false
+        let buttons = 0
+
+        const drag = new Subject<{ dx: number, dy: number, buttons: number, modifiers: MouseModifiers }>()
+        const wheel = mouseWheel.wheel
+        const pinch = new Subject<number>()
+
+        attach()
+
+        return {
+            get noScroll () { return mouseWheel.noScroll },
+            set noScroll (value: boolean) { mouseWheel.noScroll = value },
+            isDragging: () => dragging,
+            isPinching,
+
+            drag,
+            wheel,
+            pinch,
+
+            dispose
+        }
+
+        function attach () {
+            element.addEventListener('mousedown', onInputDown as any, false)
+
+            // for dragging to work outside canvas bounds,
+            // mouse move/up events have to be added to parent, i.e. window
+            parent.addEventListener('mousemove', onInputMove as any, false)
+            parent.addEventListener('mouseup', onInputUp as any, false)
+
+            // don't allow simulated mouse events
+            element.addEventListener('touchstart', preventDefault as any, false)
+
+            element.addEventListener('touchmove', onTouchMove as any, false)
+
+            touchPinch.place.subscribe(onPinchPlace)
+            touchPinch.lift.subscribe(onPinchLift)
+            touchPinch.change.subscribe(onPinchChange)
+
+            element.addEventListener('blur', handleBlur)
+            element.addEventListener('keyup', handleMods as EventListener)
+            element.addEventListener('keydown', handleMods as EventListener)
+            element.addEventListener('keypress', handleMods as EventListener)
+
+            if (!(element instanceof Window)) {
+                window.addEventListener('blur', handleBlur)
+                window.addEventListener('keyup', handleMods)
+                window.addEventListener('keydown', handleMods)
+                window.addEventListener('keypress', handleMods)
+            }
+        }
+
+        function dispose () {
+            if (disposed) return
+            disposed = true
+
+            mouseWheel.dispose()
+            touchPinch.dispose()
+
+            element.removeEventListener('touchstart', preventDefault as any, false)
+            element.removeEventListener('touchmove', onTouchMove as any, false)
+
+            element.removeEventListener('mousedown', onInputDown as any, false)
+
+            parent.removeEventListener('mousemove', onInputMove as any, false)
+            parent.removeEventListener('mouseup', onInputUp as any, false)
+
+            element.removeEventListener('blur', handleBlur)
+            element.removeEventListener('keyup', handleMods as EventListener)
+            element.removeEventListener('keydown', handleMods as EventListener)
+            element.removeEventListener('keypress', handleMods as EventListener)
+
+            if (!(element instanceof Window)) {
+                window.removeEventListener('blur', handleBlur)
+                window.removeEventListener('keyup', handleMods)
+                window.removeEventListener('keydown', handleMods)
+                window.removeEventListener('keypress', handleMods)
+            }
+        }
+
+        function preventDefault (ev: Event | Touch) {
+            if ('preventDefault' in ev) ev.preventDefault()
+        }
+
+        function handleBlur () {
+            if (buttons || modifiers.shift || modifiers.alt || modifiers.meta || modifiers.control) {
+                buttons = 0
+                modifiers.shift = modifiers.alt = modifiers.control = modifiers.meta = false
+            }
+        }
+
+        function handleMods (event: MouseEvent | KeyboardEvent) {
+            if ('altKey' in event) modifiers.alt = !!event.altKey
+            if ('shiftKey' in event) modifiers.shift = !!event.shiftKey
+            if ('ctrlKey' in event) modifiers.control = !!event.ctrlKey
+            if ('metaKey' in event) modifiers.meta = !!event.metaKey
+        }
+
+        function onTouchMove (ev: TouchEvent) {
+            if (!dragging || isPinching()) return
+
+            // find currently active finger
+            for (let i = 0; i < ev.changedTouches.length; i++) {
+                const changed = ev.changedTouches[i]
+                const idx = touchPinch.indexOfTouch(changed)
+                if (idx !== -1) {
+                    onInputMove(changed)
+                    break
+                }
+            }
+        }
+
+        function onPinchPlace ({ newTouch, oldTouch }: { newTouch?: Touch, oldTouch?: Touch }) {
+            dragging = !isPinching()
+            if (dragging) {
+                const firstFinger = oldTouch || newTouch
+                if (firstFinger) onInputDown(firstFinger)
+            }
+        }
+
+        function onPinchLift ({ removed, otherTouch }: { removed?: Touch, otherTouch?: Touch }) {
+            // if either finger is down, consider it dragging
+            const sum = touchPinch.fingers.reduce((sum, item) => sum + (item ? 1 : 0), 0)
+            dragging = sum >= 1
+
+            if (dragging && otherTouch) {
+                eventOffset(mouseStart, otherTouch, element)
+            }
+        }
+
+        function isPinching () {
+            return touchPinch.pinching
+        }
+
+        function onPinchChange ({ currentDistance, lastDistance }: { currentDistance: number, lastDistance: number }) {
+            pinch.next(currentDistance - lastDistance)
+        }
+
+        function onInputDown (ev: MouseEvent | Touch) {
+            preventDefault(ev)
+            eventOffset(mouseStart, ev, element)
+            if (insideBounds(mouseStart)) {
+                dragging = true
+            }
+        }
+
+        function onInputUp () {
+            dragging = false
+        }
+
+        function onInputMove (ev: MouseEvent | Touch) {
+            buttons = getButtons(ev)
+            const end = eventOffset(tmp, ev, element)
+            if (pinch && isPinching()) {
+                Vec2.copy(mouseStart, end)
+                return
+            }
+            if (!dragging) return
+            const rect = getClientSize(tmp2)
+            const dx = (end[0] - mouseStart[0]) / rect[0]
+            const dy = (end[1] - mouseStart[1]) / rect[1]
+            drag.next({ dx, dy, buttons, modifiers })
+            mouseStart[0] = end[0]
+            mouseStart[1] = end[1]
+        }
+
+        function insideBounds (pos: Vec2) {
+            if (element instanceof Window || element instanceof Document || element === document.body) {
+                return true
+            } else {
+                const rect = element.getBoundingClientRect()
+                return pos[0] >= 0 && pos[1] >= 0 && pos[0] < rect.width && pos[1] < rect.height
+            }
+        }
+
+        function getClientSize (out: Vec2) {
+            let source = element
+            if (source instanceof Window || source instanceof Document || source === document.body) {
+                source = document.documentElement
+            }
+            out[0] = source.clientWidth
+            out[1] = source.clientHeight
+            return out
+        }
+    }
+}
+
+export default InputObserver
\ No newline at end of file
diff --git a/src/mol-util/input/mouse-change.ts b/src/mol-util/input/mouse-change.ts
new file mode 100644
index 000000000..5dc41eaf9
--- /dev/null
+++ b/src/mol-util/input/mouse-change.ts
@@ -0,0 +1,184 @@
+/**
+ * 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/mikolalysenko/mouse-change,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import { Subject } from 'rxjs';
+
+import * as mouse from './mouse-event'
+
+
+
+interface MouseChange {
+    change: Subject<number>,
+    dispose: () => void
+}
+
+namespace MouseChange {
+    export type Modifiers = {
+        shift: boolean,
+        alt: boolean,
+        control: boolean,
+        meta: boolean
+    }
+    export type Info = {
+        buttons: number,
+        x: number,
+        y: number,
+        modifiers: Modifiers
+    }
+
+    export function create(element: Element) {
+        let buttonState = 0
+        let x = 0
+        let y = 0
+        const mods: Modifiers = {
+            shift: false,
+            alt: false,
+            control: false,
+            meta: false
+        }
+        let attached = false
+
+        const change = new Subject<Info>()
+
+        // Attach listeners
+        attachListeners()
+
+        return {
+            change,
+            dispose
+        }
+
+        function updateMods (event: MouseEvent | KeyboardEvent) {
+            let changed = false
+            if ('altKey' in event) {
+                changed = changed || event.altKey !== mods.alt
+                mods.alt = !!event.altKey
+            }
+            if ('shiftKey' in event) {
+                changed = changed || event.shiftKey !== mods.shift
+                mods.shift = !!event.shiftKey
+            }
+            if ('ctrlKey' in event) {
+                changed = changed || event.ctrlKey !== mods.control
+                mods.control = !!event.ctrlKey
+            }
+            if ('metaKey' in event) {
+                changed = changed || event.metaKey !== mods.meta
+                mods.meta = !!event.metaKey
+            }
+            return changed
+        }
+
+        function handleEvent (nextButtons: number, event: MouseEvent) {
+            const nextX = mouse.x(event)
+            const nextY = mouse.y(event)
+            if ('buttons' in event) {
+                nextButtons = event.buttons | 0
+            }
+            if (nextButtons !== buttonState || nextX !== x || nextY !== y || updateMods(event) ) {
+                buttonState = nextButtons | 0
+                x = nextX || 0
+                y = nextY || 0
+
+                change.next({ buttons: buttonState, x, y, modifiers: mods })
+            }
+        }
+
+        function clearState (event: MouseEvent) {
+            handleEvent(0, event)
+        }
+
+        function handleBlur () {
+            if (buttonState || x || y || mods.shift || mods.alt || mods.meta || mods.control) {
+                x = y = 0
+                buttonState = 0
+                mods.shift = mods.alt = mods.control = mods.meta = false
+                change.next({ buttons: 0, x: 0, y: 0, modifiers: mods })
+            }
+        }
+
+        function handleMods (event: MouseEvent | KeyboardEvent) {
+            if (updateMods(event)) {
+                change.next({ buttons: buttonState, x, y, modifiers: mods })
+            }
+        }
+
+        function handleMouseMove (event: MouseEvent) {
+            if (mouse.buttons(event) === 0) {
+                handleEvent(0, event)
+            } else {
+                handleEvent(buttonState, event)
+            }
+        }
+
+        function handleMouseDown (event: MouseEvent) {
+            handleEvent(buttonState | mouse.buttons(event), event)
+        }
+
+        function handleMouseUp (event: MouseEvent) {
+            handleEvent(buttonState & ~mouse.buttons(event), event)
+        }
+
+        function attachListeners () {
+            if (attached) return
+            attached = true
+
+            element.addEventListener('mousemove', handleMouseMove as EventListener)
+            element.addEventListener('mousedown', handleMouseDown as EventListener)
+            element.addEventListener('mouseup', handleMouseUp as EventListener)
+
+            element.addEventListener('mouseleave', clearState as EventListener)
+            element.addEventListener('mouseenter', clearState as EventListener)
+            element.addEventListener('mouseout', clearState as EventListener)
+            element.addEventListener('mouseover', clearState as EventListener)
+
+            element.addEventListener('blur', handleBlur)
+            element.addEventListener('keyup', handleMods as EventListener)
+            element.addEventListener('keydown', handleMods as EventListener)
+            element.addEventListener('keypress', handleMods as EventListener)
+
+            if (!(element instanceof Window)) {
+                window.addEventListener('blur', handleBlur)
+                window.addEventListener('keyup', handleMods)
+                window.addEventListener('keydown', handleMods)
+                window.addEventListener('keypress', handleMods)
+            }
+        }
+
+        function dispose () {
+            if (!attached) return
+            attached = false
+
+            element.removeEventListener('mousemove', handleMouseMove as EventListener)
+            element.removeEventListener('mousedown', handleMouseDown as EventListener)
+            element.removeEventListener('mouseup', handleMouseUp as EventListener)
+
+            element.removeEventListener('mouseleave', clearState as EventListener)
+            element.removeEventListener('mouseenter', clearState as EventListener)
+            element.removeEventListener('mouseout', clearState as EventListener)
+            element.removeEventListener('mouseover', clearState as EventListener)
+
+            element.removeEventListener('blur', handleBlur)
+            element.removeEventListener('keyup', handleMods as EventListener)
+            element.removeEventListener('keydown', handleMods as EventListener)
+            element.removeEventListener('keypress', handleMods as EventListener)
+
+            if (!(element instanceof Window)) {
+                window.removeEventListener('blur', handleBlur)
+                window.removeEventListener('keyup', handleMods)
+                window.removeEventListener('keydown', handleMods)
+                window.removeEventListener('keypress', handleMods)
+            }
+        }
+    }
+}
+
+export default MouseChange
\ No newline at end of file
diff --git a/src/mol-util/mouse-event.ts b/src/mol-util/input/mouse-event.ts
similarity index 96%
rename from src/mol-util/mouse-event.ts
rename to src/mol-util/input/mouse-event.ts
index 0f38bc7cb..7c4966b82 100644
--- a/src/mol-util/mouse-event.ts
+++ b/src/mol-util/input/mouse-event.ts
@@ -37,7 +37,7 @@ export function buttons(event: MouseEvent) {
 }
 
 export function element(event: MouseEvent) {
-    return event.target as Element || event.srcElement || window
+    return event.target as Element
 }
 
 export function x(event: MouseEvent) {
diff --git a/src/mol-util/input/mouse-wheel.ts b/src/mol-util/input/mouse-wheel.ts
new file mode 100644
index 000000000..b56bd320b
--- /dev/null
+++ b/src/mol-util/input/mouse-wheel.ts
@@ -0,0 +1,67 @@
+/**
+ * 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/mikolalysenko/mouse-wheel,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import { Subject } from 'rxjs';
+import toPixels from '../to-pixels'
+
+interface MouseWheel {
+    noScroll: boolean
+    wheel: Subject<{ dx: number, dy: number, dz: number, event: WheelEvent }>
+    dispose: () => void
+}
+
+namespace MouseWheel {
+    export function create(element: Element, noScroll = true): MouseWheel {
+        const lineHeight = toPixels('ex', element)
+        let disposed = false
+        const wheel = new Subject<{ dx: number, dy: number, dz: number, event: WheelEvent }>()
+
+        element.addEventListener('wheel', listener)
+
+        return {
+            get noScroll () { return noScroll },
+            set noScroll (value: boolean) { noScroll = value },
+
+            wheel,
+            dispose
+        }
+
+        function listener(event: MouseWheelEvent) {
+            if (noScroll) {
+                event.preventDefault()
+            }
+            const mode = event.deltaMode
+            let dx = event.deltaX || 0
+            let dy = event.deltaY || 0
+            let dz = event.deltaZ || 0
+            let scale = 1
+            switch (mode) {
+                case 1: scale = lineHeight; break
+                case 2: scale = window.innerHeight; break
+            }
+            dx *= scale
+            dy *= scale
+            dz *= scale
+            if (dx || dy || dz) {
+                wheel.next({ dx, dy, dz, event })
+            }
+        }
+
+        function dispose() {
+            if (disposed) return
+            disposed = true
+            element.removeEventListener('wheel', listener)
+            wheel.unsubscribe()
+        }
+    }
+}
+
+export default MouseWheel
\ No newline at end of file
diff --git a/src/mol-util/input/touch-pinch.ts b/src/mol-util/input/touch-pinch.ts
new file mode 100644
index 000000000..963a84ef7
--- /dev/null
+++ b/src/mol-util/input/touch-pinch.ts
@@ -0,0 +1,188 @@
+/**
+ * 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 (use TypeScript, RxJS) from https://github.com/Jam3/touch-pinch,
+ * copyright (c) 2014 Matt DesLauriers. MIT License
+ */
+
+import { Subject } from 'rxjs';
+
+import { Vec2 } from 'mol-math/linear-algebra';
+import { eventOffset } from './event-offset'
+
+interface Finger {
+    position: Vec2,
+    touch?: Touch
+}
+
+function Finger (): Finger {
+    return {
+        position: Vec2.zero(),
+        touch: undefined
+    }
+}
+
+interface TouchPinch {
+    pinching: boolean
+    fingers: (Finger|undefined)[]
+    indexOfTouch: (touch: Touch) => number
+
+    start: Subject<number>
+    end: Subject<void>
+    place: Subject<{ newTouch?: Touch, oldTouch?: Touch}>
+    change: Subject<{ currentDistance: number, lastDistance: number }>
+    lift: Subject<{ removed: Touch, otherTouch?: Touch }>
+
+    dispose: () => void
+}
+
+namespace TouchPinch {
+    export function create (target: Element): TouchPinch {
+        const fingers: (Finger|undefined)[] = []
+        let activeCount = 0
+
+        let lastDistance = 0
+        let ended = false
+        let disposed = false
+
+        const start = new Subject<number>()
+        const end = new Subject<void>()
+        const place = new Subject<{ newTouch?: Touch, oldTouch?: Touch}>()
+        const change = new Subject<{ currentDistance: number, lastDistance: number }>()
+        const lift = new Subject<{ removed: Touch, otherTouch?: Touch }>()
+
+        target.addEventListener('touchstart', onTouchStart as any, false)
+        target.addEventListener('touchmove', onTouchMove as any, false)
+        target.addEventListener('touchend', onTouchRemoved as any, false)
+        target.addEventListener('touchcancel', onTouchRemoved as any, false)
+
+        return {
+            get pinching() { return activeCount === 2 },
+            fingers,
+            indexOfTouch,
+
+            start,
+            end,
+            place,
+            change,
+            lift,
+
+            dispose
+        }
+
+        function indexOfTouch (touch: Touch) {
+            const id = touch.identifier
+            for (let i = 0; i < fingers.length; i++) {
+                const finger = fingers[i]
+                if (finger && finger.touch && finger.touch.identifier === id) {
+                    return i
+                }
+            }
+            return -1
+        }
+
+        function dispose () {
+            if (disposed) return
+            disposed = true
+            activeCount = 0
+            fingers[0] = undefined
+            fingers[1] = undefined
+            lastDistance = 0
+            ended = false
+            target.removeEventListener('touchstart', onTouchStart as any, false)
+            target.removeEventListener('touchmove', onTouchMove as any, false)
+            target.removeEventListener('touchend', onTouchRemoved as any, false)
+            target.removeEventListener('touchcancel', onTouchRemoved as any, false)
+        }
+
+        function onTouchStart (ev: TouchEvent) {
+            for (let i = 0; i < ev.changedTouches.length; i++) {
+                const newTouch = ev.changedTouches[i]
+                const idx = indexOfTouch(newTouch)
+
+                if (idx === -1 && activeCount < 2) {
+                    const first = activeCount === 0
+
+                    // newest and previous finger (previous may be undefined)
+                    const newIndex = fingers[0] ? 1 : 0
+                    const oldIndex = fingers[0] ? 0 : 1
+                    const newFinger = Finger()
+
+                    // add to stack
+                    fingers[newIndex] = newFinger
+                    activeCount++
+
+                    // update touch event & position
+                    newFinger.touch = newTouch
+                    eventOffset(newFinger.position, newTouch, target)
+
+                    const finger = fingers[oldIndex]
+                    const oldTouch = finger ? finger.touch : undefined
+                    place.next({ newTouch, oldTouch })
+
+                    if (!first) {
+                        const initialDistance = computeDistance()
+                        ended = false
+                        start.next(initialDistance)
+                        lastDistance = initialDistance
+                    }
+                }
+            }
+        }
+
+        function onTouchMove (ev: TouchEvent) {
+            let changed = false
+            for (let i = 0; i < ev.changedTouches.length; i++) {
+                const movedTouch = ev.changedTouches[i]
+                const idx = indexOfTouch(movedTouch)
+                if (idx !== -1) {
+                    const finger = fingers[idx]
+                    if (finger) {
+                        changed = true
+                        finger.touch = movedTouch // avoid caching touches
+                        eventOffset(finger.position, movedTouch, target)
+                    }
+                }
+                }
+
+                if (activeCount === 2 && changed) {
+                const currentDistance = computeDistance()
+                change.next({ currentDistance, lastDistance })
+                lastDistance = currentDistance
+            }
+        }
+
+        function onTouchRemoved (ev: TouchEvent) {
+            for (let i = 0; i < ev.changedTouches.length; i++) {
+                const removed = ev.changedTouches[i]
+                const idx = indexOfTouch(removed)
+                if (idx !== -1) {
+                    fingers[idx] = undefined
+                    activeCount--
+                    const otherIdx = idx === 0 ? 1 : 0
+                    const finger = fingers[otherIdx]
+                    if (finger) {
+                        const otherTouch = finger ? finger.touch : undefined
+                        lift.next({ removed, otherTouch })
+                    }
+                }
+            }
+
+            if (!ended && activeCount !== 2) {
+                ended = true
+                end.next()
+            }
+        }
+
+        function computeDistance () {
+            const [ f1, f2 ] = fingers
+            return (f1 && f2) ? Vec2.distance(f1.position, f2.position) : 0
+        }
+    }
+}
+
+export default TouchPinch
\ No newline at end of file
diff --git a/src/mol-util/mouse-change.ts b/src/mol-util/mouse-change.ts
deleted file mode 100644
index 96d7f87a1..000000000
--- a/src/mol-util/mouse-change.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-/**
- * 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/mikolalysenko/mouse-change,
- * copyright (c) 2015 Mikola Lysenko. MIT License
- */
-
-import * as mouse from './mouse-event'
-
-export type MouseModifiers = {
-    shift: boolean,
-    alt: boolean,
-    control: boolean,
-    meta: boolean
-}
-export type MouseChangeCallback = (buttonState: number, x: number, y: number, mods: MouseModifiers) => void
-
-export default function mouseListen (element: Element, callback: MouseChangeCallback) {
-    let buttonState = 0
-    let x = 0
-    let y = 0
-    const mods: MouseModifiers = {
-        shift: false,
-        alt: false,
-        control: false,
-        meta: false
-    }
-    let attached = false
-
-    function updateMods (event: MouseEvent | KeyboardEvent) {
-        let changed = false
-        if ('altKey' in event) {
-            changed = changed || event.altKey !== mods.alt
-            mods.alt = !!event.altKey
-        }
-        if ('shiftKey' in event) {
-            changed = changed || event.shiftKey !== mods.shift
-            mods.shift = !!event.shiftKey
-        }
-        if ('ctrlKey' in event) {
-            changed = changed || event.ctrlKey !== mods.control
-            mods.control = !!event.ctrlKey
-        }
-        if ('metaKey' in event) {
-            changed = changed || event.metaKey !== mods.meta
-            mods.meta = !!event.metaKey
-        }
-        return changed
-    }
-
-    function handleEvent (nextButtons: number, event: MouseEvent) {
-        const nextX = mouse.x(event)
-        const nextY = mouse.y(event)
-        if ('buttons' in event) {
-            nextButtons = event.buttons | 0
-        }
-        if (nextButtons !== buttonState || nextX !== x || nextY !== y || updateMods(event) ) {
-            buttonState = nextButtons | 0
-            x = nextX || 0
-            y = nextY || 0
-            callback && callback(buttonState, x, y, mods)
-        }
-    }
-
-    function clearState (event: MouseEvent) {
-        handleEvent(0, event)
-    }
-
-    function handleBlur () {
-        if (buttonState || x || y || mods.shift || mods.alt || mods.meta || mods.control) {
-            x = y = 0
-            buttonState = 0
-            mods.shift = mods.alt = mods.control = mods.meta = false
-            callback && callback(0, 0, 0, mods)
-        }
-    }
-
-    function handleMods (event: MouseEvent | KeyboardEvent) {
-        if (updateMods(event)) {
-            callback && callback(buttonState, x, y, mods)
-        }
-    }
-
-    function handleMouseMove (event: MouseEvent) {
-        if (mouse.buttons(event) === 0) {
-            handleEvent(0, event)
-        } else {
-            handleEvent(buttonState, event)
-        }
-    }
-
-    function handleMouseDown (event: MouseEvent) {
-        handleEvent(buttonState | mouse.buttons(event), event)
-    }
-
-    function handleMouseUp (event: MouseEvent) {
-        handleEvent(buttonState & ~mouse.buttons(event), event)
-    }
-
-    function attachListeners () {
-        if (attached) return
-        attached = true
-
-        element.addEventListener('mousemove', handleMouseMove as EventListener)
-        element.addEventListener('mousedown', handleMouseDown as EventListener)
-        element.addEventListener('mouseup', handleMouseUp as EventListener)
-
-        element.addEventListener('mouseleave', clearState as EventListener)
-        element.addEventListener('mouseenter', clearState as EventListener)
-        element.addEventListener('mouseout', clearState as EventListener)
-        element.addEventListener('mouseover', clearState as EventListener)
-
-        element.addEventListener('blur', handleBlur)
-        element.addEventListener('keyup', handleMods as EventListener)
-        element.addEventListener('keydown', handleMods as EventListener)
-        element.addEventListener('keypress', handleMods as EventListener)
-
-        if (!(element instanceof Window)) {
-            window.addEventListener('blur', handleBlur)
-            window.addEventListener('keyup', handleMods)
-            window.addEventListener('keydown', handleMods)
-            window.addEventListener('keypress', handleMods)
-        }
-    }
-
-    function detachListeners () {
-        if (!attached) return
-        attached = false
-
-        element.removeEventListener('mousemove', handleMouseMove as EventListener)
-        element.removeEventListener('mousedown', handleMouseDown as EventListener)
-        element.removeEventListener('mouseup', handleMouseUp as EventListener)
-
-        element.removeEventListener('mouseleave', clearState as EventListener)
-        element.removeEventListener('mouseenter', clearState as EventListener)
-        element.removeEventListener('mouseout', clearState as EventListener)
-        element.removeEventListener('mouseover', clearState as EventListener)
-
-        element.removeEventListener('blur', handleBlur)
-        element.removeEventListener('keyup', handleMods as EventListener)
-        element.removeEventListener('keydown', handleMods as EventListener)
-        element.removeEventListener('keypress', handleMods as EventListener)
-
-        if (!(element instanceof Window)) {
-            window.removeEventListener('blur', handleBlur)
-            window.removeEventListener('keyup', handleMods)
-            window.removeEventListener('keydown', handleMods)
-            window.removeEventListener('keypress', handleMods)
-        }
-    }
-
-    // Attach listeners
-    attachListeners()
-
-    const result = {
-        element: element
-    }
-
-    Object.defineProperties(result, {
-        enabled: {
-            get: function () { return attached },
-            set: function (f) {
-                if (f) {
-                    attachListeners()
-                } else {
-                    detachListeners()
-                }
-            },
-            enumerable: true
-        },
-        buttons: {
-            get: function () { return buttonState },
-            enumerable: true
-        },
-        x: {
-            get: function () { return x },
-            enumerable: true
-        },
-        y: {
-            get: function () { return y },
-            enumerable: true
-        },
-        mods: {
-            get: function () { return mods },
-            enumerable: true
-        }
-    })
-
-    return result
-}
\ No newline at end of file
diff --git a/src/mol-util/mouse-wheel.ts b/src/mol-util/mouse-wheel.ts
deleted file mode 100644
index 51a11f785..000000000
--- a/src/mol-util/mouse-wheel.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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/mikolalysenko/mouse-wheel,
- * copyright (c) 2015 Mikola Lysenko. MIT License
- */
-
-import toPixels from './to-pixels'
-
-export type MouseWheelCallback = (dx: number, dy: number, dz: number, event: MouseWheelEvent) => void
-
-export default function mouseWheelListen(element: Element, callback: MouseWheelCallback, noScroll = false) {
-    const lineHeight = toPixels('ex', element)
-    const listener = function (event: MouseWheelEvent) {
-        if (noScroll) {
-            event.preventDefault()
-        }
-        const mode = event.deltaMode
-        let dx = event.deltaX || 0
-        let dy = event.deltaY || 0
-        let dz = event.deltaZ || 0
-        let scale = 1
-        switch (mode) {
-            case 1: scale = lineHeight; break
-            case 2: scale = window.innerHeight; break
-        }
-        dx *= scale
-        dy *= scale
-        dz *= scale
-        if (dx || dy || dz) {
-            return callback(dx, dy, dz, event)
-        }
-    }
-    element.addEventListener('wheel', listener)
-    return listener
-}
\ No newline at end of file
-- 
GitLab