@teleporthq/teleport-shared 0.43.70 → 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.
Files changed (150) hide show
  1. package/__tests__/motion-runtime/motion-engine.ts +102 -0
  2. package/__tests__/motion-runtime/scroll-video-engine.ts +28 -0
  3. package/__tests__/scroll-scene/story-lanes.ts +126 -0
  4. package/dist/cjs/index.d.ts +5 -1
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +9 -1
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/motion-runtime/index.d.ts +3 -0
  9. package/dist/cjs/motion-runtime/index.d.ts.map +1 -0
  10. package/dist/cjs/motion-runtime/index.js +8 -0
  11. package/dist/cjs/motion-runtime/index.js.map +1 -0
  12. package/dist/cjs/motion-runtime/motion-engine-source.d.ts +16 -0
  13. package/dist/cjs/motion-runtime/motion-engine-source.d.ts.map +1 -0
  14. package/dist/cjs/motion-runtime/motion-engine-source.js +20 -0
  15. package/dist/cjs/motion-runtime/motion-engine-source.js.map +1 -0
  16. package/dist/cjs/motion-runtime/scroll-video-engine-source.d.ts +9 -0
  17. package/dist/cjs/motion-runtime/scroll-video-engine-source.d.ts.map +1 -0
  18. package/dist/cjs/motion-runtime/scroll-video-engine-source.js +13 -0
  19. package/dist/cjs/motion-runtime/scroll-video-engine-source.js.map +1 -0
  20. package/dist/cjs/page-transition/index.d.ts +4 -0
  21. package/dist/cjs/page-transition/index.d.ts.map +1 -0
  22. package/dist/cjs/page-transition/index.js +20 -0
  23. package/dist/cjs/page-transition/index.js.map +1 -0
  24. package/dist/cjs/page-transition/morph-source.d.ts +20 -0
  25. package/dist/cjs/page-transition/morph-source.d.ts.map +1 -0
  26. package/dist/cjs/page-transition/morph-source.js +25 -0
  27. package/dist/cjs/page-transition/morph-source.js.map +1 -0
  28. package/dist/cjs/page-transition/variants.d.ts +87 -0
  29. package/dist/cjs/page-transition/variants.d.ts.map +1 -0
  30. package/dist/cjs/page-transition/variants.js +330 -0
  31. package/dist/cjs/page-transition/variants.js.map +1 -0
  32. package/dist/cjs/page-transition/view-transition-css.d.ts +21 -0
  33. package/dist/cjs/page-transition/view-transition-css.d.ts.map +1 -0
  34. package/dist/cjs/page-transition/view-transition-css.js +188 -0
  35. package/dist/cjs/page-transition/view-transition-css.js.map +1 -0
  36. package/dist/cjs/scroll-scene/chapter-index.d.ts +15 -0
  37. package/dist/cjs/scroll-scene/chapter-index.d.ts.map +1 -0
  38. package/dist/cjs/scroll-scene/chapter-index.js +30 -0
  39. package/dist/cjs/scroll-scene/chapter-index.js.map +1 -0
  40. package/dist/cjs/scroll-scene/controller-source.d.ts +26 -0
  41. package/dist/cjs/scroll-scene/controller-source.d.ts.map +1 -0
  42. package/dist/cjs/scroll-scene/controller-source.js +34 -0
  43. package/dist/cjs/scroll-scene/controller-source.js.map +1 -0
  44. package/dist/cjs/scroll-scene/engine-source.d.ts +2 -0
  45. package/dist/cjs/scroll-scene/engine-source.d.ts.map +1 -0
  46. package/dist/cjs/scroll-scene/engine-source.js +26 -0
  47. package/dist/cjs/scroll-scene/engine-source.js.map +1 -0
  48. package/dist/cjs/scroll-scene/index.d.ts +7 -0
  49. package/dist/cjs/scroll-scene/index.d.ts.map +1 -0
  50. package/dist/cjs/scroll-scene/index.js +22 -0
  51. package/dist/cjs/scroll-scene/index.js.map +1 -0
  52. package/dist/cjs/scroll-scene/moment.d.ts +21 -0
  53. package/dist/cjs/scroll-scene/moment.d.ts.map +1 -0
  54. package/dist/cjs/scroll-scene/moment.js +50 -0
  55. package/dist/cjs/scroll-scene/moment.js.map +1 -0
  56. package/dist/cjs/scroll-scene/points.d.ts +23 -0
  57. package/dist/cjs/scroll-scene/points.d.ts.map +1 -0
  58. package/dist/cjs/scroll-scene/points.js +51 -0
  59. package/dist/cjs/scroll-scene/points.js.map +1 -0
  60. package/dist/cjs/scroll-scene/unclip.d.ts +15 -0
  61. package/dist/cjs/scroll-scene/unclip.d.ts.map +1 -0
  62. package/dist/cjs/scroll-scene/unclip.js +44 -0
  63. package/dist/cjs/scroll-scene/unclip.js.map +1 -0
  64. package/dist/cjs/state-bindings/index.d.ts +74 -0
  65. package/dist/cjs/state-bindings/index.d.ts.map +1 -0
  66. package/dist/cjs/state-bindings/index.js +193 -0
  67. package/dist/cjs/state-bindings/index.js.map +1 -0
  68. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  69. package/dist/esm/index.d.ts +5 -1
  70. package/dist/esm/index.d.ts.map +1 -1
  71. package/dist/esm/index.js +5 -1
  72. package/dist/esm/index.js.map +1 -1
  73. package/dist/esm/motion-runtime/index.d.ts +3 -0
  74. package/dist/esm/motion-runtime/index.d.ts.map +1 -0
  75. package/dist/esm/motion-runtime/index.js +3 -0
  76. package/dist/esm/motion-runtime/index.js.map +1 -0
  77. package/dist/esm/motion-runtime/motion-engine-source.d.ts +16 -0
  78. package/dist/esm/motion-runtime/motion-engine-source.d.ts.map +1 -0
  79. package/dist/esm/motion-runtime/motion-engine-source.js +16 -0
  80. package/dist/esm/motion-runtime/motion-engine-source.js.map +1 -0
  81. package/dist/esm/motion-runtime/scroll-video-engine-source.d.ts +9 -0
  82. package/dist/esm/motion-runtime/scroll-video-engine-source.d.ts.map +1 -0
  83. package/dist/esm/motion-runtime/scroll-video-engine-source.js +9 -0
  84. package/dist/esm/motion-runtime/scroll-video-engine-source.js.map +1 -0
  85. package/dist/esm/page-transition/index.d.ts +4 -0
  86. package/dist/esm/page-transition/index.d.ts.map +1 -0
  87. package/dist/esm/page-transition/index.js +4 -0
  88. package/dist/esm/page-transition/index.js.map +1 -0
  89. package/dist/esm/page-transition/morph-source.d.ts +20 -0
  90. package/dist/esm/page-transition/morph-source.d.ts.map +1 -0
  91. package/dist/esm/page-transition/morph-source.js +21 -0
  92. package/dist/esm/page-transition/morph-source.js.map +1 -0
  93. package/dist/esm/page-transition/variants.d.ts +87 -0
  94. package/dist/esm/page-transition/variants.d.ts.map +1 -0
  95. package/dist/esm/page-transition/variants.js +318 -0
  96. package/dist/esm/page-transition/variants.js.map +1 -0
  97. package/dist/esm/page-transition/view-transition-css.d.ts +21 -0
  98. package/dist/esm/page-transition/view-transition-css.d.ts.map +1 -0
  99. package/dist/esm/page-transition/view-transition-css.js +161 -0
  100. package/dist/esm/page-transition/view-transition-css.js.map +1 -0
  101. package/dist/esm/scroll-scene/chapter-index.d.ts +15 -0
  102. package/dist/esm/scroll-scene/chapter-index.d.ts.map +1 -0
  103. package/dist/esm/scroll-scene/chapter-index.js +26 -0
  104. package/dist/esm/scroll-scene/chapter-index.js.map +1 -0
  105. package/dist/esm/scroll-scene/controller-source.d.ts +26 -0
  106. package/dist/esm/scroll-scene/controller-source.d.ts.map +1 -0
  107. package/dist/esm/scroll-scene/controller-source.js +26 -0
  108. package/dist/esm/scroll-scene/controller-source.js.map +1 -0
  109. package/dist/esm/scroll-scene/engine-source.d.ts +2 -0
  110. package/dist/esm/scroll-scene/engine-source.d.ts.map +1 -0
  111. package/dist/esm/scroll-scene/engine-source.js +22 -0
  112. package/dist/esm/scroll-scene/engine-source.js.map +1 -0
  113. package/dist/esm/scroll-scene/index.d.ts +7 -0
  114. package/dist/esm/scroll-scene/index.d.ts.map +1 -0
  115. package/dist/esm/scroll-scene/index.js +7 -0
  116. package/dist/esm/scroll-scene/index.js.map +1 -0
  117. package/dist/esm/scroll-scene/moment.d.ts +21 -0
  118. package/dist/esm/scroll-scene/moment.d.ts.map +1 -0
  119. package/dist/esm/scroll-scene/moment.js +46 -0
  120. package/dist/esm/scroll-scene/moment.js.map +1 -0
  121. package/dist/esm/scroll-scene/points.d.ts +23 -0
  122. package/dist/esm/scroll-scene/points.d.ts.map +1 -0
  123. package/dist/esm/scroll-scene/points.js +45 -0
  124. package/dist/esm/scroll-scene/points.js.map +1 -0
  125. package/dist/esm/scroll-scene/unclip.d.ts +15 -0
  126. package/dist/esm/scroll-scene/unclip.d.ts.map +1 -0
  127. package/dist/esm/scroll-scene/unclip.js +40 -0
  128. package/dist/esm/scroll-scene/unclip.js.map +1 -0
  129. package/dist/esm/state-bindings/index.d.ts +74 -0
  130. package/dist/esm/state-bindings/index.d.ts.map +1 -0
  131. package/dist/esm/state-bindings/index.js +176 -0
  132. package/dist/esm/state-bindings/index.js.map +1 -0
  133. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  134. package/package.json +3 -3
  135. package/src/index.ts +8 -0
  136. package/src/motion-runtime/index.ts +2 -0
  137. package/src/motion-runtime/motion-engine-source.ts +289 -0
  138. package/src/motion-runtime/scroll-video-engine-source.ts +591 -0
  139. package/src/page-transition/index.ts +3 -0
  140. package/src/page-transition/morph-source.ts +86 -0
  141. package/src/page-transition/variants.ts +407 -0
  142. package/src/page-transition/view-transition-css.ts +237 -0
  143. package/src/scroll-scene/chapter-index.ts +25 -0
  144. package/src/scroll-scene/controller-source.ts +451 -0
  145. package/src/scroll-scene/engine-source.ts +330 -0
  146. package/src/scroll-scene/index.ts +12 -0
  147. package/src/scroll-scene/moment.ts +64 -0
  148. package/src/scroll-scene/points.ts +46 -0
  149. package/src/scroll-scene/unclip.ts +44 -0
  150. package/src/state-bindings/index.ts +207 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The scroll-scene lane engine as SOURCE TEXT: the constants, the lane presets
