Ver Fonte

wip, input bindings, radius-based clipping

Alexander Rose há 5 anos atrás
pai
commit
2718d42b01

+ 1 - 0
src/apps/viewer/extensions/jolecule.ts

@@ -147,6 +147,7 @@ function getCameraSnapshot(e: JoleculeSnapshot['camera']): Camera.Snapshot {
         mode: 'perspective',
         position: Vec3.scaleAndAdd(Vec3.zero(), e.pos, direction, e.slab.zoom),
         target: e.pos,
+        radius: e.slab.zoom,
         direction,
         up,
         near: e.slab.zoom + e.slab.z_front,

+ 13 - 10
src/mol-canvas3d/camera.ts

@@ -11,12 +11,9 @@ import { Object3D } from '../mol-gl/object3d';
 import { BehaviorSubject } from 'rxjs';
 import { CameraTransitionManager } from './camera/transition';
 import { Canvas3DProps } from './canvas3d';
-import Scene from '../mol-gl/scene';
 
 export { Camera }
 
-// TODO: slab controls that modify near/far planes?
-
 class Camera implements Object3D {
     readonly updatedViewProjection = new BehaviorSubject<Camera>(this);
 
@@ -58,6 +55,8 @@ class Camera implements Object3D {
         const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target);
         snapshot.zoom = this.viewport.height / height;
 
+        cameraSetClipping(snapshot, this.canvasProps);
+
         switch (this.state.mode) {
             case 'orthographic': updateOrtho(this); break;
             case 'perspective': updatePers(this); break;
@@ -102,13 +101,12 @@ class Camera implements Object3D {
         Vec3.setMagnitude(this.deltaDirection, this.state.direction, targetDistance)
         Vec3.sub(this.newPosition, target, this.deltaDirection)
 
-        const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
-        state.target = Vec3.clone(target);
-        state.position = Vec3.clone(this.newPosition);
-
-        cameraSetClipping(state, this.scene.boundingSphere, this.canvasProps)
+        const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state)
+        state.target = Vec3.clone(target)
+        state.radius = radius
+        state.position = Vec3.clone(this.newPosition)
 
-        return state;
+        return state
     }
 
     focus(target: Vec3, radius: number, durationMs?: number) {
@@ -136,7 +134,7 @@ class Camera implements Object3D {
         this.updatedViewProjection.complete();
     }
 
-    constructor(private scene: Scene, private canvasProps: Canvas3DProps, state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) {
+    constructor(private canvasProps: Canvas3DProps, state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) {
         this.viewport = viewport;
         Camera.copySnapshot(this.state, state);
     }
@@ -186,6 +184,7 @@ namespace Camera {
             up: Vec3.create(0, 1, 0),
 
             target: Vec3.create(0, 0, 0),
+            radius: 10,
 
             near: 1,
             far: 10000,
@@ -204,7 +203,9 @@ namespace Camera {
         // Normalized camera direction, from Target to Position, for some reason?
         direction: Vec3,
         up: Vec3,
+
         target: Vec3,
+        radius: number
 
         near: number,
         far: number,
@@ -223,7 +224,9 @@ namespace Camera {
         if (typeof source.position !== 'undefined') Vec3.copy(out.position, source.position);
         if (typeof source.direction !== 'undefined') Vec3.copy(out.direction, source.direction);
         if (typeof source.up !== 'undefined') Vec3.copy(out.up, source.up);
+
         if (typeof source.target !== 'undefined') Vec3.copy(out.target, source.target);
+        if (typeof source.radius !== 'undefined') out.radius = source.radius;
 
         if (typeof source.near !== 'undefined') out.near = source.near;
         if (typeof source.far !== 'undefined') out.far = source.far;

+ 3 - 2
src/mol-canvas3d/camera/transition.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -81,8 +81,9 @@ namespace CameraTransitionManager {
         Quat.slerp(rot, rot, Quat.rotationTo(Quat.zero(), source.up, target.up), t);
         Vec3.transformQuat(out.up, source.up, rot);
 
-        // Lerp target
+        // Lerp target & radius
         Vec3.lerp(out.target, source.target, target.target, t);
+        out.radius = lerp(source.radius, target.radius, t);
 
         // Update position
         const dist = -lerp(Vec3.distance(source.position, source.target), Vec3.distance(target.position, target.target), t);

+ 2 - 8
src/mol-canvas3d/camera/util.ts

@@ -6,7 +6,6 @@
 
 import { Mat4, Vec3, Vec4, EPSILON } from '../../mol-math/linear-algebra'
 import { Camera } from '../camera'
-import { Sphere3D } from '../../mol-math/geometry'
 import { Canvas3DProps } from '../canvas3d'
 
 export { Viewport }
@@ -83,9 +82,9 @@ export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target:
     }
 }
 
-export function cameraSetClipping(state: Camera.Snapshot, boundingSphere: Sphere3D, p: Canvas3DProps) {
+export function cameraSetClipping(state: Camera.Snapshot, p: Canvas3DProps) {
     const cDist = Vec3.distance(state.position, state.target)
-    const bRadius = Math.max(10, boundingSphere.radius)
+    const bRadius = Math.max(1, state.radius)
 
     const nearFactor = (50 - p.clip[0]) / 50
     const farFactor = -(50 - p.clip[1]) / 50
@@ -113,11 +112,6 @@ export function cameraSetClipping(state: Camera.Snapshot, boundingSphere: Sphere
     state.far = far;
     state.fogNear = fogNear;
     state.fogFar = fogFar;
-
-    // if (near !== currentNear || far !== currentFar || fogNear !== currentFogNear || fogFar !== currentFogFar) {
-    //     camera.setState({ near, far, fogNear, fogFar })
-    //     currentNear = near, currentFar = far, currentFogNear = fogNear, currentFogFar = fogFar
-    // }
 }
 
 const NEAR_RANGE = 0

+ 4 - 12
src/mol-canvas3d/canvas3d.ts

@@ -11,7 +11,7 @@ import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/inp
 import Renderer, { RendererStats, RendererParams } from '../mol-gl/renderer'
 import { GraphicsRenderObject } from '../mol-gl/render-object'
 import { TrackballControls, TrackballControlsParams } from './controls/trackball'
-import { Viewport, cameraSetClipping } from './camera/util'
+import { Viewport } from './camera/util'
 import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
 import { Representation } from '../mol-repr/representation';
 import Scene from '../mol-gl/scene';
@@ -124,9 +124,7 @@ namespace Canvas3D {
 
         const scene = Scene.create(webgl)
 
-        const camera = new Camera(scene, p, {
-            near: 0.1,
-            far: 10000,
+        const camera = new Camera(p, {
             position: Vec3.create(0, 0, 100),
             mode: p.cameraMode
         })
@@ -174,18 +172,12 @@ namespace Canvas3D {
             }
         }
 
-        function setClipping() {
-            cameraSetClipping(camera.state, scene.boundingSphere, p);
-        }
-
         function render(variant: 'pick' | 'draw', force: boolean) {
             if (scene.isCommiting) return false
 
             let didRender = false
-            controls.update(currentTime);
-            // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane.
-            if (!camera.transition.inTransition) setClipping();
-            const cameraChanged = camera.updateMatrices();
+            controls.update(currentTime)
+            const cameraChanged = camera.updateMatrices()
             multiSample.update(force || cameraChanged, currentTime)
 
             if (force || cameraChanged || multiSample.enabled) {

+ 49 - 0
src/mol-canvas3d/controls/bindings.ts

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
+
+const B = ButtonsType
+const M = ModifiersKeys
+
+export interface Bindings {
+    drag: {
+        rotate: Bindings.Trigger
+        rotateZ: Bindings.Trigger
+        pan: Bindings.Trigger
+        zoom: Bindings.Trigger
+        // zoomFocus: Bindings.Trigger
+    },
+    scroll: {
+        // focus: Bindings.Trigger
+        zoom: Bindings.Trigger
+        // zoomFocus: Bindings.Trigger
+        clipNear: Bindings.Trigger
+    }
+}
+
+export namespace Bindings {
+    export type Trigger = { buttons: ButtonsType, modifiers?: ModifiersKeys }
+
+    export function match(trigger: Trigger, buttons: ButtonsType, modifiers: ModifiersKeys) {
+        const { buttons: b, modifiers: m } = trigger
+        return ButtonsType.has(b, buttons) &&
+            (!m || ModifiersKeys.areEqual(m, modifiers))
+    }
+
+    export const Default: Bindings = {
+        drag: {
+            rotate: { buttons: B.Flag.Primary, modifiers: M.create() },
+            rotateZ: { buttons: B.Flag.Primary, modifiers: M.create({ shift: true }) },
+            pan: { buttons: B.Flag.Secondary },
+            zoom: { buttons: B.Flag.Auxilary }
+        },
+        scroll: {
+            zoom: { buttons: B.Flag.Auxilary, modifiers: M.create() },
+            clipNear: { buttons: B.Flag.Auxilary, modifiers: M.create({ shift: true }) }
+        }
+    }
+}

+ 140 - 95
src/mol-canvas3d/controls/trackball.ts

@@ -10,9 +10,11 @@
 
 import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
 import { cameraLookAt, Viewport } from '../camera/util';
-import InputObserver, { DragInput, WheelInput, ButtonsType, PinchInput } from '../../mol-util/input/input-observer';
-import { Object3D } from '../../mol-gl/object3d';
+import InputObserver, { DragInput, WheelInput, PinchInput } from '../../mol-util/input/input-observer';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Camera } from '../camera';
+import { Bindings } from './bindings';
+import { absMax } from '../../mol-math/misc';
 
 export const TrackballControlsParams = {
     noScroll: PD.Boolean(true, { isHidden: true }),
@@ -28,7 +30,9 @@ export const TrackballControlsParams = {
     dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
 
     minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
-    maxDistance: PD.Numeric(1e150, {}, { isHidden: true })
+    maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
+
+    bindings: PD.Value(Bindings.Default, { isHidden: true })
 }
 export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
 
@@ -44,11 +48,10 @@ interface TrackballControls {
     dispose: () => void
 }
 namespace TrackballControls {
-    export function create(input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls {
+    export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
         const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props }
 
-        const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 }
-        const target: Vec3 = object.target
+        const viewport = Viewport()
 
         let disposed = false
 
@@ -60,91 +63,113 @@ namespace TrackballControls {
         let _isInteracting = false;
 
         // For internal use
-        const lastPosition = Vec3.zero()
+        const lastPosition = Vec3()
 
-        const _eye = Vec3.zero()
+        const _eye = Vec3()
 
-        const _movePrev = Vec2.zero()
-        const _moveCurr = Vec2.zero()
+        const _rotPrev = Vec2()
+        const _rotCurr = Vec2()
+        const _rotLastAxis = Vec3()
+        let _rotLastAngle = 0
 
-        const _lastAxis = Vec3.zero()
-        let _lastAngle = 0
+        const _zRotPrev = Vec2()
+        const _zRotCurr = Vec2()
+        let _zRotLastAngle = 0
 
-        const _zoomStart = Vec2.zero()
-        const _zoomEnd = Vec2.zero()
+        const _zoomStart = Vec2()
+        const _zoomEnd = Vec2()
 
-        const _panStart = Vec2.zero()
-        const _panEnd = Vec2.zero()
+        const _panStart = Vec2()
+        const _panEnd = Vec2()
 
         // Initial values for reseting
-        const target0 = Vec3.clone(target)
-        const position0 = Vec3.clone(object.position)
-        const up0 = Vec3.clone(object.up)
+        const target0 = Vec3.clone(camera.target)
+        const position0 = Vec3.clone(camera.position)
+        const up0 = Vec3.clone(camera.up)
 
-        const mouseOnScreenVec2 = Vec2.zero()
+        const mouseOnScreenVec2 = Vec2()
         function getMouseOnScreen(pageX: number, pageY: number) {
-            Vec2.set(
+            return Vec2.set(
                 mouseOnScreenVec2,
                 (pageX - viewport.x) / viewport.width,
                 (pageY - viewport.y) / viewport.height
             );
-            return mouseOnScreenVec2;
         }
 
-        const mouseOnCircleVec2 = Vec2.zero()
+        const mouseOnCircleVec2 = Vec2()
         function getMouseOnCircle(pageX: number, pageY: number) {
-            Vec2.set(
+            return Vec2.set(
                 mouseOnCircleVec2,
-                ((pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5)),
-                ((viewport.height + 2 * (viewport.y - pageY)) / viewport.width) // screen.width intentional
+                (pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
+                (viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
             );
-            return mouseOnCircleVec2;
         }
 
-        const rotAxis = Vec3.zero()
-        const rotQuat = Quat.zero()
-        const rotEyeDir = Vec3.zero()
-        const rotObjUpDir = Vec3.zero()
-        const rotObjSideDir = Vec3.zero()
-        const rotMoveDir = Vec3.zero()
+        const rotAxis = Vec3()
+        const rotQuat = Quat()
+        const rotEyeDir = Vec3()
+        const rotObjUpDir = Vec3()
+        const rotObjSideDir = Vec3()
+        const rotMoveDir = Vec3()
 
         function rotateCamera() {
-            Vec3.set(rotMoveDir, _moveCurr[0] - _movePrev[0], _moveCurr[1] - _movePrev[1], 0);
-            let angle = Vec3.magnitude(rotMoveDir);
+            const dx = _rotCurr[0] - _rotPrev[0]
+            const dy = _rotCurr[1] - _rotPrev[1]
+            Vec3.set(rotMoveDir, dx, dy, 0);
+
+            const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed;
 
             if (angle) {
-                Vec3.copy(_eye, object.position)
-                Vec3.sub(_eye, _eye, target)
+                Vec3.sub(_eye, camera.position, camera.target)
 
-                Vec3.normalize(rotEyeDir, Vec3.copy(rotEyeDir, _eye))
-                Vec3.normalize(rotObjUpDir, Vec3.copy(rotObjUpDir, object.up))
+                Vec3.normalize(rotEyeDir, _eye)
+                Vec3.normalize(rotObjUpDir, camera.up)
                 Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir))
 
-                Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, _moveCurr[1] - _movePrev[1])
-                Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, _moveCurr[0] - _movePrev[0])
-
-                Vec3.add(rotMoveDir, Vec3.copy(rotMoveDir, rotObjUpDir), rotObjSideDir)
+                Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, dy)
+                Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, dx)
 
+                Vec3.add(rotMoveDir, rotObjUpDir, rotObjSideDir)
                 Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye))
-
-                angle *= p.rotateSpeed;
                 Quat.setAxisAngle(rotQuat, rotAxis, angle)
 
                 Vec3.transformQuat(_eye, _eye, rotQuat)
-                Vec3.transformQuat(object.up, object.up, rotQuat)
+                Vec3.transformQuat(camera.up, camera.up, rotQuat)
 
-                Vec3.copy(_lastAxis, rotAxis)
-                _lastAngle = angle;
-            } else if (!p.staticMoving && _lastAngle) {
-                _lastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
-                Vec3.sub(_eye, Vec3.copy(_eye, object.position), target)
-                Quat.setAxisAngle(rotQuat, _lastAxis, _lastAngle)
+                Vec3.copy(_rotLastAxis, rotAxis)
+                _rotLastAngle = angle;
+            } else if (!p.staticMoving && _rotLastAngle) {
+                _rotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
+                Vec3.sub(_eye, camera.position, camera.target)
+                Quat.setAxisAngle(rotQuat, _rotLastAxis, _rotLastAngle)
 
                 Vec3.transformQuat(_eye, _eye, rotQuat)
-                Vec3.transformQuat(object.up, object.up, rotQuat)
+                Vec3.transformQuat(camera.up, camera.up, rotQuat)
+            }
+
+            Vec2.copy(_rotPrev, _rotCurr)
+        }
+
+        const zRotQuat = Quat()
+
+        function zRotateCamera() {
+            const dx = _zRotCurr[0] - _zRotPrev[0]
+            const dy = _zRotCurr[1] - _zRotPrev[1]
+            const angle = p.rotateSpeed * (-dx + dy) * -0.05
+
+            if (angle) {
+                Vec3.sub(_eye, camera.position, camera.target)
+                Quat.setAxisAngle(zRotQuat, _eye, angle)
+                Vec3.transformQuat(camera.up, camera.up, zRotQuat)
+                _zRotLastAngle = angle;
+            } else if (!p.staticMoving && _zRotLastAngle) {
+                _zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
+                Vec3.sub(_eye, camera.position, camera.target)
+                Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle)
+                Vec3.transformQuat(camera.up, camera.up, zRotQuat)
             }
 
-            Vec2.copy(_movePrev, _moveCurr)
+            Vec2.copy(_zRotPrev, _zRotCurr)
         }
 
         function zoomCamera() {
@@ -160,9 +185,9 @@ namespace TrackballControls {
             }
         }
 
-        const panMouseChange = Vec2.zero()
-        const panObjUp = Vec3.zero()
-        const panOffset = Vec3.zero()
+        const panMouseChange = Vec2()
+        const panObjUp = Vec3()
+        const panOffset = Vec3()
 
         function panCamera() {
             Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart)
@@ -170,14 +195,14 @@ namespace TrackballControls {
             if (Vec2.squaredMagnitude(panMouseChange)) {
                 Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * p.panSpeed)
 
-                Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), object.up)
+                Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up)
                 Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0])
 
-                Vec3.setMagnitude(panObjUp, object.up, panMouseChange[1])
+                Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1])
                 Vec3.add(panOffset, panOffset, panObjUp)
 
-                Vec3.add(object.position, object.position, panOffset)
-                Vec3.add(target, target, panOffset)
+                Vec3.add(camera.position, camera.position, panOffset)
+                Vec3.add(camera.target, camera.target, panOffset)
 
                 if (p.staticMoving) {
                     Vec2.copy(_panStart, _panEnd)
@@ -193,13 +218,13 @@ namespace TrackballControls {
         function checkDistances() {
             if (Vec3.squaredMagnitude(_eye) > p.maxDistance * p.maxDistance) {
                 Vec3.setMagnitude(_eye, _eye, p.maxDistance)
-                Vec3.add(object.position, target, _eye)
+                Vec3.add(camera.position, camera.target, _eye)
                 Vec2.copy(_zoomStart, _zoomEnd)
             }
 
             if (Vec3.squaredMagnitude(_eye) < p.minDistance * p.minDistance) {
                 Vec3.setMagnitude(_eye, _eye, p.minDistance)
-                Vec3.add(object.position, target, _eye)
+                Vec3.add(camera.position, camera.target, _eye)
                 Vec2.copy(_zoomStart, _zoomEnd)
             }
         }
@@ -210,18 +235,19 @@ namespace TrackballControls {
             if (lastUpdated === t) return;
             if (p.spin) spin(t - lastUpdated);
 
-            Vec3.sub(_eye, object.position, target)
+            Vec3.sub(_eye, camera.position, camera.target)
 
             rotateCamera()
+            zRotateCamera()
             zoomCamera()
             panCamera()
 
-            Vec3.add(object.position, target, _eye)
+            Vec3.add(camera.position, camera.target, _eye)
             checkDistances()
-            cameraLookAt(object.position, object.up, object.direction, target)
+            cameraLookAt(camera.position, camera.up, camera.direction, camera.target)
 
-            if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON) {
-                Vec3.copy(lastPosition, object.position)
+            if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
+                Vec3.copy(lastPosition, camera.position)
             }
 
             lastUpdated = t;
@@ -229,53 +255,72 @@ namespace TrackballControls {
 
         /** Reset object's vectors and the target vector to their initial values */
         function reset() {
-            Vec3.copy(target, target0)
-            Vec3.copy(object.position, position0)
-            Vec3.copy(object.up, up0)
+            Vec3.copy(camera.target, target0)
+            Vec3.copy(camera.position, position0)
+            Vec3.copy(camera.up, up0)
 
-            Vec3.sub(_eye, object.position, target)
-            cameraLookAt(object.position, object.up, object.direction, target)
-            Vec3.copy(lastPosition, object.position)
+            Vec3.sub(_eye, camera.position, camera.target)
+            cameraLookAt(camera.position, camera.up, camera.direction, camera.target)
+            Vec3.copy(lastPosition, camera.position)
         }
 
         // listeners
 
-        function onDrag({ pageX, pageY, buttons, isStart }: DragInput) {
+        function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) {
             _isInteracting = true;
 
+            const dragRotate = Bindings.match(p.bindings.drag.rotate, buttons, modifiers)
+            const dragRotateZ = Bindings.match(p.bindings.drag.rotateZ, buttons, modifiers)
+            const dragPan = Bindings.match(p.bindings.drag.pan, buttons, modifiers)
+            const dragZoom = Bindings.match(p.bindings.drag.zoom, buttons, modifiers)
+
+            getMouseOnCircle(pageX, pageY)
+            getMouseOnScreen(pageX, pageY)
+
             if (isStart) {
-                if (buttons === ButtonsType.Flag.Primary) {
-                    Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY))
-                    Vec2.copy(_movePrev, _moveCurr)
-                } else if (buttons === ButtonsType.Flag.Auxilary) {
-                    Vec2.copy(_zoomStart, getMouseOnScreen(pageX, pageY))
+                if (dragRotate) {
+                    Vec2.copy(_rotCurr, mouseOnCircleVec2)
+                    Vec2.copy(_rotPrev, _rotCurr)
+                }
+                if (dragRotateZ) {
+                    Vec2.copy(_zRotCurr, mouseOnCircleVec2)
+                    Vec2.copy(_zRotPrev, _zRotCurr)
+                }
+                if (dragZoom) {
+                    Vec2.copy(_zoomStart, mouseOnScreenVec2)
                     Vec2.copy(_zoomEnd, _zoomStart)
-                } else if (buttons === ButtonsType.Flag.Secondary) {
-                    Vec2.copy(_panStart, getMouseOnScreen(pageX, pageY))
+                }
+                if (dragPan) {
+                    Vec2.copy(_panStart, mouseOnScreenVec2)
                     Vec2.copy(_panEnd, _panStart)
                 }
             }
 
-            if (buttons === ButtonsType.Flag.Primary) {
-                Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY))
-            } else if (buttons === ButtonsType.Flag.Auxilary) {
-                Vec2.copy(_zoomEnd, getMouseOnScreen(pageX, pageY))
-            } else if (buttons === ButtonsType.Flag.Secondary) {
-                Vec2.copy(_panEnd, getMouseOnScreen(pageX, pageY))
-            }
+            if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2)
+            if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2)
+            if (dragZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2)
+            if (dragPan) Vec2.copy(_panEnd, mouseOnScreenVec2)
         }
 
         function onInteractionEnd() {
             _isInteracting = false;
         }
 
-        function onWheel({ dy }: WheelInput) {
-            _zoomEnd[1] += dy * 0.0001
+        function onWheel({ dx, dy, dz, buttons, modifiers }: WheelInput) {
+            if (Bindings.match(p.bindings.scroll.zoom, buttons, modifiers)) {
+                _zoomEnd[1] += absMax(dx, dy, dz) * 0.0001
+            }
+            if (Bindings.match(p.bindings.scroll.clipNear, buttons, modifiers)) {
+                const radius = Math.max(0, camera.state.radius + absMax(dx, dy, dz) * 0.005)
+                camera.setState({ radius })
+            }
         }
 
-        function onPinch({ fraction }: PinchInput) {
-            _isInteracting = true;
-            _zoomEnd[1] += (fraction - 1) * 0.1
+        function onPinch({ fraction, buttons, modifiers }: PinchInput) {
+            if (Bindings.match(p.bindings.scroll.zoom, buttons, modifiers)) {
+                _isInteracting = true;
+                _zoomEnd[1] += (fraction - 1) * 0.1
+            }
         }
 
         function dispose() {
@@ -292,7 +337,7 @@ namespace TrackballControls {
         function spin(deltaT: number) {
             const frameSpeed = (p.spinSpeed || 0) / 1000;
             _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
-            if (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed);
+            if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
         }
 
         // force an update at start

+ 1 - 1
src/mol-gl/_spec/renderer.spec.ts

@@ -28,7 +28,7 @@ import { DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
 
 function createRenderer(gl: WebGLRenderingContext) {
     const ctx = createContext(gl)
-    const camera = new Camera(Scene.create(ctx), DefaultCanvas3DParams, {
+    const camera = new Camera(DefaultCanvas3DParams, {
         near: 0.01,
         far: 10000,
         position: Vec3.create(0, 0, 50)

+ 15 - 0
src/mol-math/misc.ts

@@ -14,4 +14,19 @@ export function radToDeg (rad: number) {
 
 export function isPowerOfTwo (x: number) {
     return (x !== 0) && (x & (x - 1)) === 0
+}
+
+/** return the value that has the largest absolute value */
+export function absMax(...values: number[]) {
+    let max = 0
+    let absMax = 0
+    for (let i = 0, il = values.length; i < il; ++i) {
+        const value = values[i]
+        const abs = Math.abs(value)
+        if (abs > absMax) {
+            max = value
+            absMax = abs
+        }
+    }
+    return max
 }

+ 26 - 5
src/mol-util/input/input-observer.ts

@@ -60,11 +60,20 @@ export type ModifiersKeys = {
     meta: boolean
 }
 export namespace ModifiersKeys {
-    export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false };
+    export const None = create();
 
     export function areEqual(a: ModifiersKeys, b: ModifiersKeys) {
         return a.shift === b.shift && a.alt === b.alt && a.control === b.control && a.meta === b.meta;
     }
+
+    export function create(modifierKeys: Partial<ModifiersKeys> = {}): ModifiersKeys {
+        return {
+            shift: !!modifierKeys.shift,
+            alt: !!modifierKeys.alt,
+            control: !!modifierKeys.control,
+            meta: !!modifierKeys.meta
+        }
+    }
 }
 
 export type ButtonsType = BitFlags<ButtonsType.Flag>
@@ -130,7 +139,7 @@ export type PinchInput = {
     fraction: number,
     distance: number,
     isStart: boolean
-}
+} & BaseInput
 
 export type ResizeInput = {
 
@@ -342,12 +351,19 @@ namespace InputObserver {
                 buttons = ButtonsType.Flag.Primary
                 onPointerDown(ev.touches[0])
             } else if (ev.touches.length >= 2) {
-                buttons = ButtonsType.Flag.Secondary
+                buttons = ButtonsType.Flag.Secondary & ButtonsType.Flag.Auxilary
                 onPointerDown(getCenterTouch(ev))
 
                 const touchDistance = getTouchDistance(ev)
                 lastTouchDistance = touchDistance
-                pinch.next({ distance: touchDistance, fraction: 1, delta: 0, isStart: true })
+                pinch.next({
+                    distance: touchDistance,
+                    fraction: 1,
+                    delta: 0,
+                    isStart: true,
+                    buttons,
+                    modifiers: getModifierKeys()
+                })
             }
         }
 
@@ -375,11 +391,14 @@ namespace InputObserver {
                     buttons = ButtonsType.Flag.Secondary
                     onPointerMove(getCenterTouch(ev))
                 } else {
+                    buttons = ButtonsType.Flag.Auxilary
                     pinch.next({
                         delta: touchDelta,
                         fraction: lastTouchDistance / touchDistance,
                         distance: touchDistance,
-                        isStart: false
+                        isStart: false,
+                        buttons,
+                        modifiers: getModifierKeys()
                     })
                 }
                 lastTouchDistance = touchDistance
@@ -461,6 +480,8 @@ namespace InputObserver {
             const dy = (ev.deltaY || 0) * scale
             const dz = (ev.deltaZ || 0) * scale
 
+            buttons = ButtonsType.Flag.Auxilary
+
             if (dx || dy || dz) {
                 wheel.next({ dx, dy, dz, buttons, modifiers: getModifierKeys() })
             }