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,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
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Web Animations API transition driver.
3
+ * Uses element.animate() — no external dependencies.
4
+ *
5
+ * @param {Object} spec - Transition spec from the engine
6
+ * @param {function} onComplete - MUST be called exactly once when done
7
+ */
8
+ import {
9
+ parseDurationMs,
10
+ showViews,
11
+ applyZoomInEndState,
12
+ applyZoomOutPreviousState,
13
+ applyZoomOutLastState,
14
+ removeViewFromCanvas,
15
+ createFinishGuard,
16
+ SAFETY_BUFFER_MS,
17
+ } from './driver-helpers.js'
18
+
19
+ // Re-export for tests that import it directly
20
+ export { parseDurationMs }
21
+
22
+ export function runTransition (spec, onComplete) {
23
+ const { type, currentView, previousView, lastView, currentStage, duration, ease, canvas } = spec
24
+
25
+ if (!currentView || !previousView || !currentStage) {
26
+ onComplete()
27
+ return
28
+ }
29
+
30
+ const durationMs = parseDurationMs(duration)
31
+
32
+ if (type === 'lateral') {
33
+ runLateralAnimated(spec, durationMs, ease, onComplete)
34
+ return
35
+ }
36
+
37
+ if (type === 'zoomIn') {
38
+ runZoomIn(currentView, previousView, lastView, currentStage, durationMs, ease, onComplete)
39
+ return
40
+ }
41
+
42
+ if (type === 'zoomOut') {
43
+ runZoomOut(currentView, previousView, lastView, currentStage, durationMs, ease, canvas, onComplete)
44
+ return
45
+ }
46
+
47
+ onComplete()
48
+ }
49
+
50
+ // ─── Zoom In ─────────────────────────────────────────────────────────
51
+
52
+ function runZoomIn (currentView, previousView, lastView, currentStage, durationMs, ease, onComplete) {
53
+ showViews(currentView, previousView, lastView)
54
+
55
+ const v0 = currentStage.views[0]
56
+ const v1 = currentStage.views[1]
57
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
58
+
59
+ // Set initial state from snapshot
60
+ currentView.style.transformOrigin = v0.backwardState.origin
61
+ currentView.style.transform = v0.backwardState.transform
62
+ previousView.style.transformOrigin = v1.backwardState.origin
63
+ previousView.style.transform = v1.backwardState.transform
64
+ if (v2) {
65
+ lastView.style.transformOrigin = v2.backwardState.origin
66
+ lastView.style.transform = v2.backwardState.transform
67
+ }
68
+
69
+ // Start WAAPI animations with stagger delay
70
+ const stagger = currentStage.stagger || 0
71
+ const anims = []
72
+ anims.push(currentView.animate(
73
+ [{ transform: v0.backwardState.transform }, { transform: v0.forwardState.transform }],
74
+ { duration: durationMs, easing: ease, fill: 'forwards' }
75
+ ))
76
+ anims.push(previousView.animate(
77
+ [{ transform: v1.backwardState.transform }, { transform: v1.forwardState.transform }],
78
+ { duration: durationMs, delay: stagger, easing: ease, fill: 'forwards' }
79
+ ))
80
+ if (v2) {
81
+ anims.push(lastView.animate(
82
+ [{ transform: v2.backwardState.transform }, { transform: v2.forwardState.transform }],
83
+ { duration: durationMs, delay: stagger * 2, easing: ease, fill: 'forwards' }
84
+ ))
85
+ }
86
+
87
+ const maxDelay = v2 ? stagger * 2 : stagger
88
+ const { finish } = createFinishGuard(() => {
89
+ cancelAll(anims)
90
+ try {
91
+ applyZoomInEndState(currentView, currentStage)
92
+ applyZoomInEndState(previousView, currentStage)
93
+ if (lastView) applyZoomInEndState(lastView, currentStage)
94
+ } catch (e) { /* DOM cleanup should not block onComplete */ }
95
+ onComplete()
96
+ }, durationMs + maxDelay + SAFETY_BUFFER_MS)
97
+
98
+ Promise.all(anims.map(a => a.finished)).then(finish).catch(finish)
99
+ }
100
+
101
+ // ─── Zoom Out ────────────────────────────────────────────────────────
102
+
103
+ function runZoomOut (currentView, previousView, lastView, currentStage, durationMs, ease, canvas, onComplete) {
104
+ const v0 = currentStage.views[0]
105
+ const v1 = currentStage.views[1]
106
+ const v2 = lastView && currentStage.views[2] ? currentStage.views[2] : null
107
+ const to1 = v1.backwardState
108
+ const to2 = v2 ? v2.backwardState : null
109
+
110
+ // Set current view initial state
111
+ currentView.style.transformOrigin = v0.forwardState.origin
112
+ currentView.style.transform = v0.forwardState.transform
113
+
114
+ // Defer animation start to ensure computed styles are flushed
115
+ requestAnimationFrame(() => {
116
+ requestAnimationFrame(() => {
117
+ previousView.style.transformOrigin = v1.forwardState.origin
118
+ if (lastView && v2) lastView.style.transformOrigin = v2.forwardState.origin
119
+
120
+ // Use computed transform as "from" so animation runs from actual position
121
+ const from1 = previousView.style.transform || getComputedStyle(previousView).transform || v1.forwardState.transform
122
+ const from2 = lastView && v2
123
+ ? (lastView.style.transform || getComputedStyle(lastView).transform || v2.forwardState.transform)
124
+ : null
125
+
126
+ const stagger = currentStage.stagger || 0
127
+ const anims = []
128
+ anims.push(currentView.animate(
129
+ [{ transform: v0.forwardState.transform }, { transform: v0.backwardState.transform }],
130
+ { duration: durationMs, easing: ease, fill: 'forwards' }
131
+ ))
132
+ anims.push(previousView.animate(
133
+ [{ transform: from1 }, { transform: to1.transform }],
134
+ { duration: durationMs, delay: stagger, easing: ease, fill: 'both' }
135
+ ))
136
+ if (lastView && to2) {
137
+ anims.push(lastView.animate(
138
+ [{ transform: from2 }, { transform: to2.transform }],
139
+ { duration: durationMs, delay: stagger * 2, easing: ease, fill: 'both' }
140
+ ))
141
+ }
142
+
143
+ // Let WAAPI drive transforms — remove inline styles so WAAPI fill takes over
144
+ previousView.style.removeProperty('transform')
145
+ if (lastView) lastView.style.removeProperty('transform')
146
+
147
+ const maxDelay = (lastView && to2) ? stagger * 2 : stagger
148
+ const { finish } = createFinishGuard(() => {
149
+ cancelAll(anims)
150
+ try {
151
+ removeViewFromCanvas(currentView, canvas)
152
+ applyZoomOutPreviousState(previousView, to1)
153
+ if (lastView && to2) applyZoomOutLastState(lastView, to2)
154
+ } catch (e) { /* DOM cleanup should not block onComplete */ }
155
+ onComplete()
156
+ }, durationMs + maxDelay + SAFETY_BUFFER_MS)
157
+
158
+ Promise.all(anims.map(a => a.finished)).then(finish).catch(finish)
159
+ })
160
+ })
161
+ }
162
+
163
+ // ─── Lateral ────────────────────────────────────────────────────────
164
+
165
+ function runLateralAnimated (spec, durationMs, ease, onComplete) {
166
+ const {
167
+ currentView: incomingView,
168
+ previousView: outgoingView,
169
+ backView,
170
+ backViewState,
171
+ lastView,
172
+ lastViewState,
173
+ incomingTransformStart,
174
+ incomingTransformEnd,
175
+ outgoingTransform,
176
+ outgoingTransformEnd,
177
+ currentStage,
178
+ canvas
179
+ } = spec
180
+ const v0 = currentStage.views[0]
181
+
182
+ showViews(incomingView)
183
+ incomingView.classList.replace('is-new-current-view', 'is-current-view')
184
+ incomingView.classList.remove('zoom-current-view', 'has-no-events')
185
+ incomingView.style.transformOrigin = v0.forwardState.origin
186
+
187
+ const anims = []
188
+
189
+ // Incoming view slides in
190
+ anims.push(incomingView.animate(
191
+ [{ transform: incomingTransformStart, opacity: 0 }, { transform: incomingTransformEnd, opacity: 1 }],
192
+ { duration: durationMs, easing: ease, fill: 'forwards' }
193
+ ))
194
+
195
+ // Outgoing view slides out (skip animation in 'visible' keepAlive mode)
196
+ if (spec.keepAlive !== 'visible') {
197
+ anims.push(outgoingView.animate(
198
+ [{ transform: outgoingTransform, opacity: 1 }, { transform: outgoingTransformEnd, opacity: 0 }],
199
+ { duration: durationMs, easing: ease, fill: 'forwards' }
200
+ ))
201
+ }
202
+
203
+ // Back view shifts
204
+ if (backView && backViewState) {
205
+ anims.push(backView.animate(
206
+ [{ transform: backViewState.transformStart }, { transform: backViewState.transformEnd }],
207
+ { duration: durationMs, easing: ease, fill: 'forwards' }
208
+ ))
209
+ }
210
+
211
+ // Last view shifts
212
+ if (lastView && lastViewState) {
213
+ anims.push(lastView.animate(
214
+ [{ transform: lastViewState.transformStart }, { transform: lastViewState.transformEnd }],
215
+ { duration: durationMs, easing: ease, fill: 'forwards' }
216
+ ))
217
+ }
218
+
219
+ const { finish } = createFinishGuard(() => {
220
+ cancelAll(anims)
221
+ // Apply final state
222
+ incomingView.style.transform = incomingTransformEnd || v0.forwardState.transform
223
+ if (backView && backViewState) backView.style.transform = backViewState.transformEnd
224
+ if (lastView && lastViewState) lastView.style.transform = lastViewState.transformEnd
225
+ if (spec.keepAlive) {
226
+ outgoingView.style.opacity = ''
227
+ outgoingView.style.transform = outgoingTransformEnd
228
+ } else {
229
+ removeViewFromCanvas(outgoingView, canvas)
230
+ }
231
+ onComplete()
232
+ }, durationMs + SAFETY_BUFFER_MS)
233
+
234
+ Promise.all(anims.map(a => a.finished)).then(finish).catch(finish)
235
+ }
236
+
237
+ // ─── Helpers ─────────────────────────────────────────────────────────
238
+
239
+ function cancelAll (anims) {
240
+ for (const a of anims) {
241
+ try { a.cancel() } catch (e) { /* already finished or removed */ }
242
+ }
243
+ }
package/types/zumly.d.ts CHANGED
@@ -77,7 +77,7 @@ export interface ViewEntry {
77
77
 
78
78
  /** Zoom snapshot stored in the storedViews stack. */
79
79
  export interface ZoomSnapshot {
80
- level: number
80
+ zoomLevel: number
81
81
  views: [ViewEntry, ViewEntry, ViewEntry | null, ViewEntry | null]
82
82
  scale?: number
83
83
  stagger?: number
@@ -98,8 +98,10 @@ export interface TransitionOptions {
98
98
  effects?: [string] | [string, string]
99
99
  /** Progressive delay (ms) between view layers during zoom. Default: 0. */
100
100
  stagger?: number
101
- /** Parallax intensity (0-1). Default: 0. */
101
+ /** Accepted for compatibility but not applied yet (always treated as 0). */
102
102
  parallax?: number
103
+ /** Hide the trigger element during zoom: 'fade' (crossfade), true (hide), or false. Default: false. */
104
+ hideTrigger?: 'fade' | boolean
103
105
  }
104
106
 
105
107
  /** Lateral navigation UI configuration. */
@@ -194,8 +196,6 @@ export interface ZumlyOptions {
194
196
  inputs?: InputsOptions
195
197
  /** Enable deferred rendering (view content inserted after zoom animation). */
196
198
  deferred?: boolean
197
- /** Hide trigger mode during zoom: 'fade', 'remove', or falsy. */
198
- hideTrigger?: string | false
199
199
  }
200
200
 
201
201
  // ─── Events ─────────────────────────────────────────────────────────