@hyperframes/studio 0.7.70 → 0.7.72
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-DpKeYLEo.js → hyperframes-player-Bjf2HPzR.js} +1 -1
- package/dist/assets/index-BgZMle9I.css +1 -0
- package/dist/assets/{index-DXjAllII.js → index-Ch1hbJ3e.js} +1 -1
- package/dist/assets/{index-CbX-xoud.js → index-Ct_pxETK.js} +1 -1
- package/dist/assets/index-DCyWLHnx.js +428 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +2 -2
- package/dist/index.js +5383 -3804
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +10 -4
- package/src/components/editor/AnimationCard.test.tsx +100 -0
- package/src/components/editor/AnimationCard.tsx +5 -2
- package/src/components/editor/EaseCurveSection.test.tsx +348 -0
- package/src/components/editor/EaseCurveSection.tsx +444 -237
- package/src/components/editor/EaseParamFields.test.tsx +188 -0
- package/src/components/editor/EaseParamFields.tsx +246 -0
- package/src/components/editor/PropertyPanel.test.tsx +9 -7
- package/src/components/editor/PropertyPanelFlat.tsx +74 -88
- package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
- package/src/components/editor/easeCurveSvg.tsx +65 -0
- package/src/components/editor/easePresetLibrary.test.ts +114 -0
- package/src/components/editor/easePresetLibrary.ts +49 -0
- package/src/components/editor/gsapAnimationConstants.ts +15 -13
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
- package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
- package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +5 -5
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
- package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
- package/src/components/editor/propertyPanelTypes.ts +13 -0
- package/src/components/editor/useColorGradingController.test.ts +235 -30
- package/src/components/editor/useColorGradingController.ts +46 -88
- package/src/components/editor/useColorGradingPreviews.ts +307 -0
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/contexts/PanelLayoutContext.tsx +3 -6
- package/src/hooks/useBlockCatalog.ts +31 -13
- package/src/hooks/useBlockHandlers.ts +22 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
- package/src/hooks/useGsapScriptCommits.ts +14 -1
- package/src/hooks/usePanelLayout.test.ts +68 -1
- package/src/hooks/usePanelLayout.ts +72 -34
- package/src/player/components/PlayerControls.tsx +68 -229
- package/src/player/components/ShortcutsPanel.tsx +2 -4
- package/src/player/components/SpeedMenu.tsx +1 -2
- package/src/telemetry/events.test.ts +9 -0
- package/src/telemetry/events.ts +5 -0
- package/src/utils/blockInstaller.test.ts +67 -0
- package/src/utils/blockInstaller.ts +162 -109
- package/src/utils/studioUiPreferences.test.ts +5 -0
- package/src/utils/studioUiPreferences.ts +8 -0
- package/dist/assets/index-CAeU317U.js +0 -428
- package/dist/assets/index-Ceyz8Qt2.css +0 -1
- package/src/player/components/PlayerControls.test.ts +0 -20
- package/src/player/components/useSeekBarDrag.ts +0 -169
|
@@ -30,7 +30,7 @@ interface AddBlockOptions {
|
|
|
30
30
|
projectId: string;
|
|
31
31
|
blockName: string;
|
|
32
32
|
activeCompPath: string | null;
|
|
33
|
-
placement?: { start: number; track
|
|
33
|
+
placement?: { start: number; duration?: number; track?: number };
|
|
34
34
|
visualPosition?: { left: number; top: number };
|
|
35
35
|
previewIframe?: HTMLIFrameElement | null;
|
|
36
36
|
currentTime?: number;
|
|
@@ -56,6 +56,121 @@ function buildUniqueCompositionId(baseName: string, existingIds: Iterable<string
|
|
|
56
56
|
return `${baseName}_${i}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
async function installRegistryItem({
|
|
60
|
+
projectId,
|
|
61
|
+
blockName,
|
|
62
|
+
showToast,
|
|
63
|
+
}: Pick<AddBlockOptions, "projectId" | "blockName" | "showToast">): Promise<{
|
|
64
|
+
block: RegistryItem;
|
|
65
|
+
compositionFile: string;
|
|
66
|
+
} | null> {
|
|
67
|
+
const response = await fetch(`/api/projects/${projectId}/registry/install`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ blockName }),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const error = await response.json().catch(() => ({ error: "Install failed" }));
|
|
74
|
+
showToast((error as { error?: string }).error || "Failed to install block");
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const { written, block } = (await response.json()) as {
|
|
78
|
+
written: string[];
|
|
79
|
+
block: RegistryItem;
|
|
80
|
+
};
|
|
81
|
+
const compositionFile = written.find((file) => file.endsWith(".html")) ?? written[0];
|
|
82
|
+
if (!compositionFile) {
|
|
83
|
+
showToast("Installed but no composition file was written");
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { block, compositionFile };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function makeComponentBackgroundTransparent(
|
|
90
|
+
block: RegistryItem,
|
|
91
|
+
compositionFile: string,
|
|
92
|
+
readProjectFile: AddBlockOptions["readProjectFile"],
|
|
93
|
+
writeProjectFile: AddBlockOptions["writeProjectFile"],
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (block.type !== "hyperframes:component") return;
|
|
96
|
+
const content = await readProjectFile(compositionFile);
|
|
97
|
+
const transparentContent = content.replace(
|
|
98
|
+
/background:\s*(?:#(?:0a0a0a|000000|000|0a0805)|rgba?\([^)]*\))\s*;/g,
|
|
99
|
+
"background: transparent;",
|
|
100
|
+
);
|
|
101
|
+
if (transparentContent !== content) await writeProjectFile(compositionFile, transparentContent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveBlockPlacement({
|
|
105
|
+
block,
|
|
106
|
+
placement,
|
|
107
|
+
timelineElements,
|
|
108
|
+
currentTime,
|
|
109
|
+
}: {
|
|
110
|
+
block: RegistryItem;
|
|
111
|
+
placement: AddBlockOptions["placement"];
|
|
112
|
+
timelineElements: TimelineElement[];
|
|
113
|
+
currentTime: number;
|
|
114
|
+
}) {
|
|
115
|
+
const isBlock = block.type === "hyperframes:block";
|
|
116
|
+
const {
|
|
117
|
+
start: placementStart = currentTime,
|
|
118
|
+
duration: placementDuration,
|
|
119
|
+
track: placementTrack,
|
|
120
|
+
} = placement ?? {};
|
|
121
|
+
const start = Number(formatTimelineAttributeNumber(placementStart));
|
|
122
|
+
const blockDuration = "duration" in block ? (block as { duration: number }).duration : undefined;
|
|
123
|
+
const duration =
|
|
124
|
+
placementDuration ??
|
|
125
|
+
blockDuration ??
|
|
126
|
+
timelineElements.reduce(
|
|
127
|
+
(max, element) => Math.max(max, (element.start ?? 0) + (element.duration ?? 0)),
|
|
128
|
+
10,
|
|
129
|
+
);
|
|
130
|
+
const nextTrack = Math.max(0, ...timelineElements.map((element) => element.track)) + 1;
|
|
131
|
+
const track = placementTrack ?? (isBlock ? 0 : nextTrack);
|
|
132
|
+
return { duration, isBlock, start, track };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildSubCompositionHtml({
|
|
136
|
+
id,
|
|
137
|
+
compositionFile,
|
|
138
|
+
start,
|
|
139
|
+
duration,
|
|
140
|
+
track,
|
|
141
|
+
width,
|
|
142
|
+
height,
|
|
143
|
+
left,
|
|
144
|
+
top,
|
|
145
|
+
zIndex,
|
|
146
|
+
}: {
|
|
147
|
+
id: string;
|
|
148
|
+
compositionFile: string;
|
|
149
|
+
start: number;
|
|
150
|
+
duration: number;
|
|
151
|
+
track: number;
|
|
152
|
+
width: number;
|
|
153
|
+
height: number;
|
|
154
|
+
left: number;
|
|
155
|
+
top: number;
|
|
156
|
+
zIndex: number;
|
|
157
|
+
}): string {
|
|
158
|
+
return [
|
|
159
|
+
`<div`,
|
|
160
|
+
` id="${id}"`,
|
|
161
|
+
` data-hf-id="hf-${generateId()}"`,
|
|
162
|
+
` data-composition-id="${id}"`,
|
|
163
|
+
` data-composition-src="${compositionFile}"`,
|
|
164
|
+
` data-start="${formatTimelineAttributeNumber(start)}"`,
|
|
165
|
+
` data-duration="${formatTimelineAttributeNumber(duration)}"`,
|
|
166
|
+
` data-track-index="${track}"`,
|
|
167
|
+
` data-width="${width}"`,
|
|
168
|
+
` data-height="${height}"`,
|
|
169
|
+
` style="position: absolute; left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px; z-index: ${zIndex}"`,
|
|
170
|
+
`></div>`,
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
59
174
|
export async function addBlockToProject(
|
|
60
175
|
opts: AddBlockOptions,
|
|
61
176
|
): Promise<{ block: RegistryItem; compositionPath: string } | null> {
|
|
@@ -75,115 +190,53 @@ export async function addBlockToProject(
|
|
|
75
190
|
} = opts;
|
|
76
191
|
|
|
77
192
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
193
|
+
const installed = await installRegistryItem({ projectId, blockName, showToast });
|
|
194
|
+
if (!installed) return null;
|
|
195
|
+
const { block, compositionFile } = installed;
|
|
196
|
+
await makeComponentBackgroundTransparent(
|
|
197
|
+
block,
|
|
198
|
+
compositionFile,
|
|
199
|
+
readProjectFile,
|
|
200
|
+
writeProjectFile,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const targetPath = activeCompPath || "index.html";
|
|
204
|
+
const originalContent = await readProjectFile(targetPath);
|
|
205
|
+
const relevantElements = timelineElements.filter(
|
|
206
|
+
(element) => (element.sourceFile || targetPath) === targetPath,
|
|
207
|
+
);
|
|
208
|
+
const { duration, isBlock, start, track } = resolveBlockPlacement({
|
|
209
|
+
block,
|
|
210
|
+
placement,
|
|
211
|
+
timelineElements: relevantElements,
|
|
212
|
+
currentTime: opts.currentTime ?? 0,
|
|
213
|
+
});
|
|
214
|
+
const { width, height } = resolveTimelineAssetCompositionSize(originalContent);
|
|
215
|
+
const subComposition = buildSubCompositionHtml({
|
|
216
|
+
id: buildUniqueCompositionId(block.name, collectHtmlIds(originalContent)),
|
|
217
|
+
compositionFile,
|
|
218
|
+
start,
|
|
219
|
+
duration,
|
|
220
|
+
track,
|
|
221
|
+
width,
|
|
222
|
+
height,
|
|
223
|
+
left: visualPosition ? Math.round(visualPosition.left) : 0,
|
|
224
|
+
top: visualPosition ? Math.round(visualPosition.top) : 0,
|
|
225
|
+
zIndex: getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1,
|
|
226
|
+
});
|
|
227
|
+
const patchedContent = extendRootDurationInSource(
|
|
228
|
+
insertTimelineAssetIntoSource(originalContent, subComposition),
|
|
229
|
+
start + duration,
|
|
230
|
+
);
|
|
231
|
+
await saveProjectFilesWithHistory({
|
|
232
|
+
projectId,
|
|
233
|
+
label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
|
|
234
|
+
kind: "timeline",
|
|
235
|
+
files: { [targetPath]: patchedContent },
|
|
236
|
+
readFile: async () => originalContent,
|
|
237
|
+
writeFile: writeProjectFile,
|
|
238
|
+
recordEdit,
|
|
82
239
|
});
|
|
83
|
-
|
|
84
|
-
if (!res.ok) {
|
|
85
|
-
const err = await res.json().catch(() => ({ error: "Install failed" }));
|
|
86
|
-
showToast((err as { error?: string }).error || "Failed to install block");
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const { written, block } = (await res.json()) as {
|
|
91
|
-
written: string[];
|
|
92
|
-
block: RegistryItem;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const compositionFile = written.find((f) => f.endsWith(".html")) ?? written[0];
|
|
96
|
-
if (!compositionFile) {
|
|
97
|
-
showToast("Installed but no composition file was written");
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (block.type === "hyperframes:component") {
|
|
102
|
-
const compContent = await readProjectFile(compositionFile);
|
|
103
|
-
const transparentContent = compContent.replace(
|
|
104
|
-
/background:\s*(?:#(?:0a0a0a|000000|000|0a0805)|rgba?\([^)]*\))\s*;/g,
|
|
105
|
-
"background: transparent;",
|
|
106
|
-
);
|
|
107
|
-
if (transparentContent !== compContent) {
|
|
108
|
-
await writeProjectFile(compositionFile, transparentContent);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
const targetPath = activeCompPath || "index.html";
|
|
114
|
-
const originalContent = await readProjectFile(targetPath);
|
|
115
|
-
const existingIds = collectHtmlIds(originalContent);
|
|
116
|
-
const compId = buildUniqueCompositionId(block.name, existingIds);
|
|
117
|
-
|
|
118
|
-
const resolvedTargetPath = targetPath || "index.html";
|
|
119
|
-
const relevantElements = timelineElements.filter(
|
|
120
|
-
(te) => (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const isBlock = block.type === "hyperframes:block";
|
|
124
|
-
const { width: hostWidth, height: hostHeight } =
|
|
125
|
-
resolveTimelineAssetCompositionSize(originalContent);
|
|
126
|
-
const hostDims = { left: 0, top: 0, width: hostWidth, height: hostHeight };
|
|
127
|
-
|
|
128
|
-
const currentTime = opts.currentTime ?? 0;
|
|
129
|
-
const start = placement
|
|
130
|
-
? Number(formatTimelineAttributeNumber(placement.start))
|
|
131
|
-
: Number(formatTimelineAttributeNumber(currentTime));
|
|
132
|
-
const blockDuration =
|
|
133
|
-
"duration" in block ? (block as { duration: number }).duration : undefined;
|
|
134
|
-
const duration =
|
|
135
|
-
blockDuration ??
|
|
136
|
-
relevantElements.reduce(
|
|
137
|
-
(max, te) => Math.max(max, (te.start ?? 0) + (te.duration ?? 0)),
|
|
138
|
-
10,
|
|
139
|
-
);
|
|
140
|
-
const track =
|
|
141
|
-
placement?.track ??
|
|
142
|
-
(isBlock
|
|
143
|
-
? 0
|
|
144
|
-
: relevantElements.length > 0
|
|
145
|
-
? Math.max(...relevantElements.map((te) => te.track)) + 1
|
|
146
|
-
: 1);
|
|
147
|
-
|
|
148
|
-
const zIndex = getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1;
|
|
149
|
-
|
|
150
|
-
const width = hostDims.width;
|
|
151
|
-
const height = hostDims.height;
|
|
152
|
-
|
|
153
|
-
const left = visualPosition ? Math.round(visualPosition.left) : 0;
|
|
154
|
-
const top = visualPosition ? Math.round(visualPosition.top) : 0;
|
|
155
|
-
|
|
156
|
-
const subCompHtml = [
|
|
157
|
-
`<div`,
|
|
158
|
-
// A stable id (+ hf-id) is what authored sub-comps carry; without it the
|
|
159
|
-
// timeline can't dedup the host and renders duplicate clips that multiply
|
|
160
|
-
// on every interaction. Matches the authored-comp shape.
|
|
161
|
-
` id="${compId}"`,
|
|
162
|
-
` data-hf-id="hf-${generateId()}"`,
|
|
163
|
-
` data-composition-id="${compId}"`,
|
|
164
|
-
` data-composition-src="${compositionFile}"`,
|
|
165
|
-
` data-start="${formatTimelineAttributeNumber(start)}"`,
|
|
166
|
-
` data-duration="${formatTimelineAttributeNumber(duration)}"`,
|
|
167
|
-
` data-track-index="${track}"`,
|
|
168
|
-
` data-width="${width}"`,
|
|
169
|
-
` data-height="${height}"`,
|
|
170
|
-
` style="position: absolute; left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px; z-index: ${zIndex}"`,
|
|
171
|
-
`></div>`,
|
|
172
|
-
].join("\n");
|
|
173
|
-
|
|
174
|
-
let patchedContent = insertTimelineAssetIntoSource(originalContent, subCompHtml);
|
|
175
|
-
patchedContent = extendRootDurationInSource(patchedContent, start + duration);
|
|
176
|
-
|
|
177
|
-
await saveProjectFilesWithHistory({
|
|
178
|
-
projectId,
|
|
179
|
-
label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
|
|
180
|
-
kind: "timeline",
|
|
181
|
-
files: { [targetPath]: patchedContent },
|
|
182
|
-
readFile: async () => originalContent,
|
|
183
|
-
writeFile: writeProjectFile,
|
|
184
|
-
recordEdit,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
240
|
|
|
188
241
|
await refreshFileTree();
|
|
189
242
|
reloadPreview();
|
|
@@ -20,12 +20,15 @@ describe("studio UI preferences", () => {
|
|
|
20
20
|
const storage = createStorage();
|
|
21
21
|
|
|
22
22
|
writeStudioUiPreferences({ timelineVisible: false }, storage);
|
|
23
|
+
writeStudioUiPreferences({ leftWidth: 384, rightWidth: 424 }, storage);
|
|
23
24
|
writeStudioUiPreferences({ playbackRate: 1.5 }, storage);
|
|
24
25
|
writeStudioUiPreferences({ audioMuted: true }, storage);
|
|
25
26
|
writeStudioUiPreferences({ previewZoom: { zoomPercent: 160, panX: -20, panY: 12 } }, storage);
|
|
26
27
|
|
|
27
28
|
expect(readStudioUiPreferences(storage)).toEqual({
|
|
28
29
|
timelineVisible: false,
|
|
30
|
+
leftWidth: 384,
|
|
31
|
+
rightWidth: 424,
|
|
29
32
|
playbackRate: 1.5,
|
|
30
33
|
audioMuted: true,
|
|
31
34
|
previewZoom: { zoomPercent: 160, panX: -20, panY: 12 },
|
|
@@ -38,6 +41,8 @@ describe("studio UI preferences", () => {
|
|
|
38
41
|
"hf-studio-ui-preferences",
|
|
39
42
|
JSON.stringify({
|
|
40
43
|
leftCollapsed: "yes",
|
|
44
|
+
leftWidth: "wide",
|
|
45
|
+
rightWidth: Number.NaN,
|
|
41
46
|
timelineVisible: true,
|
|
42
47
|
playbackRate: Number.NaN,
|
|
43
48
|
audioMuted: "false",
|
|
@@ -6,6 +6,8 @@ export interface StoredPreviewZoomState {
|
|
|
6
6
|
|
|
7
7
|
export interface StudioUiPreferences {
|
|
8
8
|
leftCollapsed?: boolean;
|
|
9
|
+
leftWidth?: number;
|
|
10
|
+
rightWidth?: number;
|
|
9
11
|
timelineVisible?: boolean;
|
|
10
12
|
timelineHeight?: number;
|
|
11
13
|
playbackRate?: number;
|
|
@@ -58,6 +60,12 @@ function readStorage(storage: Storage | null): StudioUiPreferences {
|
|
|
58
60
|
if (typeof parsed.leftCollapsed === "boolean") {
|
|
59
61
|
preferences.leftCollapsed = parsed.leftCollapsed;
|
|
60
62
|
}
|
|
63
|
+
if (typeof parsed.leftWidth === "number" && Number.isFinite(parsed.leftWidth)) {
|
|
64
|
+
preferences.leftWidth = parsed.leftWidth;
|
|
65
|
+
}
|
|
66
|
+
if (typeof parsed.rightWidth === "number" && Number.isFinite(parsed.rightWidth)) {
|
|
67
|
+
preferences.rightWidth = parsed.rightWidth;
|
|
68
|
+
}
|
|
61
69
|
if (typeof parsed.timelineVisible === "boolean") {
|
|
62
70
|
preferences.timelineVisible = parsed.timelineVisible;
|
|
63
71
|
}
|