@octane-xplat/motion 0.9.0 → 0.10.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/src/engine.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { keyframes, spring } from 'motion-dom'
2
2
  import type { Clock } from './clock-types.js'
3
- import type { Transition } from './types.js'
3
+ import type {
4
+ Transition,
5
+ TransitionInput,
6
+ TransitionOrchestration,
7
+ MotionKey,
8
+ RepeatType,
9
+ } from './types.js'
4
10
 
5
11
  /** Terminal status; cancellation never masquerades as completion. */
6
12
  export type AnimationResult = 'finished' | 'cancelled' | 'replaced'
@@ -10,16 +16,55 @@ export interface AnimationControls {
10
16
  stop(reason?: Exclude<AnimationResult, 'finished'>): void
11
17
  }
12
18
 
19
+ const MOTION_KEYS: MotionKey[] = ['opacity', 'x', 'y', 'scale', 'scaleX', 'scaleY', 'rotate']
20
+
21
+ /** Platform animators take one timing curve per run — cubic-bezier only. */
22
+ const BEZIER_EASES = new Set(['linear', 'easeIn', 'easeOut', 'easeInOut'])
23
+ export function isBezierEase(ease: Transition['ease']): boolean {
24
+ return ease === undefined || Array.isArray(ease) || BEZIER_EASES.has(ease)
25
+ }
26
+
27
+ const BEZIER_POINTS: Record<string, [number, number, number, number]> = {
28
+ linear: [0, 0, 1, 1],
29
+ easeIn: [0.42, 0, 1, 1],
30
+ easeOut: [0, 0, 0.58, 1],
31
+ easeInOut: [0.42, 0, 0.58, 1],
32
+ }
33
+
34
+ /** CSS cubic-bezier control points for a delegatable ease; tuples pass through. */
35
+ export function bezierPoints(ease: Transition['ease']): [number, number, number, number] {
36
+ return Array.isArray(ease) ? ease : BEZIER_POINTS[ease ?? 'easeInOut']
37
+ }
38
+
39
+ export function isOrchestrated(t: TransitionInput | undefined): t is TransitionOrchestration {
40
+ if (!t || typeof t !== 'object') {
41
+ return false
42
+ }
43
+
44
+ return 'default' in t || MOTION_KEYS.some((key) => key in t)
45
+ }
46
+
47
+ export function resolveTransition(t: TransitionInput, key: MotionKey): Transition {
48
+ return isOrchestrated(t) ? { ...t.default, ...t[key] } : t
49
+ }
50
+
13
51
  export function validateTransition(t: Transition): void {
14
- if (t.type === 'spring' && (t.duration !== undefined || t.ease !== undefined)) {
15
- throw new Error('motion: physical springs do not accept duration or ease')
52
+ if (t.type === 'spring' && t.ease !== undefined) {
53
+ throw new Error('motion: physical springs do not accept ease')
16
54
  }
17
55
 
18
56
  const allowed = [
57
+ 'staggerChildren',
58
+ 'delayChildren',
59
+ 'when',
19
60
  'type',
20
61
  'duration',
21
62
  'delay',
22
63
  'ease',
64
+ 'repeat',
65
+ 'repeatType',
66
+ 'repeatDelay',
67
+ 'bounce',
23
68
  'stiffness',
24
69
  'damping',
25
70
  'mass',
@@ -34,13 +79,21 @@ export function validateTransition(t: Transition): void {
34
79
  }
35
80
  }
36
81
 
82
+ if (t.when !== undefined && !['beforeChildren', 'afterChildren'].includes(t.when)) {
83
+ throw new Error('motion: unsupported when')
84
+ }
85
+
37
86
  if (t.type !== undefined && t.type !== 'tween' && t.type !== 'spring') {
38
87
  throw new Error('motion: unsupported transition type')
39
88
  }
40
89
 
41
90
  for (const key of [
91
+ 'staggerChildren',
92
+ 'delayChildren',
42
93
  'duration',
43
94
  'delay',
95
+ 'repeatDelay',
96
+ 'bounce',
44
97
  'stiffness',
45
98
  'damping',
46
99
  'mass',
@@ -54,6 +107,17 @@ export function validateTransition(t: Transition): void {
54
107
  }
55
108
  }
56
109
 
110
+ if (
111
+ t.repeat !== undefined &&
112
+ (t.repeat < 0 || (!Number.isInteger(t.repeat) && t.repeat !== Infinity))
113
+ ) {
114
+ throw new Error('motion: repeat must be a non-negative integer or Infinity')
115
+ }
116
+
117
+ if (t.repeatType !== undefined && !['loop', 'reverse', 'mirror'].includes(t.repeatType)) {
118
+ throw new Error('motion: unsupported repeatType')
119
+ }
120
+
57
121
  for (const key of ['mass', 'stiffness', 'damping', 'restSpeed', 'restDelta'] as const) {
58
122
  if (t[key] === 0) {
59
123
  throw new Error(`motion: ${key} must be positive`)
@@ -72,12 +136,44 @@ export function validateTransition(t: Transition): void {
72
136
  ) {
73
137
  throw new Error('motion: invalid cubic Bezier')
74
138
  }
75
- } else if (!['linear', 'easeIn', 'easeOut', 'easeInOut'].includes(t.ease)) {
139
+ } else if (
140
+ ![
141
+ 'linear',
142
+ 'easeIn',
143
+ 'easeOut',
144
+ 'easeInOut',
145
+ 'circIn',
146
+ 'circOut',
147
+ 'circInOut',
148
+ 'backIn',
149
+ 'backOut',
150
+ 'backInOut',
151
+ 'anticipate',
152
+ ].includes(t.ease)
153
+ ) {
76
154
  throw new Error('motion: unsupported easing')
77
155
  }
78
156
  }
79
157
  }
80
158
 
159
+ export function validateTransitionInput(t: TransitionInput): void {
160
+ if (isOrchestrated(t)) {
161
+ const { staggerChildren, delayChildren, when, ...channels } = t
162
+ validateTransition({ staggerChildren, delayChildren, when })
163
+ for (const [key, value] of Object.entries(channels)) {
164
+ if (key !== 'default' && !MOTION_KEYS.includes(key as MotionKey)) {
165
+ throw new Error(`motion: unsupported transition ${key}`)
166
+ }
167
+
168
+ if (value) {
169
+ validateTransition(value as Transition)
170
+ }
171
+ }
172
+ } else {
173
+ validateTransition(t)
174
+ }
175
+ }
176
+
81
177
  // Pure upstream generators share trajectories across platforms; host scheduling
82
178
  // stays outside Motion's browser-global frame loop.
83
179
  export function runAnimation(
@@ -110,37 +206,63 @@ export function runAnimation(
110
206
  }
111
207
 
112
208
  const duration = transition.duration ?? 0.3
113
- const generator =
209
+ const repeat = transition.repeat ?? 0
210
+ const repeatType: RepeatType = transition.repeatType ?? 'loop'
211
+ const repeatDelay = (transition.repeatDelay ?? 0) * 1000
212
+
213
+ const makeGenerator = (legFrom: number, legTo: number) =>
114
214
  transition.type === 'spring'
115
215
  ? spring({
116
- keyframes: [from, to],
216
+ keyframes: [legFrom, legTo],
117
217
  stiffness: transition.stiffness ?? 100,
118
218
  damping: transition.damping ?? 10,
119
219
  mass: transition.mass ?? 1,
120
220
  velocity: transition.velocity ?? 0,
121
221
  restSpeed: transition.restSpeed,
122
222
  restDelta: transition.restDelta,
223
+ // Visual-duration spring spec — duration/bounce instead of
224
+ // stiffness/damping; motion-dom takes milliseconds.
225
+ duration: transition.duration !== undefined ? duration * 1000 : undefined,
226
+ bounce: transition.bounce,
123
227
  })
124
228
  : keyframes({
125
- keyframes: [from, to],
229
+ keyframes: [legFrom, legTo],
126
230
  duration: duration * 1000,
127
231
  ease: transition.ease ?? 'easeInOut',
128
232
  })
129
233
 
130
- const start = clock.now() + (transition.delay ?? 0) * 1000
234
+ let leg = 0
235
+ let generator = makeGenerator(from, to)
236
+ let legStart = clock.now() + (transition.delay ?? 0) * 1000
131
237
  const tick = () => {
132
238
  if (settled) {
133
239
  return
134
240
  }
135
241
 
136
- const elapsed = Math.max(0, clock.now() - start)
137
- if (clock.now() >= start) {
138
- const state = generator.next(elapsed)
139
- update(state.done ? to : state.value)
140
- if (state.done) {
141
- settle('finished')
242
+ const now = clock.now()
243
+ if (now < legStart) {
244
+ frame = clock.request(tick)
245
+ return
246
+ }
247
+
248
+ const elapsed = Math.max(0, now - legStart)
249
+ const state = generator.next(elapsed)
250
+ const legEnd = repeatType === 'loop' || leg % 2 === 0 ? to : from
251
+ update(state.done ? legEnd : state.value)
252
+ if (state.done) {
253
+ if (leg < repeat) {
254
+ leg++
255
+ // 'loop' replays the same direction; 'reverse'/'mirror' alternate
256
+ // legs (identical for two keyframes, which is all we generate).
257
+ const [legFrom, legTo] = repeatType === 'loop' || leg % 2 === 0 ? [from, to] : [to, from]
258
+ generator = makeGenerator(legFrom, legTo)
259
+ legStart = now + repeatDelay
260
+ frame = clock.request(tick)
142
261
  return
143
262
  }
263
+
264
+ settle('finished')
265
+ return
144
266
  }
145
267
 
146
268
  if (!settled) {
@@ -148,7 +270,7 @@ export function runAnimation(
148
270
  }
149
271
  }
150
272
 
151
- if (transition.type !== 'spring' && duration === 0 && !transition.delay) {
273
+ if (transition.type !== 'spring' && duration === 0 && !transition.delay && repeat === 0) {
152
274
  update(to)
153
275
  settle('finished')
154
276
  } else {
package/src/hooks.tsrx CHANGED
@@ -1,8 +1,11 @@
1
- import { useRef, useLayoutEffect } from 'octane'
1
+ import { useRef, useLayoutEffect, useContext } from 'octane'
2
2
  import { interpolate } from 'motion-dom'
3
3
  import { clock } from './clock'
4
+ import { ConfigContext, useReducedMotion } from './config.tsrx'
5
+ import { controllers } from './components.tsrx'
4
6
  import { MotionValue, type MotionValueEvents } from './value'
5
- import type { Transition } from './types'
7
+ import type { AnimationControls, AnimationResult } from './engine'
8
+ import type { Target, Transition, TransitionInput } from './types'
6
9
 
7
10
  /** Create an owned numeric value that writes to bound hosts without rendering. */
8
11
  export function useMotionValue(initial: number): MotionValue {
@@ -54,3 +57,51 @@ export function useSpring(source: number | MotionValue, options: Omit<Transition
54
57
 
55
58
  return value
56
59
  }
60
+
61
+ /** Scoped imperative animation: `const [scope, animate] = useAnimate()` then
62
+ * `ref={scope}` on a motion host. `animate(target, transition)` drives that
63
+ * host; `animate(value, to, transition)` drives a MotionValue directly. */
64
+ export function useAnimate(): [
65
+ { current: any },
66
+ (
67
+ target: Target | MotionValue | object,
68
+ transitionOrTo?: TransitionInput | number,
69
+ transition?: Transition,
70
+ ) => Promise<AnimationResult> | AnimationControls,
71
+ ] {
72
+ const scope = useRef<any>(null)
73
+ const config = useContext(ConfigContext)
74
+ const systemReduced = useReducedMotion()
75
+ const reducedRef = useRef(config.reducedMotion === 'always' || (config.reducedMotion === 'user' && systemReduced))
76
+ reducedRef.current = config.reducedMotion === 'always' || (config.reducedMotion === 'user' && systemReduced)
77
+ const animate = useRef(
78
+ (
79
+ target: any,
80
+ transitionOrTo?: any,
81
+ transition?: any,
82
+ ): Promise<AnimationResult> | AnimationControls => {
83
+ if (target instanceof MotionValue) {
84
+ return target.animate(transitionOrTo, transition)
85
+ }
86
+
87
+ // A registered host node targets that element's controller; a plain
88
+ // channel target drives the scope's host.
89
+ const controller =
90
+ target && typeof target === 'object' && controllers.has(target)
91
+ ? controllers.get(target)!
92
+ : scope.current
93
+ ? controllers.get(scope.current)
94
+ : undefined
95
+
96
+ if (!controller) {
97
+ return Promise.resolve('cancelled' as const)
98
+ }
99
+
100
+ const destination = controllers.has(target) ? (transitionOrTo ?? {}) : target
101
+ const spec = controllers.has(target) ? (transition ?? {}) : (transitionOrTo ?? {})
102
+ return controller.animate(destination, spec, reducedRef.current)
103
+ },
104
+ ).current
105
+
106
+ return [scope, animate]
107
+ }
package/src/host-types.ts CHANGED
@@ -1,6 +1,49 @@
1
- import type { Target } from './types'
1
+ import type { Target, Transition, DragInfo } from './types'
2
+
3
+ export interface DelegatedRequest {
4
+ /** Channels the caller asked to animate. */
5
+ target: Target
6
+ /** Full channel-space destination: retained values plus the target. */
7
+ dest: Target
8
+ /** Effective native destination — `scale` folded into scaleX/scaleY. */
9
+ eff: Target
10
+ transition: Transition
11
+ }
12
+
13
+ /** A run handed to the platform animator; presentation state stays readable. */
14
+ export interface DelegatedRun {
15
+ /** Reads current rendered values in effective channels (scale folded). */
16
+ sample(): Target
17
+ /** Stops the run at its presentation state; the model is synced to it. */
18
+ cancel(): void
19
+ finished: Promise<'finished' | 'cancelled'>
20
+ }
21
+
22
+ /** Gesture-target kinds: press covers whileTap, focus covers whileFocus. */
23
+ export type GestureKind = 'press' | 'focus'
24
+
25
+ export interface GestureCallbacks {
26
+ start(): void
27
+ end(): void
28
+ }
29
+
30
+ export interface PanCallbacks {
31
+ start(event: any, info: DragInfo): void
32
+ move(event: any, info: DragInfo): void
33
+ end(event: any, info: DragInfo): void
34
+ }
35
+
2
36
  export interface HostAdapter {
37
+ pan?(axis: true | 'x' | 'y', callbacks: PanCallbacks): () => void
3
38
  read(): Target
4
39
  write(values: Target): void
5
40
  restore(): void
41
+ /**
42
+ * Run a declarative tween on the platform animator. Returns null to fall
43
+ * back to the JS engine.
44
+ */
45
+ delegate?(request: DelegatedRequest): DelegatedRun | null
46
+ /** Subscribe to a gesture lifecycle; returns an unsubscribe, or null if the
47
+ * host cannot emit it (focus events only exist on focusable hosts). */
48
+ gesture?(kind: GestureKind, callbacks: GestureCallbacks): (() => void) | null
6
49
  }
package/src/host.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { HostAdapter } from './host-types'
1
+ import type { DelegatedRequest, DelegatedRun, HostAdapter } from './host-types'
2
+ import { attachPan } from './pan'
3
+ import { delegatedRun } from './driver'
2
4
  // The retained NativeScript View exposes direct transform channels in DIP.
3
5
  export function attachHost(node: any): HostAdapter {
4
6
  const original = {
@@ -46,5 +48,40 @@ export function attachHost(node: any): HostAdapter {
46
48
  }
47
49
  }
48
50
 
49
- return { read: () => ({ ...original, scale: 1 }), write, restore: () => write(original) }
51
+ return {
52
+ pan: (axis, callbacks) => attachPan(node, axis, callbacks),
53
+ read: () => ({ ...original, scale: 1 }),
54
+ write,
55
+ restore: () => write(original),
56
+ delegate: (req: DelegatedRequest): DelegatedRun | null => delegatedRun(node, req, write),
57
+ gesture(kind, callbacks) {
58
+ if (typeof node.on !== 'function' || typeof node.off !== 'function') {
59
+ return null
60
+ }
61
+
62
+ if (kind === 'press') {
63
+ // NativeScript's `touch` event: action down/up/cancel.
64
+ const onTouch = (event: any) => {
65
+ if (event.action === 'down') {
66
+ callbacks.start()
67
+ } else if (event.action === 'up' || event.action === 'cancel') {
68
+ callbacks.end()
69
+ }
70
+ }
71
+
72
+ node.on('touch', onTouch)
73
+ return () => node.off('touch', onTouch)
74
+ }
75
+
76
+ // focus/blur exist only on focusable hosts (editable text, buttons).
77
+ const onFocus = () => callbacks.start()
78
+ const onBlur = () => callbacks.end()
79
+ node.on('focus', onFocus)
80
+ node.on('blur', onBlur)
81
+ return () => {
82
+ node.off('focus', onFocus)
83
+ node.off('blur', onBlur)
84
+ }
85
+ },
86
+ }
50
87
  }
package/src/host.web.ts CHANGED
@@ -1,4 +1,7 @@
1
- import type { HostAdapter } from './host-types'
1
+ import { attachPan } from './pan.web'
2
+ import { delegatedRun } from './driver.web'
3
+ import type { DelegatedRequest, DelegatedRun, HostAdapter } from './host-types'
4
+ import type { Target } from './types'
2
5
  export function attachHost(node: HTMLElement): HostAdapter {
3
6
  const transform = node.style.transform
4
7
  const opacity = node.style.opacity
@@ -7,7 +10,18 @@ export function attachHost(node: HTMLElement): HostAdapter {
7
10
  throw new Error('motion: put existing CSS transforms on an outer container')
8
11
  }
9
12
 
13
+ const write = (values: Target) => {
14
+ if (values.opacity !== undefined) {
15
+ node.style.opacity = String(Math.min(1, Math.max(0, values.opacity)))
16
+ }
17
+
18
+ if (Object.keys(values).some((key) => key !== 'opacity')) {
19
+ node.style.transform = `translateX(${values.x ?? 0}px) translateY(${values.y ?? 0}px) scale(${values.scale ?? 1}) scaleX(${values.scaleX ?? 1}) scaleY(${values.scaleY ?? 1}) rotate(${values.rotate ?? 0}deg)`
20
+ }
21
+ }
22
+
10
23
  return {
24
+ pan: (axis, callbacks) => attachPan(node, axis, callbacks),
11
25
  read: () => ({
12
26
  opacity: Number(computed.opacity || 1),
13
27
  x: 0,
@@ -17,18 +31,45 @@ export function attachHost(node: HTMLElement): HostAdapter {
17
31
  scaleY: 1,
18
32
  rotate: 0,
19
33
  }),
20
- write(values) {
21
- if (values.opacity !== undefined) {
22
- node.style.opacity = String(Math.min(1, Math.max(0, values.opacity)))
23
- }
24
-
25
- if (Object.keys(values).some((key) => key !== 'opacity')) {
26
- node.style.transform = `translateX(${values.x ?? 0}px) translateY(${values.y ?? 0}px) scale(${values.scale ?? 1}) scaleX(${values.scaleX ?? 1}) scaleY(${values.scaleY ?? 1}) rotate(${values.rotate ?? 0}deg)`
27
- }
28
- },
34
+ write,
35
+ delegate: (req: DelegatedRequest): DelegatedRun | null => delegatedRun(node, req, write),
29
36
  restore() {
30
37
  node.style.transform = transform
31
38
  node.style.opacity = opacity
32
39
  },
40
+ gesture(kind, callbacks) {
41
+ if (kind === 'press') {
42
+ const down = (event: PointerEvent) => {
43
+ try {
44
+ // Preserve descendant click targets when this host wraps controls.
45
+ ;(event.target as Element | null)?.setPointerCapture?.(event.pointerId)
46
+ } catch (error) {
47
+ // Synthetic pointer dispatch has no active browser pointer.
48
+ if (event.isTrusted || (error as Error).name !== 'NotFoundError') {
49
+ throw error
50
+ }
51
+ }
52
+
53
+ callbacks.start()
54
+ }
55
+
56
+ const end = () => callbacks.end()
57
+ node.addEventListener('pointerdown', down)
58
+ node.addEventListener('pointerup', end)
59
+ node.addEventListener('pointercancel', end)
60
+ return () => {
61
+ node.removeEventListener('pointerdown', down)
62
+ node.removeEventListener('pointerup', end)
63
+ node.removeEventListener('pointercancel', end)
64
+ }
65
+ }
66
+
67
+ node.addEventListener('focus', callbacks.start)
68
+ node.addEventListener('blur', callbacks.end)
69
+ return () => {
70
+ node.removeEventListener('focus', callbacks.start)
71
+ node.removeEventListener('blur', callbacks.end)
72
+ }
73
+ },
33
74
  }
34
75
  }
package/src/index.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { MotionView, MotionRow, MotionPressable } from './components.tsrx'
2
- /** Bounded motion hosts for shared xplat UI primitives. */
3
- export const motion = { View: MotionView, Row: MotionRow, Pressable: MotionPressable }
1
+ import { MotionView, MotionRow, MotionPressable, createMotion } from './components.tsrx'
2
+ /** Bounded motion hosts for shared xplat UI primitives; `motion.create` wraps
3
+ * any component that accepts `ref`/`style`/`children`. */
4
+ export const motion = {
5
+ View: MotionView,
6
+ Row: MotionRow,
7
+ Pressable: MotionPressable,
8
+ create: createMotion,
9
+ }
10
+
4
11
  export { MotionConfig, useReducedMotion } from './config.tsrx'
5
- export { useMotionValue, useTransform, useSpring, useMotionValueEvent } from './hooks.tsrx'
12
+ export {
13
+ useMotionValue,
14
+ useTransform,
15
+ useSpring,
16
+ useMotionValueEvent,
17
+ useAnimate,
18
+ } from './hooks.tsrx'
19
+
6
20
  export type { MotionValue, MotionValueEvents } from './value'
7
21
  export type { AnimationControls, AnimationResult } from './engine'
8
22
  export type * from './types'
package/src/index.web.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { MotionView, MotionRow, MotionPressable } from './components.tsrx'
2
- /** Bounded motion hosts for shared xplat UI primitives. */
3
- export const motion = { View: MotionView, Row: MotionRow, Pressable: MotionPressable }
1
+ import { MotionView, MotionRow, MotionPressable, createMotion } from './components.tsrx'
2
+ /** Bounded motion hosts for shared xplat UI primitives; `motion.create` wraps
3
+ * any component that accepts `ref`/`style`/`children`. */
4
+ export const motion = {
5
+ View: MotionView,
6
+ Row: MotionRow,
7
+ Pressable: MotionPressable,
8
+ create: createMotion,
9
+ }
10
+
4
11
  export { MotionConfig, useReducedMotion } from './config.tsrx'
5
- export { useMotionValue, useTransform, useSpring, useMotionValueEvent } from './hooks.tsrx'
12
+ export {
13
+ useMotionValue,
14
+ useTransform,
15
+ useSpring,
16
+ useMotionValueEvent,
17
+ useAnimate,
18
+ } from './hooks.tsrx'
19
+
6
20
  export type { MotionValue, MotionValueEvents } from './value'
7
21
  export type { AnimationControls, AnimationResult } from './engine'
8
22
  export type * from './types'
package/src/pan.ts ADDED
@@ -0,0 +1,92 @@
1
+ import {
2
+ Manager,
3
+ HandlerType,
4
+ GestureState,
5
+ GestureHandlerStateEvent,
6
+ GestureHandlerTouchEvent,
7
+ } from '@nativescript-community/gesturehandler'
8
+
9
+ import { Utils } from '@nativescript/core'
10
+ import type { PanCallbacks } from './host-types'
11
+ import type { DragInfo } from './types'
12
+
13
+ // Separate tag range from the plugin's built-in gesture observers. Manager
14
+ // shares the registry; applications should reserve this range for motion.
15
+ let tag = 700000000
16
+
17
+ export function attachPan(node: any, axis: true | 'x' | 'y', callbacks: PanCallbacks) {
18
+ // 2.0.45 config setters take raw Android pixels (their converter is
19
+ // getter-only); payloads already convert back to DIP.
20
+ const androidHost = Boolean((globalThis as any).android)
21
+ const slop = androidHost ? Utils.layout.toDevicePixels(8) : 8
22
+ const handler = Manager.getInstance().createGestureHandler(HandlerType.PAN, tag++, {
23
+ shouldCancelWhenOutside: false,
24
+ // Android retains its radial touch-slop default alongside activeOffset.
25
+ // Disable that alternative trigger for single-axis arbitration.
26
+ ...(androidHost && axis !== true ? { minDist: 1e9 } : {}),
27
+ ...(axis === true
28
+ ? { minDist: slop }
29
+ : axis === 'x'
30
+ ? {
31
+ activeOffsetXStart: -slop,
32
+ activeOffsetXEnd: slop,
33
+ failOffsetYStart: -slop,
34
+ failOffsetYEnd: slop,
35
+ }
36
+ : {
37
+ activeOffsetYStart: -slop,
38
+ activeOffsetYEnd: slop,
39
+ failOffsetXStart: -slop,
40
+ failOffsetXEnd: slop,
41
+ }),
42
+ })
43
+
44
+ let active = false
45
+ let last = { x: 0, y: 0 }
46
+ const sample = (args: any, cancelled = false): DragInfo => {
47
+ const extra = args.data.extraData ?? {}
48
+ const offset = { x: extra.translationX ?? last.x, y: extra.translationY ?? last.y }
49
+ const delta = { x: offset.x - last.x, y: offset.y - last.y }
50
+ last = offset
51
+ return {
52
+ point: { x: extra.absoluteX ?? extra.x ?? 0, y: extra.absoluteY ?? extra.y ?? 0 },
53
+ delta,
54
+ offset,
55
+ velocity: { x: extra.velocityX ?? 0, y: extra.velocityY ?? 0 },
56
+ cancelled,
57
+ }
58
+ }
59
+
60
+ const state = (args: any) => {
61
+ const next = args.data.state
62
+ if (next === GestureState.ACTIVE && !active) {
63
+ last = { x: 0, y: 0 }
64
+ active = true
65
+ const info = sample(args)
66
+ callbacks.start(args, info)
67
+ callbacks.move(args, info)
68
+ } else if (
69
+ active &&
70
+ [GestureState.END, GestureState.CANCELLED, GestureState.FAILED].includes(next)
71
+ ) {
72
+ active = false
73
+ callbacks.end(args, sample(args, next !== GestureState.END))
74
+ }
75
+ }
76
+
77
+ const touch = (args: any) => {
78
+ if (active && args.data.state === GestureState.ACTIVE) {
79
+ callbacks.move(args, sample(args))
80
+ }
81
+ }
82
+
83
+ handler.on(GestureHandlerStateEvent, state)
84
+ handler.on(GestureHandlerTouchEvent, touch)
85
+ handler.attachToView(node)
86
+ return () => {
87
+ active = false
88
+ handler.off(GestureHandlerStateEvent, state)
89
+ handler.off(GestureHandlerTouchEvent, touch)
90
+ handler.detachFromView(node)
91
+ }
92
+ }