zumly 0.92.4 → 0.92.6

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.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * CSS-based transition driver (default).
3
+ * Uses CSS variables and keyframe classes; completes via animationend.
4
+ * Includes safety timeout so onComplete runs even if animationend is missed.
5
+ *
6
+ * @param {Object} spec - Transition spec from the engine
7
+ * @param {function} onComplete - MUST be called exactly once when done
8
+ */
9
+ import {
10
+ parseDurationMs,
11
+ showViews,
12
+ applyZoomInEndState,
13
+ applyZoomOutPreviousState,
14
+ applyZoomOutLastState,
15
+ removeViewFromCanvas,
16
+ createFinishGuard,
17
+ SAFETY_BUFFER_MS,
18
+ } from './driver-helpers.js'
19
+
20
+ export function runTransition (spec, onComplete) {
21
+ const { type, currentView, previousView, lastView, currentStage, duration, ease, canvas } = spec
22
+
23
+ if (!currentView || !previousView || !currentStage) {
24
+ onComplete()
25
+ return
26
+ }
27
+
28
+ if (type === 'lateral') {
29
+ runLateral(spec, onComplete)
30
+ } else if (type === 'zoomIn') {
31
+ runZoomIn(currentView, previousView, lastView, currentStage, duration, ease, onComplete)
32
+ } else if (type === 'zoomOut') {
33
+ runZoomOut(currentView, previousView, lastView, currentStage, duration, ease, canvas, onComplete)
34
+ } else {
35
+ onComplete()
36
+ }
37
+ }
38
+
39
+ // ─── Lateral ─────────────────────────────────────────────────────────
40
+
41
+ function runLateral (spec, onComplete) {
42
+ const {
43
+ currentView: incomingView,
44
+ previousView: outgoingView,
45
+ backView,
46
+ backViewState,
47
+ lastView,
48
+ lastViewState,
49
+ incomingTransformStart,
50
+ incomingTransformEnd,
51
+ outgoingTransform,
52
+ outgoingTransformEnd,
53
+ currentStage,
54
+ duration,
55
+ ease,
56
+ canvas,
57
+ } = spec
58
+ const durationMs = parseDurationMs(duration)
59
+ const v0 = currentStage.views[0]
60
+
61
+ showViews(incomingView)
62
+ incomingView.classList.replace('is-new-current-view', 'is-current-view')
63
+ incomingView.classList.remove('zoom-current-view', 'has-no-events')
64
+ incomingView.style.transformOrigin = v0.forwardState.origin
65
+
66
+ // Set CSS variables and animation classes for each participating element
67
+ if (backView && backViewState) {
68
+ setCSSVars(backView, duration, ease, { '--lateral-from': backViewState.transformStart, '--lateral-to': backViewState.transformEnd })
69
+ backView.classList.add('zoom-lateral-back')
70
+ }
71
+ if (lastView && lastViewState) {
72
+ setCSSVars(lastView, duration, ease, { '--lateral-from': lastViewState.transformStart, '--lateral-to': lastViewState.transformEnd })
73
+ lastView.classList.add('zoom-lateral-back')
74
+ }
75
+
76
+ // Skip outgoing animation in 'visible' keepAlive mode
77
+ if (spec.keepAlive !== 'visible') {
78
+ setCSSVars(outgoingView, duration, ease, { '--lateral-out-from': outgoingTransform, '--lateral-out-to': outgoingTransformEnd })
79
+ outgoingView.classList.add('zoom-lateral-out')
80
+ }
81
+
82
+ setCSSVars(incomingView, duration, ease, { '--lateral-in-from': incomingTransformStart, '--lateral-in-to': incomingTransformEnd })
83
+ incomingView.classList.add('zoom-lateral-in')
84
+
85
+ // Collect all animated elements
86
+ const elements = spec.keepAlive === 'visible' ? [incomingView] : [outgoingView, incomingView]
87
+ if (backView && backViewState) elements.push(backView)
88
+ if (lastView && lastViewState) elements.push(lastView)
89
+
90
+ let pending = elements.length
91
+ const budgetMs = durationMs + SAFETY_BUFFER_MS
92
+ const zoomAnims = new Set(['zoom-lateral-in', 'zoom-lateral-out', 'zoom-lateral-back'])
93
+ const { finish, extend } = createFinishGuard(() => {
94
+ cleanupAnimListeners(elements, handleEnd, handleStart)
95
+ if (spec.keepAlive) {
96
+ outgoingView.classList.remove('zoom-lateral-out')
97
+ outgoingView.style.opacity = ''
98
+ } else {
99
+ removeViewFromCanvas(outgoingView, canvas)
100
+ }
101
+ if (backView) {
102
+ backView.classList.remove('zoom-lateral-back')
103
+ backView.style.transform = backViewState?.transformEnd || backView.style.transform
104
+ }
105
+ if (lastView) {
106
+ lastView.classList.remove('zoom-lateral-back')
107
+ lastView.style.transform = lastViewState?.transformEnd || lastView.style.transform
108
+ }
109
+ incomingView.classList.remove('zoom-lateral-in')
110
+ incomingView.style.transform = incomingTransformEnd
111
+ onComplete()
112
+ }, budgetMs)
113
+
114
+ // See runZoomIn: re-arm the deadline from the real animation start.
115
+ function handleStart (event) {
116
+ if (event.target !== event.currentTarget || !zoomAnims.has(event.animationName)) return
117
+ extend(budgetMs)
118
+ }
119
+
120
+ function handleEnd (event) {
121
+ if (event.target !== event.currentTarget || !zoomAnims.has(event.animationName)) return
122
+ const el = event.currentTarget
123
+ el.removeEventListener('animationend', handleEnd)
124
+ el.removeEventListener('animationstart', handleStart)
125
+ pending--
126
+ if (pending <= 0) finish()
127
+ }
128
+
129
+ elements.forEach(el => {
130
+ el.addEventListener('animationstart', handleStart)
131
+ el.addEventListener('animationend', handleEnd)
132
+ })
133
+ }
134
+
135
+ // ─── Zoom In ─────────────────────────────────────────────────────────
136
+
137
+ function runZoomIn (currentView, previousView, lastView, currentStage, duration, ease, onComplete) {
138
+ showViews(currentView, previousView, lastView)
139
+
140
+ const stagger = currentStage.stagger || 0
141
+
142
+ // Set CSS variables for keyframe animations
143
+ setCSSVars(currentView, duration, ease, {
144
+ '--current-view-transform-start': currentStage.views[0].backwardState.transform,
145
+ '--current-view-transform-end': currentStage.views[0].forwardState.transform,
146
+ })
147
+ if (stagger > 0) currentView.style.setProperty('animation-delay', '0ms')
148
+ setCSSVars(previousView, duration, ease, {
149
+ '--previous-view-transform-start': currentStage.views[1].backwardState.transform,
150
+ '--previous-view-transform-end': currentStage.views[1].forwardState.transform,
151
+ })
152
+ if (stagger > 0) previousView.style.setProperty('animation-delay', `${stagger}ms`)
153
+ if (lastView) {
154
+ setCSSVars(lastView, duration, ease, {
155
+ '--last-view-transform-start': currentStage.views[2].backwardState.transform,
156
+ '--last-view-transform-end': currentStage.views[2].forwardState.transform,
157
+ })
158
+ if (stagger > 0) lastView.style.setProperty('animation-delay', `${stagger * 2}ms`)
159
+ }
160
+
161
+ // Trigger animations via classes
162
+ currentView.classList.add('zoom-current-view')
163
+ previousView.classList.add('zoom-previous-view')
164
+ if (lastView) lastView.classList.add('zoom-last-view')
165
+
166
+ const elements = lastView ? [currentView, previousView, lastView] : [currentView, previousView]
167
+ let pending = elements.length
168
+ const durationMs = parseDurationMs(duration)
169
+ const maxDelay = lastView ? stagger * 2 : stagger
170
+ const budgetMs = durationMs + maxDelay + SAFETY_BUFFER_MS
171
+ const zoomAnims = new Set(['zoom-current-view', 'zoom-previous-view', 'zoom-last-view'])
172
+
173
+ const { finish, extend } = createFinishGuard(() => {
174
+ cleanupAnimListeners(elements, handleEnd, handleStart)
175
+ elements.forEach(el => el.style.removeProperty('animation-delay'))
176
+ // Safety path (missed animationend: render lag, hidden tab, removed
177
+ // element): apply the end state ourselves — never leave views stuck
178
+ // with zoom classes and has-no-events.
179
+ elements.forEach(el => {
180
+ if (el?.isConnected) {
181
+ try { applyZoomInEndState(el, currentStage) } catch (e) { /* ignore */ }
182
+ }
183
+ })
184
+ onComplete()
185
+ }, budgetMs)
186
+
187
+ // Animations begin on the next render frame, which can lag far behind this
188
+ // JS call when the incoming view is heavy (iframes, large subtrees). Re-arm
189
+ // the safety deadline from the real start so it can't fire mid-animation.
190
+ function handleStart (event) {
191
+ if (event.target !== event.currentTarget || !zoomAnims.has(event.animationName)) return
192
+ extend(budgetMs)
193
+ }
194
+
195
+ function handleEnd (event) {
196
+ // Animation events bubble: ignore animations from view content, and only
197
+ // accept this element's own zoom keyframes.
198
+ if (event.target !== event.currentTarget || !zoomAnims.has(event.animationName)) return
199
+ const el = event.currentTarget
200
+ el.removeEventListener('animationend', handleEnd)
201
+ el.removeEventListener('animationstart', handleStart)
202
+ if (el.isConnected) {
203
+ try { applyZoomInEndState(el, currentStage) } catch (e) { /* ignore */ }
204
+ }
205
+ pending--
206
+ if (pending <= 0) finish()
207
+ }
208
+
209
+ elements.forEach(el => {
210
+ el.addEventListener('animationstart', handleStart)
211
+ el.addEventListener('animationend', handleEnd)
212
+ })
213
+ }
214
+
215
+ // ─── Zoom Out ────────────────────────────────────────────────────────
216
+
217
+ function runZoomOut (currentView, previousView, lastView, currentStage, duration, ease, canvas, onComplete) {
218
+ const v0 = currentStage.views[0]
219
+ const v1 = currentStage.views[1]
220
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
221
+ const stagger = currentStage.stagger || 0
222
+
223
+ setCSSVars(currentView, duration, ease, {
224
+ '--current-view-transform-start': v0.backwardState.transform,
225
+ '--current-view-transform-end': v0.forwardState.transform,
226
+ })
227
+ if (stagger > 0) currentView.style.setProperty('animation-delay', '0ms')
228
+ setCSSVars(previousView, duration, ease, {
229
+ '--previous-view-transform-start': v1.backwardState.transform,
230
+ '--previous-view-transform-end': v1.forwardState.transform,
231
+ })
232
+ if (stagger > 0) previousView.style.setProperty('animation-delay', `${stagger}ms`)
233
+ if (lastView && v2) {
234
+ setCSSVars(lastView, duration, ease, {
235
+ '--last-view-transform-start': v2.backwardState.transform,
236
+ '--last-view-transform-end': v2.forwardState.transform,
237
+ })
238
+ if (stagger > 0) lastView.style.setProperty('animation-delay', `${stagger * 2}ms`)
239
+ }
240
+
241
+ // Trigger reverse animations
242
+ currentView.classList.add('zoom-current-view-reverse')
243
+ previousView.classList.add('zoom-previous-view-reverse')
244
+ if (lastView) lastView.classList.add('zoom-last-view-reverse')
245
+
246
+ const elements = lastView ? [currentView, previousView, lastView] : [currentView, previousView]
247
+ let pending = elements.length
248
+ const durationMs = parseDurationMs(duration)
249
+ const maxDelay = lastView ? stagger * 2 : stagger
250
+ const budgetMs = durationMs + maxDelay + SAFETY_BUFFER_MS
251
+ const zoomAnims = new Set(['zoom-current-view-reverse', 'zoom-previous-view-reverse', 'zoom-last-view-reverse'])
252
+
253
+ const { finish, extend } = createFinishGuard(() => {
254
+ cleanupAnimListeners(elements, handleEnd, handleStart)
255
+ elements.forEach(el => el.style.removeProperty('animation-delay'))
256
+ // Safety path: apply end states so the outgoing view is removed and the
257
+ // remaining views are restored even when animationend never arrived.
258
+ elements.forEach(el => {
259
+ if (el?.isConnected) {
260
+ try { applyZoomOutEndState(el, currentStage, canvas) } catch (e) { /* ignore */ }
261
+ }
262
+ })
263
+ onComplete()
264
+ }, budgetMs)
265
+
266
+ // See runZoomIn: re-arm the deadline from the real animation start.
267
+ function handleStart (event) {
268
+ if (event.target !== event.currentTarget || !zoomAnims.has(event.animationName)) return
269
+ extend(budgetMs)
270
+ }
271
+
272
+ function handleEnd (event) {
273
+ if (event.target !== event.currentTarget || !zoomAnims.has(event.animationName)) return
274
+ const el = event.currentTarget
275
+ el.removeEventListener('animationend', handleEnd)
276
+ el.removeEventListener('animationstart', handleStart)
277
+ if (el.isConnected) {
278
+ try { applyZoomOutEndState(el, currentStage, canvas) } catch (e) { /* ignore */ }
279
+ }
280
+ pending--
281
+ if (pending <= 0) finish()
282
+ }
283
+
284
+ elements.forEach(el => {
285
+ el.addEventListener('animationstart', handleStart)
286
+ el.addEventListener('animationend', handleEnd)
287
+ })
288
+ }
289
+
290
+ function applyZoomOutEndState (element, currentStage, canvas) {
291
+ if (element.classList.contains('zoom-current-view-reverse')) {
292
+ removeViewFromCanvas(element, canvas)
293
+ return
294
+ }
295
+ if (element.classList.contains('zoom-previous-view-reverse')) {
296
+ applyZoomOutPreviousState(element, currentStage.views[1].backwardState)
297
+ return
298
+ }
299
+ if (element.classList.contains('zoom-last-view-reverse')) {
300
+ applyZoomOutLastState(element, currentStage.views[2].backwardState)
301
+ }
302
+ }
303
+
304
+ // ─── Internal helpers ────────────────────────────────────────────────
305
+
306
+ function setCSSVars (el, duration, ease, vars) {
307
+ el.style.setProperty('--zoom-duration', duration)
308
+ el.style.setProperty('--zoom-ease', ease)
309
+ for (const [key, value] of Object.entries(vars)) {
310
+ el.style.setProperty(key, value)
311
+ }
312
+ }
313
+
314
+ function cleanupAnimListeners (elements, endHandler, startHandler) {
315
+ for (const el of elements) {
316
+ if (!el?.removeEventListener) continue
317
+ el.removeEventListener('animationend', endHandler)
318
+ el.removeEventListener('animationstart', startHandler)
319
+ }
320
+ }
@@ -0,0 +1,23 @@
1
+ // Type definitions for zumly/driver-helpers
2
+
3
+ import type { ZoomSnapshot, ViewState, TransitionSpec, MatrixComponents } from '../../types/zumly.js'
4
+
5
+ export function parseDurationMs(duration: string | number): number
6
+ export function parseDurationSec(duration: string | number): number
7
+ export function applyZoomInEndState(element: HTMLElement, currentStage: ZoomSnapshot): void
8
+ export function applyZoomOutPreviousState(element: HTMLElement, backwardState: ViewState): void
9
+ export function applyZoomOutLastState(element: HTMLElement, backwardState: ViewState): void
10
+ export function removeViewFromCanvas(element: HTMLElement, canvas: HTMLElement): void
11
+ export function showViews(...elements: (HTMLElement | null | undefined)[]): void
12
+ export function runLateralInstant(spec: TransitionSpec, onComplete: () => void): void
13
+ export const SAFETY_BUFFER_MS: number
14
+ export function createFinishGuard(
15
+ cleanup: () => void,
16
+ timeoutMs: number
17
+ ): { finish: () => void; safetyTimer: number }
18
+ export function identityMatrix(): MatrixComponents
19
+ export function parseMatrixString(mStr: string): MatrixComponents
20
+ export function matrixToString(m: MatrixComponents): string
21
+ export function lerp(a: number, b: number, t: number): number
22
+ export function interpolateMatrix(from: MatrixComponents, to: MatrixComponents, t: number): MatrixComponents
23
+ export function readComputedMatrix(element: HTMLElement, origin: string, transformStr: string): MatrixComponents
@@ -211,10 +211,10 @@ export const SAFETY_BUFFER_MS = 150
211
211
  *
