@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.
@@ -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 innerVideo = el.querySelector("video[src]");
351
- if (innerVideo) {
352
- entry.src = innerVideo.getAttribute("src") || undefined;
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