@hyperframes/studio 0.7.60 → 0.7.62
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/assets/hyperframes-player-CWpmBjzv.js +459 -0
- package/dist/assets/{index-D6etaey-.js → index-BJzSiIBj.js} +1 -1
- package/dist/assets/index-Ceyz8Qt2.css +1 -0
- package/dist/assets/{index-Dh_WhagG.js → index-DJQPB78e.js} +1 -1
- package/dist/assets/index-vDRVQtTK.js +426 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.html +2 -2
- package/dist/index.js +4392 -3314
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +15 -15
- package/src/components/DesignPanelPromoteProvider.tsx +27 -1
- package/src/components/EditorShell.tsx +12 -0
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioLeftSidebar.tsx +3 -0
- package/src/components/StudioRightPanel.tsx +34 -26
- package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
- package/src/components/editor/PromotableControl.tsx +4 -2
- package/src/components/editor/PropertyPanel.tsx +151 -149
- package/src/components/editor/PropertyPanelFlat.tsx +50 -42
- package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
- package/src/components/editor/domEditing.test.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +6 -6
- package/src/components/editor/manualEditingAvailability.ts +5 -3
- package/src/components/editor/propertyPanelColor.test.tsx +31 -0
- package/src/components/editor/propertyPanelColor.tsx +150 -28
- package/src/components/editor/propertyPanelCommitField.tsx +178 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +44 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +26 -10
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +21 -32
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -2
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +18 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +3 -92
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +6 -0
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/components/sidebar/LeftSidebar.tsx +3 -0
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
- package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
- package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
- package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
- package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
- package/src/components/storyboard/storyboardReviewStage.ts +45 -0
- package/src/components/storyboard/useFrameComments.ts +22 -6
- package/src/contexts/StudioContext.tsx +4 -0
- package/src/contexts/ViewModeContext.tsx +60 -7
- package/src/hooks/timelineEditingHelpers.ts +10 -4
- package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
- package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
- package/src/hooks/usePreviewPersistence.ts +7 -1
- package/src/hooks/useRazorSplit.history.test.tsx +21 -28
- package/src/hooks/useRazorSplit.test.tsx +20 -9
- package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
- package/src/hooks/useRazorSplit.ts +78 -350
- package/src/hooks/useSlideshowTabState.test.ts +96 -0
- package/src/hooks/useSlideshowTabState.ts +61 -0
- package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +52 -2
- package/src/hooks/useTimelineEditing.ts +19 -17
- package/src/hooks/useTimelineGroupEditing.ts +6 -7
- package/src/player/components/Timeline.tsx +12 -1
- package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
- package/src/player/components/timelineAuthoredTrack.ts +29 -0
- package/src/player/components/timelineCallbacks.ts +4 -0
- package/src/player/components/timelineClipDragCommit.test.ts +176 -0
- package/src/player/components/timelineClipDragCommit.ts +77 -105
- package/src/player/components/timelineClipDragPreview.test.ts +33 -1
- package/src/player/components/timelineClipDragPreview.ts +2 -1
- package/src/player/components/timelineCollision.test.ts +18 -0
- package/src/player/components/timelineCollision.ts +12 -9
- package/src/player/components/timelineDragDrop.ts +28 -7
- package/src/player/components/timelineEditCapabilities.ts +4 -1
- package/src/player/components/timelineGroupEditing.test.ts +10 -0
- package/src/player/components/timelineGroupEditing.ts +4 -3
- package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
- package/src/player/components/timelineLayout.ts +3 -1
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.ts +5 -0
- package/src/player/components/useTimelineEditPinning.ts +12 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/lib/playbackTypes.ts +9 -0
- package/src/player/lib/runtimeProtocol.test.ts +6 -1
- package/src/player/lib/timelineDOM.test.ts +58 -0
- package/src/player/lib/timelineDOM.ts +26 -12
- package/src/player/lib/timelineElementHelpers.ts +21 -11
- package/src/player/lib/timelineIframeHelpers.ts +153 -107
- package/src/player/store/playerStore.ts +2 -1
- package/src/utils/razorSplitTransaction.test.ts +175 -0
- package/src/utils/razorSplitTransaction.ts +200 -0
- package/src/utils/studioPreviewHelpers.test.ts +27 -0
- package/src/utils/timelineCompositionDrop.test.ts +12 -0
- package/src/utils/timelineCompositionDrop.ts +16 -0
- package/src/utils/timelineCompositionInsert.test.ts +142 -0
- package/src/utils/timelineCompositionInsert.ts +90 -0
- package/src/utils/timelineElementSplit.test.ts +14 -2
- package/src/utils/timelineElementSplit.ts +5 -1
- package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
- package/dist/assets/index-DXbu6IPT.css +0 -1
- package/dist/assets/index-cH6NfVV_.js +0 -426
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
|
@@ -48,9 +48,51 @@ describe("buildExpandedElements", () => {
|
|
|
48
48
|
const out = buildExpandedElements(elements, manifest, parentMap, "s3", "s3");
|
|
49
49
|
const child = out.find((e) => e.domId === "stat-1")!;
|
|
50
50
|
expect(child.expandedParentStart).toBe(16);
|
|
51
|
+
expect(child.expandedHostKey).toBe("s3");
|
|
51
52
|
expect(child.sourceFile).toBe("stats.html");
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
it("keeps repeated same-source composition hosts as distinct move identities", () => {
|
|
56
|
+
const elements = [
|
|
57
|
+
el({
|
|
58
|
+
id: "host-a",
|
|
59
|
+
key: "index.html#host-a",
|
|
60
|
+
start: 0,
|
|
61
|
+
duration: 5,
|
|
62
|
+
compositionSrc: "scene.html",
|
|
63
|
+
}),
|
|
64
|
+
el({
|
|
65
|
+
id: "host-b",
|
|
66
|
+
key: "index.html#host-b",
|
|
67
|
+
start: 8,
|
|
68
|
+
duration: 5,
|
|
69
|
+
compositionSrc: "scene.html",
|
|
70
|
+
}),
|
|
71
|
+
];
|
|
72
|
+
const manifest = [
|
|
73
|
+
clip({ id: "host-a", start: 0, duration: 5, compositionSrc: "scene.html" }),
|
|
74
|
+
clip({ id: "child-a", start: 1, duration: 2 }),
|
|
75
|
+
clip({ id: "host-b", start: 8, duration: 5, compositionSrc: "scene.html" }),
|
|
76
|
+
clip({ id: "child-b", start: 9, duration: 2 }),
|
|
77
|
+
];
|
|
78
|
+
const parentMap = new Map([
|
|
79
|
+
["child-a", "host-a"],
|
|
80
|
+
["child-b", "host-b"],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const childA = buildExpandedElements(elements, manifest, parentMap, "host-a", "host-a").find(
|
|
84
|
+
(element) => element.domId === "child-a",
|
|
85
|
+
);
|
|
86
|
+
const childB = buildExpandedElements(elements, manifest, parentMap, "host-b", "host-b").find(
|
|
87
|
+
(element) => element.domId === "child-b",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(childA?.sourceFile).toBe("scene.html");
|
|
91
|
+
expect(childB?.sourceFile).toBe("scene.html");
|
|
92
|
+
expect(childA?.expandedHostKey).toBe("index.html#host-a");
|
|
93
|
+
expect(childB?.expandedHostKey).toBe("index.html#host-b");
|
|
94
|
+
});
|
|
95
|
+
|
|
54
96
|
// fallow-ignore-next-line code-duplication
|
|
55
97
|
it("rebases a 2-level child onto its NESTED host, not the top-level scene", () => {
|
|
56
98
|
// top host A@10 (a.html) embeds host B@12 (b.html); child C lives in b.html.
|
|
@@ -133,6 +133,7 @@ function buildChildElements(
|
|
|
133
133
|
siblings: ClipManifestClip[],
|
|
134
134
|
display: DisplayBounds,
|
|
135
135
|
editBasis: { start: number; sourceFile: string | undefined },
|
|
136
|
+
expandedHostKey: string,
|
|
136
137
|
): TimelineElement[] {
|
|
137
138
|
const result: TimelineElement[] = [];
|
|
138
139
|
for (const child of siblings) {
|
|
@@ -182,6 +183,7 @@ function buildChildElements(
|
|
|
182
183
|
authoredTrack: base.authoredTrack,
|
|
183
184
|
stackingContextId: base.stackingContextId,
|
|
184
185
|
expandedParentStart: editBasis.start,
|
|
186
|
+
expandedHostKey,
|
|
185
187
|
domId,
|
|
186
188
|
selector,
|
|
187
189
|
sourceFile: editBasis.sourceFile,
|
|
@@ -260,6 +262,7 @@ export function buildExpandedElements(
|
|
|
260
262
|
track: topLevelElement.track,
|
|
261
263
|
},
|
|
262
264
|
editBasis,
|
|
265
|
+
parentKey,
|
|
263
266
|
);
|
|
264
267
|
if (expanded.length === 0) return filterToTopLevel(elements, parentMap);
|
|
265
268
|
|
|
@@ -46,13 +46,22 @@ export interface ClipManifestClip {
|
|
|
46
46
|
compositionAncestors?: string[];
|
|
47
47
|
parentCompositionId: string | null;
|
|
48
48
|
compositionSrc: string | null;
|
|
49
|
+
playbackStart?: number;
|
|
50
|
+
playbackRate?: number;
|
|
49
51
|
assetUrl: string | null;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface ClipManifest {
|
|
55
|
+
protocolVersion?: number;
|
|
56
|
+
compositionContractVersion?: number;
|
|
57
|
+
capabilities?: readonly string[];
|
|
58
|
+
fps?: { numerator: number; denominator: number };
|
|
59
|
+
durationSeconds?: number;
|
|
53
60
|
clips: ClipManifestClip[];
|
|
54
61
|
scenes: Array<{ id: string; label: string; start: number; duration: number }>;
|
|
55
62
|
durationInFrames: number;
|
|
63
|
+
compositionWidth?: number;
|
|
64
|
+
compositionHeight?: number;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
export type IframeWindow = Window & {
|
|
@@ -14,7 +14,12 @@ describe("Studio runtime protocol", () => {
|
|
|
14
14
|
type: "control",
|
|
15
15
|
action: "seek",
|
|
16
16
|
protocolVersion: 1,
|
|
17
|
-
capabilities: [
|
|
17
|
+
capabilities: [
|
|
18
|
+
"seconds-time",
|
|
19
|
+
"rational-fps",
|
|
20
|
+
"seek-keep-playing",
|
|
21
|
+
"composition-manifest-v1",
|
|
22
|
+
],
|
|
18
23
|
fps: { numerator: 60, denominator: 1 },
|
|
19
24
|
timeSeconds: 1.25,
|
|
20
25
|
});
|
|
@@ -110,7 +110,65 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
describe("parseTimelineFromDOM — canonical playback rate", () => {
|
|
114
|
+
it.each([
|
|
115
|
+
["10", 5],
|
|
116
|
+
["0.01", 0.1],
|
|
117
|
+
])("clamps authored rate %s to %s for trim and split math", (authored, expected) => {
|
|
118
|
+
const doc = makeDoc(`
|
|
119
|
+
<div data-composition-id="root">
|
|
120
|
+
<div id="nested" class="clip" data-composition-src="scene.html"
|
|
121
|
+
data-start="0" data-duration="5" data-playback-rate="${authored}"></div>
|
|
122
|
+
</div>
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
const nested = parseTimelineFromDOM(doc, 10).find((entry) => entry.domId === "nested");
|
|
126
|
+
|
|
127
|
+
expect(nested?.playbackRate).toBe(expected);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
113
131
|
describe("createTimelineElementFromManifestClip — source-scoped selector identity", () => {
|
|
132
|
+
it("preserves composition kind and source timing on first translation", () => {
|
|
133
|
+
const doc = makeDoc(`
|
|
134
|
+
<div data-composition-id="root" data-composition-file="index.html">
|
|
135
|
+
<div id="host" data-composition-id="scene" data-composition-src="scene.html"
|
|
136
|
+
data-playback-start="1.5" data-playback-rate="2"></div>
|
|
137
|
+
</div>
|
|
138
|
+
`);
|
|
139
|
+
const host = doc.getElementById("host");
|
|
140
|
+
|
|
141
|
+
const element = createTimelineElementFromManifestClip({
|
|
142
|
+
clip: {
|
|
143
|
+
id: "host",
|
|
144
|
+
label: "Scene",
|
|
145
|
+
kind: "composition",
|
|
146
|
+
tagName: "div",
|
|
147
|
+
start: 2,
|
|
148
|
+
duration: 4,
|
|
149
|
+
track: 0,
|
|
150
|
+
compositionId: "scene",
|
|
151
|
+
parentCompositionId: "root",
|
|
152
|
+
compositionSrc: "scene.html",
|
|
153
|
+
playbackStart: 1.5,
|
|
154
|
+
playbackRate: 2,
|
|
155
|
+
assetUrl: null,
|
|
156
|
+
},
|
|
157
|
+
fallbackIndex: 0,
|
|
158
|
+
doc,
|
|
159
|
+
hostEl: host,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(element).toMatchObject({
|
|
163
|
+
kind: "composition",
|
|
164
|
+
compositionSrc: "scene.html",
|
|
165
|
+
playbackStart: 1.5,
|
|
166
|
+
playbackStartAttr: "playback-start",
|
|
167
|
+
playbackRate: 2,
|
|
168
|
+
domId: "host",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
114
172
|
it("ignores an index.html duplicate when indexing a scene.html selector", () => {
|
|
115
173
|
const doc = makeDoc(`
|
|
116
174
|
<div data-composition-id="root" data-composition-file="index.html">
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
|
+
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
14
15
|
import {
|
|
15
16
|
resolveMediaElement,
|
|
16
17
|
applyMediaMetadataFromElement,
|
|
@@ -110,6 +111,7 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
110
111
|
id: identity.id,
|
|
111
112
|
label,
|
|
112
113
|
key: identity.key,
|
|
114
|
+
kind: clip.kind,
|
|
113
115
|
tag: resolveClipTag(clip),
|
|
114
116
|
start: clip.start,
|
|
115
117
|
duration: clip.duration,
|
|
@@ -128,6 +130,8 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
128
130
|
selector,
|
|
129
131
|
selectorIndex,
|
|
130
132
|
sourceFile,
|
|
133
|
+
playbackStart: clip.playbackStart,
|
|
134
|
+
playbackRate: clip.playbackRate,
|
|
131
135
|
};
|
|
132
136
|
|
|
133
137
|
if (hostEl) {
|
|
@@ -139,6 +143,8 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
139
143
|
}
|
|
140
144
|
if (clip.assetUrl) entry.src = clip.assetUrl;
|
|
141
145
|
if (clip.kind === "composition" && clip.compositionId) {
|
|
146
|
+
entry.playbackStart ??= 0;
|
|
147
|
+
entry.playbackRate ??= 1;
|
|
142
148
|
let resolvedSrc = clip.compositionSrc;
|
|
143
149
|
if (!resolvedSrc) {
|
|
144
150
|
hostEl =
|
|
@@ -255,24 +261,20 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
255
261
|
if (node === rootComp) return;
|
|
256
262
|
if (isTimelineIgnoredElement(node)) return;
|
|
257
263
|
const el = node as HTMLElement;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (isNaN(start)) return;
|
|
264
|
+
const timing = readClipTiming(el);
|
|
265
|
+
const start = timing.start;
|
|
266
|
+
if (start == null) return;
|
|
262
267
|
if (Number.isFinite(rootDuration) && rootDuration > 0 && start >= rootDuration) return;
|
|
263
268
|
|
|
264
269
|
const tagLower = el.tagName.toLowerCase();
|
|
265
|
-
let dur = 0;
|
|
266
|
-
|
|
267
|
-
if (durStr != null) dur = parseFloat(durStr);
|
|
268
|
-
if (isNaN(dur) || dur <= 0) dur = Math.max(0, rootDuration - start);
|
|
270
|
+
let dur = timing.duration ?? 0;
|
|
271
|
+
if (dur <= 0) dur = Math.max(0, rootDuration - start);
|
|
269
272
|
if (Number.isFinite(rootDuration) && rootDuration > 0) {
|
|
270
273
|
dur = Math.min(dur, Math.max(0, rootDuration - start));
|
|
271
274
|
}
|
|
272
275
|
if (!Number.isFinite(dur) || dur <= 0) return;
|
|
273
276
|
|
|
274
|
-
const
|
|
275
|
-
const track = trackStr != null ? parseInt(trackStr, 10) : trackCounter++;
|
|
277
|
+
const track = timing.trackSource === "default" ? trackCounter++ : timing.trackIndex;
|
|
276
278
|
// fallow-ignore-next-line code-duplication
|
|
277
279
|
const compId = el.getAttribute("data-composition-id");
|
|
278
280
|
const selector = getTimelineElementSelector(el);
|
|
@@ -296,10 +298,18 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
296
298
|
id: identity.id,
|
|
297
299
|
label,
|
|
298
300
|
key: identity.key,
|
|
301
|
+
kind:
|
|
302
|
+
compId && compId !== rootComp?.getAttribute("data-composition-id")
|
|
303
|
+
? "composition"
|
|
304
|
+
: tagLower === "video" || tagLower === "audio"
|
|
305
|
+
? tagLower
|
|
306
|
+
: tagLower === "img"
|
|
307
|
+
? "image"
|
|
308
|
+
: "element",
|
|
299
309
|
tag: tagLower,
|
|
300
310
|
start,
|
|
301
311
|
duration: dur,
|
|
302
|
-
track
|
|
312
|
+
track,
|
|
303
313
|
domId: el.id || undefined,
|
|
304
314
|
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
305
315
|
selector,
|
|
@@ -311,13 +321,13 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
311
321
|
};
|
|
312
322
|
|
|
313
323
|
const mediaEl = resolveMediaElement(el);
|
|
324
|
+
applyMediaMetadataFromElement(entry, el);
|
|
314
325
|
if (mediaEl) {
|
|
315
326
|
if (mediaEl.tagName === "IMG") {
|
|
316
327
|
entry.tag = "img";
|
|
317
328
|
}
|
|
318
329
|
const vol = el.getAttribute("data-volume") ?? mediaEl.getAttribute("data-volume");
|
|
319
330
|
if (vol) entry.volume = parseFloat(vol);
|
|
320
|
-
applyMediaMetadataFromElement(entry, el);
|
|
321
331
|
// Override AFTER the helper (which sets the raw relative attribute) so the
|
|
322
332
|
// resolved absolute URL wins — the Studio can then fetch the asset
|
|
323
333
|
// regardless of whether the attribute value was relative or absolute.
|
|
@@ -348,6 +358,10 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
348
358
|
entry.tag = "video";
|
|
349
359
|
}
|
|
350
360
|
}
|
|
361
|
+
if (entry.kind === "composition") {
|
|
362
|
+
entry.playbackStart ??= 0;
|
|
363
|
+
entry.playbackRate ??= 1;
|
|
364
|
+
}
|
|
351
365
|
|
|
352
366
|
els.push(entry);
|
|
353
367
|
});
|
|
@@ -76,6 +76,10 @@ function readDurationAttribute(el: Element | null | undefined): number {
|
|
|
76
76
|
return isFinitePositive(duration) ? duration : 0;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function normalizePlaybackRate(raw: number): number {
|
|
80
|
+
return Number.isFinite(raw) && raw > 0 ? Math.max(0.1, Math.min(5, raw)) : 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
export function isTimelineIgnoredElement(el: Element): boolean {
|
|
80
84
|
return Boolean(
|
|
81
85
|
el.closest(
|
|
@@ -149,19 +153,25 @@ export function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageEl
|
|
|
149
153
|
: null;
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
? "media-start"
|
|
157
|
-
: undefined;
|
|
158
|
-
const mediaStartValue =
|
|
159
|
-
el.getAttribute("data-playback-start") ?? el.getAttribute("data-media-start");
|
|
156
|
+
function applyPlaybackMetadataFromElement(entry: TimelineElement, el: Element): void {
|
|
157
|
+
const playbackStartValue = el.getAttribute("data-playback-start");
|
|
158
|
+
const legacyMediaStartValue = el.getAttribute("data-media-start");
|
|
159
|
+
const mediaStartValue = playbackStartValue ?? legacyMediaStartValue;
|
|
160
160
|
if (mediaStartValue != null) {
|
|
161
161
|
const playbackStart = parseFloat(mediaStartValue);
|
|
162
162
|
if (Number.isFinite(playbackStart)) entry.playbackStart = playbackStart;
|
|
163
163
|
}
|
|
164
|
-
if (
|
|
164
|
+
if (playbackStartValue != null) entry.playbackStartAttr = "playback-start";
|
|
165
|
+
else if (legacyMediaStartValue != null) entry.playbackStartAttr = "media-start";
|
|
166
|
+
|
|
167
|
+
const authoredPlaybackRate = Number.parseFloat(el.getAttribute("data-playback-rate") ?? "");
|
|
168
|
+
if (Number.isFinite(authoredPlaybackRate) && authoredPlaybackRate > 0) {
|
|
169
|
+
entry.playbackRate = normalizePlaybackRate(authoredPlaybackRate);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function applyMediaMetadataFromElement(entry: TimelineElement, el: Element): void {
|
|
174
|
+
applyPlaybackMetadataFromElement(entry, el);
|
|
165
175
|
|
|
166
176
|
const mediaEl = resolveMediaElement(el);
|
|
167
177
|
if (!mediaEl) return;
|
|
@@ -182,8 +192,8 @@ export function applyMediaMetadataFromElement(entry: TimelineElement, el: Elemen
|
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
const playbackRate = mediaEl.defaultPlaybackRate;
|
|
185
|
-
if (Number.isFinite(playbackRate) && playbackRate > 0) {
|
|
186
|
-
entry.playbackRate = playbackRate;
|
|
195
|
+
if (entry.playbackRate == null && Number.isFinite(playbackRate) && playbackRate > 0) {
|
|
196
|
+
entry.playbackRate = normalizePlaybackRate(playbackRate);
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
199
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { TimelineElement } from "../store/playerStore";
|
|
14
14
|
import type { IframeWindow } from "./playbackTypes";
|
|
15
|
+
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
16
|
import {
|
|
16
17
|
getTimelineElementSelector,
|
|
17
18
|
getTimelineElementSourceFile,
|
|
@@ -38,6 +39,9 @@ export function normalizePreviewViewport(doc: Document, win: Window): void {
|
|
|
38
39
|
win.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// Legacy recovery retained until versioned composition manifests complete
|
|
43
|
+
// their compatibility soak across published CDN runtimes.
|
|
44
|
+
// fallow-ignore-next-line complexity
|
|
41
45
|
export function autoHealMissingCompositionIds(doc: Document): void {
|
|
42
46
|
const compositionIdRe = /data-composition-id=["']([^"']+)["']/gi;
|
|
43
47
|
const referencedIds = new Set<string>();
|
|
@@ -258,6 +262,141 @@ export function stopScrubPreviewAudio(): void {
|
|
|
258
262
|
// Enrich missing compositions from DOM
|
|
259
263
|
// ---------------------------------------------------------------------------
|
|
260
264
|
|
|
265
|
+
function timelineDuration(iframeWin: IframeWindow, compositionId: string): number {
|
|
266
|
+
return (
|
|
267
|
+
(
|
|
268
|
+
iframeWin.__timelines?.[compositionId] as { duration?: () => number } | undefined
|
|
269
|
+
)?.duration?.() ?? 0
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function createTimedElementLookup(doc: Document): Map<string, Element> {
|
|
274
|
+
const timedById = new Map<string, Element>();
|
|
275
|
+
for (const timed of doc.querySelectorAll("[data-start]")) {
|
|
276
|
+
for (const id of [
|
|
277
|
+
timed.id,
|
|
278
|
+
timed.getAttribute("data-hf-id"),
|
|
279
|
+
timed.getAttribute("data-composition-id"),
|
|
280
|
+
]) {
|
|
281
|
+
if (id) timedById.set(id, timed);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return timedById;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createReferenceEndResolver(
|
|
288
|
+
timedById: ReadonlyMap<string, Element>,
|
|
289
|
+
iframeWin: IframeWindow,
|
|
290
|
+
): (refId: string, visiting: ReadonlySet<string>) => number | null {
|
|
291
|
+
const resolveEnd = (refId: string, visiting: ReadonlySet<string>): number | null => {
|
|
292
|
+
if (visiting.has(refId)) return null;
|
|
293
|
+
const referenced = timedById.get(refId);
|
|
294
|
+
if (!referenced) return null;
|
|
295
|
+
const next = new Set(visiting).add(refId);
|
|
296
|
+
const timing = readClipTiming(referenced, {
|
|
297
|
+
resolveReferenceEnd: (nestedId) => resolveEnd(nestedId, next),
|
|
298
|
+
});
|
|
299
|
+
if (timing.end != null) return timing.end;
|
|
300
|
+
const compositionId = referenced.getAttribute("data-composition-id");
|
|
301
|
+
const duration = compositionId ? timelineDuration(iframeWin, compositionId) : 0;
|
|
302
|
+
return timing.start == null || duration <= 0 ? null : timing.start + duration;
|
|
303
|
+
};
|
|
304
|
+
return resolveEnd;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function clampCompositionWindow(
|
|
308
|
+
start: number,
|
|
309
|
+
duration: number,
|
|
310
|
+
rootDuration: number,
|
|
311
|
+
): { start: number; duration: number } | null {
|
|
312
|
+
if (!Number.isFinite(duration) || duration <= 0) return null;
|
|
313
|
+
const safeStart = Number.isFinite(start) ? start : 0;
|
|
314
|
+
if (!Number.isFinite(rootDuration) || rootDuration <= 0) {
|
|
315
|
+
return { start: safeStart, duration };
|
|
316
|
+
}
|
|
317
|
+
if (safeStart >= rootDuration) return null;
|
|
318
|
+
const clamped = Math.min(duration, Math.max(0, rootDuration - safeStart));
|
|
319
|
+
return clamped > 0 ? { start: safeStart, duration: clamped } : null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function nonEmpty(value: string, fallback: string): string {
|
|
323
|
+
return value || fallback;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function optionalNonEmpty(value: string | null): string | undefined {
|
|
327
|
+
return value || undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function attachCompositionSource(
|
|
331
|
+
entry: TimelineElement,
|
|
332
|
+
element: HTMLElement,
|
|
333
|
+
compositionSrc: string | null,
|
|
334
|
+
): TimelineElement {
|
|
335
|
+
if (compositionSrc) return { ...entry, compositionSrc };
|
|
336
|
+
const innerVideo = element.querySelector("video[src]");
|
|
337
|
+
if (!innerVideo) return entry;
|
|
338
|
+
return { ...entry, src: optionalNonEmpty(innerVideo.getAttribute("src")), tag: "video" };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildMissingCompositionEntry(params: {
|
|
342
|
+
doc: Document;
|
|
343
|
+
iframeWin: IframeWindow;
|
|
344
|
+
element: HTMLElement;
|
|
345
|
+
compositionId: string;
|
|
346
|
+
rootDuration: number;
|
|
347
|
+
fallbackIndex: number;
|
|
348
|
+
resolveEnd: (refId: string, visiting: ReadonlySet<string>) => number | null;
|
|
349
|
+
}): TimelineElement | null {
|
|
350
|
+
const { doc, iframeWin, element, compositionId, rootDuration, fallbackIndex, resolveEnd } =
|
|
351
|
+
params;
|
|
352
|
+
const timing = readClipTiming(element, {
|
|
353
|
+
resolveReferenceEnd: (refId) => resolveEnd(refId, new Set([compositionId])),
|
|
354
|
+
});
|
|
355
|
+
const window = clampCompositionWindow(
|
|
356
|
+
timing.start ?? 0,
|
|
357
|
+
timing.duration ?? timelineDuration(iframeWin, compositionId),
|
|
358
|
+
rootDuration,
|
|
359
|
+
);
|
|
360
|
+
if (!window) return null;
|
|
361
|
+
|
|
362
|
+
const preferredId = nonEmpty(element.id, compositionId);
|
|
363
|
+
const compositionSrc =
|
|
364
|
+
element.getAttribute("data-composition-src") ?? element.getAttribute("data-composition-file");
|
|
365
|
+
const selector = getTimelineElementSelector(element);
|
|
366
|
+
const sourceFile = getTimelineElementSourceFile(element);
|
|
367
|
+
const selectorIndex = getTimelineElementSelectorIndex(doc, element, selector);
|
|
368
|
+
const label = getTimelineElementDisplayLabel({
|
|
369
|
+
id: preferredId,
|
|
370
|
+
label: element.getAttribute("data-timeline-label") ?? element.getAttribute("data-label"),
|
|
371
|
+
tag: element.tagName,
|
|
372
|
+
});
|
|
373
|
+
const identity = buildTimelineElementIdentity({
|
|
374
|
+
preferredId,
|
|
375
|
+
label,
|
|
376
|
+
fallbackIndex,
|
|
377
|
+
domId: optionalNonEmpty(element.id),
|
|
378
|
+
selector,
|
|
379
|
+
selectorIndex,
|
|
380
|
+
sourceFile,
|
|
381
|
+
});
|
|
382
|
+
const entry: TimelineElement = {
|
|
383
|
+
id: identity.id,
|
|
384
|
+
label,
|
|
385
|
+
key: identity.key,
|
|
386
|
+
tag: element.tagName.toLowerCase(),
|
|
387
|
+
start: window.start,
|
|
388
|
+
duration: window.duration,
|
|
389
|
+
track: timing.trackIndex,
|
|
390
|
+
domId: optionalNonEmpty(element.id),
|
|
391
|
+
hfId: optionalNonEmpty(element.getAttribute("data-hf-id")),
|
|
392
|
+
selector,
|
|
393
|
+
selectorIndex,
|
|
394
|
+
sourceFile,
|
|
395
|
+
zIndex: readTimelineElementZIndex(element),
|
|
396
|
+
};
|
|
397
|
+
return attachCompositionSource(entry, element, compositionSrc);
|
|
398
|
+
}
|
|
399
|
+
|
|
261
400
|
/**
|
|
262
401
|
* Scan the iframe DOM for composition hosts missing from the current
|
|
263
402
|
* timeline elements and add them. The CDN runtime often fails to resolve
|
|
@@ -279,117 +418,24 @@ export function buildMissingCompositionElements(
|
|
|
279
418
|
const hosts = doc.querySelectorAll("[data-composition-id][data-start]");
|
|
280
419
|
const missing: TimelineElement[] = [];
|
|
281
420
|
|
|
282
|
-
|
|
421
|
+
const resolveEnd = createReferenceEndResolver(createTimedElementLookup(doc), iframeWin);
|
|
422
|
+
|
|
423
|
+
for (const host of hosts) {
|
|
283
424
|
const el = host as HTMLElement;
|
|
284
425
|
const compId = el.getAttribute("data-composition-id");
|
|
285
|
-
if (!compId || compId === rootCompId)
|
|
286
|
-
if (existingIds.has(el.id) || existingIds.has(compId))
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
doc.getElementById(startAttr) ||
|
|
294
|
-
doc.querySelector(`[data-composition-id="${CSS.escape(startAttr)}"]`);
|
|
295
|
-
if (ref) {
|
|
296
|
-
const refStartAttr = ref.getAttribute("data-start") ?? "0";
|
|
297
|
-
let refStart = parseFloat(refStartAttr);
|
|
298
|
-
// Recursively resolve one level of reference for the ref's own start
|
|
299
|
-
if (isNaN(refStart)) {
|
|
300
|
-
const refRef =
|
|
301
|
-
doc.getElementById(refStartAttr) ||
|
|
302
|
-
doc.querySelector(`[data-composition-id="${CSS.escape(refStartAttr)}"]`);
|
|
303
|
-
const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
|
|
304
|
-
const rrCompId = refRef?.getAttribute("data-composition-id");
|
|
305
|
-
const rrDur =
|
|
306
|
-
parseFloat(refRef?.getAttribute("data-duration") ?? "") ||
|
|
307
|
-
(rrCompId
|
|
308
|
-
? ((
|
|
309
|
-
iframeWin.__timelines?.[rrCompId] as { duration?: () => number } | undefined
|
|
310
|
-
)?.duration?.() ?? 0)
|
|
311
|
-
: 0);
|
|
312
|
-
refStart = rrStart + rrDur;
|
|
313
|
-
}
|
|
314
|
-
const refCompId = ref.getAttribute("data-composition-id");
|
|
315
|
-
const refDur =
|
|
316
|
-
parseFloat(ref.getAttribute("data-duration") ?? "") ||
|
|
317
|
-
(refCompId
|
|
318
|
-
? ((
|
|
319
|
-
iframeWin.__timelines?.[refCompId] as { duration?: () => number } | undefined
|
|
320
|
-
)?.duration?.() ?? 0)
|
|
321
|
-
: 0);
|
|
322
|
-
start = refStart + refDur;
|
|
323
|
-
} else {
|
|
324
|
-
start = 0;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Resolve duration from data-duration or GSAP timeline
|
|
329
|
-
let dur = parseFloat(el.getAttribute("data-duration") ?? "");
|
|
330
|
-
if (isNaN(dur) || dur <= 0) {
|
|
331
|
-
dur =
|
|
332
|
-
(
|
|
333
|
-
iframeWin.__timelines?.[compId] as { duration?: () => number } | undefined
|
|
334
|
-
)?.duration?.() ?? 0;
|
|
335
|
-
}
|
|
336
|
-
if (!Number.isFinite(dur) || dur <= 0) return;
|
|
337
|
-
if (!Number.isFinite(start)) start = 0;
|
|
338
|
-
if (Number.isFinite(rootDuration) && rootDuration > 0) {
|
|
339
|
-
if (start >= rootDuration) return;
|
|
340
|
-
dur = Math.min(dur, Math.max(0, rootDuration - start));
|
|
341
|
-
if (dur <= 0) return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const trackStr = el.getAttribute("data-track-index");
|
|
345
|
-
const track = trackStr != null ? parseInt(trackStr, 10) : 0;
|
|
346
|
-
// fallow-ignore-next-line code-duplication
|
|
347
|
-
const compSrc =
|
|
348
|
-
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
|
|
349
|
-
const selector = getTimelineElementSelector(el);
|
|
350
|
-
const sourceFile = getTimelineElementSourceFile(el);
|
|
351
|
-
const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
|
|
352
|
-
const label = getTimelineElementDisplayLabel({
|
|
353
|
-
id: el.id || compId || null,
|
|
354
|
-
label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
|
|
355
|
-
tag: el.tagName,
|
|
356
|
-
});
|
|
357
|
-
const identity = buildTimelineElementIdentity({
|
|
358
|
-
preferredId: el.id || compId || null,
|
|
359
|
-
label,
|
|
426
|
+
if (!compId || compId === rootCompId) continue;
|
|
427
|
+
if (existingIds.has(el.id) || existingIds.has(compId)) continue;
|
|
428
|
+
const entry = buildMissingCompositionEntry({
|
|
429
|
+
doc,
|
|
430
|
+
iframeWin,
|
|
431
|
+
element: el,
|
|
432
|
+
compositionId: compId,
|
|
433
|
+
rootDuration,
|
|
360
434
|
fallbackIndex: missing.length,
|
|
361
|
-
|
|
362
|
-
selector,
|
|
363
|
-
selectorIndex,
|
|
364
|
-
sourceFile,
|
|
435
|
+
resolveEnd,
|
|
365
436
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
label,
|
|
369
|
-
key: identity.key,
|
|
370
|
-
tag: el.tagName.toLowerCase(),
|
|
371
|
-
start,
|
|
372
|
-
duration: dur,
|
|
373
|
-
track: isNaN(track) ? 0 : track,
|
|
374
|
-
domId: el.id || undefined,
|
|
375
|
-
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
376
|
-
selector,
|
|
377
|
-
selectorIndex,
|
|
378
|
-
sourceFile,
|
|
379
|
-
zIndex: readTimelineElementZIndex(el),
|
|
380
|
-
};
|
|
381
|
-
if (compSrc) {
|
|
382
|
-
entry.compositionSrc = compSrc;
|
|
383
|
-
} else {
|
|
384
|
-
// Inline composition — expose inner video for thumbnails
|
|
385
|
-
const innerVideo = el.querySelector("video[src]");
|
|
386
|
-
if (innerVideo) {
|
|
387
|
-
entry.src = innerVideo.getAttribute("src") || undefined;
|
|
388
|
-
entry.tag = "video";
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
missing.push(entry);
|
|
392
|
-
});
|
|
437
|
+
if (entry) missing.push(entry);
|
|
438
|
+
}
|
|
393
439
|
|
|
394
440
|
// Patch existing elements that are missing compositionSrc
|
|
395
441
|
let patched = false;
|
|
@@ -25,6 +25,7 @@ export interface TimelineElement {
|
|
|
25
25
|
id: string;
|
|
26
26
|
label?: string;
|
|
27
27
|
key?: string;
|
|
28
|
+
kind?: ClipManifestClip["kind"];
|
|
28
29
|
tag: string;
|
|
29
30
|
start: number;
|
|
30
31
|
duration: number;
|
|
@@ -82,8 +83,8 @@ export interface TimelineElement {
|
|
|
82
83
|
* the child's local (sourceFile-relative) time. Works at any nesting depth.
|
|
83
84
|
*/
|
|
84
85
|
expandedParentStart?: number;
|
|
86
|
+
expandedHostKey?: string;
|
|
85
87
|
}
|
|
86
|
-
|
|
87
88
|
export type ZoomMode = "fit" | "manual";
|
|
88
89
|
type TimelineTool = "select" | "razor";
|
|
89
90
|
|