@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,591 @@
1
+ /**
2
+ * The scroll-video maths as SOURCE TEXT: the clip window, the progress a
3
+ * rectangle has made through the viewport, and the clip held in memory. Shared
4
+ * word for word by the Next export (TqScrollVideo) and the HTML export's
5
+ * vanilla controller; mirrors the canvas renderer's scroll-video-runtime and
6
+ * its full-file buffering.
7
+ */
8
+ export const scrollVideoEngineSource =
9
+ (): string => `const clamp01 = (value) => Math.min(1, Math.max(0, value))
10
+
11
+ const MIN_WINDOW_SPAN = 1
12
+
13
+ const normalizeWindow = (startRaw, endRaw) => {
14
+ const parse = (value, fallback) => {
15
+ const parsed = Number(value)
16
+ return Number.isFinite(parsed) ? Math.min(100, Math.max(0, parsed)) : fallback
17
+ }
18
+ const start = parse(startRaw, 0)
19
+ const end = parse(endRaw, 100)
20
+ if (end - start >= MIN_WINDOW_SPAN) {
21
+ return { start, end }
22
+ }
23
+ return start <= 100 - MIN_WINDOW_SPAN
24
+ ? { start, end: start + MIN_WINDOW_SPAN }
25
+ : { start: 100 - MIN_WINDOW_SPAN, end: 100 }
26
+ }
27
+
28
+ // Mirrors the canvas renderer's scrollProgressForOffset for the two modes a
29
+ // scrub video needs: 'contained' (a pinned scene's stage holds while its track
30
+ // scrolls under it) and 'pass' (a plain trip through the viewport).
31
+ const progressForRect = (rect, viewportHeight, offset) => {
32
+ const vh = viewportHeight || 1
33
+ let raw
34
+ if (offset === 'contained') {
35
+ const travel = rect.height - vh
36
+ raw = travel > 0 ? -rect.top / travel : (vh - rect.top) / (vh + rect.height)
37
+ } else {
38
+ raw = (vh - rect.top) / (vh + rect.height)
39
+ }
40
+ return clamp01(raw)
41
+ }
42
+
43
+ // Where in the clip a window progress lands. Played back to the start, the
44
+ // clip reaches its last frame halfway through the window and its first frame
45
+ // again at the end (the editor's clipPositionForWindowProgress, word for word).
46
+ const clipPositionFor = (windowProgress, backToStart) => {
47
+ const position = clamp01(windowProgress)
48
+ if (!backToStart) {
49
+ return position
50
+ }
51
+ return position <= 0.5 ? position * 2 : (1 - position) * 2
52
+ }
53
+
54
+ // ── Holding the clip ──
55
+ // A scroll clip is consumed frame by frame at arbitrary positions, so a seek
56
+ // must never wait on the network: a streaming <video> holds only what it has
57
+ // fetched around the playhead (Safari on iPhone fetches nothing ahead), so a
58
+ // fast scroll outruns it and the frame freezes. Two ways to hold it, tried in
59
+ // order once the scene is about a screen away (the assets bucket allows the
60
+ // read from any site):
61
+ // 1. Streamed by the second. A clip the media worker made is fragmented,
62
+ // with an index of every fragment in its first bytes. With MediaSource
63
+ // the page keeps the seconds around the scroll position in the element
64
+ // and fetches fragments as the position moves — a clip of any length costs
65
+ // the same memory and is ready to scrub almost at once.
66
+ // 2. Whole, in memory: the file fetched once and played from a blob — an
67
+ // older encode, an upload, a browser without MediaSource (an iPhone), an
68
+ // index that could not be read.
69
+ // Either way the streamed element keeps scrubbing meanwhile; then a copy takes
70
+ // its place at the frame on screen and the streamed one lets go of its
71
+ // download. A fresh element, not a new src on the old one: engines get stuck
72
+ // after a src swap on an element that has already seeked. The page asks the
73
+ // streamed element for its first frame only (preload="metadata"), so the clip
74
+ // is not downloaded twice; where the clip cannot be held (a host that refuses,
75
+ // a whole clip too big, no fetch) it goes back to preloading the way it
76
+ // streamed before. A visitor saving data keeps the first frame only.
77
+ const MAX_BUFFERED_CLIP_BYTES = 100 * 1024 * 1024
78
+ // The first bytes of a clip: its header and, for a fragmented clip, the index.
79
+ const CLIP_HEAD_BYTES = 65536
80
+ // Streamed: the seconds kept around the scroll position — fetched in order of
81
+ // need (the second on screen, then ahead, then behind) and let go beyond it. A
82
+ // clip shorter than twice this ends up whole in the element, like before, only
83
+ // scrubbable long before it has all arrived.
84
+ const STREAM_KEEP_SECONDS = 12
85
+ // One request fetches consecutive missing fragments up to this much.
86
+ const STREAM_REQUEST_SECONDS = 2
87
+ const STREAM_REQUEST_BYTES = 6 * 1024 * 1024
88
+
89
+ // The fragment index a scrub encode carries in its first bytes (ftyp, moov,
90
+ // then a sidx naming every fragment): where each second of the clip is in the
91
+ // file. Null for a file without one, which is then held whole.
92
+ const readClipIndex = (head) => {
93
+ const view = new DataView(head)
94
+ const boxType = (at) => String.fromCharCode(view.getUint8(at + 4), view.getUint8(at + 5), view.getUint8(at + 6), view.getUint8(at + 7))
95
+ let at = 0
96
+ let moovEnd = 0
97
+ let sidx = null
98
+ while (at + 8 <= head.byteLength) {
99
+ const size = view.getUint32(at)
100
+ const type = boxType(at)
101
+ if (size < 8) {
102
+ return null
103
+ }
104
+ if (type === 'moov') {
105
+ moovEnd = at + size
106
+ } else if (type === 'sidx') {
107
+ sidx = { at: at, size: size }
108
+ break
109
+ } else if (type !== 'ftyp' && type !== 'free' && type !== 'skip') {
110
+ break
111
+ }
112
+ at += size
113
+ }
114
+ if (!moovEnd || !sidx || sidx.at + sidx.size > head.byteLength || sidx.at < moovEnd) {
115
+ return null
116
+ }
117
+ // the codec, from the avcC box inside moov: H.264, profile and level as the file says
118
+ let codec = null
119
+ const bytes = new Uint8Array(head)
120
+ for (let i = 0; i < moovEnd - 8; i++) {
121
+ if (bytes[i] === 0x61 && bytes[i + 1] === 0x76 && bytes[i + 2] === 0x63 && bytes[i + 3] === 0x43) {
122
+ const hex = (value) => (value < 16 ? '0' : '') + value.toString(16).toUpperCase()
123
+ codec = 'avc1.' + hex(bytes[i + 5]) + hex(bytes[i + 6]) + hex(bytes[i + 7])
124
+ break
125
+ }
126
+ }
127
+ if (!codec) {
128
+ return null
129
+ }
130
+ const body = sidx.at + 8
131
+ const version = view.getUint8(body)
132
+ const timescale = view.getUint32(body + 8)
133
+ let cursor = body + 12
134
+ let firstOffset
135
+ if (version === 0) {
136
+ firstOffset = view.getUint32(cursor + 4)
137
+ cursor += 8
138
+ } else {
139
+ firstOffset = Number(view.getBigUint64(cursor + 8))
140
+ cursor += 16
141
+ }
142
+ const count = view.getUint16(cursor + 2)
143
+ cursor += 4
144
+ if (!timescale || !count) {
145
+ return null
146
+ }
147
+ const fragments = []
148
+ let start = sidx.at + sidx.size + firstOffset
149
+ let time = 0
150
+ for (let i = 0; i < count; i++) {
151
+ const size = view.getUint32(cursor) & 0x7fffffff
152
+ const duration = view.getUint32(cursor + 4) / timescale
153
+ fragments.push({ start: start, end: start + size - 1, time: time, duration: duration })
154
+ start += size
155
+ time += duration
156
+ cursor += 12
157
+ }
158
+ return { initEnd: moovEnd, codec: codec, fragments: fragments, duration: time }
159
+ }
160
+
161
+ // A copy of the streamed element, to take its place once it shows the frame on screen.
162
+ const copyOf = (video) => {
163
+ const copy = video.cloneNode(false)
164
+ copy.removeAttribute('src')
165
+ copy.muted = true
166
+ copy.style.opacity = '0'
167
+ return copy
168
+ }
169
+
170
+ // Streams the clip by the second through MediaSource. Null where the browser
171
+ // cannot (no MediaSource, or not for this codec) — the clip is then held whole.
172
+ // Ranged requests go to the clip under a URL of their own: WebKit holds a second
173
+ // request for a URL it is still downloading (the streamed element's) until that
174
+ // one is done, which would delay the first second by the whole file.
175
+ const rangedUrl = (src) => src + (src.indexOf('?') >= 0 ? '&' : '?') + 'tq-stream'
176
+
177
+ const streamClip = (video, src, head, index, onSwap, signal, onFail) => {
178
+ const MediaSourceCtor = window.MediaSource
179
+ const mime = 'video/mp4; codecs="' + index.codec + '"'
180
+ if (!MediaSourceCtor || typeof MediaSourceCtor.isTypeSupported !== 'function' || !MediaSourceCtor.isTypeSupported(mime)) {
181
+ return null
182
+ }
183
+ const fragments = index.fragments
184
+ const source = new MediaSourceCtor()
185
+ const objectUrl = URL.createObjectURL(source)
186
+ const copy = copyOf(video)
187
+ copy.setAttribute('data-clip-held', 'stream')
188
+ let buffer = null
189
+ let loaded = []
190
+ let failed = 0
191
+ let busy = false
192
+ let inFlight = false
193
+ let pausedUntil = 0
194
+ let wanted = 0
195
+ let swapped = false
196
+ let stopped = false
197
+ const pending = []
198
+ const fragmentAt = (seconds) => {
199
+ for (let i = 0; i < fragments.length; i++) {
200
+ if (seconds < fragments[i].time + fragments[i].duration) {
201
+ return i
202
+ }
203
+ }
204
+ return fragments.length - 1
205
+ }
206
+ const takeOver = () => {
207
+ if (stopped) {
208
+ return
209
+ }
210
+ copy.style.opacity = ''
211
+ video.style.display = 'none'
212
+ video.removeAttribute('src')
213
+ video.load()
214
+ onSwap(copy)
215
+ }
216
+ const run = () => {
217
+ if (stopped || !buffer || busy || buffer.updating || !pending.length) {
218
+ return
219
+ }
220
+ const task = pending.shift()
221
+ busy = true
222
+ try {
223
+ if (task.remove) {
224
+ buffer.remove(task.from, task.to)
225
+ } else {
226
+ buffer.appendBuffer(task.data)
227
+ }
228
+ } catch (e) {
229
+ busy = false
230
+ if (task.remove) {
231
+ run()
232
+ return
233
+ }
234
+ // no room for another second: let every other second go, then try once more
235
+ if (!task.retried) {
236
+ task.retried = true
237
+ letGo(true)
238
+ pending.push(task)
239
+ } else {
240
+ for (let i = task.from; i <= task.to; i++) {
241
+ loaded[i] = false
242
+ }
243
+ }
244
+ run()
245
+ }
246
+ }
247
+ const onUpdateEnd = () => {
248
+ busy = false
249
+ if (stopped) {
250
+ return
251
+ }
252
+ if (!swapped && loaded[fragmentAt(wanted)]) {
253
+ swapped = true
254
+ copy.addEventListener('seeked', takeOver, { once: true })
255
+ copy.currentTime = wanted
256
+ }
257
+ run()
258
+ fetchNext()
259
+ }
260
+ const letGo = (everything) => {
261
+ const keepFrom = everything ? wanted : wanted - STREAM_KEEP_SECONDS
262
+ const keepTo = everything ? wanted : wanted + STREAM_KEEP_SECONDS
263
+ let from = -1
264
+ for (let i = 0; i <= fragments.length; i++) {
265
+ const far = i < fragments.length && loaded[i] && (fragments[i].time + fragments[i].duration < keepFrom || fragments[i].time > keepTo)
266
+ if (far && from < 0) {
267
+ from = i
268
+ }
269
+ if (!far && from >= 0) {
270
+ for (let k = from; k < i; k++) {
271
+ loaded[k] = false
272
+ }
273
+ pending.push({ remove: true, from: fragments[from].time, to: fragments[i - 1].time + fragments[i - 1].duration })
274
+ from = -1
275
+ }
276
+ }
277
+ }
278
+ const fetchNext = () => {
279
+ if (stopped || !buffer || inFlight || Date.now() < pausedUntil) {
280
+ return
281
+ }
282
+ const at = fragmentAt(wanted)
283
+ // the second on screen first, then ahead, then behind, out to what is kept
284
+ const order = [at]
285
+ for (let k = 1; k < fragments.length; k++) {
286
+ if (at + k < fragments.length && fragments[at + k].time - wanted <= STREAM_KEEP_SECONDS) {
287
+ order.push(at + k)
288
+ }
289
+ if (
290
+ at - k >= 0 &&
291
+ wanted - (fragments[at - k].time + fragments[at - k].duration) <= STREAM_KEEP_SECONDS
292
+ ) {
293
+ order.push(at - k)
294
+ }
295
+ }
296
+ let first = -1
297
+ for (let i = 0; i < order.length; i++) {
298
+ if (!loaded[order[i]]) {
299
+ first = order[i]
300
+ break
301
+ }
302
+ }
303
+ if (first < 0) {
304
+ return
305
+ }
306
+ let last = first
307
+ while (
308
+ last + 1 < fragments.length &&
309
+ !loaded[last + 1] &&
310
+ fragments[last + 1].end - fragments[first].start < STREAM_REQUEST_BYTES &&
311
+ fragments[last + 1].time + fragments[last + 1].duration - fragments[first].time <= STREAM_REQUEST_SECONDS
312
+ ) {
313
+ last++
314
+ }
315
+ inFlight = true
316
+ fetch(rangedUrl(src), { headers: { range: 'bytes=' + fragments[first].start + '-' + fragments[last].end }, signal: signal })
317
+ .then((response) => (response.status === 206 ? response.arrayBuffer() : Promise.reject(new Error('no range'))))
318
+ .then((data) => {
319
+ inFlight = false
320
+ if (stopped) {
321
+ return
322
+ }
323
+ for (let i = first; i <= last; i++) {
324
+ loaded[i] = true
325
+ }
326
+ letGo(false)
327
+ pending.push({ data: data, from: first, to: last })
328
+ run()
329
+ })
330
+ .catch(() => {
331
+ inFlight = false
332
+ if (stopped) {
333
+ return
334
+ }
335
+ // a second that could not be fetched: hold the clip whole instead when
336
+ // nothing has played yet, otherwise try again in a moment
337
+ failed++
338
+ pausedUntil = Date.now() + 2000
339
+ if (!swapped && failed >= 2) {
340
+ onFail()
341
+ }
342
+ })
343
+ }
344
+ source.addEventListener('sourceopen', () => {
345
+ if (stopped) {
346
+ return
347
+ }
348
+ try {
349
+ buffer = source.addSourceBuffer(mime)
350
+ } catch (e) {
351
+ onFail()
352
+ return
353
+ }
354
+ buffer.mode = 'segments'
355
+ buffer.addEventListener('updateend', onUpdateEnd)
356
+ buffer.addEventListener('error', onFail)
357
+ // the clip's length comes from the index, not from a header written before the fragments
358
+ source.duration = index.duration
359
+ pending.push({ data: head.slice(0, index.initEnd), from: 0, to: -1 })
360
+ run()
361
+ fetchNext()
362
+ }, { once: true })
363
+ copy.src = objectUrl
364
+ video.parentNode.insertBefore(copy, video.nextSibling)
365
+ // The streamed element's own download is let go: the copy takes over long
366
+ // before it would show a frame (a browser may fetch the whole file for the
367
+ // element's metadata), and its bytes would only slow the seconds down.
368
+ video.removeAttribute('src')
369
+ video.load()
370
+ return {
371
+ want: (progress) => {
372
+ wanted = clamp01(progress) * index.duration
373
+ fetchNext()
374
+ },
375
+ stop: () => {
376
+ stopped = true
377
+ copy.remove()
378
+ URL.revokeObjectURL(objectUrl)
379
+ },
380
+ }
381
+ }
382
+
383
+ // Holds the clip whole: fetched once (or already fetched) and played from memory.
384
+ const holdWholeClip = (video, src, onSwap, signal, prefetched, onFail) => {
385
+ let stopped = false
386
+ let objectUrl = null
387
+ let copy = null
388
+ const takeOver = () => {
389
+ if (stopped) {
390
+ return
391
+ }
392
+ copy.style.opacity = ''
393
+ video.style.display = 'none'
394
+ video.removeAttribute('src')
395
+ video.load()
396
+ onSwap(copy)
397
+ }
398
+ const hold = (blob) => {
399
+ if (stopped) {
400
+ return
401
+ }
402
+ objectUrl = URL.createObjectURL(blob)
403
+ copy = copyOf(video)
404
+ copy.setAttribute('data-clip-held', 'memory')
405
+ copy.addEventListener('loadedmetadata', () => {
406
+ copy.addEventListener('seeked', takeOver, { once: true })
407
+ copy.currentTime = video.currentTime
408
+ }, { once: true })
409
+ copy.src = objectUrl
410
+ video.parentNode.insertBefore(copy, video.nextSibling)
411
+ }
412
+ if (prefetched) {
413
+ hold(prefetched)
414
+ } else {
415
+ fetch(src, signal ? { signal: signal } : undefined)
416
+ .then((response) => {
417
+ if (!response.ok || Number(response.headers.get('content-length')) > MAX_BUFFERED_CLIP_BYTES) {
418
+ throw new Error('keep streaming')
419
+ }
420
+ return response.blob()
421
+ })
422
+ .then(hold)
423
+ .catch(() => {
424
+ if (!stopped) {
425
+ onFail()
426
+ }
427
+ })
428
+ }
429
+ return {
430
+ want: () => {},
431
+ stop: () => {
432
+ stopped = true
433
+ if (copy) {
434
+ copy.remove()
435
+ }
436
+ if (objectUrl) {
437
+ URL.revokeObjectURL(objectUrl)
438
+ }
439
+ },
440
+ }
441
+ }
442
+
443
+ const holdClip = (host, video, src, onSwap) => {
444
+ const noHold = { want: () => {}, stop: () => {} }
445
+ const stream = () => {
446
+ video.preload = 'auto'
447
+ }
448
+ if (navigator.connection && navigator.connection.saveData) {
449
+ return noHold
450
+ }
451
+ if (!src || /^(blob|data):/.test(src) || typeof fetch !== 'function' || !window.URL || !URL.createObjectURL) {
452
+ stream()
453
+ return noHold
454
+ }
455
+ let stopped = false
456
+ let observer = null
457
+ let holder = null
458
+ let released = false
459
+ let wanted = 0
460
+ const controller = typeof AbortController === 'function' ? new AbortController() : null
461
+ const signal = controller ? controller.signal : undefined
462
+ const swapped = (copy) => {
463
+ released = true
464
+ onSwap(copy)
465
+ }
466
+ const giveUp = () => {
467
+ if (!stopped && !released) {
468
+ stream()
469
+ }
470
+ }
471
+ const whole = (prefetched) => {
472
+ holder = holdWholeClip(video, src, swapped, signal, prefetched, giveUp)
473
+ }
474
+ // The first bytes tell which way the clip can be held. A host that will not
475
+ // answer a ranged request (or its cross-origin permission) still serves the
476
+ // whole file to a plain request, the way clips were held before.
477
+ const load = () => {
478
+ fetch(rangedUrl(src), { headers: { range: 'bytes=0-' + (CLIP_HEAD_BYTES - 1) }, signal: signal })
479
+ .then((response) => {
480
+ if (!response.ok) {
481
+ throw new Error('keep streaming')
482
+ }
483
+ const total = response.status === 206
484
+ ? Number((response.headers.get('content-range') || '').split('/')[1])
485
+ : Number(response.headers.get('content-length'))
486
+ return response.arrayBuffer().then((head) => ({ head: head, partial: response.status === 206, total: total }))
487
+ })
488
+ .then((answer) => {
489
+ if (stopped) {
490
+ return
491
+ }
492
+ // a host that answered with the whole file: it is already here
493
+ if (!answer.partial) {
494
+ if (answer.head.byteLength > MAX_BUFFERED_CLIP_BYTES) {
495
+ throw new Error('too big')
496
+ }
497
+ whole(new Blob([answer.head], { type: 'video/mp4' }))
498
+ return
499
+ }
500
+ const index = readClipIndex(answer.head)
501
+ if (index && typeof MediaSource !== 'undefined') {
502
+ holder = streamClip(video, src, answer.head, index, swapped, signal, () => {
503
+ if (!stopped && !released) {
504
+ holder.stop()
505
+ whole(null)
506
+ }
507
+ })
508
+ if (holder) {
509
+ holder.want(wanted)
510
+ return
511
+ }
512
+ }
513
+ if (answer.total > MAX_BUFFERED_CLIP_BYTES) {
514
+ throw new Error('too big')
515
+ }
516
+ whole(null)
517
+ })
518
+ .catch((error) => {
519
+ if (stopped) {
520
+ return
521
+ }
522
+ if (String(error && error.message) === 'too big') {
523
+ giveUp()
524
+ return
525
+ }
526
+ whole(null)
527
+ })
528
+ }
529
+ // A scene about a screen away loads at once — when it is there from the
530
+ // start, when it comes near, or at the first scrub request that finds it
531
+ // near (an observer's first word waits for a rendering update, which a page
532
+ // still loading its media may not give for a while).
533
+ let started = false
534
+ const near = () => {
535
+ const box = host.getBoundingClientRect()
536
+ const screen = window.innerHeight || 1
537
+ return box.bottom > -screen && box.top < screen * 2
538
+ }
539
+ const start = () => {
540
+ if (started) {
541
+ return
542
+ }
543
+ started = true
544
+ if (observer) {
545
+ observer.disconnect()
546
+ observer = null
547
+ }
548
+ load()
549
+ }
550
+ if (near()) {
551
+ start()
552
+ } else if (typeof IntersectionObserver === 'function') {
553
+ observer = new IntersectionObserver((entries) => {
554
+ if (entries.some((entry) => entry.isIntersecting)) {
555
+ start()
556
+ }
557
+ }, { rootMargin: '100% 0px' })
558
+ observer.observe(host)
559
+ } else {
560
+ start()
561
+ }
562
+ return {
563
+ // where the scrub is, as clip progress (0-1, known before any metadata): a
564
+ // streamed clip keeps the seconds around it ready
565
+ want: (progress) => {
566
+ wanted = progress
567
+ if (!started && near()) {
568
+ start()
569
+ }
570
+ if (holder) {
571
+ holder.want(progress)
572
+ }
573
+ },
574
+ stop: () => {
575
+ stopped = true
576
+ if (observer) {
577
+ observer.disconnect()
578
+ }
579
+ if (controller) {
580
+ controller.abort()
581
+ }
582
+ if (holder) {
583
+ holder.stop()
584
+ }
585
+ video.style.display = ''
586
+ if (video.isConnected && !video.getAttribute('src')) {
587
+ video.setAttribute('src', src)
588
+ }
589
+ },
590
+ }
591
+ }`
@@ -0,0 +1,3 @@
1
+ export * from './variants'
2
+ export * from './view-transition-css'
3
+ export * from './morph-source'
@@ -0,0 +1,86 @@
1
+ import { MORPH_NAME } from './view-transition-css'
2
+
3
+ /**
4
+ * Which image flies from one page into the next, as SOURCE TEXT both exports
5
+ * run in the browser (the static HTML export in its page head, the Next export
6
+ * inside TqPageTransition).
7
+ *
8
+ * The pair is found when the visitor follows a link, not stamped at
9
+ * generation time: the image inside the link that was clicked, or inside any
10
+ * other visible link to the same page (a card whose title and picture both
11
+ * link), flies to the image with the same address on the arriving page. So a
12
+ * product card grows into its details hero, a project thumbnail into its case
13
+ * study, with nothing to set up and no knowledge of how the data is bound.
14
+ * Only one pair is ever named: two elements sharing a transition name would
15
+ * cancel the whole transition. A picture the Next export serves through
16
+ * next/image is matched by the picture it serves, not by the shared
17
+ * `/_next/image` address every such picture has.
18
+ *
19
+ * Plain ES5 inside: the text runs untranspiled in a page head.
20
+ */
21
+ export const morphHelpersSource = (): string => `var TQ_MORPH_NAME = '${MORPH_NAME}'
22
+ function tqMorphPath(url) {
23
+ try {
24
+ var parsed = new URL(url, location.href)
25
+ return parsed.origin + parsed.pathname.replace(/\\/(index\\.html)?$/, '').replace(/\\.html$/, '') + parsed.search
26
+ } catch (e) {
27
+ return String(url)
28
+ }
29
+ }
30
+ function tqMorphImageKey(url) {
31
+ try {
32
+ var parsed = new URL(url, location.href)
33
+ var served = /\\/_next\\/image$/.test(parsed.pathname) ? parsed.searchParams.get('url') : null
34
+ return served ? tqMorphImageKey(served) : parsed.origin + parsed.pathname
35
+ } catch (e) {
36
+ return String(url)
37
+ }
38
+ }
39
+ function tqMorphVisible(element) {
40
+ var rect = element.getBoundingClientRect()
41
+ return rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 &&
42
+ rect.top < window.innerHeight && rect.left < window.innerWidth
43
+ }
44
+ function tqMorphClear() {
45
+ var images = document.images
46
+ for (var i = 0; i < images.length; i++) {
47
+ if (images[i].style.viewTransitionName === TQ_MORPH_NAME) {
48
+ images[i].style.viewTransitionName = ''
49
+ }
50
+ }
51
+ }
52
+ function tqMorphSource(destination, clicked) {
53
+ var target = tqMorphPath(destination)
54
+ var anchors = Array.prototype.slice.call(document.querySelectorAll('a[href]')).filter(function (anchor) {
55
+ return tqMorphPath(anchor.href) === target
56
+ })
57
+ if (clicked && anchors.indexOf(clicked) > 0) {
58
+ anchors.splice(anchors.indexOf(clicked), 1)
59
+ anchors.unshift(clicked)
60
+ }
61
+ for (var i = 0; i < anchors.length; i++) {
62
+ var image = anchors[i].querySelector('img')
63
+ if (image && (image.currentSrc || image.src) && tqMorphVisible(image)) {
64
+ return image
65
+ }
66
+ }
67
+ return null
68
+ }
69
+ function tqMorphTarget(src) {
70
+ var key = tqMorphImageKey(src)
71
+ var loose = null
72
+ var images = document.images
73
+ for (var i = 0; i < images.length; i++) {
74
+ var own = images[i].currentSrc || images[i].src
75
+ if (!own || images[i].getBoundingClientRect().width === 0) {
76
+ continue
77
+ }
78
+ if (own === src) {
79
+ return images[i]
80
+ }
81
+ if (!loose && tqMorphImageKey(own) === key) {
82
+ loose = images[i]
83
+ }
84
+ }
85
+ return loose
86
+ }`