@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/README.md +31 -2
- package/UPSTREAM.md +284 -16
- package/dist/native/index.js +980 -112
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/index.js +1104 -166
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +8 -3
- package/src/clock.ts +13 -1
- package/src/components.mobile.test.tsrx +58 -0
- package/src/components.tsrx +185 -39
- package/src/components.web.test.tsrx +234 -1
- package/src/controller.ts +122 -10
- package/src/delegation.test.ts +269 -0
- package/src/delegation.ts +29 -0
- package/src/delegation.web.test.ts +270 -0
- package/src/drag.mobile.test.ts +81 -0
- package/src/drag.test.ts +167 -0
- package/src/drag.ts +157 -0
- package/src/driver.ts +249 -0
- package/src/driver.web.ts +180 -0
- package/src/engine.test.ts +86 -0
- package/src/engine.ts +137 -15
- package/src/hooks.tsrx +53 -2
- package/src/host-types.ts +44 -1
- package/src/host.ts +39 -2
- package/src/host.web.ts +51 -10
- package/src/index.ts +18 -4
- package/src/index.web.ts +18 -4
- package/src/pan.ts +92 -0
- package/src/pan.web.ts +116 -0
- package/src/presence.tsrx +6 -7
- package/src/reduced-motion.ts +23 -4
- package/src/types.ts +99 -10
- package/src/value.ts +15 -5
- package/src/variants.test.ts +197 -0
- package/src/variants.ts +226 -0
- package/types/engine.d.ts +7 -1
- package/types/index.d.ts +22 -1
- package/types/types.d.ts +93 -10
- package/types/value.d.ts +4 -1
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 {
|
|
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' &&
|
|
15
|
-
throw new Error('motion: physical springs do not accept
|
|
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 (
|
|
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
|
|
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: [
|
|
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: [
|
|
229
|
+
keyframes: [legFrom, legTo],
|
|
126
230
|
duration: duration * 1000,
|
|
127
231
|
ease: transition.ease ?? 'easeInOut',
|
|
128
232
|
})
|
|
129
233
|
|
|
130
|
-
|
|
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
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|