zumly 0.92.3 → 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.
- package/README.md +10 -2
- package/dist/zumly.cjs +7 -0
- package/dist/zumly.css +2 -3
- package/dist/zumly.js +2 -2
- package/dist/zumly.min.css +1 -1
- package/dist/zumly.mjs +2 -2
- package/docs/DRIVER_API.md +1 -1
- package/package.json +10 -6
- package/src/drivers/anime-transition.js +185 -0
- package/src/drivers/css-transition.js +261 -0
- package/src/drivers/driver-helpers.d.ts +23 -0
- package/src/drivers/driver-helpers.js +11 -3
- package/src/drivers/gsap-transition.js +149 -0
- package/src/drivers/motion-transition.js +178 -0
- package/src/drivers/none-transition.js +54 -0
- package/src/drivers/waapi-transition.js +243 -0
- package/types/zumly.d.ts +12 -4
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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. */
|
|
@@ -114,6 +116,14 @@ export interface LateralNavOptions {
|
|
|
114
116
|
keepAlive?: boolean | 'visible'
|
|
115
117
|
/** Position of the lateral nav bar. Default: 'bottom-center'. */
|
|
116
118
|
position?: 'bottom-center' | 'top-center'
|
|
119
|
+
/**
|
|
120
|
+
* Declared sibling order for lateral navigation. Takes precedence over
|
|
121
|
+
* inferring order/slide direction from trigger positions in the parent view.
|
|
122
|
+
* Array = one lateral group; map = keyed by parent view name.
|
|
123
|
+
* @example ['today', 'tomorrow', 'week']
|
|
124
|
+
* @example { home: ['today', 'tomorrow', 'week'] }
|
|
125
|
+
*/
|
|
126
|
+
siblings?: string[] | Record<string, string[]>
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
/** Depth navigation UI configuration. */
|
|
@@ -186,8 +196,6 @@ export interface ZumlyOptions {
|
|
|
186
196
|
inputs?: InputsOptions
|
|
187
197
|
/** Enable deferred rendering (view content inserted after zoom animation). */
|
|
188
198
|
deferred?: boolean
|
|
189
|
-
/** Hide trigger mode during zoom: 'fade', 'remove', or falsy. */
|
|
190
|
-
hideTrigger?: string | false
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
// ─── Events ─────────────────────────────────────────────────────────
|