123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
- import { Subject, Observable } from 'rxjs';
- import { Vec2 } from '../../mol-math/linear-algebra';
- import { BitFlags, noop } from '../../mol-util';
- 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 function getModifiers(event: MouseEvent | Touch) {
- return {
- alt: 'altKey' in event ? event.altKey : false,
- shift: 'shiftKey' in event ? event.shiftKey : false,
- control: 'ctrlKey' in event ? event.ctrlKey : false,
- meta: 'metaKey' in event ? event.metaKey : false
- }
- }
- export const DefaultInputObserverProps = {
- noScroll: true,
- noContextMenu: true,
- noPinchZoom: true
- }
- export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
- export type ModifiersKeys = {
- shift: boolean,
- alt: boolean,
- control: boolean,
- meta: boolean
- }
- export namespace ModifiersKeys {
- export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false };
- 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 type ButtonsType = BitFlags<ButtonsType.Flag>
- export namespace ButtonsType {
- export const has: (btn: ButtonsType, f: Flag) => boolean = BitFlags.has
- export const create: (fs: Flag) => ButtonsType = BitFlags.create
- export const enum Flag {
- /** No button or un-initialized */
- None = 0x0,
- /** Primary button (usually left) */
- Primary = 0x1,
- /** Secondary button (usually right) */
- Secondary = 0x2,
- /** Auxilary button (usually middle or mouse wheel button) */
- Auxilary = 0x4,
- /** 4th button (typically the "Browser Back" button) */
- Forth = 0x8,
- /** 5th button (typically the "Browser Forward" button) */
- Five = 0x10,
- }
- }
- type BaseInput = {
- buttons: ButtonsType
- modifiers: ModifiersKeys
- }
- export type DragInput = {
- x: number,
- y: number,
- dx: number,
- dy: number,
- pageX: number,
- pageY: number,
- isStart: boolean
- } & BaseInput
- export type WheelInput = {
- dx: number,
- dy: number,
- dz: number,
- } & BaseInput
- export type ClickInput = {
- x: number,
- y: number,
- pageX: number,
- pageY: number,
- } & BaseInput
- export type MoveInput = {
- x: number,
- y: number,
- pageX: number,
- pageY: number,
- inside: boolean,
- } & BaseInput
- export type PinchInput = {
- delta: number,
- fraction: number,
- distance: number,
- isStart: boolean
- }
- export type ResizeInput = {
- }
- const enum DraggingState {
- Stopped = 0,
- Started = 1,
- Moving = 2
- }
- type PointerEvent = {
- clientX: number
- clientY: number
- pageX: number
- pageY: number
- }
- interface InputObserver {
- noScroll: boolean
- noContextMenu: boolean
- readonly drag: Observable<DragInput>,
- // Equivalent to mouseUp and touchEnd
- readonly interactionEnd: Observable<undefined>,
- readonly wheel: Observable<WheelInput>,
- readonly pinch: Observable<PinchInput>,
- readonly click: Observable<ClickInput>,
- readonly move: Observable<MoveInput>,
- readonly leave: Observable<undefined>,
- readonly enter: Observable<undefined>,
- readonly resize: Observable<ResizeInput>,
- readonly modifiers: Observable<ModifiersKeys>
- dispose: () => void
- }
- function createEvents() {
- return {
- drag: new Subject<DragInput>(),
- interactionEnd: new Subject<undefined>(),
- click: new Subject<ClickInput>(),
- move: new Subject<MoveInput>(),
- wheel: new Subject<WheelInput>(),
- pinch: new Subject<PinchInput>(),
- resize: new Subject<ResizeInput>(),
- leave: new Subject<undefined>(),
- enter: new Subject<undefined>(),
- modifiers: new Subject<ModifiersKeys>(),
- }
- }
- namespace InputObserver {
- export function create(props: InputObserverProps = {}): InputObserver {
- const { noScroll, noContextMenu } = { ...DefaultInputObserverProps, ...props }
- return {
- noScroll,
- noContextMenu,
- ...createEvents(),
- dispose: noop
- }
- }
- export function fromElement(element: Element, props: InputObserverProps = {}): InputObserver {
- let { noScroll, noContextMenu, noPinchZoom } = { ...DefaultInputObserverProps, ...props }
- let lastTouchDistance = 0
- const pointerDown = Vec2.zero()
- const pointerStart = Vec2.zero()
- const pointerEnd = Vec2.zero()
- const pointerDelta = Vec2.zero()
- const rectSize = Vec2.zero()
- const modifierKeys: ModifiersKeys = {
- shift: false,
- alt: false,
- control: false,
- meta: false
- }
- function getModifierKeys(): ModifiersKeys {
- return { ...modifierKeys };
- }
- let dragging: DraggingState = DraggingState.Stopped
- let disposed = false
- let buttons = 0 as ButtonsType
- let isInside = false
- const events = createEvents()
- const { drag, interactionEnd, wheel, pinch, click, move, leave, enter, resize, modifiers } = events
- attach()
- return {
- get noScroll () { return noScroll },
- set noScroll (value: boolean) { noScroll = value },
- get noContextMenu () { return noContextMenu },
- set noContextMenu (value: boolean) { noContextMenu = value },
- ...events,
- dispose
- }
- function attach () {
- element.addEventListener('contextmenu', onContextMenu, false )
- element.addEventListener('wheel', onMouseWheel as any, false)
- element.addEventListener('mousedown', onMouseDown as any, false)
- // for dragging to work outside canvas bounds,
- // mouse move/up events have to be added to a parent, i.e. window
- window.addEventListener('mousemove', onMouseMove as any, false)
- window.addEventListener('mouseup', onMouseUp as any, false)
- element.addEventListener('mouseenter', onMouseEnter as any, false)
- element.addEventListener('mouseleave', onMouseLeave as any, false)
- element.addEventListener('touchstart', onTouchStart as any, false)
- element.addEventListener('touchmove', onTouchMove as any, false)
- element.addEventListener('touchend', onTouchEnd as any, false)
- element.addEventListener('blur', handleBlur)
- window.addEventListener('keyup', handleKeyUp as EventListener, false)
- window.addEventListener('keydown', handleKeyDown as EventListener, false)
- window.addEventListener('resize', onResize, false)
- }
- function dispose () {
- if (disposed) return
- disposed = true
- element.removeEventListener( 'contextmenu', onContextMenu, false )
- element.removeEventListener('wheel', onMouseWheel as any, false)
- element.removeEventListener('mousedown', onMouseDown as any, false)
- window.removeEventListener('mousemove', onMouseMove as any, false)
- window.removeEventListener('mouseup', onMouseUp as any, false)
- element.removeEventListener('mouseenter', onMouseEnter as any, false)
- element.removeEventListener('mouseleave', onMouseLeave as any, false)
- element.removeEventListener('touchstart', onTouchStart as any, false)
- element.removeEventListener('touchmove', onTouchMove as any, false)
- element.removeEventListener('touchend', onTouchEnd as any, false)
- element.removeEventListener('blur', handleBlur)
- window.removeEventListener('keyup', handleKeyUp as EventListener, false)
- window.removeEventListener('keydown', handleKeyDown as EventListener, false)
- window.removeEventListener('resize', onResize, false)
- }
- function onContextMenu(event: Event) {
- if (noContextMenu) {
- event.preventDefault()
- }
- }
- function handleBlur () {
- if (buttons || modifierKeys.shift || modifierKeys.alt || modifierKeys.meta || modifierKeys.control) {
- buttons = 0 as ButtonsType
- modifierKeys.shift = modifierKeys.alt = modifierKeys.control = modifierKeys.meta = false
- }
- }
- function handleKeyDown (event: KeyboardEvent) {
- let changed = false;
- if (!modifierKeys.alt && event.altKey) { changed = true; modifierKeys.alt = true; }
- if (!modifierKeys.shift && event.shiftKey) { changed = true; modifierKeys.shift = true; }
- if (!modifierKeys.control && event.ctrlKey) { changed = true; modifierKeys.control = true; }
- if (!modifierKeys.meta && event.metaKey) { changed = true; modifierKeys.meta = true; }
- if (changed && isInside) modifiers.next(getModifierKeys());
- }
- function handleKeyUp (event: KeyboardEvent) {
- let changed = false;
- if (modifierKeys.alt && !event.altKey) { changed = true; modifierKeys.alt = false; }
- if (modifierKeys.shift && !event.shiftKey) { changed = true; modifierKeys.shift = false; }
- if (modifierKeys.control && !event.ctrlKey) { changed = true; modifierKeys.control = false; }
- if (modifierKeys.meta && !event.metaKey) { changed = true; modifierKeys.meta = false; }
- if (changed && isInside) modifiers.next(getModifierKeys());
- }
- function getCenterTouch (ev: TouchEvent): PointerEvent {
- const t0 = ev.touches[0]
- const t1 = ev.touches[1]
- return {
- clientX: (t0.clientX + t1.clientX) / 2,
- clientY: (t0.clientY + t1.clientY) / 2,
- pageX: (t0.pageX + t1.pageX) / 2,
- pageY: (t0.pageY + t1.pageY) / 2
- }
- }
- function getTouchDistance (ev: TouchEvent) {
- const dx = ev.touches[0].pageX - ev.touches[1].pageX;
- const dy = ev.touches[0].pageY - ev.touches[1].pageY;
- return Math.sqrt(dx * dx + dy * dy);
- }
- function onTouchStart (ev: TouchEvent) {
- if (ev.touches.length === 1) {
- buttons = ButtonsType.Flag.Primary
- onPointerDown(ev.touches[0])
- } else if (ev.touches.length >= 2) {
- buttons = ButtonsType.Flag.Secondary
- onPointerDown(getCenterTouch(ev))
- const touchDistance = getTouchDistance(ev)
- lastTouchDistance = touchDistance
- pinch.next({ distance: touchDistance, fraction: 1, delta: 0, isStart: true })
- }
- }
- function onTouchEnd (ev: TouchEvent) {
- endDrag()
- }
- function onTouchMove (ev: TouchEvent) {
- if (noPinchZoom) {
- ev.preventDefault();
- ev.stopPropagation();
- if ((ev as any).originalEvent) {
- (ev as any).originalEvent.preventDefault();
- (ev as any).originalEvent.stopPropagation();
- }
- }
- if (ev.touches.length === 1) {
- buttons = ButtonsType.Flag.Primary
- onPointerMove(ev.touches[0])
- } else if (ev.touches.length >= 2) {
- const touchDistance = getTouchDistance(ev)
- const touchDelta = lastTouchDistance - touchDistance
- if (Math.abs(touchDelta) < 4) {
- buttons = ButtonsType.Flag.Secondary
- onPointerMove(getCenterTouch(ev))
- } else {
- pinch.next({
- delta: touchDelta,
- fraction: lastTouchDistance / touchDistance,
- distance: touchDistance,
- isStart: false
- })
- }
- lastTouchDistance = touchDistance
- }
- }
- function onMouseDown (ev: MouseEvent) {
- buttons = getButtons(ev)
- onPointerDown(ev)
- }
- function onMouseMove (ev: MouseEvent) {
- buttons = getButtons(ev)
- onPointerMove(ev)
- }
- function onMouseUp (ev: MouseEvent) {
- onPointerUp(ev)
- endDrag()
- }
- function endDrag() {
- interactionEnd.next()
- }
- function onPointerDown (ev: PointerEvent) {
- eventOffset(pointerStart, ev)
- Vec2.copy(pointerDown, pointerStart)
- if (insideBounds(pointerStart)) {
- dragging = DraggingState.Started
- }
- }
- function onPointerUp (ev: PointerEvent) {
- dragging = DraggingState.Stopped
- eventOffset(pointerEnd, ev);
- if (Vec2.distance(pointerEnd, pointerDown) < 4) {
- const { pageX, pageY } = ev
- const [ x, y ] = pointerEnd
- click.next({ x, y, pageX, pageY, buttons, modifiers: getModifierKeys() })
- }
- }
- function onPointerMove (ev: PointerEvent) {
- eventOffset(pointerEnd, ev)
- const { pageX, pageY } = ev
- const [ x, y ] = pointerEnd
- const inside = insideBounds(pointerEnd)
- move.next({ x, y, pageX, pageY, buttons, modifiers: getModifierKeys(), inside })
- if (dragging === DraggingState.Stopped) return
- Vec2.div(pointerDelta, Vec2.sub(pointerDelta, pointerEnd, pointerStart), getClientSize(rectSize))
- const isStart = dragging === DraggingState.Started
- const [ dx, dy ] = pointerDelta
- drag.next({ x, y, dx, dy, pageX, pageY, buttons, modifiers: getModifierKeys(), isStart })
- Vec2.copy(pointerStart, pointerEnd)
- dragging = DraggingState.Moving
- }
- function onMouseWheel(ev: WheelEvent) {
- if (noScroll) {
- ev.preventDefault()
- }
- let scale = 1
- switch (ev.deltaMode) {
- case 0: scale = 1; break // pixels
- case 1: scale = 40; break // lines
- case 2: scale = 800; break // pages
- }
- const dx = (ev.deltaX || 0) * scale
- const dy = (ev.deltaY || 0) * scale
- const dz = (ev.deltaZ || 0) * scale
- if (dx || dy || dz) {
- wheel.next({ dx, dy, dz, buttons, modifiers: getModifierKeys() })
- }
- }
- function onMouseEnter (ev: Event) {
- isInside = true;
- enter.next();
- }
- function onMouseLeave (ev: Event) {
- isInside = false;
- leave.next();
- }
- function onResize (ev: Event) {
- resize.next()
- }
- 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) {
- out[0] = element.clientWidth
- out[1] = element.clientHeight
- return out
- }
- function eventOffset (out: Vec2, ev: PointerEvent) {
- const cx = ev.clientX || 0
- const cy = ev.clientY || 0
- const rect = element.getBoundingClientRect()
- out[0] = cx - rect.left
- out[1] = cy - rect.top
- return out
- }
- }
- }
- export default InputObserver
|