@hyperframes/studio 0.6.0 → 0.6.1
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-CzwFysqv.js +418 -0
- package/dist/assets/index-hYc4aP7M.js +117 -0
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/App.tsx +2 -13
- package/src/captions/components/CaptionOverlay.tsx +13 -246
- package/src/captions/components/CaptionOverlayUtils.ts +221 -0
- package/src/components/StudioPreviewArea.tsx +6 -2
- package/src/components/editor/DomEditOverlay.tsx +88 -1007
- package/src/components/editor/EaseCurveEditor.tsx +221 -0
- package/src/components/editor/FileTree.tsx +13 -621
- package/src/components/editor/FileTreeIcons.tsx +128 -0
- package/src/components/editor/FileTreeNodes.tsx +496 -0
- package/src/components/editor/MotionPanel.tsx +16 -390
- package/src/components/editor/MotionPanelFields.tsx +185 -0
- package/src/components/editor/domEditOverlayGeometry.ts +211 -0
- package/src/components/editor/domEditOverlayGestures.ts +138 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
- package/src/components/editor/domEditing.ts +44 -1150
- package/src/components/editor/domEditingAgentPrompt.ts +97 -0
- package/src/components/editor/domEditingDom.ts +266 -0
- package/src/components/editor/domEditingElement.ts +329 -0
- package/src/components/editor/domEditingLayers.ts +460 -0
- package/src/components/editor/domEditingTypes.ts +125 -0
- package/src/components/editor/manualEdits.ts +84 -1081
- package/src/components/editor/manualEditsDom.ts +436 -0
- package/src/components/editor/manualEditsParsing.ts +280 -0
- package/src/components/editor/manualEditsSnapshot.ts +333 -0
- package/src/components/editor/manualEditsTypes.ts +141 -0
- package/src/components/editor/studioMotion.ts +47 -434
- package/src/components/editor/studioMotionOps.ts +299 -0
- package/src/components/editor/studioMotionTypes.ts +168 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
- package/src/components/editor/useDomEditOverlayRects.ts +207 -0
- package/src/components/nle/NLELayout.tsx +60 -144
- package/src/components/nle/useCompositionStack.ts +126 -0
- package/src/hooks/useToast.ts +20 -0
- package/src/player/components/Timeline.tsx +189 -1418
- package/src/player/components/TimelineCanvas.tsx +434 -0
- package/src/player/components/TimelineEmptyState.tsx +102 -0
- package/src/player/components/TimelineRuler.tsx +90 -0
- package/src/player/components/timelineIcons.tsx +49 -0
- package/src/player/components/timelineLayout.ts +215 -0
- package/src/player/components/timelineUtils.ts +211 -0
- package/src/player/components/useTimelineClipDrag.ts +388 -0
- package/src/player/components/useTimelinePlayhead.ts +200 -0
- package/src/player/components/useTimelineRangeSelection.ts +135 -0
- package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
- package/src/player/hooks/useTimelinePlayer.ts +69 -1372
- package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
- package/src/player/lib/playbackAdapter.ts +145 -0
- package/src/player/lib/playbackShortcuts.ts +68 -0
- package/src/player/lib/playbackTypes.ts +60 -0
- package/src/player/lib/timelineDOM.ts +373 -0
- package/src/player/lib/timelineElementHelpers.ts +303 -0
- package/src/player/lib/timelineIframeHelpers.ts +269 -0
- package/dist/assets/hyperframes-player-DOFETgjy.js +0 -418
- package/dist/assets/index-DUqUmaoH.js +0 -117
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime iframe integration utilities.
|
|
3
|
+
*
|
|
4
|
+
* Handles the boundary between the studio host page and the preview iframe:
|
|
5
|
+
* - Viewport normalisation on load
|
|
6
|
+
* - Auto-healing missing data-composition-id attributes
|
|
7
|
+
* - Unmuting media via postMessage
|
|
8
|
+
* - Resolving the underlying <iframe> from any wrapper element
|
|
9
|
+
* - Scanning the DOM for composition hosts the manifest missed
|
|
10
|
+
* (element-reference starts that the CDN runtime fails to resolve)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
14
|
+
import type { IframeWindow } from "./playbackTypes";
|
|
15
|
+
import {
|
|
16
|
+
getTimelineElementSelector,
|
|
17
|
+
getTimelineElementSourceFile,
|
|
18
|
+
getTimelineElementSelectorIndex,
|
|
19
|
+
getTimelineElementDisplayLabel,
|
|
20
|
+
buildTimelineElementIdentity,
|
|
21
|
+
} from "./timelineElementHelpers";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Viewport / DOM normalisation
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export function normalizePreviewViewport(doc: Document, win: Window): void {
|
|
28
|
+
if (doc.documentElement) {
|
|
29
|
+
doc.documentElement.style.overflow = "hidden";
|
|
30
|
+
doc.documentElement.style.margin = "0";
|
|
31
|
+
}
|
|
32
|
+
if (doc.body) {
|
|
33
|
+
doc.body.style.overflow = "hidden";
|
|
34
|
+
doc.body.style.margin = "0";
|
|
35
|
+
}
|
|
36
|
+
win.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function autoHealMissingCompositionIds(doc: Document): void {
|
|
40
|
+
const compositionIdRe = /data-composition-id=["']([^"']+)["']/gi;
|
|
41
|
+
const referencedIds = new Set<string>();
|
|
42
|
+
const scopedNodes = Array.from(doc.querySelectorAll("style, script"));
|
|
43
|
+
for (const node of scopedNodes) {
|
|
44
|
+
const text = node.textContent || "";
|
|
45
|
+
if (!text) continue;
|
|
46
|
+
let match: RegExpExecArray | null;
|
|
47
|
+
while ((match = compositionIdRe.exec(text)) !== null) {
|
|
48
|
+
const id = (match[1] || "").trim();
|
|
49
|
+
if (id) referencedIds.add(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (referencedIds.size === 0) return;
|
|
54
|
+
|
|
55
|
+
const existingIds = new Set<string>();
|
|
56
|
+
const existingNodes = Array.from(doc.querySelectorAll<HTMLElement>("[data-composition-id]"));
|
|
57
|
+
for (const node of existingNodes) {
|
|
58
|
+
const id = node.getAttribute("data-composition-id");
|
|
59
|
+
if (id) existingIds.add(id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const compId of referencedIds) {
|
|
63
|
+
if (compId === "root" || existingIds.has(compId)) continue;
|
|
64
|
+
const host =
|
|
65
|
+
doc.getElementById(`${compId}-layer`) ||
|
|
66
|
+
doc.getElementById(`${compId}-comp`) ||
|
|
67
|
+
doc.getElementById(compId);
|
|
68
|
+
if (!host) continue;
|
|
69
|
+
if (!host.getAttribute("data-composition-id")) {
|
|
70
|
+
host.setAttribute("data-composition-id", compId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Muting / iframe resolution
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
export function unmutePreviewMedia(iframe: HTMLIFrameElement | null): void {
|
|
80
|
+
if (!iframe) return;
|
|
81
|
+
try {
|
|
82
|
+
iframe.contentWindow?.postMessage(
|
|
83
|
+
{ source: "hf-parent", type: "control", action: "set-muted", muted: false },
|
|
84
|
+
"*",
|
|
85
|
+
);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn("[useTimelinePlayer] Failed to unmute preview media", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the underlying iframe from any host element. Supports:
|
|
93
|
+
* - Direct `<iframe>` element (most common — studio's own `Player.tsx`)
|
|
94
|
+
* - Custom elements (e.g. `<hyperframes-player>`) whose shadow DOM contains an iframe
|
|
95
|
+
* - Wrapper elements whose light DOM contains a descendant iframe
|
|
96
|
+
*
|
|
97
|
+
* Exported so web-component consumers can pre-resolve the iframe before
|
|
98
|
+
* assigning it to `iframeRef` returned by `useTimelinePlayer`. Returns `null`
|
|
99
|
+
* when the element has no associated iframe yet.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* const { iframeRef } = useTimelinePlayer();
|
|
104
|
+
* const playerElRef = useRef<HyperframesPlayer>(null);
|
|
105
|
+
*
|
|
106
|
+
* useEffect(() => {
|
|
107
|
+
* iframeRef.current = resolveIframe(playerElRef.current);
|
|
108
|
+
* }, [iframeRef]);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function resolveIframe(el: Element | null): HTMLIFrameElement | null {
|
|
112
|
+
if (!el) return null;
|
|
113
|
+
if (el instanceof HTMLIFrameElement) return el;
|
|
114
|
+
return el.shadowRoot?.querySelector("iframe") ?? el.querySelector("iframe") ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Enrich missing compositions from DOM
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Scan the iframe DOM for composition hosts missing from the current
|
|
123
|
+
* timeline elements and add them. The CDN runtime often fails to resolve
|
|
124
|
+
* element-reference starts (`data-start="intro"`) so composition hosts
|
|
125
|
+
* are silently dropped from `__clipManifest`. This pass reads the DOM +
|
|
126
|
+
* GSAP timeline registry directly to fill the gaps.
|
|
127
|
+
*/
|
|
128
|
+
export function buildMissingCompositionElements(
|
|
129
|
+
doc: Document,
|
|
130
|
+
iframeWin: IframeWindow,
|
|
131
|
+
currentEls: readonly TimelineElement[],
|
|
132
|
+
rootDuration: number,
|
|
133
|
+
): { missing: TimelineElement[]; updatedEls: TimelineElement[]; patched: boolean } {
|
|
134
|
+
const existingIds = new Set(currentEls.map((e) => e.id));
|
|
135
|
+
const rootComp = doc.querySelector("[data-composition-id]");
|
|
136
|
+
const rootCompId = rootComp?.getAttribute("data-composition-id");
|
|
137
|
+
// Use [data-composition-id][data-start] — the composition loader strips
|
|
138
|
+
// data-composition-src after loading, so we can't rely on it.
|
|
139
|
+
const hosts = doc.querySelectorAll("[data-composition-id][data-start]");
|
|
140
|
+
const missing: TimelineElement[] = [];
|
|
141
|
+
|
|
142
|
+
hosts.forEach((host) => {
|
|
143
|
+
const el = host as HTMLElement;
|
|
144
|
+
const compId = el.getAttribute("data-composition-id");
|
|
145
|
+
if (!compId || compId === rootCompId) return;
|
|
146
|
+
if (existingIds.has(el.id) || existingIds.has(compId)) return;
|
|
147
|
+
|
|
148
|
+
// Resolve start: numeric or element-reference
|
|
149
|
+
const startAttr = el.getAttribute("data-start") ?? "0";
|
|
150
|
+
let start = parseFloat(startAttr);
|
|
151
|
+
if (isNaN(start)) {
|
|
152
|
+
const ref =
|
|
153
|
+
doc.getElementById(startAttr) || doc.querySelector(`[data-composition-id="${startAttr}"]`);
|
|
154
|
+
if (ref) {
|
|
155
|
+
const refStartAttr = ref.getAttribute("data-start") ?? "0";
|
|
156
|
+
let refStart = parseFloat(refStartAttr);
|
|
157
|
+
// Recursively resolve one level of reference for the ref's own start
|
|
158
|
+
if (isNaN(refStart)) {
|
|
159
|
+
const refRef =
|
|
160
|
+
doc.getElementById(refStartAttr) ||
|
|
161
|
+
doc.querySelector(`[data-composition-id="${refStartAttr}"]`);
|
|
162
|
+
const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
|
|
163
|
+
const rrCompId = refRef?.getAttribute("data-composition-id");
|
|
164
|
+
const rrDur =
|
|
165
|
+
parseFloat(refRef?.getAttribute("data-duration") ?? "") ||
|
|
166
|
+
(rrCompId
|
|
167
|
+
? ((
|
|
168
|
+
iframeWin.__timelines?.[rrCompId] as { duration?: () => number } | undefined
|
|
169
|
+
)?.duration?.() ?? 0)
|
|
170
|
+
: 0);
|
|
171
|
+
refStart = rrStart + rrDur;
|
|
172
|
+
}
|
|
173
|
+
const refCompId = ref.getAttribute("data-composition-id");
|
|
174
|
+
const refDur =
|
|
175
|
+
parseFloat(ref.getAttribute("data-duration") ?? "") ||
|
|
176
|
+
(refCompId
|
|
177
|
+
? ((
|
|
178
|
+
iframeWin.__timelines?.[refCompId] as { duration?: () => number } | undefined
|
|
179
|
+
)?.duration?.() ?? 0)
|
|
180
|
+
: 0);
|
|
181
|
+
start = refStart + refDur;
|
|
182
|
+
} else {
|
|
183
|
+
start = 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Resolve duration from data-duration or GSAP timeline
|
|
188
|
+
let dur = parseFloat(el.getAttribute("data-duration") ?? "");
|
|
189
|
+
if (isNaN(dur) || dur <= 0) {
|
|
190
|
+
dur =
|
|
191
|
+
(
|
|
192
|
+
iframeWin.__timelines?.[compId] as { duration?: () => number } | undefined
|
|
193
|
+
)?.duration?.() ?? 0;
|
|
194
|
+
}
|
|
195
|
+
if (!Number.isFinite(dur) || dur <= 0) return;
|
|
196
|
+
if (!Number.isFinite(start)) start = 0;
|
|
197
|
+
if (Number.isFinite(rootDuration) && rootDuration > 0) {
|
|
198
|
+
if (start >= rootDuration) return;
|
|
199
|
+
dur = Math.min(dur, Math.max(0, rootDuration - start));
|
|
200
|
+
if (dur <= 0) return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const trackStr = el.getAttribute("data-track-index");
|
|
204
|
+
const track = trackStr != null ? parseInt(trackStr, 10) : 0;
|
|
205
|
+
const compSrc =
|
|
206
|
+
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
|
|
207
|
+
const selector = getTimelineElementSelector(el);
|
|
208
|
+
const sourceFile = getTimelineElementSourceFile(el);
|
|
209
|
+
const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
|
|
210
|
+
const label = getTimelineElementDisplayLabel({
|
|
211
|
+
id: el.id || compId || null,
|
|
212
|
+
label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
|
|
213
|
+
tag: el.tagName,
|
|
214
|
+
});
|
|
215
|
+
const identity = buildTimelineElementIdentity({
|
|
216
|
+
preferredId: el.id || compId || null,
|
|
217
|
+
label,
|
|
218
|
+
fallbackIndex: missing.length,
|
|
219
|
+
domId: el.id || undefined,
|
|
220
|
+
selector,
|
|
221
|
+
selectorIndex,
|
|
222
|
+
sourceFile,
|
|
223
|
+
});
|
|
224
|
+
const entry: TimelineElement = {
|
|
225
|
+
id: identity.id,
|
|
226
|
+
label,
|
|
227
|
+
key: identity.key,
|
|
228
|
+
tag: el.tagName.toLowerCase(),
|
|
229
|
+
start,
|
|
230
|
+
duration: dur,
|
|
231
|
+
track: isNaN(track) ? 0 : track,
|
|
232
|
+
domId: el.id || undefined,
|
|
233
|
+
selector,
|
|
234
|
+
selectorIndex,
|
|
235
|
+
sourceFile,
|
|
236
|
+
};
|
|
237
|
+
if (compSrc) {
|
|
238
|
+
entry.compositionSrc = compSrc;
|
|
239
|
+
} else {
|
|
240
|
+
// Inline composition — expose inner video for thumbnails
|
|
241
|
+
const innerVideo = el.querySelector("video[src]");
|
|
242
|
+
if (innerVideo) {
|
|
243
|
+
entry.src = innerVideo.getAttribute("src") || undefined;
|
|
244
|
+
entry.tag = "video";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
missing.push(entry);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Patch existing elements that are missing compositionSrc
|
|
251
|
+
let patched = false;
|
|
252
|
+
const updatedEls = (currentEls as TimelineElement[]).map((existing) => {
|
|
253
|
+
if (existing.compositionSrc) return existing;
|
|
254
|
+
// Find the matching DOM host by element id or composition id
|
|
255
|
+
const host =
|
|
256
|
+
doc.getElementById(existing.id) ??
|
|
257
|
+
doc.querySelector(`[data-composition-id="${existing.id}"]`);
|
|
258
|
+
if (!host) return existing;
|
|
259
|
+
const compSrc =
|
|
260
|
+
host.getAttribute("data-composition-src") || host.getAttribute("data-composition-file");
|
|
261
|
+
if (compSrc) {
|
|
262
|
+
patched = true;
|
|
263
|
+
return { ...existing, compositionSrc: compSrc };
|
|
264
|
+
}
|
|
265
|
+
return existing;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return { missing, updatedEls, patched };
|
|
269
|
+
}
|