@hyperframes/core 0.8.57 → 0.8.59
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/dist/audioFade.d.ts +24 -0
- package/dist/audioFade.d.ts.map +1 -0
- package/dist/audioFade.js +56 -0
- package/dist/audioFade.js.map +1 -0
- package/dist/audioGain.d.ts +4 -0
- package/dist/audioGain.d.ts.map +1 -1
- package/dist/audioGain.js +12 -4
- package/dist/audioGain.js.map +1 -1
- package/dist/compositionReadiness.d.ts +12 -3
- package/dist/compositionReadiness.d.ts.map +1 -1
- package/dist/compositionReadiness.js +49 -5
- package/dist/compositionReadiness.js.map +1 -1
- package/dist/generated/audio-fx-runtime-inline.js +1 -1
- package/dist/generated/audio-fx-runtime-inline.js.map +1 -1
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/generated/runtime-inline.js.map +1 -1
- package/dist/hyperframe.runtime.iife.js +25 -25
- package/dist/runtime/authoredTiming.d.ts +2 -0
- package/dist/runtime/authoredTiming.d.ts.map +1 -1
- package/dist/runtime/authoredTiming.js +2 -0
- package/dist/runtime/authoredTiming.js.map +1 -1
- package/dist/runtime/media.d.ts +115 -0
- package/dist/runtime/media.d.ts.map +1 -0
- package/dist/runtime/media.js +450 -0
- package/dist/runtime/media.js.map +1 -0
- package/dist/runtime/timeline.d.ts +14 -0
- package/dist/runtime/timeline.d.ts.map +1 -0
- package/dist/runtime/timeline.js +650 -0
- package/dist/runtime/timeline.js.map +1 -0
- package/docs/core.md +1 -0
- package/package.json +8 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RuntimeTimelineMessage, RuntimeTimelineLike } from "./types";
|
|
2
|
+
import { createRuntimeStartTimeResolver } from "./startResolver";
|
|
3
|
+
export declare function isRuntimeElementVisibleAt(rawNode: HTMLElement, options: {
|
|
4
|
+
currentTime: number;
|
|
5
|
+
compositionDuration: number;
|
|
6
|
+
canonicalFps: number;
|
|
7
|
+
exportRenderSeek: boolean;
|
|
8
|
+
timelineRegistry: Record<string, RuntimeTimelineLike | undefined>;
|
|
9
|
+
resolver: ReturnType<typeof createRuntimeStartTimeResolver>;
|
|
10
|
+
}): boolean;
|
|
11
|
+
export declare function collectRuntimeTimelinePayload(params: {
|
|
12
|
+
canonicalFps: number;
|
|
13
|
+
}): RuntimeTimelineMessage;
|
|
14
|
+
//# sourceMappingURL=timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../../src/runtime/timeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EAEtB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAgBjB,OAAO,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAC;AAQjE,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAClE,QAAQ,EAAE,UAAU,CAAC,OAAO,8BAA8B,CAAC,CAAC;CAC7D,GACA,OAAO,CAiDT;AAmJD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,sBAAsB,CAudzB"}
|
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { stableClipId } from "./clipTree.js";
|
|
2
|
+
import { AUTHORED_DURATION_ATTR, AUTHORED_END_ATTR, resolveAuthoredTimingWindow, } from "./authoredTiming.js";
|
|
3
|
+
import { swallow } from "./diagnostics.js";
|
|
4
|
+
import { readElementPlaybackRate, readElementPlaybackStart } from "./media.js";
|
|
5
|
+
import { parseStrictFiniteTimingNumber, resolveMediaElementDurationSeconds, resolveNaturalMediaTimelineDuration, resolveTimedImageDurationSeconds, } from "./playbackRate.js";
|
|
6
|
+
import { resolveCssStackingContextId } from "./stackingContext.js";
|
|
7
|
+
import { createRuntimeStartTimeResolver } from "./startResolver.js";
|
|
8
|
+
import { isClipVisibleAt } from "./clipWindow.js";
|
|
9
|
+
import { snapTimeToFrameBoundary } from "../inline-scripts/parityContract.js";
|
|
10
|
+
import { isSceneLikeCompositionId } from "../slideshow/index.js";
|
|
11
|
+
import { COMPOSITION_CONTRACT_VERSION } from "../compositionContract.js";
|
|
12
|
+
import { runtimeProtocolMetadata } from "./protocol.js";
|
|
13
|
+
import { isElementNode, isMediaElement } from "./domRealm.js";
|
|
14
|
+
export function isRuntimeElementVisibleAt(rawNode, options) {
|
|
15
|
+
const tag = rawNode.tagName.toLowerCase();
|
|
16
|
+
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const isMedia = tag === "video" || tag === "audio";
|
|
20
|
+
const start = isMedia
|
|
21
|
+
? options.resolver.resolveMediaStartForElement(rawNode)
|
|
22
|
+
: options.resolver.resolveStartForElement(rawNode, 0);
|
|
23
|
+
let duration = options.resolver.resolveDurationForElement(rawNode);
|
|
24
|
+
const compId = rawNode.getAttribute("data-composition-id");
|
|
25
|
+
if (compId) {
|
|
26
|
+
const compTimeline = options.timelineRegistry[compId];
|
|
27
|
+
const liveDuration = compTimeline && typeof compTimeline.duration === "function"
|
|
28
|
+
? Number(compTimeline.duration())
|
|
29
|
+
: null;
|
|
30
|
+
const hasAuthoredTiming = rawNode.hasAttribute("data-duration") ||
|
|
31
|
+
rawNode.hasAttribute("data-end") ||
|
|
32
|
+
rawNode.hasAttribute(AUTHORED_DURATION_ATTR) ||
|
|
33
|
+
rawNode.hasAttribute(AUTHORED_END_ATTR);
|
|
34
|
+
if (!hasAuthoredTiming &&
|
|
35
|
+
(duration == null || duration <= 0) &&
|
|
36
|
+
liveDuration != null &&
|
|
37
|
+
Number.isFinite(liveDuration) &&
|
|
38
|
+
liveDuration > 0) {
|
|
39
|
+
duration = liveDuration;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const computedEnd = duration != null && duration > 0 ? start + duration : Number.POSITIVE_INFINITY;
|
|
43
|
+
// Export seeks snap to frame boundaries; interactive visibility uses authored seconds.
|
|
44
|
+
const visibilityStart = options.exportRenderSeek
|
|
45
|
+
? snapTimeToFrameBoundary(start, options.canonicalFps)
|
|
46
|
+
: start;
|
|
47
|
+
const visibilityEnd = options.exportRenderSeek && Number.isFinite(computedEnd)
|
|
48
|
+
? snapTimeToFrameBoundary(computedEnd, options.canonicalFps)
|
|
49
|
+
: computedEnd;
|
|
50
|
+
return isClipVisibleAt(options.currentTime, visibilityStart, visibilityEnd, options.compositionDuration);
|
|
51
|
+
}
|
|
52
|
+
function parseNum(value) {
|
|
53
|
+
return parseStrictFiniteTimingNumber(value);
|
|
54
|
+
}
|
|
55
|
+
function parseElementDurationAttr(element) {
|
|
56
|
+
const publicDuration = element.getAttribute("data-duration");
|
|
57
|
+
const authoredDuration = element.getAttribute(AUTHORED_DURATION_ATTR);
|
|
58
|
+
const resolved = resolveAuthoredTimingWindow({
|
|
59
|
+
start: 0,
|
|
60
|
+
duration: publicDuration,
|
|
61
|
+
authoredDuration,
|
|
62
|
+
})?.duration;
|
|
63
|
+
if (resolved != null)
|
|
64
|
+
return resolved;
|
|
65
|
+
const hasExplicitNonpositive = [publicDuration, authoredDuration]
|
|
66
|
+
.map(parseNum)
|
|
67
|
+
.some((duration) => duration != null && duration <= 0);
|
|
68
|
+
return hasExplicitNonpositive ? 0 : null;
|
|
69
|
+
}
|
|
70
|
+
function parseElementEndAttr(element) {
|
|
71
|
+
return (resolveAuthoredTimingWindow({
|
|
72
|
+
start: 0,
|
|
73
|
+
end: element.getAttribute("data-end"),
|
|
74
|
+
authoredEnd: element.getAttribute(AUTHORED_END_ATTR),
|
|
75
|
+
})?.end ?? null);
|
|
76
|
+
}
|
|
77
|
+
function readInlineZIndex(element) {
|
|
78
|
+
try {
|
|
79
|
+
const inline = element.style?.zIndex;
|
|
80
|
+
if (inline && inline !== "auto") {
|
|
81
|
+
const parsed = parseInt(inline, 10);
|
|
82
|
+
if (Number.isFinite(parsed))
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function maxDefinedNumber(...values) {
|
|
92
|
+
const finite = values.filter((value) => Number.isFinite(value ?? null));
|
|
93
|
+
if (finite.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
return Math.max(...finite);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse an authored track attribute, honoring 0 (a valid top-lane index).
|
|
99
|
+
* `parseInt(...) || fallback` silently replaced authored track 0 with the
|
|
100
|
+
* synthetic fallback, so track-0 clips drifted to the bottom of the timeline.
|
|
101
|
+
*/
|
|
102
|
+
function parseAuthoredTrack(el, fallback) {
|
|
103
|
+
const raw = el.getAttribute("data-track-index") ?? el.getAttribute("data-track");
|
|
104
|
+
if (raw == null)
|
|
105
|
+
return fallback;
|
|
106
|
+
const parsed = Number.parseInt(raw, 10);
|
|
107
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
108
|
+
}
|
|
109
|
+
function toAbsoluteAssetUrl(rawValue) {
|
|
110
|
+
const raw = String(rawValue ?? "").trim();
|
|
111
|
+
if (!raw)
|
|
112
|
+
return null;
|
|
113
|
+
const lowered = raw.toLowerCase();
|
|
114
|
+
if (lowered.startsWith("data:") || lowered.startsWith("javascript:"))
|
|
115
|
+
return null;
|
|
116
|
+
try {
|
|
117
|
+
return new URL(raw, document.baseURI).toString();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return raw;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function resolveNodeAssetUrl(node) {
|
|
124
|
+
const src = node.getAttribute("src") ?? node.getAttribute("data-src");
|
|
125
|
+
if (src)
|
|
126
|
+
return toAbsoluteAssetUrl(src);
|
|
127
|
+
const compositionSrc = node.getAttribute("data-composition-src");
|
|
128
|
+
if (compositionSrc)
|
|
129
|
+
return toAbsoluteAssetUrl(compositionSrc);
|
|
130
|
+
const mediaDescendant = node.querySelector("img[src], video[src], audio[src], source[src]");
|
|
131
|
+
if (!mediaDescendant)
|
|
132
|
+
return null;
|
|
133
|
+
return toAbsoluteAssetUrl(mediaDescendant.getAttribute("src"));
|
|
134
|
+
}
|
|
135
|
+
function getFirstClassToken(node) {
|
|
136
|
+
const className = node.className;
|
|
137
|
+
if (typeof className !== "string")
|
|
138
|
+
return null;
|
|
139
|
+
return (className
|
|
140
|
+
.split(/\s+/)
|
|
141
|
+
.map((value) => value.trim())
|
|
142
|
+
.find((value) => value && value !== "clip" && !value.startsWith("__hf-")) ?? null);
|
|
143
|
+
}
|
|
144
|
+
function filenameFromAssetUrl(url) {
|
|
145
|
+
if (!url)
|
|
146
|
+
return null;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = new URL(url, document.baseURI);
|
|
149
|
+
return parsed.pathname.split("/").filter(Boolean).at(-1) ?? null;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return url.split(/[\\/]/).filter(Boolean).at(-1) ?? null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function textPreview(node) {
|
|
156
|
+
const text = node.textContent?.replace(/\s+/g, " ").trim();
|
|
157
|
+
if (!text)
|
|
158
|
+
return null;
|
|
159
|
+
return text.length > 32 ? `${text.slice(0, 31)}...` : text;
|
|
160
|
+
}
|
|
161
|
+
function humanizeTimelineToken(value) {
|
|
162
|
+
const normalized = value
|
|
163
|
+
.replace(/\.[^.]+$/i, "")
|
|
164
|
+
.replace(/[-_]+/g, " ")
|
|
165
|
+
.replace(/\s+/g, " ")
|
|
166
|
+
.trim();
|
|
167
|
+
if (!normalized)
|
|
168
|
+
return value;
|
|
169
|
+
return normalized.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
170
|
+
}
|
|
171
|
+
function buildTimelineClipLabel(node, kind, ordinal) {
|
|
172
|
+
const explicit = node.getAttribute("data-timeline-label") ??
|
|
173
|
+
node.getAttribute("data-label") ??
|
|
174
|
+
node.getAttribute("aria-label") ??
|
|
175
|
+
null;
|
|
176
|
+
if (explicit?.trim())
|
|
177
|
+
return explicit.trim();
|
|
178
|
+
const compositionId = node.getAttribute("data-composition-id");
|
|
179
|
+
if (compositionId)
|
|
180
|
+
return humanizeTimelineToken(compositionId);
|
|
181
|
+
const id = node.id;
|
|
182
|
+
if (id)
|
|
183
|
+
return humanizeTimelineToken(id);
|
|
184
|
+
const classToken = getFirstClassToken(node);
|
|
185
|
+
if (classToken)
|
|
186
|
+
return humanizeTimelineToken(classToken);
|
|
187
|
+
const assetName = filenameFromAssetUrl(resolveNodeAssetUrl(node));
|
|
188
|
+
if (assetName)
|
|
189
|
+
return humanizeTimelineToken(assetName);
|
|
190
|
+
const text = textPreview(node);
|
|
191
|
+
if (text)
|
|
192
|
+
return text;
|
|
193
|
+
return `${humanizeTimelineToken(kind)} ${ordinal + 1}`;
|
|
194
|
+
}
|
|
195
|
+
export function collectRuntimeTimelinePayload(params) {
|
|
196
|
+
const runtimeWindow = window;
|
|
197
|
+
const timelineRegistry = runtimeWindow.__timelines ?? {};
|
|
198
|
+
const startResolver = createRuntimeStartTimeResolver({
|
|
199
|
+
timelineRegistry,
|
|
200
|
+
includeAuthoredTimingAttrs: true,
|
|
201
|
+
});
|
|
202
|
+
const resolveTimelineDurationSeconds = (compositionId) => {
|
|
203
|
+
if (!compositionId)
|
|
204
|
+
return null;
|
|
205
|
+
const timeline = timelineRegistry[compositionId] ?? null;
|
|
206
|
+
if (!timeline || typeof timeline.duration !== "function")
|
|
207
|
+
return null;
|
|
208
|
+
try {
|
|
209
|
+
const duration = Number(timeline.duration());
|
|
210
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const resolveMediaWindowEndSeconds = () => {
|
|
217
|
+
const mediaNodes = Array.from(document.querySelectorAll("video[data-start], audio[data-start]"));
|
|
218
|
+
if (mediaNodes.length === 0)
|
|
219
|
+
return null;
|
|
220
|
+
let maxWindowEndSeconds = 0;
|
|
221
|
+
for (const mediaNode of mediaNodes) {
|
|
222
|
+
const start = startResolver.resolveMediaStartForElement(mediaNode);
|
|
223
|
+
if (!Number.isFinite(start))
|
|
224
|
+
continue;
|
|
225
|
+
const duration = resolveMediaElementDurationSeconds(mediaNode);
|
|
226
|
+
if (duration == null || duration <= 0)
|
|
227
|
+
continue;
|
|
228
|
+
maxWindowEndSeconds = Math.max(maxWindowEndSeconds, Math.max(0, start) + duration);
|
|
229
|
+
}
|
|
230
|
+
return maxWindowEndSeconds > 0 ? maxWindowEndSeconds : null;
|
|
231
|
+
};
|
|
232
|
+
const resolveNearestCompositionContext = (node, root) => {
|
|
233
|
+
const ancestors = [];
|
|
234
|
+
let inheritedStart = null;
|
|
235
|
+
let inheritedDuration = null;
|
|
236
|
+
let parentCompositionId = null;
|
|
237
|
+
let cursor = node.parentElement;
|
|
238
|
+
while (cursor) {
|
|
239
|
+
const compositionId = cursor.getAttribute("data-composition-id");
|
|
240
|
+
if (compositionId) {
|
|
241
|
+
ancestors.push(compositionId);
|
|
242
|
+
if (!parentCompositionId && cursor !== root) {
|
|
243
|
+
parentCompositionId = compositionId;
|
|
244
|
+
}
|
|
245
|
+
if (inheritedStart == null) {
|
|
246
|
+
inheritedStart = startResolver.resolveStartForElement(cursor, 0);
|
|
247
|
+
}
|
|
248
|
+
if (inheritedDuration == null) {
|
|
249
|
+
inheritedDuration =
|
|
250
|
+
parseNum(cursor.getAttribute("data-duration")) ??
|
|
251
|
+
resolveTimelineDurationSeconds(compositionId) ??
|
|
252
|
+
null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
cursor = cursor.parentElement;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
parentCompositionId,
|
|
259
|
+
compositionAncestors: ancestors.reverse(),
|
|
260
|
+
inheritedStart,
|
|
261
|
+
inheritedDuration,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
const root = document.querySelector("[data-composition-id]");
|
|
265
|
+
const compositionNodes = Array.from(document.querySelectorAll("[data-composition-id]"));
|
|
266
|
+
const rootCompositionId = root?.getAttribute("data-composition-id") ?? null;
|
|
267
|
+
const rootCompositionStart = root ? startResolver.resolveStartForElement(root, 0) : 0;
|
|
268
|
+
const mediaWindowEnd = resolveMediaWindowEndSeconds();
|
|
269
|
+
const mediaWindowDuration = mediaWindowEnd != null ? Math.max(0, mediaWindowEnd - Math.max(0, rootCompositionStart)) : null;
|
|
270
|
+
const rootDurationFromTimeline = resolveTimelineDurationSeconds(rootCompositionId);
|
|
271
|
+
const rootDurationFromAttr = parseElementDurationAttr(root ?? document.body);
|
|
272
|
+
const compositionWindowEnd = maxDefinedNumber(...compositionNodes
|
|
273
|
+
.filter((node) => node !== root)
|
|
274
|
+
.map((node) => {
|
|
275
|
+
const start = startResolver.resolveStartForElement(node, 0);
|
|
276
|
+
const duration = startResolver.resolveDurationForElement(node) ??
|
|
277
|
+
resolveTimelineDurationSeconds(node.getAttribute("data-composition-id")) ??
|
|
278
|
+
null;
|
|
279
|
+
if (!Number.isFinite(start) || duration == null || duration <= 0)
|
|
280
|
+
return null;
|
|
281
|
+
return Math.max(0, start) + duration;
|
|
282
|
+
}));
|
|
283
|
+
const compositionWindowDuration = compositionWindowEnd != null
|
|
284
|
+
? Math.max(0, compositionWindowEnd - Math.max(0, rootCompositionStart))
|
|
285
|
+
: null;
|
|
286
|
+
const timelineDurationCandidate = typeof rootDurationFromTimeline === "number" &&
|
|
287
|
+
Number.isFinite(rootDurationFromTimeline) &&
|
|
288
|
+
rootDurationFromTimeline > 0
|
|
289
|
+
? rootDurationFromTimeline
|
|
290
|
+
: null;
|
|
291
|
+
const attrDurationCandidate = typeof rootDurationFromAttr === "number" &&
|
|
292
|
+
Number.isFinite(rootDurationFromAttr) &&
|
|
293
|
+
rootDurationFromAttr > 0
|
|
294
|
+
? rootDurationFromAttr
|
|
295
|
+
: null;
|
|
296
|
+
const mediaWindowDurationCandidate = typeof mediaWindowDuration === "number" &&
|
|
297
|
+
Number.isFinite(mediaWindowDuration) &&
|
|
298
|
+
mediaWindowDuration > 0
|
|
299
|
+
? mediaWindowDuration
|
|
300
|
+
: null;
|
|
301
|
+
const compositionWindowDurationCandidate = typeof compositionWindowDuration === "number" &&
|
|
302
|
+
Number.isFinite(compositionWindowDuration) &&
|
|
303
|
+
compositionWindowDuration > 0
|
|
304
|
+
? compositionWindowDuration
|
|
305
|
+
: null;
|
|
306
|
+
const finiteWindowFloor = maxDefinedNumber(mediaWindowDurationCandidate, compositionWindowDurationCandidate);
|
|
307
|
+
const timelineLooksLoopInflated = timelineDurationCandidate != null &&
|
|
308
|
+
finiteWindowFloor != null &&
|
|
309
|
+
timelineDurationCandidate > finiteWindowFloor + 1;
|
|
310
|
+
// Prefer explicit authored root duration first.
|
|
311
|
+
// If absent, guard against loop-inflated GSAP durations by trusting finite media window.
|
|
312
|
+
const preferredRootDuration = attrDurationCandidate ??
|
|
313
|
+
(timelineLooksLoopInflated
|
|
314
|
+
? finiteWindowFloor
|
|
315
|
+
: maxDefinedNumber(timelineDurationCandidate, mediaWindowDurationCandidate, compositionWindowDurationCandidate));
|
|
316
|
+
const rootCompositionDuration = preferredRootDuration ?? null;
|
|
317
|
+
const rootCompositionEnd = rootCompositionDuration != null ? rootCompositionStart + rootCompositionDuration : null;
|
|
318
|
+
const timelineWindowEnd = rootCompositionEnd ??
|
|
319
|
+
(typeof mediaWindowEnd === "number" && Number.isFinite(mediaWindowEnd) && mediaWindowEnd > 0
|
|
320
|
+
? mediaWindowEnd
|
|
321
|
+
: null);
|
|
322
|
+
const clampDurationToRootWindow = (start, duration) => {
|
|
323
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
324
|
+
return 0;
|
|
325
|
+
if (timelineWindowEnd == null || !Number.isFinite(timelineWindowEnd))
|
|
326
|
+
return duration;
|
|
327
|
+
if (!Number.isFinite(start) || start >= timelineWindowEnd)
|
|
328
|
+
return 0;
|
|
329
|
+
return Math.max(0, Math.min(duration, timelineWindowEnd - start));
|
|
330
|
+
};
|
|
331
|
+
const clips = [];
|
|
332
|
+
const scenes = [];
|
|
333
|
+
// Only collect elements that are explicitly part of the timeline:
|
|
334
|
+
// - Elements with data-start or data-track-index (timed clips)
|
|
335
|
+
// - Elements with data-composition-id (sub-compositions)
|
|
336
|
+
// - Media elements (video, audio, img)
|
|
337
|
+
// Elements without data-start (e.g. GSAP-animated scenes) are not included
|
|
338
|
+
// as clips — they have no declared timing so the timeline can't show their
|
|
339
|
+
// actual visibility window. They can still appear as scenes via the separate
|
|
340
|
+
// scene collection below.
|
|
341
|
+
const nodes = Array.from(document.querySelectorAll("[data-start], [data-track-index], [data-composition-id], video, audio, img"));
|
|
342
|
+
let maxEnd = 0;
|
|
343
|
+
for (const [i, node] of nodes.entries()) {
|
|
344
|
+
if (node === root)
|
|
345
|
+
continue;
|
|
346
|
+
if (["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"].includes(node.tagName))
|
|
347
|
+
continue;
|
|
348
|
+
const compositionContext = resolveNearestCompositionContext(node, root);
|
|
349
|
+
const tag = node.tagName.toLowerCase();
|
|
350
|
+
const start = tag === "video" || tag === "audio"
|
|
351
|
+
? startResolver.resolveMediaStartForElement(node)
|
|
352
|
+
: startResolver.resolveStartForElement(node, compositionContext.inheritedStart ?? 0);
|
|
353
|
+
const nodeCompositionId = node.getAttribute("data-composition-id");
|
|
354
|
+
let duration = parseElementDurationAttr(node);
|
|
355
|
+
if (duration == null && nodeCompositionId && nodeCompositionId !== rootCompositionId) {
|
|
356
|
+
duration = resolveTimelineDurationSeconds(nodeCompositionId);
|
|
357
|
+
}
|
|
358
|
+
if (duration == null && isMediaElement(node)) {
|
|
359
|
+
if (Number.isFinite(node.duration)) {
|
|
360
|
+
duration = resolveNaturalMediaTimelineDuration(node, node.duration);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (duration == null)
|
|
364
|
+
duration = resolveTimedImageDurationSeconds(node, start);
|
|
365
|
+
if (duration == null) {
|
|
366
|
+
const inheritedDuration = compositionContext.inheritedDuration;
|
|
367
|
+
if (inheritedDuration != null && inheritedDuration > 0) {
|
|
368
|
+
const inheritedStart = compositionContext.inheritedStart ?? 0;
|
|
369
|
+
const inheritedEnd = inheritedStart + inheritedDuration;
|
|
370
|
+
duration = Math.max(0, inheritedEnd - start);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (duration == null || duration <= 0)
|
|
374
|
+
continue;
|
|
375
|
+
duration = clampDurationToRootWindow(start, duration);
|
|
376
|
+
if (duration <= 0)
|
|
377
|
+
continue;
|
|
378
|
+
const end = start + duration;
|
|
379
|
+
maxEnd = Math.max(maxEnd, end);
|
|
380
|
+
const kind = nodeCompositionId && nodeCompositionId !== rootCompositionId
|
|
381
|
+
? "composition"
|
|
382
|
+
: tag === "video"
|
|
383
|
+
? "video"
|
|
384
|
+
: tag === "audio"
|
|
385
|
+
? "audio"
|
|
386
|
+
: tag === "img"
|
|
387
|
+
? "image"
|
|
388
|
+
: "element";
|
|
389
|
+
clips.push({
|
|
390
|
+
id: stableClipId(node) ?? nodeCompositionId ?? null,
|
|
391
|
+
label: buildTimelineClipLabel(node, kind, clips.length),
|
|
392
|
+
start,
|
|
393
|
+
duration,
|
|
394
|
+
track: parseAuthoredTrack(node, i),
|
|
395
|
+
zIndex: readInlineZIndex(node),
|
|
396
|
+
stackingContextId: resolveCssStackingContextId(node),
|
|
397
|
+
kind,
|
|
398
|
+
tagName: tag,
|
|
399
|
+
compositionId: node.getAttribute("data-composition-id"),
|
|
400
|
+
compositionAncestors: compositionContext.compositionAncestors,
|
|
401
|
+
parentCompositionId: compositionContext.parentCompositionId,
|
|
402
|
+
nodePath: null,
|
|
403
|
+
compositionSrc: toAbsoluteAssetUrl(node.getAttribute("data-composition-src")),
|
|
404
|
+
playbackStart: readElementPlaybackStart(node),
|
|
405
|
+
playbackRate: readElementPlaybackRate(node),
|
|
406
|
+
assetUrl: resolveNodeAssetUrl(node),
|
|
407
|
+
timelineRole: node.getAttribute("data-timeline-role"),
|
|
408
|
+
timelineLabel: node.getAttribute("data-timeline-label"),
|
|
409
|
+
timelineGroup: node.getAttribute("data-timeline-group"),
|
|
410
|
+
timelinePriority: parseNum(node.getAttribute("data-timeline-priority")),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// ── GSAP introspection ──────────────────────────────────────────────────
|
|
414
|
+
// Discover elements animated by GSAP that weren't picked up by the DOM query
|
|
415
|
+
// (e.g. scene divs controlled purely via opacity/display tweens).
|
|
416
|
+
// Introspect the master timeline's tweens to find their targets and time ranges.
|
|
417
|
+
// ── GSAP introspection ──────────────────────────────────────────────────
|
|
418
|
+
// Discover scene-level elements animated by GSAP that weren't picked up by
|
|
419
|
+
// the DOM query. Introspect the master timeline's tweens, resolve absolute
|
|
420
|
+
// time ranges, and bubble child tween ranges up to their nearest scene-level
|
|
421
|
+
// ancestor (direct child of root with an id).
|
|
422
|
+
const gsapClipIds = new Set(clips.map((c) => c.id));
|
|
423
|
+
const rootCompositionIdForGsap = root?.getAttribute("data-composition-id") ?? null;
|
|
424
|
+
const masterTimeline = rootCompositionIdForGsap
|
|
425
|
+
? (timelineRegistry[rootCompositionIdForGsap] ?? null)
|
|
426
|
+
: null;
|
|
427
|
+
if (masterTimeline && root) {
|
|
428
|
+
const tlWithChildren = masterTimeline;
|
|
429
|
+
if (typeof tlWithChildren.getChildren === "function") {
|
|
430
|
+
try {
|
|
431
|
+
const tweens = tlWithChildren.getChildren(true, true, false) ?? [];
|
|
432
|
+
// Build a set of direct children of root that have an id — these are
|
|
433
|
+
// scene-level containers. Tween ranges on their descendants get bubbled
|
|
434
|
+
// up to expand the scene's time range.
|
|
435
|
+
const sceneElements = new Map();
|
|
436
|
+
for (const child of root.children) {
|
|
437
|
+
const childEl = child;
|
|
438
|
+
if (!childEl.id)
|
|
439
|
+
continue;
|
|
440
|
+
const tag = childEl.tagName.toLowerCase();
|
|
441
|
+
if (tag === "script" || tag === "style" || tag === "link")
|
|
442
|
+
continue;
|
|
443
|
+
sceneElements.set(childEl, { id: childEl.id, start: Infinity, end: -Infinity });
|
|
444
|
+
}
|
|
445
|
+
// Find the scene-level ancestor for a given element
|
|
446
|
+
const findSceneAncestor = (el) => {
|
|
447
|
+
let cursor = el;
|
|
448
|
+
while (cursor) {
|
|
449
|
+
if (sceneElements.has(cursor))
|
|
450
|
+
return cursor;
|
|
451
|
+
if (cursor === root)
|
|
452
|
+
return null;
|
|
453
|
+
cursor = cursor.parentElement;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
};
|
|
457
|
+
// Walk all tweens and accumulate time ranges per scene element
|
|
458
|
+
for (const tween of tweens) {
|
|
459
|
+
if (typeof tween.targets !== "function")
|
|
460
|
+
continue;
|
|
461
|
+
if (typeof tween.startTime !== "function" || typeof tween.duration !== "function")
|
|
462
|
+
continue;
|
|
463
|
+
let tweenStart = tween.startTime();
|
|
464
|
+
let parent = tween.parent;
|
|
465
|
+
while (parent && parent !== masterTimeline && typeof parent.startTime === "function") {
|
|
466
|
+
tweenStart += parent.startTime();
|
|
467
|
+
parent = parent.parent;
|
|
468
|
+
}
|
|
469
|
+
const tweenEnd = tweenStart + tween.duration();
|
|
470
|
+
if (!Number.isFinite(tweenStart) || !Number.isFinite(tweenEnd))
|
|
471
|
+
continue;
|
|
472
|
+
for (const target of tween.targets()) {
|
|
473
|
+
if (!isElementNode(target))
|
|
474
|
+
continue;
|
|
475
|
+
// Bubble up to the scene-level ancestor
|
|
476
|
+
const scene = findSceneAncestor(target);
|
|
477
|
+
if (!scene)
|
|
478
|
+
continue;
|
|
479
|
+
const range = sceneElements.get(scene);
|
|
480
|
+
if (!range)
|
|
481
|
+
continue;
|
|
482
|
+
range.start = Math.min(range.start, tweenStart);
|
|
483
|
+
range.end = Math.max(range.end, tweenEnd);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Create clips for scene elements that have tween ranges
|
|
487
|
+
const gsapTrack = clips.length > 0 ? Math.max(...clips.map((c) => c.track)) + 1 : 0;
|
|
488
|
+
for (const [element, range] of sceneElements) {
|
|
489
|
+
if (range.start === Infinity || range.end === -Infinity)
|
|
490
|
+
continue;
|
|
491
|
+
const el = element;
|
|
492
|
+
if (gsapClipIds.has(el.id))
|
|
493
|
+
continue;
|
|
494
|
+
const duration = Math.max(0, range.end - range.start);
|
|
495
|
+
if (duration <= 0)
|
|
496
|
+
continue;
|
|
497
|
+
const clampedDuration = clampDurationToRootWindow(range.start, duration);
|
|
498
|
+
if (clampedDuration <= 0)
|
|
499
|
+
continue;
|
|
500
|
+
maxEnd = Math.max(maxEnd, range.start + clampedDuration);
|
|
501
|
+
clips.push({
|
|
502
|
+
id: el.id,
|
|
503
|
+
label: el.getAttribute("data-timeline-label") ??
|
|
504
|
+
el.getAttribute("data-label") ??
|
|
505
|
+
el.getAttribute("aria-label") ??
|
|
506
|
+
el.id,
|
|
507
|
+
start: range.start,
|
|
508
|
+
duration: clampedDuration,
|
|
509
|
+
track: parseAuthoredTrack(el, gsapTrack),
|
|
510
|
+
zIndex: readInlineZIndex(el),
|
|
511
|
+
stackingContextId: resolveCssStackingContextId(el),
|
|
512
|
+
kind: "element",
|
|
513
|
+
tagName: el.tagName.toLowerCase(),
|
|
514
|
+
compositionId: el.getAttribute("data-composition-id"),
|
|
515
|
+
compositionAncestors: rootCompositionIdForGsap ? [rootCompositionIdForGsap] : [],
|
|
516
|
+
parentCompositionId: rootCompositionIdForGsap,
|
|
517
|
+
nodePath: null,
|
|
518
|
+
compositionSrc: null,
|
|
519
|
+
playbackStart: readElementPlaybackStart(el),
|
|
520
|
+
playbackRate: readElementPlaybackRate(el),
|
|
521
|
+
assetUrl: null,
|
|
522
|
+
timelineRole: el.getAttribute("data-timeline-role"),
|
|
523
|
+
timelineLabel: el.getAttribute("data-timeline-label"),
|
|
524
|
+
timelineGroup: el.getAttribute("data-timeline-group"),
|
|
525
|
+
timelinePriority: parseNum(el.getAttribute("data-timeline-priority")),
|
|
526
|
+
});
|
|
527
|
+
gsapClipIds.add(el.id);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
// GSAP introspection is best-effort — don't break timeline if it fails
|
|
532
|
+
swallow("runtime.timeline.site1", err);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// ── Persistent overlays ─────────────────────────────────────────────────
|
|
537
|
+
// Direct children of root that are pure structural overlays should only
|
|
538
|
+
// surface in the timeline when authors explicitly opt them in. Otherwise
|
|
539
|
+
// background layers like "backdrop" make the whole composition read as a
|
|
540
|
+
// long clip, which is misleading in Studio.
|
|
541
|
+
if (root && rootCompositionDuration != null && rootCompositionDuration > 0) {
|
|
542
|
+
const overlayTrack = clips.length > 0 ? Math.max(...clips.map((c) => c.track)) + 1 : 0;
|
|
543
|
+
for (const child of root.children) {
|
|
544
|
+
const el = child;
|
|
545
|
+
if (!el.id)
|
|
546
|
+
continue;
|
|
547
|
+
if (gsapClipIds.has(el.id))
|
|
548
|
+
continue;
|
|
549
|
+
const timelineRole = el.getAttribute("data-timeline-role");
|
|
550
|
+
if (timelineRole !== "overlay" && timelineRole !== "persistent-overlay")
|
|
551
|
+
continue;
|
|
552
|
+
const tag = el.tagName.toLowerCase();
|
|
553
|
+
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta")
|
|
554
|
+
continue;
|
|
555
|
+
// Skip elements that are invisible (display:none in their CSS class)
|
|
556
|
+
const computed = window.getComputedStyle(el);
|
|
557
|
+
if (computed.display === "none")
|
|
558
|
+
continue;
|
|
559
|
+
const clampedDuration = clampDurationToRootWindow(0, rootCompositionDuration);
|
|
560
|
+
if (clampedDuration <= 0)
|
|
561
|
+
continue;
|
|
562
|
+
maxEnd = Math.max(maxEnd, clampedDuration);
|
|
563
|
+
clips.push({
|
|
564
|
+
id: el.id,
|
|
565
|
+
label: el.getAttribute("data-timeline-label") ??
|
|
566
|
+
el.getAttribute("data-label") ??
|
|
567
|
+
el.getAttribute("aria-label") ??
|
|
568
|
+
el.id,
|
|
569
|
+
start: 0,
|
|
570
|
+
duration: clampedDuration,
|
|
571
|
+
track: parseAuthoredTrack(el, overlayTrack),
|
|
572
|
+
zIndex: readInlineZIndex(el),
|
|
573
|
+
stackingContextId: resolveCssStackingContextId(el),
|
|
574
|
+
kind: "element",
|
|
575
|
+
tagName: tag,
|
|
576
|
+
compositionId: el.getAttribute("data-composition-id"),
|
|
577
|
+
compositionAncestors: rootCompositionIdForGsap ? [rootCompositionIdForGsap] : [],
|
|
578
|
+
parentCompositionId: rootCompositionIdForGsap,
|
|
579
|
+
nodePath: null,
|
|
580
|
+
compositionSrc: null,
|
|
581
|
+
playbackStart: readElementPlaybackStart(el),
|
|
582
|
+
playbackRate: readElementPlaybackRate(el),
|
|
583
|
+
assetUrl: null,
|
|
584
|
+
timelineRole,
|
|
585
|
+
timelineLabel: el.getAttribute("data-timeline-label"),
|
|
586
|
+
timelineGroup: el.getAttribute("data-timeline-group"),
|
|
587
|
+
timelinePriority: parseNum(el.getAttribute("data-timeline-priority")),
|
|
588
|
+
});
|
|
589
|
+
gsapClipIds.add(el.id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Track assignment honors the authored data-track-index verbatim: a clip stays
|
|
593
|
+
// on the track it was placed on, regardless of kind. (Previously mixed-kind
|
|
594
|
+
// tracks were split onto separate rows, but that renumbered tracks — breaking
|
|
595
|
+
// "drop a clip onto an existing track" and causing the written track to drift
|
|
596
|
+
// from the displayed one on every move. Track index is display-only; render
|
|
597
|
+
// never reads it, so honoring it verbatim is the correct NLE behavior.)
|
|
598
|
+
for (const compositionNode of compositionNodes) {
|
|
599
|
+
if (compositionNode === root)
|
|
600
|
+
continue;
|
|
601
|
+
const compositionId = compositionNode.getAttribute("data-composition-id");
|
|
602
|
+
if (!compositionId || !isSceneLikeCompositionId(compositionId))
|
|
603
|
+
continue;
|
|
604
|
+
const start = startResolver.resolveStartForElement(compositionNode, 0);
|
|
605
|
+
let durationFromAttr = parseElementDurationAttr(compositionNode);
|
|
606
|
+
if ((durationFromAttr == null || durationFromAttr <= 0) &&
|
|
607
|
+
parseElementEndAttr(compositionNode) != null) {
|
|
608
|
+
const end = parseElementEndAttr(compositionNode);
|
|
609
|
+
durationFromAttr = Math.max(0, end - start);
|
|
610
|
+
}
|
|
611
|
+
const durationFromTimeline = resolveTimelineDurationSeconds(compositionId);
|
|
612
|
+
const duration = durationFromAttr && durationFromAttr > 0 ? durationFromAttr : durationFromTimeline;
|
|
613
|
+
if (duration == null || duration <= 0)
|
|
614
|
+
continue;
|
|
615
|
+
const clampedDuration = clampDurationToRootWindow(start, duration);
|
|
616
|
+
if (clampedDuration <= 0)
|
|
617
|
+
continue;
|
|
618
|
+
scenes.push({
|
|
619
|
+
id: compositionId,
|
|
620
|
+
label: compositionNode.getAttribute("data-label") ?? compositionId,
|
|
621
|
+
start,
|
|
622
|
+
duration: clampedDuration,
|
|
623
|
+
thumbnailUrl: toAbsoluteAssetUrl(compositionNode.getAttribute("data-thumbnail-url")),
|
|
624
|
+
avatarName: null,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// Timeline payload duration should reflect the playable composition window,
|
|
628
|
+
// not just the furthest currently-surfaced clip. Studio can intentionally
|
|
629
|
+
// hide structural/background tracks from the timeline UI; if we collapse the
|
|
630
|
+
// payload duration down to the last visible clip end, the controls jump even
|
|
631
|
+
// though playback still runs for the full authored root duration.
|
|
632
|
+
const safeDuration = Math.max(1, maxEnd || 1, rootCompositionDuration ?? 0);
|
|
633
|
+
const shouldEmitNonDeterministicInf = timelineLooksLoopInflated && attrDurationCandidate == null;
|
|
634
|
+
const durationInFrames = shouldEmitNonDeterministicInf
|
|
635
|
+
? Number.POSITIVE_INFINITY
|
|
636
|
+
: Math.max(1, Math.ceil(safeDuration * Math.max(1, params.canonicalFps)));
|
|
637
|
+
return {
|
|
638
|
+
...runtimeProtocolMetadata(params.canonicalFps),
|
|
639
|
+
source: "hf-preview",
|
|
640
|
+
type: "timeline",
|
|
641
|
+
compositionContractVersion: COMPOSITION_CONTRACT_VERSION,
|
|
642
|
+
durationSeconds: shouldEmitNonDeterministicInf ? Number.POSITIVE_INFINITY : safeDuration,
|
|
643
|
+
durationInFrames,
|
|
644
|
+
clips,
|
|
645
|
+
scenes,
|
|
646
|
+
compositionWidth: parseNum(root?.getAttribute("data-width")) ?? 1920,
|
|
647
|
+
compositionHeight: parseNum(root?.getAttribute("data-height")) ?? 1080,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
//# sourceMappingURL=timeline.js.map
|