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.
- package/README.md +7 -1
- package/dist/zumly.cjs +7 -0
- package/dist/zumly.css +1 -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 +320 -0
- package/src/drivers/driver-helpers.d.ts +23 -0
- package/src/drivers/driver-helpers.js +13 -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 +4 -4
|
@@ -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
|
-
|
|
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. */
|
|
@@ -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 ─────────────────────────────────────────────────────────
|