rege 0.1.0 → 0.3.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/dist/{index.d.mts → index.d.ts} +12 -12
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.ts +53 -0
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/{types-dbd32c09.d.ts → types-372314b9.d.ts} +81 -66
- package/package.json +61 -35
- package/src/drag/index.ts +126 -0
- package/src/drag/react.ts +22 -0
- package/src/drag/types.ts +29 -0
- package/src/hover/index.ts +121 -0
- package/src/hover/react.ts +22 -0
- package/src/hover/types.ts +25 -0
- package/src/index.ts +7 -0
- package/src/key/index.ts +37 -0
- package/src/key/react.ts +20 -0
- package/src/key/types.ts +12 -0
- package/src/pinch/index.ts +447 -0
- package/src/pinch/react.ts +22 -0
- package/src/pinch/types.ts +63 -0
- package/src/pinch/utils.ts +106 -0
- package/src/react.ts +7 -0
- package/src/resize/index.ts +49 -0
- package/src/resize/react.ts +22 -0
- package/src/resize/types.ts +10 -0
- package/src/scroll/index.ts +101 -0
- package/src/scroll/react.ts +22 -0
- package/src/scroll/types.ts +29 -0
- package/src/scroll/utils.ts +7 -0
- package/src/utils.ts +109 -0
- package/src/wheel/index.ts +98 -0
- package/src/wheel/react.ts +22 -0
- package/src/wheel/types.ts +29 -0
- package/src/wheel/utils.ts +19 -0
- package/dist/react.d.mts +0 -52
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { EventState, event } from 'reev/src'
|
|
2
|
+
import { PinchState } from './types'
|
|
3
|
+
import { vec2, addV, subV, cpV } from '../utils'
|
|
4
|
+
import { pinchDevice, touchDistanceAngle, pointerDistanceAngle, getCurrentTargetTouchIds, wheelPinchDelta, WebKitGestureEvent, DistanceAngle } from './utils'
|
|
5
|
+
|
|
6
|
+
export * from './types'
|
|
7
|
+
|
|
8
|
+
export const EVENT_FOR_PINCH = {
|
|
9
|
+
touch: {
|
|
10
|
+
start: 'touchstart',
|
|
11
|
+
move: 'touchmove',
|
|
12
|
+
end: 'touchend',
|
|
13
|
+
cancel: 'touchcancel',
|
|
14
|
+
},
|
|
15
|
+
pointer: {
|
|
16
|
+
start: 'pointerdown',
|
|
17
|
+
move: 'pointermove',
|
|
18
|
+
end: 'pointerup',
|
|
19
|
+
cancel: 'pointercancel',
|
|
20
|
+
},
|
|
21
|
+
gesture: {
|
|
22
|
+
start: 'gesturestart',
|
|
23
|
+
change: 'gesturechange',
|
|
24
|
+
end: 'gestureend',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const pinchEvent = <El extends Element = Element>(config: Partial<PinchState<El>> = {}) => {
|
|
29
|
+
const initValues = () => {
|
|
30
|
+
vec2(0, 0, self.value)
|
|
31
|
+
vec2(0, 0, self._value)
|
|
32
|
+
vec2(0, 0, self.delta)
|
|
33
|
+
vec2(0, 0, self.movement)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pinch = () => {
|
|
37
|
+
self.isPinchStart = self.active && !self._active
|
|
38
|
+
self.isPinching = self.active && self._active
|
|
39
|
+
self.isPinchEnd = !self.active && self._active
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ===== Common pinch handlers =====
|
|
43
|
+
|
|
44
|
+
const pinchStart = (e: Event, payload: DistanceAngle) => {
|
|
45
|
+
self.event = e
|
|
46
|
+
self.active = true
|
|
47
|
+
|
|
48
|
+
// Store initial values [distance, angle]
|
|
49
|
+
vec2(payload.distance, payload.angle, self.value)
|
|
50
|
+
vec2(payload.distance, payload.angle, self.initial)
|
|
51
|
+
cpV(payload.origin, self.origin)
|
|
52
|
+
|
|
53
|
+
self.scale = 1
|
|
54
|
+
self.turns = 0
|
|
55
|
+
|
|
56
|
+
self.pinch(self)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pinching = (e: Event, payload: DistanceAngle) => {
|
|
60
|
+
if (!self.active) return
|
|
61
|
+
|
|
62
|
+
self.event = e
|
|
63
|
+
self._active = self.active
|
|
64
|
+
|
|
65
|
+
// Store previous values
|
|
66
|
+
cpV(self.value, self._value)
|
|
67
|
+
|
|
68
|
+
// Current values [distance, angle]
|
|
69
|
+
const prevAngle = self._value[1]
|
|
70
|
+
let newAngle = payload.angle
|
|
71
|
+
|
|
72
|
+
// Handle angle wrapping (for full rotations)
|
|
73
|
+
const deltaAngle = newAngle - prevAngle
|
|
74
|
+
if (Math.abs(deltaAngle) > 270) {
|
|
75
|
+
self.turns += Math.sign(deltaAngle)
|
|
76
|
+
newAngle -= 360 * Math.sign(deltaAngle)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
vec2(payload.distance, newAngle, self.value)
|
|
80
|
+
cpV(payload.origin, self.origin)
|
|
81
|
+
|
|
82
|
+
// Calculate movement: [scaleRatio - 1, angleDelta]
|
|
83
|
+
// scaleRatio = currentDistance / initialDistance
|
|
84
|
+
const scaleRatio = self.value[0] / self.initial[0] - 1
|
|
85
|
+
const angleDelta = self.value[1] - self.initial[1]
|
|
86
|
+
vec2(scaleRatio, angleDelta, self.movement)
|
|
87
|
+
|
|
88
|
+
// Calculate delta from previous frame
|
|
89
|
+
subV(self.value, self._value, self.delta)
|
|
90
|
+
// Convert distance delta to scale delta for consistency
|
|
91
|
+
self.delta[0] = self._value[0] !== 0 ? self.value[0] / self._value[0] - 1 : 0
|
|
92
|
+
|
|
93
|
+
// Update offset (cumulative)
|
|
94
|
+
addV(self.offset, self.delta, self.offset)
|
|
95
|
+
|
|
96
|
+
// Update scale
|
|
97
|
+
self.scale = self.initial[0] !== 0 ? self.value[0] / self.initial[0] : 1
|
|
98
|
+
|
|
99
|
+
self.pinch(self)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pinchEnd = (e: Event) => {
|
|
103
|
+
self.event = e
|
|
104
|
+
self._active = true
|
|
105
|
+
self.active = false
|
|
106
|
+
initValues()
|
|
107
|
+
self.pinch(self)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ===== Touch event handlers =====
|
|
111
|
+
|
|
112
|
+
const touchStart = (e: TouchEvent) => {
|
|
113
|
+
const touchIds = getCurrentTargetTouchIds(e)
|
|
114
|
+
|
|
115
|
+
if (self.active) {
|
|
116
|
+
// Check if the touches that started the gesture are still present
|
|
117
|
+
if (self._touchIds.every((id) => touchIds.includes(id))) return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (touchIds.length < 2) return
|
|
121
|
+
|
|
122
|
+
// Store the first two touch ids
|
|
123
|
+
self._touchIds = touchIds.slice(0, 2)
|
|
124
|
+
|
|
125
|
+
const payload = touchDistanceAngle(e, self._touchIds)
|
|
126
|
+
if (!payload) return
|
|
127
|
+
|
|
128
|
+
pinchStart(e, payload)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const touchMove = (e: TouchEvent) => {
|
|
132
|
+
if (!self.active) return
|
|
133
|
+
|
|
134
|
+
const payload = touchDistanceAngle(e, self._touchIds)
|
|
135
|
+
if (!payload) return
|
|
136
|
+
|
|
137
|
+
pinching(e, payload)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const touchEnd = (e: TouchEvent) => {
|
|
141
|
+
if (!self.active) return
|
|
142
|
+
|
|
143
|
+
// Check if any of our tracked touches ended
|
|
144
|
+
const currentTouchIds = Array.from(e.touches).map((t) => t.identifier)
|
|
145
|
+
if (self._touchIds.some((id) => !currentTouchIds.includes(id))) {
|
|
146
|
+
pinchEnd(e)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ===== Pointer event handlers =====
|
|
151
|
+
|
|
152
|
+
const pointerStart = (e: PointerEvent) => {
|
|
153
|
+
// Only track left mouse button or touch
|
|
154
|
+
if (e.buttons != null && e.buttons % 2 !== 1) return
|
|
155
|
+
|
|
156
|
+
const _pointerEvents = self._pointerEvents
|
|
157
|
+
|
|
158
|
+
if (self.active) {
|
|
159
|
+
// Check if the pointers that started the gesture are still present
|
|
160
|
+
if (Array.from(_pointerEvents.keys()).every((id) => self._pointerEvents.has(id))) return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Capture the pointer
|
|
164
|
+
try {
|
|
165
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
166
|
+
} catch {}
|
|
167
|
+
|
|
168
|
+
if (_pointerEvents.size < 2) {
|
|
169
|
+
_pointerEvents.set(e.pointerId, e)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (_pointerEvents.size < 2) return
|
|
173
|
+
|
|
174
|
+
const payload = pointerDistanceAngle(_pointerEvents)
|
|
175
|
+
if (!payload) return
|
|
176
|
+
|
|
177
|
+
pinchStart(e, payload)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const pointerMove = (e: PointerEvent) => {
|
|
181
|
+
const _pointerEvents = self._pointerEvents
|
|
182
|
+
|
|
183
|
+
if (_pointerEvents.has(e.pointerId)) {
|
|
184
|
+
_pointerEvents.set(e.pointerId, e)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!self.active) return
|
|
188
|
+
|
|
189
|
+
const payload = pointerDistanceAngle(_pointerEvents)
|
|
190
|
+
if (!payload) return
|
|
191
|
+
|
|
192
|
+
pinching(e, payload)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const pointerEnd = (e: PointerEvent) => {
|
|
196
|
+
try {
|
|
197
|
+
;(e.target as HTMLElement).releasePointerCapture(e.pointerId)
|
|
198
|
+
} catch {}
|
|
199
|
+
|
|
200
|
+
const _pointerEvents = self._pointerEvents
|
|
201
|
+
|
|
202
|
+
if (_pointerEvents.has(e.pointerId)) {
|
|
203
|
+
_pointerEvents.delete(e.pointerId)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!self.active) return
|
|
207
|
+
|
|
208
|
+
if (_pointerEvents.size < 2) {
|
|
209
|
+
pinchEnd(e)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ===== Safari Gesture event handlers =====
|
|
214
|
+
|
|
215
|
+
const gestureStart = (e: Event) => {
|
|
216
|
+
const ge = e as WebKitGestureEvent
|
|
217
|
+
if (e.cancelable) e.preventDefault()
|
|
218
|
+
|
|
219
|
+
if (self.active) return
|
|
220
|
+
|
|
221
|
+
self.event = e
|
|
222
|
+
self.active = true
|
|
223
|
+
|
|
224
|
+
// GestureEvent provides scale and rotation directly
|
|
225
|
+
vec2(ge.scale, ge.rotation, self.value)
|
|
226
|
+
vec2(ge.scale, ge.rotation, self.initial)
|
|
227
|
+
vec2(ge.clientX, ge.clientY, self.origin)
|
|
228
|
+
|
|
229
|
+
self.scale = 1
|
|
230
|
+
self.turns = 0
|
|
231
|
+
|
|
232
|
+
self.pinch(self)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const gestureChange = (e: Event) => {
|
|
236
|
+
const ge = e as WebKitGestureEvent
|
|
237
|
+
if (e.cancelable) e.preventDefault()
|
|
238
|
+
|
|
239
|
+
if (!self.active) return
|
|
240
|
+
|
|
241
|
+
self.event = e
|
|
242
|
+
self._active = self.active
|
|
243
|
+
|
|
244
|
+
cpV(self.value, self._value)
|
|
245
|
+
|
|
246
|
+
// GestureEvent gives us scale and rotation directly
|
|
247
|
+
vec2(ge.scale, ge.rotation, self.value)
|
|
248
|
+
vec2(ge.clientX, ge.clientY, self.origin)
|
|
249
|
+
|
|
250
|
+
// movement = [scale - 1, rotation]
|
|
251
|
+
vec2(ge.scale - 1, ge.rotation, self.movement)
|
|
252
|
+
|
|
253
|
+
// delta from previous
|
|
254
|
+
subV(self.value, self._value, self.delta)
|
|
255
|
+
|
|
256
|
+
// Update offset
|
|
257
|
+
addV(self.offset, self.delta, self.offset)
|
|
258
|
+
|
|
259
|
+
self.scale = ge.scale
|
|
260
|
+
|
|
261
|
+
self.pinch(self)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const gestureEnd = (e: Event) => {
|
|
265
|
+
if (!self.active) return
|
|
266
|
+
pinchEnd(e)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ===== Wheel event handlers (trackpad pinch fallback) =====
|
|
270
|
+
|
|
271
|
+
const wheelStart = (e: WheelEvent) => {
|
|
272
|
+
self.event = e
|
|
273
|
+
self.active = true
|
|
274
|
+
|
|
275
|
+
// For wheel, we track scale change as value[0], rotation as value[1] (always 0)
|
|
276
|
+
vec2(1, 0, self.value)
|
|
277
|
+
vec2(1, 0, self.initial)
|
|
278
|
+
vec2(e.clientX, e.clientY, self.origin)
|
|
279
|
+
|
|
280
|
+
self.scale = 1
|
|
281
|
+
self.turns = 0
|
|
282
|
+
|
|
283
|
+
// Apply the initial wheel delta
|
|
284
|
+
const scaleDelta = wheelPinchDelta(e, self.offset[0] + 1)
|
|
285
|
+
self.delta[0] = scaleDelta
|
|
286
|
+
self.delta[1] = 0
|
|
287
|
+
self.value[0] += scaleDelta
|
|
288
|
+
addV(self.offset, self.delta, self.offset)
|
|
289
|
+
vec2(self.value[0] - 1, 0, self.movement)
|
|
290
|
+
|
|
291
|
+
self.scale = self.value[0]
|
|
292
|
+
|
|
293
|
+
self.pinch(self)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const wheeling = (e: WheelEvent) => {
|
|
297
|
+
// Set up timeout to detect wheel end
|
|
298
|
+
const id = setTimeout(() => wheelEnd(e), self.timeout)
|
|
299
|
+
self.clearTimeout()
|
|
300
|
+
self.clearTimeout = () => clearTimeout(id)
|
|
301
|
+
|
|
302
|
+
if (!self.active) {
|
|
303
|
+
wheelStart(e)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
self.event = e
|
|
308
|
+
self._active = self.active
|
|
309
|
+
|
|
310
|
+
cpV(self.value, self._value)
|
|
311
|
+
vec2(e.clientX, e.clientY, self.origin)
|
|
312
|
+
|
|
313
|
+
// Calculate scale delta from wheel
|
|
314
|
+
const scaleDelta = wheelPinchDelta(e, self.offset[0] + 1)
|
|
315
|
+
self.delta[0] = scaleDelta
|
|
316
|
+
self.delta[1] = 0
|
|
317
|
+
|
|
318
|
+
self.value[0] += scaleDelta
|
|
319
|
+
addV(self.offset, self.delta, self.offset)
|
|
320
|
+
vec2(self.value[0] - 1, 0, self.movement)
|
|
321
|
+
|
|
322
|
+
self.scale = self.value[0]
|
|
323
|
+
|
|
324
|
+
self.pinch(self)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const wheelEnd = (e: Event) => {
|
|
328
|
+
if (!self.active) return
|
|
329
|
+
self.event = e
|
|
330
|
+
self._active = true
|
|
331
|
+
self.active = false
|
|
332
|
+
initValues()
|
|
333
|
+
self.pinch(self)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ===== Lifecycle methods =====
|
|
337
|
+
|
|
338
|
+
const mount = (target: El) => {
|
|
339
|
+
self.target = target
|
|
340
|
+
const device = self.device
|
|
341
|
+
|
|
342
|
+
if (device === 'wheel') {
|
|
343
|
+
target.addEventListener('wheel', wheeling as EventListener, { passive: false })
|
|
344
|
+
} else if (device === 'gesture') {
|
|
345
|
+
const events = EVENT_FOR_PINCH.gesture
|
|
346
|
+
target.addEventListener(events.start, gestureStart)
|
|
347
|
+
target.addEventListener(events.change, gestureChange)
|
|
348
|
+
target.addEventListener(events.end, gestureEnd)
|
|
349
|
+
} else if (device === 'touch') {
|
|
350
|
+
const events = EVENT_FOR_PINCH.touch
|
|
351
|
+
target.addEventListener(events.start, touchStart as EventListener)
|
|
352
|
+
target.addEventListener(events.move, touchMove as EventListener)
|
|
353
|
+
target.addEventListener(events.end, touchEnd as EventListener)
|
|
354
|
+
target.addEventListener(events.cancel, touchEnd as EventListener)
|
|
355
|
+
} else if (device === 'pointer') {
|
|
356
|
+
const events = EVENT_FOR_PINCH.pointer
|
|
357
|
+
target.addEventListener(events.start, pointerStart as EventListener)
|
|
358
|
+
target.addEventListener(events.move, pointerMove as EventListener)
|
|
359
|
+
target.addEventListener(events.end, pointerEnd as EventListener)
|
|
360
|
+
target.addEventListener(events.cancel, pointerEnd as EventListener)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const clean = () => {
|
|
365
|
+
const target = self.target
|
|
366
|
+
if (!target) return
|
|
367
|
+
const device = self.device
|
|
368
|
+
|
|
369
|
+
if (device === 'wheel') {
|
|
370
|
+
target.removeEventListener('wheel', wheeling as EventListener)
|
|
371
|
+
} else if (device === 'gesture') {
|
|
372
|
+
const events = EVENT_FOR_PINCH.gesture
|
|
373
|
+
target.removeEventListener(events.start, gestureStart)
|
|
374
|
+
target.removeEventListener(events.change, gestureChange)
|
|
375
|
+
target.removeEventListener(events.end, gestureEnd)
|
|
376
|
+
} else if (device === 'touch') {
|
|
377
|
+
const events = EVENT_FOR_PINCH.touch
|
|
378
|
+
target.removeEventListener(events.start, touchStart as EventListener)
|
|
379
|
+
target.removeEventListener(events.move, touchMove as EventListener)
|
|
380
|
+
target.removeEventListener(events.end, touchEnd as EventListener)
|
|
381
|
+
target.removeEventListener(events.cancel, touchEnd as EventListener)
|
|
382
|
+
} else if (device === 'pointer') {
|
|
383
|
+
const events = EVENT_FOR_PINCH.pointer
|
|
384
|
+
target.removeEventListener(events.start, pointerStart as EventListener)
|
|
385
|
+
target.removeEventListener(events.move, pointerMove as EventListener)
|
|
386
|
+
target.removeEventListener(events.end, pointerEnd as EventListener)
|
|
387
|
+
target.removeEventListener(events.cancel, pointerEnd as EventListener)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const ref = (el: El | null) => {
|
|
392
|
+
self(config as PinchState<El>)
|
|
393
|
+
if (el) {
|
|
394
|
+
self.mount(el)
|
|
395
|
+
} else self.clean()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const self = event({
|
|
399
|
+
_active: false,
|
|
400
|
+
active: false,
|
|
401
|
+
device: pinchDevice(),
|
|
402
|
+
_value: vec2(0, 0),
|
|
403
|
+
value: vec2(0, 0),
|
|
404
|
+
delta: vec2(0, 0),
|
|
405
|
+
offset: vec2(0, 0),
|
|
406
|
+
movement: vec2(0, 0),
|
|
407
|
+
initial: vec2(0, 0),
|
|
408
|
+
origin: vec2(0, 0),
|
|
409
|
+
_touchIds: [] as number[],
|
|
410
|
+
_pointerEvents: new Map<number, PointerEvent>(),
|
|
411
|
+
scale: 1,
|
|
412
|
+
turns: 0,
|
|
413
|
+
target: null as unknown as El,
|
|
414
|
+
event: null as unknown as Event,
|
|
415
|
+
memo: {},
|
|
416
|
+
timeout: 100,
|
|
417
|
+
clearTimeout: () => {},
|
|
418
|
+
isPinchStart: false,
|
|
419
|
+
isPinching: false,
|
|
420
|
+
isPinchEnd: false,
|
|
421
|
+
pinch,
|
|
422
|
+
pinchStart: (e: Event) => {
|
|
423
|
+
// This is called externally, need to detect device and delegate
|
|
424
|
+
if (self.device === 'touch') touchStart(e as TouchEvent)
|
|
425
|
+
else if (self.device === 'pointer') pointerStart(e as PointerEvent)
|
|
426
|
+
else if (self.device === 'gesture') gestureStart(e)
|
|
427
|
+
else if (self.device === 'wheel') wheeling(e as WheelEvent)
|
|
428
|
+
},
|
|
429
|
+
pinching: (e: Event) => {
|
|
430
|
+
if (self.device === 'touch') touchMove(e as TouchEvent)
|
|
431
|
+
else if (self.device === 'pointer') pointerMove(e as PointerEvent)
|
|
432
|
+
else if (self.device === 'gesture') gestureChange(e)
|
|
433
|
+
else if (self.device === 'wheel') wheeling(e as WheelEvent)
|
|
434
|
+
},
|
|
435
|
+
pinchEnd: (e: Event) => {
|
|
436
|
+
if (self.device === 'touch') touchEnd(e as TouchEvent)
|
|
437
|
+
else if (self.device === 'pointer') pointerEnd(e as PointerEvent)
|
|
438
|
+
else if (self.device === 'gesture') gestureEnd(e)
|
|
439
|
+
else if (self.device === 'wheel') wheelEnd(e)
|
|
440
|
+
},
|
|
441
|
+
mount,
|
|
442
|
+
clean,
|
|
443
|
+
ref,
|
|
444
|
+
}) as EventState<PinchState<El>>
|
|
445
|
+
|
|
446
|
+
return self
|
|
447
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useOnce, useMutable } from 'reev/src/react'
|
|
2
|
+
import { pinchEvent } from '.'
|
|
3
|
+
import { PinchArg, PinchState } from './types'
|
|
4
|
+
import { isF } from '../utils'
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
|
|
7
|
+
export const usePinch = <El extends Element = Element>(arg?: PinchArg) => {
|
|
8
|
+
if (isF(arg)) arg = { pinch: arg }
|
|
9
|
+
const memo = useMutable(arg)
|
|
10
|
+
return useOnce(() => pinchEvent<El>(memo as any))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default usePinch
|
|
14
|
+
|
|
15
|
+
export interface PinchProps<El extends Element = Element> extends Partial<PinchState<El>> {
|
|
16
|
+
children: (state: PinchState<El>) => ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Pinch = (props: PinchProps) => {
|
|
20
|
+
const { children, ...other } = props
|
|
21
|
+
return children(usePinch(other))
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Vec2 } from '../utils'
|
|
2
|
+
|
|
3
|
+
export type PinchDevice = 'mouse' | 'pointer' | 'touch' | 'wheel' | 'gesture'
|
|
4
|
+
|
|
5
|
+
export interface PinchState<El extends Element = Element> {
|
|
6
|
+
device: PinchDevice
|
|
7
|
+
|
|
8
|
+
// Lifecycle flags
|
|
9
|
+
active: boolean
|
|
10
|
+
_active: boolean
|
|
11
|
+
|
|
12
|
+
// Pinch values: [distance, angle] for touch/pointer, [scale, rotation] for gesture
|
|
13
|
+
value: Vec2
|
|
14
|
+
_value: Vec2
|
|
15
|
+
delta: Vec2
|
|
16
|
+
offset: Vec2 // [cumulativeScale, cumulativeAngle]
|
|
17
|
+
movement: Vec2 // [scaleChange, angleChange] since start
|
|
18
|
+
|
|
19
|
+
// Initial values for computing relative changes
|
|
20
|
+
initial: Vec2
|
|
21
|
+
|
|
22
|
+
// Origin point (center between two fingers)
|
|
23
|
+
origin: Vec2
|
|
24
|
+
|
|
25
|
+
// Touch tracking
|
|
26
|
+
_touchIds: number[]
|
|
27
|
+
|
|
28
|
+
// Pointer tracking
|
|
29
|
+
_pointerEvents: Map<number, PointerEvent>
|
|
30
|
+
|
|
31
|
+
// Scale (derived from distance change)
|
|
32
|
+
scale: number
|
|
33
|
+
|
|
34
|
+
// Rotation turns (for tracking full rotations)
|
|
35
|
+
turns: number
|
|
36
|
+
|
|
37
|
+
// Element and event
|
|
38
|
+
target: El
|
|
39
|
+
event: Event | null
|
|
40
|
+
memo: Record<string, unknown>
|
|
41
|
+
|
|
42
|
+
// Timeout for wheel end detection
|
|
43
|
+
timeout: number
|
|
44
|
+
clearTimeout: () => void
|
|
45
|
+
|
|
46
|
+
// State flags
|
|
47
|
+
isPinchStart: boolean
|
|
48
|
+
isPinching: boolean
|
|
49
|
+
isPinchEnd: boolean
|
|
50
|
+
|
|
51
|
+
// Callbacks
|
|
52
|
+
pinch: (self: PinchState<El>) => void
|
|
53
|
+
pinchStart: (e: Event) => void
|
|
54
|
+
pinching: (e: Event) => void
|
|
55
|
+
pinchEnd: (e: Event) => void
|
|
56
|
+
|
|
57
|
+
// Lifecycle methods
|
|
58
|
+
mount: (target: El) => void
|
|
59
|
+
clean: () => void
|
|
60
|
+
ref: (el: El | null) => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type PinchArg<El extends Element = Element> = Partial<PinchState<El>> | PinchState<El>['pinch']
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SUPPORT, Vec2, vec2 } from '../utils'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ref:
|
|
5
|
+
* https://github.com/pmndrs/use-gesture/blob/main/packages/core/src/config/pinchConfigResolver.ts
|
|
6
|
+
*/
|
|
7
|
+
export const pinchDevice = (touch = false) => {
|
|
8
|
+
if (!SUPPORT.touch && SUPPORT.gesture) return 'gesture'
|
|
9
|
+
if (SUPPORT.touch && touch) return 'touch'
|
|
10
|
+
if (SUPPORT.touchscreen) {
|
|
11
|
+
if (SUPPORT.pointer) return 'pointer'
|
|
12
|
+
if (SUPPORT.touch) return 'touch'
|
|
13
|
+
}
|
|
14
|
+
return 'wheel'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wheel delta normalization constants
|
|
19
|
+
* ref: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
|
|
20
|
+
*/
|
|
21
|
+
const LINE_HEIGHT = 40
|
|
22
|
+
const PAGE_HEIGHT = 800
|
|
23
|
+
const PINCH_WHEEL_RATIO = 100
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate distance and angle between two touch/pointer points
|
|
27
|
+
* ref: https://github.com/pmndrs/use-gesture/blob/main/packages/core/src/utils/events.ts
|
|
28
|
+
*/
|
|
29
|
+
export interface DistanceAngle {
|
|
30
|
+
distance: number
|
|
31
|
+
angle: number
|
|
32
|
+
origin: Vec2
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const distanceAngle = (p1: Touch | PointerEvent, p2: Touch | PointerEvent): DistanceAngle | null => {
|
|
36
|
+
try {
|
|
37
|
+
const dx = p2.clientX - p1.clientX
|
|
38
|
+
const dy = p2.clientY - p1.clientY
|
|
39
|
+
const cx = (p2.clientX + p1.clientX) / 2
|
|
40
|
+
const cy = (p2.clientY + p1.clientY) / 2
|
|
41
|
+
|
|
42
|
+
const distance = Math.hypot(dx, dy)
|
|
43
|
+
// Convert to degrees, negative to match natural gesture direction
|
|
44
|
+
const angle = -(Math.atan2(dx, dy) * 180) / Math.PI
|
|
45
|
+
const origin = vec2(cx, cy)
|
|
46
|
+
|
|
47
|
+
return { distance, angle, origin }
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get touch distance and angle from touch event using stored touch ids
|
|
55
|
+
*/
|
|
56
|
+
export const touchDistanceAngle = (event: TouchEvent, ids: number[]): DistanceAngle | null => {
|
|
57
|
+
const touches = Array.from(event.touches).filter((touch) => ids.includes(touch.identifier))
|
|
58
|
+
if (touches.length < 2) return null
|
|
59
|
+
return distanceAngle(touches[0], touches[1])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get pointer distance and angle from pointer events map
|
|
64
|
+
*/
|
|
65
|
+
export const pointerDistanceAngle = (pointerEvents: Map<number, PointerEvent>): DistanceAngle | null => {
|
|
66
|
+
const pointers = Array.from(pointerEvents.values())
|
|
67
|
+
if (pointers.length < 2) return null
|
|
68
|
+
return distanceAngle(pointers[0], pointers[1])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all touch identifiers for the current target
|
|
73
|
+
*/
|
|
74
|
+
export const getCurrentTargetTouchIds = (event: TouchEvent): number[] => {
|
|
75
|
+
return Array.from(event.touches)
|
|
76
|
+
.filter((touch) => touch.target === event.currentTarget || (event.currentTarget as Node)?.contains?.(touch.target as Node))
|
|
77
|
+
.map((touch) => touch.identifier)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalize wheel values for pinch gesture (trackpad pinch)
|
|
82
|
+
* Returns scale delta based on wheel deltaY
|
|
83
|
+
*/
|
|
84
|
+
export const wheelPinchDelta = (event: WheelEvent, currentScale: number): number => {
|
|
85
|
+
let { deltaY, deltaMode } = event
|
|
86
|
+
|
|
87
|
+
// Normalize wheel values
|
|
88
|
+
if (deltaMode === 1) {
|
|
89
|
+
deltaY *= LINE_HEIGHT
|
|
90
|
+
} else if (deltaMode === 2) {
|
|
91
|
+
deltaY *= PAGE_HEIGHT
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Convert to scale delta (negative because wheel down = zoom out)
|
|
95
|
+
return (-deltaY / PINCH_WHEEL_RATIO) * currentScale
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* WebKit gesture event type (Safari)
|
|
100
|
+
*/
|
|
101
|
+
export interface WebKitGestureEvent extends UIEvent {
|
|
102
|
+
scale: number
|
|
103
|
+
rotation: number
|
|
104
|
+
clientX: number
|
|
105
|
+
clientY: number
|
|
106
|
+
}
|
package/src/react.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventState, event } from 'reev/src'
|
|
2
|
+
import { ResizeState } from './types'
|
|
3
|
+
|
|
4
|
+
export * from './types'
|
|
5
|
+
|
|
6
|
+
const DELAY = 100
|
|
7
|
+
|
|
8
|
+
type ResizeEventCallback = (entry: ResizeObserverEntry) => () => void
|
|
9
|
+
|
|
10
|
+
export const resizeEvent = <El extends Element = Element>(state: ResizeState) => {
|
|
11
|
+
const on: ResizeEventCallback = (entry) => () => {
|
|
12
|
+
// ???
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const mount = (target: El) => {
|
|
16
|
+
const register = (entry: ResizeObserverEntry) => {
|
|
17
|
+
if (entry.target !== target) return
|
|
18
|
+
const id = setTimeout(on(entry), DELAY)
|
|
19
|
+
self.listener()
|
|
20
|
+
self.listener = () => clearTimeout(id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
self.observer = new ResizeObserver((entries) => {
|
|
24
|
+
entries.forEach(register)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
self.observer.observe(target)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const clean = () => {}
|
|
31
|
+
|
|
32
|
+
const ref = (el: El) => {
|
|
33
|
+
self(state)
|
|
34
|
+
if (el) {
|
|
35
|
+
self.mount(el)
|
|
36
|
+
} else self.clean(null)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const self = event({
|
|
40
|
+
observer: null,
|
|
41
|
+
listener: () => {},
|
|
42
|
+
resize: () => {},
|
|
43
|
+
mount,
|
|
44
|
+
clean,
|
|
45
|
+
ref,
|
|
46
|
+
}) as EventState<ResizeState<El>>
|
|
47
|
+
|
|
48
|
+
return self
|
|
49
|
+
}
|