212
212
  * @param {function} cleanup - The actual cleanup + onComplete work
213
213
  * @param {number} timeoutMs - Safety timeout duration
214
- * @returns {{ finish: function, safetyTimer: number }}
214
+ * @returns {{ finish: function, extend: function, safetyTimer: number }}
215
215
  *
216
216
  * @example
217
- * const { finish, safetyTimer } = createFinishGuard(() => {
217
+ * const { finish, extend } = createFinishGuard(() => {
218
218
  * cancelAnimations()
219
219
  * applyFinalState()
220
220
  * onComplete()
@@ -224,10 +224,13 @@ export const SAFETY_BUFFER_MS = 150
224
224
  * animation.onfinish = finish
225
225
  *
226
226
  * // The safetyTimer ensures finish() runs even if onfinish never fires.
227
+ * // Call extend(ms) when the animation actually starts to re-arm the
228
+ * // deadline from that moment (styles flush on the next render frame,
229
+ * // which can lag far behind the JS call on heavy views).
227
230
  */
228
231
  export function createFinishGuard (cleanup, timeoutMs) {
229
232
  let completed = false
230
- const safetyTimer = setTimeout(() => {
233
+ let safetyTimer = setTimeout(() => {
231
234
  if (!completed) { completed = true; cleanup() }
232
235
  }, timeoutMs)
233
236
 
@@ -238,6 +241,13 @@ export function createFinishGuard (cleanup, timeoutMs) {
238
241
  clearTimeout(safetyTimer)
239
242
  cleanup()
240
243
  },
244
+ extend (ms) {
245
+ if (completed) return
246
+ clearTimeout(safetyTimer)
247
+ safetyTimer = setTimeout(() => {
248
+ if (!completed) { completed = true; cleanup() }
249
+ }, ms)
250
+ },
241
251
  safetyTimer
242
252
  }
243
253
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * GSAP transition driver.
3
+ * Requires global `gsap` — load from CDN before use:
4
+ * <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
5
+ *
6
+ * @param {Object} spec - Transition spec from the engine
7
+ * @param {function} onComplete - MUST be called exactly once when done
8
+ */
9
+ import {
10
+ parseDurationSec,
11
+ showViews,
12
+ applyZoomInEndState,
13
+ applyZoomOutPreviousState,
14
+ applyZoomOutLastState,
15
+ removeViewFromCanvas,
16
+ runLateralInstant,
17
+ } from './driver-helpers.js'
18
+
19
+ export function runTransition (spec, onComplete) {
20
+ const gsap = typeof globalThis !== 'undefined' && globalThis.gsap
21
+ if (!gsap || typeof gsap.to !== 'function') {
22
+ console.warn('Zumly GSAP driver: GSAP not loaded. Add <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>')
23
+ onComplete()
24
+ return
25
+ }
26
+
27
+ const { type, currentView, previousView, lastView, currentStage, duration, ease, canvas } = spec
28
+ if (!currentView || !previousView || !currentStage) {
29
+ onComplete()
30
+ return
31
+ }
32
+
33
+ const durationSec = parseDurationSec(duration)
34
+
35
+ if (type === 'lateral') {
36
+ runLateralInstant(spec, onComplete)
37
+ } else if (type === 'zoomIn') {
38
+ runZoomIn(gsap, currentView, previousView, lastView, currentStage, durationSec, ease, onComplete)
39
+ } else if (type === 'zoomOut') {
40
+ runZoomOut(gsap, currentView, previousView, lastView, currentStage, durationSec, ease, canvas, onComplete)
41
+ } else {
42
+ onComplete()
43
+ }
44
+ }
45
+
46
+ // ─── Zoom In ─────────────────────────────────────────────────────────
47
+
48
+ function runZoomIn (gsap, currentView, previousView, lastView, currentStage, durationSec, ease, onComplete) {
49
+ showViews(currentView, previousView, lastView)
50
+
51
+ const v0 = currentStage.views[0]
52
+ const v1 = currentStage.views[1]
53
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
54
+
55
+ // Set initial transforms
56
+ setTransform(currentView, v0.backwardState)
57
+ setTransform(previousView, v1.backwardState)
58
+ if (v2) setTransform(lastView, v2.backwardState)
59
+
60
+ const tl = gsap.timeline({
61
+ onComplete: () => {
62
+ applyZoomInEndState(currentView, currentStage)
63
+ applyZoomInEndState(previousView, currentStage)
64
+ if (lastView) applyZoomInEndState(lastView, currentStage)
65
+ onComplete()
66
+ }
67
+ })
68
+
69
+ const gsapEase = normalizeEasing(ease)
70
+ const staggerSec = (currentStage.stagger || 0) / 1000
71
+ tl.to(currentView, { transform: v0.forwardState.transform, duration: durationSec, ease: gsapEase }, 0)
72
+ tl.to(previousView, { transform: v1.forwardState.transform, duration: durationSec, ease: gsapEase }, staggerSec)
73
+ if (v2) {
74
+ tl.to(lastView, { transform: v2.forwardState.transform, duration: durationSec, ease: gsapEase }, staggerSec * 2)
75
+ }
76
+ }
77
+
78
+ // ─── Zoom Out ────────────────────────────────────────────────────────
79
+
80
+ function runZoomOut (gsap, currentView, previousView, lastView, currentStage, durationSec, ease, canvas, onComplete) {
81
+ const v0 = currentStage.views[0]
82
+ const v1 = currentStage.views[1]
83
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
84
+ const to1 = v1.backwardState
85
+ const to2 = v2 ? v2.backwardState : null
86
+
87
+ setTransform(currentView, v0.forwardState)
88
+
89
+ const from1 = previousView.style.transform || getComputedStyle(previousView).transform || v1.forwardState.transform
90
+ const from2 = lastView && v2
91
+ ? (lastView.style.transform || getComputedStyle(lastView).transform || v2.forwardState.transform)
92
+ : null
93
+
94
+ previousView.style.transformOrigin = v1.forwardState.origin
95
+ if (lastView && v2) lastView.style.transformOrigin = v2.forwardState.origin
96
+
97
+ const gsapEase = normalizeEasing(ease)
98
+
99
+ const tl = gsap.timeline({
100
+ onComplete: () => {
101
+ removeViewFromCanvas(currentView, canvas)
102
+ applyZoomOutPreviousState(previousView, to1)
103
+ if (lastView && to2) applyZoomOutLastState(lastView, to2)
104
+ onComplete()
105
+ }
106
+ })
107
+
108
+ const staggerSec = (currentStage.stagger || 0) / 1000
109
+
110
+ // Set initial transforms so views hold position during stagger delay
111
+ gsap.set(previousView, { transform: from1 })
112
+ if (lastView && from2) gsap.set(lastView, { transform: from2 })
113
+
114
+ tl.fromTo(currentView,
115
+ { transform: v0.forwardState.transform },
116
+ { transform: v0.backwardState.transform, duration: durationSec, ease: gsapEase },
117
+ 0
118
+ )
119
+ tl.fromTo(previousView,
120
+ { transform: from1 },
121
+ { transform: to1.transform, duration: durationSec, ease: gsapEase },
122
+ staggerSec
123
+ )
124
+
125
+ if (lastView && to2) {
126
+ tl.fromTo(lastView,
127
+ { transform: from2 },
128
+ { transform: to2.transform, duration: durationSec, ease: gsapEase },
129
+ staggerSec * 2
130
+ )
131
+ }
132
+ }
133
+
134
+ // ─── Helpers ─────────────────────────────────────────────────────────
135
+
136
+ function setTransform (el, state) {
137
+ el.style.transformOrigin = state.origin
138
+ el.style.transform = state.transform
139
+ }
140
+
141
+ function normalizeEasing (ease) {
142
+ if (typeof ease !== 'string') return 'power2.inOut'
143
+ const s = ease.toLowerCase()
144
+ if (s === 'linear') return 'none'
145
+ if (s.includes('ease-in-out')) return 'power2.inOut'
146
+ if (s.includes('ease-in')) return 'power2.in'
147
+ if (s.includes('ease-out')) return 'power2.out'
148
+ return s
149
+ }