ソースを参照

wip, camera & controls

Alexander Rose 7 年 前
コミット
158f3de52b

ファイルの差分が大きいため隠しています
+ 289 - 119
package-lock.json


+ 1 - 0
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",

+ 2 - 4
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)
     }
 }

+ 0 - 192
src/mol-gl/camera.ts

@@ -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 }
-        }
-    }
-}

+ 100 - 0
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
+        }
+    }
+}

+ 5 - 0
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>
+ */

+ 61 - 0
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 },
+        }
+    }
+}

+ 96 - 0
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)
+}

+ 237 - 0
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

+ 0 - 0
src/mol-gl/controls/trackball.ts


+ 2 - 0
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

+ 5 - 2
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'
         }
     }
 }

+ 4 - 0
src/mol-gl/renderable/point.ts

@@ -38,6 +38,10 @@ namespace Point {
         })
         return {
             draw: () => command(),
+            get stats() {
+                return command.stats
+            },
+            name: 'point'
         }
     }
 }

+ 154 - 90
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())
         }
     }
-}
+}
+
+export default Renderer

+ 67 - 0
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
+        }
+    }
+}

+ 3 - 1
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 }
+export { Mat4, Mat3, Vec2, Vec3, Vec4, Quat, EPSILON }

+ 34 - 0
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];

+ 85 - 0
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

+ 25 - 0
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

+ 2 - 3
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;

+ 31 - 0
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()
+    }
+}

+ 273 - 0
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

+ 184 - 0
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

+ 1 - 1
src/mol-util/mouse-event.ts → 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) {

+ 67 - 0
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

+ 188 - 0
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

+ 0 - 194
src/mol-util/mouse-change.ts

@@ -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
-}

+ 0 - 40
src/mol-util/mouse-wheel.ts

@@ -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
-}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません