mirror of
				https://github.com/retspen/webvirtcloud
				synced 2025-07-31 12:41:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			567 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			567 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable file
		
	
	
	
	
| /*
 | |
|  * noVNC: HTML5 VNC client
 | |
|  * Copyright (C) 2020 The noVNC Authors
 | |
|  * Licensed under MPL 2.0 (see LICENSE.txt)
 | |
|  *
 | |
|  * See README.md for usage and integration instructions.
 | |
|  *
 | |
|  */
 | |
| 
 | |
| const GH_NOGESTURE = 0;
 | |
| const GH_ONETAP    = 1;
 | |
| const GH_TWOTAP    = 2;
 | |
| const GH_THREETAP  = 4;
 | |
| const GH_DRAG      = 8;
 | |
| const GH_LONGPRESS = 16;
 | |
| const GH_TWODRAG   = 32;
 | |
| const GH_PINCH     = 64;
 | |
| 
 | |
| const GH_INITSTATE = 127;
 | |
| 
 | |
| const GH_MOVE_THRESHOLD = 50;
 | |
| const GH_ANGLE_THRESHOLD = 90; // Degrees
 | |
| 
 | |
| // Timeout when waiting for gestures (ms)
 | |
| const GH_MULTITOUCH_TIMEOUT = 250;
 | |
| 
 | |
| // Maximum time between press and release for a tap (ms)
 | |
| const GH_TAP_TIMEOUT = 1000;
 | |
| 
 | |
| // Timeout when waiting for longpress (ms)
 | |
| const GH_LONGPRESS_TIMEOUT = 1000;
 | |
| 
 | |
| // Timeout when waiting to decide between PINCH and TWODRAG (ms)
 | |
| const GH_TWOTOUCH_TIMEOUT = 50;
 | |
| 
 | |
