@hyperframes/studio 0.8.59 → 0.8.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-BBub3ZdI.js → hyperframes-player-nmVkPQza.js} +1 -1
- package/dist/assets/{index-CI7WON-E.js → index-2-HsQB2C.js} +1 -1
- package/dist/assets/{index-D1KNPijK.js → index-BK36b0XY.js} +162 -162
- package/dist/assets/{index-BxZ5Cw5w.js → index-COG0K-CQ.js} +1 -1
- package/dist/assets/{index-DZBHG9TW.css → index-DS0kLbuM.css} +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.html +2 -2
- package/dist/index.js +4977 -4273
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -0
- package/src/components/StudioHeader.tsx +2 -13
- package/src/components/TimelineToolbar.test.tsx +42 -0
- package/src/components/TimelineToolbar.tsx +5 -10
- package/src/components/editor/propertyPanelFill.tsx +3 -2
- package/src/components/icons/AudioMetersIcon.tsx +16 -0
- package/src/components/icons/InspectorIcon.tsx +18 -0
- package/src/components/icons/ReverseGradientIcon.tsx +23 -0
- package/src/components/icons/RippleEditIcon.tsx +19 -0
- package/src/components/nle/AudioMeterStrip.test.tsx +278 -7
- package/src/components/nle/AudioMeterStrip.tsx +264 -47
- package/src/components/ui/Menu.test.tsx +3 -4
- package/src/components/ui/Menu.tsx +18 -5
- package/src/components/ui/Select.tsx +7 -1
- package/src/components/ui/Tooltip.tsx +6 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +56 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +13 -0
- package/src/hooks/useRenderClipContent.test.ts +33 -4
- package/src/hooks/useRenderClipContent.ts +7 -3
- package/src/hooks/useSdkSession.lifecycle.test.tsx +179 -0
- package/src/hooks/useSdkSession.ts +84 -14
- package/src/hooks/useStudioSdkSessions.ts +8 -1
- package/src/player/components/AudioWaveform.test.tsx +53 -1
- package/src/player/components/AudioWaveform.tsx +124 -65
- package/src/player/components/ImageThumbnail.tsx +3 -5
- package/src/player/components/LayerDisclosureRow.tsx +9 -4
- package/src/player/components/PlayheadIndicator.tsx +3 -3
- package/src/player/components/TimelineAutomationLane.test.tsx +1 -1
- package/src/player/components/TimelineAutomationLane.tsx +5 -5
- package/src/player/components/TimelineCanvas.tsx +18 -15
- package/src/player/components/TimelineClip.test.tsx +41 -4
- package/src/player/components/TimelineClip.tsx +11 -4
- package/src/player/components/TimelineClipDiamonds.test.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.tsx +2 -2
- package/src/player/components/TimelineClipFades.tsx +64 -31
- package/src/player/components/TimelineDiamondConnectors.tsx +2 -2
- package/src/player/components/TimelineEmptyState.tsx +1 -1
- package/src/player/components/TimelineFxButton.tsx +10 -8
- package/src/player/components/TimelineGroupHeader.tsx +18 -9
- package/src/player/components/TimelineGroupLaneLabels.tsx +12 -5
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +14 -0
- package/src/player/components/TimelineLanes.tsx +15 -8
- package/src/player/components/TimelinePropertyLanes.test.tsx +1 -1
- package/src/player/components/TimelineRazorInteraction.tsx +1 -1
- package/src/player/components/TimelineRuler.tsx +3 -2
- package/src/player/components/TimelineShortcutHint.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +4 -6
- package/src/player/components/TimelineTrackPlainHeader.tsx +10 -3
- package/src/player/components/TimelineTransitionBadge.tsx +59 -0
- package/src/player/components/TimelineTransitionOverlays.tsx +30 -0
- package/src/player/components/VideoThumbnail.test.tsx +23 -12
- package/src/player/components/VideoThumbnail.tsx +86 -32
- package/src/player/components/audioClipLink.test.ts +34 -0
- package/src/player/components/audioClipLink.ts +33 -0
- package/src/player/components/audioWaveformPeaks.test.ts +32 -0
- package/src/player/components/audioWaveformPeaks.ts +46 -0
- package/src/player/components/thumbnailUtils.test.ts +17 -0
- package/src/player/components/thumbnailUtils.ts +7 -0
- package/src/player/components/timelineMotionStyles.test.ts +96 -30
- package/src/player/components/timelineTheme.test.ts +14 -0
- package/src/player/components/timelineTheme.ts +18 -11
- package/src/player/components/timelineTransitionSeams.test.ts +48 -0
- package/src/player/components/timelineTransitionSeams.ts +36 -0
- package/src/player/components/timelineZones.test.ts +18 -0
- package/src/player/components/timelineZones.ts +4 -1
- package/src/player/lib/playbackTypes.ts +2 -0
- package/src/player/lib/timelineDOM.test.ts +28 -0
- package/src/player/lib/timelineDOM.ts +24 -2
- package/src/player/lib/timelineIframeHelpers.ts +7 -0
- package/src/player/lib/timelineTransitionMetadata.ts +52 -0
- package/src/player/lib/timelineViewportBudgets.ts +2 -1
- package/src/player/store/timelineElement.ts +1 -0
- package/src/styles/hex-baseline.json +3 -22
- package/src/styles/studio.css +126 -46
- package/src/styles/theme.css +113 -1
- package/src/styles/theme.test.ts +9 -0
- package/src/utils/audioMeterVisibility.ts +2 -2
- package/src/utils/sdkCutover.test.ts +102 -1
- package/src/utils/sdkCutover.ts +16 -21
- package/src/utils/sdkEditTransaction.ts +50 -1
- package/src/utils/studioUiPreferences.ts +1 -1
|
@@ -27,14 +27,14 @@ export interface TimelineTheme {
|
|
|
27
27
|
clipBorder: string;
|
|
28
28
|
clipBorderHover: string;
|
|
29
29
|
clipBorderActive: string;
|
|
30
|
-
clipShadow: string;
|
|
31
|
-
clipShadowHover: string;
|
|
32
|
-
clipShadowActive: string;
|
|
33
|
-
clipShadowDragging: string;
|
|
34
30
|
handleColor: string;
|
|
35
31
|
panelResizeSeam: string;
|
|
36
32
|
panelResizeActive: string;
|
|
37
33
|
clipRadius: string;
|
|
34
|
+
audioClipRadius: string;
|
|
35
|
+
transitionZone: string;
|
|
36
|
+
transitionBadge: string;
|
|
37
|
+
transitionBadgeInk: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const TRACK_STYLE: TimelineTrackStyle = {
|
|
@@ -67,18 +67,25 @@ export const defaultTimelineTheme: TimelineTheme = {
|
|
|
67
67
|
clipBorder: "var(--timeline-clip-border)",
|
|
68
68
|
clipBorderHover: "var(--timeline-clip-border-hover)",
|
|
69
69
|
clipBorderActive: "var(--timeline-clip-border-active)",
|
|
70
|
-
// Shadows stay literal: a light host still wants a dark contact shadow, so
|
|
71
|
-
// these are not part of the surface/hairline/text/clip-colour theming ask.
|
|
72
|
-
clipShadow: "none",
|
|
73
|
-
clipShadowHover: "0 2px 8px rgba(0,0,0,0.2)",
|
|
74
|
-
clipShadowActive: "0 2px 8px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.04)",
|
|
75
|
-
clipShadowDragging: "0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
76
70
|
handleColor: "var(--timeline-handle)",
|
|
77
71
|
panelResizeSeam: "var(--timeline-resize-seam)",
|
|
78
72
|
panelResizeActive: "var(--timeline-resize-active)",
|
|
79
|
-
clipRadius: "var(--radius
|
|
73
|
+
clipRadius: "var(--timeline-clip-radius)",
|
|
74
|
+
audioClipRadius: "var(--timeline-clip-audio-radius)",
|
|
75
|
+
transitionZone: "var(--timeline-transition-zone)",
|
|
76
|
+
transitionBadge: "var(--timeline-transition-badge)",
|
|
77
|
+
transitionBadgeInk: "var(--timeline-transition-badge-ink)",
|
|
80
78
|
};
|
|
81
79
|
|
|
80
|
+
export type ClipWidthLadder = "labeled" | "picture" | "frame";
|
|
81
|
+
|
|
82
|
+
/** Label chip from 60px. Under 24px the clip is one cropped frame. */
|
|
83
|
+
export function clipWidthLadder(widthPx: number): ClipWidthLadder {
|
|
84
|
+
if (widthPx < 24) return "frame";
|
|
85
|
+
if (widthPx < 60) return "picture";
|
|
86
|
+
return "labeled";
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
export function getTimelineTrackStyle(_tag: string): TimelineTrackStyle {
|
|
83
90
|
return TRACK_STYLE;
|
|
84
91
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { deriveTimelineTransitionSeams } from "./timelineTransitionSeams";
|
|
3
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
4
|
+
|
|
5
|
+
const clip = (
|
|
6
|
+
id: string,
|
|
7
|
+
start: number,
|
|
8
|
+
duration: number,
|
|
9
|
+
track = 0,
|
|
10
|
+
transitionLabel?: string,
|
|
11
|
+
): TimelineElement => ({
|
|
12
|
+
id,
|
|
13
|
+
tag: "video",
|
|
14
|
+
start,
|
|
15
|
+
duration,
|
|
16
|
+
track,
|
|
17
|
+
transitionLabel,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("deriveTimelineTransitionSeams", () => {
|
|
21
|
+
it("finds a labelled transition and centers the seam in the shared time", () => {
|
|
22
|
+
const label = "hf:transition:out:in:crossfade";
|
|
23
|
+
const [seam] = deriveTimelineTransitionSeams([
|
|
24
|
+
clip("in", 1.8, 2, 0, label),
|
|
25
|
+
clip("out", 0, 2, 0, label),
|
|
26
|
+
]);
|
|
27
|
+
expect(seam?.outgoing).toEqual(clip("out", 0, 2, 0, label));
|
|
28
|
+
expect(seam?.incoming).toEqual(clip("in", 1.8, 2, 0, label));
|
|
29
|
+
expect(seam?.centerTime).toBeCloseTo(1.9);
|
|
30
|
+
expect(seam?.duration).toBeCloseTo(0.2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not badge an unlabelled overlap", () => {
|
|
34
|
+
expect(deriveTimelineTransitionSeams([clip("out", 0, 2), clip("in", 1.8, 2)])).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not badge a cross-track overlap", () => {
|
|
38
|
+
const label = "hf:transition:out:in:crossfade";
|
|
39
|
+
expect(
|
|
40
|
+
deriveTimelineTransitionSeams([clip("out", 0, 2, 0, label), clip("in", 1.8, 2, 1, label)]),
|
|
41
|
+
).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rejects crisp gaps and touching edges", () => {
|
|
45
|
+
expect(deriveTimelineTransitionSeams([clip("a", 0, 2), clip("b", 2.04, 1)])).toEqual([]);
|
|
46
|
+
expect(deriveTimelineTransitionSeams([clip("a", 0, 2), clip("b", 2, 1)])).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
2
|
+
|
|
3
|
+
export interface TimelineTransitionSeam {
|
|
4
|
+
outgoing: TimelineElement;
|
|
5
|
+
incoming: TimelineElement;
|
|
6
|
+
centerTime: number;
|
|
7
|
+
duration: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isTransitionPair(a: TimelineElement, b: TimelineElement): boolean {
|
|
11
|
+
return Boolean(a.transitionLabel && a.transitionLabel === b.transitionLabel);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function deriveTimelineTransitionSeams(
|
|
15
|
+
elements: readonly TimelineElement[],
|
|
16
|
+
): TimelineTransitionSeam[] {
|
|
17
|
+
const sorted = [...elements].sort((left, right) => left.start - right.start);
|
|
18
|
+
const seams: TimelineTransitionSeam[] = [];
|
|
19
|
+
for (let index = 0; index < sorted.length - 1; index += 1) {
|
|
20
|
+
const outgoing = sorted[index]!;
|
|
21
|
+
const incoming = sorted[index + 1]!;
|
|
22
|
+
if (outgoing.track !== incoming.track || !isTransitionPair(outgoing, incoming)) continue;
|
|
23
|
+
const outgoingEnd = outgoing.start + outgoing.duration;
|
|
24
|
+
const overlapStart = Math.max(outgoing.start, incoming.start);
|
|
25
|
+
const overlapEnd = Math.min(outgoingEnd, incoming.start + incoming.duration);
|
|
26
|
+
const duration = overlapEnd - overlapStart;
|
|
27
|
+
if (duration <= 0) continue;
|
|
28
|
+
seams.push({
|
|
29
|
+
outgoing,
|
|
30
|
+
incoming,
|
|
31
|
+
centerTime: overlapStart + duration / 2,
|
|
32
|
+
duration,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return seams;
|
|
36
|
+
}
|
|
@@ -171,6 +171,24 @@ describe("normalizeToZones — CapCut-stable lanes follow the track-index (never
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
describe("normalizeToZones — legacy overlap spill (display-only, deterministic)", () => {
|
|
174
|
+
it("keeps a labelled transition pair on one row", () => {
|
|
175
|
+
const transitionLabel = "hf:transition:outgoing:incoming:crossfade";
|
|
176
|
+
const out = normalizeToZones([
|
|
177
|
+
{ ...zClip("outgoing", 0, 2.5, 0, 0), transitionLabel },
|
|
178
|
+
{ ...zClip("incoming", 2, 2.5, 0, 0), transitionLabel },
|
|
179
|
+
]);
|
|
180
|
+
expect(trackOf(out, "outgoing")).toBe(0);
|
|
181
|
+
expect(trackOf(out, "incoming")).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("still splits an unlabelled overlap", () => {
|
|
185
|
+
const out = normalizeToZones([
|
|
186
|
+
zClip("outgoing", 0, 2.5, 0, 0),
|
|
187
|
+
zClip("incoming", 2, 2.5, 0, 0),
|
|
188
|
+
]);
|
|
189
|
+
expect(trackOf(out, "outgoing")).not.toBe(trackOf(out, "incoming"));
|
|
190
|
+
});
|
|
191
|
+
|
|
174
192
|
it("splits time-overlapping SAME-track clips onto adjacent sub-lanes (no visible overlap)", () => {
|
|
175
193
|
// a [0,5), b [2,7) overlaps a, c [6,9) sequential — all authored on track 1.
|
|
176
194
|
// The editor forbids per-track overlap, but a legacy file can carry it; the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TimelineElement } from "../store/playerStore";
|
|
2
2
|
import { isAudioTimelineElement } from "../../utils/timelineInspector";
|
|
3
|
+
import { isTransitionPair } from "./timelineTransitionSeams";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Free-form vertical zones, top → bottom: visual, audio. Canvas layering is
|
|
@@ -66,7 +67,9 @@ function packTrackLanes(
|
|
|
66
67
|
const ordered = [...clips].sort(byStableId);
|
|
67
68
|
const lanes: TimelineElement[][] = [];
|
|
68
69
|
for (const el of ordered) {
|
|
69
|
-
let sub = lanes.findIndex((occ) =>
|
|
70
|
+
let sub = lanes.findIndex((occ) =>
|
|
71
|
+
occ.every((o) => !overlaps(o, el) || isTransitionPair(o, el)),
|
|
72
|
+
);
|
|
70
73
|
if (sub === -1) {
|
|
71
74
|
sub = lanes.length;
|
|
72
75
|
lanes.push([]);
|
|
@@ -32,6 +32,8 @@ export interface TimelineLike {
|
|
|
32
32
|
time: () => number;
|
|
33
33
|
duration: () => number;
|
|
34
34
|
isActive: () => boolean;
|
|
35
|
+
labels?: Record<string, number>;
|
|
36
|
+
getLabels?: () => Record<string, number>;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface ClipManifestClip extends RuntimeTimelineClipIdentity {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { gsap } from "gsap";
|
|
3
4
|
import {
|
|
4
5
|
createTimelineElementFromManifestClip,
|
|
5
6
|
parseTimelineFromDOM,
|
|
@@ -20,6 +21,33 @@ function makeDoc(html: string): Document {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
24
|
+
it("bridges a real GSAP transition marker onto both named clips", () => {
|
|
25
|
+
document.body.innerHTML = `
|
|
26
|
+
<div data-composition-id="root">
|
|
27
|
+
<img data-hf-id="outgoing" data-start="0" data-duration="2.5" data-track-index="0" />
|
|
28
|
+
<img data-hf-id="incoming" data-start="2" data-duration="2.5" data-track-index="0" />
|
|
29
|
+
</div>
|
|
30
|
+
`;
|
|
31
|
+
const timeline = gsap.timeline({ paused: true });
|
|
32
|
+
timeline.addLabel("hf:transition:outgoing:incoming:crossfade", 2);
|
|
33
|
+
const runtimeWindow = window as Window & {
|
|
34
|
+
__timelines?: Record<string, typeof timeline>;
|
|
35
|
+
};
|
|
36
|
+
runtimeWindow.__timelines = { main: timeline };
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const elements = parseTimelineFromDOM(document, 5);
|
|
40
|
+
expect(elements.map((element) => element.transitionLabel)).toEqual([
|
|
41
|
+
"hf:transition:outgoing:incoming:crossfade",
|
|
42
|
+
"hf:transition:outgoing:incoming:crossfade",
|
|
43
|
+
]);
|
|
44
|
+
} finally {
|
|
45
|
+
delete runtimeWindow.__timelines;
|
|
46
|
+
timeline.kill();
|
|
47
|
+
document.body.innerHTML = "";
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
23
51
|
it("harvests hfId from a data-start element that has data-hf-id", () => {
|
|
24
52
|
const doc = makeDoc(`
|
|
25
53
|
<div data-composition-id="root">
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
|
-
import type { ClipManifestClip } from "./playbackTypes";
|
|
12
|
+
import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
15
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
16
|
+
import { transitionLabelsForDocument } from "./timelineTransitionMetadata";
|
|
16
17
|
import {
|
|
17
18
|
resolveMediaElement,
|
|
18
19
|
applyMediaMetadataFromElement,
|
|
@@ -75,6 +76,9 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
75
76
|
}): TimelineElement {
|
|
76
77
|
const { clip, fallbackIndex, doc } = params;
|
|
77
78
|
let hostEl = params.hostEl ?? null;
|
|
79
|
+
const transitionLabels = doc
|
|
80
|
+
? transitionLabelsForDocument(doc, (doc.defaultView as IframeWindow | null)?.__timelines)
|
|
81
|
+
: new Map<Element, string>();
|
|
78
82
|
const label = getTimelineElementDisplayLabel({
|
|
79
83
|
id: clip.id,
|
|
80
84
|
label: clip.label,
|
|
@@ -108,6 +112,10 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
108
112
|
const entry: TimelineElement = {
|
|
109
113
|
id: identity.id,
|
|
110
114
|
label,
|
|
115
|
+
transitionLabel:
|
|
116
|
+
(hostEl && transitionLabels.get(hostEl)) ||
|
|
117
|
+
hostEl?.getAttribute("data-transition-label") ||
|
|
118
|
+
undefined,
|
|
111
119
|
key: identity.key,
|
|
112
120
|
kind: clip.kind,
|
|
113
121
|
tag: resolveClipTag(clip),
|
|
@@ -134,6 +142,10 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
134
142
|
|
|
135
143
|
if (hostEl) {
|
|
136
144
|
applyMediaMetadataFromElement(entry, hostEl);
|
|
145
|
+
if (!entry.src) {
|
|
146
|
+
const rawSrc = hostEl.getAttribute("src");
|
|
147
|
+
if (rawSrc) entry.src = new URL(rawSrc, hostEl.baseURI).href;
|
|
148
|
+
}
|
|
137
149
|
if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
|
|
138
150
|
const timelineRole = hostEl.getAttribute("data-timeline-role");
|
|
139
151
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
@@ -205,11 +217,19 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
205
217
|
* Parse [data-start] elements from a Document into TimelineElement[].
|
|
206
218
|
* Shared helper — used by onIframeLoad fallback, handleMessage, and enrichMissingCompositions.
|
|
207
219
|
*/
|
|
208
|
-
export function parseTimelineFromDOM(
|
|
220
|
+
export function parseTimelineFromDOM(
|
|
221
|
+
doc: Document,
|
|
222
|
+
rootDuration: number,
|
|
223
|
+
timelines?: Readonly<Record<string, TimelineLike>>,
|
|
224
|
+
): TimelineElement[] {
|
|
209
225
|
const rootComp = doc.querySelector("[data-composition-id]");
|
|
210
226
|
const nodes = doc.querySelectorAll("[data-start]");
|
|
211
227
|
const els: TimelineElement[] = [];
|
|
212
228
|
let trackCounter = 0;
|
|
229
|
+
const transitionLabels = transitionLabelsForDocument(
|
|
230
|
+
doc,
|
|
231
|
+
timelines ?? (doc.defaultView as IframeWindow | null)?.__timelines,
|
|
232
|
+
);
|
|
213
233
|
|
|
214
234
|
// fallow-ignore-next-line complexity
|
|
215
235
|
nodes.forEach((node) => {
|
|
@@ -252,6 +272,8 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
252
272
|
const entry: TimelineElement = {
|
|
253
273
|
id: identity.id,
|
|
254
274
|
label,
|
|
275
|
+
transitionLabel:
|
|
276
|
+
transitionLabels.get(el) ?? el.getAttribute("data-transition-label") ?? undefined,
|
|
255
277
|
key: identity.key,
|
|
256
278
|
kind:
|
|
257
279
|
compId && compId !== rootComp?.getAttribute("data-composition-id")
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
readTimelineElementZIndex,
|
|
23
23
|
} from "./timelineElementHelpers";
|
|
24
24
|
import { postRuntimeControlMessage } from "./runtimeProtocol";
|
|
25
|
+
import { transitionLabelsForDocument } from "./timelineTransitionMetadata";
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Viewport / DOM normalisation
|
|
@@ -414,6 +415,7 @@ function buildMissingCompositionEntry(params: {
|
|
|
414
415
|
}): TimelineElement | null {
|
|
415
416
|
const { doc, iframeWin, element, compositionId, rootDuration, fallbackIndex, resolveEnd } =
|
|
416
417
|
params;
|
|
418
|
+
const transitionLabels = transitionLabelsForDocument(doc, iframeWin.__timelines);
|
|
417
419
|
const timing = readClipTiming(element, {
|
|
418
420
|
resolveReferenceEnd: (refId) => resolveEnd(refId, new Set([compositionId])),
|
|
419
421
|
});
|
|
@@ -447,6 +449,8 @@ function buildMissingCompositionEntry(params: {
|
|
|
447
449
|
const entry: TimelineElement = {
|
|
448
450
|
id: identity.id,
|
|
449
451
|
label,
|
|
452
|
+
transitionLabel:
|
|
453
|
+
transitionLabels.get(element) ?? element.getAttribute("data-transition-label") ?? undefined,
|
|
450
454
|
key: identity.key,
|
|
451
455
|
tag: element.tagName.toLowerCase(),
|
|
452
456
|
start: window.start,
|
|
@@ -459,6 +463,9 @@ function buildMissingCompositionEntry(params: {
|
|
|
459
463
|
selectorIndex,
|
|
460
464
|
sourceFile,
|
|
461
465
|
zIndex: readTimelineElementZIndex(element),
|
|
466
|
+
src: optionalNonEmpty(element.getAttribute("src"))
|
|
467
|
+
? new URL(element.getAttribute("src")!, element.baseURI).href
|
|
468
|
+
: undefined,
|
|
462
469
|
};
|
|
463
470
|
return attachCompositionSource(entry, element, compositionSrc);
|
|
464
471
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TimelineLike } from "./playbackTypes";
|
|
2
|
+
|
|
3
|
+
const TRANSITION_LABEL_PREFIX = "hf:transition:";
|
|
4
|
+
|
|
5
|
+
type TimelineWithLabels = TimelineLike & {
|
|
6
|
+
labels?: Record<string, number>;
|
|
7
|
+
getLabels?: () => Record<string, number>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function transitionParts(label: string): [string, string] | null {
|
|
11
|
+
if (!label.startsWith(TRANSITION_LABEL_PREFIX)) return null;
|
|
12
|
+
const parts = label.slice(TRANSITION_LABEL_PREFIX.length).split(":");
|
|
13
|
+
if (parts.length < 3 || !parts[0] || !parts[1]) return null;
|
|
14
|
+
return [parts[0], parts[1]];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findTimedElement(doc: Document, markerId: string): Element | null {
|
|
18
|
+
for (const candidate of [markerId, `el-${markerId}`]) {
|
|
19
|
+
const byId = doc.getElementById(candidate);
|
|
20
|
+
if (byId?.hasAttribute("data-start")) return byId;
|
|
21
|
+
}
|
|
22
|
+
return (
|
|
23
|
+
Array.from(doc.querySelectorAll("[data-start]")).find(
|
|
24
|
+
(element) =>
|
|
25
|
+
element.getAttribute("data-hf-id") === markerId ||
|
|
26
|
+
element.getAttribute("data-composition-id") === markerId,
|
|
27
|
+
) ?? null
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function transitionLabelsForDocument(
|
|
32
|
+
doc: Document,
|
|
33
|
+
timelines: Readonly<Record<string, TimelineLike>> | undefined,
|
|
34
|
+
): ReadonlyMap<Element, string> {
|
|
35
|
+
const labels = new Map<Element, string>();
|
|
36
|
+
if (!timelines) return labels;
|
|
37
|
+
|
|
38
|
+
for (const timeline of Object.values(timelines) as TimelineWithLabels[]) {
|
|
39
|
+
const timelineLabels = timeline.getLabels?.() ?? timeline.labels ?? {};
|
|
40
|
+
for (const label of Object.keys(timelineLabels)) {
|
|
41
|
+
const pair = transitionParts(label);
|
|
42
|
+
if (!pair) continue;
|
|
43
|
+
const outgoing = findTimedElement(doc, pair[0]);
|
|
44
|
+
const incoming = findTimedElement(doc, pair[1]);
|
|
45
|
+
if (outgoing && incoming) {
|
|
46
|
+
labels.set(outgoing, label);
|
|
47
|
+
labels.set(incoming, label);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return labels;
|
|
52
|
+
}
|
|
@@ -49,6 +49,7 @@ export interface TimelineViewportBudgets {
|
|
|
49
49
|
|
|
50
50
|
const MEBIBYTE = 1024 * 1024;
|
|
51
51
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
52
|
+
export const MAX_VISIBLE_THUMBNAIL_FRAMES = Math.ceil(3840 / (66 * (16 / 9))); // 4K timeline width / tallest 16:9 tile.
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* The sole default budget owner for timeline viewport and media virtualization.
|
|
@@ -66,7 +67,7 @@ export const TIMELINE_VIEWPORT_BUDGETS: Readonly<TimelineViewportBudgets> = Obje
|
|
|
66
67
|
posterMaxPhysicalWidth: 240,
|
|
67
68
|
posterMaxPhysicalHeight: 135,
|
|
68
69
|
posterDprCap: 1.5,
|
|
69
|
-
richPreviewFrameCount:
|
|
70
|
+
richPreviewFrameCount: MAX_VISIBLE_THUMBNAIL_FRAMES,
|
|
70
71
|
concurrentVideoDecodes: 2,
|
|
71
72
|
concurrentMetadataJobs: 4,
|
|
72
73
|
concurrentCompositionFetches: 2,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"total":
|
|
2
|
+
"total": 297,
|
|
3
3
|
"files": {
|
|
4
4
|
"packages/studio/src/captions/components/CaptionOverlay.tsx": 5,
|
|
5
5
|
"packages/studio/src/captions/components/CaptionTimeline.tsx": 11,
|
|
@@ -49,35 +49,16 @@
|
|
|
49
49
|
"packages/studio/src/components/sidebar/AudioRow.tsx": 2,
|
|
50
50
|
"packages/studio/src/components/ui/HyperframesMark.tsx": 4,
|
|
51
51
|
"packages/studio/src/hooks/useInlineTextEdit.ts": 1,
|
|
52
|
-
"packages/studio/src/player/components/AudioWaveform.tsx":
|
|
52
|
+
"packages/studio/src/player/components/AudioWaveform.tsx": 2,
|
|
53
53
|
"packages/studio/src/player/components/BeatStrip.tsx": 6,
|
|
54
54
|
"packages/studio/src/player/components/CompositionThumbnail.tsx": 2,
|
|
55
|
-
"packages/studio/src/player/components/ImageThumbnail.tsx": 6,
|
|
56
|
-
"packages/studio/src/player/components/LayerDisclosureRow.tsx": 3,
|
|
57
55
|
"packages/studio/src/player/components/Player.tsx": 2,
|
|
58
56
|
"packages/studio/src/player/components/PlayerControls.tsx": 1,
|
|
59
|
-
"packages/studio/src/player/components/PlayheadIndicator.tsx": 3,
|
|
60
57
|
"packages/studio/src/player/components/ShortcutsPanel.tsx": 7,
|
|
61
58
|
"packages/studio/src/player/components/SpeedMenu.tsx": 2,
|
|
62
|
-
"packages/studio/src/player/components/TimelineAutomationLane.tsx": 4,
|
|
63
|
-
"packages/studio/src/player/components/TimelineCanvas.tsx": 13,
|
|
64
|
-
"packages/studio/src/player/components/TimelineClipDiamonds.tsx": 2,
|
|
65
|
-
"packages/studio/src/player/components/TimelineDiamondConnectors.tsx": 2,
|
|
66
|
-
"packages/studio/src/player/components/TimelineEmptyState.tsx": 1,
|
|
67
|
-
"packages/studio/src/player/components/TimelineFxButton.tsx": 8,
|
|
68
|
-
"packages/studio/src/player/components/TimelineGroupHeader.tsx": 6,
|
|
69
|
-
"packages/studio/src/player/components/TimelineGroupLaneLabels.tsx": 1,
|
|
70
|
-
"packages/studio/src/player/components/TimelineGroupRow.tsx": 1,
|
|
71
|
-
"packages/studio/src/player/components/TimelineRazorInteraction.tsx": 1,
|
|
72
|
-
"packages/studio/src/player/components/TimelineRuler.tsx": 1,
|
|
73
|
-
"packages/studio/src/player/components/TimelineShortcutHint.tsx": 2,
|
|
74
|
-
"packages/studio/src/player/components/TimelineTrackHeader.tsx": 2,
|
|
75
|
-
"packages/studio/src/player/components/TimelineTrackPlainHeader.tsx": 2,
|
|
76
|
-
"packages/studio/src/player/components/VideoThumbnail.tsx": 6,
|
|
77
|
-
"packages/studio/src/player/components/timelineTheme.ts": 5,
|
|
78
59
|
"packages/studio/src/player/components/trackHeaderLabelRows.tsx": 6,
|
|
79
|
-
"packages/studio/src/styles/studio.css": 63,
|
|
80
60
|
"packages/studio/src/telemetry/client.ts": 1,
|
|
61
|
+
"packages/studio/src/styles/studio.css": 48,
|
|
81
62
|
"packages/studio/src/webmcp/webmcpTestUtils.ts": 1
|
|
82
63
|
}
|
|
83
64
|
}
|