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