| export default class GestureHandler {
 | |
|     constructor() {
 | |
|         this._target = null;
 | |
| 
 | |
|         this._state = GH_INITSTATE;
 | |
| 
 | |
|         this._tracked = [];
 | |
|         this._ignored = [];
 | |
| 
 | |
|         this._waitingRelease = false;
 | |
|         this._releaseStart = 0.0;
 | |
| 
 | |
|         this._longpressTimeoutId = null;
 | |
|         this._twoTouchTimeoutId = null;
 | |
| 
 | |
|         this._boundEventHandler = this._eventHandler.bind(this);
 | |
|     }
 | |
| 
 | |
|     attach(target) {
 | |
|         this.detach();
 | |
| 
 | |
|         this._target = target;
 | |
|         this._target.addEventListener('touchstart',
 | |
|                                       this._boundEventHandler);
 | |
|         this._target.addEventListener('touchmove',
 | |
|                                       this._boundEventHandler);
 | |
|         this._target.addEventListener('touchend',
 | |
|                                       this._boundEventHandler);
 | |
|         this._target.addEventListener('touchcancel',
 | |
|                                       this._boundEventHandler);
 | |
|     }
 | |
| 
 | |
|     detach() {
 | |
|         if (!this._target) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         this._stopLongpressTimeout();
 | |
|         this._stopTwoTouchTimeout();
 | |
| 
 | |
|         this._target.removeEventListener('touchstart',
 | |
|                                          this._boundEventHandler);
 | |
|         this._target.removeEventListener('touchmove',
 | |
|                                          this._boundEventHandler);
 | |
|         this._target.removeEventListener('touchend',
 | |
|                                          this._boundEventHandler);
 | |
|         this._target.removeEventListener('touchcancel',
 | |
|                                          this._boundEventHandler);
 | |
|         this._target = null;
 | |
|     }
 | |
| 
 | |
|     _eventHandler(e) {
 | |
|         let fn;
 | |
| 
 | |
|         e.stopPropagation();
 | |
|         e.preventDefault();
 | |
| 
 | |
|         switch (e.type) {
 | |
|             case 'touchstart':
 | |
|                 fn = this._touchStart;
 | |
|                 break;
 | |
|             case 'touchmove':
 | |
|                 fn = this._touchMove;
 | |
|                 break;
 | |
|             case 'touchend':
 | |
|             case 'touchcancel':
 | |
|                 fn = this._touchEnd;
 | |
|                 break;
 | |
|         }
 | |
| 
 | |
|         for (let i = 0; i < e.changedTouches.length; i++) {
 | |
|             let touch = e.changedTouches[i];
 | |
|             fn.call(this, touch.identifier, touch.clientX, touch.clientY);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     _touchStart(id, x, y) {
 | |
|         // Ignore any new touches if there is already an active gesture,
 | |
|         // or we're in a cleanup state
 | |
|         if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {
 | |
|             this._ignored.push(id);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Did it take too long between touches that we should no longer
 | |
|         // consider this a single gesture?
 | |
|         if ((this._tracked.length > 0) &&
 | |
|             ((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {
 | |
|             this._state = GH_NOGESTURE;
 | |
|             this._ignored.push(id);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // If we're waiting for fingers to release then we should no longer
 | |
|         // recognize new touches
 | |
|         if (this._waitingRelease) {
 | |
|             this._state = GH_NOGESTURE;
 | |
|             this._ignored.push(id);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         this._tracked.push({
 | |
|             id: id,
 | |
|             started: Date.now(),
 | |
|             active: true,
 | |
|             firstX: x,
 | |
|             firstY: y,
 | |
|             lastX: x,
 | |
|             lastY: y,
 | |
|             angle: 0
 | |
|         });
 | |
| 
 | |
|         switch (this._tracked.length) {
 | |
|             case 1:
 | |
|                 this._startLongpressTimeout();
 | |
|                 break;
 | |
| 
 | |
|             case 2:
 | |
|                 this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
 | |
|                 this._stopLongpressTimeout();
 | |
|                 break;
 | |
| 
 | |
|             case 3:
 | |
|                 this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
 | |
|                 break;
 | |
| 
 | |
|             default:
 | |
|                 this._state = GH_NOGESTURE;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     _touchMove(id, x, y) {
 | |
|         let touch = this._tracked.find(t => t.id === id);
 | |
| 
 | |
|         // If this is an update for a touch we're not tracking, ignore it
 | |
|         if (touch === undefined) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Update the touches last position with the event coordinates
 | |
|         touch.lastX = x;
 | |
|         touch.lastY = y;
 | |
| 
 | |
|         let deltaX = x - touch.firstX;
 | |
|         let deltaY = y - touch.firstY;
 | |
| 
 | |
|         // Update angle when the touch has moved
 | |
|         if ((touch.firstX !== touch.lastX) ||
 | |
|             (touch.firstY !== touch.lastY)) {
 | |
|             touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
 | |
|         }
 | |
| 
 | |
|         if (!this._hasDetectedGesture()) {
 | |
|             // Ignore moves smaller than the minimum threshold
 | |
|             if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // Can't be a tap or long press as we've seen movement
 | |
|             this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
 | |
|             this._stopLongpressTimeout();
 | |
| 
 | |
|             if (this._tracked.length !== 1) {
 | |
|                 this._state &= ~(GH_DRAG);
 | |
|             }
 | |
|             if (this._tracked.length !== 2) {
 | |
|                 this._state &= ~(GH_TWODRAG | GH_PINCH);
 | |
|             }
 | |
| 
 | |
|             // We need to figure out which of our different two touch gestures
 | |
|             // this might be
 | |
|             if (this._tracked.length === 2) {
 | |
| 
 | |
|                 // The other touch is the one where the id doesn't match
 | |
|                 let prevTouch = this._tracked.find(t => t.id !== id);
 | |
| 
 | |
|                 // How far the previous touch point has moved since start
 | |
|                 let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,
 | |
|                                                prevTouch.firstY - prevTouch.lastY);
 | |
| 
 | |
|                 // We know that the current touch moved far enough,
 | |
|                 // but unless both touches moved further than their
 | |
|                 // threshold we don't want to disqualify any gestures
 | |
|                 if (prevDeltaMove > GH_MOVE_THRESHOLD) {
 | |
| 
 | |
|                     // The angle difference between the direction of the touch points
 | |
|                     let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
 | |
|                     deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);
 | |
| 
 | |
|                     // PINCH or TWODRAG can be eliminated depending on the angle
 | |
|                     if (deltaAngle > GH_ANGLE_THRESHOLD) {
 | |
|                         this._state &= ~GH_TWODRAG;
 | |
|                     } else {
 | |
|                         this._state &= ~GH_PINCH;
 | |
|                     }
 | |
| 
 | |
|                     if (this._isTwoTouchTimeoutRunning()) {
 | |
|                         this._stopTwoTouchTimeout();
 | |
|                     }
 | |
|                 } else if (!this._isTwoTouchTimeoutRunning()) {
 | |
|                     // We can't determine the gesture right now, let's
 | |
|                     // wait and see if more events are on their way
 | |
|                     this._startTwoTouchTimeout();
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (!this._hasDetectedGesture()) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             this._pushEvent('gesturestart');
 | |
|         }
 | |
| 
 | |
|         this._pushEvent('gesturemove');
 | |
|     }
 | |
| 
 | |
|     _touchEnd(id, x, y) {
 | |
|         // Check if this is an ignored touch
 | |
|         if (this._ignored.indexOf(id) !== -1) {
 | |
|             // Remove this touch from ignored
 | |
|             this._ignored.splice(this._ignored.indexOf(id), 1);
 | |
| 
 | |
|             // And reset the state if there are no more touches
 | |
|             if ((this._ignored.length === 0) &&
 | |
|                 (this._tracked.length === 0)) {
 | |
|                 this._state = GH_INITSTATE;
 | |
|                 this._waitingRelease = false;
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // We got a touchend before the timer triggered,
 | |
|         // this cannot result in a gesture anymore.
 | |
|         if (!this._hasDetectedGesture() &&
 | |
|             this._isTwoTouchTimeoutRunning()) {
 | |
|             this._stopTwoTouchTimeout();
 | |
|             this._state = GH_NOGESTURE;
 | |
|         }
 | |
| 
 | |
|         // Some gestures don't trigger until a touch is released
 | |
|         if (!this._hasDetectedGesture()) {
 | |
|             // Can't be a gesture that relies on movement
 | |
|             this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
 | |
|             // Or something that relies on more time
 | |
|             this._state &= ~GH_LONGPRESS;
 | |
|             this._stopLongpressTimeout();
 | |
| 
 | |
|             if (!this._waitingRelease) {
 | |
|                 this._releaseStart = Date.now();
 | |
|                 this._waitingRelease = true;
 | |
| 
 | |
|                 // Can't be a tap that requires more touches than we current have
 | |
|                 switch (this._tracked.length) {
 | |
|                     case 1:
 | |
|                         this._state &= ~(GH_TWOTAP | GH_THREETAP);
 | |
|                         break;
 | |
| 
 | |
|                     case 2:
 | |
|                         this._state &= ~(GH_ONETAP | GH_THREETAP);
 | |
|                         break;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Waiting for all touches to release? (i.e. some tap)
 | |
|         if (this._waitingRelease) {
 | |
|             // Were all touches released at roughly the same time?
 | |
|             if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {
 | |
|                 this._state = GH_NOGESTURE;
 | |
|             }
 | |
| 
 | |
|             // Did too long time pass between press and release?
 | |
|             if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {
 | |
|                 this._state = GH_NOGESTURE;
 | |
|             }
 | |
| 
 | |
|             let touch = this._tracked.find(t => t.id === id);
 | |
|             touch.active = false;
 | |
| 
 | |
|             // Are we still waiting for more releases?
 | |
|             if (this._hasDetectedGesture()) {
 | |
|                 this._pushEvent('gesturestart');
 | |
|             } else {
 | |
|                 // Have we reached a dead end?
 | |
|                 if (this._state !== GH_NOGESTURE) {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (this._hasDetectedGesture()) {
 | |
|             this._pushEvent('gestureend');
 | |
|         }
 | |
| 
 | |
|         // Ignore any remaining touches until they are ended
 | |
|         for (let i = 0; i < this._tracked.length; i++) {
 | |
|             if (this._tracked[i].active) {
 | |
|                 this._ignored.push(this._tracked[i].id);
 | |
|             }
 | |
|         }
 | |
|         this._tracked = [];
 | |
| 
 | |
|         this._state = GH_NOGESTURE;
 | |
| 
 | |
|         // Remove this touch from ignored if it's in there
 | |
|         if (this._ignored.indexOf(id) !== -1) {
 | |
|             this._ignored.splice(this._ignored.indexOf(id), 1);
 | |
|         }
 | |
| 
 | |
|         // We reset the state if ignored is empty
 | |
|         if ((this._ignored.length === 0)) {
 | |
|             this._state = GH_INITSTATE;
 | |
|             this._waitingRelease = false;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     _hasDetectedGesture() {
 | |
|         if (this._state === GH_NOGESTURE) {
 | |
|             return false;
 | |
|         }
 | |
|         // Check to see if the bitmask value is a power of 2
 | |
|         // (i.e. only one bit set). If it is, we have a state.
 | |
|         if (this._state & (this._state - 1)) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // For taps we also need to have all touches released
 | |
|         // before we've fully detected the gesture
 | |
|         if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
 | |
|             if (this._tracked.some(t => t.active)) {
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     _startLongpressTimeout() {
 | |
|         this._stopLongpressTimeout();
 | |
|         this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),
 | |
|                                               GH_LONGPRESS_TIMEOUT);
 | |
|     }
 | |
| 
 | |
|     _stopLongpressTimeout() {
 | |
|         clearTimeout(this._longpressTimeoutId);
 | |
|         this._longpressTimeoutId = null;
 | |
|     }
 | |
| 
 | |
|     _longpressTimeout() {
 | |
|         if (this._hasDetectedGesture()) {
 | |
|             throw new Error("A longpress gesture failed, conflict with a different gesture");
 | |
|         }
 | |
| 
 | |
|         this._state = GH_LONGPRESS;
 | |
|         this._pushEvent('gesturestart');
 | |
|     }
 | |
| 
 | |
|     _startTwoTouchTimeout() {
 | |
|         this._stopTwoTouchTimeout();
 | |
|         this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),
 | |
|                                              GH_TWOTOUCH_TIMEOUT);
 | |
|     }
 | |
| 
 | |
|     _stopTwoTouchTimeout() {
 | |
|         clearTimeout(this._twoTouchTimeoutId);
 | |
|         this._twoTouchTimeoutId = null;
 | |
|     }
 | |
| 
 | |
|     _isTwoTouchTimeoutRunning() {
 | |
|         return this._twoTouchTimeoutId !== null;
 | |
|     }
 | |
| 
 | |
|     _twoTouchTimeout() {
 | |
|         if (this._tracked.length === 0) {
 | |
|             throw new Error("A pinch or two drag gesture failed, no tracked touches");
 | |
|         }
 | |
| 
 | |
|         // How far each touch point has moved since start
 | |
|         let avgM = this._getAverageMovement();
 | |
|         let avgMoveH = Math.abs(avgM.x);
 | |
|         let avgMoveV = Math.abs(avgM.y);
 | |
| 
 | |
|         // The difference in the distance between where
 | |
|         // the touch points started and where they are now
 | |
|         let avgD = this._getAverageDistance();
 | |
|         let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -
 | |
|                                           Math.hypot(avgD.last.x, avgD.last.y));
 | |
| 
 | |
|         if ((avgMoveV < deltaTouchDistance) &&
 | |
|             (avgMoveH < deltaTouchDistance)) {
 | |
|             this._state = GH_PINCH;
 | |
|         } else {
 | |
|             this._state = GH_TWODRAG;
 | |
|         }
 | |
| 
 | |
|         this._pushEvent('gesturestart');
 | |
|         this._pushEvent('gesturemove');
 | |
|     }
 | |
| 
 | |
|     _pushEvent(type) {
 | |
|         let detail = { type: this._stateToGesture(this._state) };
 | |
| 
 | |
|         // For most gesture events the current (average) position is the
 | |
|         // most useful
 | |
|         let avg = this._getPosition();
 | |
|         let pos = avg.last;
 | |
| 
 | |
|         // However we have a slight distance to detect gestures, so for the
 | |
|         // first gesture event we want to use the first positions we saw
 | |
|         if (type === 'gesturestart') {
 | |
|             pos = avg.first;
 | |
|         }
 | |
| 
 | |
|         // For these gestures, we always want the event coordinates
 | |
|         // to be where the gesture began, not the current touch location.
 | |
|         switch (this._state) {
 | |
|             case GH_TWODRAG:
 | |
|             case GH_PINCH:
 | |
|                 pos = avg.first;
 | |
|                 break;
 | |
|         }
 | |
| 
 | |
|         detail['clientX'] = pos.x;
 | |
|         detail['clientY'] = pos.y;
 | |
| 
 | |
|         // FIXME: other coordinates?
 | |
| 
 | |
|         // Some gestures also have a magnitude
 | |
|         if (this._state === GH_PINCH) {
 | |
|             let distance = this._getAverageDistance();
 | |
|             if (type === 'gesturestart') {
 | |
|                 detail['magnitudeX'] = distance.first.x;
 | |
|                 detail['magnitudeY'] = distance.first.y;
 | |
|             } else {
 | |
|                 detail['magnitudeX'] = distance.last.x;
 | |
|                 detail['magnitudeY'] = distance.last.y;
 | |
|             }
 | |
|         } else if (this._state === GH_TWODRAG) {
 | |
|             if (type === 'gesturestart') {
 | |
|                 detail['magnitudeX'] = 0.0;
 | |
|                 detail['magnitudeY'] = 0.0;
 | |
|             } else {
 | |
|                 let movement = this._getAverageMovement();
 | |
|                 detail['magnitudeX'] = movement.x;
 | |
|                 detail['magnitudeY'] = movement.y;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let gev = new CustomEvent(type, { detail: detail });
 | |
|         this._target.dispatchEvent(gev);
 | |
|     }
 | |
| 
 | |
|     _stateToGesture(state) {
 | |
|         switch (state) {
 | |
|             case GH_ONETAP:
 | |
|                 return 'onetap';
 | |
|             case GH_TWOTAP:
 | |
|                 return 'twotap';
 | |
|             case GH_THREETAP:
 | |
|                 return 'threetap';
 | |
|             case GH_DRAG:
 | |
|                 return 'drag';
 | |
|             case GH_LONGPRESS:
 | |
|                 return 'longpress';
 | |
|             case GH_TWODRAG:
 | |
|                 return 'twodrag';
 | |
|             case GH_PINCH:
 | |
|                 return 'pinch';
 | |
|         }
 | |
| 
 | |
|         throw new Error("Unknown gesture state: " + state);
 | |
|     }
 | |
| 
 | |
|     _getPosition() {
 | |
|         if (this._tracked.length === 0) {
 | |
|             throw new Error("Failed to get gesture position, no tracked touches");
 | |
|         }
 | |
| 
 | |
|         let size = this._tracked.length;
 | |
|         let fx = 0, fy = 0, lx = 0, ly = 0;
 | |
| 
 | |
|         for (let i = 0; i < this._tracked.length; i++) {
 | |
|             fx += this._tracked[i].firstX;
 | |
|             fy += this._tracked[i].firstY;
 | |
|             lx += this._tracked[i].lastX;
 | |
|             ly += this._tracked[i].lastY;
 | |
|         }
 | |
| 
 | |
|         return { first: { x: fx / size,
 | |
|                           y: fy / size },
 | |
|                  last: { x: lx / size,
 | |
|                          y: ly / size } };
 | |
|     }
 | |
| 
 | |
|     _getAverageMovement() {
 | |
|         if (this._tracked.length === 0) {
 | |
|             throw new Error("Failed to get gesture movement, no tracked touches");
 | |
|         }
 | |
| 
 | |
|         let totalH, totalV;
 | |
|         totalH = totalV = 0;
 | |
|         let size = this._tracked.length;
 | |
| 
 | |
|         for (let i = 0; i < this._tracked.length; i++) {
 | |
|             totalH += this._tracked[i].lastX - this._tracked[i].firstX;
 | |
|             totalV += this._tracked[i].lastY - this._tracked[i].firstY;
 | |
|         }
 | |
| 
 | |
|         return { x: totalH / size,
 | |
|                  y: totalV / size };
 | |
|     }
 | |
| 
 | |
|     _getAverageDistance() {
 | |
|         if (this._tracked.length === 0) {
 | |
|             throw new Error("Failed to get gesture distance, no tracked touches");
 | |
|         }
 | |
| 
 | |
|         // Distance between the first and last tracked touches
 | |
| 
 | |
|         let first = this._tracked[0];
 | |
|         let last = this._tracked[this._tracked.length - 1];
 | |
| 
 | |
|         let fdx = Math.abs(last.firstX - first.firstX);
 | |
|         let fdy = Math.abs(last.firstY - first.firstY);
 | |
| 
 | |
|         let ldx = Math.abs(last.lastX - first.lastX);
 | |
|         let ldy = Math.abs(last.lastY - first.lastY);
 | |
| 
 | |
|         return { first: { x: fdx, y: fdy },
 | |
|                  last: { x: ldx, y: ldy } };
 | |
|     }
 | |
| }
 |