@teleporthq/teleport-shared 0.43.69 → 0.43.71
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/__tests__/motion-runtime/motion-engine.ts +102 -0
- package/__tests__/motion-runtime/scroll-video-engine.ts +28 -0
- package/__tests__/scroll-scene/story-lanes.ts +126 -0
- package/__tests__/utils/inline-script-error-guard.ts +121 -0
- package/dist/cjs/index.d.ts +6 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/motion-runtime/index.d.ts +3 -0
- package/dist/cjs/motion-runtime/index.d.ts.map +1 -0
- package/dist/cjs/motion-runtime/index.js +8 -0
- package/dist/cjs/motion-runtime/index.js.map +1 -0
- package/dist/cjs/motion-runtime/motion-engine-source.d.ts +16 -0
- package/dist/cjs/motion-runtime/motion-engine-source.d.ts.map +1 -0
- package/dist/cjs/motion-runtime/motion-engine-source.js +20 -0
- package/dist/cjs/motion-runtime/motion-engine-source.js.map +1 -0
- package/dist/cjs/motion-runtime/scroll-video-engine-source.d.ts +9 -0
- package/dist/cjs/motion-runtime/scroll-video-engine-source.d.ts.map +1 -0
- package/dist/cjs/motion-runtime/scroll-video-engine-source.js +13 -0
- package/dist/cjs/motion-runtime/scroll-video-engine-source.js.map +1 -0
- package/dist/cjs/page-transition/index.d.ts +4 -0
- package/dist/cjs/page-transition/index.d.ts.map +1 -0
- package/dist/cjs/page-transition/index.js +20 -0
- package/dist/cjs/page-transition/index.js.map +1 -0
- package/dist/cjs/page-transition/morph-source.d.ts +20 -0
- package/dist/cjs/page-transition/morph-source.d.ts.map +1 -0
- package/dist/cjs/page-transition/morph-source.js +25 -0
- package/dist/cjs/page-transition/morph-source.js.map +1 -0
- package/dist/cjs/page-transition/variants.d.ts +87 -0
- package/dist/cjs/page-transition/variants.d.ts.map +1 -0
- package/dist/cjs/page-transition/variants.js +330 -0
- package/dist/cjs/page-transition/variants.js.map +1 -0
- package/dist/cjs/page-transition/view-transition-css.d.ts +21 -0
- package/dist/cjs/page-transition/view-transition-css.d.ts.map +1 -0
- package/dist/cjs/page-transition/view-transition-css.js +188 -0
- package/dist/cjs/page-transition/view-transition-css.js.map +1 -0
- package/dist/cjs/scroll-scene/chapter-index.d.ts +15 -0
- package/dist/cjs/scroll-scene/chapter-index.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/chapter-index.js +30 -0
- package/dist/cjs/scroll-scene/chapter-index.js.map +1 -0
- package/dist/cjs/scroll-scene/controller-source.d.ts +26 -0
- package/dist/cjs/scroll-scene/controller-source.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/controller-source.js +34 -0
- package/dist/cjs/scroll-scene/controller-source.js.map +1 -0
- package/dist/cjs/scroll-scene/engine-source.d.ts +2 -0
- package/dist/cjs/scroll-scene/engine-source.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/engine-source.js +26 -0
- package/dist/cjs/scroll-scene/engine-source.js.map +1 -0
- package/dist/cjs/scroll-scene/index.d.ts +7 -0
- package/dist/cjs/scroll-scene/index.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/index.js +22 -0
- package/dist/cjs/scroll-scene/index.js.map +1 -0
- package/dist/cjs/scroll-scene/moment.d.ts +21 -0
- package/dist/cjs/scroll-scene/moment.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/moment.js +50 -0
- package/dist/cjs/scroll-scene/moment.js.map +1 -0
- package/dist/cjs/scroll-scene/points.d.ts +23 -0
- package/dist/cjs/scroll-scene/points.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/points.js +51 -0
- package/dist/cjs/scroll-scene/points.js.map +1 -0
- package/dist/cjs/scroll-scene/unclip.d.ts +15 -0
- package/dist/cjs/scroll-scene/unclip.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/unclip.js +44 -0
- package/dist/cjs/scroll-scene/unclip.js.map +1 -0
- package/dist/cjs/state-bindings/index.d.ts +74 -0
- package/dist/cjs/state-bindings/index.d.ts.map +1 -0
- package/dist/cjs/state-bindings/index.js +193 -0
- package/dist/cjs/state-bindings/index.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/inline-script-error-guard.d.ts +29 -0
- package/dist/cjs/utils/inline-script-error-guard.d.ts.map +1 -0
- package/dist/cjs/utils/inline-script-error-guard.js +38 -0
- package/dist/cjs/utils/inline-script-error-guard.js.map +1 -0
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/motion-runtime/index.d.ts +3 -0
- package/dist/esm/motion-runtime/index.d.ts.map +1 -0
- package/dist/esm/motion-runtime/index.js +3 -0
- package/dist/esm/motion-runtime/index.js.map +1 -0
- package/dist/esm/motion-runtime/motion-engine-source.d.ts +16 -0
- package/dist/esm/motion-runtime/motion-engine-source.d.ts.map +1 -0
- package/dist/esm/motion-runtime/motion-engine-source.js +16 -0
- package/dist/esm/motion-runtime/motion-engine-source.js.map +1 -0
- package/dist/esm/motion-runtime/scroll-video-engine-source.d.ts +9 -0
- package/dist/esm/motion-runtime/scroll-video-engine-source.d.ts.map +1 -0
- package/dist/esm/motion-runtime/scroll-video-engine-source.js +9 -0
- package/dist/esm/motion-runtime/scroll-video-engine-source.js.map +1 -0
- package/dist/esm/page-transition/index.d.ts +4 -0
- package/dist/esm/page-transition/index.d.ts.map +1 -0
- package/dist/esm/page-transition/index.js +4 -0
- package/dist/esm/page-transition/index.js.map +1 -0
- package/dist/esm/page-transition/morph-source.d.ts +20 -0
- package/dist/esm/page-transition/morph-source.d.ts.map +1 -0
- package/dist/esm/page-transition/morph-source.js +21 -0
- package/dist/esm/page-transition/morph-source.js.map +1 -0
- package/dist/esm/page-transition/variants.d.ts +87 -0
- package/dist/esm/page-transition/variants.d.ts.map +1 -0
- package/dist/esm/page-transition/variants.js +318 -0
- package/dist/esm/page-transition/variants.js.map +1 -0
- package/dist/esm/page-transition/view-transition-css.d.ts +21 -0
- package/dist/esm/page-transition/view-transition-css.d.ts.map +1 -0
- package/dist/esm/page-transition/view-transition-css.js +161 -0
- package/dist/esm/page-transition/view-transition-css.js.map +1 -0
- package/dist/esm/scroll-scene/chapter-index.d.ts +15 -0
- package/dist/esm/scroll-scene/chapter-index.d.ts.map +1 -0
- package/dist/esm/scroll-scene/chapter-index.js +26 -0
- package/dist/esm/scroll-scene/chapter-index.js.map +1 -0
- package/dist/esm/scroll-scene/controller-source.d.ts +26 -0
- package/dist/esm/scroll-scene/controller-source.d.ts.map +1 -0
- package/dist/esm/scroll-scene/controller-source.js +26 -0
- package/dist/esm/scroll-scene/controller-source.js.map +1 -0
- package/dist/esm/scroll-scene/engine-source.d.ts +2 -0
- package/dist/esm/scroll-scene/engine-source.d.ts.map +1 -0
- package/dist/esm/scroll-scene/engine-source.js +22 -0
- package/dist/esm/scroll-scene/engine-source.js.map +1 -0
- package/dist/esm/scroll-scene/index.d.ts +7 -0
- package/dist/esm/scroll-scene/index.d.ts.map +1 -0
- package/dist/esm/scroll-scene/index.js +7 -0
- package/dist/esm/scroll-scene/index.js.map +1 -0
- package/dist/esm/scroll-scene/moment.d.ts +21 -0
- package/dist/esm/scroll-scene/moment.d.ts.map +1 -0
- package/dist/esm/scroll-scene/moment.js +46 -0
- package/dist/esm/scroll-scene/moment.js.map +1 -0
- package/dist/esm/scroll-scene/points.d.ts +23 -0
- package/dist/esm/scroll-scene/points.d.ts.map +1 -0
- package/dist/esm/scroll-scene/points.js +45 -0
- package/dist/esm/scroll-scene/points.js.map +1 -0
- package/dist/esm/scroll-scene/unclip.d.ts +15 -0
- package/dist/esm/scroll-scene/unclip.d.ts.map +1 -0
- package/dist/esm/scroll-scene/unclip.js +40 -0
- package/dist/esm/scroll-scene/unclip.js.map +1 -0
- package/dist/esm/state-bindings/index.d.ts +74 -0
- package/dist/esm/state-bindings/index.d.ts.map +1 -0
- package/dist/esm/state-bindings/index.js +176 -0
- package/dist/esm/state-bindings/index.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/inline-script-error-guard.d.ts +29 -0
- package/dist/esm/utils/inline-script-error-guard.d.ts.map +1 -0
- package/dist/esm/utils/inline-script-error-guard.js +34 -0
- package/dist/esm/utils/inline-script-error-guard.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +10 -0
- package/src/motion-runtime/index.ts +2 -0
- package/src/motion-runtime/motion-engine-source.ts +289 -0
- package/src/motion-runtime/scroll-video-engine-source.ts +591 -0
- package/src/page-transition/index.ts +3 -0
- package/src/page-transition/morph-source.ts +86 -0
- package/src/page-transition/variants.ts +407 -0
- package/src/page-transition/view-transition-css.ts +237 -0
- package/src/scroll-scene/chapter-index.ts +25 -0
- package/src/scroll-scene/controller-source.ts +451 -0
- package/src/scroll-scene/engine-source.ts +330 -0
- package/src/scroll-scene/index.ts +12 -0
- package/src/scroll-scene/moment.ts +64 -0
- package/src/scroll-scene/points.ts +46 -0
- package/src/scroll-scene/unclip.ts +44 -0
- package/src/state-bindings/index.ts +207 -0
- package/src/utils/inline-script-error-guard.ts +51 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three states of a route transition for a preset, and the preset the
|
|
3
|
+
* back button plays. Mirrored VERBATIM from teleport-gui's
|
|
4
|
+
* `constants/primitives/page-transition.ts` — the settings picker demonstrates
|
|
5
|
+
* these exact states. Plain JS inside the bodies on purpose: their compiled
|
|
6
|
+
* source is injected into the generated `TqPageTransition` wrapper with
|
|
7
|
+
* `toString()`, so no template literals, no backticks, no helpers from outside
|
|
8
|
+
* the two functions.
|
|
9
|
+
*/
|
|
10
|
+
export interface PageTransitionContext {
|
|
11
|
+
reverse: boolean
|
|
12
|
+
originX: string
|
|
13
|
+
originY: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PageTransitionPanel {
|
|
17
|
+
from: Record<string, unknown>
|
|
18
|
+
cover: Record<string, unknown>
|
|
19
|
+
away: Record<string, unknown>
|
|
20
|
+
side?: 'left' | 'right'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PageTransitionVariants {
|
|
24
|
+
initial: Record<string, unknown>
|
|
25
|
+
animate: Record<string, unknown>
|
|
26
|
+
exit: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function reversePageTransitionPreset(preset: string): string {
|
|
30
|
+
const pairs: Record<string, string> = {
|
|
31
|
+
'slide-up': 'slide-down',
|
|
32
|
+
'slide-down': 'slide-up',
|
|
33
|
+
'slide-left': 'slide-right',
|
|
34
|
+
'slide-right': 'slide-left',
|
|
35
|
+
'zoom-in': 'zoom-out',
|
|
36
|
+
'zoom-out': 'zoom-in',
|
|
37
|
+
'wipe-down': 'wipe-up',
|
|
38
|
+
'wipe-up': 'wipe-down',
|
|
39
|
+
'wipe-left': 'wipe-right',
|
|
40
|
+
'wipe-right': 'wipe-left',
|
|
41
|
+
stack: 'unstack',
|
|
42
|
+
unstack: 'stack',
|
|
43
|
+
}
|
|
44
|
+
return pairs[preset] || preset
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PageTransitionCustomState {
|
|
48
|
+
opacity: number
|
|
49
|
+
x: number
|
|
50
|
+
y: number
|
|
51
|
+
scale: number
|
|
52
|
+
blur: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PageTransitionCustom {
|
|
56
|
+
durationSeconds: number
|
|
57
|
+
easing: string
|
|
58
|
+
arrive: PageTransitionCustomState
|
|
59
|
+
leave: PageTransitionCustomState
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function pageTransitionVariants(
|
|
63
|
+
preset: string,
|
|
64
|
+
slidePx: number,
|
|
65
|
+
context?: PageTransitionContext,
|
|
66
|
+
options?: Record<string, string>,
|
|
67
|
+
custom?: PageTransitionCustom
|
|
68
|
+
): PageTransitionVariants | null {
|
|
69
|
+
const reverse = context ? context.reverse : false
|
|
70
|
+
const originX = context ? context.originX : '50%'
|
|
71
|
+
const originY = context ? context.originY : '50%'
|
|
72
|
+
const name = reverse ? reversePageTransitionPreset(preset) : preset
|
|
73
|
+
const bold = options
|
|
74
|
+
? options.distance === 'bold' || options.depth === 'bold' || options.amount === 'bold'
|
|
75
|
+
: false
|
|
76
|
+
const near = bold ? slidePx * 4 : slidePx
|
|
77
|
+
const far = near * 2
|
|
78
|
+
const blurPx = options && options.softness === 'heavy' ? 28 : 12
|
|
79
|
+
const shrink = bold ? 0.85 : 0.94
|
|
80
|
+
const zoomOut = bold ? 0.85 : 0.96
|
|
81
|
+
const zoomIn = bold ? 1.15 : 1.04
|
|
82
|
+
if (name === 'custom') {
|
|
83
|
+
// Neutral in the middle; the two ends are exactly the user's values.
|
|
84
|
+
const c = custom || {
|
|
85
|
+
arrive: { opacity: 0, x: 0, y: 24, scale: 1, blur: 0 },
|
|
86
|
+
leave: { opacity: 0, x: 0, y: -24, scale: 1, blur: 0 },
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
initial: {
|
|
90
|
+
opacity: c.arrive.opacity,
|
|
91
|
+
x: c.arrive.x,
|
|
92
|
+
y: c.arrive.y,
|
|
93
|
+
scale: c.arrive.scale,
|
|
94
|
+
filter: 'blur(' + c.arrive.blur + 'px)',
|
|
95
|
+
},
|
|
96
|
+
animate: { opacity: 1, x: 0, y: 0, scale: 1, filter: 'blur(0px)' },
|
|
97
|
+
exit: {
|
|
98
|
+
opacity: c.leave.opacity,
|
|
99
|
+
x: c.leave.x,
|
|
100
|
+
y: c.leave.y,
|
|
101
|
+
scale: c.leave.scale,
|
|
102
|
+
filter: 'blur(' + c.leave.blur + 'px)',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (name === 'fade') {
|
|
107
|
+
return { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }
|
|
108
|
+
}
|
|
109
|
+
if (name === 'crossfade') {
|
|
110
|
+
return {
|
|
111
|
+
initial: { opacity: 0, scale: 1.02 },
|
|
112
|
+
animate: { opacity: 1, scale: 1 },
|
|
113
|
+
exit: { opacity: 0, scale: 0.98 },
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (name === 'blur') {
|
|
117
|
+
return {
|
|
118
|
+
initial: { opacity: 0, filter: 'blur(' + blurPx + 'px)' },
|
|
119
|
+
animate: { opacity: 1, filter: 'blur(0px)' },
|
|
120
|
+
exit: { opacity: 0, filter: 'blur(' + blurPx + 'px)' },
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (name === 'slide-up') {
|
|
124
|
+
return {
|
|
125
|
+
initial: { opacity: 0, y: near },
|
|
126
|
+
animate: { opacity: 1, y: 0 },
|
|
127
|
+
exit: { opacity: 0, y: -near },
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (name === 'slide-down') {
|
|
131
|
+
return {
|
|
132
|
+
initial: { opacity: 0, y: -near },
|
|
133
|
+
animate: { opacity: 1, y: 0 },
|
|
134
|
+
exit: { opacity: 0, y: near },
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (name === 'slide-left') {
|
|
138
|
+
return {
|
|
139
|
+
initial: { opacity: 0, x: far },
|
|
140
|
+
animate: { opacity: 1, x: 0 },
|
|
141
|
+
exit: { opacity: 0, x: -far },
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (name === 'slide-right') {
|
|
145
|
+
return {
|
|
146
|
+
initial: { opacity: 0, x: -far },
|
|
147
|
+
animate: { opacity: 1, x: 0 },
|
|
148
|
+
exit: { opacity: 0, x: far },
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (name === 'stack') {
|
|
152
|
+
return {
|
|
153
|
+
initial: { opacity: 0, y: far },
|
|
154
|
+
animate: { opacity: 1, y: 0, scale: 1 },
|
|
155
|
+
exit: { opacity: 0, scale: shrink },
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (name === 'unstack') {
|
|
159
|
+
return {
|
|
160
|
+
initial: { opacity: 0, scale: shrink },
|
|
161
|
+
animate: { opacity: 1, y: 0, scale: 1 },
|
|
162
|
+
exit: { opacity: 0, y: far },
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (name === 'zoom-in') {
|
|
166
|
+
return {
|
|
167
|
+
initial: { opacity: 0, scale: zoomOut },
|
|
168
|
+
animate: { opacity: 1, scale: 1 },
|
|
169
|
+
exit: { opacity: 0, scale: zoomIn },
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (name === 'zoom-out') {
|
|
173
|
+
return {
|
|
174
|
+
initial: { opacity: 0, scale: zoomIn },
|
|
175
|
+
animate: { opacity: 1, scale: 1 },
|
|
176
|
+
exit: { opacity: 0, scale: zoomOut },
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (name === 'wipe-down') {
|
|
180
|
+
return {
|
|
181
|
+
initial: { clipPath: 'inset(0 0 100% 0)' },
|
|
182
|
+
animate: { clipPath: 'inset(0 0 0% 0)' },
|
|
183
|
+
exit: { clipPath: 'inset(100% 0 0 0)' },
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (name === 'wipe-up') {
|
|
187
|
+
return {
|
|
188
|
+
initial: { clipPath: 'inset(100% 0 0 0)' },
|
|
189
|
+
animate: { clipPath: 'inset(0% 0 0 0)' },
|
|
190
|
+
exit: { clipPath: 'inset(0 0 100% 0)' },
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (name === 'wipe-left') {
|
|
194
|
+
return {
|
|
195
|
+
initial: { clipPath: 'inset(0 0 0 100%)' },
|
|
196
|
+
animate: { clipPath: 'inset(0 0 0 0%)' },
|
|
197
|
+
exit: { clipPath: 'inset(0 100% 0 0)' },
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (name === 'wipe-right') {
|
|
201
|
+
return {
|
|
202
|
+
initial: { clipPath: 'inset(0 100% 0 0)' },
|
|
203
|
+
animate: { clipPath: 'inset(0 0% 0 0)' },
|
|
204
|
+
exit: { clipPath: 'inset(0 0 0 100%)' },
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (name === 'circle') {
|
|
208
|
+
const at = ' at ' + originX + ' ' + originY + ')'
|
|
209
|
+
return {
|
|
210
|
+
initial: { clipPath: 'circle(0%' + at },
|
|
211
|
+
animate: { clipPath: 'circle(150%' + at },
|
|
212
|
+
exit: { clipPath: 'circle(0%' + at },
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (name === 'cover' || name === 'curtain') {
|
|
216
|
+
// The panels do the work; the page itself only has to STAY while it is
|
|
217
|
+
// being covered. An exit that changes nothing would end instantly, so it
|
|
218
|
+
// moves opacity by an invisible amount — that is what keeps the leaving
|
|
219
|
+
// page on screen for exactly one panel sweep.
|
|
220
|
+
return { initial: { opacity: 1 }, animate: { opacity: 1 }, exit: { opacity: 0.999 } }
|
|
221
|
+
}
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function pageTransitionCover(preset: string): PageTransitionPanel[] | null {
|
|
226
|
+
if (preset === 'cover') {
|
|
227
|
+
return [{ from: { y: '100%' }, cover: { y: '0%' }, away: { y: '-100%' } }]
|
|
228
|
+
}
|
|
229
|
+
if (preset === 'curtain') {
|
|
230
|
+
return [
|
|
231
|
+
{ from: { x: '-100%' }, cover: { x: '0%' }, away: { x: '-100%' }, side: 'left' },
|
|
232
|
+
{ from: { x: '100%' }, cover: { x: '0%' }, away: { x: '100%' }, side: 'right' },
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The matcher for a route a page opted out of transitions with: exact path,
|
|
240
|
+
* optional trailing slash, and `[param]` segments (how the editor spells a
|
|
241
|
+
* details page) matching any single segment. Injected into the wrapper.
|
|
242
|
+
*/
|
|
243
|
+
export function pageTransitionSkipPattern(route: string): RegExp {
|
|
244
|
+
const escaped = route.replace(/[.*+?^$()|{}\\]/g, '\\$&')
|
|
245
|
+
return new RegExp('^' + escaped.replace(/\[[^\]]+\]/g, '[^/]+') + '/?$')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** How far a page travels while sliding, in px — pages move, they do not fly. */
|
|
249
|
+
export const PAGE_TRANSITION_SLIDE_PX = 24
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The preset's choices with gaps filled by defaults and unknown words replaced
|
|
253
|
+
* by the default. Mirrored verbatim from teleport-gui; runs at generation time.
|
|
254
|
+
*/
|
|
255
|
+
export function resolvePageTransitionOptions(
|
|
256
|
+
preset: string,
|
|
257
|
+
chosen?: Record<string, string>
|
|
258
|
+
): Record<string, string> {
|
|
259
|
+
const catalog: Record<string, Array<[string, string[]]>> = {
|
|
260
|
+
blur: [['softness', ['light', 'heavy']]],
|
|
261
|
+
'slide-up': [['distance', ['subtle', 'bold']]],
|
|
262
|
+
'slide-down': [['distance', ['subtle', 'bold']]],
|
|
263
|
+
'slide-left': [['distance', ['subtle', 'bold']]],
|
|
264
|
+
'slide-right': [['distance', ['subtle', 'bold']]],
|
|
265
|
+
stack: [['depth', ['subtle', 'bold']]],
|
|
266
|
+
'zoom-in': [['amount', ['subtle', 'bold']]],
|
|
267
|
+
'zoom-out': [['amount', ['subtle', 'bold']]],
|
|
268
|
+
circle: [['origin', ['click', 'center']]],
|
|
269
|
+
cover: [['color', ['brand', 'dark', 'light']]],
|
|
270
|
+
curtain: [['color', ['brand', 'dark', 'light']]],
|
|
271
|
+
}
|
|
272
|
+
const resolved: Record<string, string> = {}
|
|
273
|
+
const entries = catalog[preset] || []
|
|
274
|
+
for (let i = 0; i < entries.length; i++) {
|
|
275
|
+
const key = entries[i][0]
|
|
276
|
+
const words = entries[i][1]
|
|
277
|
+
const value = chosen ? chosen[key] : undefined
|
|
278
|
+
const known =
|
|
279
|
+
value !== undefined &&
|
|
280
|
+
(words.indexOf(value) >= 0 || (key === 'color' && isPageTransitionColorValue(value)))
|
|
281
|
+
resolved[key] = known ? (value as string) : words[0]
|
|
282
|
+
}
|
|
283
|
+
return resolved
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A colour a cover panel may carry: a project token id, a hex colour, or
|
|
288
|
+
* rgb/hsl with plain numbers — the value lands verbatim in generated code.
|
|
289
|
+
* Mirrored verbatim from teleport-gui.
|
|
290
|
+
*/
|
|
291
|
+
export function isPageTransitionColorValue(value: string | undefined): boolean {
|
|
292
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 120) {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
return (
|
|
296
|
+
/^token:[a-z0-9_-]+$/i.test(value) ||
|
|
297
|
+
/^--[a-z0-9_-]+$/i.test(value) ||
|
|
298
|
+
/^#[0-9a-f]{3,8}$/i.test(value) ||
|
|
299
|
+
/^(rgb|rgba|hsl|hsla)\([0-9.,%\s]+\)$/i.test(value)
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The CSS colour a cover-family panel paints with: token → var(), literal as is, words → theme tokens. Mirrored verbatim from teleport-gui. */
|
|
304
|
+
export function pageTransitionCoverColor(color: string | undefined): string {
|
|
305
|
+
const dark = 'var(--dl-color-theme-neutral-dark, #111111)'
|
|
306
|
+
if (color === 'dark') {
|
|
307
|
+
return dark
|
|
308
|
+
}
|
|
309
|
+
if (color === 'light') {
|
|
310
|
+
return 'var(--dl-color-theme-neutral-light, #ffffff)'
|
|
311
|
+
}
|
|
312
|
+
if (
|
|
313
|
+
color &&
|
|
314
|
+
color !== 'brand' &&
|
|
315
|
+
color.indexOf('token:') !== 0 &&
|
|
316
|
+
isPageTransitionColorValue(color)
|
|
317
|
+
) {
|
|
318
|
+
return color.indexOf('--') === 0 ? 'var(' + color + ', ' + dark + ')' : color
|
|
319
|
+
}
|
|
320
|
+
return 'var(--dl-color-theme-primary1, ' + dark + ')'
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const CUSTOM_EASINGS = [
|
|
324
|
+
'ease',
|
|
325
|
+
'ease-in',
|
|
326
|
+
'ease-out',
|
|
327
|
+
'ease-in-out',
|
|
328
|
+
'linear',
|
|
329
|
+
'spring',
|
|
330
|
+
'back',
|
|
331
|
+
'bounce',
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
const DEFAULT_CUSTOM: PageTransitionCustom = {
|
|
335
|
+
durationSeconds: 0.35,
|
|
336
|
+
easing: 'ease-out',
|
|
337
|
+
arrive: { opacity: 0, x: 0, y: 24, scale: 1, blur: 0 },
|
|
338
|
+
leave: { opacity: 0, x: 0, y: -24, scale: 1, blur: 0 },
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const clamp = (value: unknown, min: number, max: number, fallback: number): number => {
|
|
342
|
+
const n = typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
343
|
+
return Math.min(max, Math.max(min, n))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const sanitizeCustomState = (
|
|
347
|
+
input: Partial<PageTransitionCustomState> | undefined,
|
|
348
|
+
fallback: PageTransitionCustomState
|
|
349
|
+
): PageTransitionCustomState => ({
|
|
350
|
+
opacity: clamp(input?.opacity, 0, 1, fallback.opacity),
|
|
351
|
+
x: clamp(input?.x, -400, 400, fallback.x),
|
|
352
|
+
y: clamp(input?.y, -400, 400, fallback.y),
|
|
353
|
+
scale: clamp(input?.scale, 0.25, 2, fallback.scale),
|
|
354
|
+
blur: clamp(input?.blur, 0, 60, fallback.blur),
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
/** Every custom value inside its range and a known easing; mirrored verbatim from teleport-gui. Runs at generation time. */
|
|
358
|
+
export function sanitizePageTransitionCustom(
|
|
359
|
+
input: Partial<PageTransitionCustom> | undefined
|
|
360
|
+
): PageTransitionCustom {
|
|
361
|
+
const easing =
|
|
362
|
+
typeof input?.easing === 'string' && CUSTOM_EASINGS.indexOf(input.easing) >= 0
|
|
363
|
+
? input.easing
|
|
364
|
+
: DEFAULT_CUSTOM.easing
|
|
365
|
+
return {
|
|
366
|
+
durationSeconds: clamp(input?.durationSeconds, 0.05, 3, DEFAULT_CUSTOM.durationSeconds),
|
|
367
|
+
easing,
|
|
368
|
+
arrive: sanitizeCustomState(input?.arrive, DEFAULT_CUSTOM.arrive),
|
|
369
|
+
leave: sanitizeCustomState(input?.leave, DEFAULT_CUSTOM.leave),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** What the editor exports under `globals.settings.pageTransition`. */
|
|
374
|
+
export interface PageTransitionConfig {
|
|
375
|
+
preset: string
|
|
376
|
+
duration: number
|
|
377
|
+
easing: string
|
|
378
|
+
skipRoutes?: string[]
|
|
379
|
+
options?: Record<string, string>
|
|
380
|
+
custom?: { arrive?: Record<string, number>; leave?: Record<string, number> }
|
|
381
|
+
/** False when the author turned the flying pictures off; absent means they fly. */
|
|
382
|
+
flyingPictures?: boolean
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** The same curves the motion widget plays, so a route transition shares the site's motion character. */
|
|
386
|
+
export const PAGE_TRANSITION_EASING_CURVES: Record<string, [number, number, number, number]> = {
|
|
387
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
388
|
+
'ease-in': [0.42, 0, 1, 1],
|
|
389
|
+
'ease-out': [0, 0, 0.58, 1],
|
|
390
|
+
'ease-in-out': [0.42, 0, 0.58, 1],
|
|
391
|
+
linear: [0, 0, 1, 1],
|
|
392
|
+
spring: [0.34, 1.56, 0.64, 1],
|
|
393
|
+
back: [0.68, -0.6, 0.32, 1.6],
|
|
394
|
+
bounce: [0.22, 1.2, 0.36, 1],
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** The resolved transition the editor exported, or null when pages switch instantly. */
|
|
398
|
+
export const projectPageTransition = (uidl: {
|
|
399
|
+
globals?: { settings?: unknown }
|
|
400
|
+
}): PageTransitionConfig | null => {
|
|
401
|
+
const settings = (uidl.globals?.settings || {}) as { pageTransition?: PageTransitionConfig }
|
|
402
|
+
const transition = settings.pageTransition
|
|
403
|
+
if (!transition || !transition.preset || transition.preset === 'none') {
|
|
404
|
+
return null
|
|
405
|
+
}
|
|
406
|
+
return transition
|
|
407
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as PageTransition from './variants'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A site's page transition as View Transitions CSS. The static HTML export
|
|
5
|
+
* plays it between two ordinary pages (cross-document View Transitions, no
|
|
6
|
+
* script driving it); the Next export uses the same rules for the one kind of
|
|
7
|
+
* navigation it plays as a View Transition, an image flying from a list into
|
|
8
|
+
* its details page (see morph-source.ts).
|
|
9
|
+
*
|
|
10
|
+
* The states are not written here. They come from the same registry the Next
|
|
11
|
+
* export's TqPageTransition is generated from (teleport-shared), evaluated once
|
|
12
|
+
* at generation time, so a preset looks the same in both exports and a new
|
|
13
|
+
* preset reaches both. What differs is only who plays them:
|
|
14
|
+
*
|
|
15
|
+
* Next AnimatePresence, mode "wait": the leaving page runs its exit, then
|
|
16
|
+
* the arriving page runs initial → animate.
|
|
17
|
+
* HTML the browser holds a picture of the leaving page and one of the
|
|
18
|
+
* arriving page; the first animates over [0, D], the second waits D
|
|
19
|
+
* and animates over [D, 2D]. Same order, same timing.
|
|
20
|
+
*
|
|
21
|
+
* The reveals (Circle, the Wipes) are the exception, in both exports: the
|
|
22
|
+
* arriving page is revealed OVER the leaving one, which holds still underneath
|
|
23
|
+
* for the whole run. Played one after the other, the leaving page was wiped
|
|
24
|
+
* away to the site's bare background and the arriving one wiped in over it —
|
|
25
|
+
* a white sheet between every two pages. Here the leaving picture keeps no
|
|
26
|
+
* animation (the browser would otherwise fade it out under the arriving one)
|
|
27
|
+
* and the arriving picture opens over [0, D].
|
|
28
|
+
*
|
|
29
|
+
* A browser without the feature (Firefox today) simply navigates, which is
|
|
30
|
+
* also what a visitor who asked for less motion gets.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
type State = Record<string, unknown>
|
|
34
|
+
|
|
35
|
+
export const BACK_ATTR = 'data-tq-nav'
|
|
36
|
+
export const ORIGIN_X = '--tq-origin-x'
|
|
37
|
+
export const ORIGIN_Y = '--tq-origin-y'
|
|
38
|
+
|
|
39
|
+
const hasAny = (state: State, keys: string[]): boolean =>
|
|
40
|
+
keys.some((key) => state[key] !== undefined)
|
|
41
|
+
|
|
42
|
+
/** One keyframe. Both ends of an animation declare the same properties, so every one of them interpolates. */
|
|
43
|
+
const declarations = (state: State, other: State): string[] => {
|
|
44
|
+
const lines: string[] = []
|
|
45
|
+
const both = (keys: string[]) => hasAny(state, keys) || hasAny(other, keys)
|
|
46
|
+
if (both(['opacity'])) {
|
|
47
|
+
lines.push(`opacity: ${state.opacity ?? 1};`)
|
|
48
|
+
}
|
|
49
|
+
if (both(['x', 'y', 'scale'])) {
|
|
50
|
+
lines.push(
|
|
51
|
+
`transform: translate(${state.x ?? 0}px, ${state.y ?? 0}px) scale(${state.scale ?? 1});`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
if (both(['filter'])) {
|
|
55
|
+
lines.push(`filter: ${state.filter ?? 'blur(0px)'};`)
|
|
56
|
+
}
|
|
57
|
+
if (both(['clipPath'])) {
|
|
58
|
+
lines.push(`clip-path: ${state.clipPath ?? 'inset(0 0 0 0)'};`)
|
|
59
|
+
}
|
|
60
|
+
return lines
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const keyframes = (name: string, from: State, to: State): string =>
|
|
64
|
+
`@keyframes ${name} {\n from { ${declarations(from, to).join(' ')} }\n to { ${declarations(
|
|
65
|
+
to,
|
|
66
|
+
from
|
|
67
|
+
).join(' ')} }\n}`
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The cover family in the Next export is one or two coloured panels that close
|
|
71
|
+
* over the leaving page and open on the arriving one. A picture of a page can
|
|
72
|
+
* be clipped, so here the panel is what shows where the picture is clipped
|
|
73
|
+
* away: the colour sits behind both pictures and the clip edge is the panel's
|
|
74
|
+
* leading edge. Same sweep, same colour, no extra element on any page.
|
|
75
|
+
*/
|
|
76
|
+
const COVER_CLIPS: Record<string, { covered: string; opening: string }> = {
|
|
77
|
+
// one panel rising from the bottom, then leaving through the top
|
|
78
|
+
cover: { covered: 'inset(0 0 100% 0)', opening: 'inset(100% 0 0 0)' },
|
|
79
|
+
// two panels meeting in the middle, then parting
|
|
80
|
+
curtain: { covered: 'inset(0 50% 0 50%)', opening: 'inset(0 50% 0 50%)' },
|
|
81
|
+
}
|
|
82
|
+
const OPEN = 'inset(0 0 0 0)'
|
|
83
|
+
|
|
84
|
+
interface Animations {
|
|
85
|
+
leave: [State, State]
|
|
86
|
+
arrive: [State, State]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const animationsFor = (
|
|
90
|
+
preset: string,
|
|
91
|
+
context: PageTransition.PageTransitionContext,
|
|
92
|
+
options: Record<string, string>,
|
|
93
|
+
custom: PageTransition.PageTransitionCustom | undefined
|
|
94
|
+
): Animations | null => {
|
|
95
|
+
const cover = COVER_CLIPS[preset]
|
|
96
|
+
if (cover) {
|
|
97
|
+
return {
|
|
98
|
+
leave: [{ clipPath: OPEN }, { clipPath: cover.covered }],
|
|
99
|
+
arrive: [{ clipPath: cover.opening }, { clipPath: OPEN }],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const variants = PageTransition.pageTransitionVariants(
|
|
103
|
+
preset,
|
|
104
|
+
PageTransition.PAGE_TRANSITION_SLIDE_PX,
|
|
105
|
+
context,
|
|
106
|
+
options,
|
|
107
|
+
custom
|
|
108
|
+
)
|
|
109
|
+
if (!variants) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
leave: [variants.animate, variants.exit],
|
|
114
|
+
arrive: [variants.initial, variants.animate],
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The name of the one image that flies from the leaving page into the arriving page. */
|
|
119
|
+
export const MORPH_NAME = 'tq-morph'
|
|
120
|
+
|
|
121
|
+
export interface ViewTransitionPlan {
|
|
122
|
+
css: string
|
|
123
|
+
/** The back button plays a different preset, so the page has to tell the stylesheet which way it came. */
|
|
124
|
+
needsDirection: boolean
|
|
125
|
+
/** The reveal grows from where the visitor pressed, so the page has to hand that point over. */
|
|
126
|
+
needsOrigin: boolean
|
|
127
|
+
/** A picture flies from the followed link into the next page; off when the author turned it off. */
|
|
128
|
+
flies: boolean
|
|
129
|
+
/** The arriving page is revealed over the leaving one, which holds (Circle, the Wipes). */
|
|
130
|
+
revealsOver: boolean
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const viewTransitionPlan = (
|
|
134
|
+
config: PageTransition.PageTransitionConfig,
|
|
135
|
+
target: { crossDocument: boolean } = { crossDocument: true }
|
|
136
|
+
): ViewTransitionPlan | null => {
|
|
137
|
+
const preset = String(config.preset).replace(/[^a-z-]/g, '')
|
|
138
|
+
const chosen =
|
|
139
|
+
config.options && typeof config.options === 'object'
|
|
140
|
+
? Object.fromEntries(
|
|
141
|
+
Object.entries(config.options).filter(([, value]) => typeof value === 'string')
|
|
142
|
+
)
|
|
143
|
+
: undefined
|
|
144
|
+
const options = PageTransition.resolvePageTransitionOptions(preset, chosen)
|
|
145
|
+
const duration = Number.isFinite(config.duration) && config.duration > 0 ? config.duration : 0.35
|
|
146
|
+
const curve =
|
|
147
|
+
PageTransition.PAGE_TRANSITION_EASING_CURVES[config.easing] ||
|
|
148
|
+
PageTransition.PAGE_TRANSITION_EASING_CURVES['ease-out']
|
|
149
|
+
const custom =
|
|
150
|
+
preset === 'custom'
|
|
151
|
+
? PageTransition.sanitizePageTransitionCustom({
|
|
152
|
+
durationSeconds: duration,
|
|
153
|
+
easing: config.easing,
|
|
154
|
+
arrive: config.custom?.arrive as never,
|
|
155
|
+
leave: config.custom?.leave as never,
|
|
156
|
+
})
|
|
157
|
+
: undefined
|
|
158
|
+
|
|
159
|
+
const needsOrigin = preset === 'circle' && options.origin !== 'center'
|
|
160
|
+
const origin = needsOrigin
|
|
161
|
+
? { originX: `var(${ORIGIN_X}, 50%)`, originY: `var(${ORIGIN_Y}, 50%)` }
|
|
162
|
+
: { originX: '50%', originY: '50%' }
|
|
163
|
+
const forward = animationsFor(preset, { reverse: false, ...origin }, options, custom)
|
|
164
|
+
if (!forward) {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
const needsDirection = PageTransition.reversePageTransitionPreset(preset) !== preset
|
|
168
|
+
const back = needsDirection
|
|
169
|
+
? animationsFor(preset, { reverse: true, originX: '50%', originY: '50%' }, options, custom)
|
|
170
|
+
: null
|
|
171
|
+
|
|
172
|
+
const timing = `${duration}s cubic-bezier(${curve.join(', ')})`
|
|
173
|
+
// A reveal opens the arriving page over the leaving one: only the arrival
|
|
174
|
+
// animates, from the start, and the leaving picture just stays.
|
|
175
|
+
const revealsOver = !COVER_CLIPS[preset] && forward.arrive[1].clipPath !== undefined
|
|
176
|
+
const arriveTiming = revealsOver ? `${timing} both` : `${timing} ${duration}s both`
|
|
177
|
+
const rules = [
|
|
178
|
+
// Opting every page in is for page-to-page navigations only; a single-page
|
|
179
|
+
// app starts its own transitions and must not turn full reloads into them.
|
|
180
|
+
...(target.crossDocument ? ['@view-transition {\n navigation: auto;\n}'] : []),
|
|
181
|
+
'::view-transition-old(root),\n::view-transition-new(root) {\n mix-blend-mode: normal;\n}',
|
|
182
|
+
revealsOver
|
|
183
|
+
? '::view-transition-old(root) {\n animation: none;\n}'
|
|
184
|
+
: `::view-transition-old(root) {\n animation: tq-page-leave ${timing} both;\n}`,
|
|
185
|
+
`::view-transition-new(root) {\n animation: tq-page-arrive ${arriveTiming};\n}`,
|
|
186
|
+
...(revealsOver ? [] : [keyframes('tq-page-leave', forward.leave[0], forward.leave[1])]),
|
|
187
|
+
keyframes('tq-page-arrive', forward.arrive[0], forward.arrive[1]),
|
|
188
|
+
]
|
|
189
|
+
if (COVER_CLIPS[preset]) {
|
|
190
|
+
rules.push(
|
|
191
|
+
`::view-transition-group(root) {\n background: ${PageTransition.pageTransitionCoverColor(
|
|
192
|
+
options.color
|
|
193
|
+
)};\n}`
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
if (back) {
|
|
197
|
+
if (!revealsOver) {
|
|
198
|
+
rules.push(
|
|
199
|
+
`html[${BACK_ATTR}="back"]::view-transition-old(root) {\n animation-name: tq-page-leave-back;\n}`
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
rules.push(
|
|
203
|
+
`html[${BACK_ATTR}="back"]::view-transition-new(root) {\n animation-name: tq-page-arrive-back;\n}`,
|
|
204
|
+
...(revealsOver ? [] : [keyframes('tq-page-leave-back', back.leave[0], back.leave[1])]),
|
|
205
|
+
keyframes('tq-page-arrive-back', back.arrive[0], back.arrive[1])
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
// The flying image spans both halves (the leaving page's and the arriving
|
|
209
|
+
// page's — one half when the arrival plays over the leaving page), and a
|
|
210
|
+
// card's 4:3 picture grows into a 16:9 hero cropped, never stretched. A
|
|
211
|
+
// picture the arriving page does not show has nowhere to land: it leaves
|
|
212
|
+
// with its page instead of lingering over the next one.
|
|
213
|
+
const flies = config.flyingPictures !== false
|
|
214
|
+
if (flies) {
|
|
215
|
+
const flight = revealsOver ? duration : duration * 2
|
|
216
|
+
rules.push(
|
|
217
|
+
`::view-transition-group(${MORPH_NAME}) {\n animation-duration: ${flight}s;\n animation-timing-function: cubic-bezier(${curve.join(
|
|
218
|
+
', '
|
|
219
|
+
)});\n}`,
|
|
220
|
+
`::view-transition-old(${MORPH_NAME}),\n::view-transition-new(${MORPH_NAME}) {\n height: 100%;\n object-fit: cover;\n overflow: clip;\n animation-duration: ${flight}s;\n}`,
|
|
221
|
+
revealsOver
|
|
222
|
+
? `::view-transition-old(${MORPH_NAME}):only-child {\n animation: tq-morph-away ${timing} both;\n}`
|
|
223
|
+
: `::view-transition-old(${MORPH_NAME}):only-child {\n animation: tq-page-leave ${timing} both, tq-morph-away ${timing} both;\n}`,
|
|
224
|
+
`::view-transition-new(${MORPH_NAME}):only-child {\n animation: tq-page-arrive ${arriveTiming}, tq-morph-in ${arriveTiming};\n}`,
|
|
225
|
+
'@keyframes tq-morph-away {\n to { opacity: 0; }\n}',
|
|
226
|
+
'@keyframes tq-morph-in {\n from { opacity: 0; }\n}'
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
const indented = rules.map((rule) => rule.replace(/^/gm, ' ')).join('\n')
|
|
230
|
+
return {
|
|
231
|
+
css: `@media (prefers-reduced-motion: no-preference) {\n${indented}\n}\n`,
|
|
232
|
+
needsDirection,
|
|
233
|
+
needsOrigin,
|
|
234
|
+
flies,
|
|
235
|
+
revealsOver,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which chapter is ON STAGE at a scene progress: the chapter whose moment is
|
|
3
|
+
* the latest one the visitor has passed (a later chapter wins a tie), -1 before
|
|
4
|
+
* any moment is reached. Moments are read in document order but need not be
|
|
5
|
+
* monotone — an author's "Snap here" can put an early chapter's moment late.
|
|
6
|
+
* The published TqScrollScene announces every change of this index as a
|
|
7
|
+
* `tq-chapter-reached` DOM event on the chapter element — the hook the
|
|
8
|
+
* "A Chapter Is Reached" workflow trigger listens for.
|
|
9
|
+
*
|
|
10
|
+
* Plain JS inside: this function's SOURCE is injected into the generated
|
|
11
|
+
* runtime via toString(), so no TypeScript-only runtime constructs and no
|
|
12
|
+
* template literals.
|
|
13
|
+
*/
|
|
14
|
+
export function activeChapterIndex(moments: number[], progress: number): number {
|
|
15
|
+
let active = -1
|
|
16
|
+
let best = -Infinity
|
|
17
|
+
for (let index = 0; index < moments.length; index++) {
|
|
18
|
+
const moment = moments[index]
|
|
19
|
+
if (moment <= progress + 0.000001 && moment >= best) {
|
|
20
|
+
best = moment
|
|
21
|
+
active = index
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return active
|
|
25
|
+
}
|