@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.
- 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/dist/cjs/index.d.ts +5 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +9 -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/esm/index.d.ts +5 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -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/package.json +3 -3
- package/src/index.ts +8 -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
|
@@ -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,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
|
+
}`
|