@tamagui/animations-css 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/animated-number.cjs +195 -0
- package/dist/cjs/animated-number.native.cjs +292 -0
- package/dist/cjs/animated-number.native.js +294 -0
- package/dist/cjs/animated-number.native.js.map +1 -0
- package/dist/cjs/createAnimations.cjs +359 -416
- package/dist/cjs/createAnimations.native.cjs +472 -0
- package/dist/cjs/createAnimations.native.js +446 -510
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.cjs +10 -11
- package/dist/cjs/index.native.cjs +21 -0
- package/dist/cjs/index.native.js +10 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/animated-number.mjs +164 -0
- package/dist/esm/animated-number.mjs.map +1 -0
- package/dist/esm/animated-number.native.js +259 -0
- package/dist/esm/animated-number.native.js.map +1 -0
- package/dist/esm/createAnimations.mjs +344 -391
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +430 -485
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.mjs +1 -2
- package/dist/esm/index.native.js +1 -2
- package/package.json +16 -7
- package/src/animated-number.tsx +262 -0
- package/src/createAnimations.tsx +305 -501
- package/types/animated-number.d.ts +19 -0
- package/types/animated-number.d.ts.map +11 -0
- package/types/createAnimations.d.ts.map +2 -2
- 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/src/createAnimations.tsx
CHANGED
|
@@ -3,160 +3,141 @@ import {
|
|
|
3
3
|
getAnimatedProperties,
|
|
4
4
|
hasAnimation as hasNormalizedAnimation,
|
|
5
5
|
getEffectiveAnimation,
|
|
6
|
-
getAnimationConfigsForKeys,
|
|
7
6
|
} from '@tamagui/animation-helpers'
|
|
8
7
|
import { useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
9
8
|
import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
10
|
-
import type { AnimationDriver
|
|
9
|
+
import type { AnimationDriver } from '@tamagui/web'
|
|
11
10
|
import { transformsToString } from '@tamagui/web'
|
|
12
|
-
import React
|
|
11
|
+
import React from 'react'
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
import {
|
|
14
|
+
useAnimatedNumber,
|
|
15
|
+
useAnimatedNumberReaction,
|
|
16
|
+
useAnimatedNumberStyle,
|
|
17
|
+
useAnimatedNumbersStyle,
|
|
18
|
+
} from './animated-number'
|
|
19
|
+
|
|
20
|
+
// rAF-driven animated number is browser-only. read (don't call) at module scope
|
|
21
|
+
// so ssr never touches requestAnimationFrame.
|
|
22
|
+
const hasRAF = typeof requestAnimationFrame !== 'undefined'
|
|
23
|
+
|
|
24
|
+
// resolve once all WAAPI animations on `node` finish. mirrors base-ui's
|
|
25
|
+
// useAnimationsFinished: resolves immediately when the browser exposes no
|
|
26
|
+
// animations (zero-animation elements), and re-checks after an aborted
|
|
27
|
+
// animation in case a property it depended on changed mid-flight and started a
|
|
28
|
+
// new one. falls back to immediate resolve when getAnimations is unavailable
|
|
29
|
+
// (ssr / older webviews). resolves `false` when animations were canceled with
|
|
30
|
+
// nothing left running (interruption).
|
|
31
|
+
function waitForAnimations(node: HTMLElement): Promise<boolean> {
|
|
32
|
+
if (typeof node.getAnimations !== 'function') {
|
|
33
|
+
return Promise.resolve(true)
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
return new Promise<boolean>((resolve) => {
|
|
36
|
+
const check = () => {
|
|
37
|
+
const animations = node.getAnimations()
|
|
38
|
+
if (animations.length === 0) {
|
|
39
|
+
resolve(true)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
Promise.all(animations.map((a) => a.finished))
|
|
43
|
+
.then(() => resolve(true))
|
|
44
|
+
.catch(() => {
|
|
45
|
+
const remaining = node.getAnimations()
|
|
46
|
+
if (remaining.some((a) => a.playState === 'running' || a.pending)) {
|
|
47
|
+
check()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
resolve(false)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
// css transitions register as pending until the next style recalc, so give
|
|
54
|
+
// the browser one frame to start them before we read getAnimations.
|
|
55
|
+
if (hasRAF) {
|
|
56
|
+
requestAnimationFrame(check)
|
|
57
|
+
} else {
|
|
58
|
+
check()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
37
61
|
}
|
|
38
62
|
|
|
39
|
-
const
|
|
40
|
-
const S_DURATION_REGEX = /(\d+(?:\.\d+)?)\s*s(?!tiffness)/
|
|
63
|
+
const DURATION_REGEX = /(\d+(?:\.\d+)?)\s*(?:ms|s(?!tiffness))/
|
|
41
64
|
|
|
42
65
|
/**
|
|
43
66
|
* Apply duration override to a CSS animation string
|
|
44
67
|
* Replaces the existing duration with the override value
|
|
45
68
|
*/
|
|
46
69
|
function applyDurationOverride(animation: string, durationMs: number): string {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return msReplaced
|
|
51
|
-
}
|
|
70
|
+
const replaced = animation.replace(DURATION_REGEX, `${durationMs}ms`)
|
|
71
|
+
return replaced === animation ? `${durationMs}ms ${animation}` : replaced
|
|
72
|
+
}
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
const CSS_TRANSFORM_PROPERTIES: Record<string, string[]> = {
|
|
75
|
+
transform: ['translate', 'scale', 'rotate', 'transform'],
|
|
76
|
+
x: ['translate'],
|
|
77
|
+
y: ['translate'],
|
|
78
|
+
scale: ['scale'],
|
|
79
|
+
scaleX: ['scale'],
|
|
80
|
+
scaleY: ['scale'],
|
|
81
|
+
rotate: ['rotate'],
|
|
82
|
+
rotateX: ['transform'],
|
|
83
|
+
rotateY: ['transform'],
|
|
84
|
+
rotateZ: ['transform'],
|
|
85
|
+
skewX: ['transform'],
|
|
86
|
+
skewY: ['transform'],
|
|
87
|
+
}
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
return
|
|
89
|
+
const getCSSProperties = (key: string) => {
|
|
90
|
+
return CSS_TRANSFORM_PROPERTIES[key] || [key]
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
'x',
|
|
66
|
-
'y',
|
|
67
|
-
'scale',
|
|
68
|
-
'scaleX',
|
|
69
|
-
'scaleY',
|
|
70
|
-
'rotate',
|
|
71
|
-
'rotateX',
|
|
72
|
-
'rotateY',
|
|
73
|
-
'rotateZ',
|
|
74
|
-
'skewX',
|
|
75
|
-
'skewY',
|
|
76
|
-
] as const
|
|
93
|
+
const hyphenatedPropertyCache: Record<string, string> = {}
|
|
94
|
+
const emptyProperties: string[] = []
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
function hyphenateProperty(property: string): string {
|
|
97
|
+
if (property.startsWith('--')) return property
|
|
98
|
+
return (hyphenatedPropertyCache[property] ||= property.replace(
|
|
99
|
+
/[A-Z]/g,
|
|
100
|
+
(letter) => `-${letter.toLowerCase()}`
|
|
101
|
+
))
|
|
102
|
+
}
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
if (style.scaleX !== undefined) {
|
|
95
|
-
parts.push(`scaleX(${style.scaleX})`)
|
|
96
|
-
}
|
|
97
|
-
if (style.scaleY !== undefined) {
|
|
98
|
-
parts.push(`scaleY(${style.scaleY})`)
|
|
99
|
-
}
|
|
100
|
-
if (style.rotate !== undefined) {
|
|
101
|
-
const val = style.rotate
|
|
102
|
-
const unit = typeof val === 'string' && val.includes('deg') ? '' : 'deg'
|
|
103
|
-
parts.push(`rotate(${val}${unit})`)
|
|
104
|
-
}
|
|
105
|
-
if (style.rotateX !== undefined) {
|
|
106
|
-
parts.push(`rotateX(${style.rotateX}deg)`)
|
|
107
|
-
}
|
|
108
|
-
if (style.rotateY !== undefined) {
|
|
109
|
-
parts.push(`rotateY(${style.rotateY}deg)`)
|
|
110
|
-
}
|
|
111
|
-
if (style.rotateZ !== undefined) {
|
|
112
|
-
parts.push(`rotateZ(${style.rotateZ}deg)`)
|
|
113
|
-
}
|
|
114
|
-
if (style.skewX !== undefined) {
|
|
115
|
-
parts.push(`skewX(${style.skewX}deg)`)
|
|
116
|
-
}
|
|
117
|
-
if (style.skewY !== undefined) {
|
|
118
|
-
parts.push(`skewY(${style.skewY}deg)`)
|
|
104
|
+
function getLifecycleCSSProperties(keys: Set<string> | undefined): string[] {
|
|
105
|
+
if (!keys?.size) return emptyProperties
|
|
106
|
+
const properties = new Set<string>()
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
for (const property of getCSSProperties(key)) {
|
|
109
|
+
properties.add(hyphenateProperty(property))
|
|
110
|
+
}
|
|
119
111
|
}
|
|
120
|
-
|
|
121
|
-
return parts.join(' ')
|
|
112
|
+
return [...properties].sort()
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
|
|
125
|
-
* Apply a style object to a DOM node, handling transform keys specially
|
|
126
|
-
*/
|
|
127
|
-
function applyStylesToNode(
|
|
115
|
+
function readComputedProperties(
|
|
128
116
|
node: HTMLElement,
|
|
129
|
-
|
|
130
|
-
):
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
node.style.transform = transformStr
|
|
117
|
+
properties: readonly string[]
|
|
118
|
+
): Record<string, string> {
|
|
119
|
+
const computed = getComputedStyle(node)
|
|
120
|
+
const values: Record<string, string> = {}
|
|
121
|
+
for (const property of properties) {
|
|
122
|
+
const value = computed.getPropertyValue(property)
|
|
123
|
+
if (value) values[property] = value
|
|
137
124
|
}
|
|
125
|
+
return values
|
|
126
|
+
}
|
|
138
127
|
|
|
139
|
-
|
|
140
|
-
for (const
|
|
141
|
-
|
|
142
|
-
if (value === undefined) continue
|
|
143
|
-
|
|
144
|
-
if (key === 'opacity') {
|
|
145
|
-
node.style.opacity = String(value)
|
|
146
|
-
} else if (key === 'backgroundColor') {
|
|
147
|
-
node.style.backgroundColor = String(value)
|
|
148
|
-
} else if (key === 'color') {
|
|
149
|
-
node.style.color = String(value)
|
|
150
|
-
} else {
|
|
151
|
-
// generic fallback
|
|
152
|
-
node.style[key as any] = typeof value === 'number' ? `${value}px` : String(value)
|
|
153
|
-
}
|
|
128
|
+
function applyCSSProperties(node: HTMLElement, values: Record<string, string>): void {
|
|
129
|
+
for (const property in values) {
|
|
130
|
+
node.style.setProperty(property, values[property])
|
|
154
131
|
}
|
|
155
132
|
}
|
|
156
133
|
|
|
157
|
-
|
|
158
|
-
const
|
|
134
|
+
function clearCSSProperties(node: HTMLElement, properties: readonly string[]): void {
|
|
135
|
+
for (const property of properties) {
|
|
136
|
+
node.style.removeProperty(property)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
159
139
|
|
|
140
|
+
export function createAnimations<A extends object>(animations: A): AnimationDriver<A> {
|
|
160
141
|
return {
|
|
161
142
|
animations,
|
|
162
143
|
usePresence,
|
|
@@ -164,79 +145,10 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
164
145
|
inputStyle: 'css',
|
|
165
146
|
outputStyle: 'css',
|
|
166
147
|
|
|
167
|
-
useAnimatedNumber
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
getInstance() {
|
|
173
|
-
return setVal
|
|
174
|
-
},
|
|
175
|
-
getValue() {
|
|
176
|
-
return val
|
|
177
|
-
},
|
|
178
|
-
setValue(next, config, onFinish) {
|
|
179
|
-
setVal(next)
|
|
180
|
-
|
|
181
|
-
// clear any pending finish callback from a previous setValue
|
|
182
|
-
if (finishTimerRef.current) {
|
|
183
|
-
clearTimeout(finishTimerRef.current)
|
|
184
|
-
finishTimerRef.current = null
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (onFinish) {
|
|
188
|
-
if (
|
|
189
|
-
!config ||
|
|
190
|
-
config.type === 'direct' ||
|
|
191
|
-
(config.type === 'timing' && config.duration === 0)
|
|
192
|
-
) {
|
|
193
|
-
onFinish()
|
|
194
|
-
} else {
|
|
195
|
-
// estimate duration: use explicit duration, or fall back to
|
|
196
|
-
// default CSS transition duration for spring-type configs
|
|
197
|
-
const duration = config.type === 'timing' ? config.duration : 300
|
|
198
|
-
finishTimerRef.current = setTimeout(onFinish, duration)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// call reaction listeners with the new value
|
|
203
|
-
const listeners = reactionListeners.get(setVal)
|
|
204
|
-
if (listeners) {
|
|
205
|
-
listeners.forEach((listener) => listener(next))
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
stop() {
|
|
209
|
-
if (finishTimerRef.current) {
|
|
210
|
-
clearTimeout(finishTimerRef.current)
|
|
211
|
-
finishTimerRef.current = null
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
|
|
217
|
-
useAnimatedNumberReaction({ value }, onValue) {
|
|
218
|
-
React.useEffect(() => {
|
|
219
|
-
const instance = value.getInstance()
|
|
220
|
-
let queue = reactionListeners.get(instance)
|
|
221
|
-
if (!queue) {
|
|
222
|
-
const next = new Set<Function>()
|
|
223
|
-
reactionListeners.set(instance, next)
|
|
224
|
-
queue = next!
|
|
225
|
-
}
|
|
226
|
-
queue.add(onValue)
|
|
227
|
-
return () => {
|
|
228
|
-
queue?.delete(onValue)
|
|
229
|
-
}
|
|
230
|
-
}, [])
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
useAnimatedNumberStyle(val, getStyle) {
|
|
234
|
-
return getStyle(val.getValue())
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
useAnimatedNumbersStyle(vals, getStyle) {
|
|
238
|
-
return getStyle(...vals.map((v) => v.getValue()))
|
|
239
|
-
},
|
|
148
|
+
useAnimatedNumber,
|
|
149
|
+
useAnimatedNumberReaction,
|
|
150
|
+
useAnimatedNumberStyle,
|
|
151
|
+
useAnimatedNumbersStyle,
|
|
240
152
|
|
|
241
153
|
// @ts-ignore - styleState is added by createComponent
|
|
242
154
|
useAnimations: ({
|
|
@@ -246,11 +158,23 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
246
158
|
componentState,
|
|
247
159
|
stateRef,
|
|
248
160
|
styleState,
|
|
161
|
+
onTransition,
|
|
249
162
|
}: any) => {
|
|
250
163
|
const isHydrating = componentState.unmounted === true
|
|
251
164
|
const isEntering = !!componentState.unmounted
|
|
252
165
|
const isExiting = presence?.[0] === false
|
|
253
166
|
const sendExitComplete = presence?.[1]
|
|
167
|
+
const onTransitionRef = React.useRef(onTransition)
|
|
168
|
+
onTransitionRef.current = onTransition
|
|
169
|
+
const emit = (
|
|
170
|
+
phase: 'start' | 'end',
|
|
171
|
+
cause: 'enter' | 'exit' | 'update',
|
|
172
|
+
finished?: boolean
|
|
173
|
+
) => {
|
|
174
|
+
onTransitionRef.current?.(
|
|
175
|
+
phase === 'end' ? { phase, cause, finished } : { phase, cause }
|
|
176
|
+
)
|
|
177
|
+
}
|
|
254
178
|
|
|
255
179
|
// Track if we just finished entering (transition from entering to not entering)
|
|
256
180
|
// This is needed because the CSS transition happens on the render AFTER t_unmounted is removed
|
|
@@ -264,11 +188,23 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
264
188
|
const exitCycleIdRef = React.useRef(0)
|
|
265
189
|
const exitCompletedRef = React.useRef(false)
|
|
266
190
|
const wasExitingRef = React.useRef(false)
|
|
267
|
-
const exitInterruptedRef = React.useRef(false)
|
|
268
191
|
const sendExitCompleteRef = React.useRef(sendExitComplete)
|
|
269
|
-
const
|
|
192
|
+
const lastMountedStyleRef = React.useRef<Record<string, string>>({})
|
|
270
193
|
sendExitCompleteRef.current = sendExitComplete
|
|
271
194
|
|
|
195
|
+
const exitCSSProperties = getLifecycleCSSProperties(
|
|
196
|
+
styleState?.programLifecycleStyleKeys?.exit
|
|
197
|
+
)
|
|
198
|
+
const exitCSSPropertiesSignature = exitCSSProperties.join('\0')
|
|
199
|
+
|
|
200
|
+
// onTransition lifecycle bookkeeping (independent from presence completion)
|
|
201
|
+
const enterCycleIdRef = React.useRef(0)
|
|
202
|
+
const enterStartedRef = React.useRef(false)
|
|
203
|
+
const updateCycleIdRef = React.useRef(0)
|
|
204
|
+
const updateInFlightRef = React.useRef(false)
|
|
205
|
+
const prevUpdateSigRef = React.useRef<string | null>(null)
|
|
206
|
+
const exitStartedRef = React.useRef(false)
|
|
207
|
+
|
|
272
208
|
// detect transition into/out of exiting state
|
|
273
209
|
const justStartedExiting = isExiting && !wasExitingRef.current
|
|
274
210
|
const justStoppedExiting = !isExiting && wasExitingRef.current
|
|
@@ -278,10 +214,8 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
278
214
|
exitCycleIdRef.current++
|
|
279
215
|
exitCompletedRef.current = false
|
|
280
216
|
}
|
|
281
|
-
// track interruptions so we know to force-restart transitions
|
|
282
217
|
if (justStoppedExiting) {
|
|
283
218
|
exitCycleIdRef.current++
|
|
284
|
-
exitInterruptedRef.current = true
|
|
285
219
|
}
|
|
286
220
|
|
|
287
221
|
// track previous exiting state
|
|
@@ -289,14 +223,32 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
289
223
|
wasExitingRef.current = isExiting
|
|
290
224
|
})
|
|
291
225
|
|
|
226
|
+
// Snapshot the actual mounted CSS values for every property with an exit
|
|
227
|
+
// clause. Most program values live in generated classes rather than the
|
|
228
|
+
// inline `style` object, so computed style is the only complete source.
|
|
229
|
+
// The snapshot becomes the reset point for normal and interrupted exits.
|
|
292
230
|
useIsomorphicLayoutEffect(() => {
|
|
231
|
+
if (isExiting) return
|
|
293
232
|
const host = stateRef.current.host
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
opacity: computedStyle.opacity,
|
|
233
|
+
if (!host || !exitCSSProperties.length) {
|
|
234
|
+
lastMountedStyleRef.current = {}
|
|
235
|
+
return
|
|
298
236
|
}
|
|
299
|
-
|
|
237
|
+
const node = host as HTMLElement
|
|
238
|
+
const capture = () => {
|
|
239
|
+
if (stateRef.current.host !== node || wasExitingRef.current) return
|
|
240
|
+
lastMountedStyleRef.current = readComputedProperties(node, exitCSSProperties)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The first mounted layout effect can run while an enter clause is still
|
|
244
|
+
// active. Capture again after that concrete transition settles so a later
|
|
245
|
+
// exit restarts from the mounted value, not the enter value.
|
|
246
|
+
if (justFinishedEntering) {
|
|
247
|
+
void waitForAnimations(node).then(capture)
|
|
248
|
+
} else {
|
|
249
|
+
capture()
|
|
250
|
+
}
|
|
251
|
+
}, [isExiting, justFinishedEntering, exitCSSPropertiesSignature])
|
|
300
252
|
|
|
301
253
|
// use effectiveTransition computed by createComponent (single source of truth)
|
|
302
254
|
const effectiveTransition = styleState?.effectiveTransition ?? props.transition
|
|
@@ -343,6 +295,33 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
343
295
|
keys = ['all']
|
|
344
296
|
}
|
|
345
297
|
|
|
298
|
+
let transition: string | undefined
|
|
299
|
+
const getTransition = () => {
|
|
300
|
+
if (transition !== undefined) return transition
|
|
301
|
+
const delay = normalized.delay ? ` ${normalized.delay}ms` : ''
|
|
302
|
+
const duration = normalized.config?.duration
|
|
303
|
+
transition = keys
|
|
304
|
+
.flatMap((key) => {
|
|
305
|
+
const propertyAnimation = normalized.properties[key]
|
|
306
|
+
let animation = defaultAnimation
|
|
307
|
+
if (typeof propertyAnimation === 'string') {
|
|
308
|
+
animation = animations[propertyAnimation]
|
|
309
|
+
} else if (propertyAnimation?.type) {
|
|
310
|
+
animation = animations[propertyAnimation.type]
|
|
311
|
+
}
|
|
312
|
+
if (animation && duration) {
|
|
313
|
+
animation = applyDurationOverride(animation, duration)
|
|
314
|
+
}
|
|
315
|
+
return animation
|
|
316
|
+
? getCSSProperties(key).map(
|
|
317
|
+
(property) => `${property} ${animation}${delay}`
|
|
318
|
+
)
|
|
319
|
+
: []
|
|
320
|
+
})
|
|
321
|
+
.join(', ')
|
|
322
|
+
return transition
|
|
323
|
+
}
|
|
324
|
+
|
|
346
325
|
useIsomorphicLayoutEffect(() => {
|
|
347
326
|
const host = stateRef.current.host
|
|
348
327
|
if (!sendExitComplete || !isExiting || !host) return
|
|
@@ -351,11 +330,23 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
351
330
|
// capture current cycle id for this effect
|
|
352
331
|
const cycleId = exitCycleIdRef.current
|
|
353
332
|
|
|
354
|
-
//
|
|
355
|
-
|
|
333
|
+
// emit exit start once per cycle
|
|
334
|
+
if (!exitStartedRef.current) {
|
|
335
|
+
exitStartedRef.current = true
|
|
336
|
+
emit('start', 'exit')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// helper to complete exit with guards. the exit 'end' event fires
|
|
340
|
+
// immediately before presence safeToRemove so users can observe exit
|
|
341
|
+
// completion without reaching into presence internals.
|
|
342
|
+
const completeExit = (finished = true) => {
|
|
356
343
|
if (cycleId !== exitCycleIdRef.current) return
|
|
357
344
|
if (exitCompletedRef.current) return
|
|
358
345
|
exitCompletedRef.current = true
|
|
346
|
+
if (exitStartedRef.current) {
|
|
347
|
+
exitStartedRef.current = false
|
|
348
|
+
emit('end', 'exit', finished)
|
|
349
|
+
}
|
|
359
350
|
sendExitCompleteRef.current?.()
|
|
360
351
|
}
|
|
361
352
|
|
|
@@ -365,221 +356,42 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
365
356
|
return
|
|
366
357
|
}
|
|
367
358
|
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// 1. Reset to non-exit state
|
|
372
|
-
// 2. Force reflow
|
|
373
|
-
// 3. Re-apply exit state to trigger transition
|
|
359
|
+
// React can apply the exit class in the same render batch as the
|
|
360
|
+
// transition. Restart from the last mounted computed values so normal
|
|
361
|
+
// and interrupted exits both produce a concrete browser transition.
|
|
374
362
|
let rafId: number | undefined
|
|
375
|
-
|
|
376
|
-
// flag to ignore transitioncancel during reset (we intentionally cancel the old transition)
|
|
377
|
-
let ignoreCancelEvents = wasInterrupted
|
|
378
|
-
// get enter/exit styles for potential restart
|
|
379
|
-
const enterStyle = props.enterStyle as Record<string, unknown> | undefined
|
|
380
|
-
const exitStyle = props.exitStyle as Record<string, unknown> | undefined
|
|
381
|
-
|
|
382
|
-
// Build the exit transition string - needed for both normal and interrupted exits
|
|
383
|
-
const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
|
|
384
|
-
const durationOverride = normalized.config?.duration
|
|
385
|
-
const exitTransitionString = keys
|
|
386
|
-
.map((key) => {
|
|
387
|
-
const propAnimation = normalized.properties[key]
|
|
388
|
-
let animationValue: string | null = null
|
|
389
|
-
if (typeof propAnimation === 'string') {
|
|
390
|
-
animationValue = animations[propAnimation]
|
|
391
|
-
} else if (
|
|
392
|
-
propAnimation &&
|
|
393
|
-
typeof propAnimation === 'object' &&
|
|
394
|
-
propAnimation.type
|
|
395
|
-
) {
|
|
396
|
-
animationValue = animations[propAnimation.type]
|
|
397
|
-
} else if (defaultAnimation) {
|
|
398
|
-
animationValue = defaultAnimation
|
|
399
|
-
}
|
|
400
|
-
if (animationValue && durationOverride) {
|
|
401
|
-
animationValue = applyDurationOverride(animationValue, durationOverride)
|
|
402
|
-
}
|
|
403
|
-
return animationValue ? `${key} ${animationValue}${delayStr}` : null
|
|
404
|
-
})
|
|
405
|
-
.filter(Boolean)
|
|
406
|
-
.join(', ')
|
|
363
|
+
let disposed = false
|
|
407
364
|
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
lastNonExitingStyleRef.current.opacity ??
|
|
414
|
-
1
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
if (TRANSFORM_KEYS.includes(key as any)) {
|
|
418
|
-
return key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
|
|
419
|
-
}
|
|
420
|
-
return enterStyle?.[key]
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (wasInterrupted) {
|
|
424
|
-
exitInterruptedRef.current = false
|
|
425
|
-
// disable transition, reset to enter state
|
|
365
|
+
const mountedStyle = lastMountedStyleRef.current
|
|
366
|
+
const canRestart =
|
|
367
|
+
exitCSSProperties.length > 0 && Object.keys(mountedStyle).length > 0
|
|
368
|
+
let exitTarget: Record<string, string> | undefined
|
|
369
|
+
if (canRestart) {
|
|
426
370
|
node.style.transition = 'none'
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const resetStyle: Record<string, unknown> = {}
|
|
432
|
-
for (const key of Object.keys(exitStyle)) {
|
|
433
|
-
const resetValue = getResetValue(key)
|
|
434
|
-
if (resetValue !== undefined) {
|
|
435
|
-
resetStyle[key] = resetValue
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
applyStylesToNode(node, resetStyle)
|
|
439
|
-
} else {
|
|
440
|
-
// fallback if no exitStyle defined
|
|
441
|
-
node.style.opacity = '1'
|
|
442
|
-
node.style.transform = 'none'
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// force reflow
|
|
371
|
+
// With transitions disabled, the exit classes expose their final
|
|
372
|
+
// targets immediately. Capture those, then restore the mounted values.
|
|
373
|
+
exitTarget = readComputedProperties(node, exitCSSProperties)
|
|
374
|
+
applyCSSProperties(node, mountedStyle)
|
|
446
375
|
void node.offsetHeight
|
|
447
|
-
} else if (exitStyle) {
|
|
448
|
-
// For normal (non-interrupted) exits, we need to ensure the CSS transition is
|
|
449
|
-
// processed by the browser BEFORE the exitStyle takes effect. The issue is that
|
|
450
|
-
// React may have already applied exitStyle in the same render batch. To fix this:
|
|
451
|
-
// 1. Disable transition and reset to non-exit state
|
|
452
|
-
// 2. Force reflow so browser processes the reset
|
|
453
|
-
// 3. Use RAF to ensure we're in a new frame
|
|
454
|
-
// 4. Re-enable transition and apply exitStyle
|
|
455
|
-
// This mirrors the interrupted exit handling approach (which also uses RAF).
|
|
456
|
-
ignoreCancelEvents = true
|
|
457
|
-
node.style.transition = 'none'
|
|
458
|
-
|
|
459
|
-
// Reset to the active/open state (not enterStyle, which may equal exitStyle).
|
|
460
|
-
// enterStyle is the "unmounted" initial state and can share values with exitStyle
|
|
461
|
-
// (e.g., both have opacity: 0). resetting to enterStyle would mean no value change
|
|
462
|
-
// when exitStyle is applied, so the CSS transition wouldn't fire.
|
|
463
|
-
const resetStyle: Record<string, unknown> = {}
|
|
464
|
-
for (const key of Object.keys(exitStyle)) {
|
|
465
|
-
const resetValue = getResetValue(key)
|
|
466
|
-
if (resetValue !== undefined) {
|
|
467
|
-
resetStyle[key] = resetValue
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
applyStylesToNode(node, resetStyle)
|
|
471
|
-
|
|
472
|
-
// Force reflow
|
|
473
|
-
void node.offsetHeight
|
|
474
|
-
|
|
475
|
-
// Use RAF to ensure transition is applied in a new frame
|
|
476
376
|
rafId = requestAnimationFrame(() => {
|
|
477
377
|
if (cycleId !== exitCycleIdRef.current) return
|
|
478
|
-
|
|
479
|
-
node.style.transition = exitTransitionString
|
|
480
|
-
// Force reflow to ensure transition is active
|
|
378
|
+
node.style.transition = getTransition()
|
|
481
379
|
void node.offsetHeight
|
|
482
|
-
|
|
483
|
-
applyStylesToNode(node, exitStyle)
|
|
484
|
-
// Re-enable cancel event handling
|
|
485
|
-
ignoreCancelEvents = false
|
|
380
|
+
applyCSSProperties(node, exitTarget!)
|
|
486
381
|
})
|
|
487
382
|
}
|
|
488
383
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
* 2. Event handlers to stop working
|
|
496
|
-
*
|
|
497
|
-
* Fix: Calculate the MAXIMUM duration across all animated properties, not just
|
|
498
|
-
* the default. With animateOnly and per-property configs, different properties
|
|
499
|
-
* can have different durations, and we need to wait for the LONGEST one.
|
|
500
|
-
*/
|
|
501
|
-
|
|
502
|
-
// calculate max duration across all animated properties
|
|
503
|
-
let maxDuration = defaultAnimation ? extractDuration(defaultAnimation) : 200
|
|
504
|
-
|
|
505
|
-
// check per-property animation durations using shared helper
|
|
506
|
-
const animationConfigs = getAnimationConfigsForKeys(
|
|
507
|
-
normalized,
|
|
508
|
-
animations as Record<string, string>,
|
|
509
|
-
keys,
|
|
510
|
-
defaultAnimation
|
|
511
|
-
)
|
|
512
|
-
for (const animationValue of animationConfigs.values()) {
|
|
513
|
-
if (animationValue) {
|
|
514
|
-
const duration = extractDuration(animationValue)
|
|
515
|
-
if (duration > maxDuration) {
|
|
516
|
-
maxDuration = duration
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const delay = normalized.delay ?? 0
|
|
522
|
-
const fallbackTimeout = maxDuration + delay
|
|
523
|
-
|
|
524
|
-
const timeoutId = setTimeout(() => {
|
|
525
|
-
completeExit()
|
|
526
|
-
}, fallbackTimeout)
|
|
527
|
-
|
|
528
|
-
// track number of transitioning properties to wait for all to finish
|
|
529
|
-
// (each property fires its own transitionend event)
|
|
530
|
-
const transitioningProps = new Set(keys)
|
|
531
|
-
let completedCount = 0
|
|
532
|
-
|
|
533
|
-
const onFinishAnimation = (event: TransitionEvent) => {
|
|
534
|
-
// only count transitions on THIS element, not bubbled from children
|
|
535
|
-
if (event.target !== node) return
|
|
536
|
-
|
|
537
|
-
// map CSS property names to our key names
|
|
538
|
-
// e.g., transitionend fires with propertyName 'transform' for scale/x/y
|
|
539
|
-
const eventProp = event.propertyName
|
|
540
|
-
if (transitioningProps.has(eventProp) || eventProp === 'all') {
|
|
541
|
-
completedCount++
|
|
542
|
-
// wait for all properties to finish
|
|
543
|
-
if (completedCount >= transitioningProps.size) {
|
|
544
|
-
clearTimeout(timeoutId)
|
|
545
|
-
completeExit()
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// on cancel, still complete (element is exiting and animation was interrupted)
|
|
551
|
-
// the guards prevent duplicate completion if this is a stale cycle
|
|
552
|
-
const onCancelAnimation = () => {
|
|
553
|
-
// ignore cancel events during reset phase (we intentionally cancel the old transition)
|
|
554
|
-
if (ignoreCancelEvents) return
|
|
555
|
-
clearTimeout(timeoutId)
|
|
556
|
-
completeExit()
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
node.addEventListener('transitionend', onFinishAnimation)
|
|
560
|
-
node.addEventListener('transitioncancel', onCancelAnimation)
|
|
561
|
-
|
|
562
|
-
// For interrupted exits, re-enable transition and re-apply exit styles
|
|
563
|
-
// This must happen AFTER listeners are set up so we catch the transitionend
|
|
564
|
-
if (wasInterrupted) {
|
|
565
|
-
rafId = requestAnimationFrame(() => {
|
|
566
|
-
if (cycleId !== exitCycleIdRef.current) return
|
|
567
|
-
// re-enable transition using the pre-built string
|
|
568
|
-
node.style.transition = exitTransitionString
|
|
569
|
-
// force reflow again
|
|
570
|
-
void node.offsetHeight
|
|
571
|
-
// now apply exit styles - this triggers the transition
|
|
572
|
-
applyStylesToNode(node, exitStyle)
|
|
573
|
-
// re-enable cancel event handling now that reset is complete
|
|
574
|
-
ignoreCancelEvents = false
|
|
575
|
-
})
|
|
576
|
-
}
|
|
384
|
+
// wait for the browser's concrete animations. this covers `all`,
|
|
385
|
+
// transform aliases, delays, and concurrent WAAPI animations without
|
|
386
|
+
// guessing property names or maintaining a duration timer.
|
|
387
|
+
void waitForAnimations(node).then((finished) => {
|
|
388
|
+
if (!disposed) completeExit(finished)
|
|
389
|
+
})
|
|
577
390
|
|
|
578
391
|
return () => {
|
|
579
|
-
|
|
392
|
+
disposed = true
|
|
580
393
|
if (rafId !== undefined) cancelAnimationFrame(rafId)
|
|
581
|
-
node
|
|
582
|
-
node.removeEventListener('transitioncancel', onCancelAnimation)
|
|
394
|
+
clearCSSProperties(node, exitCSSProperties)
|
|
583
395
|
// restore transition: the exit handling sets node.style.transition='none'
|
|
584
396
|
// directly on the DOM (bypassing React). if exit is interrupted (e.g. same-key
|
|
585
397
|
// re-entry in AnimatePresence), React won't re-apply its managed transition
|
|
@@ -587,7 +399,88 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
587
399
|
// override lets React's value take effect again.
|
|
588
400
|
node.style.transition = ''
|
|
589
401
|
}
|
|
590
|
-
}, [isExiting])
|
|
402
|
+
}, [isExiting, exitCSSPropertiesSignature])
|
|
403
|
+
|
|
404
|
+
// signature of the animatable style, so the update effect can detect
|
|
405
|
+
// in-place style changes. the css driver applies most style values as
|
|
406
|
+
// atomic classNames (not inline style), so the signature must include the
|
|
407
|
+
// className map. only computed when a listener is attached.
|
|
408
|
+
const styleSignature = onTransition
|
|
409
|
+
? (() => {
|
|
410
|
+
const { transition: _t, ...rest } = style
|
|
411
|
+
return `${JSON.stringify(styleState?.classNames ?? null)}|${JSON.stringify(rest)}`
|
|
412
|
+
})()
|
|
413
|
+
: ''
|
|
414
|
+
|
|
415
|
+
// enter lifecycle: emit start when the enter transition kicks off, end
|
|
416
|
+
// once every animation on the node finishes (getAnimations-based, resolves
|
|
417
|
+
// immediately for zero-animation elements). the promise outlives benign
|
|
418
|
+
// re-renders because it keys off the cycle id, not the effect lifetime.
|
|
419
|
+
useIsomorphicLayoutEffect(() => {
|
|
420
|
+
const host = stateRef.current.host
|
|
421
|
+
if (!onTransitionRef.current || isExiting || !justFinishedEntering || !host) {
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
const node = host as HTMLElement
|
|
425
|
+
const cycleId = ++enterCycleIdRef.current
|
|
426
|
+
enterStartedRef.current = true
|
|
427
|
+
emit('start', 'enter')
|
|
428
|
+
void waitForAnimations(node).then((finished) => {
|
|
429
|
+
if (cycleId !== enterCycleIdRef.current || !enterStartedRef.current) return
|
|
430
|
+
enterStartedRef.current = false
|
|
431
|
+
emit('end', 'enter', finished)
|
|
432
|
+
})
|
|
433
|
+
}, [justFinishedEntering, isExiting])
|
|
434
|
+
|
|
435
|
+
// update lifecycle: a style change while mounted (not entering or exiting).
|
|
436
|
+
// a new update that supersedes an in-flight one emits end(finished:false).
|
|
437
|
+
useIsomorphicLayoutEffect(() => {
|
|
438
|
+
const host = stateRef.current.host
|
|
439
|
+
if (
|
|
440
|
+
!onTransitionRef.current ||
|
|
441
|
+
isEntering ||
|
|
442
|
+
justFinishedEntering ||
|
|
443
|
+
isExiting ||
|
|
444
|
+
!host
|
|
445
|
+
) {
|
|
446
|
+
// keep the signature current so leaving enter/exit isn't seen as an update
|
|
447
|
+
prevUpdateSigRef.current = styleSignature
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (prevUpdateSigRef.current === null) {
|
|
451
|
+
prevUpdateSigRef.current = styleSignature
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
if (styleSignature === prevUpdateSigRef.current) return
|
|
455
|
+
prevUpdateSigRef.current = styleSignature
|
|
456
|
+
|
|
457
|
+
const node = host as HTMLElement
|
|
458
|
+
if (updateInFlightRef.current) {
|
|
459
|
+
emit('end', 'update', false)
|
|
460
|
+
}
|
|
461
|
+
updateInFlightRef.current = true
|
|
462
|
+
const cycleId = ++updateCycleIdRef.current
|
|
463
|
+
emit('start', 'update')
|
|
464
|
+
void waitForAnimations(node).then((finished) => {
|
|
465
|
+
if (cycleId !== updateCycleIdRef.current) return
|
|
466
|
+
updateInFlightRef.current = false
|
|
467
|
+
emit('end', 'update', finished)
|
|
468
|
+
})
|
|
469
|
+
}, [styleSignature, isEntering, justFinishedEntering, isExiting])
|
|
470
|
+
|
|
471
|
+
// interruption: emit a finished:false end for an enter canceled by an exit,
|
|
472
|
+
// or an exit canceled by a re-enter (before its own completion fired).
|
|
473
|
+
useIsomorphicLayoutEffect(() => {
|
|
474
|
+
if (justStartedExiting && enterStartedRef.current) {
|
|
475
|
+
enterCycleIdRef.current++
|
|
476
|
+
enterStartedRef.current = false
|
|
477
|
+
emit('end', 'enter', false)
|
|
478
|
+
}
|
|
479
|
+
if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {
|
|
480
|
+
exitStartedRef.current = false
|
|
481
|
+
emit('end', 'exit', false)
|
|
482
|
+
}
|
|
483
|
+
}, [justStartedExiting, justStoppedExiting])
|
|
591
484
|
|
|
592
485
|
// tamagui doesnt even use animation output during hydration
|
|
593
486
|
if (isHydrating) {
|
|
@@ -603,38 +496,7 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
603
496
|
style.transform = transformsToString(style.transform)
|
|
604
497
|
}
|
|
605
498
|
|
|
606
|
-
|
|
607
|
-
// TODO: we disabled the transform transition, because it will create issue for inverse function and animate function
|
|
608
|
-
// for non layout transform properties either use animate function or find a workaround to do it with css
|
|
609
|
-
const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
|
|
610
|
-
const durationOverride = normalized.config?.duration
|
|
611
|
-
style.transition = keys
|
|
612
|
-
.map((key) => {
|
|
613
|
-
// Check for property-specific animation, fall back to default
|
|
614
|
-
const propAnimation = normalized.properties[key]
|
|
615
|
-
let animationValue: string | null = null
|
|
616
|
-
|
|
617
|
-
if (typeof propAnimation === 'string') {
|
|
618
|
-
animationValue = animations[propAnimation]
|
|
619
|
-
} else if (
|
|
620
|
-
propAnimation &&
|
|
621
|
-
typeof propAnimation === 'object' &&
|
|
622
|
-
propAnimation.type
|
|
623
|
-
) {
|
|
624
|
-
animationValue = animations[propAnimation.type]
|
|
625
|
-
} else if (defaultAnimation) {
|
|
626
|
-
animationValue = defaultAnimation
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Apply global duration override if specified
|
|
630
|
-
if (animationValue && durationOverride) {
|
|
631
|
-
animationValue = applyDurationOverride(animationValue, durationOverride)
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return animationValue ? `${key} ${animationValue}${delayStr}` : null
|
|
635
|
-
})
|
|
636
|
-
.filter(Boolean)
|
|
637
|
-
.join(', ')
|
|
499
|
+
style.transition = getTransition()
|
|
638
500
|
|
|
639
501
|
if (process.env.NODE_ENV === 'development' && props['debug'] === 'verbose') {
|
|
640
502
|
console.info('CSS animation', {
|
|
@@ -652,61 +514,3 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
652
514
|
},
|
|
653
515
|
}
|
|
654
516
|
}
|
|
655
|
-
|
|
656
|
-
// layout animations
|
|
657
|
-
// useIsomorphicLayoutEffect(() => {
|
|
658
|
-
// if (!host || !props.layout) {
|
|
659
|
-
// return
|
|
660
|
-
// }
|
|
661
|
-
// // @ts-ignore
|
|
662
|
-
// const boundingBox = host?.getBoundingClientRect()
|
|
663
|
-
// if (isChanged(initialPositionRef.current, boundingBox)) {
|
|
664
|
-
// const transform = invert(
|
|
665
|
-
// host,
|
|
666
|
-
// boundingBox,
|
|
667
|
-
// initialPositionRef.current
|
|
668
|
-
// )
|
|
669
|
-
|
|
670
|
-
// animate({
|
|
671
|
-
// from: transform,
|
|
672
|
-
// to: { x: 0, y: 0, scaleX: 1, scaleY: 1 },
|
|
673
|
-
// duration: 1000,
|
|
674
|
-
// onUpdate: ({ x, y, scaleX, scaleY }) => {
|
|
675
|
-
// // @ts-ignore
|
|
676
|
-
// host.style.transform = `translate(${x}px, ${y}px) scaleX(${scaleX}) scaleY(${scaleY})`
|
|
677
|
-
// // TODO: handle childRef inverse scale
|
|
678
|
-
// // childRef.current.style.transform = `scaleX(${1 / scaleX}) scaleY(${
|
|
679
|
-
// // 1 / scaleY
|
|
680
|
-
// // })`
|
|
681
|
-
// },
|
|
682
|
-
// // TODO: extract ease-in from string and convert/map it to a cubicBezier array
|
|
683
|
-
// cubicBezier: [0, 1.38, 1, -0.41],
|
|
684
|
-
// })
|
|
685
|
-
// }
|
|
686
|
-
// initialPositionRef.current = boundingBox
|
|
687
|
-
// })
|
|
688
|
-
|
|
689
|
-
// style.transition = `${keys} ${animation}${
|
|
690
|
-
// props.layout ? ',width 0s, height 0s, margin 0s, padding 0s, transform' : ''
|
|
691
|
-
// }`
|
|
692
|
-
|
|
693
|
-
// const isChanged = (initialBox: any, finalBox: any) => {
|
|
694
|
-
// // we just mounted, so we don't have complete data yet
|
|
695
|
-
// if (!initialBox || !finalBox) return false
|
|
696
|
-
|
|
697
|
-
// // deep compare the two boxes
|
|
698
|
-
// return JSON.stringify(initialBox) !== JSON.stringify(finalBox)
|
|
699
|
-
// }
|
|
700
|
-
|
|
701
|
-
// const invert = (el, from, to) => {
|
|
702
|
-
// const { x: fromX, y: fromY, width: fromWidth, height: fromHeight } = from
|
|
703
|
-
// const { x, y, width, height } = to
|
|
704
|
-
|
|
705
|
-
// const transform = {
|
|
706
|
-
// x: x - fromX - (fromWidth - width) / 2,
|
|
707
|
-
// y: y - fromY - (fromHeight - height) / 2,
|
|
708
|
-
// scaleX: width / fromWidth,
|
|
709
|
-
// scaleY: height / fromHeight,
|
|
710
|
-
// }
|
|
711
|
-
|
|
712
|
-
// el.style.transform = `
|