@tamagui/animations-motion 2.7.7 → 3.0.0-beta.643.1
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/cjs/createAnimations.cjs +926 -791
- package/dist/cjs/createAnimations.native.cjs +1158 -0
- package/dist/cjs/createAnimations.native.js +1109 -943
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.cjs +10 -11
- package/dist/cjs/index.native.cjs +42 -0
- package/dist/cjs/index.native.js +28 -29
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/polyfill.cjs +3 -2
- package/dist/cjs/polyfill.native.cjs +6 -0
- package/dist/cjs/polyfill.native.js +3 -1
- package/dist/cjs/polyfill.native.js.map +1 -1
- package/dist/esm/createAnimations.mjs +912 -769
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +1096 -923
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.native.js +14 -12
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/polyfill.mjs +2 -1
- package/dist/esm/polyfill.mjs.map +1 -1
- package/dist/esm/polyfill.native.js +2 -1
- package/dist/esm/polyfill.native.js.map +1 -1
- package/package.json +5 -5
- package/src/createAnimations.tsx +338 -56
- package/src/index.native.ts +4 -4
- package/types/createAnimations.d.ts.map +2 -2
- package/types/index.native.d.ts +2 -2
- package/types/index.native.d.ts.map +2 -2
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
package/src/createAnimations.tsx
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
getConfig,
|
|
8
8
|
getSplitStyles,
|
|
9
9
|
hooks,
|
|
10
|
+
normalizeValueWithProperty,
|
|
11
|
+
type OnTransition,
|
|
10
12
|
styleToCSS,
|
|
11
13
|
Text,
|
|
12
14
|
TransitionProp,
|
|
@@ -15,6 +17,8 @@ import {
|
|
|
15
17
|
useIsomorphicLayoutEffect,
|
|
16
18
|
useThemeWithState,
|
|
17
19
|
View,
|
|
20
|
+
createRefComponent,
|
|
21
|
+
transformsToString,
|
|
18
22
|
} from '@tamagui/web'
|
|
19
23
|
import {
|
|
20
24
|
animate as animateMotionValue,
|
|
@@ -27,14 +31,7 @@ import {
|
|
|
27
31
|
useMotionValueEvent,
|
|
28
32
|
type ValueTransition,
|
|
29
33
|
} from 'motion/react'
|
|
30
|
-
import React, {
|
|
31
|
-
forwardRef,
|
|
32
|
-
useEffect,
|
|
33
|
-
useLayoutEffect,
|
|
34
|
-
useMemo,
|
|
35
|
-
useRef,
|
|
36
|
-
useState,
|
|
37
|
-
} from 'react'
|
|
34
|
+
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
38
35
|
|
|
39
36
|
const isServer = typeof window === 'undefined'
|
|
40
37
|
|
|
@@ -43,7 +40,7 @@ const isServer = typeof window === 'undefined'
|
|
|
43
40
|
// don't need animations so we return a no-op scope/animate pair.
|
|
44
41
|
function useAnimateSSRSafe() {
|
|
45
42
|
if (isServer) {
|
|
46
|
-
return [useRef(null), (() => {}) as any] as ReturnType<typeof useAnimate>
|
|
43
|
+
return [useRef(null), (() => {}) as any] as unknown as ReturnType<typeof useAnimate>
|
|
47
44
|
}
|
|
48
45
|
return useAnimate()
|
|
49
46
|
}
|
|
@@ -57,6 +54,20 @@ type MotionAnimatedNumberStyle = {
|
|
|
57
54
|
motionValues?: MotionValue<number>[]
|
|
58
55
|
}
|
|
59
56
|
|
|
57
|
+
const asStyleRecord = (style: unknown): Record<string, unknown> =>
|
|
58
|
+
style && typeof style === 'object' && !Array.isArray(style)
|
|
59
|
+
? (style as Record<string, unknown>)
|
|
60
|
+
: {}
|
|
61
|
+
|
|
62
|
+
// raw CSSOM assignment (unlike React's style prop) silently ignores unitless
|
|
63
|
+
// numbers, and the core hands the inline-output driver RN-format numeric
|
|
64
|
+
// values — suffix px the way React would before writing
|
|
65
|
+
function assignInlineStyles(node: HTMLElement, styles: Record<string, unknown>) {
|
|
66
|
+
for (const key in styles) {
|
|
67
|
+
node.style[key] = normalizeValueWithProperty(styles[key], key)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/**
|
|
61
72
|
* Animation options with optional default and per-property configs.
|
|
62
73
|
* This extends AnimationOptions to support the default key.
|
|
@@ -137,8 +148,15 @@ type MotionRefs = {
|
|
|
137
148
|
animationState: 'enter' | 'exit' | 'default'
|
|
138
149
|
frozenExitTarget: Record<string, unknown> | null
|
|
139
150
|
exitCompleteScheduled: boolean
|
|
151
|
+
enterCompleteScheduled: boolean
|
|
152
|
+
disableAnimation: boolean
|
|
140
153
|
wasEntering: boolean
|
|
141
154
|
wasDisabled: boolean
|
|
155
|
+
onTransition: OnTransition | null | undefined
|
|
156
|
+
enterStarted: boolean
|
|
157
|
+
exitStarted: boolean
|
|
158
|
+
updateInFlight: boolean
|
|
159
|
+
updateControls: AnimationPlaybackControlsWithThen | null
|
|
142
160
|
wasNoClass: boolean
|
|
143
161
|
}
|
|
144
162
|
|
|
@@ -176,6 +194,7 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
176
194
|
stateRef,
|
|
177
195
|
useStyleEmitter,
|
|
178
196
|
presence,
|
|
197
|
+
onTransition,
|
|
179
198
|
styleProps,
|
|
180
199
|
} = animationProps
|
|
181
200
|
|
|
@@ -205,12 +224,29 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
205
224
|
animationState: 'default',
|
|
206
225
|
frozenExitTarget: null,
|
|
207
226
|
exitCompleteScheduled: false,
|
|
227
|
+
enterCompleteScheduled: false,
|
|
228
|
+
disableAnimation: true,
|
|
208
229
|
wasEntering: false,
|
|
209
230
|
wasDisabled: false,
|
|
231
|
+
onTransition: undefined,
|
|
232
|
+
enterStarted: false,
|
|
233
|
+
exitStarted: false,
|
|
234
|
+
updateInFlight: false,
|
|
235
|
+
updateControls: null,
|
|
210
236
|
wasNoClass: !!styleProps.noClass,
|
|
211
237
|
}
|
|
212
238
|
}
|
|
213
239
|
|
|
240
|
+
const emit = (
|
|
241
|
+
phase: 'start' | 'end',
|
|
242
|
+
cause: 'enter' | 'exit' | 'update',
|
|
243
|
+
finished?: boolean
|
|
244
|
+
) => {
|
|
245
|
+
refs.current.onTransition?.(
|
|
246
|
+
phase === 'end' ? { phase, cause, finished } : { phase, cause }
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
214
250
|
// track entering state transitions
|
|
215
251
|
const justFinishedEntering = refs.current.wasEntering && !isEntering
|
|
216
252
|
useEffect(() => {
|
|
@@ -227,12 +263,14 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
227
263
|
// disable animation during hydration and mounting (prevents "flying across the page")
|
|
228
264
|
const disableAnimation = isComponentHydrating || isMounting || !animationKey
|
|
229
265
|
|
|
230
|
-
const [scope
|
|
266
|
+
const [scope] = useAnimateSSRSafe()
|
|
231
267
|
|
|
232
268
|
// sync ref values for reliable access from callbacks
|
|
233
269
|
refs.current.isExiting = isExiting
|
|
234
270
|
refs.current.sendExitComplete = sendExitComplete
|
|
235
271
|
refs.current.animationState = animationState
|
|
272
|
+
refs.current.disableAnimation = disableAnimation
|
|
273
|
+
refs.current.onTransition = onTransition
|
|
236
274
|
|
|
237
275
|
// detect transition into exiting state
|
|
238
276
|
const justStartedExiting = isExiting && !refs.current.wasExiting
|
|
@@ -242,6 +280,7 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
242
280
|
if (justStartedExiting || justStoppedExiting) {
|
|
243
281
|
refs.current.frozenExitTarget = null
|
|
244
282
|
refs.current.exitCompleteScheduled = false
|
|
283
|
+
refs.current.enterCompleteScheduled = false
|
|
245
284
|
}
|
|
246
285
|
|
|
247
286
|
// track previous exiting state
|
|
@@ -249,6 +288,14 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
249
288
|
refs.current.wasExiting = isExiting
|
|
250
289
|
})
|
|
251
290
|
|
|
291
|
+
// exit interrupted by a re-enter: emit a finished:false end for it
|
|
292
|
+
useIsomorphicLayoutEffect(() => {
|
|
293
|
+
if (justStoppedExiting && refs.current.exitStarted) {
|
|
294
|
+
refs.current.exitStarted = false
|
|
295
|
+
emit('end', 'exit', false)
|
|
296
|
+
}
|
|
297
|
+
}, [justStoppedExiting])
|
|
298
|
+
|
|
252
299
|
const {
|
|
253
300
|
dontAnimate = {},
|
|
254
301
|
doAnimate,
|
|
@@ -285,6 +332,7 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
285
332
|
// read current state from refs (closure variables can be stale)
|
|
286
333
|
const isCurrentlyExiting = refs.current.isExiting
|
|
287
334
|
const currentSendExitComplete = refs.current.sendExitComplete
|
|
335
|
+
const currentAnimationState = refs.current.animationState
|
|
288
336
|
|
|
289
337
|
// freeze exit target: once the first exit animation starts, subsequent
|
|
290
338
|
// renders (e.g. direction change) should not reverse the exit animation.
|
|
@@ -345,10 +393,10 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
345
393
|
removeRemovedStyles(prevDont, dontAnimate, node, doAnimate)
|
|
346
394
|
const changed = getDiff(prevDont, dontAnimate)
|
|
347
395
|
if (changed) {
|
|
348
|
-
|
|
396
|
+
assignInlineStyles(node, changed)
|
|
349
397
|
}
|
|
350
398
|
} else {
|
|
351
|
-
|
|
399
|
+
assignInlineStyles(node, dontAnimate)
|
|
352
400
|
}
|
|
353
401
|
}
|
|
354
402
|
|
|
@@ -358,7 +406,7 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
358
406
|
if (prevDont) {
|
|
359
407
|
for (const key in prevDont) {
|
|
360
408
|
if (key in doAnimate) {
|
|
361
|
-
node.style[key] = prevDont[key]
|
|
409
|
+
node.style[key] = normalizeValueWithProperty(prevDont[key], key)
|
|
362
410
|
if (refs.current.lastDoAnimate) {
|
|
363
411
|
refs.current.lastDoAnimate[key] = prevDont[key]
|
|
364
412
|
}
|
|
@@ -562,7 +610,14 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
562
610
|
// PopoverClickDuringEnter / AnimatePresenceEnterExit.
|
|
563
611
|
// the motion-value spring owns transform — keep it out of the
|
|
564
612
|
// WAAPI animation so the two don't double-drive the property
|
|
565
|
-
let waapiDiff =
|
|
613
|
+
let waapiDiff = Object.fromEntries(
|
|
614
|
+
Object.entries(fixedDiff).map(([key, value]) => [
|
|
615
|
+
key,
|
|
616
|
+
Array.isArray(value)
|
|
617
|
+
? value.map((item) => normalizeValueWithProperty(item, key))
|
|
618
|
+
: normalizeValueWithProperty(value, key),
|
|
619
|
+
])
|
|
620
|
+
)
|
|
566
621
|
if (popperHandledTransform && 'transform' in waapiDiff) {
|
|
567
622
|
waapiDiff = { ...waapiDiff }
|
|
568
623
|
delete waapiDiff.transform
|
|
@@ -577,7 +632,11 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
577
632
|
}
|
|
578
633
|
|
|
579
634
|
if (Object.keys(waapiDiff).length > 0) {
|
|
580
|
-
startedControls =
|
|
635
|
+
startedControls = animateMotionValue(
|
|
636
|
+
node,
|
|
637
|
+
waapiDiff,
|
|
638
|
+
animationOptions
|
|
639
|
+
) as AnimationPlaybackControlsWithThen
|
|
581
640
|
refs.current.controls = startedControls
|
|
582
641
|
}
|
|
583
642
|
refs.current.lastAnimateAt = Date.now()
|
|
@@ -589,30 +648,91 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
589
648
|
} finally {
|
|
590
649
|
// exit completion: notify AnimatePresence when exit animation finishes
|
|
591
650
|
if (isCurrentlyExiting && currentSendExitComplete) {
|
|
651
|
+
// an enter animation that was still in flight is now interrupted
|
|
652
|
+
if (refs.current.enterStarted) {
|
|
653
|
+
refs.current.enterStarted = false
|
|
654
|
+
emit('end', 'enter', false)
|
|
655
|
+
}
|
|
592
656
|
if (startedControls) {
|
|
593
657
|
// new animation started — attach completion handler
|
|
658
|
+
if (!refs.current.exitStarted) {
|
|
659
|
+
refs.current.exitStarted = true
|
|
660
|
+
emit('start', 'exit')
|
|
661
|
+
}
|
|
594
662
|
refs.current.exitCompleteScheduled = true
|
|
663
|
+
const complete = (finished: boolean) => {
|
|
664
|
+
// guard: only complete if still exiting (prevents stale promise
|
|
665
|
+
// from calling sendExitComplete after a re-entry cancels the exit)
|
|
666
|
+
if (!refs.current.isExiting) return
|
|
667
|
+
if (refs.current.exitStarted) {
|
|
668
|
+
refs.current.exitStarted = false
|
|
669
|
+
// exit 'end' fires immediately before presence safeToRemove
|
|
670
|
+
emit('end', 'exit', finished)
|
|
671
|
+
}
|
|
672
|
+
currentSendExitComplete()
|
|
673
|
+
}
|
|
595
674
|
startedControls.finished
|
|
596
|
-
.then(() =>
|
|
597
|
-
|
|
598
|
-
// from calling sendExitComplete after a re-entry cancels the exit)
|
|
599
|
-
if (refs.current.isExiting) {
|
|
600
|
-
currentSendExitComplete()
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
.catch(() => {
|
|
604
|
-
if (refs.current.isExiting) {
|
|
605
|
-
currentSendExitComplete()
|
|
606
|
-
}
|
|
607
|
-
})
|
|
675
|
+
.then(() => complete(true))
|
|
676
|
+
.catch(() => complete(false))
|
|
608
677
|
} else if (!refs.current.exitCompleteScheduled) {
|
|
609
678
|
// no animation started AND none previously scheduled (e.g. diff=null
|
|
610
679
|
// on re-render mid-exit because frozenExitTarget matches lastDoAnimate)
|
|
611
680
|
// — complete immediately only if we've never started an exit animation
|
|
681
|
+
emit('start', 'exit')
|
|
682
|
+
emit('end', 'exit', true)
|
|
612
683
|
currentSendExitComplete()
|
|
613
684
|
}
|
|
614
685
|
// else: exit animation already scheduled via a previous flush,
|
|
615
686
|
// its .finished promise will call sendExitComplete when done
|
|
687
|
+
} else if (currentAnimationState === 'enter') {
|
|
688
|
+
if (startedControls) {
|
|
689
|
+
if (!refs.current.enterStarted) {
|
|
690
|
+
refs.current.enterStarted = true
|
|
691
|
+
emit('start', 'enter')
|
|
692
|
+
}
|
|
693
|
+
refs.current.enterCompleteScheduled = true
|
|
694
|
+
const complete = (finished: boolean) => {
|
|
695
|
+
if (refs.current.disposed || refs.current.animationState !== 'enter')
|
|
696
|
+
return
|
|
697
|
+
if (refs.current.enterStarted) {
|
|
698
|
+
refs.current.enterStarted = false
|
|
699
|
+
emit('end', 'enter', finished)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
startedControls.finished
|
|
703
|
+
.then(() => complete(true))
|
|
704
|
+
.catch(() => complete(false))
|
|
705
|
+
} else if (
|
|
706
|
+
!refs.current.enterCompleteScheduled &&
|
|
707
|
+
!refs.current.disableAnimation
|
|
708
|
+
) {
|
|
709
|
+
// no animation needed for this enter (no style diff) — complete
|
|
710
|
+
// immediately. skipped while animation is disabled (mount flushes
|
|
711
|
+
// apply enter styles without animating; completing there would
|
|
712
|
+
// report enter done before the real transition even starts)
|
|
713
|
+
emit('start', 'enter')
|
|
714
|
+
emit('end', 'enter', true)
|
|
715
|
+
}
|
|
716
|
+
} else if (startedControls) {
|
|
717
|
+
// update: a style change while mounted. a new update that supersedes
|
|
718
|
+
// an in-flight one closes it out as finished:false (motion silently
|
|
719
|
+
// replaces the conflicting property animation without settling the
|
|
720
|
+
// old controls, so we can't rely on its promise for the interrupted
|
|
721
|
+
// one). the latest controls reports true on natural completion.
|
|
722
|
+
const controls = startedControls
|
|
723
|
+
if (refs.current.updateInFlight) {
|
|
724
|
+
emit('end', 'update', false)
|
|
725
|
+
}
|
|
726
|
+
refs.current.updateInFlight = true
|
|
727
|
+
refs.current.updateControls = controls
|
|
728
|
+
emit('start', 'update')
|
|
729
|
+
const settle = (finished: boolean) => {
|
|
730
|
+
if (refs.current.updateControls !== controls) return
|
|
731
|
+
refs.current.updateInFlight = false
|
|
732
|
+
refs.current.updateControls = null
|
|
733
|
+
emit('end', 'update', finished)
|
|
734
|
+
}
|
|
735
|
+
controls.finished.then(() => settle(true)).catch(() => settle(false))
|
|
616
736
|
}
|
|
617
737
|
}
|
|
618
738
|
}
|
|
@@ -681,8 +801,8 @@ export function createAnimations<A extends Record<string, AnimationConfig>>(
|
|
|
681
801
|
if ((justEnabled || justStrippedClasses) && animationState !== 'enter') {
|
|
682
802
|
const node = stateRef.current.host
|
|
683
803
|
if (node instanceof HTMLElement) {
|
|
684
|
-
if (dontAnimate)
|
|
685
|
-
if (doAnimate)
|
|
804
|
+
if (dontAnimate) assignInlineStyles(node, dontAnimate)
|
|
805
|
+
if (doAnimate) assignInlineStyles(node, doAnimate)
|
|
686
806
|
// keep the popper position motion values in sync with the direct
|
|
687
807
|
// inline write so a later retarget doesn't start from a stale spot
|
|
688
808
|
const entry = PopperPositionAnims.get(node)
|
|
@@ -1028,13 +1148,103 @@ export const disableAnimationProps: Set<string> = new Set<string>([
|
|
|
1028
1148
|
'userSelect',
|
|
1029
1149
|
])
|
|
1030
1150
|
|
|
1151
|
+
// props equality for the getSplitStyles memo: functions and children can't
|
|
1152
|
+
// affect style output (they pass through), so they don't participate
|
|
1153
|
+
function motionPropsEqual(a: Record<string, any>, b: Record<string, any>) {
|
|
1154
|
+
for (const key in a) {
|
|
1155
|
+
if (!(key in b)) return false
|
|
1156
|
+
}
|
|
1157
|
+
for (const key in b) {
|
|
1158
|
+
if (key === 'children') continue
|
|
1159
|
+
const av = a[key]
|
|
1160
|
+
const bv = b[key]
|
|
1161
|
+
if (typeof bv === 'function' && typeof av === 'function') continue
|
|
1162
|
+
if (!Object.is(av, bv)) return false
|
|
1163
|
+
}
|
|
1164
|
+
return true
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// one-level style object comparison: style arrays are recreated per render
|
|
1168
|
+
// with usually-identical contents
|
|
1169
|
+
function motionStylesEqual(a: any[], b: any[]) {
|
|
1170
|
+
if (a.length !== b.length) return false
|
|
1171
|
+
for (let i = 0; i < a.length; i++) {
|
|
1172
|
+
const sa = a[i]
|
|
1173
|
+
const sb = b[i]
|
|
1174
|
+
if (sa === sb) continue
|
|
1175
|
+
if (!sa || !sb) return false
|
|
1176
|
+
let aCount = 0
|
|
1177
|
+
for (const key in sa) {
|
|
1178
|
+
aCount++
|
|
1179
|
+
if (!Object.is(sa[key], sb[key])) return false
|
|
1180
|
+
}
|
|
1181
|
+
let bCount = 0
|
|
1182
|
+
for (const _key in sb) {
|
|
1183
|
+
bCount++
|
|
1184
|
+
}
|
|
1185
|
+
if (aCount !== bCount) return false
|
|
1186
|
+
}
|
|
1187
|
+
return true
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1031
1190
|
const MotionView = createMotionView('div')
|
|
1032
1191
|
const MotionText = createMotionView('span')
|
|
1033
1192
|
|
|
1193
|
+
const transformAliases: Record<string, string> = {
|
|
1194
|
+
x: 'translateX',
|
|
1195
|
+
y: 'translateY',
|
|
1196
|
+
scale: 'scale',
|
|
1197
|
+
scaleX: 'scaleX',
|
|
1198
|
+
scaleY: 'scaleY',
|
|
1199
|
+
rotate: 'rotate',
|
|
1200
|
+
rotateX: 'rotateX',
|
|
1201
|
+
rotateY: 'rotateY',
|
|
1202
|
+
rotateZ: 'rotateZ',
|
|
1203
|
+
skewX: 'skewX',
|
|
1204
|
+
skewY: 'skewY',
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function compileAnimatedStyle(
|
|
1208
|
+
initialSource: Record<string, unknown>,
|
|
1209
|
+
initialResolved: Record<string, unknown>
|
|
1210
|
+
) {
|
|
1211
|
+
const shorthands = getConfig().shorthands
|
|
1212
|
+
const entries = Object.keys(initialSource).map((source) => ({
|
|
1213
|
+
source,
|
|
1214
|
+
target: shorthands?.[source] ?? source,
|
|
1215
|
+
tokenValue:
|
|
1216
|
+
typeof initialSource[source] === 'string' &&
|
|
1217
|
+
initialResolved[shorthands?.[source] ?? source] !== initialSource[source]
|
|
1218
|
+
? initialResolved[shorthands?.[source] ?? source]
|
|
1219
|
+
: undefined,
|
|
1220
|
+
}))
|
|
1221
|
+
|
|
1222
|
+
return (sourceStyle: Record<string, unknown>) => {
|
|
1223
|
+
const resolved: Record<string, unknown> = {}
|
|
1224
|
+
const transforms: Record<string, unknown>[] = []
|
|
1225
|
+
|
|
1226
|
+
for (const entry of entries) {
|
|
1227
|
+
const value = sourceStyle[entry.source]
|
|
1228
|
+
if (value === undefined) continue
|
|
1229
|
+
if (entry.source === 'transform' && Array.isArray(value)) {
|
|
1230
|
+
resolved.transform = transformsToString(value)
|
|
1231
|
+
} else if (entry.source in transformAliases) {
|
|
1232
|
+
transforms.push({ [transformAliases[entry.source]]: value })
|
|
1233
|
+
} else {
|
|
1234
|
+
resolved[entry.target] =
|
|
1235
|
+
entry.tokenValue ?? normalizeValueWithProperty(value, entry.target)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (transforms.length) resolved.transform = transformsToString(transforms)
|
|
1240
|
+
return resolved
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1034
1244
|
function createMotionView(defaultTag: string) {
|
|
1035
1245
|
const isText = defaultTag === 'span'
|
|
1036
1246
|
|
|
1037
|
-
const Component =
|
|
1247
|
+
const Component = createRefComponent((propsIn: any, ref) => {
|
|
1038
1248
|
const { forwardedRef, animation, render = defaultTag, style, ...propsRest } = propsIn
|
|
1039
1249
|
const [scope, animate] = useAnimateSSRSafe()
|
|
1040
1250
|
const hostRef = useRef<HTMLElement>(null)
|
|
@@ -1070,6 +1280,13 @@ function createMotionView(defaultTag: string) {
|
|
|
1070
1280
|
})()
|
|
1071
1281
|
|
|
1072
1282
|
function getProps(props: any) {
|
|
1283
|
+
if (
|
|
1284
|
+
process.env.NODE_ENV === 'development' &&
|
|
1285
|
+
propsIn.debug === 'profile' &&
|
|
1286
|
+
typeof performance !== 'undefined'
|
|
1287
|
+
) {
|
|
1288
|
+
performance.mark('tamagui-motion-style-split')
|
|
1289
|
+
}
|
|
1073
1290
|
const out = getSplitStyles(
|
|
1074
1291
|
props,
|
|
1075
1292
|
isText ? Text.staticConfig : View.staticConfig,
|
|
@@ -1097,12 +1314,76 @@ function createMotionView(defaultTag: string) {
|
|
|
1097
1314
|
return out.viewProps
|
|
1098
1315
|
}
|
|
1099
1316
|
|
|
1100
|
-
const
|
|
1317
|
+
const resolvedAnimatedStyle = useMemo(() => {
|
|
1318
|
+
if (!animatedStyle) return null
|
|
1319
|
+
const currentValues = animatedStyle.motionValues
|
|
1320
|
+
? animatedStyle.motionValues.map((value) => value.get())
|
|
1321
|
+
: animatedStyle.motionValue
|
|
1322
|
+
? [animatedStyle.motionValue.get()]
|
|
1323
|
+
: []
|
|
1324
|
+
const initialSource = animatedStyle.getStyle(...currentValues)
|
|
1325
|
+
const initialResolved = asStyleRecord(getProps({ style: initialSource }).style)
|
|
1326
|
+
return {
|
|
1327
|
+
initial: initialResolved,
|
|
1328
|
+
resolve: compileAnimatedStyle(initialSource, initialResolved),
|
|
1329
|
+
}
|
|
1330
|
+
}, [animatedStyle, state?.theme, state?.name])
|
|
1331
|
+
|
|
1332
|
+
// memoize the full getSplitStyles pass: it costs ~90us per render and its
|
|
1333
|
+
// style-affecting inputs are usually unchanged. functions and children
|
|
1334
|
+
// pass through getSplitStyles untouched, so they refresh on cache hits
|
|
1335
|
+
// without invalidating.
|
|
1336
|
+
const memoRef = useRef<null | {
|
|
1337
|
+
propsRest: any
|
|
1338
|
+
styles: any[]
|
|
1339
|
+
theme: any
|
|
1340
|
+
themeName: string | undefined
|
|
1341
|
+
result: any
|
|
1342
|
+
}>(null)
|
|
1343
|
+
|
|
1344
|
+
let props: any
|
|
1345
|
+
const cached = memoRef.current
|
|
1346
|
+
if (
|
|
1347
|
+
cached &&
|
|
1348
|
+
cached.theme === state?.theme &&
|
|
1349
|
+
cached.themeName === state?.name &&
|
|
1350
|
+
motionStylesEqual(cached.styles, nonAnimatedStyles) &&
|
|
1351
|
+
motionPropsEqual(cached.propsRest, propsRest)
|
|
1352
|
+
) {
|
|
1353
|
+
props = { ...cached.result }
|
|
1354
|
+
for (const key in propsRest) {
|
|
1355
|
+
const val = propsRest[key]
|
|
1356
|
+
if (key === 'children' || typeof val === 'function') {
|
|
1357
|
+
props[key] = val
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
} else {
|
|
1361
|
+
props = getProps({ ...propsRest, style: nonAnimatedStyles })
|
|
1362
|
+
memoRef.current = {
|
|
1363
|
+
propsRest,
|
|
1364
|
+
styles: nonAnimatedStyles,
|
|
1365
|
+
theme: state?.theme,
|
|
1366
|
+
themeName: state?.name,
|
|
1367
|
+
result: props,
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (resolvedAnimatedStyle) {
|
|
1372
|
+
// reassign so the animated initial never leaks into the memo cache
|
|
1373
|
+
props = {
|
|
1374
|
+
...props,
|
|
1375
|
+
style: {
|
|
1376
|
+
...asStyleRecord(props.style),
|
|
1377
|
+
...resolvedAnimatedStyle.initial,
|
|
1378
|
+
},
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1101
1382
|
const Element = render || 'div'
|
|
1102
1383
|
const transformedProps = hooks.usePropsTransform?.(render, props, stateRef, false)
|
|
1103
1384
|
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1385
|
+
// subscribe before passive effects so a layout effect cannot set a value
|
|
1386
|
+
// between this node mounting and its MotionValue subscription.
|
|
1106
1387
|
useIsomorphicLayoutEffect(() => {
|
|
1107
1388
|
if (!animatedStyle) return
|
|
1108
1389
|
|
|
@@ -1115,15 +1396,13 @@ function createMotionView(defaultTag: string) {
|
|
|
1115
1396
|
: animatedStyle.motionValue
|
|
1116
1397
|
? animatedStyle.getStyle(animatedStyle.motionValue.get())
|
|
1117
1398
|
: null
|
|
1118
|
-
const webStyle = currentStyle &&
|
|
1399
|
+
const webStyle = currentStyle && resolvedAnimatedStyle?.resolve(currentStyle)
|
|
1119
1400
|
if (webStyle) {
|
|
1120
|
-
|
|
1401
|
+
assignInlineStyles(node, webStyle)
|
|
1121
1402
|
seededNode.current = node
|
|
1122
1403
|
seededInCurrentFrame.current = true
|
|
1123
1404
|
requestAnimationFrame(() => {
|
|
1124
|
-
if (seededNode.current === node)
|
|
1125
|
-
seededInCurrentFrame.current = false
|
|
1126
|
-
}
|
|
1405
|
+
if (seededNode.current === node) seededInCurrentFrame.current = false
|
|
1127
1406
|
})
|
|
1128
1407
|
}
|
|
1129
1408
|
}
|
|
@@ -1132,7 +1411,7 @@ function createMotionView(defaultTag: string) {
|
|
|
1132
1411
|
animationConfig: AnimatedNumberStrategy | undefined
|
|
1133
1412
|
): AnimationOptions =>
|
|
1134
1413
|
animationConfig?.type === 'timing'
|
|
1135
|
-
? { type: 'tween', duration: (animationConfig
|
|
1414
|
+
? { type: 'tween', duration: (animationConfig.duration || 0) / 1000 }
|
|
1136
1415
|
: animationConfig?.type === 'direct'
|
|
1137
1416
|
? { type: 'tween', duration: 0 }
|
|
1138
1417
|
: { type: 'spring', ...(animationConfig as any) }
|
|
@@ -1140,44 +1419,47 @@ function createMotionView(defaultTag: string) {
|
|
|
1140
1419
|
const animateNodeTo = (
|
|
1141
1420
|
nextStyle: Record<string, unknown>,
|
|
1142
1421
|
transition: AnimationOptions,
|
|
1143
|
-
|
|
1422
|
+
motionValue: MotionValue
|
|
1144
1423
|
) => {
|
|
1145
1424
|
if (!(node instanceof HTMLElement) || hostRef.current !== node) return
|
|
1146
|
-
const webStyle =
|
|
1425
|
+
const webStyle = resolvedAnimatedStyle?.resolve(nextStyle)
|
|
1147
1426
|
if (!webStyle) return
|
|
1148
|
-
settlePendingMotionOnFinish(
|
|
1427
|
+
settlePendingMotionOnFinish(
|
|
1428
|
+
motionValue,
|
|
1429
|
+
animate(node, webStyle as any, transition)
|
|
1430
|
+
)
|
|
1149
1431
|
}
|
|
1432
|
+
|
|
1150
1433
|
const animateChangedValue = (
|
|
1151
1434
|
nextStyle: Record<string, unknown>,
|
|
1152
1435
|
transition: AnimationOptions,
|
|
1153
|
-
|
|
1436
|
+
motionValue: MotionValue
|
|
1154
1437
|
) => {
|
|
1155
1438
|
if (seededInCurrentFrame.current && seededNode.current === node) {
|
|
1156
|
-
const queuedFrame = queuedAnimationFrames.current.get(
|
|
1157
|
-
if (queuedFrame !== undefined)
|
|
1158
|
-
cancelAnimationFrame(queuedFrame)
|
|
1159
|
-
}
|
|
1439
|
+
const queuedFrame = queuedAnimationFrames.current.get(motionValue)
|
|
1440
|
+
if (queuedFrame !== undefined) cancelAnimationFrame(queuedFrame)
|
|
1160
1441
|
const frame = requestAnimationFrame(() => {
|
|
1161
|
-
if (queuedAnimationFrames.current.get(
|
|
1162
|
-
queuedAnimationFrames.current.delete(
|
|
1442
|
+
if (queuedAnimationFrames.current.get(motionValue) !== frame) return
|
|
1443
|
+
queuedAnimationFrames.current.delete(motionValue)
|
|
1163
1444
|
if (hostRef.current === node) {
|
|
1164
|
-
animateNodeTo(nextStyle, transition,
|
|
1445
|
+
animateNodeTo(nextStyle, transition, motionValue)
|
|
1165
1446
|
} else {
|
|
1166
|
-
const onFinish = PendingMotionOnFinish.get(
|
|
1167
|
-
PendingMotionOnFinish.delete(
|
|
1447
|
+
const onFinish = PendingMotionOnFinish.get(motionValue)
|
|
1448
|
+
PendingMotionOnFinish.delete(motionValue)
|
|
1168
1449
|
onFinish?.()
|
|
1169
1450
|
}
|
|
1170
1451
|
})
|
|
1171
|
-
queuedAnimationFrames.current.set(
|
|
1452
|
+
queuedAnimationFrames.current.set(motionValue, frame)
|
|
1172
1453
|
return
|
|
1173
1454
|
}
|
|
1174
|
-
animateNodeTo(nextStyle, transition,
|
|
1455
|
+
animateNodeTo(nextStyle, transition, motionValue)
|
|
1175
1456
|
}
|
|
1176
1457
|
|
|
1177
1458
|
// multi-value path: subscribe to all motion values
|
|
1178
1459
|
if (animatedStyle.motionValues) {
|
|
1179
1460
|
const mvs = animatedStyle.motionValues
|
|
1180
|
-
const styleForAll = () =>
|
|
1461
|
+
const styleForAll = () =>
|
|
1462
|
+
animatedStyle.getStyle(...mvs.map((value) => value.get()))
|
|
1181
1463
|
const unsubs = mvs.map((mv) =>
|
|
1182
1464
|
mv.on('change', () =>
|
|
1183
1465
|
animateChangedValue(
|
|
@@ -1201,7 +1483,7 @@ function createMotionView(defaultTag: string) {
|
|
|
1201
1483
|
motionValue
|
|
1202
1484
|
)
|
|
1203
1485
|
)
|
|
1204
|
-
}, [animatedStyle])
|
|
1486
|
+
}, [animatedStyle, resolvedAnimatedStyle])
|
|
1205
1487
|
|
|
1206
1488
|
return <Element {...transformedProps} ref={composedRefs} />
|
|
1207
1489
|
})
|
package/src/index.native.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// native stub - animations-motion only works on web (uses framer-motion/motion library)
|
|
2
2
|
// on native, use @tamagui/animations-react-native or @tamagui/animations-reanimated
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { AnimationDriverStub } from '@tamagui/web'
|
|
5
5
|
|
|
6
6
|
let hasWarnedOnce = false
|
|
7
7
|
|
|
8
8
|
export function createAnimations<A extends Record<string, any>>(
|
|
9
9
|
_animations: A
|
|
10
|
-
):
|
|
10
|
+
): AnimationDriverStub<A> {
|
|
11
11
|
if (process.env.NODE_ENV === 'development') {
|
|
12
12
|
if (!hasWarnedOnce) {
|
|
13
13
|
hasWarnedOnce = true
|
|
@@ -17,10 +17,10 @@ export function createAnimations<A extends Record<string, any>>(
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// return
|
|
21
|
-
// @ts-expect-error its an error anyway
|
|
20
|
+
// return an explicit unsupported driver so config resolution can skip it
|
|
22
21
|
return {
|
|
23
22
|
isReactNative: false,
|
|
23
|
+
isStub: true,
|
|
24
24
|
animations: _animations,
|
|
25
25
|
View: undefined as any,
|
|
26
26
|
Text: undefined as any,
|