@hyperframes/studio 0.7.60 → 0.7.61
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-Xvx2hkrc.js +459 -0
- package/dist/assets/{index-cH6NfVV_.js → index-2mxh_HSy.js} +177 -177
- package/dist/assets/{index-D6etaey-.js → index-BCpoiv9S.js} +1 -1
- package/dist/assets/index-BhWig0mx.css +1 -0
- package/dist/assets/{index-Dh_WhagG.js → index-D-uFclFj.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +1202 -673
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -8
- package/src/components/DesignPanelPromoteProvider.tsx +27 -1
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +34 -26
- package/src/components/editor/PromotableControl.tsx +4 -2
- package/src/components/editor/PropertyPanel.tsx +151 -149
- package/src/components/editor/PropertyPanelFlat.tsx +44 -42
- package/src/components/editor/manualEditingAvailability.test.ts +6 -6
- package/src/components/editor/manualEditingAvailability.ts +5 -3
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
- package/src/components/editor/propertyPanelFlatTextSection.tsx +2 -2
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
- 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/ViewModeContext.tsx +60 -7
- package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
- package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
- package/src/hooks/useSlideshowTabState.test.ts +96 -0
- package/src/hooks/useSlideshowTabState.ts +61 -0
- package/src/player/lib/playbackTypes.ts +7 -0
- package/src/player/lib/runtimeProtocol.test.ts +6 -1
- package/src/player/lib/timelineDOM.ts +8 -11
- package/src/player/lib/timelineIframeHelpers.ts +153 -107
- package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
- package/dist/assets/index-DXbu6IPT.css +0 -1
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
6
|
useMemo,
|
|
7
|
+
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
type ReactNode,
|
|
9
10
|
} from "react";
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
* user straight into the storyboard by navigating the tab to `?view=storyboard`.
|
|
18
19
|
*/
|
|
19
20
|
export type StudioViewMode = "timeline" | "storyboard";
|
|
21
|
+
export type ViewModeGuard = (nextMode: StudioViewMode) => boolean;
|
|
20
22
|
|
|
21
23
|
const VIEW_QUERY_PARAM = "view";
|
|
22
24
|
|
|
@@ -38,9 +40,24 @@ function writeViewModeToUrl(mode: StudioViewMode): void {
|
|
|
38
40
|
window.history.replaceState(window.history.state, "", url);
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
interface HistoryLocation {
|
|
44
|
+
href: string;
|
|
45
|
+
state: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readHistoryLocation(): HistoryLocation | null {
|
|
49
|
+
if (typeof window === "undefined") return null;
|
|
50
|
+
return {
|
|
51
|
+
href: window.location.href,
|
|
52
|
+
state: window.history.state,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
export interface ViewModeValue {
|
|
42
57
|
viewMode: StudioViewMode;
|
|
43
|
-
|
|
58
|
+
/** Returns false when an active editor vetoes the transition. */
|
|
59
|
+
setViewMode: (mode: StudioViewMode) => boolean;
|
|
60
|
+
registerViewModeGuard: (guard: ViewModeGuard) => () => void;
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
/**
|
|
@@ -49,6 +66,15 @@ export interface ViewModeValue {
|
|
|
49
66
|
*/
|
|
50
67
|
export function useViewModeState(): ViewModeValue {
|
|
51
68
|
const [viewMode, setMode] = useState<StudioViewMode>(() => readViewModeFromUrl());
|
|
69
|
+
const guardsRef = useRef(new Set<ViewModeGuard>());
|
|
70
|
+
const acceptedLocationRef = useRef<HistoryLocation | null>(readHistoryLocation());
|
|
71
|
+
|
|
72
|
+
const canSetViewMode = useCallback((mode: StudioViewMode) => {
|
|
73
|
+
for (const guard of guardsRef.current) {
|
|
74
|
+
if (!guard(mode)) return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}, []);
|
|
52
78
|
|
|
53
79
|
// Reflect genuine browser back/forward between history entries with a different
|
|
54
80
|
// `?view=`. Note: our own writes use `replaceState` (below), which does NOT fire
|
|
@@ -57,17 +83,44 @@ export function useViewModeState(): ViewModeValue {
|
|
|
57
83
|
// the mount-time read); a scripted `pushState`/`replaceState` to `?view=` would not be
|
|
58
84
|
// reflected here, by design.
|
|
59
85
|
useEffect(() => {
|
|
60
|
-
const onPopState = () =>
|
|
86
|
+
const onPopState = () => {
|
|
87
|
+
const mode = readViewModeFromUrl();
|
|
88
|
+
if (mode !== viewMode && !canSetViewMode(mode)) {
|
|
89
|
+
const acceptedLocation = acceptedLocationRef.current;
|
|
90
|
+
if (acceptedLocation) {
|
|
91
|
+
window.history.pushState(acceptedLocation.state, "", acceptedLocation.href);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setMode(mode);
|
|
96
|
+
acceptedLocationRef.current = readHistoryLocation();
|
|
97
|
+
};
|
|
61
98
|
window.addEventListener("popstate", onPopState);
|
|
62
99
|
return () => window.removeEventListener("popstate", onPopState);
|
|
63
|
-
}, []);
|
|
100
|
+
}, [canSetViewMode, viewMode]);
|
|
101
|
+
|
|
102
|
+
const setViewMode = useCallback(
|
|
103
|
+
(mode: StudioViewMode) => {
|
|
104
|
+
if (!canSetViewMode(mode)) return false;
|
|
105
|
+
setMode(mode);
|
|
106
|
+
writeViewModeToUrl(mode);
|
|
107
|
+
acceptedLocationRef.current = readHistoryLocation();
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
[canSetViewMode],
|
|
111
|
+
);
|
|
64
112
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
const registerViewModeGuard = useCallback((guard: ViewModeGuard) => {
|
|
114
|
+
guardsRef.current.add(guard);
|
|
115
|
+
return () => {
|
|
116
|
+
guardsRef.current.delete(guard);
|
|
117
|
+
};
|
|
68
118
|
}, []);
|
|
69
119
|
|
|
70
|
-
return useMemo(
|
|
120
|
+
return useMemo(
|
|
121
|
+
() => ({ viewMode, setViewMode, registerViewModeGuard }),
|
|
122
|
+
[viewMode, setViewMode, registerViewModeGuard],
|
|
123
|
+
);
|
|
71
124
|
}
|
|
72
125
|
|
|
73
126
|
const ViewModeContext = createContext<ViewModeValue | null>(null);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { usePlayerStore } from "../player";
|
|
7
|
+
import { useAddAssetAtPlayhead } from "./useAddAssetAtPlayhead";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.body.innerHTML = "";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("useAddAssetAtPlayhead", () => {
|
|
16
|
+
it("drops the asset on track 0 at the current playhead time", () => {
|
|
17
|
+
usePlayerStore.getState().setCurrentTime(4.5);
|
|
18
|
+
const handleTimelineAssetDrop = vi.fn();
|
|
19
|
+
let addAsset: (assetPath: string) => unknown = () => undefined;
|
|
20
|
+
|
|
21
|
+
function Harness() {
|
|
22
|
+
addAsset = useAddAssetAtPlayhead(handleTimelineAssetDrop);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const host = document.createElement("div");
|
|
27
|
+
document.body.append(host);
|
|
28
|
+
const root = createRoot(host);
|
|
29
|
+
act(() => {
|
|
30
|
+
root.render(React.createElement(Harness));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
act(() => {
|
|
34
|
+
addAsset("assets/clip.mp4");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(handleTimelineAssetDrop).toHaveBeenCalledWith("assets/clip.mp4", {
|
|
38
|
+
start: 4.5,
|
|
39
|
+
track: 0,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
act(() => root.unmount());
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { TimelineElement } from "../player";
|
|
3
|
+
import { usePlayerStore } from "../player";
|
|
4
|
+
|
|
5
|
+
/** Drops an asset onto track 0 at the current playhead time. */
|
|
6
|
+
export function useAddAssetAtPlayhead(
|
|
7
|
+
handleTimelineAssetDrop: (
|
|
8
|
+
assetPath: string,
|
|
9
|
+
placement: Pick<TimelineElement, "start" | "track">,
|
|
10
|
+
durationOverride?: number,
|
|
11
|
+
) => unknown,
|
|
12
|
+
) {
|
|
13
|
+
return useCallback(
|
|
14
|
+
(assetPath: string) =>
|
|
15
|
+
handleTimelineAssetDrop(assetPath, {
|
|
16
|
+
start: usePlayerStore.getState().currentTime,
|
|
17
|
+
track: 0,
|
|
18
|
+
}),
|
|
19
|
+
[handleTimelineAssetDrop],
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { useSlideshowTabState } from "./useSlideshowTabState";
|
|
7
|
+
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
const SLIDESHOW_HTML = `<html><body><script type="application/hyperframes-slideshow+json">{"slides":[]}</script></body></html>`;
|
|
12
|
+
const PLAIN_HTML = `<html><body><div id="title">hi</div></body></html>`;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
document.body.innerHTML = "";
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function renderHook(params: {
|
|
19
|
+
editingFileContent: string | null | undefined;
|
|
20
|
+
rightPanelTab: RightPanelTab;
|
|
21
|
+
}) {
|
|
22
|
+
const host = document.createElement("div");
|
|
23
|
+
document.body.append(host);
|
|
24
|
+
const root = createRoot(host);
|
|
25
|
+
const setRightPanelTabCalls: RightPanelTab[] = [];
|
|
26
|
+
let current: ReturnType<typeof useSlideshowTabState> | null = null;
|
|
27
|
+
|
|
28
|
+
function Harness() {
|
|
29
|
+
current = useSlideshowTabState({
|
|
30
|
+
editingFileContent: params.editingFileContent,
|
|
31
|
+
previewIframeRef: { current: null },
|
|
32
|
+
refreshKey: 0,
|
|
33
|
+
rightPanelTab: params.rightPanelTab,
|
|
34
|
+
setRightPanelTab: (tab) => setRightPanelTabCalls.push(tab),
|
|
35
|
+
});
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
root.render(React.createElement(Harness));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
getState: (): ReturnType<typeof useSlideshowTabState> => {
|
|
45
|
+
if (!current) throw new Error("useSlideshowTabState did not render");
|
|
46
|
+
return current;
|
|
47
|
+
},
|
|
48
|
+
setRightPanelTabCalls,
|
|
49
|
+
unmount: () => act(() => root.unmount()),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("useSlideshowTabState", () => {
|
|
54
|
+
it("detects a slideshow composition via the JSON island", () => {
|
|
55
|
+
const harness = renderHook({ editingFileContent: SLIDESHOW_HTML, rightPanelTab: "design" });
|
|
56
|
+
expect(harness.getState().isSlideshowComposition).toBe(true);
|
|
57
|
+
harness.unmount();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("reports false for a plain (non-slideshow) composition", () => {
|
|
61
|
+
const harness = renderHook({ editingFileContent: PLAIN_HTML, rightPanelTab: "design" });
|
|
62
|
+
expect(harness.getState().isSlideshowComposition).toBe(false);
|
|
63
|
+
harness.unmount();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reports false when there is no editing file yet", () => {
|
|
67
|
+
const harness = renderHook({ editingFileContent: undefined, rightPanelTab: "design" });
|
|
68
|
+
expect(harness.getState().isSlideshowComposition).toBe(false);
|
|
69
|
+
harness.unmount();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("still detects a malformed island — presence-only, not full manifest validation", () => {
|
|
73
|
+
const malformed = `<html><body><script type="application/hyperframes-slideshow+json">{not valid json</script></body></html>`;
|
|
74
|
+
const harness = renderHook({ editingFileContent: malformed, rightPanelTab: "design" });
|
|
75
|
+
expect(harness.getState().isSlideshowComposition).toBe(true);
|
|
76
|
+
harness.unmount();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("bounces rightPanelTab off 'slideshow' to 'renders' on a non-slideshow composition", () => {
|
|
80
|
+
const harness = renderHook({ editingFileContent: PLAIN_HTML, rightPanelTab: "slideshow" });
|
|
81
|
+
expect(harness.setRightPanelTabCalls).toEqual(["renders"]);
|
|
82
|
+
harness.unmount();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not bounce when the composition is a slideshow", () => {
|
|
86
|
+
const harness = renderHook({ editingFileContent: SLIDESHOW_HTML, rightPanelTab: "slideshow" });
|
|
87
|
+
expect(harness.setRightPanelTabCalls).toEqual([]);
|
|
88
|
+
harness.unmount();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("does not bounce a tab other than 'slideshow'", () => {
|
|
92
|
+
const harness = renderHook({ editingFileContent: PLAIN_HTML, rightPanelTab: "renders" });
|
|
93
|
+
expect(harness.setRightPanelTabCalls).toEqual([]);
|
|
94
|
+
harness.unmount();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useMemo, type MutableRefObject } from "react";
|
|
2
|
+
import { SLIDESHOW_ISLAND_TYPE, slideshowIslandRegex } from "@hyperframes/core/slideshow";
|
|
3
|
+
import type { SceneInfo } from "../components/panels/SlideshowPanel";
|
|
4
|
+
import type { IframeWindow } from "../player/lib/playbackTypes";
|
|
5
|
+
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Derives whether the currently-edited composition is a slideshow (carries
|
|
9
|
+
* the slideshow JSON island — the same definitive marker the CLI's `present`
|
|
10
|
+
* command requires; it refuses to run without one) and the live scene list
|
|
11
|
+
* for the Slideshow panel, and bounces `rightPanelTab` off "slideshow" the
|
|
12
|
+
* moment it stops applying (e.g. the user switches to a non-slideshow file
|
|
13
|
+
* while that tab was open) so the panel never shows a dangling active tab
|
|
14
|
+
* whose button is no longer even rendered.
|
|
15
|
+
*
|
|
16
|
+
* Extracted from StudioRightPanel to keep that file under the 600-LOC gate.
|
|
17
|
+
*/
|
|
18
|
+
export function useSlideshowTabState(params: {
|
|
19
|
+
editingFileContent: string | null | undefined;
|
|
20
|
+
previewIframeRef: MutableRefObject<HTMLIFrameElement | null>;
|
|
21
|
+
refreshKey: number;
|
|
22
|
+
rightPanelTab: RightPanelTab;
|
|
23
|
+
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
24
|
+
}): { isSlideshowComposition: boolean; slideshowScenes: SceneInfo[] } {
|
|
25
|
+
const { editingFileContent, previewIframeRef, refreshKey, rightPanelTab, setRightPanelTab } =
|
|
26
|
+
params;
|
|
27
|
+
|
|
28
|
+
// Presence-only (not full manifest validation): a malformed island should
|
|
29
|
+
// still surface the Slideshow tab so the user can see/fix it, rather than
|
|
30
|
+
// making the whole panel disappear. The plain substring check short-circuits
|
|
31
|
+
// the regex scan on every non-slideshow file (the common case) without
|
|
32
|
+
// paying for a full-content RegExp pass.
|
|
33
|
+
const isSlideshowComposition = useMemo(() => {
|
|
34
|
+
if (!editingFileContent || !editingFileContent.includes(SLIDESHOW_ISLAND_TYPE)) return false;
|
|
35
|
+
return slideshowIslandRegex("i").test(editingFileContent);
|
|
36
|
+
}, [editingFileContent]);
|
|
37
|
+
|
|
38
|
+
// Derive scene list from the live clip manifest in the preview iframe.
|
|
39
|
+
const slideshowScenes = useMemo<SceneInfo[]>(() => {
|
|
40
|
+
try {
|
|
41
|
+
const win = previewIframeRef.current?.contentWindow as IframeWindow | null;
|
|
42
|
+
return (win?.__clipManifest?.scenes ?? []).map((s) => ({
|
|
43
|
+
id: s.id,
|
|
44
|
+
label: s.label,
|
|
45
|
+
start: s.start,
|
|
46
|
+
duration: s.duration,
|
|
47
|
+
}));
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, [previewIframeRef, rightPanelTab, refreshKey]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (rightPanelTab === "slideshow" && !isSlideshowComposition) {
|
|
56
|
+
setRightPanelTab("renders");
|
|
57
|
+
}
|
|
58
|
+
}, [rightPanelTab, isSlideshowComposition, setRightPanelTab]);
|
|
59
|
+
|
|
60
|
+
return { isSlideshowComposition, slideshowScenes };
|
|
61
|
+
}
|
|
@@ -50,9 +50,16 @@ export interface ClipManifestClip {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface ClipManifest {
|
|
53
|
+
protocolVersion?: number;
|
|
54
|
+
compositionContractVersion?: number;
|
|
55
|
+
capabilities?: readonly string[];
|
|
56
|
+
fps?: { numerator: number; denominator: number };
|
|
57
|
+
durationSeconds?: number;
|
|
53
58
|
clips: ClipManifestClip[];
|
|
54
59
|
scenes: Array<{ id: string; label: string; start: number; duration: number }>;
|
|
55
60
|
durationInFrames: number;
|
|
61
|
+
compositionWidth?: number;
|
|
62
|
+
compositionHeight?: number;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
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
|
});
|
|
@@ -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,
|
|
@@ -255,24 +256,20 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
255
256
|
if (node === rootComp) return;
|
|
256
257
|
if (isTimelineIgnoredElement(node)) return;
|
|
257
258
|
const el = node as HTMLElement;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (isNaN(start)) return;
|
|
259
|
+
const timing = readClipTiming(el);
|
|
260
|
+
const start = timing.start;
|
|
261
|
+
if (start == null) return;
|
|
262
262
|
if (Number.isFinite(rootDuration) && rootDuration > 0 && start >= rootDuration) return;
|
|
263
263
|
|
|
264
264
|
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);
|
|
265
|
+
let dur = timing.duration ?? 0;
|
|
266
|
+
if (dur <= 0) dur = Math.max(0, rootDuration - start);
|
|
269
267
|
if (Number.isFinite(rootDuration) && rootDuration > 0) {
|
|
270
268
|
dur = Math.min(dur, Math.max(0, rootDuration - start));
|
|
271
269
|
}
|
|
272
270
|
if (!Number.isFinite(dur) || dur <= 0) return;
|
|
273
271
|
|
|
274
|
-
const
|
|
275
|
-
const track = trackStr != null ? parseInt(trackStr, 10) : trackCounter++;
|
|
272
|
+
const track = timing.trackSource === "default" ? trackCounter++ : timing.trackIndex;
|
|
276
273
|
// fallow-ignore-next-line code-duplication
|
|
277
274
|
const compId = el.getAttribute("data-composition-id");
|
|
278
275
|
const selector = getTimelineElementSelector(el);
|
|
@@ -299,7 +296,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
299
296
|
tag: tagLower,
|
|
300
297
|
start,
|
|
301
298
|
duration: dur,
|
|
302
|
-
track
|
|
299
|
+
track,
|
|
303
300
|
domId: el.id || undefined,
|
|
304
301
|
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
305
302
|
selector,
|