@weasel-js/gestures 1.9.4 → 1.10.1
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/README.md +106 -4
- package/dist/index.d.ts +434 -219
- package/dist/index.js +521 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# @weasel-js/gestures
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The vocabulary weasel uses to say which user input fires what, and the pure
|
|
4
|
+
functions that decide whether an input matches. It has no React, no scene, and
|
|
5
|
+
no dependencies, so it can run in a test, a worker, or a host that is not
|
|
6
|
+
weasel at all.
|
|
4
7
|
|
|
5
8
|
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
6
9
|
scene-graph canvas kit for React. See the
|
|
7
|
-
[API reference](https://orochi235.github.io/weasel/
|
|
10
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/gestures).
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
@@ -12,12 +15,111 @@ scene-graph canvas kit for React. See the
|
|
|
12
15
|
npm install @weasel-js/gestures
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
##
|
|
18
|
+
## Terms
|
|
19
|
+
|
|
20
|
+
These words have fixed meanings across weasel:
|
|
21
|
+
|
|
22
|
+
- A **gesture** is a form of input: a click, a drag, a wheel turn, a keystroke,
|
|
23
|
+
a pinch, a file dropped on the canvas.
|
|
24
|
+
- An **action** is a user-intent operation: move, delete, zoom in.
|
|
25
|
+
- An **interaction** is a gesture bound to an action: "shift-click on empty
|
|
26
|
+
canvas adds to the selection".
|
|
27
|
+
|
|
28
|
+
This package covers the gesture side only. It describes gestures, recognizes
|
|
29
|
+
them from raw pointer input, and matches them against descriptions. Actions,
|
|
30
|
+
and the dispatcher that runs the action a matched gesture is bound to, live in
|
|
31
|
+
other weasel packages.
|
|
32
|
+
|
|
33
|
+
## What is in it
|
|
34
|
+
|
|
35
|
+
### Describing a gesture
|
|
36
|
+
|
|
37
|
+
A `GestureSpec` is an object describing one gesture a binding listens for:
|
|
38
|
+
its `kind` (`'click'`, `'drag'`, `'key'`, `'wheel'`, and so on), and optionally
|
|
39
|
+
the modifier keys that must be held, what the pointer must be over, and the
|
|
40
|
+
phase the owning tool must be in.
|
|
41
|
+
|
|
42
|
+
A **route** is the same description as a string, short enough to write inline
|
|
43
|
+
and to show in a shortcut list: `[initial] click => empty +shift`.
|
|
44
|
+
|
|
45
|
+
`parseRoute` turns a route into a `ParsedRoute`, `formatRoute` writes the
|
|
46
|
+
canonical string back, `routeToSpec` converts a parsed route into the
|
|
47
|
+
`GestureSpec` the matcher takes, and `describeRoute` renders it as an English
|
|
48
|
+
sentence. `GESTURE_DESCRIPTORS` is the table of gesture names the grammar
|
|
49
|
+
accepts, with whether each takes a target and what argument it carries.
|
|
50
|
+
|
|
51
|
+
### Matching input against a description
|
|
52
|
+
|
|
53
|
+
An `InputEvent` is one normalized input: a union keyed by `kind`, each arm
|
|
54
|
+
carrying the modifier state and whatever that kind of input reports.
|
|
55
|
+
|
|
56
|
+
`matchSpec(event, spec, isMac)` returns whether an event satisfies a spec.
|
|
57
|
+
Its parts are exported too: `matchModifiers`, `matchKey`, `matchTarget`, and
|
|
58
|
+
`matchPhase`. When several specs match one event, `specificity(spec)` gives a
|
|
59
|
+
tuple to rank them by, so the narrower description wins.
|
|
60
|
+
|
|
61
|
+
### Recognizing gestures from pointer input
|
|
62
|
+
|
|
63
|
+
`createPointerRecognizer` turns a stream of pointer presses, moves, and
|
|
64
|
+
releases into `InputEvent`s. It decides when a press has moved far enough to be
|
|
65
|
+
a drag, when a release in place is a click, when two clicks make a double
|
|
66
|
+
click, when a held touch is a long-press, and when several pointers form a
|
|
67
|
+
multitouch gesture. It touches no DOM: the host feeds it samples and supplies
|
|
68
|
+
the clock, and it hands each recognized event to a sink the host provides.
|
|
69
|
+
|
|
70
|
+
The thresholds it uses are exported as constants: `DRAG_THRESHOLD_PX`,
|
|
71
|
+
`LONG_PRESS_MS`, `DOUBLE_CLICK_MAX_MS`, `DOUBLE_CLICK_MAX_PX`, and
|
|
72
|
+
`TAP_THRESHOLD_PX`.
|
|
73
|
+
|
|
74
|
+
### How they fit together
|
|
75
|
+
|
|
76
|
+
| From | Through | To |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Pointer samples | `createPointerRecognizer` | `InputEvent` |
|
|
79
|
+
| Route string | `parseRoute`, then `routeToSpec` | `GestureSpec` |
|
|
80
|
+
| `InputEvent` and `GestureSpec` | `matchSpec` | `true` or `false` |
|
|
81
|
+
|
|
82
|
+
A host listens to the DOM, feeds pointer samples to a recognizer, and builds
|
|
83
|
+
key, wheel, and drop events itself. Each binding carries a `GestureSpec`,
|
|
84
|
+
written directly or parsed from a route. For every event, the host asks
|
|
85
|
+
`matchSpec` which bindings apply, ranks them with `specificity`, and runs the
|
|
86
|
+
winner's action.
|
|
87
|
+
|
|
88
|
+
The package also exports a few helpers a host needs while doing this:
|
|
89
|
+
`isApplePlatform` to decide what the `mod` modifier means, and
|
|
90
|
+
`isEditableTarget`, `focusedControlOwnsKey`, and `isInControlWithin` to leave
|
|
91
|
+
keys and presses alone when they belong to a text field or a button.
|
|
92
|
+
|
|
93
|
+
## Example
|
|
94
|
+
|
|
95
|
+
Parse a route, turn it into a spec, and test an event against it:
|
|
16
96
|
|
|
17
97
|
```ts
|
|
18
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
describeRoute, matchSpec, parseRoute, routeToSpec, type InputEvent,
|
|
100
|
+
} from '@weasel-js/gestures';
|
|
101
|
+
|
|
102
|
+
const route = parseRoute('[*:*] click => empty +shift');
|
|
103
|
+
const spec = routeToSpec(route);
|
|
104
|
+
// { kind: 'click', target: 'empty', mods: { shift: true } }
|
|
105
|
+
|
|
106
|
+
const shiftClick: InputEvent = {
|
|
107
|
+
kind: 'click',
|
|
108
|
+
bodyTarget: 'empty',
|
|
109
|
+
altKey: false, ctrlKey: false, metaKey: false, shiftKey: true,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
matchSpec(shiftClick, spec, false); // true
|
|
113
|
+
matchSpec({ ...shiftClick, shiftKey: false }, spec, false); // false: shift is required
|
|
114
|
+
|
|
115
|
+
describeRoute(parseRoute('[initial] drag +mod ?shift'));
|
|
116
|
+
// 'Fires when the user Mod-drags anywhere, while the tool is idle (Shift optional).'
|
|
19
117
|
```
|
|
20
118
|
|
|
119
|
+
Modifier matching is strict. A modifier the spec does not mention must not be
|
|
120
|
+
held, so a spec for plain `click` does not match a shift-click. Mark a modifier
|
|
121
|
+
`?shift` in a route, or `shift: 'optional'` in a spec, to accept it either way.
|
|
122
|
+
|
|
21
123
|
## License
|
|
22
124
|
|
|
23
125
|
MIT
|