SPB Git forge

spb/admin-ka

Public
41commits 1branches 0releases
172.9 MBsize
maindefault branch
20 days agolast push
JavaScript 65.5% Python 17.8% CSS 13% HTML 3.7%
17.8 KB · 568 lines javascript
Raw Blame History
1/*2 * noVNC: HTML5 VNC client3 * Copyright (C) 2020 The noVNC authors4 * Licensed under MPL 2.0 (see LICENSE.txt)5 *6 * See README.md for usage and integration instructions.7 *8 */910const GH_NOGESTURE = 0;11const GH_ONETAP    = 1;12const GH_TWOTAP    = 2;13const GH_THREETAP  = 4;14const GH_DRAG      = 8;15const GH_LONGPRESS = 16;16const GH_TWODRAG   = 32;17const GH_PINCH     = 64;1819const GH_INITSTATE = 127;2021const GH_MOVE_THRESHOLD = 50;22const GH_ANGLE_THRESHOLD = 90; // Degrees2324// Timeout when waiting for gestures (ms)25const GH_MULTITOUCH_TIMEOUT = 250;2627// Maximum time between press and release for a tap (ms)28const GH_TAP_TIMEOUT = 1000;2930// Timeout when waiting for longpress (ms)31const GH_LONGPRESS_TIMEOUT = 1000;3233// Timeout when waiting to decide between PINCH and TWODRAG (ms)34const GH_TWOTOUCH_TIMEOUT = 50;3536export default class GestureHandler {37    constructor() {38        this._target = null;3940        this._state = GH_INITSTATE;4142        this._tracked = [];43        this._ignored = [];4445        this._waitingRelease = false;46        this._releaseStart = 0.0;4748        this._longpressTimeoutId = null;49        this._twoTouchTimeoutId = null;5051        this._boundEventHandler = this._eventHandler.bind(this);52    }5354    attach(target) {55        this.detach();5657        this._target = target;58        this._target.addEventListener('touchstart',59                                      this._boundEventHandler);60        this._target.addEventListener('touchmove',61                                      this._boundEventHandler);62        this._target.addEventListener('touchend',63                                      this._boundEventHandler);64        this._target.addEventListener('touchcancel',65                                      this._boundEventHandler);66    }6768    detach() {69        if (!this._target) {70            return;71        }7273        this._stopLongpressTimeout();74        this._stopTwoTouchTimeout();7576        this._target.removeEventListener('touchstart',77                                         this._boundEventHandler);78        this._target.removeEventListener('touchmove',79                                         this._boundEventHandler);80        this._target.removeEventListener('touchend',81                                         this._boundEventHandler);82        this._target.removeEventListener('touchcancel',83                                         this._boundEventHandler);84        this._target = null;85    }8687    _eventHandler(e) {88        let fn;8990        e.stopPropagation();91        e.preventDefault();9293        switch (e.type) {94            case 'touchstart':95                fn = this._touchStart;96                break;97            case 'touchmove':98                fn = this._touchMove;99                break;100            case 'touchend':101            case 'touchcancel':102                fn = this._touchEnd;103                break;104        }105106        for (let i = 0; i < e.changedTouches.length; i++) {107            let touch = e.changedTouches[i];108            fn.call(this, touch.identifier, touch.clientX, touch.clientY);109        }110    }111112    _touchStart(id, x, y) {113        // Ignore any new touches if there is already an active gesture,114        // or we're in a cleanup state115        if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {116            this._ignored.push(id);117            return;118        }119120        // Did it take too long between touches that we should no longer121        // consider this a single gesture?122        if ((this._tracked.length > 0) &&123            ((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {124            this._state = GH_NOGESTURE;125            this._ignored.push(id);126            return;127        }128129        // If we're waiting for fingers to release then we should no longer130        // recognize new touches131        if (this._waitingRelease) {132            this._state = GH_NOGESTURE;133            this._ignored.push(id);134            return;135        }136137        this._tracked.push({138            id: id,139            started: Date.now(),140            active: true,141            firstX: x,142            firstY: y,143            lastX: x,144            lastY: y,145            angle: 0146        });147148        switch (this._tracked.length) {149            case 1:150                this._startLongpressTimeout();151                break;152153            case 2:154                this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);155                this._stopLongpressTimeout();156                break;157158            case 3:159                this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);160                break;161162            default:163                this._state = GH_NOGESTURE;164        }165    }166167    _touchMove(id, x, y) {168        let touch = this._tracked.find(t => t.id === id);169170        // If this is an update for a touch we're not tracking, ignore it171        if (touch === undefined) {172            return;173        }174175        // Update the touches last position with the event coordinates176        touch.lastX = x;177        touch.lastY = y;178179        let deltaX = x - touch.firstX;180        let deltaY = y - touch.firstY;181182        // Update angle when the touch has moved183        if ((touch.firstX !== touch.lastX) ||184            (touch.firstY !== touch.lastY)) {185            touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;186        }187188        if (!this._hasDetectedGesture()) {189            // Ignore moves smaller than the minimum threshold190            if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {191                return;192            }193194            // Can't be a tap or long press as we've seen movement195            this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);196            this._stopLongpressTimeout();197198            if (this._tracked.length !== 1) {199                this._state &= ~(GH_DRAG);200            }201            if (this._tracked.length !== 2) {202                this._state &= ~(GH_TWODRAG | GH_PINCH);203            }204205            // We need to figure out which of our different two touch gestures206            // this might be207            if (this._tracked.length === 2) {208209                // The other touch is the one where the id doesn't match210                let prevTouch = this._tracked.find(t => t.id !== id);211212                // How far the previous touch point has moved since start213                let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,214                                               prevTouch.firstY - prevTouch.lastY);215216                // We know that the current touch moved far enough,217                // but unless both touches moved further than their218                // threshold we don't want to disqualify any gestures219                if (prevDeltaMove > GH_MOVE_THRESHOLD) {220221                    // The angle difference between the direction of the touch points222                    let deltaAngle = Math.abs(touch.angle - prevTouch.angle);223                    deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);224225                    // PINCH or TWODRAG can be eliminated depending on the angle226                    if (deltaAngle > GH_ANGLE_THRESHOLD) {227                        this._state &= ~GH_TWODRAG;228                    } else {229                        this._state &= ~GH_PINCH;230                    }231232                    if (this._isTwoTouchTimeoutRunning()) {233                        this._stopTwoTouchTimeout();234                    }235                } else if (!this._isTwoTouchTimeoutRunning()) {236                    // We can't determine the gesture right now, let's237                    // wait and see if more events are on their way238                    this._startTwoTouchTimeout();239                }240            }241242            if (!this._hasDetectedGesture()) {243                return;244            }245246            this._pushEvent('gesturestart');247        }248249        this._pushEvent('gesturemove');250    }251252    _touchEnd(id, x, y) {253        // Check if this is an ignored touch254        if (this._ignored.indexOf(id) !== -1) {255            // Remove this touch from ignored256            this._ignored.splice(this._ignored.indexOf(id), 1);257258            // And reset the state if there are no more touches259            if ((this._ignored.length === 0) &&260                (this._tracked.length === 0)) {261                this._state = GH_INITSTATE;262                this._waitingRelease = false;263            }264            return;265        }266267        // We got a touchend before the timer triggered,268        // this cannot result in a gesture anymore.269        if (!this._hasDetectedGesture() &&270            this._isTwoTouchTimeoutRunning()) {271            this._stopTwoTouchTimeout();272            this._state = GH_NOGESTURE;273        }274275        // Some gestures don't trigger until a touch is released276        if (!this._hasDetectedGesture()) {277            // Can't be a gesture that relies on movement278            this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);279            // Or something that relies on more time280            this._state &= ~GH_LONGPRESS;281            this._stopLongpressTimeout();282283            if (!this._waitingRelease) {284                this._releaseStart = Date.now();285                this._waitingRelease = true;286287                // Can't be a tap that requires more touches than we current have288                switch (this._tracked.length) {289                    case 1:290                        this._state &= ~(GH_TWOTAP | GH_THREETAP);291                        break;292293                    case 2:294                        this._state &= ~(GH_ONETAP | GH_THREETAP);295                        break;296                }297            }298        }299300        // Waiting for all touches to release? (i.e. some tap)301        if (this._waitingRelease) {302            // Were all touches released at roughly the same time?303            if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {304                this._state = GH_NOGESTURE;305            }306307            // Did too long time pass between press and release?308            if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {309                this._state = GH_NOGESTURE;310            }311312            let touch = this._tracked.find(t => t.id === id);313            touch.active = false;314315            // Are we still waiting for more releases?316            if (this._hasDetectedGesture()) {317                this._pushEvent('gesturestart');318            } else {319                // Have we reached a dead end?320                if (this._state !== GH_NOGESTURE) {321                    return;322                }323            }324        }325326        if (this._hasDetectedGesture()) {327            this._pushEvent('gestureend');328        }329330        // Ignore any remaining touches until they are ended331        for (let i = 0; i < this._tracked.length; i++) {332            if (this._tracked[i].active) {333                this._ignored.push(this._tracked[i].id);334            }335        }336        this._tracked = [];337338        this._state = GH_NOGESTURE;339340        // Remove this touch from ignored if it's in there341        if (this._ignored.indexOf(id) !== -1) {342            this._ignored.splice(this._ignored.indexOf(id), 1);343        }344345        // We reset the state if ignored is empty346        if ((this._ignored.length === 0)) {347            this._state = GH_INITSTATE;348            this._waitingRelease = false;349        }350    }351352    _hasDetectedGesture() {353        if (this._state === GH_NOGESTURE) {354            return false;355        }356        // Check to see if the bitmask value is a power of 2357        // (i.e. only one bit set). If it is, we have a state.358        if (this._state & (this._state - 1)) {359            return false;360        }361362        // For taps we also need to have all touches released363        // before we've fully detected the gesture364        if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {365            if (this._tracked.some(t => t.active)) {366                return false;367            }368        }369370        return true;371    }372373    _startLongpressTimeout() {374        this._stopLongpressTimeout();375        this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),376                                              GH_LONGPRESS_TIMEOUT);377    }378379    _stopLongpressTimeout() {380        clearTimeout(this._longpressTimeoutId);381        this._longpressTimeoutId = null;382    }383384    _longpressTimeout() {385        if (this._hasDetectedGesture()) {386            throw new Error("A longpress gesture failed, conflict with a different gesture");387        }388389        this._state = GH_LONGPRESS;390        this._pushEvent('gesturestart');391    }392393    _startTwoTouchTimeout() {394        this._stopTwoTouchTimeout();395        this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),396                                             GH_TWOTOUCH_TIMEOUT);397    }398399    _stopTwoTouchTimeout() {400        clearTimeout(this._twoTouchTimeoutId);401        this._twoTouchTimeoutId = null;402    }403404    _isTwoTouchTimeoutRunning() {405        return this._twoTouchTimeoutId !== null;406    }407408    _twoTouchTimeout() {409        if (this._tracked.length === 0) {410            throw new Error("A pinch or two drag gesture failed, no tracked touches");411        }412413        // How far each touch point has moved since start414        let avgM = this._getAverageMovement();415        let avgMoveH = Math.abs(avgM.x);416        let avgMoveV = Math.abs(avgM.y);417418        // The difference in the distance between where419        // the touch points started and where they are now420        let avgD = this._getAverageDistance();421        let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -422                                          Math.hypot(avgD.last.x, avgD.last.y));423424        if ((avgMoveV < deltaTouchDistance) &&425            (avgMoveH < deltaTouchDistance)) {426            this._state = GH_PINCH;427        } else {428            this._state = GH_TWODRAG;429        }430431        this._pushEvent('gesturestart');432        this._pushEvent('gesturemove');433    }434435    _pushEvent(type) {436        let detail = { type: this._stateToGesture(this._state) };437438        // For most gesture events the current (average) position is the439        // most useful440        let avg = this._getPosition();441        let pos = avg.last;442443        // However we have a slight distance to detect gestures, so for the444        // first gesture event we want to use the first positions we saw445        if (type === 'gesturestart') {446            pos = avg.first;447        }448449        // For these gestures, we always want the event coordinates450        // to be where the gesture began, not the current touch location.451        switch (this._state) {452            case GH_TWODRAG:453            case GH_PINCH:454                pos = avg.first;455                break;456        }457458        detail['clientX'] = pos.x;459        detail['clientY'] = pos.y;460461        // FIXME: other coordinates?462463        // Some gestures also have a magnitude464        if (this._state === GH_PINCH) {465            let distance = this._getAverageDistance();466            if (type === 'gesturestart') {467                detail['magnitudeX'] = distance.first.x;468                detail['magnitudeY'] = distance.first.y;469            } else {470                detail['magnitudeX'] = distance.last.x;471                detail['magnitudeY'] = distance.last.y;472            }473        } else if (this._state === GH_TWODRAG) {474            if (type === 'gesturestart') {475                detail['magnitudeX'] = 0.0;476                detail['magnitudeY'] = 0.0;477            } else {478                let movement = this._getAverageMovement();479                detail['magnitudeX'] = movement.x;480                detail['magnitudeY'] = movement.y;481            }482        }483484        let gev = new CustomEvent(type, { detail: detail });485        this._target.dispatchEvent(gev);486    }487488    _stateToGesture(state) {489        switch (state) {490            case GH_ONETAP:491                return 'onetap';492            case GH_TWOTAP:493                return 'twotap';494            case GH_THREETAP:495                return 'threetap';496            case GH_DRAG:497                return 'drag';498            case GH_LONGPRESS:499                return 'longpress';500            case GH_TWODRAG:501                return 'twodrag';502            case GH_PINCH:503                return 'pinch';504        }505506        throw new Error("Unknown gesture state: " + state);507    }508509    _getPosition() {510        if (this._tracked.length === 0) {511            throw new Error("Failed to get gesture position, no tracked touches");512        }513514        let size = this._tracked.length;515        let fx = 0, fy = 0, lx = 0, ly = 0;516517        for (let i = 0; i < this._tracked.length; i++) {518            fx += this._tracked[i].firstX;519            fy += this._tracked[i].firstY;520            lx += this._tracked[i].lastX;521            ly += this._tracked[i].lastY;522        }523524        return { first: { x: fx / size,525                          y: fy / size },526                 last: { x: lx / size,527                         y: ly / size } };528    }529530    _getAverageMovement() {531        if (this._tracked.length === 0) {532            throw new Error("Failed to get gesture movement, no tracked touches");533        }534535        let totalH, totalV;536        totalH = totalV = 0;537        let size = this._tracked.length;538539        for (let i = 0; i < this._tracked.length; i++) {540            totalH += this._tracked[i].lastX - this._tracked[i].firstX;541            totalV += this._tracked[i].lastY - this._tracked[i].firstY;542        }543544        return { x: totalH / size,545                 y: totalV / size };546    }547548    _getAverageDistance() {549        if (this._tracked.length === 0) {550            throw new Error("Failed to get gesture distance, no tracked touches");551        }552553        // Distance between the first and last tracked touches554555        let first = this._tracked[0];556        let last = this._tracked[this._tracked.length - 1];557558        let fdx = Math.abs(last.firstX - first.firstX);559        let fdy = Math.abs(last.firstY - first.firstY);560561        let ldx = Math.abs(last.lastX - first.lastX);562        let ldy = Math.abs(last.lastY - first.lastY);563564        return { first: { x: fdx, y: fdy },565                 last: { x: ldx, y: ldy } };566    }567}568