From 1660b6aa60c75a7cae61def32568e0e3a4490a86 Mon Sep 17 00:00:00 2001 From: Roman Babiak Date: Sun, 22 Mar 2026 19:49:25 +0200 Subject: [PATCH 1/6] feat(modules): add `Swipe` module for detecting swipe gestures --- src/index.ts | 1 + src/modules/Swipe.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 1 + 3 files changed, 53 insertions(+) create mode 100644 src/modules/Swipe.ts diff --git a/src/index.ts b/src/index.ts index 21fde8f..9fdb86f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export { default as BaseModule, type BaseArgs, type Options, type Events } from export type { Pointer, Pointers, StdEvents, BaseOptions } from '@/types'; export { default as Click } from '@/modules/Click'; export { default as Drag } from '@/modules/Drag'; +export { default as Swipe } from '@/modules/Swipe'; export { default as MultitouchPanZoom } from '@/modules/MultitouchPanZoom'; export { default as PreventDefault } from '@/modules/PreventDefault'; export { default as WheelPanZoom } from '@/modules/WheelPanZoom'; diff --git a/src/modules/Swipe.ts b/src/modules/Swipe.ts new file mode 100644 index 0000000..6d30cff --- /dev/null +++ b/src/modules/Swipe.ts @@ -0,0 +1,51 @@ +import type { BaseOptions, Pointer, Pointers } from '@/types'; +import BaseModule from '@/BaseModule'; +import { getLast } from '@/utils'; + +interface Options extends BaseOptions { + minDistance?: number; + minVelocity?: number; + velocityWindow?: number; +} + +export default class Swipe extends BaseModule { + onPointerUp = (_e: PointerEvent, pointer: Pointer, pointers: Pointers) => { + if (pointers.size > 1) return; + + const records = pointer.records; + if (records.length < 2) return; + + const first = records[0]; + const last = getLast(records); + + const dx = last.x - first.x; + const dy = last.y - first.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + const minDistance = this.options.minDistance ?? 10; + if (distance < minDistance) return; + + const velocityWindow = this.options.velocityWindow ?? 100; + const recentStart = last.timestamp - velocityWindow; + const windowRecords = records.filter((r) => r.timestamp >= recentStart); + + let velocity = 0; + if (windowRecords.length >= 2) { + const wFirst = windowRecords[0]; + const wLast = getLast(windowRecords); + const wDx = wLast.x - wFirst.x; + const wDy = wLast.y - wFirst.y; + const wDistance = Math.sqrt(wDx * wDx + wDy * wDy); + const wTime = wLast.timestamp - wFirst.timestamp; + velocity = wTime > 0 ? wDistance / wTime : 0; + } + + const minVelocity = this.options.minVelocity ?? 0.1; + if (velocity < minVelocity) return; + + const direction = + Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up'; + + this.dispatch('swipe', { direction, velocity }); + }; +} diff --git a/src/types.ts b/src/types.ts index 9f4a603..e0c8249 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,6 +46,7 @@ export type Pointer = { export interface StdEvents { pan: { deltaX: number; deltaY: number }; drag: { deltaX: number; deltaY: number; x: number; y: number }; + swipe: { direction: 'left' | 'right' | 'up' | 'down'; velocity: number }; trueClick: Coordinates & { target: EventTarget | null; streak: number }; zoom: Coordinates & { factor: number }; } From d6d8531b50810039af857cb43306acb424caf224 Mon Sep 17 00:00:00 2001 From: Roman Babiak Date: Sun, 22 Mar 2026 19:59:30 +0200 Subject: [PATCH 2/6] feat(Swipe): add multi-touch support --- src/modules/Swipe.ts | 81 ++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/src/modules/Swipe.ts b/src/modules/Swipe.ts index 6d30cff..18c0264 100644 --- a/src/modules/Swipe.ts +++ b/src/modules/Swipe.ts @@ -6,46 +6,69 @@ interface Options extends BaseOptions { minDistance?: number; minVelocity?: number; velocityWindow?: number; + pointers?: number; } +type Direction = 'left' | 'right' | 'up' | 'down'; + export default class Swipe extends BaseModule { - onPointerUp = (_e: PointerEvent, pointer: Pointer, pointers: Pointers) => { - if (pointers.size > 1) return; + #gesturePointers: Array = []; + + onPointerDown = (_e: PointerEvent, _pointer: Pointer, pointers: Pointers) => { + if (pointers.size === 1) this.#gesturePointers = []; + }; - const records = pointer.records; - if (records.length < 2) return; + onPointerUp = (_e: PointerEvent, pointer: Pointer, pointers: Pointers) => { + this.#gesturePointers.push(pointer.records); - const first = records[0]; - const last = getLast(records); + if (pointers.size > 0) return; - const dx = last.x - first.x; - const dy = last.y - first.y; - const distance = Math.sqrt(dx * dx + dy * dy); + if (this.#gesturePointers.length < (this.options.pointers ?? 1)) return; const minDistance = this.options.minDistance ?? 10; - if (distance < minDistance) return; - + const minVelocity = this.options.minVelocity ?? 0.1; const velocityWindow = this.options.velocityWindow ?? 100; - const recentStart = last.timestamp - velocityWindow; - const windowRecords = records.filter((r) => r.timestamp >= recentStart); - - let velocity = 0; - if (windowRecords.length >= 2) { - const wFirst = windowRecords[0]; - const wLast = getLast(windowRecords); - const wDx = wLast.x - wFirst.x; - const wDy = wLast.y - wFirst.y; - const wDistance = Math.sqrt(wDx * wDx + wDy * wDy); - const wTime = wLast.timestamp - wFirst.timestamp; - velocity = wTime > 0 ? wDistance / wTime : 0; - } - const minVelocity = this.options.minVelocity ?? 0.1; - if (velocity < minVelocity) return; + let direction: Direction | null = null; + let totalVelocity = 0; + + for (const records of this.#gesturePointers) { + if (records.length < 2) return; + + const first = records[0]; + const last = getLast(records); + const dx = last.x - first.x; + const dy = last.y - first.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < minDistance) return; + + const pointerDirection: Direction = + Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up'; + + if (direction === null) direction = pointerDirection; + else if (direction !== pointerDirection) return; + + const recentStart = last.timestamp - velocityWindow; + const windowRecords = records.filter((r) => r.timestamp >= recentStart); + let velocity = 0; + if (windowRecords.length >= 2) { + const wFirst = windowRecords[0]; + const wLast = getLast(windowRecords); + const wDx = wLast.x - wFirst.x; + const wDy = wLast.y - wFirst.y; + const wDistance = Math.sqrt(wDx * wDx + wDy * wDy); + const wTime = wLast.timestamp - wFirst.timestamp; + velocity = wTime > 0 ? wDistance / wTime : 0; + } + if (velocity < minVelocity) return; + totalVelocity += velocity; + } - const direction = - Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up'; + if (!direction) return; - this.dispatch('swipe', { direction, velocity }); + this.dispatch('swipe', { + direction, + velocity: totalVelocity / this.#gesturePointers.length, + }); }; } From ea9e0d8fca908cbe343d7b56433f369b6521d2b4 Mon Sep 17 00:00:00 2001 From: Roman Babiak Date: Sun, 22 Mar 2026 20:22:46 +0200 Subject: [PATCH 3/6] chore: add swipe event tests --- tests/swipe.test.ts | 141 ++++++++++++++++++++++++++++++++++++++++++++ tests/testUtils.ts | 18 +++++- 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/swipe.test.ts diff --git a/tests/swipe.test.ts b/tests/swipe.test.ts new file mode 100644 index 0000000..a32617e --- /dev/null +++ b/tests/swipe.test.ts @@ -0,0 +1,141 @@ +import { Swipe } from '@'; +import { expect, test, vi } from 'vitest'; +import setup from './testUtils'; + +test('swipe right', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0 }); + const p = new Pointer(); + p.down(); + p.move({ x: 50, y: 0 }); + p.up(); + expect(acc.swipes).toHaveLength(1); + expect(acc.swipes[0].direction).toBe('right'); + await dispose(); +}); + +test('swipe left', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0 }); + const p = new Pointer(); + p.down({ x: 100, y: 0 }); + p.move({ x: -50, y: 0 }); + p.up(); + expect(acc.swipes).toHaveLength(1); + expect(acc.swipes[0].direction).toBe('left'); + await dispose(); +}); + +test('swipe up', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0 }); + const p = new Pointer(); + p.down({ x: 0, y: 100 }); + p.move({ x: 0, y: -50 }); + p.up(); + expect(acc.swipes).toHaveLength(1); + expect(acc.swipes[0].direction).toBe('up'); + await dispose(); +}); + +test('swipe down', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0 }); + const p = new Pointer(); + p.down(); + p.move({ x: 0, y: 50 }); + p.up(); + expect(acc.swipes).toHaveLength(1); + expect(acc.swipes[0].direction).toBe('down'); + await dispose(); +}); + +test('does not fire when distance is below threshold', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0, minDistance: 20 }); + const p = new Pointer(); + p.down(); + p.move({ x: 10, y: 0 }); + p.up(); + expect(acc.swipes).toHaveLength(0); + await dispose(); +}); + +test('does not fire when velocity is below threshold', async () => { + let time = 0; + const dateSpy = vi.spyOn(Date, 'now').mockImplementation(() => time); + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 1 }); + const p = new Pointer(); + p.down(); + // no time advances → all timestamps at 0 → wTime = 0 → velocity = 0 + p.move({ x: 50, y: 0 }); + p.up(); + expect(acc.swipes).toHaveLength(0); + dateSpy.mockRestore(); + await dispose(); +}); + +test('velocity is computed correctly', async () => { + let time = 0; + const dateSpy = vi.spyOn(Date, 'now').mockImplementation(() => time); + const { acc, dispose, Pointer } = setup([Swipe]); + const p = new Pointer(); + p.down(); // t=0, x=0 + time = 50; + p.move({ x: 50, y: 0 }); // t=50, x=50 + time = 100; + p.move({ x: 50, y: 0 }); // t=100, x=100 + p.up(); + expect(acc.swipes).toHaveLength(1); + expect(acc.swipes[0].velocity).toBeCloseTo(1, 1); // 100px / 100ms = 1 px/ms + dateSpy.mockRestore(); + await dispose(); +}); + +test('two-finger swipe fires', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0 }); + const p1 = new Pointer(); + const p2 = new Pointer(); + p1.down({ x: 0, y: 50 }); + p2.down({ x: 50, y: 50 }); + p1.move({ x: 50, y: 0 }); + p2.move({ x: 50, y: 0 }); + p1.up(); + p2.up(); + expect(acc.swipes).toHaveLength(1); + expect(acc.swipes[0].direction).toBe('right'); + await dispose(); +}); + +test('two-finger swipe in opposite directions does not fire', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0 }); + const p1 = new Pointer(); + const p2 = new Pointer(); + p1.down({ x: 50, y: 50 }); + p2.down({ x: 100, y: 50 }); + p1.move({ x: 50, y: 0 }); + p2.move({ x: -50, y: 0 }); + p1.up(); + p2.up(); + expect(acc.swipes).toHaveLength(0); + await dispose(); +}); + +test('pointers option: single finger does not fire when 2 required', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0, pointers: 2 }); + const p = new Pointer(); + p.down(); + p.move({ x: 50, y: 0 }); + p.up(); + expect(acc.swipes).toHaveLength(0); + await dispose(); +}); + +test('pointers option: two fingers fire when 2 required', async () => { + const { acc, dispose, Pointer } = setup([Swipe], { minVelocity: 0, pointers: 2 }); + const p1 = new Pointer(); + const p2 = new Pointer(); + p1.down({ x: 0, y: 50 }); + p2.down({ x: 50, y: 50 }); + p1.move({ x: 50, y: 0 }); + p2.move({ x: 50, y: 0 }); + p1.up(); + p2.up(); + expect(acc.swipes).toHaveLength(1); + await dispose(); +}); diff --git a/tests/testUtils.ts b/tests/testUtils.ts index 1299109..cefd49b 100644 --- a/tests/testUtils.ts +++ b/tests/testUtils.ts @@ -1,4 +1,11 @@ -import { type Click, type Drag, PointeractInterface, type WheelPanZoom, Pointeract } from '@'; +import { + type Click, + type Drag, + PointeractInterface, + type WheelPanZoom, + type Swipe, + Pointeract, +} from '@'; import { Window as HappyWindow, PointerEvent, HTMLDivElement, WheelEvent } from 'happy-dom'; import { beforeEach, vi } from 'vitest'; import type { Coordinates, StdEvents } from '@/types'; @@ -33,7 +40,7 @@ beforeEach(() => { ); }); -type ModulePreset = [WheelPanZoom, Drag, Click]; +type ModulePreset = [WheelPanZoom, Drag, Click, Swipe]; class Accumulator { pan = { @@ -46,6 +53,7 @@ class Accumulator { }; scale = 1; clicks = 0; + swipes: StdEvents['swipe'][] = []; private pointeract: PointeractInterface; constructor(pointeract: PointeractInterface) { this.pointeract = pointeract; @@ -53,11 +61,15 @@ class Accumulator { pointeract.on('drag', this.dragger); pointeract.on('zoom', this.zoomer); pointeract.on('trueClick', this.clicker); + pointeract.on('swipe', this.swiper); } private panner = (e: StdEvents['pan']) => { this.pan.x += e.deltaX; this.pan.y += e.deltaY; }; + private swiper = (e: StdEvents['swipe']) => { + this.swipes.push(e); + }; private dragger = (e: StdEvents['drag']) => { this.drag.x += e.deltaX; this.drag.y += e.deltaY; @@ -75,12 +87,14 @@ class Accumulator { }; this.scale = 1; this.clicks = 0; + this.swipes = []; }; dispose = () => { this.pointeract.off('pan', this.panner); this.pointeract.off('drag', this.dragger); this.pointeract.off('zoom', this.zoomer); this.pointeract.off('trueClick', this.clicker); + this.pointeract.off('swipe', this.swiper); }; } From d3e3baa44cb20e80e0d735faaf82a8d1fd804f13 Mon Sep 17 00:00:00 2001 From: Roman Babiak Date: Sun, 22 Mar 2026 20:46:21 +0200 Subject: [PATCH 4/6] docs: add Swipe module documentation and event details --- README.md | 1 + docs/.vitepress/config.ts | 2 ++ docs/en/events/swipe.md | 15 +++++++++++++++ docs/en/modules/index.md | 1 + docs/en/modules/swipe.md | 28 ++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 docs/en/events/swipe.md create mode 100644 docs/en/modules/swipe.md diff --git a/README.md b/README.md index c32a62e..94794eb 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Congratulations! You can now press your mouse or finger to the element and move, - **Click (Double Click, Triple Click, Quadruple Click, Any Click)** - **Drag** +- **Swipe (All directions)** - **Pan and Zoom via Mouse Wheel (`ctrl`/`shift` key binding, touchpad support)** - **Pan and Zoom via Multitouch (Pan, Pinch)** - **One-line Prevent Default** diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0be0aba..bf5ea91 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -66,6 +66,7 @@ export default defineConfig({ { text: 'Prevent Default', link: '/modules/prevent-default' }, { text: 'Click', link: '/modules/click' }, { text: 'Drag', link: '/modules/drag' }, + { text: 'Swipe', link: '/modules/swipe' }, { text: 'Wheel Pan Zoom', link: '/modules/wheel-pan-zoom' }, { text: 'Multitouch Pan Zoom', link: '/modules/multitouch-pan-zoom' }, { text: 'Lubricator', link: '/modules/lubricator' }, @@ -78,6 +79,7 @@ export default defineConfig({ { text: 'Pan', link: '/events/pan' }, { text: 'True Click', link: '/events/true-click' }, { text: 'Drag', link: '/events/drag' }, + { text: 'Swipe', link: '/events/swipe' }, { text: 'Zoom', link: '/events/zoom' }, ], }, diff --git a/docs/en/events/swipe.md b/docs/en/events/swipe.md new file mode 100644 index 0000000..12b5b57 --- /dev/null +++ b/docs/en/events/swipe.md @@ -0,0 +1,15 @@ +# Swipe Event + +- **Event Name**: `swipe` +- **Access Type**: `Events['swipe']` +- **Details**: + +```TypeScript +type SwipeEvent = { + direction: 'left' | 'right' | 'up' | 'down'; + velocity: number; +} +``` + +- `direction`: the direction of the swipe. +- `velocity`: the velocity of the swipe, calculated as the distance of the swipe divided by the time it took to complete the swipe (**px/ms**). \ No newline at end of file diff --git a/docs/en/modules/index.md b/docs/en/modules/index.md index db7309f..2eaedcb 100644 --- a/docs/en/modules/index.md +++ b/docs/en/modules/index.md @@ -7,6 +7,7 @@ Here you can find all modules shipped with Pointeract: | [`PreventDefault`](/modules/prevent-default) | The Element | Prevents default browser behavior. | | [`Click`](/modules/click) | Single Pointer | Checks if a click was performed without any movement. | | [`Drag`](/modules/drag) | Single Pointer | Tracks pointer movement and emits drag events. | +| [`Swipe`](/modules/swipe) | Multitouch | Detects swipe gestures and emits swipe events. | | [`WheelPanZoom`](/modules/wheel-pan-zoom) | Mouse Wheel | Tracks pointer wheel movement and key press. | | [`MultiTouchPanZoom`](/modules/multitouch-pan-zoom) | Multitouch | Resolves pan/zoom by analyzing movement of two touches. | | [`Lubricator`](/modules/lubricator) | Events | Smoothify configured events by intercepting them and re-emit interpolated ones. | diff --git a/docs/en/modules/swipe.md b/docs/en/modules/swipe.md new file mode 100644 index 0000000..743e475 --- /dev/null +++ b/docs/en/modules/swipe.md @@ -0,0 +1,28 @@ +# Swipe Module + +This module handles swipe interactions, events are dispatched when a single (or multiple) touch or mouse is pressed and moved in a specific direction. + +**Event**: [`swipe`](/events/swipe) + +## Loading + +```TypeScript +import { Swipe, Pointeract } from 'pointeract'; +const pointeract = new Pointeract({ element: app }, [Swipe]); +``` + +## Options + +```TypeScript +interface Options extends BaseOptions { + minDistance?: number; + minVelocity?: number; + velocityWindow?: number; + pointers?: number; +} +``` + +- `minDistance`: The minimum distance in pixels that the pointer must move to be considered a swipe. Defaults to **20**. +- `minVelocity`: The minimum velocity in pixels per second that the pointer must move to be considered a swipe. Defaults to **0**. +- `velocityWindow`: The time window in milliseconds used to calculate the velocity of the swipe. Defaults to **100**. +- `pointers`: The number of pointers that must be active for the swipe event to be dispatched. Defaults to **1**. \ No newline at end of file From b9b9d53fe69a66273f8e3ffbf604e03ad26b0503 Mon Sep 17 00:00:00 2001 From: Roman Babiak Date: Sun, 22 Mar 2026 21:00:10 +0200 Subject: [PATCH 5/6] chore(docs): resolve linting issues --- docs/en/events/swipe.md | 2 +- docs/en/modules/index.md | 2 +- docs/en/modules/swipe.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/events/swipe.md b/docs/en/events/swipe.md index 12b5b57..0f4c622 100644 --- a/docs/en/events/swipe.md +++ b/docs/en/events/swipe.md @@ -12,4 +12,4 @@ type SwipeEvent = { ``` - `direction`: the direction of the swipe. -- `velocity`: the velocity of the swipe, calculated as the distance of the swipe divided by the time it took to complete the swipe (**px/ms**). \ No newline at end of file +- `velocity`: the velocity of the swipe, calculated as the distance of the swipe divided by the time it took to complete the swipe (**px/ms**). diff --git a/docs/en/modules/index.md b/docs/en/modules/index.md index 2eaedcb..4f6c824 100644 --- a/docs/en/modules/index.md +++ b/docs/en/modules/index.md @@ -7,7 +7,7 @@ Here you can find all modules shipped with Pointeract: | [`PreventDefault`](/modules/prevent-default) | The Element | Prevents default browser behavior. | | [`Click`](/modules/click) | Single Pointer | Checks if a click was performed without any movement. | | [`Drag`](/modules/drag) | Single Pointer | Tracks pointer movement and emits drag events. | -| [`Swipe`](/modules/swipe) | Multitouch | Detects swipe gestures and emits swipe events. | +| [`Swipe`](/modules/swipe) | Multitouch | Detects swipe gestures and emits swipe events. | | [`WheelPanZoom`](/modules/wheel-pan-zoom) | Mouse Wheel | Tracks pointer wheel movement and key press. | | [`MultiTouchPanZoom`](/modules/multitouch-pan-zoom) | Multitouch | Resolves pan/zoom by analyzing movement of two touches. | | [`Lubricator`](/modules/lubricator) | Events | Smoothify configured events by intercepting them and re-emit interpolated ones. | diff --git a/docs/en/modules/swipe.md b/docs/en/modules/swipe.md index 743e475..235cea0 100644 --- a/docs/en/modules/swipe.md +++ b/docs/en/modules/swipe.md @@ -25,4 +25,4 @@ interface Options extends BaseOptions { - `minDistance`: The minimum distance in pixels that the pointer must move to be considered a swipe. Defaults to **20**. - `minVelocity`: The minimum velocity in pixels per second that the pointer must move to be considered a swipe. Defaults to **0**. - `velocityWindow`: The time window in milliseconds used to calculate the velocity of the swipe. Defaults to **100**. -- `pointers`: The number of pointers that must be active for the swipe event to be dispatched. Defaults to **1**. \ No newline at end of file +- `pointers`: The number of pointers that must be active for the swipe event to be dispatched. Defaults to **1**. From e77b0ad69ac9f680a029f508c21fe2e5025d9d29 Mon Sep 17 00:00:00 2001 From: Roman Babiak Date: Sun, 22 Mar 2026 21:00:26 +0200 Subject: [PATCH 6/6] refactor(Swipe): reduce complexity of onPointerUp by extracting pointer processing logic to a separate method --- src/modules/Swipe.ts | 67 ++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/modules/Swipe.ts b/src/modules/Swipe.ts index 18c0264..d44b55f 100644 --- a/src/modules/Swipe.ts +++ b/src/modules/Swipe.ts @@ -18,11 +18,42 @@ export default class Swipe extends BaseModule { if (pointers.size === 1) this.#gesturePointers = []; }; + #processPointer( + records: Pointer['records'], + minDistance: number, + minVelocity: number, + velocityWindow: number, + ): { direction: Direction; velocity: number } | null { + if (records.length < 2) return null; + + const first = records[0]; + const last = getLast(records); + const dx = last.x - first.x; + const dy = last.y - first.y; + if (Math.sqrt(dx * dx + dy * dy) < minDistance) return null; + + const direction: Direction = + Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up'; + + const windowRecords = records.filter((r) => r.timestamp >= last.timestamp - velocityWindow); + let velocity = 0; + if (windowRecords.length >= 2) { + const wFirst = windowRecords[0]; + const wLast = getLast(windowRecords); + const wDx = wLast.x - wFirst.x; + const wDy = wLast.y - wFirst.y; + const wTime = wLast.timestamp - wFirst.timestamp; + velocity = wTime > 0 ? Math.sqrt(wDx * wDx + wDy * wDy) / wTime : 0; + } + if (velocity < minVelocity) return null; + + return { direction, velocity }; + } + onPointerUp = (_e: PointerEvent, pointer: Pointer, pointers: Pointers) => { this.#gesturePointers.push(pointer.records); if (pointers.size > 0) return; - if (this.#gesturePointers.length < (this.options.pointers ?? 1)) return; const minDistance = this.options.minDistance ?? 10; @@ -33,35 +64,11 @@ export default class Swipe extends BaseModule { let totalVelocity = 0; for (const records of this.#gesturePointers) { - if (records.length < 2) return; - - const first = records[0]; - const last = getLast(records); - const dx = last.x - first.x; - const dy = last.y - first.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance < minDistance) return; - - const pointerDirection: Direction = - Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up'; - - if (direction === null) direction = pointerDirection; - else if (direction !== pointerDirection) return; - - const recentStart = last.timestamp - velocityWindow; - const windowRecords = records.filter((r) => r.timestamp >= recentStart); - let velocity = 0; - if (windowRecords.length >= 2) { - const wFirst = windowRecords[0]; - const wLast = getLast(windowRecords); - const wDx = wLast.x - wFirst.x; - const wDy = wLast.y - wFirst.y; - const wDistance = Math.sqrt(wDx * wDx + wDy * wDy); - const wTime = wLast.timestamp - wFirst.timestamp; - velocity = wTime > 0 ? wDistance / wTime : 0; - } - if (velocity < minVelocity) return; - totalVelocity += velocity; + const result = this.#processPointer(records, minDistance, minVelocity, velocityWindow); + if (!result) return; + if (direction === null) direction = result.direction; + else if (direction !== result.direction) return; + totalVelocity += result.velocity; } if (!direction) return;