@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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a scroll scene's controller that are plain DOM JavaScript, as
|
|
3
|
+
* SOURCE TEXT, shared word for word by the Next export's TqScrollScene and the
|
|
4
|
+
* HTML export's vanilla controller. Each block talks to its surroundings through
|
|
5
|
+
* the same names in both hosts:
|
|
6
|
+
*
|
|
7
|
+
* trackRef, progressRef, chapterHelpersRef, chapterStateRef,
|
|
8
|
+
* scenePointStateRef, awaitedTagsRef, restackRef objects with a `current`
|
|
9
|
+
* pin, layout, chapterSnap, shouldReduceMotion the scene's settings
|
|
10
|
+
*
|
|
11
|
+
* In React those are refs and props; in the HTML runtime they are plain objects
|
|
12
|
+
* and constants of the same names. The engine (engine-source.ts) must be in
|
|
13
|
+
* scope. Bodies that were React effects keep their early `return undefined` and
|
|
14
|
+
* their returned cleanup, so a host wraps them in a function.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Scene points and chapters announced as DOM events, with the stamps late listeners read. */
|
|
18
|
+
export const announcementsSource =
|
|
19
|
+
(): string => ` // Announces the points passed on the way down, in scroll order, one
|
|
20
|
+
// microtask later (the ancestors' effects, where generated listeners attach,
|
|
21
|
+
// have run by then). The stamp is written with the dispatch, never before it,
|
|
22
|
+
// so a listener attaching in the same effect flush sees no stamp and gets the
|
|
23
|
+
// event, while one attaching later reads the stamp and nothing is announced
|
|
24
|
+
// twice. The first announcement measures from below the scene, so a page
|
|
25
|
+
// that opens already past a point (an anchor link, a restored scroll) counts
|
|
26
|
+
// it as passed. Going up announces nothing; the next way down announces again.
|
|
27
|
+
const announceScenePoints = (p) => {
|
|
28
|
+
const state = scenePointStateRef.current
|
|
29
|
+
const from = state.last === null ? -1 : state.last
|
|
30
|
+
state.last = p
|
|
31
|
+
const passed = passedScenePoints(from, p)
|
|
32
|
+
if (passed.length === 0) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
state.pending = state.pending.concat(passed)
|
|
36
|
+
if (state.scheduled) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
state.scheduled = true
|
|
40
|
+
Promise.resolve().then(() => {
|
|
41
|
+
const current = scenePointStateRef.current
|
|
42
|
+
current.scheduled = false
|
|
43
|
+
const batch = current.pending
|
|
44
|
+
current.pending = []
|
|
45
|
+
const track = trackRef.current
|
|
46
|
+
if (!track) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
for (const key of batch) {
|
|
50
|
+
if (scenePointRank(key) > scenePointRank(track.getAttribute(SCENE_POINT_ATTR) || '')) {
|
|
51
|
+
track.setAttribute(SCENE_POINT_ATTR, key)
|
|
52
|
+
}
|
|
53
|
+
track.dispatchEvent(
|
|
54
|
+
new CustomEvent(SCENE_POINT_EVENT, {
|
|
55
|
+
bubbles: true,
|
|
56
|
+
detail: { point: key, progress: current.last },
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The chapters in document order with the moment each one is on stage —
|
|
64
|
+
// the same moment chapter snap and anchor navigation aim at; a chapter
|
|
65
|
+
// without a moment of its own takes the start of its even share.
|
|
66
|
+
const chapterRecords = () => {
|
|
67
|
+
const track = trackRef.current
|
|
68
|
+
const helpers = chapterHelpersRef.current
|
|
69
|
+
if (!track || !helpers) {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
const chapters = helpers.chapterElements(track)
|
|
73
|
+
return chapters.map((element, index) => {
|
|
74
|
+
const moment = helpers.chapterProgress(element)
|
|
75
|
+
return {
|
|
76
|
+
element,
|
|
77
|
+
moment:
|
|
78
|
+
moment === null ? index / chapters.length : Math.min(1, Math.max(0, moment)),
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Announces chapters as the visitor passes their moments: every chapter
|
|
84
|
+
// between the last announced one and the one now on stage gets its
|
|
85
|
+
// tq-chapter-reached (bubbling) in travel order, so a jump past a chapter
|
|
86
|
+
// still counts as reaching it. The current chapter carries
|
|
87
|
+
// data-chapter-active / data-chapter-count for listeners that attach later.
|
|
88
|
+
// Dispatch is deferred to a microtask: the ancestors' effects (where the
|
|
89
|
+
// generated listeners attach) have run by then, and nothing is a frame late.
|
|
90
|
+
const announceChapter = (p) => {
|
|
91
|
+
const state = chapterStateRef.current
|
|
92
|
+
if (state.records === null) {
|
|
93
|
+
state.records = chapterRecords()
|
|
94
|
+
state.moments = state.records.map((record) => record.moment)
|
|
95
|
+
// A rebuilt list may hold NEW elements for the same chapters (a repeater
|
|
96
|
+
// re-rendered): the stamp moves to the current chapter's new element
|
|
97
|
+
// without announcing it again.
|
|
98
|
+
const current = state.index >= 0 ? state.records[state.index] : null
|
|
99
|
+
if (current && current.element !== state.element) {
|
|
100
|
+
stampChapter(current.element, state.index + 1, state.records.length)
|
|
101
|
+
} else if (!current && state.index >= 0) {
|
|
102
|
+
state.index = -1
|
|
103
|
+
state.element = null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const direction = p >= state.lastProgress ? 'down' : 'up'
|
|
107
|
+
state.lastProgress = p
|
|
108
|
+
if (state.moments.length === 0 || !state.onScreen) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const next = activeChapterIndex(state.moments, p)
|
|
112
|
+
if (next === state.index) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const from = state.index
|
|
116
|
+
state.index = next
|
|
117
|
+
if (next === -1) {
|
|
118
|
+
stampChapter(null, 0, 0)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
const passed = []
|
|
122
|
+
if (from < next) {
|
|
123
|
+
for (let index = from + 1; index <= next; index++) {
|
|
124
|
+
passed.push(index)
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
for (let index = from - 1; index >= next; index--) {
|
|
128
|
+
passed.push(index)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
stampChapter(state.records[next].element, next + 1, state.records.length)
|
|
132
|
+
state.pending = { passed, progress: p, direction }
|
|
133
|
+
Promise.resolve().then(() => {
|
|
134
|
+
const batch = chapterStateRef.current.pending
|
|
135
|
+
if (!batch) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
chapterStateRef.current.pending = null
|
|
139
|
+
const records = chapterStateRef.current.records || []
|
|
140
|
+
for (const index of batch.passed) {
|
|
141
|
+
const record = records[index]
|
|
142
|
+
if (!record) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
record.element.dispatchEvent(
|
|
146
|
+
new CustomEvent(CHAPTER_REACHED_EVENT, {
|
|
147
|
+
bubbles: true,
|
|
148
|
+
detail: {
|
|
149
|
+
chapterIndex: index + 1,
|
|
150
|
+
chapterCount: records.length,
|
|
151
|
+
progress: batch.progress,
|
|
152
|
+
direction: batch.direction,
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const stampChapter = (element, chapterIndex, chapterCount) => {
|
|
161
|
+
const state = chapterStateRef.current
|
|
162
|
+
if (state.element && state.element !== element) {
|
|
163
|
+
state.element.removeAttribute(CHAPTER_ACTIVE_ATTR)
|
|
164
|
+
state.element.removeAttribute(CHAPTER_COUNT_ATTR)
|
|
165
|
+
}
|
|
166
|
+
state.element = element
|
|
167
|
+
if (element) {
|
|
168
|
+
element.setAttribute(CHAPTER_ACTIVE_ATTR, String(chapterIndex))
|
|
169
|
+
element.setAttribute(CHAPTER_COUNT_ATTR, String(chapterCount))
|
|
170
|
+
}
|
|
171
|
+
}`
|
|
172
|
+
|
|
173
|
+
/** Body of `restack`: every real grid item of a chapters stage goes into the same cell. */
|
|
174
|
+
export const restackBodySource =
|
|
175
|
+
(): string => ` if (layout !== 'chapters' || !trackRef.current) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const stage = trackRef.current.querySelector(':scope > [data-scene-stage]')
|
|
179
|
+
if (!stage) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const place = (el) => {
|
|
183
|
+
// Duck-typed, never instanceof — mirrors the editor runtime, where
|
|
184
|
+
// canvas elements belong to another realm and instanceof always fails.
|
|
185
|
+
if (!el || el.nodeType !== 1 || el.tagName === 'STYLE') {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (!el.style || typeof el.style.removeProperty !== 'function') {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
let display = ''
|
|
192
|
+
try {
|
|
193
|
+
display = getComputedStyle(el).display
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// detached node — skip
|
|
196
|
+
}
|
|
197
|
+
if (display === 'contents') {
|
|
198
|
+
// A pre-upgrade pass may have pinned this wrapper as a box — clean up.
|
|
199
|
+
if (el.style.gridArea) {
|
|
200
|
+
el.style.removeProperty('grid-area')
|
|
201
|
+
}
|
|
202
|
+
if (el.shadowRoot) {
|
|
203
|
+
Array.from(el.shadowRoot.children).forEach(place)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (typeof el.assignedElements === 'function') {
|
|
207
|
+
const assigned = el.assignedElements({ flatten: true })
|
|
208
|
+
;(assigned.length > 0 ? assigned : Array.from(el.children)).forEach(place)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
Array.from(el.children).forEach(place)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const tag = el.tagName.toLowerCase()
|
|
215
|
+
if (
|
|
216
|
+
tag.includes('-') &&
|
|
217
|
+
typeof customElements !== 'undefined' &&
|
|
218
|
+
!customElements.get(tag) &&
|
|
219
|
+
!awaitedTagsRef.current.has(tag)
|
|
220
|
+
) {
|
|
221
|
+
awaitedTagsRef.current.add(tag)
|
|
222
|
+
customElements
|
|
223
|
+
.whenDefined(tag)
|
|
224
|
+
.then(() => restackRef.current && restackRef.current())
|
|
225
|
+
.catch(() => undefined)
|
|
226
|
+
}
|
|
227
|
+
el.style.gridArea = '1 / 1'
|
|
228
|
+
}
|
|
229
|
+
Array.from(stage.children).forEach(place)`
|
|
230
|
+
|
|
231
|
+
/** Chapter geometry: where a chapter is on stage, which children are chapters, how to scroll there. */
|
|
232
|
+
export const chapterHelpersSource = (): string => ` if (chapterHelpersRef.current === null) {
|
|
233
|
+
const chapterProgress = (chapterRoot) => {
|
|
234
|
+
// 1. The author's own snap point ("Snap here" on the timeline) wins.
|
|
235
|
+
const override = parseFloat(chapterRoot.getAttribute('data-chapter-moment') || '')
|
|
236
|
+
if (Number.isFinite(override)) {
|
|
237
|
+
return Math.min(1, Math.max(0, override))
|
|
238
|
+
}
|
|
239
|
+
// 2. The settled moment — everything arrived, nothing leaving — read
|
|
240
|
+
// from the root's and every descendant's lanes.
|
|
241
|
+
const boundLanes = []
|
|
242
|
+
const boundElements = [chapterRoot].concat(
|
|
243
|
+
Array.from(chapterRoot.querySelectorAll('[' + SCROLL_BIND_ATTR + ']'))
|
|
244
|
+
)
|
|
245
|
+
for (const element of boundElements) {
|
|
246
|
+
const lanes = parseScrollBind(element.getAttribute(SCROLL_BIND_ATTR))
|
|
247
|
+
for (const lane of lanes) {
|
|
248
|
+
boundLanes.push(lane)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const settled = settledMomentForLanes(boundLanes)
|
|
252
|
+
if (settled !== null) {
|
|
253
|
+
return settled
|
|
254
|
+
}
|
|
255
|
+
// 3. Through-motion only (a crossing line): the window's middle.
|
|
256
|
+
const recorded = String(chapterRoot.getAttribute('data-chapter-window') || '')
|
|
257
|
+
.split(',')
|
|
258
|
+
.map((part) => parseFloat(part))
|
|
259
|
+
if (recorded.length === 2 && recorded.every((value) => Number.isFinite(value))) {
|
|
260
|
+
return (recorded[0] + recorded[1]) / 2
|
|
261
|
+
}
|
|
262
|
+
const lanes = parseScrollBind(chapterRoot.getAttribute(SCROLL_BIND_ATTR))
|
|
263
|
+
const stops = lanes.flatMap((lane) => lane.at)
|
|
264
|
+
if (stops.length === 0) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
return (Math.min.apply(null, stops) + Math.max.apply(null, stops)) / 2
|
|
268
|
+
}
|
|
269
|
+
chapterHelpersRef.current = {
|
|
270
|
+
stageOf: (track) => track.querySelector(':scope > [data-scene-stage]'),
|
|
271
|
+
chapterProgress,
|
|
272
|
+
// The chapters: the stage's children when the scene pins, the track's own
|
|
273
|
+
// children when it does not. Never the style tag, never a backdrop: the
|
|
274
|
+
// background video, a bare media child (img / video / picture) or any
|
|
275
|
+
// child marked data-scene-backdrop is the scene's background — it sits
|
|
276
|
+
// under the chapters and owns no story window.
|
|
277
|
+
chapterElements: (track) => {
|
|
278
|
+
const stage = chapterHelpersRef.current.stageOf(track)
|
|
279
|
+
const host = stage || track
|
|
280
|
+
return Array.from(host.children).filter(
|
|
281
|
+
(child) =>
|
|
282
|
+
!!child.getAttribute &&
|
|
283
|
+
child.tagName !== 'STYLE' &&
|
|
284
|
+
child.tagName !== 'IMG' &&
|
|
285
|
+
child.tagName !== 'VIDEO' &&
|
|
286
|
+
child.tagName !== 'PICTURE' &&
|
|
287
|
+
!child.hasAttribute('data-scroll-video') &&
|
|
288
|
+
!child.hasAttribute('data-scene-backdrop') &&
|
|
289
|
+
!child.hasAttribute('data-scene-stage')
|
|
290
|
+
)
|
|
291
|
+
},
|
|
292
|
+
chapterMoments: (track) => {
|
|
293
|
+
const moments = []
|
|
294
|
+
for (const child of chapterHelpersRef.current.chapterElements(track)) {
|
|
295
|
+
const progress = chapterProgress(child)
|
|
296
|
+
if (progress !== null) {
|
|
297
|
+
moments.push(Math.min(1, Math.max(0, progress)))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return moments.sort((a, b) => a - b)
|
|
301
|
+
},
|
|
302
|
+
scrollToProgress: (track, progress, behavior) => {
|
|
303
|
+
const rect = track.getBoundingClientRect()
|
|
304
|
+
const trackTop = rect.top + window.scrollY
|
|
305
|
+
const travel = Math.max(0, track.offsetHeight - window.innerHeight)
|
|
306
|
+
window.scrollTo({
|
|
307
|
+
top: trackTop + Math.min(1, Math.max(0, progress)) * travel,
|
|
308
|
+
behavior,
|
|
309
|
+
})
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
}`
|
|
313
|
+
|
|
314
|
+
/** Body of the anchor-navigation effect: a hash that points into a pinned scene scrolls to that chapter. */
|
|
315
|
+
export const anchorNavigationBodySource = (): string => ` const track = trackRef.current
|
|
316
|
+
if (!track || !pin || typeof window === 'undefined') {
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
const helpers = chapterHelpersRef.current
|
|
320
|
+
const chapterRootFor = (target) => {
|
|
321
|
+
const stage = helpers.stageOf(track)
|
|
322
|
+
if (!stage || !stage.contains(target)) {
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
let current = target
|
|
326
|
+
while (current && current.parentElement !== stage) {
|
|
327
|
+
current = current.parentElement
|
|
328
|
+
}
|
|
329
|
+
return current
|
|
330
|
+
}
|
|
331
|
+
const scrollToChapter = (id, behavior) => {
|
|
332
|
+
const target = id ? document.getElementById(id) : null
|
|
333
|
+
const chapterRoot = target ? chapterRootFor(target) : null
|
|
334
|
+
if (!chapterRoot) {
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
const progress = helpers.chapterProgress(chapterRoot)
|
|
338
|
+
if (progress === null) {
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
helpers.scrollToProgress(track, progress, behavior)
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
const onClick = (event) => {
|
|
345
|
+
if (event.defaultPrevented || event.button !== 0) {
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
const anchor = event.target && event.target.closest ? event.target.closest('a[href]') : null
|
|
352
|
+
if (!anchor) {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
const href = anchor.getAttribute('href') || ''
|
|
356
|
+
const hashIndex = href.indexOf('#')
|
|
357
|
+
if (hashIndex === -1) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
const beforeHash = href.slice(0, hashIndex)
|
|
361
|
+
if (beforeHash && beforeHash !== window.location.pathname) {
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
const id = decodeURIComponent(href.slice(hashIndex + 1))
|
|
365
|
+
if (scrollToChapter(id, 'smooth')) {
|
|
366
|
+
event.preventDefault()
|
|
367
|
+
try {
|
|
368
|
+
window.history.pushState(null, '', '#' + id)
|
|
369
|
+
} catch (e) {
|
|
370
|
+
// history can refuse in sandboxed frames; the scroll already happened
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const onHashChange = () => {
|
|
375
|
+
scrollToChapter(decodeURIComponent(window.location.hash.slice(1)), 'smooth')
|
|
376
|
+
}
|
|
377
|
+
document.addEventListener('click', onClick, true)
|
|
378
|
+
window.addEventListener('hashchange', onHashChange)
|
|
379
|
+
let settleTimer = 0
|
|
380
|
+
const initialId = decodeURIComponent(window.location.hash.slice(1))
|
|
381
|
+
if (initialId) {
|
|
382
|
+
window.requestAnimationFrame(() => scrollToChapter(initialId, 'auto'))
|
|
383
|
+
// late images/fonts shift the track's document position — one correction
|
|
384
|
+
settleTimer = window.setTimeout(() => scrollToChapter(initialId, 'auto'), 350)
|
|
385
|
+
}
|
|
386
|
+
return () => {
|
|
387
|
+
document.removeEventListener('click', onClick, true)
|
|
388
|
+
window.removeEventListener('hashchange', onHashChange)
|
|
389
|
+
window.clearTimeout(settleTimer)
|
|
390
|
+
}`
|
|
391
|
+
|
|
392
|
+
/** Body of the gentle chapter-snap effect: a visitor who pauses mid-scene settles on the nearest chapter. */
|
|
393
|
+
export const gentleSnapBodySource = (): string => ` const track = trackRef.current
|
|
394
|
+
if (!track || !pin || chapterSnap !== 'gentle' || typeof window === 'undefined') {
|
|
395
|
+
return undefined
|
|
396
|
+
}
|
|
397
|
+
if (shouldReduceMotion) {
|
|
398
|
+
return undefined
|
|
399
|
+
}
|
|
400
|
+
const helpers = chapterHelpersRef.current
|
|
401
|
+
let settleTimer = 0
|
|
402
|
+
let suppressUntil = 0
|
|
403
|
+
let lastProgress = progressRef.current
|
|
404
|
+
let direction = 0
|
|
405
|
+
const settle = () => {
|
|
406
|
+
if (Date.now() < suppressUntil) {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
const progress = progressRef.current
|
|
410
|
+
if (progress <= 0.001 || progress >= 0.999) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
const moments = helpers.chapterMoments(track)
|
|
414
|
+
if (moments.length === 0) {
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
// Settle in the direction the visitor was travelling: a short scroll
|
|
418
|
+
// forward lands on the next chapter, never back on the one just left.
|
|
419
|
+
const ahead = moments.filter((moment) =>
|
|
420
|
+
direction > 0 ? moment >= progress - 0.02 : direction < 0 ? moment <= progress + 0.02 : true
|
|
421
|
+
)
|
|
422
|
+
const candidates = ahead.length > 0 ? ahead : moments
|
|
423
|
+
let nearest = candidates[0]
|
|
424
|
+
for (const moment of candidates) {
|
|
425
|
+
if (Math.abs(moment - progress) < Math.abs(nearest - progress)) {
|
|
426
|
+
nearest = moment
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (Math.abs(nearest - progress) < 0.005) {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
suppressUntil = Date.now() + 900
|
|
433
|
+
helpers.scrollToProgress(track, nearest, 'smooth')
|
|
434
|
+
}
|
|
435
|
+
const onScroll = () => {
|
|
436
|
+
window.clearTimeout(settleTimer)
|
|
437
|
+
const progress = progressRef.current
|
|
438
|
+
if (progress !== lastProgress) {
|
|
439
|
+
direction = progress > lastProgress ? 1 : -1
|
|
440
|
+
lastProgress = progress
|
|
441
|
+
}
|
|
442
|
+
if (Date.now() < suppressUntil) {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
settleTimer = window.setTimeout(settle, 180)
|
|
446
|
+
}
|
|
447
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
448
|
+
return () => {
|
|
449
|
+
window.removeEventListener('scroll', onScroll)
|
|
450
|
+
window.clearTimeout(settleTimer)
|
|
451
|
+
}`
|