gesturx 0.0.0 → 1.0.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.
- package/.prettierrc.json +3 -1
- package/eslint.config.js +18 -9
- package/index.html +2 -2
- package/package.json +16 -3
- package/src/core/index.ts +4 -0
- package/src/core/manager.ts +36 -35
- package/src/core/pan/PanRecognizer.ts +192 -0
- package/src/core/pinch/PinchRecognizer.ts +131 -0
- package/src/core/recognizer/Recognizer.ts +54 -0
- package/src/core/recognizer/RecognizerEvent.ts +39 -0
- package/src/core/recognizer/mapToRecognizerEvent.ts +142 -0
- package/src/core/rotate/RotateRecognizer.ts +115 -0
- package/src/core/swipe/SwipeRecognizer.test.ts +160 -0
- package/src/core/swipe/SwipeRecognizer.ts +82 -0
- package/src/core/swipe/isRecognizedAsSwipe.ts +13 -0
- package/src/core/{tapRecognizer.test.ts → tap/TapRecognizer.test.ts} +162 -137
- package/src/core/tap/TapRecognizer.ts +172 -0
- package/src/core/utils/events.ts +186 -0
- package/src/core/utils/geometry.ts +115 -0
- package/src/core/utils/utils.test.ts +26 -0
- package/src/core/utils/utils.ts +58 -0
- package/src/demo/App.tsx +171 -0
- package/src/demo/DebugBox.tsx +10 -0
- package/src/demo/useManager.ts +40 -0
- package/src/demo/useTap.tsx +87 -0
- package/src/index.css +66 -0
- package/src/main.tsx +14 -0
- package/src/theme.ts +11 -0
- package/tsconfig.json +5 -2
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +34 -29
- package/vitest.config.ts +2 -2
- package/src/core/tapRecognizer.ts +0 -143
- package/src/core/types.ts +0 -3
- package/src/core/utils.ts +0 -211
- package/src/main.ts +0 -19
- package/src/style.css +0 -29
package/.prettierrc.json
CHANGED
package/eslint.config.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
module.exports = {
|
|
2
|
+
root: true,
|
|
3
|
+
env: { browser: true, es2020: true },
|
|
4
|
+
extends: [
|
|
5
|
+
"eslint:recommended",
|
|
6
|
+
"plugin:@typescript-eslint/recommended",
|
|
7
|
+
"plugin:react-hooks/recommended",
|
|
8
|
+
],
|
|
9
|
+
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
|
10
|
+
parser: "@typescript-eslint/parser",
|
|
11
|
+
plugins: ["react-refresh"],
|
|
12
|
+
rules: {
|
|
13
|
+
"react-refresh/only-export-components": [
|
|
14
|
+
"warn",
|
|
15
|
+
{ allowConstantExport: true },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
}
|
package/index.html
CHANGED
package/package.json
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gesturx",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "tsc && vite build",
|
|
9
|
+
"build:lib": "tsc && cross-env LIB_MODE=true vite build",
|
|
9
10
|
"preview": "vite preview",
|
|
10
|
-
"lint": "prettier --write . && eslint --fix .",
|
|
11
|
+
"lint": "prettier --write . && eslint --fix . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
11
12
|
"test": "vitest",
|
|
12
13
|
"coverage": "vitest run --coverage"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
16
|
+
"@chakra-ui/icons": "^2.1.1",
|
|
17
|
+
"@chakra-ui/react": "^2.8.2",
|
|
18
|
+
"@emotion/react": "^11.11.4",
|
|
19
|
+
"@emotion/styled": "^11.11.5",
|
|
15
20
|
"@eslint/js": "^9.0.0",
|
|
16
21
|
"@types/node": "^20.12.7",
|
|
22
|
+
"@types/react": "^18.3.2",
|
|
23
|
+
"@types/react-dom": "^18.3.0",
|
|
24
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
17
25
|
"@vitest/coverage-v8": "^1.5.0",
|
|
26
|
+
"cross-env": "^7.0.3",
|
|
18
27
|
"eslint": "^8.57.0",
|
|
28
|
+
"framer-motion": "^11.2.6",
|
|
19
29
|
"happy-dom": "^14.7.1",
|
|
20
30
|
"prettier": "3.2.5",
|
|
31
|
+
"react": "^18.3.1",
|
|
32
|
+
"react-dom": "^18.3.1",
|
|
21
33
|
"rollup-plugin-node-externals": "^7.1.1",
|
|
34
|
+
"rxjs": "^7.8.1",
|
|
22
35
|
"typescript": "^5.4.5",
|
|
23
36
|
"typescript-eslint": "^7.7.0",
|
|
24
37
|
"vite": "^5.2.0",
|
|
@@ -26,7 +39,7 @@
|
|
|
26
39
|
"vite-plugin-eslint": "^1.8.1",
|
|
27
40
|
"vitest": "^1.5.0"
|
|
28
41
|
},
|
|
29
|
-
"
|
|
42
|
+
"peerDependencies": {
|
|
30
43
|
"rxjs": "^7.8.1"
|
|
31
44
|
}
|
|
32
45
|
}
|
package/src/core/manager.ts
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { merge, share } from "rxjs"
|
|
2
|
+
import { TapRecognizer } from "./tap/TapRecognizer"
|
|
3
|
+
import { PanRecognizer } from "./pan/PanRecognizer"
|
|
4
|
+
import { SwipeRecognizer } from "./swipe/SwipeRecognizer"
|
|
5
|
+
import { RotateRecognizer } from "./rotate/RotateRecognizer"
|
|
6
|
+
import { PinchRecognizer } from "./pinch/PinchRecognizer"
|
|
7
|
+
|
|
8
|
+
export type Manager = ReturnType<typeof createManager>
|
|
4
9
|
|
|
5
10
|
export const createManager = ({
|
|
6
|
-
container,
|
|
7
11
|
afterEventReceived = (event) => event,
|
|
8
12
|
recognizers,
|
|
9
13
|
}: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
recognizers: (
|
|
15
|
+
| TapRecognizer
|
|
16
|
+
| PanRecognizer
|
|
17
|
+
| SwipeRecognizer
|
|
18
|
+
| RotateRecognizer
|
|
19
|
+
| PinchRecognizer
|
|
20
|
+
)[]
|
|
21
|
+
afterEventReceived?: (event: PointerEvent) => PointerEvent
|
|
13
22
|
}) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// maxTaps: 2,
|
|
18
|
-
// failWith: [dragRecognizer],
|
|
19
|
-
// });
|
|
20
|
-
// const pinchRecognizer = createPinchRecognizer();
|
|
21
|
-
|
|
22
|
-
// internal api
|
|
23
|
-
// dragRecognizer.initialize({ container, afterEventReceived });
|
|
23
|
+
const events$ = merge(
|
|
24
|
+
...recognizers.map((recognizer) => recognizer.events$),
|
|
25
|
+
).pipe(share())
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const initialize = (container: HTMLElement) => {
|
|
28
|
+
/**
|
|
29
|
+
* We have to disable touch-action otherwise every events will be followed
|
|
30
|
+
* by a cancel event since the browser will try to handle touch.
|
|
31
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action
|
|
32
|
+
*/
|
|
33
|
+
container.style.touchAction = `none`
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
|
|
37
|
+
*/
|
|
38
|
+
container.style.userSelect = `none`
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
...event,
|
|
40
|
-
center: getCenterFromEvent(event.srcEvent),
|
|
41
|
-
})),
|
|
42
|
-
);
|
|
40
|
+
recognizers.forEach((recognizer) => {
|
|
41
|
+
recognizer.initialize({ container, afterEventReceived })
|
|
42
|
+
})
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
return { events
|
|
45
|
-
}
|
|
45
|
+
return { events$, initialize, recognizers }
|
|
46
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Observable,
|
|
3
|
+
combineLatest,
|
|
4
|
+
exhaustMap,
|
|
5
|
+
filter,
|
|
6
|
+
first,
|
|
7
|
+
map,
|
|
8
|
+
merge,
|
|
9
|
+
mergeMap,
|
|
10
|
+
of,
|
|
11
|
+
share,
|
|
12
|
+
shareReplay,
|
|
13
|
+
skipUntil,
|
|
14
|
+
switchMap,
|
|
15
|
+
takeUntil,
|
|
16
|
+
withLatestFrom,
|
|
17
|
+
} from "rxjs"
|
|
18
|
+
import { isOutsidePosThreshold } from "../utils/utils"
|
|
19
|
+
import { Recognizer } from "../recognizer/Recognizer"
|
|
20
|
+
import { RecognizerEvent } from "../recognizer/RecognizerEvent"
|
|
21
|
+
import { mapToRecognizerEvent } from "../recognizer/mapToRecognizerEvent"
|
|
22
|
+
import { getPointerEvents, matchPointer, trackFingers } from "../utils/events"
|
|
23
|
+
|
|
24
|
+
export interface PanEvent extends RecognizerEvent {
|
|
25
|
+
type: "panStart" | "panMove" | "panEnd"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Options = {
|
|
29
|
+
posThreshold?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PanRecognizer extends Recognizer<Options> {
|
|
33
|
+
public events$: Observable<PanEvent>
|
|
34
|
+
public start$: Observable<PanEvent>
|
|
35
|
+
public end$: Observable<PanEvent>
|
|
36
|
+
public fingers$: Observable<number>
|
|
37
|
+
|
|
38
|
+
constructor(protected options: Options = {}) {
|
|
39
|
+
super(options)
|
|
40
|
+
|
|
41
|
+
this.events$ = this.validConfig$.pipe(
|
|
42
|
+
switchMap((config) => {
|
|
43
|
+
const { container, afterEventReceived } = config
|
|
44
|
+
const { posThreshold = 15 } = config.options ?? {}
|
|
45
|
+
const {
|
|
46
|
+
pointerCancel$,
|
|
47
|
+
pointerDown$,
|
|
48
|
+
pointerLeave$,
|
|
49
|
+
pointerMove$,
|
|
50
|
+
pointerUp$,
|
|
51
|
+
} = getPointerEvents({
|
|
52
|
+
container,
|
|
53
|
+
afterEventReceived,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return pointerDown$.pipe(
|
|
57
|
+
exhaustMap((initialPointerDownEvent) => {
|
|
58
|
+
const pointerDowns$ = merge(
|
|
59
|
+
of(initialPointerDownEvent),
|
|
60
|
+
pointerDown$,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const pointerUpdate$ = merge(
|
|
64
|
+
pointerCancel$,
|
|
65
|
+
pointerDown$,
|
|
66
|
+
pointerLeave$,
|
|
67
|
+
pointerMove$,
|
|
68
|
+
pointerUp$,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const trackFingers$ = pointerDowns$.pipe(
|
|
72
|
+
trackFingers({
|
|
73
|
+
pointerCancel$,
|
|
74
|
+
pointerLeave$,
|
|
75
|
+
pointerUp$,
|
|
76
|
+
pointerMove$,
|
|
77
|
+
trackMove: true,
|
|
78
|
+
}),
|
|
79
|
+
shareReplay(),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* We release when we detect a pointer leave and there are no more
|
|
84
|
+
* active fingers.
|
|
85
|
+
*
|
|
86
|
+
* @todo move to utils
|
|
87
|
+
*/
|
|
88
|
+
const panReleased$ = merge(
|
|
89
|
+
pointerUp$,
|
|
90
|
+
pointerLeave$,
|
|
91
|
+
pointerCancel$,
|
|
92
|
+
).pipe(
|
|
93
|
+
withLatestFrom(trackFingers$),
|
|
94
|
+
filter(([_, pointers]) => !pointers.length),
|
|
95
|
+
map(([event]) => event),
|
|
96
|
+
first(),
|
|
97
|
+
share(),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const firstPointerDownMovingOutOfThreshold$ = pointerDowns$.pipe(
|
|
101
|
+
mergeMap((pointerDown) =>
|
|
102
|
+
pointerMove$.pipe(
|
|
103
|
+
matchPointer(pointerDown),
|
|
104
|
+
// we start pan only if the user started moving a certain distance
|
|
105
|
+
filter((pointerMoveEvent) =>
|
|
106
|
+
isOutsidePosThreshold(
|
|
107
|
+
pointerDown,
|
|
108
|
+
pointerMoveEvent,
|
|
109
|
+
posThreshold,
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
first(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const panStart$ = firstPointerDownMovingOutOfThreshold$.pipe(
|
|
118
|
+
map(() => ({
|
|
119
|
+
type: "panStart" as const,
|
|
120
|
+
event: initialPointerDownEvent,
|
|
121
|
+
})),
|
|
122
|
+
share(),
|
|
123
|
+
takeUntil(panReleased$),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const panEnd$ = panStart$.pipe(
|
|
127
|
+
mergeMap(() =>
|
|
128
|
+
panReleased$.pipe(
|
|
129
|
+
map((endEvent) => ({
|
|
130
|
+
type: "panEnd" as const,
|
|
131
|
+
event: endEvent,
|
|
132
|
+
})),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const panUpdate$ = pointerUpdate$.pipe(
|
|
138
|
+
skipUntil(panStart$),
|
|
139
|
+
map((event) => ({
|
|
140
|
+
type: "panMove" as const,
|
|
141
|
+
event,
|
|
142
|
+
})),
|
|
143
|
+
takeUntil(panReleased$),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const rawEvent$ = merge(panStart$, panUpdate$, panEnd$).pipe(
|
|
147
|
+
withLatestFrom(trackFingers$),
|
|
148
|
+
map(([event, pointers]) => ({
|
|
149
|
+
...event,
|
|
150
|
+
latestActivePointers: pointers,
|
|
151
|
+
})),
|
|
152
|
+
share(),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return combineLatest([
|
|
156
|
+
rawEvent$.pipe(mapToRecognizerEvent),
|
|
157
|
+
rawEvent$,
|
|
158
|
+
]).pipe(
|
|
159
|
+
map(([recognizerEvent, { type }]) => ({
|
|
160
|
+
...recognizerEvent,
|
|
161
|
+
type,
|
|
162
|
+
})),
|
|
163
|
+
)
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
}),
|
|
167
|
+
share(),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
this.start$ = this.events$.pipe(
|
|
171
|
+
filter((event) => event.type === "panStart"),
|
|
172
|
+
)
|
|
173
|
+
this.end$ = this.events$.pipe(filter((event) => event.type === "panEnd"))
|
|
174
|
+
|
|
175
|
+
this.fingers$ = this.validConfig$.pipe(
|
|
176
|
+
switchMap(({ container, afterEventReceived }) => {
|
|
177
|
+
const pointerEvents = getPointerEvents({
|
|
178
|
+
container,
|
|
179
|
+
afterEventReceived,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return pointerEvents.pointerDown$.pipe(
|
|
183
|
+
trackFingers({
|
|
184
|
+
...pointerEvents,
|
|
185
|
+
trackMove: false,
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
188
|
+
}),
|
|
189
|
+
map((events) => events.length),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|