zumly 0.92.4 → 0.92.5

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,261 @@
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 { finish } = createFinishGuard(() => {
92
+ cleanupListeners(elements, handleEnd)
93
+ if (spec.keepAlive) {
94
+ outgoingView.classList.remove('zoom-lateral-out')
95
+ outgoingView.style.opacity = ''
96
+ } else {
97
+ removeViewFromCanvas(outgoingView, canvas)
98
+ }
99
+ if (backView) {
100
+ backView.classList.remove('zoom-lateral-back')
101
+ backView.style.transform = backViewState?.transformEnd || backView.style.transform
102
+ }
103
+ if (lastView) {
104
+ lastView.classList.remove('zoom-lateral-back')
105
+ lastView.style.transform = lastViewState?.transformEnd || lastView.style.transform
106
+ }
107
+ incomingView.classList.remove('zoom-lateral-in')
108
+ incomingView.style.transform = incomingTransformEnd
109
+ onComplete()
110
+ }, durationMs + SAFETY_BUFFER_MS)
111
+
112
+ function handleEnd (event) {
113
+ const el = event?.target
114
+ if (el) el.removeEventListener('animationend', handleEnd)
115
+ pending--
116
+ if (pending <= 0) finish()
117
+ }
118
+
119
+ elements.forEach(el => el.addEventListener('animationend', handleEnd))
120
+ }
121
+
122
+ // ─── Zoom In ─────────────────────────────────────────────────────────
123
+
124
+ function runZoomIn (currentView, previousView, lastView, currentStage, duration, ease, onComplete) {
125
+ showViews(currentView, previousView, lastView)
126
+
127
+ const stagger = currentStage.stagger || 0
128
+
129
+ // Set CSS variables for keyframe animations
130
+ setCSSVars(currentView, duration, ease, {
131
+ '--current-view-transform-start': currentStage.views[0].backwardState.transform,
132
+ '--current-view-transform-end': currentStage.views[0].forwardState.transform,
133
+ })
134
+ if (stagger > 0) currentView.style.setProperty('animation-delay', '0ms')
135
+ setCSSVars(previousView, duration, ease, {
136
+ '--previous-view-transform-start': currentStage.views[1].backwardState.transform,
137
+ '--previous-view-transform-end': currentStage.views[1].forwardState.transform,
138
+ })
139
+ if (stagger > 0) previousView.style.setProperty('animation-delay', `${stagger}ms`)
140
+ if (lastView) {
141
+ setCSSVars(lastView, duration, ease, {
142
+ '--last-view-transform-start': currentStage.views[2].backwardState.transform,
143
+ '--last-view-transform-end': currentStage.views[2].forwardState.transform,
144
+ })
145
+ if (stagger > 0) lastView.style.setProperty('animation-delay', `${stagger * 2}ms`)
146
+ }
147
+
148
+ // Trigger animations via classes
149
+ currentView.classList.add('zoom-current-view')
150
+ previousView.classList.add('zoom-previous-view')
151
+ if (lastView) lastView.classList.add('zoom-last-view')
152
+
153
+ const elements = lastView ? [currentView, previousView, lastView] : [currentView, previousView]
154
+ let pending = elements.length
155
+ const durationMs = parseDurationMs(duration)
156
+ const maxDelay = lastView ? stagger * 2 : stagger
157
+
158
+ const { finish } = createFinishGuard(() => {
159
+ cleanupListeners(elements, handleEnd)
160
+ // Clean up animation-delay
161
+ elements.forEach(el => el.style.removeProperty('animation-delay'))
162
+ onComplete()
163
+ }, durationMs + maxDelay + SAFETY_BUFFER_MS)
164
+
165
+ function handleEnd (event) {
166
+ const el = event?.target
167
+ if (el) el.removeEventListener('animationend', handleEnd)
168
+ if (el?.isConnected) {
169
+ try { applyZoomInEndState(el, currentStage) } catch (e) { /* ignore */ }
170
+ }
171
+ pending--
172
+ if (pending <= 0) finish()
173
+ }
174
+
175
+ elements.forEach(el => el.addEventListener('animationend', handleEnd))
176
+ }
177
+
178
+ // ─── Zoom Out ────────────────────────────────────────────────────────
179
+
180
+ function runZoomOut (currentView, previousView, lastView, currentStage, duration, ease, canvas, onComplete) {
181
+ const v0 = currentStage.views[0]
182
+ const v1 = currentStage.views[1]
183
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
184
+ const stagger = currentStage.stagger || 0
185
+
186
+ setCSSVars(currentView, duration, ease, {
187
+ '--current-view-transform-start': v0.backwardState.transform,
188
+ '--current-view-transform-end': v0.forwardState.transform,
189
+ })
190
+ if (stagger > 0) currentView.style.setProperty('animation-delay', '0ms')
191
+ setCSSVars(previousView, duration, ease, {
192
+ '--previous-view-transform-start': v1.backwardState.transform,
193
+ '--previous-view-transform-end': v1.forwardState.transform,
194
+ })
195
+ if (stagger > 0) previousView.style.setProperty('animation-delay', `${stagger}ms`)
196
+ if (lastView && v2) {
197
+ setCSSVars(lastView, duration, ease, {
198
+ '--last-view-transform-start': v2.backwardState.transform,
199
+ '--last-view-transform-end': v2.forwardState.transform,
200
+ })
201
+ if (stagger > 0) lastView.style.setProperty('animation-delay', `${stagger * 2}ms`)
202
+ }
203
+
204
+ // Trigger reverse animations
205
+ currentView.classList.add('zoom-current-view-reverse')
206
+ previousView.classList.add('zoom-previous-view-reverse')
207
+ if (lastView) lastView.classList.add('zoom-last-view-reverse')
208
+
209
+ const elements = lastView ? [currentView, previousView, lastView] : [currentView, previousView]
210
+ let pending = elements.length
211
+ const durationMs = parseDurationMs(duration)
212
+ const maxDelay = lastView ? stagger * 2 : stagger
213
+
214
+ const { finish } = createFinishGuard(() => {
215
+ cleanupListeners(elements, handleEnd)
216
+ elements.forEach(el => el.style.removeProperty('animation-delay'))
217
+ onComplete()
218
+ }, durationMs + maxDelay + SAFETY_BUFFER_MS)
219
+
220
+ function handleEnd (event) {
221
+ const el = event?.target
222
+ if (el) el.removeEventListener('animationend', handleEnd)
223
+ if (el?.isConnected) {
224
+ try { applyZoomOutEndState(el, currentStage, canvas) } catch (e) { /* ignore */ }
225
+ }
226
+ pending--
227
+ if (pending <= 0) finish()
228
+ }
229
+
230
+ elements.forEach(el => el.addEventListener('animationend', handleEnd))
231
+ }
232
+
233
+ function applyZoomOutEndState (element, currentStage, canvas) {
234
+ if (element.classList.contains('zoom-current-view-reverse')) {
235
+ removeViewFromCanvas(element, canvas)
236
+ return
237
+ }
238
+ if (element.classList.contains('zoom-previous-view-reverse')) {
239
+ applyZoomOutPreviousState(element, currentStage.views[1].backwardState)
240
+ return
241
+ }
242
+ if (element.classList.contains('zoom-last-view-reverse')) {
243
+ applyZoomOutLastState(element, currentStage.views[2].backwardState)
244
+ }
245
+ }
246
+
247
+ // ─── Internal helpers ────────────────────────────────────────────────
248
+
249
+ function setCSSVars (el, duration, ease, vars) {
250
+ el.style.setProperty('--zoom-duration', duration)
251
+ el.style.setProperty('--zoom-ease', ease)
252
+ for (const [key, value] of Object.entries(vars)) {
253
+ el.style.setProperty(key, value)
254
+ }
255
+ }
256
+
257
+ function cleanupListeners (elements, handler) {
258
+ for (const el of elements) {
259
+ if (el?.removeEventListener) el.removeEventListener('animationend', handler)
260
+ }
261
+ }
@@ -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
@@ -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
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Motion (motion.dev) transition driver.
3
+ * Interpolates computed transform matrices during animation.
4
+ * Load Motion before use:
5
+ * <script src="https://cdn.jsdelivr.net/npm/motion@11/dist/motion.min.js"></script>
6
+ *
7
+ * @param {Object} spec - Transition spec from the engine
8
+ * @param {function} onComplete - MUST be called exactly once when done
9
+ */
10
+ import {
11
+ parseDurationSec,
12
+ showViews,
13
+ applyZoomInEndState,
14
+ applyZoomOutPreviousState,
15
+ applyZoomOutLastState,
16
+ removeViewFromCanvas,
17
+ runLateralInstant,
18
+ readComputedMatrix,
19
+ interpolateMatrix,
20
+ matrixToString,
21
+ } from './driver-helpers.js'
22
+
23
+ export function runTransition (spec, onComplete) {
24
+ const animate = getMotionAnimate()
25
+ if (!animate || typeof animate !== 'function') {
26
+ console.warn('Zumly Motion driver: Motion not loaded. Add <script src="https://cdn.jsdelivr.net/npm/motion@11/dist/motion.min.js"></script>')
27
+ onComplete()
28
+ return
29
+ }
30
+
31
+ const { type, currentView, previousView, lastView, currentStage, duration, ease, canvas } = spec
32
+ if (!currentView || !previousView || !currentStage) {
33
+ onComplete()
34
+ return
35
+ }
36
+
37
+ const durationSec = parseDurationSec(duration)
38
+
39
+ if (type === 'lateral') {
40
+ runLateralInstant(spec, onComplete)
41
+ } else if (type === 'zoomIn') {
42
+ runZoomIn(animate, currentView, previousView, lastView, currentStage, durationSec, ease, onComplete)
43
+ } else if (type === 'zoomOut') {
44
+ runZoomOut(animate, currentView, previousView, lastView, currentStage, durationSec, ease, canvas, onComplete)
45
+ } else {
46
+ onComplete()
47
+ }
48
+ }
49
+
50
+ // ─── Zoom In ─────────────────────────────────────────────────────────
51
+
52
+ function runZoomIn (animate, currentView, previousView, lastView, currentStage, durationSec, ease, onComplete) {
53
+ showViews(currentView, previousView, lastView)
54
+
55
+ const matrices = computeMatrixPairs(
56
+ currentView, previousView, lastView, currentStage, 'forward'
57
+ )
58
+ applyMatricesAtProgress(matrices, 0)
59
+
60
+ const stagger = currentStage.stagger || 0
61
+ const staggerSec = stagger / 1000
62
+ const durationMs = durationSec * 1000
63
+ const totalSec = durationSec + (matrices.length > 2 ? staggerSec * 2 : staggerSec)
64
+
65
+ const controls = animate(0, 1, {
66
+ duration: totalSec,
67
+ ease: normalizeEasing(ease),
68
+ onUpdate: t => {
69
+ const elapsed = t * totalSec * 1000
70
+ applyStaggeredMatrices(matrices, elapsed, durationMs, stagger)
71
+ },
72
+ })
73
+
74
+ controls.then(() => {
75
+ applyZoomInEndState(currentView, currentStage)
76
+ applyZoomInEndState(previousView, currentStage)
77
+ if (lastView) applyZoomInEndState(lastView, currentStage)
78
+ onComplete()
79
+ }).catch(() => onComplete())
80
+ }
81
+
82
+ // ─── Zoom Out ────────────────────────────────────────────────────────
83
+
84
+ function runZoomOut (animate, currentView, previousView, lastView, currentStage, durationSec, ease, canvas, onComplete) {
85
+ const v1 = currentStage.views[1]
86
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
87
+ const to1 = v1.backwardState
88
+ const to2 = v2 ? v2.backwardState : null
89
+
90
+ const matrices = computeMatrixPairs(
91
+ currentView, previousView, lastView, currentStage, 'backward'
92
+ )
93
+ applyMatricesAtProgress(matrices, 0)
94
+
95
+ const stagger = currentStage.stagger || 0
96
+ const staggerSec = stagger / 1000
97
+ const durationMs = durationSec * 1000
98
+ const totalSec = durationSec + (matrices.length > 2 ? staggerSec * 2 : staggerSec)
99
+
100
+ const controls = animate(0, 1, {
101
+ duration: totalSec,
102
+ ease: normalizeEasing(ease),
103
+ onUpdate: t => {
104
+ const elapsed = t * totalSec * 1000
105
+ applyStaggeredMatrices(matrices, elapsed, durationMs, stagger)
106
+ },
107
+ })
108
+
109
+ controls.then(() => {
110
+ removeViewFromCanvas(currentView, canvas)
111
+ applyZoomOutPreviousState(previousView, to1)
112
+ if (lastView && to2) applyZoomOutLastState(lastView, to2)
113
+ onComplete()
114
+ }).catch(() => onComplete())
115
+ }
116
+
117
+ // ─── Matrix computation ──────────────────────────────────────────────
118
+
119
+ function computeMatrixPairs (currentView, previousView, lastView, currentStage, direction) {
120
+ const v0 = currentStage.views[0]
121
+ const v1 = currentStage.views[1]
122
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
123
+
124
+ const entries = [
125
+ { el: currentView, backward: v0.backwardState, forward: v0.forwardState },
126
+ { el: previousView, backward: v1.backwardState, forward: v1.forwardState },
127
+ ]
128
+ if (v2) entries.push({ el: lastView, backward: v2.backwardState, forward: v2.forwardState })
129
+
130
+ return entries.map(({ el, backward, forward }) => {
131
+ if (direction === 'forward') {
132
+ return {
133
+ el,
134
+ from: readComputedMatrix(el, backward.origin, backward.transform),
135
+ to: readComputedMatrix(el, backward.origin, forward.transform),
136
+ }
137
+ } else {
138
+ return {
139
+ el,
140
+ from: readComputedMatrix(el, forward.origin, forward.transform),
141
+ to: readComputedMatrix(el, forward.origin, backward.transform),
142
+ }
143
+ }
144
+ })
145
+ }
146
+
147
+ function applyMatricesAtProgress (matrices, t) {
148
+ for (const { el, from, to } of matrices) {
149
+ el.style.transform = matrixToString(interpolateMatrix(from, to, t))
150
+ }
151
+ }
152
+
153
+ function applyStaggeredMatrices (matrices, elapsed, durationMs, stagger) {
154
+ for (let i = 0; i < matrices.length; i++) {
155
+ const { el, from, to } = matrices[i]
156
+ const delay = i * stagger
157
+ const localElapsed = Math.max(0, elapsed - delay)
158
+ const t = durationMs > 0 ? Math.min(1, localElapsed / durationMs) : 1
159
+ el.style.transform = matrixToString(interpolateMatrix(from, to, t))
160
+ }
161
+ }
162
+
163
+ // ─── Helpers ─────────────────────────────────────────────────────────
164
+
165
+ function getMotionAnimate () {
166
+ const g = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : {}
167
+ return g.motion?.animate || g.Motion?.animate || g.animate
168
+ }
169
+
170
+ function normalizeEasing (ease) {
171
+ if (typeof ease !== 'string') return 'easeInOut'
172
+ const s = ease.toLowerCase()
173
+ if (s === 'linear') return 'linear'
174
+ if (s.includes('ease-in-out')) return 'easeInOut'
175
+ if (s.includes('ease-in')) return 'easeIn'
176
+ if (s.includes('ease-out')) return 'easeOut'
177
+ return ease
178
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * No-animation transition driver.
3
+ * Applies final state immediately and calls onComplete synchronously.
4
+ * Useful for tests, instant UX, or reduced-motion preference.
5
+ *
6
+ * This is the simplest possible Zumly driver — a good starting point
7
+ * for writing your own. See docs/DRIVER_API.md for the full guide.
8
+ *
9
+ * @param {Object} spec - Transition spec from the engine
10
+ * @param {function} onComplete - MUST be called exactly once when done
11
+ */
12
+ import {
13
+ showViews,
14
+ applyZoomInEndState,
15
+ applyZoomOutPreviousState,
16
+ applyZoomOutLastState,
17
+ removeViewFromCanvas,
18
+ runLateralInstant,
19
+ } from './driver-helpers.js'
20
+
21
+ export function runTransition (spec, onComplete) {
22
+ const { type, currentView, previousView, lastView, currentStage, canvas } = spec
23
+
24
+ if (!currentView || !previousView || !currentStage) {
25
+ onComplete()
26
+ return
27
+ }
28
+
29
+ if (type === 'lateral') {
30
+ runLateralInstant(spec, onComplete)
31
+ return
32
+ }
33
+
34
+ if (type === 'zoomIn') {
35
+ showViews(currentView, previousView, lastView)
36
+ applyZoomInEndState(currentView, currentStage)
37
+ applyZoomInEndState(previousView, currentStage)
38
+ if (lastView) applyZoomInEndState(lastView, currentStage)
39
+ onComplete()
40
+ return
41
+ }
42
+
43
+ if (type === 'zoomOut') {
44
+ removeViewFromCanvas(currentView, canvas)
45
+ showViews(previousView, lastView)
46
+ applyZoomOutPreviousState(previousView, currentStage.views[1].backwardState)
47
+ if (lastView) applyZoomOutLastState(lastView, currentStage.views[2].backwardState)
48
+ onComplete()
49
+ return
50
+ }
51
+
52
+ // Unknown type — still must call onComplete
53
+ onComplete()
54
+ }