/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/gesture-recognizers.js
955 строк
22 KB
Scott Rippey
✨ [Bento] Implemented bento-pan-zoom component (#37863)
19 апр 2022, 17:29
Не верифицирован
19 апр 2022, 17:29
fb56d5a
Код
Авторство
О чём код?
import {GestureRecognizer} from './gesture'; import {calcVelocity} from './motion'; const DOUBLETAP_DELAY = 200; /** * A "tap" gesture. * @typedef {{ * clientX: number, * clientY: number * }} */ export let TapDef; /** * Recognizes "tap" gestures. * @extends {GestureRecognizer<TapDef>} */ export class TapRecognizer extends GestureRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('tap', manager); /** @private {number} */ this.startX_ = 0; /** @private {number} */ this.startY_ = 0; /** @private {number} */ this.lastX_ = 0; /** @private {number} */ this.lastY_ = 0; /** @private {?EventTarget} */ this.target_ = null; } /** @override */ onTouchStart(e) { const {touches} = e; this.target_ = e.target; if (touches && touches.length == 1) { this.startX_ = touches[0].clientX; this.startY_ = touches[0].clientY; return true; } else { return false; } } /** @override */ onTouchMove(e) { const touches = e.changedTouches || e.touches; if (touches && touches.length == 1) { this.lastX_ = touches[0].clientX; this.lastY_ = touches[0].clientY; const dx = Math.abs(this.lastX_ - this.startX_) >= 8; const dy = Math.abs(this.lastY_ - this.startY_) >= 8; if (dx || dy) { return false; } } return true; } /** @override */ onTouchEnd(unusedE) { this.signalReady(0); } /** @override */ acceptStart() { this.signalEmit( { clientX: this.lastX_, clientY: this.lastY_, target: this.target_, }, null ); this.signalEnd(); } } /** * A "doubletap" gesture. * @typedef {{ * clientX: number, * clientY: number * }} */ export let DoubletapDef; /** * Recognizes a "doubletap" gesture. This gesture will block a single "tap" * for about 200ms while it's expecting the second "tap". * @extends {GestureRecognizer<DoubletapDef>} */ export class DoubletapRecognizer extends GestureRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('doubletap', manager); /** @private {number} */ this.startX_ = 0; /** @private {number} */ this.startY_ = 0; /** @private {number} */ this.lastX_ = 0; /** @private {number} */ this.lastY_ = 0; /** @private {number} */ this.tapCount_ = 0; /** @private {?Event} */ this.event_ = null; } /** @override */ onTouchStart(e) { if (this.tapCount_ > 1) { return false; } const {touches} = e; if (touches && touches.length == 1) { this.startX_ = touches[0].clientX; this.startY_ = touches[0].clientY; this.lastX_ = touches[0].clientX; this.lastY_ = touches[0].clientY; return true; } else { return false; } } /** @override */ onTouchMove(e) { const {touches} = e; if (touches && touches.length == 1) { this.lastX_ = touches[0].clientX; this.lastY_ = touches[0].clientY; const dx = Math.abs(this.lastX_ - this.startX_) >= 8; const dy = Math.abs(this.lastY_ - this.startY_) >= 8; if (dx || dy) { this.acceptCancel(); return false; } return true; } else { return false; } } /** @override */ onTouchEnd(e) { this.tapCount_++; if (this.tapCount_ < 2) { this.signalPending(DOUBLETAP_DELAY); } else { this.event_ = e; this.signalReady(0); } } /** @override */ acceptStart() { this.tapCount_ = 0; this.signalEmit({clientX: this.lastX_, clientY: this.lastY_}, this.event_); this.signalEnd(); } /** @override */ acceptCancel() { this.tapCount_ = 0; } } /** * A "swipe-xy", "swipe-x" or "swipe-y" gesture. A number of these gestures * may be emitted for a single touch series. * @typedef {{ * first: boolean, * last: boolean, * deltaX: number, * deltaY: number, * velocityX: number, * velocityY: number * }} */ export let SwipeDef; /** * Recognizes swipe gestures. This gesture will yield about 10ms to other * gestures. * @extends {GestureRecognizer<SwipeDef>} */ class SwipeRecognizer extends GestureRecognizer { /** * @param {string} type * @param {!./gesture.Gestures} manager * @param {boolean} horiz * @param {boolean} vert */ constructor(type, manager, horiz, vert) { super(type, manager); /** @private {boolean} */ this.horiz_ = horiz; /** @private {boolean} */ this.vert_ = vert; /** @private {boolean} */ this.eventing_ = false; /** @private {number} */ this.startX_ = 0; /** @private {number} */ this.startY_ = 0; /** @private {number} */ this.lastX_ = 0; /** @private {number} */ this.lastY_ = 0; /** @private {number} */ this.prevX_ = 0; /** @private {number} */ this.prevY_ = 0; /** @private {time} */ this.startTime_ = 0; /** @private {time} */ this.lastTime_ = 0; /** @private {time} */ this.prevTime_ = 0; /** @private {number} */ this.velocityX_ = 0; /** @private {number} */ this.velocityY_ = 0; } /** @override */ onTouchStart(e) { const {touches} = e; // If already eventing, ignore additional touches if (this.eventing_ && touches && touches.length > 1) { return true; } if (touches && touches.length == 1) { this.startTime_ = Date.now(); this.startX_ = touches[0].clientX; this.startY_ = touches[0].clientY; return true; } else { return false; } } /** @override */ onTouchMove(e) { const {touches} = e; if (touches && touches.length >= 1) { const {clientX: x, clientY: y} = touches[0]; this.lastX_ = x; this.lastY_ = y; if (this.eventing_) { // If already eventing, always emit new coordinates this.emit_(false, false, e); } else { // Figure out whether or not we should start eventing const dx = Math.abs(x - this.startX_); const dy = Math.abs(y - this.startY_); // Swipe is penalized slightly since it's one of the least demanding // gesture, thus -10 in signalReady. if (this.horiz_ && this.vert_) { if (dx >= 8 || dy >= 8) { this.signalReady(-10); } } else if (this.horiz_) { if (dx >= 8 && dx > dy) { this.signalReady(-10); } else if (dy >= 8) { return false; } } else if (this.vert_) { if (dy >= 8 && dy > dx) { this.signalReady(-10); } else if (dx >= 8) { return false; } } else { return false; } } return true; } else { return false; } } /** @override */ onTouchEnd(e) { const {touches} = e; // Number of current touches on the page if (touches && touches.length == 0) { this.end_(e); } } /** @override */ acceptStart() { this.eventing_ = true; // Reset start coordinates to where the gesture began to avoid visible // jump, but preserve them as "prev" coordinates to calculate the right // velocity. this.prevX_ = this.startX_; this.prevY_ = this.startY_; this.prevTime_ = this.startTime_; this.startX_ = this.lastX_; this.startY_ = this.lastY_; this.emit_(true, false, null); } /** @override */ acceptCancel() { this.eventing_ = false; } /** * @param {boolean} first * @param {boolean} last * @param {?Event} event * @private */ emit_(first, last, event) { this.lastTime_ = Date.now(); const deltaTime = this.lastTime_ - this.prevTime_; // It's often that `touchend` arrives on the next frame. These should // be ignored to avoid a significant velocity downgrade. if ((!last && deltaTime > 4) || (last && deltaTime > 16)) { const velocityX = calcVelocity( this.lastX_ - this.prevX_, deltaTime, this.velocityX_ ); const velocityY = calcVelocity( this.lastY_ - this.prevY_, deltaTime, this.velocityY_ ); // On iOS, the touchend will always have the same x/y position as the // last touchmove, so we want to make sure we do not remove the velocity. // The touchend event with zero velocity can occur within a couple of // frames of the last touchmove. if (!last || deltaTime > 32 || velocityX != 0 || velocityY != 0) { this.velocityX_ = Math.abs(velocityX) > 1e-4 ? velocityX : 0; this.velocityY_ = Math.abs(velocityY) > 1e-4 ? velocityY : 0; } this.prevX_ = this.lastX_; this.prevY_ = this.lastY_; this.prevTime_ = this.lastTime_; } this.signalEmit( { first, last, time: this.lastTime_, deltaX: this.lastX_ - this.startX_, deltaY: this.lastY_ - this.startY_, startX: this.startX_, startY: this.startY_, lastX: this.lastX_, lastY: this.lastY_, velocityX: this.velocityX_, velocityY: this.velocityY_, }, event ); } /** * @param {?Event} event * @private */ end_(event) { if (this.eventing_) { this.eventing_ = false; this.emit_(false, true, event); this.signalEnd(); } } } /** * Recognizes "swipe-xy" gesture. Yields about 10ms to other gestures. */ export class SwipeXYRecognizer extends SwipeRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('swipe-xy', manager, true, true); } } /** * Recognizes "swipe-x" gesture. Yields about 10ms to other gestures. */ export class SwipeXRecognizer extends SwipeRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('swipe-x', manager, true, false); } } /** * Recognizes "swipe-y" gesture. Yields about 10ms to other gestures. */ export class SwipeYRecognizer extends SwipeRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('swipe-y', manager, false, true); } } /** * A "tapzoom" gesture. It has a center, delta off the center center and * the velocity of moving away from the center. * @typedef {{ * first: boolean, * last: boolean, * centerClientX: number, * centerClientY: number, * deltaX: number, * deltaY: number, * velocityX: number, * velocityY: number * }} */ export let TapzoomDef; /** * Recognizes a "tapzoom" gesture. This gesture will block other gestures * for about 400ms after first "tap" while it's expecting swipe. * @extends {GestureRecognizer<TapzoomDef>} */ export class TapzoomRecognizer extends GestureRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('tapzoom', manager); /** @private {boolean} */ this.eventing_ = false; /** @private {number} */ this.startX_ = 0; /** @private {number} */ this.startY_ = 0; /** @private {number} */ this.lastX_ = 0; /** @private {number} */ this.lastY_ = 0; /** @private {number} */ this.tapCount_ = 0; /** @private {number} */ this.prevX_ = 0; /** @private {number} */ this.prevY_ = 0; /** @private {time} */ this.lastTime_ = 0; /** @private {time} */ this.prevTime_ = 0; /** @private {number} */ this.velocityX_ = 0; /** @private {number} */ this.velocityY_ = 0; } /** @override */ onTouchStart(e) { if (this.eventing_) { return false; } const {touches} = e; if (touches && touches.length == 1) { this.startX_ = touches[0].clientX; this.startY_ = touches[0].clientY; return true; } else { return false; } } /** @override */ onTouchMove(e) { const {touches} = e; if (touches && touches.length == 1) { this.lastX_ = touches[0].clientX; this.lastY_ = touches[0].clientY; if (this.eventing_) { this.emit_(false, false, e); } else { const dx = Math.abs(this.lastX_ - this.startX_) >= 8; const dy = Math.abs(this.lastY_ - this.startY_) >= 8; if (dx || dy) { if (this.tapCount_ == 0) { this.acceptCancel(); return false; } else { this.signalReady(0); } } } return true; } else { return false; } } /** @override */ onTouchEnd(e) { if (this.eventing_) { this.end_(e); return; } this.tapCount_++; if (this.tapCount_ == 1) { this.signalPending(400); return; } this.acceptCancel(); } /** @override */ acceptStart() { this.tapCount_ = 0; this.eventing_ = true; this.emit_(true, false, null); } /** @override */ acceptCancel() { this.tapCount_ = 0; this.eventing_ = false; } /** * @param {boolean} first * @param {boolean} last * @param {?Event} event * @private */ emit_(first, last, event) { this.lastTime_ = Date.now(); if (first) { this.velocityX_ = this.velocityY_ = 0; } else if (this.lastTime_ - this.prevTime_ > 2) { this.velocityX_ = calcVelocity( this.lastX_ - this.prevX_, this.lastTime_ - this.prevTime_, this.velocityX_ ); this.velocityY_ = calcVelocity( this.lastY_ - this.prevY_, this.lastTime_ - this.prevTime_, this.velocityY_ ); } this.prevX_ = this.lastX_; this.prevY_ = this.lastY_; this.prevTime_ = this.lastTime_; this.signalEmit( { first, last, centerClientX: this.startX_, centerClientY: this.startY_, deltaX: this.lastX_ - this.startX_, deltaY: this.lastY_ - this.startY_, velocityX: this.velocityX_, velocityY: this.velocityY_, }, event ); } /** * @param {?Event} event * @private */ end_(event) { if (this.eventing_) { this.eventing_ = false; this.emit_(false, true, event); this.signalEnd(); } } } /** * A "pinch" gesture. It has a center, delta off the center center and * the velocity of moving away from the center. "dir" component of `1` * indicates that it's a expand motion and `-1` indicates pinch motion. * @typedef {{ * first: boolean, * last: boolean, * centerClientX: number, * centerClientY: number, * dir: number, * distance: number, * deltaX: number, * deltaY: number, * velocityX: number, * velocityY: number * }} */ export let PinchDef; /** * Threshold in pixels for how much two touches move away from * each other before we recognize the gesture as a pinch. */ const PINCH_ACCEPT_THRESHOLD = 4; /** * Threshold in pixels for how much two touches move in the same * direction before we reject the gesture as a pinch. */ const PINCH_REJECT_THRESHOLD = 10; /** * Recognizes a "pinch" gesture. * @extends {GestureRecognizer<PinchDef>} */ export class PinchRecognizer extends GestureRecognizer { /** * @param {!./gesture.Gestures} manager */ constructor(manager) { super('pinch', manager); /** @private {boolean} */ this.eventing_ = false; /** @private {number} */ this.startX1_ = 0; /** @private {number} */ this.startY1_ = 0; /** @private {number} */ this.startX2_ = 0; /** @private {number} */ this.startY2_ = 0; /** @private {number} */ this.lastX1_ = 0; /** @private {number} */ this.lastY1_ = 0; /** @private {number} */ this.lastX2_ = 0; /** @private {number} */ this.lastY2_ = 0; /** @private {number} */ this.prevDeltaX_ = 0; /** @private {number} */ this.prevDeltaY_ = 0; /** @private {number} */ this.centerClientX_ = 0; /** @private {number} */ this.centerClientY_ = 0; /** @private {time} */ this.startTime_ = 0; /** @private {time} */ this.lastTime_ = 0; /** @private {time} */ this.prevTime_ = 0; /** @private {number} */ this.velocityX_ = 0; /** @private {number} */ this.velocityY_ = 0; } /** @override */ onTouchStart(e) { const {touches} = e; if (!touches) { return false; } // Pinch touches are not always simultaneous, continue to listen // for second touch. if (touches.length == 1) { return true; } // If already in the middle of a pinch event, ignore additional touches. if (this.eventing_ && touches.length > 2) { return true; } if (touches.length == 2) { this.startTime_ = Date.now(); this.startX1_ = touches[0].clientX; this.startY1_ = touches[0].clientY; this.startX2_ = touches[1].clientX; this.startY2_ = touches[1].clientY; return true; } else { return false; } } /** @override */ onTouchMove(e) { const {touches} = e; if (!touches || touches.length == 0) { return false; } // Pinch touches are not always simultaneous, continue to listen // for second touch. if (touches.length == 1) { return true; } // Have 2+ touches this.lastX1_ = touches[0].clientX; this.lastY1_ = touches[0].clientY; this.lastX2_ = touches[1].clientX; this.lastY2_ = touches[1].clientY; // If eventing, always emit gesture with new coordinates if (this.eventing_) { this.emit_(false, false, e); return true; } // Gesture is 2+ touch but direction indicates not a pinch if (this.isPinchRejected_()) { return false; } if (this.isPinchReady_()) { this.signalReady(0); } // Pinch gesture detected but threshold not reached, continue listening return true; } /** * @return {boolean} * @private */ isPinchReady_() { const dx1 = this.lastX1_ - this.startX1_; const dy1 = this.lastY1_ - this.startY1_; const dx2 = this.lastX2_ - this.startX2_; const dy2 = this.lastY2_ - this.startY2_; const pinchDirectionCorrect = dx1 * dx2 <= 0 && dy1 * dy2 <= 0; const xPinchRecognized = Math.abs(dx1 - dx2) >= PINCH_ACCEPT_THRESHOLD; const yPinchRecognized = Math.abs(dy1 - dy2) >= PINCH_ACCEPT_THRESHOLD; return pinchDirectionCorrect && (xPinchRecognized || yPinchRecognized); } /** * @return {boolean} * @private */ isPinchRejected_() { const dx1 = this.lastX1_ - this.startX1_; const dy1 = this.lastY1_ - this.startY1_; const dx2 = this.lastX2_ - this.startX2_; const dy2 = this.lastY2_ - this.startY2_; const pinchDirectionIncorrect = dx1 * dx2 > 0 || dy1 * dy2 > 0; const xPinchRejected = Math.abs(dx1 + dx2) >= PINCH_REJECT_THRESHOLD; const yPinchRejected = Math.abs(dy1 + dy2) >= PINCH_REJECT_THRESHOLD; return pinchDirectionIncorrect && (xPinchRejected || yPinchRejected); } /** @override */ onTouchEnd(e) { // Pinch requires at least two touches on the page const {touches} = e; if (touches && touches.length < 2) { this.end_(e); } } /** @override */ acceptStart() { this.eventing_ = true; this.prevTime_ = this.startTime_; this.prevDeltaX_ = 0; this.prevDeltaY_ = 0; this.centerClientX_ = (this.startX1_ + this.startX2_) * 0.5; this.centerClientY_ = (this.startY1_ + this.startY2_) * 0.5; this.emit_(true, false, null); } /** @override */ acceptCancel() { this.eventing_ = false; } /** * @param {boolean} first * @param {boolean} last * @param {?Event} event * @private */ emit_(first, last, event) { this.lastTime_ = Date.now(); const deltaTime = this.lastTime_ - this.prevTime_; const deltaX = this.deltaX_(); const deltaY = this.deltaY_(); // It's often that `touchend` arrives on the next frame. These should // be ignored to avoid a significant velocity downgrade. if ((!last && deltaTime > 4) || (last && deltaTime > 16)) { this.velocityX_ = calcVelocity( deltaX - this.prevDeltaX_, deltaTime, this.velocityX_ ); this.velocityY_ = calcVelocity( deltaY - this.prevDeltaY_, deltaTime, this.velocityY_ ); this.velocityX_ = Math.abs(this.velocityX_) > 1e-4 ? this.velocityX_ : 0; this.velocityY_ = Math.abs(this.velocityY_) > 1e-4 ? this.velocityY_ : 0; this.prevDeltaX_ = deltaX; this.prevDeltaY_ = deltaY; this.prevTime_ = this.lastTime_; } const startDist = this.dist_( this.startX1_, this.startX2_, this.startY1_, this.startY2_ ); const lastDist = this.dist_( this.lastX1_, this.lastX2_, this.lastY1_, this.lastY2_ ); const distance = lastDist - startDist; this.signalEmit( { first, last, time: this.lastTime_, centerClientX: this.centerClientX_, centerClientY: this.centerClientY_, dir: Math.sign(distance), distance, deltaX: deltaX * 0.5, deltaY: deltaY * 0.5, velocityX: this.velocityX_ * 0.5, velocityY: this.velocityY_ * 0.5, }, event ); } /** * @param {?Event} event * @private */ end_(event) { if (this.eventing_) { this.eventing_ = false; this.emit_(false, true, event); this.signalEnd(); } } /** * @param {number} x1 * @param {number} x2 * @param {number} y1 * @param {number} y2 * @return {number} * @private */ dist_(x1, x2, y1, y2) { const dx = x1 - x2; const dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } /** * @return {number} * @private */ deltaX_() { return Math.abs( this.lastX1_ - this.startX1_ - (this.lastX2_ - this.startX2_) ); } /** * @return {number} * @private */ deltaY_() { return Math.abs( this.lastY1_ - this.startY1_ - (this.lastY2_ - this.startY2_) ); } }