@hyperframes/studio 0.8.72 → 0.8.73
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-jTC1EDAI.js → hyperframes-player-BUauoSsb.js} +1 -1
- package/dist/assets/{index-DpVD1igA.js → index-CMFPPAGq.js} +1 -1
- package/dist/assets/{index-HuYqM2Fi.js → index-DKInxDDv.js} +1 -1
- package/dist/assets/{index-BqUGSWqN.js → index-DxIHGgGZ.js} +173 -173
- package/dist/index.d.ts +14 -1
- package/dist/index.html +1 -1
- package/dist/index.js +48 -27
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/hooks/appHotkeysDispatch.ts +9 -3
- package/src/index.ts +2 -0
- package/src/player/components/PlayerControls.tsx +5 -0
- package/src/player/components/ShortcutsPanel.test.tsx +51 -0
- package/src/player/components/ShortcutsPanel.tsx +7 -90
- package/src/player/components/studioShortcuts.ts +101 -0
- package/src/player/components/useTimelinePlayhead.test.tsx +65 -0
- package/src/player/components/useTimelinePlayhead.ts +2 -1
- package/src/player/hooks/useShadowPreviewReload.ts +15 -5
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +43 -6
- package/src/player/lib/timelineDOM.test.ts +86 -0
- package/src/player/lib/timelineDOM.ts +5 -5
- package/src/player/lib/timelineElementHelpers.ts +10 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
mergeTimelineElementsPreservingDowngrades,
|
|
8
8
|
} from "./timelineDOM";
|
|
9
9
|
import { isTimelineIgnoredElement } from "./timelineElementHelpers";
|
|
10
|
+
import { computeResizePreview } from "../components/timelineClipDragPreview";
|
|
10
11
|
import { invalidateGroupInfoCache } from "./timelineGroupInfo";
|
|
11
12
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
13
|
|
|
@@ -392,3 +393,88 @@ describe("audio FX attributes on parsed elements", () => {
|
|
|
392
393
|
expect(bgm?.automation).toBeUndefined();
|
|
393
394
|
});
|
|
394
395
|
});
|
|
396
|
+
|
|
397
|
+
// A composition clip trimmed by 30 px collapsed to its first nested video's 1.77 s: that length capped the trim.
|
|
398
|
+
describe("a composition clip's source length", () => {
|
|
399
|
+
const scene = `
|
|
400
|
+
<div data-composition-id="root">
|
|
401
|
+
<div id="host" data-composition-id="scene" data-composition-src="scene.html"
|
|
402
|
+
data-start="0" data-duration="18" data-track-index="1">
|
|
403
|
+
<div data-composition-id="scene">
|
|
404
|
+
<video id="bg" src="bg.mp4" data-source-duration="1.77" data-start="2" data-duration="1.75"></video>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
<div id="wrapper" class="clip" data-start="0" data-duration="3" data-track-index="2">
|
|
408
|
+
<video src="talk.mp4" data-source-duration="4"></video>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
`;
|
|
412
|
+
|
|
413
|
+
it("is not taken from media inside the composition, parsed from the DOM", () => {
|
|
414
|
+
const parsed = parseTimelineFromDOM(makeDoc(scene), 18);
|
|
415
|
+
const host = parsed.find((entry) => entry.domId === "host");
|
|
416
|
+
expect(host?.sourceDuration).toBeUndefined();
|
|
417
|
+
expect(host?.tag).not.toBe("video");
|
|
418
|
+
expect(parsed.find((entry) => entry.domId === "wrapper")?.sourceDuration).toBe(4);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("does not cap trimming the composition's end at that media's length", () => {
|
|
422
|
+
const host = parseTimelineFromDOM(makeDoc(scene), 18).find((entry) => entry.domId === "host");
|
|
423
|
+
const trimmed = computeResizePreview(
|
|
424
|
+
{
|
|
425
|
+
element: host!,
|
|
426
|
+
edge: "end",
|
|
427
|
+
originClientX: 360,
|
|
428
|
+
previewStart: 0,
|
|
429
|
+
previewDuration: 18,
|
|
430
|
+
started: true,
|
|
431
|
+
},
|
|
432
|
+
330,
|
|
433
|
+
{ scroll: null, pps: 20, buildSnapTargets: () => [] },
|
|
434
|
+
);
|
|
435
|
+
expect(trimmed.previewDuration).toBeCloseTo(16.5, 3);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("is not taken from media inside the composition, from the runtime manifest", () => {
|
|
439
|
+
const doc = makeDoc(scene);
|
|
440
|
+
const element = createTimelineElementFromManifestClip({
|
|
441
|
+
clip: {
|
|
442
|
+
id: "host",
|
|
443
|
+
label: "Scene",
|
|
444
|
+
kind: "composition",
|
|
445
|
+
tagName: "div",
|
|
446
|
+
start: 0,
|
|
447
|
+
duration: 18,
|
|
448
|
+
track: 1,
|
|
449
|
+
compositionId: "scene",
|
|
450
|
+
parentCompositionId: "root",
|
|
451
|
+
compositionSrc: "scene.html",
|
|
452
|
+
playbackStart: null,
|
|
453
|
+
playbackRate: null,
|
|
454
|
+
assetUrl: null,
|
|
455
|
+
},
|
|
456
|
+
fallbackIndex: 0,
|
|
457
|
+
doc,
|
|
458
|
+
hostEl: doc.getElementById("host"),
|
|
459
|
+
});
|
|
460
|
+
expect(element.sourceDuration).toBeUndefined();
|
|
461
|
+
expect(element.src).toBeUndefined();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("still shows an inline composition's image as its thumbnail", () => {
|
|
465
|
+
const parsed = parseTimelineFromDOM(
|
|
466
|
+
makeDoc(`
|
|
467
|
+
<div data-composition-id="root">
|
|
468
|
+
<div id="card" data-composition-id="card" data-start="0" data-duration="5" data-track-index="1">
|
|
469
|
+
<img src="card.png" />
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
`),
|
|
473
|
+
5,
|
|
474
|
+
);
|
|
475
|
+
const card = parsed.find((entry) => entry.domId === "card");
|
|
476
|
+
expect(card?.tag).toBe("img");
|
|
477
|
+
expect(card?.src).toBe("card.png");
|
|
478
|
+
expect(card?.sourceDuration).toBeUndefined();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
@@ -346,11 +346,11 @@ export function parseTimelineFromDOM(
|
|
|
346
346
|
if (compSrc) {
|
|
347
347
|
entry.compositionSrc = compSrc;
|
|
348
348
|
} else if (compId && compId !== rootComp?.getAttribute("data-composition-id")) {
|
|
349
|
-
// Inline composition — expose inner video for thumbnails
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
entry.src =
|
|
353
|
-
entry.tag = "video";
|
|
349
|
+
// Inline composition — expose inner video or image for thumbnails
|
|
350
|
+
const innerMedia = el.querySelector("video[src], img[src]");
|
|
351
|
+
if (innerMedia) {
|
|
352
|
+
entry.src = innerMedia.getAttribute("src") || undefined;
|
|
353
|
+
entry.tag = innerMedia.tagName === "IMG" ? "img" : "video";
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
if (entry.kind === "composition") {
|
|
@@ -146,11 +146,21 @@ function isHtmlElement(el: Element): el is HTMLElement {
|
|
|
146
146
|
return typeof HtmlElementCtor !== "undefined" && el instanceof HtmlElementCtor;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function isCompositionHost(el: Element): boolean {
|
|
150
|
+
return (
|
|
151
|
+
el.hasAttribute("data-composition-id") ||
|
|
152
|
+
el.hasAttribute("data-composition-src") ||
|
|
153
|
+
el.hasAttribute("data-composition-file")
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
export function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageElement | null {
|
|
150
158
|
const win = el.ownerDocument.defaultView ?? window;
|
|
151
159
|
const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
|
|
152
160
|
const ImageElementCtor = win.HTMLImageElement ?? globalThis.HTMLImageElement;
|
|
153
161
|
if (el instanceof MediaElementCtor || el instanceof ImageElementCtor) return el;
|
|
162
|
+
// A composition's media belongs to its own timeline, not the clip's: its length would cap a trim.
|
|
163
|
+
if (isCompositionHost(el)) return null;
|
|
154
164
|
const candidate = el.querySelector("video, audio, img");
|
|
155
165
|
return candidate instanceof MediaElementCtor || candidate instanceof ImageElementCtor
|
|
156
166
|
? candidate
|