@hyperframes/studio 0.5.0-alpha.11 → 0.5.0-alpha.12
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-Bl4Deziq.js → index-JhhmFie-.js} +31 -31
- 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 +139 -0
- package/src/player/hooks/useTimelinePlayer.ts +199 -87
- 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-JhhmFie-.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-KioPDrX6.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.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.12",
|
|
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.5.0-alpha.
|
|
36
|
-
"@hyperframes/player": "0.5.0-alpha.
|
|
35
|
+
"@hyperframes/core": "0.5.0-alpha.12",
|
|
36
|
+
"@hyperframes/player": "0.5.0-alpha.12"
|
|
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.5.0-alpha.
|
|
50
|
+
"@hyperframes/producer": "0.5.0-alpha.12"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -98,6 +98,10 @@ interface AppToast {
|
|
|
98
98
|
tone: "error" | "info";
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function getTimelineElementLabel(element: TimelineElement): string {
|
|
102
|
+
return element.label || element.id || element.tag;
|
|
103
|
+
}
|
|
104
|
+
|
|
101
105
|
type RightPanelTab = "design" | "renders";
|
|
102
106
|
|
|
103
107
|
const GENERIC_FONT_FAMILIES = new Set([
|
|
@@ -900,7 +904,7 @@ export function StudioApp() {
|
|
|
900
904
|
return (
|
|
901
905
|
<CompositionThumbnail
|
|
902
906
|
previewUrl={`/api/projects/${pid}/preview/comp/${compSrc}`}
|
|
903
|
-
label={el
|
|
907
|
+
label={getTimelineElementLabel(el)}
|
|
904
908
|
labelColor={style.label}
|
|
905
909
|
accentColor={style.clip}
|
|
906
910
|
seekTime={0}
|
|
@@ -915,7 +919,7 @@ export function StudioApp() {
|
|
|
915
919
|
return (
|
|
916
920
|
<CompositionThumbnail
|
|
917
921
|
previewUrl={activePreviewUrl}
|
|
918
|
-
label={el
|
|
922
|
+
label={getTimelineElementLabel(el)}
|
|
919
923
|
labelColor={style.label}
|
|
920
924
|
accentColor={style.clip}
|
|
921
925
|
selector={el.selector}
|
|
@@ -953,7 +957,7 @@ export function StudioApp() {
|
|
|
953
957
|
<AudioWaveform
|
|
954
958
|
audioUrl={audioUrl}
|
|
955
959
|
waveformUrl={waveformUrl}
|
|
956
|
-
label={el
|
|
960
|
+
label={getTimelineElementLabel(el)}
|
|
957
961
|
labelColor={style.label}
|
|
958
962
|
/>
|
|
959
963
|
);
|
|
@@ -966,7 +970,7 @@ export function StudioApp() {
|
|
|
966
970
|
return (
|
|
967
971
|
<VideoThumbnail
|
|
968
972
|
videoSrc={mediaSrc}
|
|
969
|
-
label={el
|
|
973
|
+
label={getTimelineElementLabel(el)}
|
|
970
974
|
labelColor={style.label}
|
|
971
975
|
duration={el.duration}
|
|
972
976
|
/>
|
|
@@ -977,7 +981,7 @@ export function StudioApp() {
|
|
|
977
981
|
return (
|
|
978
982
|
<CompositionThumbnail
|
|
979
983
|
previewUrl={`/api/projects/${pid}/preview`}
|
|
980
|
-
label={el
|
|
984
|
+
label={getTimelineElementLabel(el)}
|
|
981
985
|
labelColor={style.label}
|
|
982
986
|
accentColor={style.clip}
|
|
983
987
|
selector={el.selector}
|
|
@@ -1046,7 +1046,10 @@ export const Timeline = memo(function Timeline({
|
|
|
1046
1046
|
|
|
1047
1047
|
const getPreviewElement = useCallback(
|
|
1048
1048
|
(element: TimelineElement): TimelineElement => {
|
|
1049
|
-
if (
|
|
1049
|
+
if (
|
|
1050
|
+
resizingClip &&
|
|
1051
|
+
(resizingClip.element.key ?? resizingClip.element.id) === (element.key ?? element.id)
|
|
1052
|
+
) {
|
|
1050
1053
|
return {
|
|
1051
1054
|
...element,
|
|
1052
1055
|
start: resizingClip.previewStart,
|
|
@@ -1273,7 +1276,7 @@ export const Timeline = memo(function Timeline({
|
|
|
1273
1276
|
draggedClip?.started === true && draggedElement
|
|
1274
1277
|
? getRenderedTimelineElement({
|
|
1275
1278
|
element: draggedElement,
|
|
1276
|
-
draggedElementId: draggedElement.id,
|
|
1279
|
+
draggedElementId: draggedElement.key ?? draggedElement.id,
|
|
1277
1280
|
previewStart: draggedClip.previewStart,
|
|
1278
1281
|
previewTrack: draggedClip.previewTrack,
|
|
1279
1282
|
})
|
|
@@ -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
|
const baseBackgroundImage = isSelected ? theme.clipBackgroundActive : theme.clipBackground;
|
|
66
67
|
const glossBackgroundImage = isSelected
|
|
@@ -106,7 +107,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
106
107
|
title={
|
|
107
108
|
isComposition
|
|
108
109
|
? `${el.compositionSrc} \u2022 Double-click to open`
|
|
109
|
-
: `${
|
|
110
|
+
: `${displayLabel} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
|
|
110
111
|
}
|
|
111
112
|
onPointerEnter={onHoverStart}
|
|
112
113
|
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 {
|
|
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { Window } from "happy-dom";
|
|
3
3
|
import {
|
|
4
4
|
buildStandaloneRootTimelineElement,
|
|
5
|
+
createTimelineElementFromManifestClip,
|
|
5
6
|
findTimelineDomNodeForClip,
|
|
6
7
|
getTimelineElementSelector,
|
|
8
|
+
parseTimelineFromDOM,
|
|
7
9
|
type ClipManifestClip,
|
|
8
10
|
mergeTimelineElementsPreservingDowngrades,
|
|
9
11
|
resolveStandaloneRootCompositionSrc,
|
|
@@ -66,6 +68,7 @@ describe("buildStandaloneRootTimelineElement", () => {
|
|
|
66
68
|
}),
|
|
67
69
|
).toEqual({
|
|
68
70
|
id: "hero",
|
|
71
|
+
label: "hero",
|
|
69
72
|
key: 'scenes/hero.html:[data-composition-id="hero"]:0',
|
|
70
73
|
tag: "div",
|
|
71
74
|
start: 0,
|
|
@@ -146,6 +149,83 @@ describe("findTimelineDomNodeForClip", () => {
|
|
|
146
149
|
});
|
|
147
150
|
});
|
|
148
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
|
+
|
|
149
229
|
describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
150
230
|
it("preserves missing current elements when a shorter manifest arrives", () => {
|
|
151
231
|
expect(
|
|
@@ -174,6 +254,65 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
174
254
|
),
|
|
175
255
|
).toEqual([{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }]);
|
|
176
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
|
+
});
|
|
177
316
|
});
|
|
178
317
|
|
|
179
318
|
describe("shouldIgnorePlaybackShortcutTarget", () => {
|