@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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS,
|
|
4
|
+
HF_COLOR_GRADING_EFFECT_PRESETS,
|
|
5
|
+
HF_COLOR_GRADING_PALETTES,
|
|
6
|
+
normalizeHfColorGrading,
|
|
7
|
+
type HfColorGradingActiveEffectKey,
|
|
8
|
+
type HfColorGradingEffectKey,
|
|
9
|
+
type NormalizedHfColorGrading,
|
|
10
|
+
} from "@hyperframes/core/color-grading";
|
|
11
|
+
import { Plus, RotateCcw, X } from "../../icons/SystemIcons";
|
|
12
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
13
|
+
import { FLAT_PREVIEW_GRID, FlatSlider } from "./propertyPanelFlatPrimitives";
|
|
14
|
+
import type {
|
|
15
|
+
ColorGradingPresetPreviews,
|
|
16
|
+
ColorGradingPreviewOptions,
|
|
17
|
+
} from "./useColorGradingController";
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_EFFECTS,
|
|
20
|
+
EFFECT_GROUPS,
|
|
21
|
+
EFFECT_SPECS,
|
|
22
|
+
type EffectControl,
|
|
23
|
+
type EffectSpec,
|
|
24
|
+
} from "./propertyPanelFlatEffectSpecs";
|
|
25
|
+
import { FlatEffectControl } from "./propertyPanelFlatEffectControl";
|
|
26
|
+
import { presetPreviewHandlers } from "./propertyPanelPresetPreview";
|
|
27
|
+
|
|
28
|
+
export function activeColorGradingEffectCount(grading: NormalizedHfColorGrading): number {
|
|
29
|
+
return EFFECT_SPECS.filter((effect) => grading.effects[effect.key] > 0.0001).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function FlatEffectsAccessory({
|
|
33
|
+
grading,
|
|
34
|
+
onCommitColorGrading,
|
|
35
|
+
}: {
|
|
36
|
+
grading: NormalizedHfColorGrading;
|
|
37
|
+
onCommitColorGrading: (next: NormalizedHfColorGrading) => void;
|
|
38
|
+
}) {
|
|
39
|
+
const track = useTrackDesignInput();
|
|
40
|
+
if (!activeColorGradingEffectCount(grading)) return null;
|
|
41
|
+
return (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
data-flat-effects-reset="true"
|
|
45
|
+
title="Reset effects"
|
|
46
|
+
onClick={(event) => {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
track("button", "Reset effects");
|
|
49
|
+
onCommitColorGrading({ ...grading, effects: { ...DEFAULT_EFFECTS }, palette: null });
|
|
50
|
+
}}
|
|
51
|
+
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1"
|
|
52
|
+
>
|
|
53
|
+
<RotateCcw size={12} />
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function FlatEffectsSection({
|
|
59
|
+
grading,
|
|
60
|
+
previews,
|
|
61
|
+
presetPreviews,
|
|
62
|
+
onCommitColorGrading,
|
|
63
|
+
onPreviewColorGrading,
|
|
64
|
+
onRequestEffectPreviews,
|
|
65
|
+
onRequestPresetPreviews,
|
|
66
|
+
}: {
|
|
67
|
+
grading: NormalizedHfColorGrading;
|
|
68
|
+
previews: ColorGradingPresetPreviews;
|
|
69
|
+
presetPreviews: ColorGradingPresetPreviews;
|
|
70
|
+
onCommitColorGrading: (next: NormalizedHfColorGrading) => void;
|
|
71
|
+
onPreviewColorGrading: (
|
|
72
|
+
next: NormalizedHfColorGrading | null,
|
|
73
|
+
options?: ColorGradingPreviewOptions,
|
|
74
|
+
) => void;
|
|
75
|
+
onRequestEffectPreviews: (effects: readonly HfColorGradingActiveEffectKey[]) => void;
|
|
76
|
+
onRequestPresetPreviews: () => void;
|
|
77
|
+
}) {
|
|
78
|
+
const track = useTrackDesignInput();
|
|
79
|
+
const activeEffects = EFFECT_SPECS.filter((effect) => grading.effects[effect.key] > 0.0001);
|
|
80
|
+
const [catalogOpen, setCatalogOpen] = useState(activeEffects.length === 0);
|
|
81
|
+
const [catalogGroup, setCatalogGroup] = useState(EFFECT_GROUPS[0].label);
|
|
82
|
+
const [selectedKey, setSelectedKey] = useState<HfColorGradingActiveEffectKey | null>(
|
|
83
|
+
activeEffects[0]?.key ?? null,
|
|
84
|
+
);
|
|
85
|
+
const selectedEffect =
|
|
86
|
+
activeEffects.find((effect) => effect.key === selectedKey) ?? activeEffects[0] ?? null;
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!catalogOpen) return;
|
|
90
|
+
const group = EFFECT_GROUPS.find((candidate) => candidate.label === catalogGroup);
|
|
91
|
+
if (!group) return;
|
|
92
|
+
const effectKeys = group.effects.map((effect) => effect.key);
|
|
93
|
+
if (previews.status !== "loading" && effectKeys.some((effect) => !previews.images[effect])) {
|
|
94
|
+
onRequestEffectPreviews(effectKeys);
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
group.presets?.some((preset) => !presetPreviews.images[preset]) &&
|
|
98
|
+
presetPreviews.status !== "loading"
|
|
99
|
+
) {
|
|
100
|
+
onRequestPresetPreviews();
|
|
101
|
+
}
|
|
102
|
+
}, [
|
|
103
|
+
catalogGroup,
|
|
104
|
+
catalogOpen,
|
|
105
|
+
onRequestEffectPreviews,
|
|
106
|
+
onRequestPresetPreviews,
|
|
107
|
+
presetPreviews.images,
|
|
108
|
+
presetPreviews.status,
|
|
109
|
+
previews.images,
|
|
110
|
+
previews.status,
|
|
111
|
+
]);
|
|
112
|
+
useEffect(() => () => onPreviewColorGrading(null), [onPreviewColorGrading]);
|
|
113
|
+
|
|
114
|
+
const commitEffects = (effects: NormalizedHfColorGrading["effects"]) => {
|
|
115
|
+
onCommitColorGrading({
|
|
116
|
+
...grading,
|
|
117
|
+
effects,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
const commitEffect = (key: HfColorGradingEffectKey, value: number) => {
|
|
121
|
+
commitEffects({ ...grading.effects, [key]: value });
|
|
122
|
+
};
|
|
123
|
+
const resolveEffect = (effect: EffectSpec): NormalizedHfColorGrading => ({
|
|
124
|
+
...grading,
|
|
125
|
+
effects: {
|
|
126
|
+
...grading.effects,
|
|
127
|
+
...HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS[effect.key],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const applyEffect = (effect: EffectSpec) => {
|
|
131
|
+
track("button", `Add ${effect.label}`);
|
|
132
|
+
onCommitColorGrading(resolveEffect(effect));
|
|
133
|
+
setSelectedKey(effect.key);
|
|
134
|
+
setCatalogOpen(false);
|
|
135
|
+
};
|
|
136
|
+
const resolvePreset = (presetId: string) =>
|
|
137
|
+
normalizeHfColorGrading({ preset: presetId, lut: grading.lut }) ?? grading;
|
|
138
|
+
const removeEffect = (effect: EffectSpec) => {
|
|
139
|
+
track("button", `Remove ${effect.label}`);
|
|
140
|
+
commitEffect(effect.key, 0);
|
|
141
|
+
setSelectedKey(null);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const renderPalette = (kind: "mono" | "art") => {
|
|
145
|
+
const fallback = kind === "art" ? ["#1a1a1a", "#f5f5dc"] : ["#000000", "#ffffff"];
|
|
146
|
+
const palette = grading.palette;
|
|
147
|
+
return (
|
|
148
|
+
<div data-flat-effects-palette="true" className="space-y-2 py-1">
|
|
149
|
+
<div className="grid grid-cols-3 gap-1">
|
|
150
|
+
{HF_COLOR_GRADING_PALETTES.map((preset) => {
|
|
151
|
+
const selected =
|
|
152
|
+
palette?.length === preset.colors.length &&
|
|
153
|
+
palette.every((color, index) => color === preset.colors[index]);
|
|
154
|
+
return (
|
|
155
|
+
<button
|
|
156
|
+
key={preset.id}
|
|
157
|
+
type="button"
|
|
158
|
+
title={`${preset.group}: ${preset.label}`}
|
|
159
|
+
data-flat-effects-palette-preset={preset.id}
|
|
160
|
+
aria-pressed={selected}
|
|
161
|
+
onClick={() => {
|
|
162
|
+
track("button", `Use ${preset.label} palette`);
|
|
163
|
+
onCommitColorGrading({ ...grading, palette: [...preset.colors] });
|
|
164
|
+
}}
|
|
165
|
+
className={`min-w-0 border p-1 text-left ${
|
|
166
|
+
selected
|
|
167
|
+
? "border-panel-accent bg-panel-accent/10"
|
|
168
|
+
: "border-panel-hairline hover:border-panel-border-input"
|
|
169
|
+
}`}
|
|
170
|
+
>
|
|
171
|
+
<span className="mb-1 flex h-3 overflow-hidden rounded-[2px]">
|
|
172
|
+
{preset.colors.map((color) => (
|
|
173
|
+
<span key={color} className="flex-1" style={{ backgroundColor: color }} />
|
|
174
|
+
))}
|
|
175
|
+
</span>
|
|
176
|
+
<span className="block truncate text-[8px] text-panel-text-3">{preset.label}</span>
|
|
177
|
+
</button>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
</div>
|
|
181
|
+
{!palette ? (
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
data-flat-effects-add-palette="true"
|
|
185
|
+
onClick={() => {
|
|
186
|
+
track("button", "Customize effect palette");
|
|
187
|
+
onCommitColorGrading({ ...grading, palette: fallback });
|
|
188
|
+
}}
|
|
189
|
+
className="flex min-h-[28px] items-center gap-1 text-[10px] font-medium text-panel-accent hover:text-panel-accent/80"
|
|
190
|
+
>
|
|
191
|
+
<Plus size={11} /> Custom palette
|
|
192
|
+
</button>
|
|
193
|
+
) : (
|
|
194
|
+
<>
|
|
195
|
+
<div className="flex items-center justify-between">
|
|
196
|
+
<span className="text-[11px] text-panel-text-3">Custom palette</span>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
title="Use default palette"
|
|
200
|
+
onClick={() => onCommitColorGrading({ ...grading, palette: null })}
|
|
201
|
+
className="text-panel-text-4 hover:text-panel-text-1"
|
|
202
|
+
>
|
|
203
|
+
<RotateCcw size={11} />
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
207
|
+
{palette.map((color, index) => (
|
|
208
|
+
<span key={`${index}-${color}`} className="group/swatch relative">
|
|
209
|
+
<input
|
|
210
|
+
type="color"
|
|
211
|
+
aria-label={`Palette color ${index + 1}`}
|
|
212
|
+
value={color}
|
|
213
|
+
onChange={(event) => {
|
|
214
|
+
const nextPalette = [...palette];
|
|
215
|
+
nextPalette[index] = event.target.value;
|
|
216
|
+
onCommitColorGrading({ ...grading, palette: nextPalette });
|
|
217
|
+
}}
|
|
218
|
+
className="h-6 w-6 cursor-pointer rounded-sm border border-panel-border-input bg-transparent p-0"
|
|
219
|
+
/>
|
|
220
|
+
{palette.length > 2 && (
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
aria-label={`Remove palette color ${index + 1}`}
|
|
224
|
+
onClick={() =>
|
|
225
|
+
onCommitColorGrading({
|
|
226
|
+
...grading,
|
|
227
|
+
palette: palette.filter((_, colorIndex) => colorIndex !== index),
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
className="absolute -right-1 -top-1 hidden h-3.5 w-3.5 items-center justify-center rounded-full bg-panel-bg text-panel-text-2 shadow group-hover/swatch:flex"
|
|
231
|
+
>
|
|
232
|
+
<X size={8} />
|
|
233
|
+
</button>
|
|
234
|
+
)}
|
|
235
|
+
</span>
|
|
236
|
+
))}
|
|
237
|
+
{palette.length < 6 && (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
aria-label="Add palette color"
|
|
241
|
+
onClick={() =>
|
|
242
|
+
onCommitColorGrading({
|
|
243
|
+
...grading,
|
|
244
|
+
palette: [...palette, palette.at(-1) ?? "#ffffff"],
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
className="flex h-6 w-6 items-center justify-center rounded-sm border border-panel-border-input text-panel-text-4 hover:text-panel-text-1"
|
|
248
|
+
>
|
|
249
|
+
<Plus size={11} />
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className="space-y-2" data-flat-effects-section="true">
|
|
261
|
+
{activeEffects.length > 0 && (
|
|
262
|
+
<div data-flat-effects-active-list="true" className="space-y-1">
|
|
263
|
+
{activeEffects.map((effect) => {
|
|
264
|
+
const selected = selectedEffect?.key === effect.key;
|
|
265
|
+
return (
|
|
266
|
+
<button
|
|
267
|
+
key={effect.key}
|
|
268
|
+
type="button"
|
|
269
|
+
data-flat-effect-active={effect.key}
|
|
270
|
+
aria-pressed={selected}
|
|
271
|
+
onClick={() => setSelectedKey(effect.key)}
|
|
272
|
+
className={`flex min-h-[30px] w-full items-center justify-between border px-2 text-left text-[10px] transition-colors ${
|
|
273
|
+
selected
|
|
274
|
+
? "border-panel-accent bg-panel-accent/10 text-panel-text-0"
|
|
275
|
+
: "border-panel-hairline bg-panel-bg-soft text-panel-text-3 hover:border-panel-border-input hover:text-panel-text-1"
|
|
276
|
+
}`}
|
|
277
|
+
>
|
|
278
|
+
<span>{effect.label}</span>
|
|
279
|
+
<span className="text-[9px] text-panel-text-5">
|
|
280
|
+
{selected ? "Editing" : "Active"}
|
|
281
|
+
</span>
|
|
282
|
+
</button>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{selectedEffect && (
|
|
289
|
+
<div
|
|
290
|
+
data-flat-effect-editor={selectedEffect.key}
|
|
291
|
+
className="space-y-1 border-l-2 border-panel-border-input pl-2.5"
|
|
292
|
+
>
|
|
293
|
+
<div className="flex min-h-[26px] items-center justify-between gap-2">
|
|
294
|
+
<span className="text-[11px] font-medium text-panel-text-1">
|
|
295
|
+
{selectedEffect.label}
|
|
296
|
+
</span>
|
|
297
|
+
<span className="flex items-center gap-2">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
title={`Reset ${selectedEffect.label}`}
|
|
301
|
+
onClick={() => applyEffect(selectedEffect)}
|
|
302
|
+
className="text-panel-text-4 hover:text-panel-text-1"
|
|
303
|
+
>
|
|
304
|
+
<RotateCcw size={11} />
|
|
305
|
+
</button>
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
title={`Remove ${selectedEffect.label}`}
|
|
309
|
+
onClick={() => removeEffect(selectedEffect)}
|
|
310
|
+
className="text-panel-text-4 hover:text-red-300"
|
|
311
|
+
>
|
|
312
|
+
<X size={11} />
|
|
313
|
+
</button>
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
{selectedEffect.showMaster !== false && (
|
|
317
|
+
<FlatSlider
|
|
318
|
+
label={selectedEffect.masterLabel ?? "Mix"}
|
|
319
|
+
value={grading.effects[selectedEffect.key] * 100}
|
|
320
|
+
min={0}
|
|
321
|
+
max={selectedEffect.max ?? 100}
|
|
322
|
+
tier="explicitCustom"
|
|
323
|
+
displayValue={
|
|
324
|
+
selectedEffect.masterFormat?.(grading.effects[selectedEffect.key]) ??
|
|
325
|
+
`${Math.round(grading.effects[selectedEffect.key] * 100)}%`
|
|
326
|
+
}
|
|
327
|
+
onCommit={(next) => commitEffect(selectedEffect.key, next / 100)}
|
|
328
|
+
onReset={() =>
|
|
329
|
+
commitEffect(
|
|
330
|
+
selectedEffect.key,
|
|
331
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS[selectedEffect.key][selectedEffect.key] ??
|
|
332
|
+
1,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
{selectedEffect.settings?.map((control: EffectControl) => (
|
|
338
|
+
<FlatEffectControl
|
|
339
|
+
key={control.key}
|
|
340
|
+
control={control}
|
|
341
|
+
effect={selectedEffect}
|
|
342
|
+
effects={grading.effects}
|
|
343
|
+
onCommit={commitEffect}
|
|
344
|
+
/>
|
|
345
|
+
))}
|
|
346
|
+
{selectedEffect.palette && renderPalette(selectedEffect.palette)}
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
data-flat-effects-add-toggle="true"
|
|
353
|
+
aria-expanded={catalogOpen}
|
|
354
|
+
onClick={() => setCatalogOpen((open) => !open)}
|
|
355
|
+
className="flex min-h-[30px] items-center gap-1 text-[10px] font-medium text-panel-accent hover:text-panel-accent/80"
|
|
356
|
+
>
|
|
357
|
+
<Plus size={11} /> Add effect
|
|
358
|
+
</button>
|
|
359
|
+
|
|
360
|
+
{catalogOpen && (
|
|
361
|
+
<div data-flat-effects-catalog="true" className="space-y-2">
|
|
362
|
+
<div
|
|
363
|
+
role="tablist"
|
|
364
|
+
aria-label="Effect families"
|
|
365
|
+
className="grid grid-cols-4 gap-px overflow-hidden border border-panel-hairline bg-panel-hairline"
|
|
366
|
+
>
|
|
367
|
+
{EFFECT_GROUPS.map((group) => (
|
|
368
|
+
<button
|
|
369
|
+
key={group.label}
|
|
370
|
+
type="button"
|
|
371
|
+
role="tab"
|
|
372
|
+
aria-selected={catalogGroup === group.label}
|
|
373
|
+
data-flat-effect-group={group.label}
|
|
374
|
+
onClick={() => setCatalogGroup(group.label)}
|
|
375
|
+
className={`min-h-[25px] min-w-0 truncate bg-panel-bg px-2 text-[9px] ${
|
|
376
|
+
catalogGroup === group.label
|
|
377
|
+
? "font-medium text-panel-accent"
|
|
378
|
+
: "text-panel-text-4 hover:text-panel-text-1"
|
|
379
|
+
}`}
|
|
380
|
+
>
|
|
381
|
+
{group.label}
|
|
382
|
+
</button>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
{EFFECT_GROUPS.filter((group) => group.label === catalogGroup).map((group) => (
|
|
386
|
+
<section key={group.label} className="space-y-1" role="tabpanel">
|
|
387
|
+
<div className={FLAT_PREVIEW_GRID}>
|
|
388
|
+
{group.presets?.map((presetId) => {
|
|
389
|
+
const preset = HF_COLOR_GRADING_EFFECT_PRESETS.find(
|
|
390
|
+
(candidate) => candidate.id === presetId,
|
|
391
|
+
);
|
|
392
|
+
if (!preset) return null;
|
|
393
|
+
const selected = grading.preset === preset.id;
|
|
394
|
+
const preview = presetPreviews.images[preset.id];
|
|
395
|
+
return (
|
|
396
|
+
<button
|
|
397
|
+
key={preset.id}
|
|
398
|
+
type="button"
|
|
399
|
+
data-flat-effect-preset={preset.id}
|
|
400
|
+
aria-pressed={selected}
|
|
401
|
+
{...presetPreviewHandlers({
|
|
402
|
+
id: preset.id,
|
|
403
|
+
label: preset.label,
|
|
404
|
+
resolve: () => resolvePreset(preset.id),
|
|
405
|
+
onPreview: onPreviewColorGrading,
|
|
406
|
+
onCommit: onCommitColorGrading,
|
|
407
|
+
onTrack: (name) => track("button", `Apply ${name}`),
|
|
408
|
+
})}
|
|
409
|
+
className={`min-w-0 overflow-hidden border text-left text-[10px] transition-colors ${
|
|
410
|
+
selected
|
|
411
|
+
? "border-panel-accent bg-panel-accent/10 text-panel-text-0"
|
|
412
|
+
: "border-panel-hairline bg-panel-bg-soft text-panel-text-3 hover:border-panel-border-input hover:text-panel-text-1"
|
|
413
|
+
}`}
|
|
414
|
+
>
|
|
415
|
+
<span
|
|
416
|
+
className="flex w-full items-center justify-center overflow-hidden bg-black/20"
|
|
417
|
+
style={{
|
|
418
|
+
aspectRatio: `${presetPreviews.width} / ${presetPreviews.height}`,
|
|
419
|
+
}}
|
|
420
|
+
>
|
|
421
|
+
{preview ? (
|
|
422
|
+
<img
|
|
423
|
+
data-flat-effect-preset-preview={preset.id}
|
|
424
|
+
src={preview}
|
|
425
|
+
alt=""
|
|
426
|
+
draggable={false}
|
|
427
|
+
className="block h-full w-full object-contain"
|
|
428
|
+
/>
|
|
429
|
+
) : (
|
|
430
|
+
<span
|
|
431
|
+
data-flat-effect-preset-placeholder={presetPreviews.status}
|
|
432
|
+
className="h-full w-full bg-panel-bg-soft"
|
|
433
|
+
/>
|
|
434
|
+
)}
|
|
435
|
+
</span>
|
|
436
|
+
<span className="block truncate px-2 py-1.5">{preset.label}</span>
|
|
437
|
+
</button>
|
|
438
|
+
);
|
|
439
|
+
})}
|
|
440
|
+
{group.effects.map((effect) => {
|
|
441
|
+
const active = grading.effects[effect.key] > 0.0001;
|
|
442
|
+
const preview = previews.images[effect.key];
|
|
443
|
+
return (
|
|
444
|
+
<button
|
|
445
|
+
key={effect.key}
|
|
446
|
+
type="button"
|
|
447
|
+
data-flat-effect-option={effect.key}
|
|
448
|
+
aria-pressed={active}
|
|
449
|
+
title={`Preview ${effect.label}`}
|
|
450
|
+
onPointerEnter={() =>
|
|
451
|
+
onPreviewColorGrading(resolveEffect(effect), {
|
|
452
|
+
animatedPreview: { kind: "effects", id: effect.key },
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
onPointerLeave={() => onPreviewColorGrading(null)}
|
|
456
|
+
onFocus={() => onPreviewColorGrading(resolveEffect(effect))}
|
|
457
|
+
onBlur={() => onPreviewColorGrading(null)}
|
|
458
|
+
onClick={() => {
|
|
459
|
+
if (active) {
|
|
460
|
+
setSelectedKey(effect.key);
|
|
461
|
+
setCatalogOpen(false);
|
|
462
|
+
} else {
|
|
463
|
+
applyEffect(effect);
|
|
464
|
+
}
|
|
465
|
+
}}
|
|
466
|
+
className={`min-w-0 overflow-hidden border text-left text-[10px] transition-colors ${
|
|
467
|
+
active
|
|
468
|
+
? "border-panel-accent bg-panel-accent/10 text-panel-text-1"
|
|
469
|
+
: "border-panel-hairline bg-panel-bg-soft text-panel-text-3 hover:border-panel-border-input hover:text-panel-text-1"
|
|
470
|
+
}`}
|
|
471
|
+
>
|
|
472
|
+
<span
|
|
473
|
+
data-flat-effect-preview-frame={effect.key}
|
|
474
|
+
className="flex w-full items-center justify-center overflow-hidden bg-black/20"
|
|
475
|
+
style={{ aspectRatio: `${previews.width} / ${previews.height}` }}
|
|
476
|
+
>
|
|
477
|
+
{preview ? (
|
|
478
|
+
<img
|
|
479
|
+
data-flat-effect-preview={effect.key}
|
|
480
|
+
src={preview}
|
|
481
|
+
alt=""
|
|
482
|
+
draggable={false}
|
|
483
|
+
className="block h-full w-full object-contain"
|
|
484
|
+
/>
|
|
485
|
+
) : (
|
|
486
|
+
<span
|
|
487
|
+
data-flat-effect-preview-placeholder={previews.status}
|
|
488
|
+
className="h-full w-full bg-panel-bg-soft"
|
|
489
|
+
/>
|
|
490
|
+
)}
|
|
491
|
+
</span>
|
|
492
|
+
<span className="block truncate px-2 py-1.5">{effect.label}</span>
|
|
493
|
+
</button>
|
|
494
|
+
);
|
|
495
|
+
})}
|
|
496
|
+
</div>
|
|
497
|
+
</section>
|
|
498
|
+
))}
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import type { RegistryItem } from "@hyperframes/core/registry";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
deriveMediaOverlayPlacement,
|
|
9
|
+
filterMediaTreatmentOverlays,
|
|
10
|
+
FlatOverlaysSection,
|
|
11
|
+
MEDIA_TREATMENT_OVERLAY_TAG,
|
|
12
|
+
} from "./propertyPanelFlatOverlaysSection";
|
|
13
|
+
|
|
14
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
function registryItem(
|
|
17
|
+
name: string,
|
|
18
|
+
tags: string[],
|
|
19
|
+
type: RegistryItem["type"] = "hyperframes:component",
|
|
20
|
+
preview?: RegistryItem["preview"],
|
|
21
|
+
): RegistryItem {
|
|
22
|
+
const base = {
|
|
23
|
+
name,
|
|
24
|
+
title: name,
|
|
25
|
+
description: `${name} description`,
|
|
26
|
+
tags,
|
|
27
|
+
preview,
|
|
28
|
+
files: [{ path: `${name}.html`, target: `${name}.html`, type: "hyperframes:snippet" as const }],
|
|
29
|
+
};
|
|
30
|
+
if (type === "hyperframes:component") return { ...base, type };
|
|
31
|
+
if (type === "hyperframes:block") {
|
|
32
|
+
return { ...base, type, dimensions: { width: 1920, height: 1080 }, duration: 2 };
|
|
33
|
+
}
|
|
34
|
+
return { ...base, type, dimensions: { width: 1920, height: 1080 }, duration: 2 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
document.body.innerHTML = "";
|
|
39
|
+
vi.unstubAllGlobals();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("FlatOverlaysSection", () => {
|
|
43
|
+
it("places an overlay over the selected media in its own composition", () => {
|
|
44
|
+
expect(
|
|
45
|
+
deriveMediaOverlayPlacement(
|
|
46
|
+
{
|
|
47
|
+
sourceFile: "compositions/scene.html",
|
|
48
|
+
dataAttributes: { "track-index": "2" },
|
|
49
|
+
},
|
|
50
|
+
{ start: 1.5, duration: 3 },
|
|
51
|
+
),
|
|
52
|
+
).toEqual({
|
|
53
|
+
start: 1.5,
|
|
54
|
+
duration: 3,
|
|
55
|
+
track: 3,
|
|
56
|
+
compositionPath: "compositions/scene.html",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps only tagged Registry overlay blocks", () => {
|
|
61
|
+
const overlay = registryItem(
|
|
62
|
+
"camcorder-hud",
|
|
63
|
+
[MEDIA_TREATMENT_OVERLAY_TAG],
|
|
64
|
+
"hyperframes:block",
|
|
65
|
+
);
|
|
66
|
+
const ordinary = registryItem("caption", ["caption"]);
|
|
67
|
+
const taggedComponent = registryItem("scene", [MEDIA_TREATMENT_OVERLAY_TAG]);
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
filterMediaTreatmentOverlays([overlay, ordinary, taggedComponent]).map(({ name }) => name),
|
|
71
|
+
).toEqual(["camcorder-hud"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("loads tagged overlays and delegates installation by Registry name", async () => {
|
|
75
|
+
const items = [
|
|
76
|
+
registryItem("camcorder-hud", [MEDIA_TREATMENT_OVERLAY_TAG], "hyperframes:block", {
|
|
77
|
+
poster: "https://example.com/camcorder.png",
|
|
78
|
+
video: "https://example.com/camcorder.mp4",
|
|
79
|
+
}),
|
|
80
|
+
registryItem("ordinary-component", ["overlay"]),
|
|
81
|
+
];
|
|
82
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => items }));
|
|
83
|
+
const onAddOverlay = vi.fn().mockResolvedValue(undefined);
|
|
84
|
+
const host = document.createElement("div");
|
|
85
|
+
document.body.append(host);
|
|
86
|
+
const root = createRoot(host);
|
|
87
|
+
|
|
88
|
+
await act(async () => {
|
|
89
|
+
root.render(<FlatOverlaysSection onAddOverlay={onAddOverlay} />);
|
|
90
|
+
await Promise.resolve();
|
|
91
|
+
await Promise.resolve();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(host.querySelector('[data-flat-overlay="ordinary-component"]')).toBeNull();
|
|
95
|
+
const add = host.querySelector<HTMLButtonElement>('[data-flat-overlay="camcorder-hud"]');
|
|
96
|
+
expect(add).not.toBeNull();
|
|
97
|
+
expect(
|
|
98
|
+
host.querySelector<HTMLImageElement>('[data-flat-overlay-preview="camcorder-hud"]')?.src,
|
|
99
|
+
).toBe("https://example.com/camcorder.png");
|
|
100
|
+
expect(host.querySelector('[data-flat-overlays="true"]')?.className).toContain("auto-fill");
|
|
101
|
+
act(() => add?.dispatchEvent(new MouseEvent("pointerover", { bubbles: true })));
|
|
102
|
+
expect(host.querySelector<HTMLVideoElement>("video")?.src).toBe(
|
|
103
|
+
"https://example.com/camcorder.mp4",
|
|
104
|
+
);
|
|
105
|
+
await act(async () => {
|
|
106
|
+
add?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
107
|
+
await Promise.resolve();
|
|
108
|
+
});
|
|
109
|
+
expect(onAddOverlay).toHaveBeenCalledWith("camcorder-hud");
|
|
110
|
+
|
|
111
|
+
act(() => root.unmount());
|
|
112
|
+
});
|
|
113
|
+
});
|