gesturx 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +17 -1
  2. package/dist/index.d.ts +192 -0
  3. package/dist/index.js +627 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.umd.cjs +2 -0
  6. package/dist/index.umd.cjs.map +1 -0
  7. package/package.json +12 -1
  8. package/.github/workflows/publish.yml +0 -17
  9. package/.prettierrc.json +0 -3
  10. package/eslint.config.js +0 -18
  11. package/index.html +0 -13
  12. package/src/core/index.ts +0 -4
  13. package/src/core/manager.ts +0 -46
  14. package/src/core/pan/PanRecognizer.ts +0 -192
  15. package/src/core/pinch/PinchRecognizer.ts +0 -131
  16. package/src/core/recognizer/Recognizer.ts +0 -54
  17. package/src/core/recognizer/RecognizerEvent.ts +0 -39
  18. package/src/core/recognizer/mapToRecognizerEvent.ts +0 -142
  19. package/src/core/rotate/RotateRecognizer.ts +0 -115
  20. package/src/core/swipe/SwipeRecognizer.test.ts +0 -160
  21. package/src/core/swipe/SwipeRecognizer.ts +0 -82
  22. package/src/core/swipe/isRecognizedAsSwipe.ts +0 -13
  23. package/src/core/tap/TapRecognizer.test.ts +0 -567
  24. package/src/core/tap/TapRecognizer.ts +0 -172
  25. package/src/core/utils/events.ts +0 -186
  26. package/src/core/utils/geometry.ts +0 -115
  27. package/src/core/utils/utils.test.ts +0 -26
  28. package/src/core/utils/utils.ts +0 -58
  29. package/src/demo/App.tsx +0 -171
  30. package/src/demo/DebugBox.tsx +0 -10
  31. package/src/demo/useManager.ts +0 -40
  32. package/src/demo/useTap.tsx +0 -87
  33. package/src/index.css +0 -66
  34. package/src/main.tsx +0 -14
  35. package/src/theme.ts +0 -11
  36. package/src/vite-env.d.ts +0 -1
  37. package/tsconfig.json +0 -26
  38. package/tsconfig.node.json +0 -11
  39. package/vite.config.ts +0 -38
  40. package/vitest.config.ts +0 -8
  41. /package/{public → dist}/vite.svg +0 -0
