@tamagui/animation-helpers 2.7.7 → 3.0.0-beta.1097.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/index.cjs +32 -21
- package/dist/cjs/index.native.cjs +46 -0
- package/dist/cjs/index.native.js +33 -21
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/presets.cjs +77 -0
- package/dist/cjs/presets.native.cjs +79 -0
- package/dist/cjs/presets.native.js +81 -0
- package/dist/cjs/presets.native.js.map +1 -0
- package/dist/cjs/propertyNames.cjs +91 -0
- package/dist/cjs/propertyNames.native.cjs +95 -0
- package/dist/cjs/propertyNames.native.js +97 -0
- package/dist/cjs/propertyNames.native.js.map +1 -0
- package/dist/cjs/resolveTransition.cjs +345 -0
- package/dist/cjs/resolveTransition.native.cjs +424 -0
- package/dist/cjs/resolveTransition.native.js +426 -0
- package/dist/cjs/resolveTransition.native.js.map +1 -0
- package/dist/cjs/transitionResolver.cjs +34 -0
- package/dist/cjs/transitionResolver.native.cjs +37 -0
- package/dist/cjs/transitionResolver.native.js +39 -0
- package/dist/cjs/transitionResolver.native.js.map +1 -0
- package/dist/cjs/types.cjs +9 -11
- package/dist/cjs/types.native.cjs +18 -0
- package/dist/cjs/types.native.js +9 -10
- package/dist/cjs/types.native.js.map +1 -1
- package/dist/esm/index.js +6 -3
- package/dist/esm/index.mjs +6 -3
- package/dist/esm/index.native.js +6 -3
- package/dist/esm/presets.mjs +57 -0
- package/dist/esm/presets.mjs.map +1 -0
- package/dist/esm/presets.native.js +57 -0
- package/dist/esm/presets.native.js.map +1 -0
- package/dist/esm/propertyNames.mjs +67 -0
- package/dist/esm/propertyNames.mjs.map +1 -0
- package/dist/esm/propertyNames.native.js +69 -0
- package/dist/esm/propertyNames.native.js.map +1 -0
- package/dist/esm/resolveTransition.mjs +315 -0
- package/dist/esm/resolveTransition.mjs.map +1 -0
- package/dist/esm/resolveTransition.native.js +392 -0
- package/dist/esm/resolveTransition.native.js.map +1 -0
- package/dist/esm/transitionResolver.mjs +11 -0
- package/dist/esm/transitionResolver.mjs.map +1 -0
- package/dist/esm/transitionResolver.native.js +12 -0
- package/dist/esm/transitionResolver.native.js.map +1 -0
- package/dist/esm/types.mjs +0 -2
- package/dist/esm/types.native.js +0 -2
- package/package.json +23 -2
- package/src/index.ts +39 -9
- package/src/presets.ts +40 -0
- package/src/propertyNames.ts +78 -0
- package/src/resolveTransition.ts +503 -0
- package/src/transitionResolver.ts +35 -0
- package/src/types.ts +42 -46
- package/types/index.d.ts +6 -2
- package/types/index.d.ts.map +2 -2
- package/types/presets.d.ts +79 -0
- package/types/presets.d.ts.map +11 -0
- package/types/propertyNames.d.ts +13 -0
- package/types/propertyNames.d.ts.map +11 -0
- package/types/resolveTransition.d.ts +108 -0
- package/types/resolveTransition.d.ts.map +11 -0
- package/types/transitionResolver.d.ts +30 -0
- package/types/transitionResolver.d.ts.map +11 -0
- package/types/types.d.ts +35 -42
- package/types/types.d.ts.map +2 -2
- package/dist/cjs/normalizeTransition.cjs +0 -164
- package/dist/cjs/normalizeTransition.native.js +0 -231
- package/dist/cjs/normalizeTransition.native.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
- package/dist/esm/normalizeTransition.mjs +0 -134
- package/dist/esm/normalizeTransition.mjs.map +0 -1
- package/dist/esm/normalizeTransition.native.js +0 -198
- package/dist/esm/normalizeTransition.native.js.map +0 -1
- package/dist/esm/types.mjs.map +0 -1
- package/dist/esm/types.native.js.map +0 -1
- package/src/normalizeTransition.ts +0 -255
- package/types/normalizeTransition.d.ts +0 -54
- package/types/normalizeTransition.d.ts.map +0 -11
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// which css property a tamagui style key animates under.
|
|
2
|
+
//
|
|
3
|
+
// this is a module of its own, with no imports, because `@tamagui/web` needs
|
|
4
|
+
// it on every styled component while the parser and the spring solver in
|
|
5
|
+
// `resolveTransition` are only needed when a driver is in the bundle.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* transform parts map onto the css property that actually animates them.
|
|
9
|
+
* `translate`, `scale`, and `rotate` are real individual transform properties;
|
|
10
|
+
* everything else can only be reached through `transform` as a whole.
|
|
11
|
+
*
|
|
12
|
+
* collapsing here is deliberate. css cannot animate `rotateX` separately from
|
|
13
|
+
* `skewY` (both live in `transform`), so letting a js driver do it would be a
|
|
14
|
+
* capability that silently disappears the moment you build for the web.
|
|
15
|
+
*/
|
|
16
|
+
const transformProperties: Record<string, string> = {
|
|
17
|
+
x: 'translate',
|
|
18
|
+
y: 'translate',
|
|
19
|
+
z: 'translate',
|
|
20
|
+
translate: 'translate',
|
|
21
|
+
translateX: 'translate',
|
|
22
|
+
translateY: 'translate',
|
|
23
|
+
translateZ: 'translate',
|
|
24
|
+
scale: 'scale',
|
|
25
|
+
scaleX: 'scale',
|
|
26
|
+
scaleY: 'scale',
|
|
27
|
+
scaleZ: 'scale',
|
|
28
|
+
rotate: 'rotate',
|
|
29
|
+
rotateX: 'transform',
|
|
30
|
+
rotateY: 'transform',
|
|
31
|
+
rotateZ: 'transform',
|
|
32
|
+
skewX: 'transform',
|
|
33
|
+
skewY: 'transform',
|
|
34
|
+
perspective: 'transform',
|
|
35
|
+
matrix: 'transform',
|
|
36
|
+
transform: 'transform',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* the css property name an entry is filed under, so `backgroundColor` and
|
|
41
|
+
* `background-color` are one key and cannot both silently apply.
|
|
42
|
+
*/
|
|
43
|
+
export function canonicalTransitionProperty(key: string): string {
|
|
44
|
+
if (key === 'all' || key === 'none') return key
|
|
45
|
+
const transform = transformProperties[key]
|
|
46
|
+
if (transform) return transform
|
|
47
|
+
return key.includes('-') ? key : key.replace(/[A-Z]/g, '-$&').toLowerCase()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isTransformProperty(key: string): boolean {
|
|
51
|
+
return key in transformProperties
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const styleKeysByProperty: Record<string, readonly string[]> = {
|
|
55
|
+
translate: ['translate', 'translateX', 'translateY', 'translateZ', 'x', 'y', 'z'],
|
|
56
|
+
scale: ['scale', 'scaleX', 'scaleY', 'scaleZ'],
|
|
57
|
+
rotate: ['rotate'],
|
|
58
|
+
transform: [
|
|
59
|
+
'transform',
|
|
60
|
+
'rotateX',
|
|
61
|
+
'rotateY',
|
|
62
|
+
'rotateZ',
|
|
63
|
+
'skewX',
|
|
64
|
+
'skewY',
|
|
65
|
+
'perspective',
|
|
66
|
+
'matrix',
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* the style keys a css property covers, for drivers that key their per-property
|
|
72
|
+
* options by style key rather than css property (motion, react-native).
|
|
73
|
+
*/
|
|
74
|
+
export function styleKeysForProperty(property: string): readonly string[] {
|
|
75
|
+
const transform = styleKeysByProperty[property]
|
|
76
|
+
if (transform) return transform
|
|
77
|
+
return [property.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())]
|
|
78
|
+
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
// the one place a `transition` prop becomes driver-ready values.
|
|
2
|
+
//
|
|
3
|
+
// every driver and `getSplitStyles` call `resolveTransition`, so there is a
|
|
4
|
+
// single grammar, a single preset lookup, and a single spring solver behind
|
|
5
|
+
// css, reanimated, motion, and react-native. a transition that means one thing
|
|
6
|
+
// on one driver cannot mean something else on another, because none of them
|
|
7
|
+
// decide anything on their own any more.
|
|
8
|
+
//
|
|
9
|
+
// the shape is CSS's: an ordered entry list where the LAST entry naming a
|
|
10
|
+
// property wins for that property, and entries do not inherit from each other.
|
|
11
|
+
// `transition="all 200ms, opacity 150ms ease-out"` reads exactly as it does in
|
|
12
|
+
// a stylesheet.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
parseTransition,
|
|
16
|
+
parseTransitionObject,
|
|
17
|
+
springFromDurationBounce,
|
|
18
|
+
springSettleTime,
|
|
19
|
+
springToDurationBounce,
|
|
20
|
+
springToLinearEasing,
|
|
21
|
+
type TransitionBehavior,
|
|
22
|
+
type TransitionDiagnostic,
|
|
23
|
+
type TransitionEntry,
|
|
24
|
+
type TransitionObjectValue,
|
|
25
|
+
type TransitionTiming,
|
|
26
|
+
} from '@tamagui/style-grammar/transitions'
|
|
27
|
+
|
|
28
|
+
import { canonicalTransitionProperty } from './propertyNames'
|
|
29
|
+
import { setTransitionResolver } from './transitionResolver'
|
|
30
|
+
|
|
31
|
+
export type DriverTiming =
|
|
32
|
+
| {
|
|
33
|
+
kind: 'timing'
|
|
34
|
+
durationMs: number
|
|
35
|
+
easing: string
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
kind: 'spring'
|
|
39
|
+
/** the undamped period, the portable "how fast does this feel" number */
|
|
40
|
+
durationMs: number
|
|
41
|
+
bounce: number
|
|
42
|
+
stiffness: number
|
|
43
|
+
damping: number
|
|
44
|
+
mass: number
|
|
45
|
+
/** authored low-level overrides, for drivers that take them raw */
|
|
46
|
+
extra?: Record<string, unknown>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ResolvedEntry {
|
|
50
|
+
/** canonical property name, kebab-case, or `all` / `none` */
|
|
51
|
+
property: string
|
|
52
|
+
timing: DriverTiming
|
|
53
|
+
delayMs: number
|
|
54
|
+
behavior: TransitionBehavior
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ResolvedTransition {
|
|
58
|
+
/** in authored order; later entries win under the css last-wins rule */
|
|
59
|
+
entries: readonly ResolvedEntry[]
|
|
60
|
+
/** the winning `all` entry, or null when only specific properties were named */
|
|
61
|
+
all: ResolvedEntry | null
|
|
62
|
+
/** winning entry per canonical property name, excluding `all` */
|
|
63
|
+
byProperty: Readonly<Record<string, ResolvedEntry>>
|
|
64
|
+
/** true when the author disabled transitions entirely (`transition="none"`) */
|
|
65
|
+
none: boolean
|
|
66
|
+
/**
|
|
67
|
+
* true when any entry was authored as a preset name or a `spring()`, the two
|
|
68
|
+
* things that have no css spelling of their own. a resolution without one is
|
|
69
|
+
* already plain css and needs no driver at all.
|
|
70
|
+
*/
|
|
71
|
+
fused: boolean
|
|
72
|
+
/** replaces this whole resolution while mounting, when the author set one */
|
|
73
|
+
enter: ResolvedTransition | null
|
|
74
|
+
/** replaces this whole resolution while unmounting */
|
|
75
|
+
exit: ResolvedTransition | null
|
|
76
|
+
diagnostics: readonly TransitionDiagnostic[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const emptyResolved: ResolvedTransition = {
|
|
80
|
+
entries: [],
|
|
81
|
+
all: null,
|
|
82
|
+
byProperty: {},
|
|
83
|
+
none: false,
|
|
84
|
+
fused: false,
|
|
85
|
+
enter: null,
|
|
86
|
+
exit: null,
|
|
87
|
+
diagnostics: [],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** the resolution that applies in a given animation state */
|
|
91
|
+
export function forAnimationState(
|
|
92
|
+
resolved: ResolvedTransition,
|
|
93
|
+
state: 'enter' | 'exit' | 'default'
|
|
94
|
+
): ResolvedTransition {
|
|
95
|
+
if (state === 'enter' && resolved.enter) return resolved.enter
|
|
96
|
+
if (state === 'exit' && resolved.exit) return resolved.exit
|
|
97
|
+
return resolved
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function timeToMs(value: string): number {
|
|
101
|
+
// the grammar guarantees a `<number>ms` or `<number>s` token here
|
|
102
|
+
return value.endsWith('ms') ? Number.parseFloat(value) : Number.parseFloat(value) * 1000
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function springTiming(
|
|
106
|
+
durationMs: number,
|
|
107
|
+
bounce: number,
|
|
108
|
+
extra?: Record<string, unknown>
|
|
109
|
+
): DriverTiming {
|
|
110
|
+
const mass = typeof extra?.mass === 'number' ? extra.mass : 1
|
|
111
|
+
const physics = springFromDurationBounce({ duration: durationMs, bounce }, mass)
|
|
112
|
+
return {
|
|
113
|
+
kind: 'spring',
|
|
114
|
+
durationMs,
|
|
115
|
+
bounce,
|
|
116
|
+
// authored physics win over the derived pair, so the escape hatch is exact
|
|
117
|
+
stiffness: typeof extra?.stiffness === 'number' ? extra.stiffness : physics.stiffness,
|
|
118
|
+
damping: typeof extra?.damping === 'number' ? extra.damping : physics.damping,
|
|
119
|
+
mass,
|
|
120
|
+
extra,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* turns one entry in a driver's `animations` config into a timing.
|
|
126
|
+
*
|
|
127
|
+
* accepts every shape `PresetConfig` allows: a css string (`'350ms ease-out'`),
|
|
128
|
+
* spring physics (`{ type: 'spring', damping, stiffness, mass }`), a timing
|
|
129
|
+
* (`{ type: 'timing', duration, easing }`), and the canonical pair
|
|
130
|
+
* (`{ duration, bounce }`). configs written for any driver therefore resolve
|
|
131
|
+
* to the same motion on all four.
|
|
132
|
+
*/
|
|
133
|
+
export function presetToTiming(preset: unknown): DriverTiming | null {
|
|
134
|
+
if (typeof preset === 'string') {
|
|
135
|
+
const parsed = parseTransition(preset)
|
|
136
|
+
if (!parsed.ok || parsed.value.kind !== 'transition') return null
|
|
137
|
+
const entry = parsed.value.entries[0]
|
|
138
|
+
if (!entry) return null
|
|
139
|
+
return timingToDriver(entry.timing, null)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!preset || typeof preset !== 'object') return null
|
|
143
|
+
const config = preset as Record<string, unknown>
|
|
144
|
+
const duration = typeof config.duration === 'number' ? config.duration : undefined
|
|
145
|
+
const hasPhysics =
|
|
146
|
+
typeof config.stiffness === 'number' || typeof config.damping === 'number'
|
|
147
|
+
|
|
148
|
+
const stiffness = typeof config.stiffness === 'number' ? config.stiffness : undefined
|
|
149
|
+
const damping = typeof config.damping === 'number' ? config.damping : undefined
|
|
150
|
+
|
|
151
|
+
const isSpring =
|
|
152
|
+
config.type === 'spring' || typeof config.bounce === 'number' || hasPhysics
|
|
153
|
+
|
|
154
|
+
if (isSpring) {
|
|
155
|
+
const mass = typeof config.mass === 'number' ? config.mass : 1
|
|
156
|
+
if (stiffness !== undefined) {
|
|
157
|
+
const canonical = springToDurationBounce({
|
|
158
|
+
stiffness,
|
|
159
|
+
damping: damping ?? 2 * Math.sqrt(stiffness * mass),
|
|
160
|
+
mass,
|
|
161
|
+
})
|
|
162
|
+
return {
|
|
163
|
+
kind: 'spring',
|
|
164
|
+
durationMs: duration ?? canonical.duration,
|
|
165
|
+
bounce: typeof config.bounce === 'number' ? config.bounce : canonical.bounce,
|
|
166
|
+
stiffness,
|
|
167
|
+
damping: damping ?? 2 * Math.sqrt(stiffness * mass),
|
|
168
|
+
mass,
|
|
169
|
+
extra: config,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (duration !== undefined) {
|
|
173
|
+
return springTiming(
|
|
174
|
+
duration,
|
|
175
|
+
typeof config.bounce === 'number' ? config.bounce : 0,
|
|
176
|
+
config
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (duration === undefined) return null
|
|
183
|
+
const easing = typeof config.easing === 'string' ? config.easing : 'ease'
|
|
184
|
+
return { kind: 'timing', durationMs: duration, easing }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* lowers a grammar timing atom into driver values, resolving a preset name
|
|
189
|
+
* against the driver's configured animations.
|
|
190
|
+
*/
|
|
191
|
+
function timingToDriver(
|
|
192
|
+
timing: TransitionTiming,
|
|
193
|
+
animations: Record<string, unknown> | null
|
|
194
|
+
): DriverTiming | null {
|
|
195
|
+
if (timing.type === 'css') {
|
|
196
|
+
return {
|
|
197
|
+
kind: 'timing',
|
|
198
|
+
durationMs: timeToMs(timing.duration),
|
|
199
|
+
easing: timing.timingFunction,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (timing.type === 'spring') {
|
|
204
|
+
return springTiming(
|
|
205
|
+
timeToMs(timing.duration),
|
|
206
|
+
timing.bounce,
|
|
207
|
+
timing.config as Record<string, unknown> | undefined
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const preset = animations?.[timing.name]
|
|
212
|
+
if (preset === undefined) return null
|
|
213
|
+
const base = presetToTiming(preset)
|
|
214
|
+
if (!base || !timing.config) return base
|
|
215
|
+
|
|
216
|
+
// authored overrides land on top of the preset, in the preset's own terms
|
|
217
|
+
const overrides = timing.config as Record<string, unknown>
|
|
218
|
+
if (base.kind === 'spring' || typeof overrides.bounce === 'number') {
|
|
219
|
+
const overrideDuration =
|
|
220
|
+
typeof overrides.duration === 'number'
|
|
221
|
+
? overrides.duration
|
|
222
|
+
: typeof overrides.duration === 'string'
|
|
223
|
+
? timeToMs(overrides.duration)
|
|
224
|
+
: undefined
|
|
225
|
+
const overrideBounce =
|
|
226
|
+
typeof overrides.bounce === 'number' ? overrides.bounce : undefined
|
|
227
|
+
const durationMs = overrideDuration ?? base.durationMs
|
|
228
|
+
const bounce = overrideBounce ?? (base.kind === 'spring' ? base.bounce : 0)
|
|
229
|
+
let carried = base.kind === 'spring' ? base.extra : undefined
|
|
230
|
+
// a duration or bounce override re-solves the spring, so a preset written as
|
|
231
|
+
// stiffness/damping cannot pass those two through: they ARE what the
|
|
232
|
+
// override replaces. mass belongs to the object rather than the curve, so it
|
|
233
|
+
// carries. physics written in the override still win, exactly as elsewhere.
|
|
234
|
+
if (carried && (overrideDuration !== undefined || overrideBounce !== undefined)) {
|
|
235
|
+
const { stiffness, damping, ...rest } = carried
|
|
236
|
+
carried = rest
|
|
237
|
+
}
|
|
238
|
+
return springTiming(durationMs, bounce, { ...carried, ...overrides })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
kind: 'timing',
|
|
243
|
+
durationMs:
|
|
244
|
+
typeof overrides.duration === 'number'
|
|
245
|
+
? overrides.duration
|
|
246
|
+
: typeof overrides.duration === 'string'
|
|
247
|
+
? timeToMs(overrides.duration)
|
|
248
|
+
: base.durationMs,
|
|
249
|
+
easing: typeof overrides.easing === 'string' ? overrides.easing : base.easing,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const namedEasings: Record<string, readonly [number, number, number, number]> = {
|
|
254
|
+
linear: [0, 0, 1, 1],
|
|
255
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
256
|
+
'ease-in': [0.42, 0, 1, 1],
|
|
257
|
+
'ease-out': [0, 0, 0.58, 1],
|
|
258
|
+
'ease-in-out': [0.42, 0, 0.58, 1],
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* a css easing as cubic-bezier control points, which is the only easing shape
|
|
263
|
+
* reanimated, motion, and react-native all accept.
|
|
264
|
+
*
|
|
265
|
+
* returns null for `steps()` and `linear()`, which have no bezier equivalent.
|
|
266
|
+
* a driver that gets null should fall back to its own default rather than
|
|
267
|
+
* pretending it applied something.
|
|
268
|
+
*/
|
|
269
|
+
export function easingToBezier(
|
|
270
|
+
easing: string
|
|
271
|
+
): readonly [number, number, number, number] | null {
|
|
272
|
+
const named = namedEasings[easing]
|
|
273
|
+
if (named) return named
|
|
274
|
+
if (!easing.startsWith('cubic-bezier(')) return null
|
|
275
|
+
const parts = easing.slice('cubic-bezier('.length, -1).split(',')
|
|
276
|
+
if (parts.length !== 4) return null
|
|
277
|
+
const points = parts.map((part) => Number.parseFloat(part))
|
|
278
|
+
if (points.some((point) => !Number.isFinite(point))) return null
|
|
279
|
+
return points as unknown as [number, number, number, number]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface ResolveTransitionOptions {
|
|
283
|
+
/** the driver's animations config; its keys are the valid preset names */
|
|
284
|
+
animations?: Record<string, unknown> | null
|
|
285
|
+
/** config shorthands, so `transition="bg 200ms"` names backgroundColor */
|
|
286
|
+
shorthands?: Record<string, string> | null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const cacheByAnimations = new WeakMap<object, Map<string, ResolvedTransition>>()
|
|
290
|
+
const objectCache = new WeakMap<object, ResolvedTransition>()
|
|
291
|
+
const noAnimations: Record<string, unknown> = {}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* parses and resolves a `transition` prop into driver-ready entries.
|
|
295
|
+
*
|
|
296
|
+
* memoized per (animations config, authored value), because this runs on every
|
|
297
|
+
* render of every component carrying a transition.
|
|
298
|
+
*/
|
|
299
|
+
export function resolveTransition(
|
|
300
|
+
transition: TransitionObjectValue | null | undefined,
|
|
301
|
+
options: ResolveTransitionOptions = {}
|
|
302
|
+
): ResolvedTransition {
|
|
303
|
+
if (transition == null || transition === '') return emptyResolved
|
|
304
|
+
|
|
305
|
+
const animations = options.animations ?? null
|
|
306
|
+
const cacheKey = (animations ?? noAnimations) as object
|
|
307
|
+
|
|
308
|
+
if (typeof transition === 'string') {
|
|
309
|
+
let byValue = cacheByAnimations.get(cacheKey)
|
|
310
|
+
if (!byValue) {
|
|
311
|
+
byValue = new Map()
|
|
312
|
+
cacheByAnimations.set(cacheKey, byValue)
|
|
313
|
+
}
|
|
314
|
+
const hit = byValue.get(transition)
|
|
315
|
+
if (hit) return hit
|
|
316
|
+
const built = buildResolved(transition, animations, options.shorthands ?? null)
|
|
317
|
+
// an authored value set is bounded in practice, but a codegen loop is not
|
|
318
|
+
if (byValue.size > 2048) byValue.clear()
|
|
319
|
+
byValue.set(transition, built)
|
|
320
|
+
return built
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (typeof transition !== 'object') return emptyResolved
|
|
324
|
+
const hit = objectCache.get(transition)
|
|
325
|
+
if (hit) return hit
|
|
326
|
+
const built = buildResolved(transition, animations, options.shorthands ?? null)
|
|
327
|
+
objectCache.set(transition, built)
|
|
328
|
+
return built
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildResolved(
|
|
332
|
+
transition: TransitionObjectValue,
|
|
333
|
+
animations: Record<string, unknown> | null,
|
|
334
|
+
shorthands: Record<string, string> | null
|
|
335
|
+
): ResolvedTransition {
|
|
336
|
+
const presetNames = animations ? new Set(Object.keys(animations)) : undefined
|
|
337
|
+
const parsed = parseTransitionObject(transition, presetNames)
|
|
338
|
+
|
|
339
|
+
if (!parsed.ok) {
|
|
340
|
+
return { ...emptyResolved, diagnostics: parsed.diagnostics }
|
|
341
|
+
}
|
|
342
|
+
if (parsed.value.kind !== 'transition') return emptyResolved
|
|
343
|
+
|
|
344
|
+
return fromEntries(
|
|
345
|
+
parsed.value.entries,
|
|
346
|
+
animations,
|
|
347
|
+
shorthands,
|
|
348
|
+
parsed.value.enter
|
|
349
|
+
? fromEntries(parsed.value.enter, animations, shorthands, null, null)
|
|
350
|
+
: null,
|
|
351
|
+
parsed.value.exit
|
|
352
|
+
? fromEntries(parsed.value.exit, animations, shorthands, null, null)
|
|
353
|
+
: null
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const transformFamily = ['transform', 'translate', 'scale', 'rotate'] as const
|
|
358
|
+
|
|
359
|
+
function fromEntries(
|
|
360
|
+
source: readonly TransitionEntry[],
|
|
361
|
+
animations: Record<string, unknown> | null,
|
|
362
|
+
shorthands: Record<string, string> | null,
|
|
363
|
+
enter: ResolvedTransition | null,
|
|
364
|
+
exit: ResolvedTransition | null
|
|
365
|
+
): ResolvedTransition {
|
|
366
|
+
const entries: ResolvedEntry[] = []
|
|
367
|
+
const byProperty: Record<string, ResolvedEntry> = {}
|
|
368
|
+
const diagnostics: TransitionDiagnostic[] = []
|
|
369
|
+
let all: ResolvedEntry | null = null
|
|
370
|
+
let none = false
|
|
371
|
+
let fused = false
|
|
372
|
+
|
|
373
|
+
for (const entry of source) {
|
|
374
|
+
if (entry.property === 'none') {
|
|
375
|
+
none = true
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (entry.timing.type !== 'css') fused = true
|
|
380
|
+
|
|
381
|
+
const timing = timingToDriver(entry.timing, animations)
|
|
382
|
+
if (!timing) {
|
|
383
|
+
diagnostics.push({
|
|
384
|
+
code: 'transition-invalid-token',
|
|
385
|
+
token: entry.timing.type === 'preset' ? entry.timing.name : undefined,
|
|
386
|
+
message:
|
|
387
|
+
entry.timing.type === 'preset'
|
|
388
|
+
? `"${entry.timing.name}" is not a configured animation`
|
|
389
|
+
: 'transition timing could not be resolved',
|
|
390
|
+
})
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const expanded = shorthands?.[entry.property] ?? entry.property
|
|
395
|
+
const property = canonicalTransitionProperty(expanded)
|
|
396
|
+
const delayMs = timeToMs(entry.delay)
|
|
397
|
+
// tamagui writes x/y into the css `translate` longhand, scale into
|
|
398
|
+
// `scale` and rotate into `rotate`, so an authored `transform` covers the
|
|
399
|
+
// whole family or it transitions nothing the author can see.
|
|
400
|
+
for (const name of expanded === 'transform' ? transformFamily : [property]) {
|
|
401
|
+
const resolved: ResolvedEntry = {
|
|
402
|
+
property: name,
|
|
403
|
+
timing,
|
|
404
|
+
delayMs,
|
|
405
|
+
behavior: entry.behavior,
|
|
406
|
+
}
|
|
407
|
+
entries.push(resolved)
|
|
408
|
+
// last wins, exactly as a stylesheet resolves a repeated property
|
|
409
|
+
if (name === 'all') all = resolved
|
|
410
|
+
else byProperty[name] = resolved
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { entries, all, byProperty, none, fused, enter, exit, diagnostics }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* the transition that applies to one style key, under css last-wins.
|
|
419
|
+
*
|
|
420
|
+
* an authored `transform` already expanded into the whole family in
|
|
421
|
+
* `fromEntries`, so `x`, `scale` and `rotate` find a direct entry here.
|
|
422
|
+
*/
|
|
423
|
+
export function getTransitionForKey(
|
|
424
|
+
resolved: ResolvedTransition,
|
|
425
|
+
key: string
|
|
426
|
+
): ResolvedEntry | null {
|
|
427
|
+
if (resolved.none) return null
|
|
428
|
+
const direct = resolved.byProperty[canonicalTransitionProperty(key)]
|
|
429
|
+
if (direct) return direct
|
|
430
|
+
return resolved.all
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** true when anything at all will animate */
|
|
434
|
+
export function hasTransition(resolved: ResolvedTransition): boolean {
|
|
435
|
+
return !resolved.none && resolved.entries.length > 0
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* how long a timing actually runs, which for a spring is its settle time and
|
|
440
|
+
* not its nominal duration. this is the number a completion deadline needs.
|
|
441
|
+
*/
|
|
442
|
+
export function getSettleMs(timing: DriverTiming): number {
|
|
443
|
+
if (timing.kind === 'timing') return timing.durationMs
|
|
444
|
+
return springSettleTime({ duration: timing.durationMs, bounce: timing.bounce })
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** the longest anything will take, for the driver's completion bookkeeping */
|
|
448
|
+
export function getMaxDurationMs(resolved: ResolvedTransition): number {
|
|
449
|
+
let max = 0
|
|
450
|
+
for (const entry of resolved.entries) {
|
|
451
|
+
const total = getSettleMs(entry.timing) + entry.delayMs
|
|
452
|
+
if (total > max) max = total
|
|
453
|
+
}
|
|
454
|
+
return max
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const linearEasingCache = new Map<string, { easing: string; durationMs: number }>()
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* one entry as css. a spring becomes a `linear()` easing sampled across its
|
|
461
|
+
* settle time, which is how a real spring curve, overshoot included, survives
|
|
462
|
+
* with no javascript running.
|
|
463
|
+
*/
|
|
464
|
+
export function entryToCSS(entry: ResolvedEntry): string {
|
|
465
|
+
const delay = entry.delayMs ? ` ${entry.delayMs}ms` : ''
|
|
466
|
+
const behavior = entry.behavior === 'allow-discrete' ? ' allow-discrete' : ''
|
|
467
|
+
|
|
468
|
+
if (entry.timing.kind === 'timing') {
|
|
469
|
+
return `${entry.property} ${entry.timing.durationMs}ms ${entry.timing.easing}${delay}${behavior}`
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const key = `${entry.timing.durationMs}/${entry.timing.bounce}`
|
|
473
|
+
let spring = linearEasingCache.get(key)
|
|
474
|
+
if (!spring) {
|
|
475
|
+
spring = springToLinearEasing({
|
|
476
|
+
duration: entry.timing.durationMs,
|
|
477
|
+
bounce: entry.timing.bounce,
|
|
478
|
+
})
|
|
479
|
+
if (linearEasingCache.size > 256) linearEasingCache.clear()
|
|
480
|
+
linearEasingCache.set(key, spring)
|
|
481
|
+
}
|
|
482
|
+
return `${entry.property} ${Math.round(spring.durationMs)}ms ${spring.easing}${delay}${behavior}`
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* the whole resolution as a css `transition` value, or `undefined` when
|
|
487
|
+
* nothing animates. entry order is preserved, so css last-wins does the
|
|
488
|
+
* per-property resolution for us in the browser.
|
|
489
|
+
*/
|
|
490
|
+
export function toCSSTransition(resolved: ResolvedTransition): string | undefined {
|
|
491
|
+
if (resolved.none) return 'none'
|
|
492
|
+
if (!resolved.entries.length) return undefined
|
|
493
|
+
|
|
494
|
+
let out = ''
|
|
495
|
+
for (const entry of resolved.entries) {
|
|
496
|
+
out += `${out ? ', ' : ''}${entryToCSS(entry)}`
|
|
497
|
+
}
|
|
498
|
+
return out || undefined
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// see `transitionResolver.ts`: this is what keeps the grammar out of a bundle
|
|
502
|
+
// that has no animation driver in it.
|
|
503
|
+
setTransitionResolver({ resolve: resolveTransition, toCSS: toCSSTransition })
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResolvedTransition } from './resolveTransition'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `getSplitStyles` needs to know whether a `transition` string is plain css or
|
|
5
|
+
* needs a driver, and that answer has to come from the same grammar the
|
|
6
|
+
* compiler uses. Importing the grammar into `@tamagui/web` would put the
|
|
7
|
+
* parser and the spring solver in every bundle, including one with no
|
|
8
|
+
* animations in it at all, which is about 3.6kb gzip of runtime nobody asked
|
|
9
|
+
* for.
|
|
10
|
+
*
|
|
11
|
+
* So `resolveTransition` registers itself here on import. Loading any driver
|
|
12
|
+
* loads it, and a bundle with no driver in it has no presets to resolve, so
|
|
13
|
+
* the absent resolver is the correct answer rather than a missing one.
|
|
14
|
+
*
|
|
15
|
+
* The slot lives on `globalThis` because a bundler routinely gives one process
|
|
16
|
+
* two copies of this module: Vite's SSR graph and its optimized-deps graph each
|
|
17
|
+
* carry their own, and a module-local `let` then leaves the reader looking at
|
|
18
|
+
* the copy nobody registered into. The server rendered a transition as a static
|
|
19
|
+
* class while the client resolved it to a driver animation, which React reports
|
|
20
|
+
* as a hydration mismatch.
|
|
21
|
+
*/
|
|
22
|
+
export type TransitionResolver = {
|
|
23
|
+
resolve: (value: any, options: { animations?: any }) => ResolvedTransition
|
|
24
|
+
toCSS: (resolved: ResolvedTransition) => string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const slot = Symbol.for('tamagui.transitionResolver')
|
|
28
|
+
|
|
29
|
+
export function setTransitionResolver(next: TransitionResolver): void {
|
|
30
|
+
;(globalThis as any)[slot] = next
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getTransitionResolver(): TransitionResolver | null {
|
|
34
|
+
return (globalThis as any)[slot] ?? null
|
|
35
|
+
}
|