@hyperframes/studio 0.8.5 → 0.8.6
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-DcM3fVcp.js → hyperframes-player-DdNgW-Np.js} +1 -1
- package/dist/assets/{index-Del6XWZj.js → index-Ci7Uy8fR.js} +1 -1
- package/dist/assets/{index-Cx_UrhoW.js → index-Cy6fGN1u.js} +4 -4
- package/dist/assets/{index-DgJrvYGf.js → index-bv8b5eiQ.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/player/hooks/useExpandedTimelineElements.test.ts +64 -17
- package/src/player/hooks/useExpandedTimelineElements.ts +8 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"gsap": "^3.13.0",
|
|
48
48
|
"marked": "^14.1.4",
|
|
49
49
|
"mediabunny": "^1.45.3",
|
|
50
|
-
"@hyperframes/core": "0.8.
|
|
51
|
-
"@hyperframes/parsers": "0.8.
|
|
52
|
-
"@hyperframes/player": "0.8.
|
|
53
|
-
"@hyperframes/sdk": "0.8.
|
|
54
|
-
"@hyperframes/studio-server": "0.8.
|
|
50
|
+
"@hyperframes/core": "0.8.6",
|
|
51
|
+
"@hyperframes/parsers": "0.8.6",
|
|
52
|
+
"@hyperframes/player": "0.8.6",
|
|
53
|
+
"@hyperframes/sdk": "0.8.6",
|
|
54
|
+
"@hyperframes/studio-server": "0.8.6"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "19",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"vite": "^6.4.2",
|
|
69
69
|
"vitest": "^3.2.4",
|
|
70
70
|
"zustand": "^5.0.0",
|
|
71
|
-
"@hyperframes/producer": "0.8.
|
|
71
|
+
"@hyperframes/producer": "0.8.6"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"react": "19",
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act, useEffect } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
6
|
import {
|
|
3
7
|
buildExpandedElements,
|
|
4
8
|
resolveTimelineExpansionRawId,
|
|
9
|
+
useExpandedTimelineElements,
|
|
5
10
|
} from "./useExpandedTimelineElements";
|
|
6
11
|
import { buildTimelineElementKey } from "../lib/timelineElementHelpers";
|
|
7
|
-
import type
|
|
12
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
8
13
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
9
14
|
|
|
15
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
|
+
|
|
10
17
|
const clip = (over: Partial<ClipManifestClip>): ClipManifestClip => ({
|
|
11
18
|
id: "x",
|
|
12
19
|
label: "x",
|
|
@@ -31,6 +38,19 @@ const el = (over: Partial<TimelineElement>): TimelineElement => ({
|
|
|
31
38
|
...over,
|
|
32
39
|
});
|
|
33
40
|
|
|
41
|
+
function TimelineExpansionHarness({ onValue }: { onValue: (value: TimelineElement[]) => void }) {
|
|
42
|
+
const value = useExpandedTimelineElements();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
onValue(value);
|
|
45
|
+
}, [onValue, value]);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
document.body.innerHTML = "";
|
|
51
|
+
usePlayerStore.getState().reset();
|
|
52
|
+
});
|
|
53
|
+
|
|
34
54
|
describe("buildExpandedElements", () => {
|
|
35
55
|
it("rebases a 1-level child onto its sub-comp host (start + sourceFile)", () => {
|
|
36
56
|
// host s3 at absolute 16 → stats-panel.html; children live in that file.
|
|
@@ -357,7 +377,7 @@ describe("buildExpandedElements", () => {
|
|
|
357
377
|
it("keeps the host row present at every playhead position (keyframe lane repro)", () => {
|
|
358
378
|
// Live repro with no drag at all: seek 0 gave 3 diamonds, seek 7.68 gave 0,
|
|
359
379
|
// seek 0.2 gave 3. Diamonds render per row from keyframeCache.get(elementKey),
|
|
360
|
-
// so the whole lane went with the host row whenever the
|
|
380
|
+
// so the whole lane went with the host row whenever the store-time drill-in
|
|
361
381
|
// substituted it for its children.
|
|
362
382
|
const elements = [
|
|
363
383
|
el({ id: "scene", domId: "scene", key: "index.html#scene", start: 0, duration: 12 }),
|
|
@@ -371,7 +391,6 @@ describe("buildExpandedElements", () => {
|
|
|
371
391
|
for (const currentTime of [0, 7.68, 0.2]) {
|
|
372
392
|
const rawId = resolveTimelineExpansionRawId({
|
|
373
393
|
selectedElementId: null,
|
|
374
|
-
isPlaying: false,
|
|
375
394
|
currentTime,
|
|
376
395
|
manifest,
|
|
377
396
|
parentMap,
|
|
@@ -424,13 +443,12 @@ describe("buildExpandedElements", () => {
|
|
|
424
443
|
});
|
|
425
444
|
|
|
426
445
|
describe("resolveTimelineExpansionRawId", () => {
|
|
427
|
-
it("returns null
|
|
446
|
+
it("returns null inside a childless top-level clip", () => {
|
|
428
447
|
const manifest = [clip({ id: "title", start: 0, duration: 4 })];
|
|
429
448
|
|
|
430
449
|
expect(
|
|
431
450
|
resolveTimelineExpansionRawId({
|
|
432
451
|
selectedElementId: null,
|
|
433
|
-
isPlaying: false,
|
|
434
452
|
currentTime: 2,
|
|
435
453
|
manifest,
|
|
436
454
|
parentMap: new Map(),
|
|
@@ -438,7 +456,7 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
438
456
|
).toBeNull();
|
|
439
457
|
});
|
|
440
458
|
|
|
441
|
-
it("auto-expands an active composition with children when
|
|
459
|
+
it("auto-expands an active composition with children when nothing is selected", () => {
|
|
442
460
|
const manifest = [
|
|
443
461
|
clip({ id: "scene", start: 1, duration: 5 }),
|
|
444
462
|
clip({ id: "headline", start: 1.5, duration: 2 }),
|
|
@@ -448,7 +466,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
448
466
|
expect(
|
|
449
467
|
resolveTimelineExpansionRawId({
|
|
450
468
|
selectedElementId: null,
|
|
451
|
-
isPlaying: false,
|
|
452
469
|
currentTime: 2,
|
|
453
470
|
manifest,
|
|
454
471
|
parentMap,
|
|
@@ -468,7 +485,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
468
485
|
expect(
|
|
469
486
|
resolveTimelineExpansionRawId({
|
|
470
487
|
selectedElementId: null,
|
|
471
|
-
isPlaying: false,
|
|
472
488
|
currentTime: 12,
|
|
473
489
|
manifest,
|
|
474
490
|
parentMap,
|
|
@@ -491,7 +507,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
491
507
|
expect(
|
|
492
508
|
resolveTimelineExpansionRawId({
|
|
493
509
|
selectedElementId: null,
|
|
494
|
-
isPlaying: false,
|
|
495
510
|
currentTime: 5,
|
|
496
511
|
manifest,
|
|
497
512
|
parentMap,
|
|
@@ -499,7 +514,7 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
499
514
|
).toBe("second");
|
|
500
515
|
});
|
|
501
516
|
|
|
502
|
-
it("auto-expands the innermost active nested composition
|
|
517
|
+
it("auto-expands the innermost active nested composition", () => {
|
|
503
518
|
const manifest = [
|
|
504
519
|
clip({ id: "outer", start: 0, duration: 10 }),
|
|
505
520
|
clip({ id: "inner", start: 2, duration: 5 }),
|
|
@@ -513,7 +528,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
513
528
|
expect(
|
|
514
529
|
resolveTimelineExpansionRawId({
|
|
515
530
|
selectedElementId: null,
|
|
516
|
-
isPlaying: false,
|
|
517
531
|
currentTime: 3.5,
|
|
518
532
|
manifest,
|
|
519
533
|
parentMap,
|
|
@@ -521,7 +535,7 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
521
535
|
).toBe("inner");
|
|
522
536
|
});
|
|
523
537
|
|
|
524
|
-
it("
|
|
538
|
+
it("resolves an active composition from the current store time", () => {
|
|
525
539
|
const manifest = [
|
|
526
540
|
clip({ id: "scene", start: 0, duration: 5 }),
|
|
527
541
|
clip({ id: "headline", start: 1, duration: 2 }),
|
|
@@ -531,15 +545,49 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
531
545
|
expect(
|
|
532
546
|
resolveTimelineExpansionRawId({
|
|
533
547
|
selectedElementId: null,
|
|
534
|
-
isPlaying: true,
|
|
535
548
|
currentTime: 2,
|
|
536
549
|
manifest,
|
|
537
550
|
parentMap,
|
|
538
551
|
}),
|
|
539
|
-
).
|
|
552
|
+
).toBe("scene");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("keeps inline children visible when the master timeline is playing", () => {
|
|
556
|
+
const elements = [
|
|
557
|
+
el({ id: "scene", domId: "scene", key: "index.html#scene", start: 0, duration: 5 }),
|
|
558
|
+
];
|
|
559
|
+
const manifest = [
|
|
560
|
+
clip({ id: "scene", start: 0, duration: 5, compositionSrc: "scene.html" }),
|
|
561
|
+
clip({ id: "headline", start: 1, duration: 2 }),
|
|
562
|
+
];
|
|
563
|
+
const parentMap = new Map([["headline", "scene"]]);
|
|
564
|
+
let rows: TimelineElement[] | undefined;
|
|
565
|
+
const host = document.createElement("div");
|
|
566
|
+
document.body.append(host);
|
|
567
|
+
const root = createRoot(host);
|
|
568
|
+
|
|
569
|
+
act(() => {
|
|
570
|
+
usePlayerStore.setState({
|
|
571
|
+
elements,
|
|
572
|
+
clipManifest: manifest,
|
|
573
|
+
clipParentMap: parentMap,
|
|
574
|
+
currentTime: 2,
|
|
575
|
+
isPlaying: true,
|
|
576
|
+
});
|
|
577
|
+
root.render(
|
|
578
|
+
React.createElement(TimelineExpansionHarness, {
|
|
579
|
+
onValue: (value) => (rows = value),
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(usePlayerStore.getState().isPlaying).toBe(true);
|
|
585
|
+
expect(rows?.map((row) => row.domId ?? row.id)).toEqual(["scene", "headline"]);
|
|
586
|
+
|
|
587
|
+
act(() => root.unmount());
|
|
540
588
|
});
|
|
541
589
|
|
|
542
|
-
it("keeps selected elements ahead of
|
|
590
|
+
it("keeps selected elements ahead of active composition auto-expansion", () => {
|
|
543
591
|
const manifest = [
|
|
544
592
|
clip({ id: "scene", start: 0, duration: 6 }),
|
|
545
593
|
clip({ id: "headline", start: 1, duration: 2 }),
|
|
@@ -553,7 +601,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
553
601
|
expect(
|
|
554
602
|
resolveTimelineExpansionRawId({
|
|
555
603
|
selectedElementId: "caption",
|
|
556
|
-
isPlaying: false,
|
|
557
604
|
currentTime: 1.5,
|
|
558
605
|
manifest,
|
|
559
606
|
parentMap,
|
|
@@ -35,7 +35,6 @@ function resolveRawId(
|
|
|
35
35
|
|
|
36
36
|
interface TimelineExpansionRawIdInput {
|
|
37
37
|
selectedElementId: string | null;
|
|
38
|
-
isPlaying: boolean;
|
|
39
38
|
currentTime: number;
|
|
40
39
|
manifest: ClipManifestClip[];
|
|
41
40
|
parentMap: Map<string, string>;
|
|
@@ -97,14 +96,12 @@ function findActiveExpandableCompositionId(
|
|
|
97
96
|
|
|
98
97
|
export function resolveTimelineExpansionRawId({
|
|
99
98
|
selectedElementId,
|
|
100
|
-
isPlaying,
|
|
101
99
|
currentTime,
|
|
102
100
|
manifest,
|
|
103
101
|
parentMap,
|
|
104
102
|
}: TimelineExpansionRawIdInput): string | null {
|
|
105
103
|
const selectedRawId = resolveRawId(selectedElementId, manifest, parentMap);
|
|
106
104
|
if (selectedRawId) return selectedRawId;
|
|
107
|
-
if (isPlaying) return null;
|
|
108
105
|
return findActiveExpandableCompositionId(currentTime, manifest, parentMap);
|
|
109
106
|
}
|
|
110
107
|
|
|
@@ -334,7 +331,7 @@ export function buildExpandedElements(
|
|
|
334
331
|
: (el.key ?? el.id) === parentKey;
|
|
335
332
|
|
|
336
333
|
// ADDITIVE drill-in: the host row stays and its children are appended under
|
|
337
|
-
// it. Expansion is also triggered by the playhead alone (
|
|
334
|
+
// it. Expansion is also triggered by the playhead alone (active auto-expand),
|
|
338
335
|
// so substituting the host row made it vanish on an ordinary seek, and with
|
|
339
336
|
// it the host's keyframe lane, since diamonds render per row from
|
|
340
337
|
// `keyframeCache.get(elementKey)`. The synthetic fractional lanes above sit
|
|
@@ -356,13 +353,14 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
356
353
|
const clipParentMap = usePlayerStore((s) => s.clipParentMap);
|
|
357
354
|
const domClipChildren = usePlayerStore((s) => s.domClipChildren);
|
|
358
355
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
359
|
-
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
360
356
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
361
357
|
|
|
362
|
-
// Resolve which raw clip drives expansion
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// the
|
|
358
|
+
// Resolve which raw clip drives expansion from the store's committed playhead
|
|
359
|
+
// time. The RAF loop keeps live time out of React and Zustand during playback,
|
|
360
|
+
// so this target deliberately stays stable until the loop commits or a seek /
|
|
361
|
+
// pause updates the store. The scan re-runs on scrub ticks, but its RESULT only
|
|
362
|
+
// changes when the playhead crosses a composition boundary. Keying the expensive
|
|
363
|
+
// build below on these ids (not raw currentTime) avoids re-allocating
|
|
366
364
|
// expandedElements — and cascading TimelineClip re-renders — on every tick.
|
|
367
365
|
const { rawId, selectedRawId } = useMemo(() => {
|
|
368
366
|
if (!clipManifest || clipManifest.length === 0 || clipParentMap.size === 0) {
|
|
@@ -371,14 +369,13 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
371
369
|
return {
|
|
372
370
|
rawId: resolveTimelineExpansionRawId({
|
|
373
371
|
selectedElementId,
|
|
374
|
-
isPlaying,
|
|
375
372
|
currentTime,
|
|
376
373
|
manifest: clipManifest,
|
|
377
374
|
parentMap: clipParentMap,
|
|
378
375
|
}),
|
|
379
376
|
selectedRawId: resolveRawId(selectedElementId, clipManifest, clipParentMap),
|
|
380
377
|
};
|
|
381
|
-
}, [clipManifest, clipParentMap, selectedElementId,
|
|
378
|
+
}, [clipManifest, clipParentMap, selectedElementId, currentTime]);
|
|
382
379
|
|
|
383
380
|
return useMemo(() => {
|
|
384
381
|
if (!clipManifest || clipManifest.length === 0 || clipParentMap.size === 0) {
|