@hyperframes/studio 0.4.38 → 0.4.39
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/{index-18P_dZeo.js → index-D4-n3yWG.js} +26 -26
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/App.tsx +9 -5
- package/src/player/components/Timeline.tsx +5 -2
- package/src/player/components/TimelineClip.tsx +2 -1
- package/src/player/components/timelineTheme.test.ts +19 -0
- package/src/player/components/timelineTheme.ts +5 -1
- package/src/player/hooks/useTimelinePlayer.test.ts +198 -0
- package/src/player/hooks/useTimelinePlayer.ts +261 -106
- package/src/player/store/playerStore.ts +1 -0
package/dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<title>HyperFrames Studio</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-D4-n3yWG.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-BLrgRQSu.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.39",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"codemirror": "^6.0.1",
|
|
34
34
|
"motion": "^12.38.0",
|
|
35
|
-
"@hyperframes/core": "0.4.
|
|
36
|
-
"@hyperframes/player": "0.4.
|
|
35
|
+
"@hyperframes/core": "0.4.39",
|
|
36
|
+
"@hyperframes/player": "0.4.39"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"vite": "^6.4.2",
|
|
48
48
|
"vitest": "^3.2.4",
|
|
49
49
|
"zustand": "^5.0.0",
|
|
50
|
-
"@hyperframes/producer": "0.4.
|
|
50
|
+
"@hyperframes/producer": "0.4.39"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -61,6 +61,10 @@ interface AppToast {
|
|
|
61
61
|
tone: "error" | "info";
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function getTimelineElementLabel(element: TimelineElement): string {
|
|
65
|
+
return element.label || element.id || element.tag;
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
const DEFAULT_TIMELINE_ASSET_DURATION: Record<TimelineAssetKind, number> = {
|
|
65
69
|
image: 3,
|
|
66
70
|
video: 5,
|
|
@@ -392,7 +396,7 @@ export function StudioApp() {
|
|
|
392
396
|
return (
|
|
393
397
|
<CompositionThumbnail
|
|
394
398
|
previewUrl={`/api/projects/${pid}/preview/comp/${compSrc}`}
|
|
395
|
-
label={el
|
|
399
|
+
label={getTimelineElementLabel(el)}
|
|
396
400
|
labelColor={style.label}
|
|
397
401
|
accentColor={style.clip}
|
|
398
402
|
selector={el.selector}
|
|
@@ -408,7 +412,7 @@ export function StudioApp() {
|
|
|
408
412
|
return (
|
|
409
413
|
<CompositionThumbnail
|
|
410
414
|
previewUrl={activePreviewUrl}
|
|
411
|
-
label={el
|
|
415
|
+
label={getTimelineElementLabel(el)}
|
|
412
416
|
labelColor={style.label}
|
|
413
417
|
accentColor={style.clip}
|
|
414
418
|
selector={el.selector}
|
|
@@ -445,7 +449,7 @@ export function StudioApp() {
|
|
|
445
449
|
<AudioWaveform
|
|
446
450
|
audioUrl={audioUrl}
|
|
447
451
|
waveformUrl={waveformUrl}
|
|
448
|
-
label={el
|
|
452
|
+
label={getTimelineElementLabel(el)}
|
|
449
453
|
labelColor={style.label}
|
|
450
454
|
/>
|
|
451
455
|
);
|
|
@@ -458,7 +462,7 @@ export function StudioApp() {
|
|
|
458
462
|
return (
|
|
459
463
|
<VideoThumbnail
|
|
460
464
|
videoSrc={mediaSrc}
|
|
461
|
-
label={el
|
|
465
|
+
label={getTimelineElementLabel(el)}
|
|
462
466
|
labelColor={style.label}
|
|
463
467
|
duration={el.duration}
|
|
464
468
|
/>
|
|
@@ -469,7 +473,7 @@ export function StudioApp() {
|
|
|
469
473
|
return (
|
|
470
474
|
<CompositionThumbnail
|
|
471
475
|
previewUrl={`/api/projects/${pid}/preview`}
|
|
472
|
-
label={el
|
|
476
|
+
label={getTimelineElementLabel(el)}
|
|
473
477
|
labelColor={style.label}
|
|
474
478
|
accentColor={style.clip}
|
|
475
479
|
selector={el.selector}
|
|
@@ -1014,7 +1014,10 @@ export const Timeline = memo(function Timeline({
|
|
|
1014
1014
|
major.length >= 2 ? Math.max(0.25, major[1] - major[0]) : effectiveDuration;
|
|
1015
1015
|
const getPreviewElement = useCallback(
|
|
1016
1016
|
(element: TimelineElement): TimelineElement => {
|
|
1017
|
-
if (
|
|
1017
|
+
if (
|
|
1018
|
+
resizingClip &&
|
|
1019
|
+
(resizingClip.element.key ?? resizingClip.element.id) === (element.key ?? element.id)
|
|
1020
|
+
) {
|
|
1018
1021
|
return {
|
|
1019
1022
|
...element,
|
|
1020
1023
|
start: resizingClip.previewStart,
|
|
@@ -1242,7 +1245,7 @@ export const Timeline = memo(function Timeline({
|
|
|
1242
1245
|
draggedClip?.started === true && draggedElement
|
|
1243
1246
|
? getRenderedTimelineElement({
|
|
1244
1247
|
element: draggedElement,
|
|
1245
|
-
draggedElementId: draggedElement.id,
|
|
1248
|
+
draggedElementId: draggedElement.key ?? draggedElement.id,
|
|
1246
1249
|
previewStart: draggedClip.previewStart,
|
|
1247
1250
|
previewTrack: draggedClip.previewTrack,
|
|
1248
1251
|
})
|
|
@@ -61,6 +61,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
61
61
|
? theme.clipShadowHover
|
|
62
62
|
: theme.clipShadow;
|
|
63
63
|
const capabilities = getTimelineEditCapabilities(el);
|
|
64
|
+
const displayLabel = el.label || el.id || el.tag;
|
|
64
65
|
const showHandles = handleOpacity > 0.01;
|
|
65
66
|
|
|
66
67
|
return (
|
|
@@ -93,7 +94,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
93
94
|
title={
|
|
94
95
|
isComposition
|
|
95
96
|
? `${el.compositionSrc} \u2022 Double-click to open`
|
|
96
|
-
: `${
|
|
97
|
+
: `${displayLabel} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
|
|
97
98
|
}
|
|
98
99
|
onPointerEnter={onHoverStart}
|
|
99
100
|
onPointerLeave={onHoverEnd}
|
|
@@ -53,4 +53,23 @@ describe("getRenderedTimelineElement", () => {
|
|
|
53
53
|
}),
|
|
54
54
|
).toEqual({ ...element, start: 2.4, track: 3 });
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it("uses key before id when matching the dragged clip", () => {
|
|
58
|
+
const element = {
|
|
59
|
+
id: "Card",
|
|
60
|
+
key: "index.html:.card:1",
|
|
61
|
+
tag: "div",
|
|
62
|
+
start: 1,
|
|
63
|
+
duration: 2,
|
|
64
|
+
track: 0,
|
|
65
|
+
};
|
|
66
|
+
expect(
|
|
67
|
+
getRenderedTimelineElement({
|
|
68
|
+
element,
|
|
69
|
+
draggedElementId: "index.html:.card:1",
|
|
70
|
+
previewStart: 2.4,
|
|
71
|
+
previewTrack: 3,
|
|
72
|
+
}),
|
|
73
|
+
).toEqual({ ...element, start: 2.4, track: 3 });
|
|
74
|
+
});
|
|
56
75
|
});
|
|
@@ -130,7 +130,11 @@ export function getRenderedTimelineElement({
|
|
|
130
130
|
previewStart: number | null;
|
|
131
131
|
previewTrack: number | null;
|
|
132
132
|
}): TimelineElement {
|
|
133
|
-
if (
|
|
133
|
+
if (
|
|
134
|
+
(element.key ?? element.id) !== draggedElementId ||
|
|
135
|
+
previewStart === null ||
|
|
136
|
+
previewTrack === null
|
|
137
|
+
) {
|
|
134
138
|
return element;
|
|
135
139
|
}
|
|
136
140
|
return {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Window } from "happy-dom";
|
|
2
3
|
import {
|
|
3
4
|
buildStandaloneRootTimelineElement,
|
|
5
|
+
createTimelineElementFromManifestClip,
|
|
6
|
+
findTimelineDomNodeForClip,
|
|
7
|
+
getTimelineElementSelector,
|
|
8
|
+
parseTimelineFromDOM,
|
|
9
|
+
type ClipManifestClip,
|
|
4
10
|
mergeTimelineElementsPreservingDowngrades,
|
|
5
11
|
resolveStandaloneRootCompositionSrc,
|
|
6
12
|
shouldIgnorePlaybackShortcutEvent,
|
|
@@ -27,6 +33,29 @@ function mockKeyboardEvent(
|
|
|
27
33
|
};
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
function createDocument(markup: string): Document {
|
|
37
|
+
const window = new Window();
|
|
38
|
+
window.document.body.innerHTML = markup;
|
|
39
|
+
return window.document;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createClip(overrides: Partial<ClipManifestClip>): ClipManifestClip {
|
|
43
|
+
return {
|
|
44
|
+
id: null,
|
|
45
|
+
label: "Element",
|
|
46
|
+
start: 0,
|
|
47
|
+
duration: 4,
|
|
48
|
+
track: 0,
|
|
49
|
+
kind: "element",
|
|
50
|
+
tagName: "div",
|
|
51
|
+
compositionId: null,
|
|
52
|
+
parentCompositionId: null,
|
|
53
|
+
compositionSrc: null,
|
|
54
|
+
assetUrl: null,
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
describe("buildStandaloneRootTimelineElement", () => {
|
|
31
60
|
it("includes selector and source metadata for standalone composition fallback clips", () => {
|
|
32
61
|
expect(
|
|
@@ -39,6 +68,7 @@ describe("buildStandaloneRootTimelineElement", () => {
|
|
|
39
68
|
}),
|
|
40
69
|
).toEqual({
|
|
41
70
|
id: "hero",
|
|
71
|
+
label: "hero",
|
|
42
72
|
key: 'scenes/hero.html:[data-composition-id="hero"]:0',
|
|
43
73
|
tag: "div",
|
|
44
74
|
start: 0,
|
|
@@ -87,6 +117,115 @@ describe("resolveStandaloneRootCompositionSrc", () => {
|
|
|
87
117
|
});
|
|
88
118
|
});
|
|
89
119
|
|
|
120
|
+
describe("findTimelineDomNodeForClip", () => {
|
|
121
|
+
it("matches anonymous manifest clips back to repeated DOM nodes in timeline order", () => {
|
|
122
|
+
const doc = createDocument(`
|
|
123
|
+
<div data-composition-id="main" data-start="0" data-duration="8">
|
|
124
|
+
<section id="identity-card" class="clip identity-card" data-start="0" data-duration="4" data-track-index="0"></section>
|
|
125
|
+
<div class="clip duplicate-card first" data-start="0" data-duration="4" data-track-index="1"></div>
|
|
126
|
+
<div class="clip duplicate-card second" data-start="0" data-duration="4" data-track-index="2"></div>
|
|
127
|
+
</div>
|
|
128
|
+
`);
|
|
129
|
+
const used = new Set<Element>();
|
|
130
|
+
|
|
131
|
+
const first = findTimelineDomNodeForClip(
|
|
132
|
+
doc,
|
|
133
|
+
createClip({ id: "__node__index_2", track: 1 }),
|
|
134
|
+
1,
|
|
135
|
+
used,
|
|
136
|
+
) as HTMLElement;
|
|
137
|
+
used.add(first);
|
|
138
|
+
const second = findTimelineDomNodeForClip(
|
|
139
|
+
doc,
|
|
140
|
+
createClip({ id: "__node__index_3", track: 2 }),
|
|
141
|
+
2,
|
|
142
|
+
used,
|
|
143
|
+
) as HTMLElement;
|
|
144
|
+
|
|
145
|
+
expect(first.className).toBe("clip duplicate-card first");
|
|
146
|
+
expect(second.className).toBe("clip duplicate-card second");
|
|
147
|
+
expect(getTimelineElementSelector(first)).toBe(".duplicate-card");
|
|
148
|
+
expect(getTimelineElementSelector(second)).toBe(".duplicate-card");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("anonymous timeline identity", () => {
|
|
153
|
+
it("keeps fallback-parsed anonymous clips distinct when labels match", () => {
|
|
154
|
+
const doc = createDocument(`
|
|
155
|
+
<div data-composition-id="main" data-start="0" data-duration="8">
|
|
156
|
+
<div class="clip card" data-label="Card" data-start="0" data-duration="3" data-track-index="0"></div>
|
|
157
|
+
<div class="clip card" data-label="Card" data-start="3" data-duration="3" data-track-index="1"></div>
|
|
158
|
+
</div>
|
|
159
|
+
`);
|
|
160
|
+
|
|
161
|
+
const elements = parseTimelineFromDOM(doc, 8);
|
|
162
|
+
|
|
163
|
+
expect(elements).toHaveLength(2);
|
|
164
|
+
expect(elements.map((element) => element.label)).toEqual(["Card", "Card"]);
|
|
165
|
+
expect(new Set(elements.map((element) => element.id)).size).toBe(2);
|
|
166
|
+
expect(new Set(elements.map((element) => element.key)).size).toBe(2);
|
|
167
|
+
expect(elements.map((element) => element.selectorIndex)).toEqual([0, 1]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("keeps runtime-manifest anonymous clips distinct when labels match", () => {
|
|
171
|
+
const doc = createDocument(`
|
|
172
|
+
<div data-composition-id="main" data-start="0" data-duration="8">
|
|
173
|
+
<div class="clip card" data-start="0" data-duration="3" data-track-index="0"></div>
|
|
174
|
+
<div class="clip card" data-start="3" data-duration="3" data-track-index="1"></div>
|
|
175
|
+
</div>
|
|
176
|
+
`);
|
|
177
|
+
const clips = [
|
|
178
|
+
createClip({ id: null, label: "Card", start: 0, duration: 3, track: 0 }),
|
|
179
|
+
createClip({ id: null, label: "Card", start: 3, duration: 3, track: 1 }),
|
|
180
|
+
];
|
|
181
|
+
const used = new Set<Element>();
|
|
182
|
+
const elements = clips.map((clip, index) => {
|
|
183
|
+
const hostEl = findTimelineDomNodeForClip(doc, clip, index, used);
|
|
184
|
+
if (hostEl) used.add(hostEl);
|
|
185
|
+
return createTimelineElementFromManifestClip({
|
|
186
|
+
clip,
|
|
187
|
+
fallbackIndex: index,
|
|
188
|
+
doc,
|
|
189
|
+
hostEl,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(elements.map((element) => element.label)).toEqual(["Card", "Card"]);
|
|
194
|
+
expect(new Set(elements.map((element) => element.id)).size).toBe(2);
|
|
195
|
+
expect(new Set(elements.map((element) => element.key)).size).toBe(2);
|
|
196
|
+
expect(elements.map((element) => element.selectorIndex)).toEqual([0, 1]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("reads media metadata from owner-window media elements", () => {
|
|
200
|
+
const doc = createDocument(`
|
|
201
|
+
<div data-composition-id="main" data-start="0" data-duration="8">
|
|
202
|
+
<div class="clip video-card" data-start="0" data-duration="3" data-track-index="0">
|
|
203
|
+
<video src="/clip.mp4" data-source-duration="12"></video>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
`);
|
|
207
|
+
const hostEl = doc.querySelector(".video-card");
|
|
208
|
+
const video = hostEl?.querySelector("video");
|
|
209
|
+
if (!hostEl || !video) throw new Error("missing video test fixture");
|
|
210
|
+
Object.defineProperty(video, "defaultPlaybackRate", {
|
|
211
|
+
value: 1.5,
|
|
212
|
+
configurable: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const element = createTimelineElementFromManifestClip({
|
|
216
|
+
clip: createClip({ kind: "video", tagName: "div" }),
|
|
217
|
+
fallbackIndex: 0,
|
|
218
|
+
doc,
|
|
219
|
+
hostEl,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(element.tag).toBe("video");
|
|
223
|
+
expect(element.src).toBe("/clip.mp4");
|
|
224
|
+
expect(element.sourceDuration).toBe(12);
|
|
225
|
+
expect(element.playbackRate).toBe(1.5);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
90
229
|
describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
91
230
|
it("preserves missing current elements when a shorter manifest arrives", () => {
|
|
92
231
|
expect(
|
|
@@ -115,6 +254,65 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
115
254
|
),
|
|
116
255
|
).toEqual([{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }]);
|
|
117
256
|
});
|
|
257
|
+
|
|
258
|
+
it("preserves distinct anonymous clips that share the same friendly id label", () => {
|
|
259
|
+
expect(
|
|
260
|
+
mergeTimelineElementsPreservingDowngrades(
|
|
261
|
+
[
|
|
262
|
+
{
|
|
263
|
+
id: "Card",
|
|
264
|
+
key: "index.html:.card:0",
|
|
265
|
+
label: "Card",
|
|
266
|
+
tag: "div",
|
|
267
|
+
start: 0,
|
|
268
|
+
duration: 3,
|
|
269
|
+
track: 0,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "Card",
|
|
273
|
+
key: "index.html:.card:1",
|
|
274
|
+
label: "Card",
|
|
275
|
+
tag: "div",
|
|
276
|
+
start: 3,
|
|
277
|
+
duration: 3,
|
|
278
|
+
track: 1,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
[
|
|
282
|
+
{
|
|
283
|
+
id: "Card",
|
|
284
|
+
key: "index.html:.card:0",
|
|
285
|
+
label: "Card",
|
|
286
|
+
tag: "div",
|
|
287
|
+
start: 0,
|
|
288
|
+
duration: 3,
|
|
289
|
+
track: 0,
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
8,
|
|
293
|
+
8,
|
|
294
|
+
),
|
|
295
|
+
).toEqual([
|
|
296
|
+
{
|
|
297
|
+
id: "Card",
|
|
298
|
+
key: "index.html:.card:0",
|
|
299
|
+
label: "Card",
|
|
300
|
+
tag: "div",
|
|
301
|
+
start: 0,
|
|
302
|
+
duration: 3,
|
|
303
|
+
track: 0,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "Card",
|
|
307
|
+
key: "index.html:.card:1",
|
|
308
|
+
label: "Card",
|
|
309
|
+
tag: "div",
|
|
310
|
+
start: 3,
|
|
311
|
+
duration: 3,
|
|
312
|
+
track: 1,
|
|
313
|
+
},
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
118
316
|
});
|
|
119
317
|
|
|
120
318
|
describe("shouldIgnorePlaybackShortcutTarget", () => {
|