1 | import { Controller } from '../Controller'
|
---|
2 | import { getEventDetails } from '../utils/events'
|
---|
3 | import { call } from '../utils/fn'
|
---|
4 | import { V, computeRubberband } from '../utils/maths'
|
---|
5 | import { GestureKey, IngKey, State, Vector2 } from '../types'
|
---|
6 | import { NonUndefined } from '../types'
|
---|
7 |
|
---|
8 | /**
|
---|
9 | * The lib doesn't compute the kinematics on the last event of the gesture
|
---|
10 | * (i.e. for a drag gesture, the `pointerup` coordinates will generally match the
|
---|
11 | * last `pointermove` coordinates which would result in all drags ending with a
|
---|
12 | * `[0,0]` velocity). However, when the timestamp difference between the last
|
---|
13 | * event (ie pointerup) and the before last event (ie pointermove) is greater
|
---|
14 | * than BEFORE_LAST_KINEMATICS_DELAY, the kinematics are computed (which would
|
---|
15 | * mean that if you release your drag after stopping for more than
|
---|
16 | * BEFORE_LAST_KINEMATICS_DELAY, the velocity will be indeed 0).
|
---|
17 | *
|
---|
18 | * See https://github.com/pmndrs/use-gesture/issues/332 for more details.
|
---|
19 | */
|
---|
20 |
|
---|
21 | const BEFORE_LAST_KINEMATICS_DELAY = 32
|
---|
22 |
|
---|
23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
---|
24 | export interface Engine<Key extends GestureKey> {
|
---|
25 | /**
|
---|
26 | * Function that some gestures can use to add initilization
|
---|
27 | * properties to the state when it is created.
|
---|
28 | */
|
---|
29 | init?(): void
|
---|
30 | /**
|
---|
31 | * Setup function that some gestures can use to set additional properties of
|
---|
32 | * the state when the gesture starts.
|
---|
33 | */
|
---|
34 | setup?(): void
|
---|
35 | /**
|
---|
36 | * Function used by some gestures to determine the intentionality of a
|
---|
37 | * a movement depending on thresholds. The intent function can change the
|
---|
38 | * `state._active` or `state._blocked` flags if the gesture isn't intentional.
|
---|
39 | * @param event
|
---|
40 | */
|
---|
41 | axisIntent?(event?: UIEvent): void
|
---|
42 |
|
---|
43 | restrictToAxis?(movement: Vector2): void
|
---|
44 | }
|
---|
45 |
|
---|
46 | export abstract class Engine<Key extends GestureKey> {
|
---|
47 | /**
|
---|
48 | * The Controller handling state.
|
---|
49 | */
|
---|
50 | ctrl: Controller
|
---|
51 | /**
|
---|
52 | * The gesture key ('drag' | 'pinch' | 'wheel' | 'scroll' | 'move' | 'hover')
|
---|
53 | */
|
---|
54 | readonly key: Key
|
---|
55 | /**
|
---|
56 | * The key representing the active state of the gesture in the shared state.
|
---|
57 | * ('dragging' | 'pinching' | 'wheeling' | 'scrolling' | 'moving' | 'hovering')
|
---|
58 | */
|
---|
59 | abstract readonly ingKey: IngKey
|
---|
60 | /**
|
---|
61 | * The arguments passed to the `bind` function.
|
---|
62 | */
|
---|
63 |
|
---|
64 | /**
|
---|
65 | * State prop that aliases state values (`xy` or `da`).
|
---|
66 | */
|
---|
67 | abstract readonly aliasKey: string
|
---|
68 |
|
---|
69 | args: any[]
|
---|
70 |
|
---|
71 | constructor(ctrl: Controller, args: any[], key: Key) {
|
---|
72 | this.ctrl = ctrl
|
---|
73 | this.args = args
|
---|
74 | this.key = key
|
---|
75 |
|
---|
76 | if (!this.state) {
|
---|
77 | this.state = {} as any
|
---|
78 | this.computeValues([0, 0])
|
---|
79 | this.computeInitial()
|
---|
80 |
|
---|
81 | if (this.init) this.init()
|
---|
82 | this.reset()
|
---|
83 | }
|
---|
84 | }
|
---|
85 | /**
|
---|
86 | * Function implemented by gestures that compute the offset from the state
|
---|
87 | * movement.
|
---|
88 | */
|
---|
89 | abstract computeOffset(): void
|
---|
90 | /**
|
---|
91 | * Function implemented by the gestures that compute the movement from the
|
---|
92 | * corrected offset (after bounds and potential rubberbanding).
|
---|
93 | */
|
---|
94 | abstract computeMovement(): void
|
---|
95 | /**
|
---|
96 | * Executes the bind function so that listeners are properly set by the
|
---|
97 | * Controller.
|
---|
98 | * @param bindFunction
|
---|
99 | */
|
---|
100 | abstract bind(
|
---|
101 | bindFunction: (
|
---|
102 | device: string,
|
---|
103 | action: string,
|
---|
104 | handler: (event: any) => void,
|
---|
105 | options?: AddEventListenerOptions
|
---|
106 | ) => void
|
---|
107 | ): void
|
---|
108 |
|
---|
109 | /**
|
---|
110 | * Shortcut to the gesture state read from the Controller.
|
---|
111 | */
|
---|
112 | get state() {
|
---|
113 | return this.ctrl.state[this.key]!
|
---|
114 | }
|
---|
115 | set state(state) {
|
---|
116 | this.ctrl.state[this.key] = state
|
---|
117 | }
|
---|
118 | /**
|
---|
119 | * Shortcut to the shared state read from the Controller
|
---|
120 | */
|
---|
121 | get shared() {
|
---|
122 | return this.ctrl.state.shared
|
---|
123 | }
|
---|
124 | /**
|
---|
125 | * Shortcut to the gesture event store read from the Controller.
|
---|
126 | */
|
---|
127 | get eventStore() {
|
---|
128 | return this.ctrl.gestureEventStores[this.key]!
|
---|
129 | }
|
---|
130 | /**
|
---|
131 | * Shortcut to the gesture timeout store read from the Controller.
|
---|
132 | */
|
---|
133 | get timeoutStore() {
|
---|
134 | return this.ctrl.gestureTimeoutStores[this.key]!
|
---|
135 | }
|
---|
136 | /**
|
---|
137 | * Shortcut to the gesture config read from the Controller.
|
---|
138 | */
|
---|
139 | get config() {
|
---|
140 | return this.ctrl.config[this.key]!
|
---|
141 | }
|
---|
142 | /**
|
---|
143 | * Shortcut to the shared config read from the Controller.
|
---|
144 | */
|
---|
145 | get sharedConfig() {
|
---|
146 | return this.ctrl.config.shared
|
---|
147 | }
|
---|
148 | /**
|
---|
149 | * Shortcut to the gesture handler read from the Controller.
|
---|
150 | */
|
---|
151 | get handler() {
|
---|
152 | return this.ctrl.handlers[this.key]!
|
---|
153 | }
|
---|
154 |
|
---|
155 | reset() {
|
---|
156 | const { state, shared, ingKey, args } = this
|
---|
157 | shared[ingKey] = state._active = state.active = state._blocked = state._force = false
|
---|
158 | state._step = [false, false]
|
---|
159 | state.intentional = false
|
---|
160 | state._movement = [0, 0]
|
---|
161 | state._distance = [0, 0]
|
---|
162 | state._direction = [0, 0]
|
---|
163 | state._delta = [0, 0]
|
---|
164 | // prettier-ignore
|
---|
165 | state._bounds = [[-Infinity, Infinity], [-Infinity, Infinity]]
|
---|
166 | state.args = args
|
---|
167 | state.axis = undefined
|
---|
168 | state.memo = undefined
|
---|
169 | state.elapsedTime = state.timeDelta = 0
|
---|
170 | state.direction = [0, 0]
|
---|
171 | state.distance = [0, 0]
|
---|
172 | state.overflow = [0, 0]
|
---|
173 | state._movementBound = [false, false]
|
---|
174 | state.velocity = [0, 0]
|
---|
175 | state.movement = [0, 0]
|
---|
176 | state.delta = [0, 0]
|
---|
177 | state.timeStamp = 0
|
---|
178 | }
|
---|
179 | /**
|
---|
180 | * Function ran at the start of the gesture.
|
---|
181 | * @param event
|
---|
182 | */
|
---|
183 | start(event: NonUndefined<State[Key]>['event']) {
|
---|
184 | const state = this.state
|
---|
185 | const config = this.config
|
---|
186 | if (!state._active) {
|
---|
187 | this.reset()
|
---|
188 | this.computeInitial()
|
---|
189 |
|
---|
190 | state._active = true
|
---|
191 | state.target = event.target!
|
---|
192 | state.currentTarget = event.currentTarget!
|
---|
193 | state.lastOffset = config.from ? call(config.from, state) : state.offset
|
---|
194 | state.offset = state.lastOffset
|
---|
195 | state.startTime = state.timeStamp = event.timeStamp
|
---|
196 | }
|
---|
197 | }
|
---|
198 |
|
---|
199 | /**
|
---|
200 | * Assign raw values to `state._values` and transformed values to
|
---|
201 | * `state.values`.
|
---|
202 | * @param values
|
---|
203 | */
|
---|
204 | computeValues(values: Vector2) {
|
---|
205 | const state = this.state
|
---|
206 | state._values = values
|
---|
207 | // transforming values into user-defined coordinates (#402)
|
---|
208 | state.values = this.config.transform(values)
|
---|
209 | }
|
---|
210 |
|
---|
211 | /**
|
---|
212 | * Assign `state._values` to `state._initial` and transformed `state.values` to
|
---|
213 | * `state.initial`.
|
---|
214 | * @param values
|
---|
215 | */
|
---|
216 | computeInitial() {
|
---|
217 | const state = this.state
|
---|
218 | state._initial = state._values
|
---|
219 | state.initial = state.values
|
---|
220 | }
|
---|
221 |
|
---|
222 | /**
|
---|
223 | * Computes all sorts of state attributes, including kinematics.
|
---|
224 | * @param event
|
---|
225 | */
|
---|
226 | compute(event?: NonUndefined<State[Key]>['event']) {
|
---|
227 | const { state, config, shared } = this
|
---|
228 | state.args = this.args
|
---|
229 |
|
---|
230 | let dt = 0
|
---|
231 |
|
---|
232 | if (event) {
|
---|
233 | // sets the shared state with event properties
|
---|
234 | state.event = event
|
---|
235 | // if config.preventDefault is true, then preventDefault
|
---|
236 | if (config.preventDefault && event.cancelable) state.event.preventDefault()
|
---|
237 | state.type = event.type
|
---|
238 | shared.touches = this.ctrl.pointerIds.size || this.ctrl.touchIds.size
|
---|
239 | shared.locked = !!document.pointerLockElement
|
---|
240 | Object.assign(shared, getEventDetails(event))
|
---|
241 | shared.down = shared.pressed = shared.buttons % 2 === 1 || shared.touches > 0
|
---|
242 |
|
---|
243 | // sets time stamps
|
---|
244 | dt = event.timeStamp - state.timeStamp
|
---|
245 | state.timeStamp = event.timeStamp
|
---|
246 | state.elapsedTime = state.timeStamp - state.startTime
|
---|
247 | }
|
---|
248 |
|
---|
249 | // only compute _distance if the state is active otherwise we might compute it
|
---|
250 | // twice when the gesture ends because state._delta wouldn't have changed on
|
---|
251 | // the last frame.
|
---|
252 | if (state._active) {
|
---|
253 | const _absoluteDelta = state._delta.map(Math.abs) as Vector2
|
---|
254 | V.addTo(state._distance, _absoluteDelta)
|
---|
255 | }
|
---|
256 |
|
---|
257 | // let's run intentionality check.
|
---|
258 | if (this.axisIntent) this.axisIntent(event)
|
---|
259 |
|
---|
260 | // _movement is calculated by each gesture engine
|
---|
261 | const [_m0, _m1] = state._movement
|
---|
262 | const [t0, t1] = config.threshold
|
---|
263 |
|
---|
264 | const { _step, values } = state
|
---|
265 |
|
---|
266 | if (config.hasCustomTransform) {
|
---|
267 | // When the user is using a custom transform, we're using `_step` to store
|
---|
268 | // the first value passing the threshold.
|
---|
269 | if (_step[0] === false) _step[0] = Math.abs(_m0) >= t0 && values[0]
|
---|
270 | if (_step[1] === false) _step[1] = Math.abs(_m1) >= t1 && values[1]
|
---|
271 | } else {
|
---|
272 | // `_step` will hold the threshold at which point the gesture was triggered.
|
---|
273 | // The threshold is signed depending on which direction triggered it.
|
---|
274 | if (_step[0] === false) _step[0] = Math.abs(_m0) >= t0 && Math.sign(_m0) * t0
|
---|
275 | if (_step[1] === false) _step[1] = Math.abs(_m1) >= t1 && Math.sign(_m1) * t1
|
---|
276 | }
|
---|
277 |
|
---|
278 | state.intentional = _step[0] !== false || _step[1] !== false
|
---|
279 |
|
---|
280 | if (!state.intentional) return
|
---|
281 |
|
---|
282 | const movement: Vector2 = [0, 0]
|
---|
283 |
|
---|
284 | if (config.hasCustomTransform) {
|
---|
285 | const [v0, v1] = values
|
---|
286 | movement[0] = _step[0] !== false ? v0 - _step[0] : 0
|
---|
287 | movement[1] = _step[1] !== false ? v1 - _step[1] : 0
|
---|
288 | } else {
|
---|
289 | movement[0] = _step[0] !== false ? _m0 - _step[0] : 0
|
---|
290 | movement[1] = _step[1] !== false ? _m1 - _step[1] : 0
|
---|
291 | }
|
---|
292 |
|
---|
293 | if (this.restrictToAxis && !state._blocked) this.restrictToAxis(movement)
|
---|
294 |
|
---|
295 | const previousOffset = state.offset
|
---|
296 |
|
---|
297 | const gestureIsActive = (state._active && !state._blocked) || state.active
|
---|
298 |
|
---|
299 | if (gestureIsActive) {
|
---|
300 | state.first = state._active && !state.active
|
---|
301 | state.last = !state._active && state.active
|
---|
302 | state.active = shared[this.ingKey] = state._active
|
---|
303 |
|
---|
304 | if (event) {
|
---|
305 | if (state.first) {
|
---|
306 | if ('bounds' in config) state._bounds = call(config.bounds, state)
|
---|
307 | if (this.setup) this.setup()
|
---|
308 | }
|
---|
309 |
|
---|
310 | state.movement = movement
|
---|
311 | this.computeOffset()
|
---|
312 | }
|
---|
313 | }
|
---|
314 |
|
---|
315 | const [ox, oy] = state.offset
|
---|
316 | const [[x0, x1], [y0, y1]] = state._bounds
|
---|
317 | state.overflow = [ox < x0 ? -1 : ox > x1 ? 1 : 0, oy < y0 ? -1 : oy > y1 ? 1 : 0]
|
---|
318 |
|
---|
319 | // _movementBound will store the latest _movement value
|
---|
320 | // before it went off bounds.
|
---|
321 | state._movementBound[0] = state.overflow[0]
|
---|
322 | ? state._movementBound[0] === false
|
---|
323 | ? state._movement[0]
|
---|
324 | : state._movementBound[0]
|
---|
325 | : false
|
---|
326 |
|
---|
327 | state._movementBound[1] = state.overflow[1]
|
---|
328 | ? state._movementBound[1] === false
|
---|
329 | ? state._movement[1]
|
---|
330 | : state._movementBound[1]
|
---|
331 | : false
|
---|
332 |
|
---|
333 | // @ts-ignore
|
---|
334 | const rubberband: Vector2 = state._active ? config.rubberband || [0, 0] : [0, 0]
|
---|
335 | state.offset = computeRubberband(state._bounds, state.offset, rubberband)
|
---|
336 | state.delta = V.sub(state.offset, previousOffset)
|
---|
337 |
|
---|
338 | this.computeMovement()
|
---|
339 |
|
---|
340 | if (gestureIsActive && (!state.last || dt > BEFORE_LAST_KINEMATICS_DELAY)) {
|
---|
341 | state.delta = V.sub(state.offset, previousOffset)
|
---|
342 | const absoluteDelta = state.delta.map(Math.abs) as Vector2
|
---|
343 |
|
---|
344 | V.addTo(state.distance, absoluteDelta)
|
---|
345 | state.direction = state.delta.map(Math.sign) as Vector2
|
---|
346 | state._direction = state._delta.map(Math.sign) as Vector2
|
---|
347 |
|
---|
348 | // calculates kinematics unless the gesture starts or ends or if the
|
---|
349 | // dt === 0 (which can happen on high frame rate monitors, see issue #581)
|
---|
350 | // because of privacy protection:
|
---|
351 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/timeStamp#reduced_time_precision
|
---|
352 | if (!state.first && dt > 0) {
|
---|
353 | state.velocity = [absoluteDelta[0] / dt, absoluteDelta[1] / dt]
|
---|
354 | state.timeDelta = dt
|
---|
355 | }
|
---|
356 | }
|
---|
357 | }
|
---|
358 | /**
|
---|
359 | * Fires the gesture handler.
|
---|
360 | */
|
---|
361 | emit() {
|
---|
362 | const state = this.state
|
---|
363 | const shared = this.shared
|
---|
364 | const config = this.config
|
---|
365 |
|
---|
366 | if (!state._active) this.clean()
|
---|
367 |
|
---|
368 | // we don't trigger the handler if the gesture is blocked or non intentional,
|
---|
369 | // unless the `_force` flag was set or the `triggerAllEvents` option was set
|
---|
370 | // to true in the config.
|
---|
371 | if ((state._blocked || !state.intentional) && !state._force && !config.triggerAllEvents) return
|
---|
372 |
|
---|
373 | // @ts-ignore
|
---|
374 | const memo = this.handler({ ...shared, ...state, [this.aliasKey]: state.values })
|
---|
375 |
|
---|
376 | // Sets memo to the returned value of the handler (unless it's undefined)
|
---|
377 | if (memo !== undefined) state.memo = memo
|
---|
378 | }
|
---|
379 | /**
|
---|
380 | * Cleans the gesture timeouts and event listeners.
|
---|
381 | */
|
---|
382 | clean() {
|
---|
383 | this.eventStore.clean()
|
---|
384 | this.timeoutStore.clean()
|
---|
385 | }
|
---|
386 | }
|
---|