src/viewer/TouchService.ts (289 lines of code) (raw):

import { merge as observableMerge, fromEvent as observableFromEvent, timer as observableTimer, BehaviorSubject, Observable, Subject, } from "rxjs"; import { bufferWhen, distinctUntilChanged, filter, first, map, mergeMap, publishReplay, refCount, scan, share, skip, switchMap, take, takeUntil, } from "rxjs/operators"; import { SubscriptionHolder } from "../util/SubscriptionHolder"; import { TouchPinch } from "./interfaces/TouchPinch"; interface PinchOperation { (pinch: TouchPinch): TouchPinch; } export class TouchService { private _activeSubject$: BehaviorSubject<boolean>; private _active$: Observable<boolean>; private _touchStart$: Observable<TouchEvent>; private _touchMove$: Observable<TouchEvent>; private _touchEnd$: Observable<TouchEvent>; private _touchCancel$: Observable<TouchEvent>; private _singleTouchDrag$: Observable<TouchEvent>; private _singleTouchDragStart$: Observable<TouchEvent>; private _singleTouchDragEnd$: Observable<TouchEvent>; private _singleTouchMove$: Observable<TouchEvent>; private _pinchOperation$: Subject<PinchOperation>; private _pinch$: Observable<TouchPinch>; private _pinchStart$: Observable<TouchEvent>; private _pinchEnd$: Observable<TouchEvent>; private _pinchChange$: Observable<TouchPinch>; private _doubleTap$: Observable<TouchEvent>; private _subscriptions: SubscriptionHolder = new SubscriptionHolder(); constructor(canvasContainer: HTMLElement, domContainer: HTMLElement) { const subs = this._subscriptions; this._activeSubject$ = new BehaviorSubject<boolean>(false); this._active$ = this._activeSubject$.pipe( distinctUntilChanged(), publishReplay(1), refCount()); subs.push(observableFromEvent<TouchEvent>(domContainer, "touchmove") .subscribe( (event: TouchEvent): void => { event.preventDefault(); })); this._touchStart$ = observableFromEvent<TouchEvent>(canvasContainer, "touchstart"); this._touchMove$ = observableFromEvent<TouchEvent>(canvasContainer, "touchmove"); this._touchEnd$ = observableFromEvent<TouchEvent>(canvasContainer, "touchend"); this._touchCancel$ = observableFromEvent<TouchEvent>(canvasContainer, "touchcancel"); const tapStart$: Observable<TouchEvent> = this._touchStart$.pipe( filter( (te: TouchEvent): boolean => { return te.touches.length === 1 && te.targetTouches.length === 1; }), share()); this._doubleTap$ = tapStart$.pipe( bufferWhen( (): Observable<number | TouchEvent> => { return tapStart$.pipe( first(), switchMap( (): Observable<number | TouchEvent> => { return observableMerge( observableTimer(300), tapStart$).pipe( take(1)); })); }), filter( (events: TouchEvent[]): boolean => { return events.length === 2; }), map( (events: TouchEvent[]): TouchEvent => { return events[events.length - 1]; }), share()); subs.push(this._doubleTap$ .subscribe( (event: TouchEvent): void => { event.preventDefault(); })); this._singleTouchMove$ = this._touchMove$.pipe( filter( (te: TouchEvent): boolean => { return te.touches.length === 1 && te.targetTouches.length === 1; }), share()); let singleTouchStart$: Observable<TouchEvent> = observableMerge( this._touchStart$, this._touchEnd$, this._touchCancel$).pipe( filter( (te: TouchEvent): boolean => { return te.touches.length === 1 && te.targetTouches.length === 1; })); let multipleTouchStart$: Observable<TouchEvent> = observableMerge( this._touchStart$, this._touchEnd$, this._touchCancel$).pipe( filter( (te: TouchEvent): boolean => { return te.touches.length >= 1; })); let touchStop$: Observable<TouchEvent> = observableMerge( this._touchEnd$, this._touchCancel$).pipe( filter( (te: TouchEvent): boolean => { return te.touches.length === 0; })); this._singleTouchDragStart$ = singleTouchStart$.pipe( mergeMap( (): Observable<TouchEvent> => { return this._singleTouchMove$.pipe( takeUntil( observableMerge( touchStop$, multipleTouchStart$)), take(1)); })); this._singleTouchDragEnd$ = singleTouchStart$.pipe( mergeMap( (): Observable<TouchEvent> => { return observableMerge( touchStop$, multipleTouchStart$).pipe( first()); })); this._singleTouchDrag$ = singleTouchStart$.pipe( switchMap( (): Observable<TouchEvent> => { return this._singleTouchMove$.pipe( skip(1), takeUntil( observableMerge( multipleTouchStart$, touchStop$))); })); let touchesChanged$: Observable<TouchEvent> = observableMerge( this._touchStart$, this._touchEnd$, this._touchCancel$); this._pinchStart$ = touchesChanged$.pipe( filter( (te: TouchEvent): boolean => { return te.touches.length === 2 && te.targetTouches.length === 2; })); this._pinchEnd$ = touchesChanged$.pipe( filter( (te: TouchEvent): boolean => { return te.touches.length !== 2 || te.targetTouches.length !== 2; })); this._pinchOperation$ = new Subject<PinchOperation>(); this._pinch$ = this._pinchOperation$.pipe( scan( (pinch: TouchPinch, operation: PinchOperation): TouchPinch => { return operation(pinch); }, { changeX: 0, changeY: 0, clientX: 0, clientY: 0, distance: 0, distanceChange: 0, distanceX: 0, distanceY: 0, originalEvent: null, pageX: 0, pageY: 0, screenX: 0, screenY: 0, touch1: null, touch2: null, })); const pinchSubscription = this._touchMove$.pipe( filter( (te: TouchEvent): boolean => { return te.touches.length === 2 && te.targetTouches.length === 2; }), map( (te: TouchEvent): PinchOperation => { return (previous: TouchPinch): TouchPinch => { let touch1: Touch = te.touches[0]; let touch2: Touch = te.touches[1]; let minX: number = Math.min(touch1.clientX, touch2.clientX); let maxX: number = Math.max(touch1.clientX, touch2.clientX); let minY: number = Math.min(touch1.clientY, touch2.clientY); let maxY: number = Math.max(touch1.clientY, touch2.clientY); let centerClientX: number = minX + (maxX - minX) / 2; let centerClientY: number = minY + (maxY - minY) / 2; let centerPageX: number = centerClientX + touch1.pageX - touch1.clientX; let centerPageY: number = centerClientY + touch1.pageY - touch1.clientY; let centerScreenX: number = centerClientX + touch1.screenX - touch1.clientX; let centerScreenY: number = centerClientY + touch1.screenY - touch1.clientY; let distanceX: number = Math.abs(touch1.clientX - touch2.clientX); let distanceY: number = Math.abs(touch1.clientY - touch2.clientY); let distance: number = Math.sqrt(distanceX * distanceX + distanceY * distanceY); let distanceChange: number = distance - previous.distance; let changeX: number = distanceX - previous.distanceX; let changeY: number = distanceY - previous.distanceY; let current: TouchPinch = { changeX: changeX, changeY: changeY, clientX: centerClientX, clientY: centerClientY, distance: distance, distanceChange: distanceChange, distanceX: distanceX, distanceY: distanceY, originalEvent: te, pageX: centerPageX, pageY: centerPageY, screenX: centerScreenX, screenY: centerScreenY, touch1: touch1, touch2: touch2, }; return current; }; })) .subscribe(this._pinchOperation$); subs.push(pinchSubscription); this._pinchChange$ = this._pinchStart$.pipe( switchMap( (): Observable<TouchPinch> => { return this._pinch$.pipe( skip(1), takeUntil(this._pinchEnd$)); })); } public get active$(): Observable<boolean> { return this._active$; } public get activate$(): Subject<boolean> { return this._activeSubject$; } public get doubleTap$(): Observable<TouchEvent> { return this._doubleTap$; } public get touchStart$(): Observable<TouchEvent> { return this._touchStart$; } public get touchMove$(): Observable<TouchEvent> { return this._touchMove$; } public get touchEnd$(): Observable<TouchEvent> { return this._touchEnd$; } public get touchCancel$(): Observable<TouchEvent> { return this._touchCancel$; } public get singleTouchDragStart$(): Observable<TouchEvent> { return this._singleTouchDragStart$; } public get singleTouchDrag$(): Observable<TouchEvent> { return this._singleTouchDrag$; } public get singleTouchDragEnd$(): Observable<TouchEvent> { return this._singleTouchDragEnd$; } public get pinch$(): Observable<TouchPinch> { return this._pinchChange$; } public get pinchStart$(): Observable<TouchEvent> { return this._pinchStart$; } public get pinchEnd$(): Observable<TouchEvent> { return this._pinchEnd$; } public dispose(): void { this._subscriptions.unsubscribe(); } }