3
+ * and the functions that parse a `data-scroll-bind`, read a lane at a progress
4
+ * and write it onto an element. It is plain DOM JavaScript with no framework in
5
+ * it, which is why two runtimes can share it word for word:
6
+ *
7
+ * - the Next export wraps it in the TqScrollScene React component, where
8
+ * framer-motion supplies the scroll progress;
9
+ * - the HTML export wraps it in a small vanilla controller, where a scroll
10
+ * listener supplies the same progress.
11
+ *
12
+ * One copy means a new lane property or preset reaches both exports at once.
13
+ * The text mirrors the canvas renderer's scroll-scene-lanes module, so the
14
+ * canvas, the Next site and the HTML site move identically. (No backticks in
15
+ * the engine's own comments: it is a template literal.)
16
+ */
17
+ import { settledMomentForLanes } from './moment'
18
+ import { activeChapterIndex } from './chapter-index'
19
+ import { passedScenePoints, scenePointList, scenePointRank } from './points'
20
+ import { unclipStickyAncestors } from './unclip'
21
+
22
+ export const scrollSceneEngineSource = (): string => `const SCROLL_BIND_ATTR = 'data-scroll-bind'
23
+ // A chapter coming on stage is announced on the chapter element (bubbling), so
24
+ // a workflow trigger bound to the chapter can listen without knowing the scene.
25
+ const CHAPTER_REACHED_EVENT = 'tq-chapter-reached'
26
+ const CHAPTER_ACTIVE_ATTR = 'data-chapter-active'
27
+ const CHAPTER_COUNT_ATTR = 'data-chapter-count'
28
+ // Scrolling DOWN past a quarter / halfway / three quarters / the end of the
29
+ // scene is announced on the track (the element with the scene's id), bubbling;
30
+ // the furthest point passed so far is stamped for listeners that attach later.
31
+ const SCENE_POINT_EVENT = 'tq-scene-point-passed'
32
+ const SCENE_POINT_ATTR = 'data-scene-point'
33
+
34
+ const LANE_PROPS = [
35
+ 'x',
36
+ 'y',
37
+ 'scale',
38
+ 'rotate',
39
+ 'rotate-x',
40
+ 'rotate-y',
41
+ 'opacity',
42
+ 'blur',
43
+ 'grayscale',
44
+ 'saturate',
45
+ 'brightness',
46
+ 'clip',
47
+ 'clip-y',
48
+ 'count',
49
+ ]
50
+
51
+ // Mirrors SCROLL_COUNT_ATTR / SCROLL_COUNT_CSS in the editor's lane contract.
52
+ const COUNT_ATTR = 'data-scroll-count'
53
+ const HIDDEN_ATTR = 'data-scene-hidden'
54
+ const HIDDEN_CSS =
55
+ '[data-scene-hidden], [data-scene-hidden] * { pointer-events: none !important; }'
56
+ const COUNT_CSS =
57
+ '[data-scroll-count]::before { counter-reset: tq-count var(--tq-count, 0); content: counter(tq-count); }'
58
+ const clampPercent = (value) => Math.min(100, Math.max(0, Math.round(value * 100) / 100))
59
+
60
+ const LANE_PRESETS = {
61
+ 'depth-1': [{ prop: 'y', at: [0, 1], values: [40, -40] }],
62
+ 'depth-2': [{ prop: 'y', at: [0, 1], values: [80, -80] }],
63
+ 'depth-3': [{ prop: 'y', at: [0, 1], values: [140, -140] }],
64
+ 'fade-window': [{ prop: 'opacity', at: [0, 0.15, 0.85, 1], values: [0, 1, 1, 0] }],
65
+ 'rail-x': [{ prop: 'x', at: [0, 1], values: [0, -100], unit: '%' }],
66
+ 'zoom-through': [{ prop: 'scale', at: [0, 1], values: [0.85, 1.1] }],
67
+ 'unblur-in': [{ prop: 'blur', at: [0, 0.3], values: [10, 0] }],
68
+ 'rise-in': [
69
+ { prop: 'y', at: [0, 0.35], values: [70, 0] },
70
+ { prop: 'opacity', at: [0, 0.3], values: [0, 1] },
71
+ ],
72
+ 'ghost-out': [
73
+ { prop: 'opacity', at: [0.7, 1], values: [1, 0] },
74
+ { prop: 'blur', at: [0.7, 1], values: [0, 8] },
75
+ ],
76
+ 'color-in': [{ prop: 'grayscale', at: [0, 0.45], values: [1, 0] }],
77
+ 'sun-up': [{ prop: 'brightness', at: [0, 0.4], values: [0.35, 1] }],
78
+ 'fade-to-mono': [
79
+ { prop: 'grayscale', at: [0.6, 1], values: [0, 1] },
80
+ { prop: 'saturate', at: [0.6, 1], values: [1, 0.6] },
81
+ ],
82
+ 'crash-zoom': [
83
+ { prop: 'scale', at: [0, 0.25], values: [1.6, 1] },
84
+ { prop: 'opacity', at: [0, 0.15], values: [0, 1] },
85
+ ],
86
+ 'tilt-reveal': [
87
+ { prop: 'rotate-x', at: [0, 0.4], values: [35, 0] },
88
+ { prop: 'opacity', at: [0, 0.3], values: [0, 1] },
89
+ ],
90
+ 'spin-in': [
91
+ { prop: 'rotate', at: [0, 0.35], values: [-90, 0] },
92
+ { prop: 'opacity', at: [0, 0.25], values: [0, 1] },
93
+ ],
94
+ breathe: [{ prop: 'scale', at: [0, 0.5, 1], values: [0.98, 1.03, 0.98] }],
95
+ }
96
+
97
+ const isValidLane = (lane) => {
98
+ if (!lane || typeof lane !== 'object' || !LANE_PROPS.includes(lane.prop)) {
99
+ return false
100
+ }
101
+ if (!Array.isArray(lane.at) || !Array.isArray(lane.values)) {
102
+ return false
103
+ }
104
+ if (lane.at.length < 2 || lane.at.length !== lane.values.length) {
105
+ return false
106
+ }
107
+ const numbers = (list) => list.every((entry) => typeof entry === 'number' && isFinite(entry))
108
+ if (!numbers(lane.at) || !numbers(lane.values)) {
109
+ return false
110
+ }
111
+ for (let index = 1; index < lane.at.length; index++) {
112
+ if (lane.at[index] < lane.at[index - 1]) {
113
+ return false
114
+ }
115
+ }
116
+ return lane.unit === undefined || lane.unit === 'px' || lane.unit === '%' || lane.unit === 'vw'
117
+ }
118
+
119
+ ${settledMomentForLanes.toString()}
120
+
121
+ ${activeChapterIndex.toString()}
122
+
123
+ ${scenePointList.toString()}
124
+
125
+ ${passedScenePoints.toString()}
126
+
127
+ ${scenePointRank.toString()}
128
+
129
+ ${unclipStickyAncestors.toString()}
130
+
131
+ const parseScrollBind = (value) => {
132
+ const raw = String(value || '').trim()
133
+ if (!raw) {
134
+ return []
135
+ }
136
+ if (LANE_PRESETS[raw]) {
137
+ return LANE_PRESETS[raw].map((lane) => ({ ...lane }))
138
+ }
139
+ if (!raw.startsWith('[')) {
140
+ return []
141
+ }
142
+ try {
143
+ const parsed = JSON.parse(raw)
144
+ return Array.isArray(parsed) ? parsed.filter(isValidLane) : []
145
+ } catch (e) {
146
+ return []
147
+ }
148
+ }
149
+
150
+ const laneValueAt = (lane, p) => {
151
+ const at = lane.at
152
+ const values = lane.values
153
+ if (p <= at[0]) {
154
+ return values[0]
155
+ }
156
+ if (p >= at[at.length - 1]) {
157
+ return values[values.length - 1]
158
+ }
159
+ for (let index = 1; index < at.length; index++) {
160
+ if (p <= at[index]) {
161
+ const span = at[index] - at[index - 1]
162
+ const local = span === 0 ? 1 : (p - at[index - 1]) / span
163
+ return values[index - 1] + (values[index] - values[index - 1]) * local
164
+ }
165
+ }
166
+ return values[values.length - 1]
167
+ }
168
+
169
+ const FILTER_PROP_ORDER = ['blur', 'grayscale', 'saturate', 'brightness']
170
+
171
+ // Motion lanes write the INDIVIDUAL transform properties (translate / scale /
172
+ // rotate) and never the transform property itself: the browser composes them
173
+ // with whatever the author put in transform (a translateX(-50%) centering, a
174
+ // tilt), so the scene never clobbers the user's own layout. 3D lanes use the
175
+ // axis form of rotate; the stage supplies the vanishing point. Mirrors the
176
+ // canvas. (No backticks in this file's comments — it is a template literal.)
177
+ const applyLanesAt = (element, lanes, p) => {
178
+ const filterByProp = {}
179
+ let translateX
180
+ let translateY
181
+ let rotate
182
+ let scale
183
+ for (const lane of lanes) {
184
+ const value = laneValueAt(lane, p)
185
+ switch (lane.prop) {
186
+ case 'x':
187
+ translateX = value + (lane.unit || 'px')
188
+ break
189
+ case 'y':
190
+ translateY = value + (lane.unit || 'px')
191
+ break
192
+ case 'scale':
193
+ scale = String(value)
194
+ break
195
+ case 'rotate':
196
+ rotate = rotate === undefined ? value + 'deg' : rotate
197
+ break
198
+ case 'rotate-x':
199
+ rotate = 'x ' + value + 'deg'
200
+ break
201
+ case 'rotate-y':
202
+ rotate = 'y ' + value + 'deg'
203
+ break
204
+ case 'opacity':
205
+ element.style.opacity = String(value)
206
+ // A faded-out element must not swallow clicks (stacked chapters: the
207
+ // invisible top one would take every tap meant for the visible story).
208
+ // Attribute + CSS rule, never inline pointer-events, so lanes cannot
209
+ // overwrite a pointer-events the author set. Mirrors the editor.
210
+ if (value <= 0.02) {
211
+ element.setAttribute(HIDDEN_ATTR, '')
212
+ } else {
213
+ element.removeAttribute(HIDDEN_ATTR)
214
+ }
215
+ break
216
+ case 'blur':
217
+ filterByProp.blur = 'blur(' + value + 'px)'
218
+ break
219
+ case 'grayscale':
220
+ filterByProp.grayscale = 'grayscale(' + value + ')'
221
+ break
222
+ case 'saturate':
223
+ filterByProp.saturate = 'saturate(' + value + ')'
224
+ break
225
+ case 'brightness':
226
+ filterByProp.brightness = 'brightness(' + value + ')'
227
+ break
228
+ case 'clip':
229
+ element.style.clipPath = 'inset(0 ' + clampPercent(100 - value) + '% 0 0)'
230
+ break
231
+ case 'clip-y':
232
+ element.style.clipPath = 'inset(0 0 ' + clampPercent(100 - value) + '% 0)'
233
+ break
234
+ case 'count':
235
+ element.style.setProperty('--tq-count', String(Math.round(value)))
236
+ if (!element.hasAttribute(COUNT_ATTR)) {
237
+ element.setAttribute(COUNT_ATTR, '')
238
+ }
239
+ break
240
+ }
241
+ }
242
+ if (translateX !== undefined || translateY !== undefined) {
243
+ element.style.translate = (translateX || '0px') + ' ' + (translateY || '0px')
244
+ }
245
+ if (scale !== undefined) {
246
+ element.style.scale = scale
247
+ }
248
+ if (rotate !== undefined) {
249
+ element.style.rotate = rotate
250
+ }
251
+ const filterParts = FILTER_PROP_ORDER.map((prop) => filterByProp[prop]).filter(Boolean)
252
+ if (filterParts.length > 0) {
253
+ element.style.filter = filterParts.join(' ')
254
+ }
255
+ }
256
+
257
+ // A scene set to keep the story for visitors who ask for less motion
258
+ // (scene-reduced-motion "story"): the scroll still drives the scene, chapters
259
+ // still come and go and every text still appears in turn, but nothing moves.
260
+ // Slides, zooms, rotations and wipes are dropped, so each element stays where
261
+ // the layout puts it; fades and filters still follow the scroll; a counting
262
+ // number shows the figure it counts to. Mirrors the editor's lane contract.
263
+ const MOVEMENT_PROPS = ['x', 'y', 'scale', 'rotate', 'rotate-x', 'rotate-y', 'clip', 'clip-y']
264
+
265
+ const storyLanes = (lanes) =>
266
+ lanes
267
+ .filter((lane) => !MOVEMENT_PROPS.includes(lane.prop))
268
+ .map((lane) => {
269
+ if (lane.prop !== 'count') {
270
+ return lane
271
+ }
272
+ const figure = lane.values[lane.values.length - 1]
273
+ return { prop: 'count', at: [0, 1], values: [figure, figure] }
274
+ })
275
+
276
+ // What a moving lane painted, taken off again when the scene switches to the
277
+ // story without movement while the page is open.
278
+ const clearMovement = (element, lanes) => {
279
+ for (const lane of lanes) {
280
+ if (lane.prop === 'x' || lane.prop === 'y') {
281
+ element.style.translate = ''
282
+ } else if (lane.prop === 'scale') {
283
+ element.style.scale = ''
284
+ } else if (lane.prop === 'rotate' || lane.prop === 'rotate-x' || lane.prop === 'rotate-y') {
285
+ element.style.rotate = ''
286
+ } else if (lane.prop === 'clip' || lane.prop === 'clip-y') {
287
+ element.style.clipPath = ''
288
+ }
289
+ }
290
+ }
291
+
292
+ // A row wider than the stage that travelled sideways with the scroll (a
293
+ // rail) would show only its first cards once the movement is dropped, so it
294
+ // becomes a row the visitor swipes: every card stays reachable.
295
+ const SWIPE_ATTR = 'data-scene-swipe'
296
+ const letRowsSwipe = (bound, stage) => {
297
+ if (!stage) {
298
+ return
299
+ }
300
+ const stageWidth = stage.getBoundingClientRect().width
301
+ for (const child of bound) {
302
+ const travels = child.lanes.some((lane) => lane.prop === 'x')
303
+ if (!travels || child.element.getBoundingClientRect().width <= stageWidth + 1) {
304
+ continue
305
+ }
306
+ child.element.style.maxWidth = '100%'
307
+ child.element.style.overflowX = 'auto'
308
+ child.element.setAttribute(SWIPE_ATTR, '')
309
+ }
310
+ }
311
+
312
+ const collectBound = (track) => {
313
+ const bound = []
314
+ track.querySelectorAll('[' + SCROLL_BIND_ATTR + ']').forEach((element) => {
315
+ const lanes = parseScrollBind(element.getAttribute(SCROLL_BIND_ATTR))
316
+ if (lanes.length > 0) {
317
+ bound.push({ element, lanes })
318
+ }
319
+ })
320
+ return bound
321
+ }
322
+
323
+ const normalizeSceneLength = (value) => {
324
+ const match = /^(\\d{2,4})vh$/.exec(String(value || '').trim())
325
+ if (!match) {
326
+ return '300vh'
327
+ }
328
+ // Hard safety bound only — the editor owns the story-sized soft cap.
329
+ return Math.min(4000, Math.max(100, Number(match[1]))) + 'vh'
330
+ }`
@@ -0,0 +1,12 @@
1
+ export { settledMomentForLanes, MomentLane } from './moment'
2
+ export { activeChapterIndex } from './chapter-index'
3
+ export { passedScenePoints, scenePointList, scenePointRank } from './points'
4
+ export { unclipStickyAncestors } from './unclip'
5
+ export { scrollSceneEngineSource as engineSource } from './engine-source'
6
+ export {
7
+ announcementsSource,
8
+ restackBodySource,
9
+ chapterHelpersSource,
10
+ anchorNavigationBodySource,
11
+ gentleSnapBodySource,
12
+ } from './controller-source'
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The progress where a chapter is fully ON STAGE — every lane has arrived
3
+ * and none has started leaving — the centre of that settled span, in the
4
+ * lanes' own (absolute) progress space. The gentle snap and anchor
5
+ * navigation aim here instead of the window's midpoint, which catches the
6
+ * last beat still rising (a half-visible line, a half-arrived card).
7
+ * Null when nothing settles (a chapter made only of through-motion, e.g. a
8
+ * crossing line — its best moment is the window's middle, the caller's
9
+ * fallback).
10
+ *
11
+ * MIRRORED VERBATIM from teleport-gui's chapter-window.ts (the editor draws
12
+ * the same moment on the timeline). Plain JS inside: this function's SOURCE
13
+ * is injected into the generated TqScrollScene runtime via toString(), so no
14
+ * TypeScript-only runtime constructs and no template literals.
15
+ */
16
+ export interface MomentLane {
17
+ at: number[]
18
+ values: number[]
19
+ }
20
+
21
+ export function settledMomentForLanes(lanes: MomentLane[]): number | null {
22
+ let arrive = -Infinity
23
+ let leave = Infinity
24
+ let lastStop = -Infinity
25
+ let found = false
26
+ for (const lane of lanes) {
27
+ if (lane.at.length < 3) {
28
+ continue
29
+ }
30
+ let settledAt: number | null = null
31
+ for (let index = 1; index < lane.values.length - 1; index++) {
32
+ if (lane.values[index] === lane.values[index + 1] && lane.values[index] !== lane.values[0]) {
33
+ settledAt = index
34
+ break
35
+ }
36
+ }
37
+ if (settledAt === null && lane.values[0] === lane.values[1]) {
38
+ settledAt = 0
39
+ }
40
+ if (settledAt === null) {
41
+ continue
42
+ }
43
+ found = true
44
+ arrive = Math.max(arrive, lane.at[settledAt])
45
+ lastStop = Math.max(lastStop, lane.at[lane.at.length - 1])
46
+ const settledValue = lane.values[settledAt]
47
+ for (let index = settledAt + 1; index < lane.values.length; index++) {
48
+ if (lane.values[index] !== settledValue) {
49
+ leave = Math.min(leave, lane.at[index - 1])
50
+ break
51
+ }
52
+ }
53
+ }
54
+ if (!found) {
55
+ return null
56
+ }
57
+ if (leave === Infinity) {
58
+ leave = lastStop
59
+ }
60
+ if (leave < arrive) {
61
+ return null
62
+ }
63
+ return Math.round(((arrive + leave) / 2) * 10000) / 10000
64
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The "scrolled past a point" contract of a published Scroll Scene: the four
3
+ * plain points in scroll order, which of them a move from one progress to
4
+ * another passes DOWNWARD, and the rank of a point for late listeners. The
5
+ * scene announces every passed point as a `tq-scene-point-passed` DOM event
6
+ * on its track (the element carrying the scene's id) and stamps the furthest
7
+ * point passed so far as `data-scene-point`.
8
+ *
9
+ * Plain JS inside: these functions' SOURCE is injected into the generated
10
+ * runtime via toString(), so no TypeScript-only runtime constructs, no
11
+ * template literals and no reference to anything outside the function. The
12
+ * list mirrors SCROLL_POINT_PROGRESS in teleport-plugin-next-workflows
13
+ * (pinned by a test).
14
+ */
15
+ export function scenePointList(): Array<{ key: string; at: number }> {
16
+ return [
17
+ { key: 'quarter', at: 0.25 },
18
+ { key: 'half', at: 0.5 },
19
+ { key: 'three-quarters', at: 0.75 },
20
+ { key: 'end', at: 1 },
21
+ ]
22
+ }
23
+
24
+ /** Points passed going DOWN from one progress to another, in scroll order; none when going up. */
25
+ export function passedScenePoints(from: number, to: number): string[] {
26
+ const passed = []
27
+ const points = scenePointList()
28
+ for (let index = 0; index < points.length; index++) {
29
+ const point = points[index]
30
+ if (point.at > from + 0.000001 && point.at <= to + 0.000001) {
31
+ passed.push(point.key)
32
+ }
33
+ }
34
+ return passed
35
+ }
36
+
37
+ /** 1-based rank of a point in scroll order, 0 for anything else. */
38
+ export function scenePointRank(key: string): number {
39
+ const points = scenePointList()
40
+ for (let index = 0; index < points.length; index++) {
41
+ if (points[index].key === key) {
42
+ return index + 1
43
+ }
44
+ }
45
+ return 0
46
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Keep a pinned scene's stage pinnable: an ancestor with `overflow: hidden`
3
+ * (either axis) turns position:sticky off, silently. Authors and the AI write
4
+ * it for good reasons (a horizontal rail must clip its overhang — run
5
+ * e7f332df wrote `section.rail-scene { overflow: hidden }` around the
6
+ * homepage's story and the stage scrolled straight through). `overflow: clip`
7
+ * keeps the clipping and, unlike hidden, does not make the element a scroll
8
+ * container, so the stage sticks again. Ancestors that really scroll (auto /
9
+ * scroll) are left alone: sticky then rightly binds to that container.
10
+ *
11
+ * Injected into the generated widget via toString — no imports, no template
12
+ * literals (the generated project transpiles it).
13
+ */
14
+ export function unclipStickyAncestors(
15
+ track: Element,
16
+ getStyle: (element: Element) => CSSStyleDeclaration
17
+ ): string[] {
18
+ const unclipped: string[] = []
19
+ let element = track.parentElement
20
+ while (element && element !== element.ownerDocument.documentElement) {
21
+ const style = getStyle(element)
22
+ const both = style.overflow === 'hidden'
23
+ const axes: string[] = []
24
+ if (both || style.overflowX === 'hidden') {
25
+ axes.push('x')
26
+ }
27
+ if (both || style.overflowY === 'hidden') {
28
+ axes.push('y')
29
+ }
30
+ if (axes.length > 0) {
31
+ const host = element as HTMLElement
32
+ for (let i = 0; i < axes.length; i++) {
33
+ host.style.setProperty('overflow-' + axes[i], 'clip')
34
+ }
35
+ host.setAttribute('data-scene-unclipped', axes.join(''))
36
+ unclipped.push(
37
+ host.tagName.toLowerCase() +
38
+ (host.className ? '.' + String(host.className).split(' ')[0] : '')
39
+ )
40
+ }
41
+ element = element.parentElement
42
+ }
43
+ return unclipped
44
+ }