@@ -1,131 +0,0 @@
1
- import {
2
- Observable,
3
- combineLatest,
4
- distinctUntilChanged,
5
- filter,
6
- first,
7
- map,
8
- merge,
9
- mergeMap,
10
- scan,
11
- switchMap,
12
- takeUntil,
13
- } from "rxjs"
14
- import { RecognizerEvent } from "../recognizer/RecognizerEvent"
15
- import { Recognizer } from "../recognizer/Recognizer"
16
- import { PanEvent, PanRecognizer } from "../pan/PanRecognizer"
17
-
18
- export interface PinchEvent extends RecognizerEvent {
19
- type: "pinchStart" | "pinchMove" | "pinchEnd"
20
- scale: number
21
- /**
22
- * Distance between start and current
23
- */
24
- distance: number
25
- /**
26
- * Delta distance between events
27
- */
28
- deltaDistance: number
29
- /**
30
- * Delta scale between events
31
- */
32
- deltaDistanceScale: number
33
- }
34
-
35
- type Options = {
36
- posThreshold?: number
37
- }
38
-
39
- export class PinchRecognizer extends Recognizer<Options> {
40
- public events$: Observable<PinchEvent>
41
-
42
- constructor(protected options: Options = {}) {
43
- super(options)
44
-
45
- this.events$ = this.config$.pipe(
46
- switchMap((initializedWith) => {
47
- const { posThreshold } = initializedWith.options ?? {}
48
- const panRecognizer = new PanRecognizer({ posThreshold })
49
-
50
- panRecognizer.initialize(initializedWith)
51
-
52
- const hasLessThanTwoFinger$ = panRecognizer.events$.pipe(
53
- filter(({ pointers }) => pointers.length < 2),
54
- distinctUntilChanged(),
55
- )
56
-
57
- const hasMoreThanOneFinger$ = panRecognizer.events$.pipe(
58
- map((event) => [event, event.pointers.length > 1] as const),
59
- distinctUntilChanged(
60
- (
61
- [_, previousHasMoreThanOneFinger],
62
- [__, currentHasMoreThanOneFinger],
63
- ) => previousHasMoreThanOneFinger === currentHasMoreThanOneFinger,
64
- ),
65
- filter(([_, hasMoreThanOneFinger]) => hasMoreThanOneFinger),
66
- )
67
-
68
- const start$ = hasMoreThanOneFinger$.pipe(
69
- map(([event]) => ({
70
- ...event,
71
- type: "pinchStart" as const,
72
- scale: 1,
73
- distance: 0,
74
- deltaDistance: 0,
75
- deltaDistanceScale: 1,
76
- })),
77
- )
78
-
79
- const rotate$ = start$.pipe(
80
- mergeMap((startEvent) => {
81
- return panRecognizer.events$.pipe(
82
- scan<PanEvent, PinchEvent, Partial<PinchEvent>>(
83
- (acc, current) => {
84
- const previousPointersAverageDistance =
85
- acc.pointersAverageDistance ??
86
- current.pointersAverageDistance
87
-
88
- const scale =
89
- current.pointersAverageDistance /
90
- startEvent.pointersAverageDistance
91
-
92
- return {
93
- ...acc,
94
- ...current,
95
- scale,
96
- distance:
97
- current.pointersAverageDistance -
98
- startEvent.pointersAverageDistance,
99
- deltaDistance:
100
- current.pointersAverageDistance -
101
- previousPointersAverageDistance,
102
- deltaDistanceScale:
103
- current.pointersAverageDistance /
104
- previousPointersAverageDistance,
105
- type: "pinchMove" as const,
106
- }
107
- },
108
- { scale: 0 },
109
- ),
110
- takeUntil(hasLessThanTwoFinger$),
111
- )
112
- }),
113
- )
114
-
115
- const end$ = start$.pipe(
116
- mergeMap(() =>
117
- combineLatest([rotate$, hasLessThanTwoFinger$] as const).pipe(
118
- first(),
119
- map(([event]) => ({
120
- ...event,
121
- type: "pinchEnd" as const,
122
- })),
123
- ),
124
- ),
125
- )
126
-
127
- return merge(start$, rotate$, end$)
128
- }),
129
- )
130
- }
131
- }
@@ -1,54 +0,0 @@
1
- import { BehaviorSubject, Observable, filter } from "rxjs"
2
-
3
- type RecognizerConfig<Options> = {
4
- container?: HTMLElement
5
- afterEventReceived?: (event: PointerEvent) => PointerEvent
6
- options?: Options
7
- }
8
-
9
- type ValidRecognizerConfig<Options> = Required<
10
- Pick<RecognizerConfig<Options>, "container">
11
- > &
12
- RecognizerConfig<Options>
13
-
14
- export interface RecognizerOptions {
15
- failWith?: { start$: Observable<unknown> }[]
16
- }
17
-
18
- export class Recognizer<Options> {
19
- protected config$ = new BehaviorSubject<RecognizerConfig<Options>>({})
20
- protected validConfig$ = this.config$.pipe(
21
- filter(
22
- (config): config is ValidRecognizerConfig<Options> => !!config.container,
23
- ),
24
- )
25
-
26
- constructor(options: Options) {
27
- this.update(options)
28
- }
29
-
30
- public initialize(config: RecognizerConfig<Options>) {
31
- const prevConfig = this.config$.getValue()
32
-
33
- this.config$.next({
34
- ...prevConfig,
35
- ...config,
36
- options: {
37
- ...prevConfig.options,
38
- ...config.options,
39
- } as Options,
40
- })
41
- }
42
-
43
- public update(options: Options) {
44
- const config = this.config$.getValue()
45
-
46
- this.config$.next({
47
- ...config,
48
- options: {
49
- ...config.options,
50
- ...options,
51
- },
52
- })
53
- }
54
- }
@@ -1,39 +0,0 @@
1
- export interface RecognizerEvent {
2
- center: { x: number; y: number }
3
- event: PointerEvent
4
- /**
5
- * Contain the last known pointer event for each active fingers.
6
- * It should contain a unique list, with n being the number of active fingers.
7
- */
8
- pointers: PointerEvent[]
9
- /**
10
- * Delay between the user action and the event recognition
11
- */
12
- delay: number
13
- deltaX: number
14
- deltaY: number
15
- /**
16
- * @important
17
- * Can be negative, 0, positive
18
- */
19
- velocityX: number
20
- /**
21
- * @important
22
- * Can be negative, 0, positive
23
- */
24
- velocityY: number
25
- startTime: number
26
- /**
27
- * Angle delta (in degrees) between pointers between this event
28
- * and the previous one.
29
- *
30
- * @important
31
- * - Will only register a delta if the previous event and current event
32
- * have the same amount of fingers
33
- */
34
- deltaPointersAngle: number
35
- /**
36
- * Average distance between all pointers
37
- */
38
- pointersAverageDistance: number
39
- }
@@ -1,142 +0,0 @@
1
- import { Observable, map, scan } from "rxjs"
2
- import { RecognizerEvent } from "./RecognizerEvent"
3
- import { calculateVelocity } from "../utils/utils"
4
- import {
5
- calculateAngleDelta,
6
- calculateAverageDistance,
7
- calculateCentroid,
8
- } from "../utils/geometry"
9
-
10
- export interface RecognizerEventInput {
11
- event: PointerEvent
12
- latestActivePointers: PointerEvent[]
13
- }
14
-
15
- interface RecognizerEventState extends RecognizerEventInput {
16
- startTime: number
17
- center: { x: number; y: number }
18
- deltaX: number
19
- deltaY: number
20
- deltaPointersAngle: number
21
- }
22
-
23
- const calculateNewDelta = (
24
- newCenter: { x: number; y: number },
25
- prevCenter: { x: number; y: number },
26
- prevDeltaX: number,
27
- prevDeltaY: number,
28
- ) => {
29
- const deltaX = newCenter.x - prevCenter.x + prevDeltaX
30
- const deltaY = newCenter.y - prevCenter.y + prevDeltaY
31
-
32
- return {
33
- deltaX,
34
- deltaY,
35
- }
36
- }
37
-
38
- export const mapToRecognizerEvent = <T extends RecognizerEventInput>(
39
- stream: Observable<T>,
40
- ): Observable<RecognizerEvent> => {
41
- const getCenterFromData = (data: T) =>
42
- calculateCentroid(
43
- !data.latestActivePointers.length
44
- ? [data.event]
45
- : data.latestActivePointers,
46
- )
47
-
48
- return stream.pipe(
49
- scan<
50
- T,
51
- RecognizerEventState,
52
- Pick<
53
- RecognizerEventState,
54
- "deltaX" | "center" | "deltaY" | "latestActivePointers"
55
- >
56
- >(
57
- (acc, curr) => {
58
- const newCenter = getCenterFromData(curr)
59
- const prevCenter = acc.center
60
- const prevDeltaX = acc.deltaX
61
- const prevDeltaY = acc.deltaY
62
- const prevActivePointersNumber = acc.latestActivePointers?.length ?? 0
63
- const currActivePointersNumber = curr.latestActivePointers?.length ?? 0
64
-
65
- const hasAddedOrRemovedAFinger =
66
- [
67
- "pointerdown",
68
- "pointercancel",
69
- "pointerleave",
70
- "pointerup",
71
- ].includes(curr.event.type) &&
72
- prevActivePointersNumber !== currActivePointersNumber
73
- const hasAtLeastOneFinger = currActivePointersNumber > 0
74
-
75
- const { deltaX, deltaY } =
76
- hasAddedOrRemovedAFinger && hasAtLeastOneFinger
77
- ? calculateNewDelta(newCenter, newCenter, prevDeltaX, prevDeltaY)
78
- : calculateNewDelta(newCenter, prevCenter, prevDeltaX, prevDeltaY)
79
-
80
- const { degreesDelta: deltaPointersAngle } =
81
- prevActivePointersNumber === currActivePointersNumber &&
82
- prevActivePointersNumber >= 2 &&
83
- currActivePointersNumber >= 2
84
- ? calculateAngleDelta(
85
- acc.latestActivePointers ?? [],
86
- curr.latestActivePointers ?? [],
87
- )
88
- : { degreesDelta: 0 }
89
-
90
- return {
91
- startTime: new Date().getTime(),
92
- ...acc,
93
- ...curr,
94
- center: newCenter,
95
- deltaX,
96
- deltaY,
97
- deltaPointersAngle,
98
- }
99
- },
100
- {
101
- deltaX: 0,
102
- deltaY: 0,
103
- center: { x: 0, y: 0 },
104
- latestActivePointers: [],
105
- },
106
- ),
107
- map((data) => {
108
- const {
109
- deltaX,
110
- deltaY,
111
- latestActivePointers,
112
- startTime,
113
- center,
114
- event,
115
- deltaPointersAngle,
116
- } = data
117
- const events = latestActivePointers ?? []
118
-
119
- // Calculate the change in time
120
- const now = Date.now()
121
- const delay = now - startTime
122
-
123
- const { velocityX, velocityY } = calculateVelocity(delay, deltaX, deltaY)
124
- const pointersAverageDistance =
125
- calculateAverageDistance(latestActivePointers)
126
-
127
- return {
128
- deltaX,
129
- deltaY,
130
- velocityX,
131
- velocityY,
132
- delay,
133
- center,
134
- pointers: events,
135
- startTime,
136
- event,
137
- deltaPointersAngle,
138
- pointersAverageDistance,
139
- } satisfies RecognizerEvent
140
- }),
141
- )
142
- }
@@ -1,115 +0,0 @@
1
- import {
2
- Observable,
3
- combineLatest,
4
- distinctUntilChanged,
5
- filter,
6
- first,
7
- map,
8
- merge,
9
- mergeMap,
10
- scan,
11
- switchMap,
12
- takeUntil,
13
- } from "rxjs"
14
- import { Recognizer } from "../recognizer/Recognizer"
15
- import { PanEvent, PanRecognizer } from "../pan/PanRecognizer"
16
- import { RecognizerEvent } from "../recognizer/RecognizerEvent"
17
-
18
- export interface RotateEvent extends RecognizerEvent {
19
- type: "rotate" | "rotateStart" | "rotateEnd"
20
- /**
21
- * Current rotation angle
22
- */
23
- angle: number
24
- /**
25
- * Delta angle between events
26
- */
27
- deltaAngle: number
28
- }
29
-
30
- type Options = {
31
- posThreshold?: number
32
- }
33
-
34
- export class RotateRecognizer extends Recognizer<Options> {
35
- public events$: Observable<RotateEvent>
36
-
37
- constructor(protected options: Options = {}) {
38
- super(options)
39
-
40
- this.events$ = this.config$.pipe(
41
- switchMap((initializedWith) => {
42
- const { posThreshold } = initializedWith.options ?? {}
43
-
44
- const panRecognizer = new PanRecognizer({ posThreshold })
45
-
46
- panRecognizer.initialize(initializedWith)
47
-
48
- const hasLessThanTwoFinger$ = panRecognizer.events$.pipe(
49
- filter(({ pointers }) => pointers.length < 2),
50
- distinctUntilChanged(),
51
- )
52
-
53
- const hasMoreThanOneFinger$ = panRecognizer.events$.pipe(
54
- map((event) => [event, event.pointers.length > 1] as const),
55
- distinctUntilChanged(
56
- (
57
- [_, previousHasMoreThanOneFinger],
58
- [__, currentHasMoreThanOneFinger],
59
- ) => previousHasMoreThanOneFinger === currentHasMoreThanOneFinger,
60
- ),
61
- filter(([_, hasMoreThanOneFinger]) => hasMoreThanOneFinger),
62
- )
63
-
64
- const start$ = hasMoreThanOneFinger$.pipe(
65
- map(([event]) => ({
66
- ...event,
67
- type: "rotateStart" as const,
68
- angle: 0,
69
- deltaAngle: 0,
70
- })),
71
- )
72
-
73
- const rotate$ = start$.pipe(
74
- mergeMap(() =>
75
- panRecognizer.events$.pipe(
76
- scan<
77
- PanEvent,
78
- RotateEvent,
79
- Pick<RotateEvent, "angle" | "deltaAngle">
80
- >(
81
- (acc, current) => {
82
- const angle = acc.angle + current.deltaPointersAngle
83
-
84
- return {
85
- ...acc,
86
- ...current,
87
- type: "rotate" as const,
88
- angle,
89
- deltaAngle: current.deltaPointersAngle,
90
- }
91
- },
92
- { angle: 0, deltaAngle: 0 },
93
- ),
94
- takeUntil(hasLessThanTwoFinger$),
95
- ),
96
- ),
97
- )
98
-
99
- const end$ = start$.pipe(
100
- mergeMap(() =>
101
- combineLatest([rotate$, hasLessThanTwoFinger$] as const).pipe(
102
- first(),
103
- map(([event]) => ({
104
- ...event,
105
- type: "rotateEnd" as const,
106
- })),
107
- ),
108
- ),
109
- )
110
-
111
- return merge(start$, rotate$, end$)
112
- }),
113
- )
114
- }
115
- }
@@ -1,160 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest"
2
- import { buffer, first, lastValueFrom, tap, timer } from "rxjs"
3
- import { SwipeRecognizer } from "./SwipeRecognizer"
4
-
5
- const waitFor = (time: number) =>
6
- new Promise((resolve) => setTimeout(resolve, time))
7
-
8
- describe("SwipeRecognizer", () => {
9
- let container = document.createElement("div")
10
-
11
- beforeEach(() => {
12
- container = document.createElement("div")
13
- document.body.appendChild(container)
14
- })
15
-
16
- function createPointerEvent({
17
- type,
18
- x,
19
- y,
20
- identifier,
21
- }: {
22
- type: string
23
- x: number
24
- y: number
25
- identifier: number
26
- }) {
27
- const event = new PointerEvent(type, {
28
- clientX: x,
29
- clientY: y,
30
- pointerId: identifier,
31
- pointerType: "touch",
32
- bubbles: true,
33
- })
34
-
35
- // @ts-ignore
36
- event.x = x
37
- // @ts-ignore
38
- event.y = y
39
-
40
- return event
41
- }
42
-
43
- function sendPointerEvent({
44
- type,
45
- x,
46
- y,
47
- container,
48
- identifier,
49
- }: {
50
- container: HTMLElement
51
- type: string
52
- x: number
53
- y: number
54
- identifier: number
55
- }) {
56
- const event = createPointerEvent({
57
- type,
58
- x,
59
- y,
60
- identifier,
61
- })
62
-
63
- sendPointerEventFromEvent({ event, container })
64
- }
65
-
66
- function sendPointerEventFromEvent({
67
- event,
68
- container,
69
- }: {
70
- event: PointerEvent
71
- container: HTMLElement
72
- }) {
73
- container.dispatchEvent(event)
74
- }
75
-
76
- // ~0.8 velocityX
77
- const createVelocityOfOnePanMoving = async () => {
78
- await waitFor(1)
79
-
80
- sendPointerEvent({
81
- container,
82
- identifier: 1,
83
- type: "pointerdown",
84
- x: 0,
85
- y: 0,
86
- })
87
-
88
- await waitFor(10)
89
-
90
- sendPointerEvent({
91
- container,
92
- identifier: 1,
93
- type: "pointermove",
94
- x: 10,
95
- y: 0,
96
- })
97
-
98
- await waitFor(10)
99
-
100
- sendPointerEvent({
101
- container,
102
- identifier: 1,
103
- type: "pointerup",
104
- x: 20,
105
- y: 0,
106
- })
107
- }
108
-
109
- it("should detect a swipe if above threshold", async () => {
110
- const waitLongEnough$ = timer(100)
111
- const recognizer = new SwipeRecognizer({
112
- escapeVelocity: 0.4,
113
- posThreshold: 5
114
- })
115
-
116
- recognizer.initialize({
117
- container,
118
- })
119
-
120
- const values = await lastValueFrom(
121
- recognizer.events$.pipe(
122
- tap({
123
- subscribe: async () => {
124
- await createVelocityOfOnePanMoving()
125
- },
126
- }),
127
- buffer(waitLongEnough$),
128
- first(),
129
- ),
130
- )
131
-
132
- expect(values).toMatchObject([{ type: "swipe" }])
133
- })
134
-
135
- it("should not detect a swipe if below threshold", async () => {
136
- const waitLongEnough$ = timer(100)
137
- const recognizer = new SwipeRecognizer({
138
- escapeVelocity: 10,
139
- posThreshold: 5
140
- })
141
-
142
- recognizer.initialize({
143
- container,
144
- })
145
-
146
- const values = await lastValueFrom(
147
- recognizer.events$.pipe(
148
- tap({
149
- subscribe: async () => {
150
- await createVelocityOfOnePanMoving()
151
- },
152
- }),
153
- buffer(waitLongEnough$),
154
- first(),
155
- ),
156
- )
157
-
158
- expect(values).toMatchObject([])
159
- })
160
- })