1 | import { Engine } from './Engine'
|
---|
2 | import { touchDistanceAngle, distanceAngle, wheelValues } from '../utils/events'
|
---|
3 | import { V } from '../utils/maths'
|
---|
4 | import { Vector2, WebKitGestureEvent } from '../types'
|
---|
5 | import { clampStateInternalMovementToBounds } from '../utils/state'
|
---|
6 |
|
---|
7 | const SCALE_ANGLE_RATIO_INTENT_DEG = 30
|
---|
8 | const PINCH_WHEEL_RATIO = 100
|
---|
9 |
|
---|
10 | export class PinchEngine extends Engine<'pinch'> {
|
---|
11 | ingKey = 'pinching' as const
|
---|
12 | aliasKey = 'da'
|
---|
13 |
|
---|
14 | init() {
|
---|
15 | this.state.offset = [1, 0]
|
---|
16 | this.state.lastOffset = [1, 0]
|
---|
17 | this.state._pointerEvents = new Map()
|
---|
18 | }
|
---|
19 |
|
---|
20 | // superseeds generic Engine reset call
|
---|
21 | reset() {
|
---|
22 | super.reset()
|
---|
23 | const state = this.state
|
---|
24 | state._touchIds = []
|
---|
25 | state.canceled = false
|
---|
26 | state.cancel = this.cancel.bind(this)
|
---|
27 | state.turns = 0
|
---|
28 | }
|
---|
29 |
|
---|
30 | computeOffset() {
|
---|
31 | const { type, movement, lastOffset } = this.state
|
---|
32 | if (type === 'wheel') {
|
---|
33 | this.state.offset = V.add(movement, lastOffset)
|
---|
34 | } else {
|
---|
35 | this.state.offset = [(1 + movement[0]) * lastOffset[0], movement[1] + lastOffset[1]]
|
---|
36 | }
|
---|
37 | }
|
---|
38 |
|
---|
39 | computeMovement() {
|
---|
40 | const { offset, lastOffset } = this.state
|
---|
41 | this.state.movement = [offset[0] / lastOffset[0], offset[1] - lastOffset[1]]
|
---|
42 | }
|
---|
43 |
|
---|
44 | axisIntent() {
|
---|
45 | const state = this.state
|
---|
46 | const [_m0, _m1] = state._movement
|
---|
47 | if (!state.axis) {
|
---|
48 | const axisMovementDifference = Math.abs(_m0) * SCALE_ANGLE_RATIO_INTENT_DEG - Math.abs(_m1)
|
---|
49 | if (axisMovementDifference < 0) state.axis = 'angle'
|
---|
50 | else if (axisMovementDifference > 0) state.axis = 'scale'
|
---|
51 | }
|
---|
52 | }
|
---|
53 |
|
---|
54 | restrictToAxis(v: Vector2) {
|
---|
55 | if (this.config.lockDirection) {
|
---|
56 | if (this.state.axis === 'scale') v[1] = 0
|
---|
57 | else if (this.state.axis === 'angle') v[0] = 0
|
---|
58 | }
|
---|
59 | }
|
---|
60 |
|
---|
61 | cancel() {
|
---|
62 | const state = this.state
|
---|
63 | if (state.canceled) return
|
---|
64 | setTimeout(() => {
|
---|
65 | state.canceled = true
|
---|
66 | state._active = false
|
---|
67 | // we run compute with no event so that kinematics won't be computed
|
---|
68 | this.compute()
|
---|
69 | this.emit()
|
---|
70 | }, 0)
|
---|
71 | }
|
---|
72 |
|
---|
73 | touchStart(event: TouchEvent) {
|
---|
74 | this.ctrl.setEventIds(event)
|
---|
75 | const state = this.state
|
---|
76 | const ctrlTouchIds = this.ctrl.touchIds
|
---|
77 |
|
---|
78 | if (state._active) {
|
---|
79 | // check that the touchIds that initiated the gesture are still enabled
|
---|
80 | // This is useful for when the page loses track of the pointers (minifying
|
---|
81 | // gesture on iPad).
|
---|
82 | if (state._touchIds.every((id) => ctrlTouchIds.has(id))) return
|
---|
83 | // The gesture is still active, but probably didn't have the opportunity to
|
---|
84 | // end properly, so we restart the pinch.
|
---|
85 | }
|
---|
86 |
|
---|
87 | if (ctrlTouchIds.size < 2) return
|
---|
88 |
|
---|
89 | this.start(event)
|
---|
90 | state._touchIds = Array.from(ctrlTouchIds).slice(0, 2) as [number, number]
|
---|
91 |
|
---|
92 | const payload = touchDistanceAngle(event, state._touchIds)
|
---|
93 |
|
---|
94 | if (!payload) return
|
---|
95 | this.pinchStart(event, payload)
|
---|
96 | }
|
---|
97 |
|
---|
98 | pointerStart(event: PointerEvent) {
|
---|
99 | if (event.buttons != null && event.buttons % 2 !== 1) return
|
---|
100 | this.ctrl.setEventIds(event)
|
---|
101 | ;(event.target as HTMLElement).setPointerCapture(event.pointerId)
|
---|
102 | const state = this.state
|
---|
103 | const _pointerEvents = state._pointerEvents
|
---|
104 | const ctrlPointerIds = this.ctrl.pointerIds
|
---|
105 |
|
---|
106 | if (state._active) {
|
---|
107 | // see touchStart comment
|
---|
108 | if (Array.from(_pointerEvents.keys()).every((id) => ctrlPointerIds.has(id))) return
|
---|
109 | }
|
---|
110 |
|
---|
111 | if (_pointerEvents.size < 2) {
|
---|
112 | _pointerEvents.set(event.pointerId, event)
|
---|
113 | }
|
---|
114 |
|
---|
115 | if (state._pointerEvents.size < 2) return
|
---|
116 |
|
---|
117 | this.start(event)
|
---|
118 |
|
---|
119 | // @ts-ignore
|
---|
120 | const payload = distanceAngle(...Array.from(_pointerEvents.values()))
|
---|
121 |
|
---|
122 | if (!payload) return
|
---|
123 | this.pinchStart(event, payload)
|
---|
124 | }
|
---|
125 |
|
---|
126 | pinchStart(event: PointerEvent | TouchEvent, payload: { distance: number; angle: number; origin: Vector2 }) {
|
---|
127 | const state = this.state
|
---|
128 | state.origin = payload.origin
|
---|
129 | this.computeValues([payload.distance, payload.angle])
|
---|
130 | this.computeInitial()
|
---|
131 |
|
---|
132 | this.compute(event)
|
---|
133 | this.emit()
|
---|
134 | }
|
---|
135 |
|
---|
136 | touchMove(event: TouchEvent) {
|
---|
137 | if (!this.state._active) return
|
---|
138 | const payload = touchDistanceAngle(event, this.state._touchIds)
|
---|
139 |
|
---|
140 | if (!payload) return
|
---|
141 | this.pinchMove(event, payload)
|
---|
142 | }
|
---|
143 |
|
---|
144 | pointerMove(event: PointerEvent) {
|
---|
145 | const _pointerEvents = this.state._pointerEvents
|
---|
146 | if (_pointerEvents.has(event.pointerId)) {
|
---|
147 | _pointerEvents.set(event.pointerId, event)
|
---|
148 | }
|
---|
149 | if (!this.state._active) return
|
---|
150 | // @ts-ignore
|
---|
151 | const payload = distanceAngle(...Array.from(_pointerEvents.values()))
|
---|
152 |
|
---|
153 | if (!payload) return
|
---|
154 | this.pinchMove(event, payload)
|
---|
155 | }
|
---|
156 |
|
---|
157 | pinchMove(event: PointerEvent | TouchEvent, payload: { distance: number; angle: number; origin: Vector2 }) {
|
---|
158 | const state = this.state
|
---|
159 | const prev_a = state._values[1]
|
---|
160 | const delta_a = payload.angle - prev_a
|
---|
161 |
|
---|
162 | let delta_turns = 0
|
---|
163 | if (Math.abs(delta_a) > 270) delta_turns += Math.sign(delta_a)
|
---|
164 |
|
---|
165 | this.computeValues([payload.distance, payload.angle - 360 * delta_turns])
|
---|
166 |
|
---|
167 | state.origin = payload.origin
|
---|
168 | state.turns = delta_turns
|
---|
169 | state._movement = [state._values[0] / state._initial[0] - 1, state._values[1] - state._initial[1]]
|
---|
170 |
|
---|
171 | this.compute(event)
|
---|
172 | this.emit()
|
---|
173 | }
|
---|
174 |
|
---|
175 | touchEnd(event: TouchEvent) {
|
---|
176 | this.ctrl.setEventIds(event)
|
---|
177 | if (!this.state._active) return
|
---|
178 |
|
---|
179 | if (this.state._touchIds.some((id) => !this.ctrl.touchIds.has(id))) {
|
---|
180 | this.state._active = false
|
---|
181 |
|
---|
182 | this.compute(event)
|
---|
183 | this.emit()
|
---|
184 | }
|
---|
185 | }
|
---|
186 |
|
---|
187 | pointerEnd(event: PointerEvent) {
|
---|
188 | const state = this.state
|
---|
189 | this.ctrl.setEventIds(event)
|
---|
190 | try {
|
---|
191 | // @ts-ignore r3f
|
---|
192 | event.target.releasePointerCapture(event.pointerId)
|
---|
193 | } catch {}
|
---|
194 |
|
---|
195 | if (state._pointerEvents.has(event.pointerId)) {
|
---|
196 | state._pointerEvents.delete(event.pointerId)
|
---|
197 | }
|
---|
198 |
|
---|
199 | if (!state._active) return
|
---|
200 |
|
---|
201 | if (state._pointerEvents.size < 2) {
|
---|
202 | state._active = false
|
---|
203 | this.compute(event)
|
---|
204 | this.emit()
|
---|
205 | }
|
---|
206 | }
|
---|
207 |
|
---|
208 | gestureStart(event: WebKitGestureEvent) {
|
---|
209 | if (event.cancelable) event.preventDefault()
|
---|
210 | const state = this.state
|
---|
211 |
|
---|
212 | if (state._active) return
|
---|
213 |
|
---|
214 | this.start(event)
|
---|
215 | this.computeValues([event.scale, event.rotation])
|
---|
216 | state.origin = [event.clientX, event.clientY]
|
---|
217 | this.compute(event)
|
---|
218 |
|
---|
219 | this.emit()
|
---|
220 | }
|
---|
221 |
|
---|
222 | gestureMove(event: WebKitGestureEvent) {
|
---|
223 | if (event.cancelable) event.preventDefault()
|
---|
224 |
|
---|
225 | if (!this.state._active) return
|
---|
226 |
|
---|
227 | const state = this.state
|
---|
228 |
|
---|
229 | this.computeValues([event.scale, event.rotation])
|
---|
230 | state.origin = [event.clientX, event.clientY]
|
---|
231 | const _previousMovement = state._movement
|
---|
232 | state._movement = [event.scale - 1, event.rotation]
|
---|
233 | state._delta = V.sub(state._movement, _previousMovement)
|
---|
234 | this.compute(event)
|
---|
235 | this.emit()
|
---|
236 | }
|
---|
237 |
|
---|
238 | gestureEnd(event: WebKitGestureEvent) {
|
---|
239 | if (!this.state._active) return
|
---|
240 |
|
---|
241 | this.state._active = false
|
---|
242 |
|
---|
243 | this.compute(event)
|
---|
244 | this.emit()
|
---|
245 | }
|
---|
246 |
|
---|
247 | wheel(event: WheelEvent) {
|
---|
248 | const modifierKey = this.config.modifierKey
|
---|
249 | if (modifierKey && (Array.isArray(modifierKey) ? !modifierKey.find((k) => event[k]) : !event[modifierKey])) return
|
---|
250 | if (!this.state._active) this.wheelStart(event)
|
---|
251 | else this.wheelChange(event)
|
---|
252 | this.timeoutStore.add('wheelEnd', this.wheelEnd.bind(this))
|
---|
253 | }
|
---|
254 |
|
---|
255 | wheelStart(event: WheelEvent) {
|
---|
256 | this.start(event)
|
---|
257 | this.wheelChange(event)
|
---|
258 | }
|
---|
259 |
|
---|
260 | wheelChange(event: WheelEvent) {
|
---|
261 | const isR3f = 'uv' in event
|
---|
262 | if (!isR3f) {
|
---|
263 | if (event.cancelable) {
|
---|
264 | event.preventDefault()
|
---|
265 | }
|
---|
266 | if (process.env.NODE_ENV === 'development' && !event.defaultPrevented) {
|
---|
267 | // eslint-disable-next-line no-console
|
---|
268 | console.warn(
|
---|
269 | `[@use-gesture]: To properly support zoom on trackpads, try using the \`target\` option.\n\nThis message will only appear in development mode.`
|
---|
270 | )
|
---|
271 | }
|
---|
272 | }
|
---|
273 | const state = this.state
|
---|
274 | state._delta = [(-wheelValues(event)[1] / PINCH_WHEEL_RATIO) * state.offset[0], 0]
|
---|
275 | V.addTo(state._movement, state._delta)
|
---|
276 |
|
---|
277 | // _movement rolls back to when it passed the bounds.
|
---|
278 | clampStateInternalMovementToBounds(state)
|
---|
279 |
|
---|
280 | this.state.origin = [event.clientX, event.clientY]
|
---|
281 |
|
---|
282 | this.compute(event)
|
---|
283 | this.emit()
|
---|
284 | }
|
---|
285 |
|
---|
286 | wheelEnd() {
|
---|
287 | if (!this.state._active) return
|
---|
288 | this.state._active = false
|
---|
289 | this.compute()
|
---|
290 | this.emit()
|
---|
291 | }
|
---|
292 |
|
---|
293 | bind(bindFunction: any) {
|
---|
294 | const device = this.config.device
|
---|
295 | if (!!device) {
|
---|
296 | // @ts-ignore
|
---|
297 | bindFunction(device, 'start', this[device + 'Start'].bind(this))
|
---|
298 | // @ts-ignore
|
---|
299 | bindFunction(device, 'change', this[device + 'Move'].bind(this))
|
---|
300 | // @ts-ignore
|
---|
301 | bindFunction(device, 'end', this[device + 'End'].bind(this))
|
---|
302 | // @ts-ignore
|
---|
303 | bindFunction(device, 'cancel', this[device + 'End'].bind(this))
|
---|
304 | // @ts-ignore
|
---|
305 | bindFunction('lostPointerCapture', '', this[device + 'End'].bind(this))
|
---|
306 | }
|
---|
307 | // we try to set a passive listener, knowing that in any case React will
|
---|
308 | // ignore it.
|
---|
309 | if (this.config.pinchOnWheel) {
|
---|
310 | bindFunction('wheel', '', this.wheel.bind(this), { passive: false })
|
---|
311 | }
|
---|
312 | }
|
---|
313 | }
|
---|