@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,305 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getHfColorGradingCapabilities,
|
|
3
|
+
normalizeHfColorGrading,
|
|
4
|
+
type HfColorGradingActiveEffectKey,
|
|
5
|
+
type HfColorGradingEffectKey,
|
|
6
|
+
type HfColorGradingPresetId,
|
|
7
|
+
} from "@hyperframes/core/color-grading";
|
|
8
|
+
|
|
9
|
+
type SliderControl = {
|
|
10
|
+
kind: "slider";
|
|
11
|
+
key: HfColorGradingEffectKey;
|
|
12
|
+
label: string;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
step?: number;
|
|
16
|
+
scale?: number;
|
|
17
|
+
unit?: string;
|
|
18
|
+
format?: (value: number) => string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type EffectControl =
|
|
22
|
+
| SliderControl
|
|
23
|
+
| { kind: "toggle"; key: HfColorGradingEffectKey; label: string }
|
|
24
|
+
| {
|
|
25
|
+
kind: "select";
|
|
26
|
+
key: HfColorGradingEffectKey;
|
|
27
|
+
label: string;
|
|
28
|
+
options: Array<{ value: string; label: string }>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type EffectSpec = {
|
|
32
|
+
key: HfColorGradingActiveEffectKey;
|
|
33
|
+
label: string;
|
|
34
|
+
showMaster?: false;
|
|
35
|
+
masterLabel?: string;
|
|
36
|
+
masterFormat?: (value: number) => string;
|
|
37
|
+
max?: number;
|
|
38
|
+
settings?: readonly EffectControl[];
|
|
39
|
+
palette?: "mono" | "art";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type EffectGroup = {
|
|
43
|
+
label: string;
|
|
44
|
+
effects: readonly EffectSpec[];
|
|
45
|
+
presets?: readonly HfColorGradingPresetId[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const EFFECT_CONTROL_LIMITS = new Map(
|
|
49
|
+
getHfColorGradingCapabilities().effects.flatMap((effect) =>
|
|
50
|
+
effect.controls.map((control) => [control.key, control] as const),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
function controlRange(key: HfColorGradingEffectKey, scale: number) {
|
|
55
|
+
const control = EFFECT_CONTROL_LIMITS.get(key);
|
|
56
|
+
return control ? { min: control.min * scale, max: control.max * scale, scale } : { scale };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const percent = (key: HfColorGradingEffectKey, label: string): SliderControl => ({
|
|
60
|
+
kind: "slider",
|
|
61
|
+
key,
|
|
62
|
+
label,
|
|
63
|
+
...controlRange(key, 100),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const degrees = (key: HfColorGradingEffectKey, label: string, max: number): SliderControl => ({
|
|
67
|
+
kind: "slider",
|
|
68
|
+
key,
|
|
69
|
+
label,
|
|
70
|
+
...controlRange(key, max),
|
|
71
|
+
unit: "deg",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function enumOptions(key: HfColorGradingEffectKey, labels: readonly string[]) {
|
|
75
|
+
const control = EFFECT_CONTROL_LIMITS.get(key);
|
|
76
|
+
const first = control?.min ?? 0;
|
|
77
|
+
const count = (control?.max ?? labels.length - 1) - first + 1;
|
|
78
|
+
if (labels.length !== count) throw new Error(`${key} labels do not match Core capabilities`);
|
|
79
|
+
return labels.map((label, index) => ({ value: String(first + index), label }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ASCII_STYLES = enumOptions("asciiStyle", [
|
|
83
|
+
"Standard",
|
|
84
|
+
"Dense",
|
|
85
|
+
"Minimal",
|
|
86
|
+
"Blocks",
|
|
87
|
+
"Braille",
|
|
88
|
+
"Technical",
|
|
89
|
+
"Matrix",
|
|
90
|
+
"Hatching",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const SCREEN_SHAPES = enumOptions("monoScreenShape", [
|
|
94
|
+
"Circle",
|
|
95
|
+
"Square",
|
|
96
|
+
"Diamond",
|
|
97
|
+
"Triangle",
|
|
98
|
+
"Line",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
export const EFFECT_GROUPS: readonly EffectGroup[] = [
|
|
102
|
+
{
|
|
103
|
+
label: "Essentials",
|
|
104
|
+
effects: [
|
|
105
|
+
{
|
|
106
|
+
key: "blur",
|
|
107
|
+
label: "Blur",
|
|
108
|
+
masterLabel: "Radius",
|
|
109
|
+
masterFormat: (value) => `${(0.75 + Math.pow(value, 1.35) * 32).toFixed(1)}px`,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: "pixelate",
|
|
113
|
+
label: "Pixelate",
|
|
114
|
+
masterLabel: "Cell Size",
|
|
115
|
+
masterFormat: (value) => `${Math.round(1 + value * 47)}px`,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "bloom",
|
|
119
|
+
label: "Bloom",
|
|
120
|
+
masterLabel: "Intensity",
|
|
121
|
+
max: controlRange("bloom", 100).max,
|
|
122
|
+
settings: [
|
|
123
|
+
{
|
|
124
|
+
kind: "slider",
|
|
125
|
+
key: "bloomRadius",
|
|
126
|
+
label: "Radius",
|
|
127
|
+
...controlRange("bloomRadius", 1),
|
|
128
|
+
unit: "px",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: "Retro & Glitch",
|
|
136
|
+
presets: ["creator-camcorder", "vhs-playback", "home-movie-8mm"],
|
|
137
|
+
effects: [
|
|
138
|
+
{ key: "chromaBleed", label: "Chroma Softening", masterLabel: "Smear" },
|
|
139
|
+
{
|
|
140
|
+
key: "tapeDamage",
|
|
141
|
+
label: "Tape Damage",
|
|
142
|
+
showMaster: false,
|
|
143
|
+
settings: [
|
|
144
|
+
percent("tapeTracking", "Tracking"),
|
|
145
|
+
percent("tapeNoise", "Noise"),
|
|
146
|
+
percent("tapeSpeed", "Speed"),
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{ key: "filmArtifacts", label: "Film Artifacts", masterLabel: "Density" },
|
|
150
|
+
{
|
|
151
|
+
key: "scanlines",
|
|
152
|
+
label: "Scanlines",
|
|
153
|
+
masterLabel: "Opacity",
|
|
154
|
+
settings: [
|
|
155
|
+
{
|
|
156
|
+
...percent("scanlineCount", "Line Count"),
|
|
157
|
+
format: (value) => `${Math.round(50 + value * 450)}`,
|
|
158
|
+
},
|
|
159
|
+
percent("scanlineSoftness", "Softness"),
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{ key: "crtCurvature", label: "CRT Curvature", masterLabel: "Curvature" },
|
|
163
|
+
{
|
|
164
|
+
key: "chromaticAberration",
|
|
165
|
+
label: "Channel Separation",
|
|
166
|
+
masterLabel: "Separation",
|
|
167
|
+
settings: [degrees("chromaticAngle", "Angle", 360)],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: "digitalGlitch",
|
|
171
|
+
label: "Digital Glitch",
|
|
172
|
+
showMaster: false,
|
|
173
|
+
settings: [
|
|
174
|
+
percent("digitalGlitchColorSplit", "Color Split"),
|
|
175
|
+
percent("digitalGlitchLineTear", "Line Tear"),
|
|
176
|
+
percent("digitalGlitchPixelate", "Pixelation"),
|
|
177
|
+
percent("digitalGlitchBlockAmount", "Block Amount"),
|
|
178
|
+
percent("digitalGlitchBlockDisplacement", "Displacement"),
|
|
179
|
+
percent("digitalGlitchBlockOpacity", "Block Opacity"),
|
|
180
|
+
percent("digitalGlitchSpeed", "Speed"),
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
label: "Print",
|
|
187
|
+
presets: ["editorial-halftone", "two-ink-print"],
|
|
188
|
+
effects: [
|
|
189
|
+
{
|
|
190
|
+
key: "halftone",
|
|
191
|
+
label: "Halftone",
|
|
192
|
+
showMaster: false,
|
|
193
|
+
settings: [percent("halftoneSize", "Dot Size")],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: "twoInkPrint",
|
|
197
|
+
label: "Two-Ink Print",
|
|
198
|
+
showMaster: false,
|
|
199
|
+
settings: [percent("twoInkPrintSize", "Dot Size")],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: "dither",
|
|
203
|
+
label: "Ordered Dither",
|
|
204
|
+
showMaster: false,
|
|
205
|
+
palette: "mono",
|
|
206
|
+
settings: [
|
|
207
|
+
{
|
|
208
|
+
...percent("ditherSize", "Point Size"),
|
|
209
|
+
format: (value) => `${(1 + value * 4).toFixed(1)}px`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
key: "monoScreen",
|
|
215
|
+
label: "Mono Screen",
|
|
216
|
+
showMaster: false,
|
|
217
|
+
palette: "mono",
|
|
218
|
+
settings: [
|
|
219
|
+
{
|
|
220
|
+
...percent("monoScreenSize", "Cell Size"),
|
|
221
|
+
format: (value) => `${Math.round(4 + value * 14)}px`,
|
|
222
|
+
},
|
|
223
|
+
degrees("monoScreenAngle", "Angle", 90),
|
|
224
|
+
percent("monoScreenSpread", "Spread"),
|
|
225
|
+
{ kind: "select", key: "monoScreenShape", label: "Shape", options: SCREEN_SHAPES },
|
|
226
|
+
{ kind: "toggle", key: "monoScreenInvert", label: "Invert" },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: "Art",
|
|
233
|
+
effects: [
|
|
234
|
+
{
|
|
235
|
+
key: "ascii",
|
|
236
|
+
label: "ASCII",
|
|
237
|
+
showMaster: false,
|
|
238
|
+
palette: "mono",
|
|
239
|
+
settings: [
|
|
240
|
+
{
|
|
241
|
+
...percent("asciiSize", "Character Size"),
|
|
242
|
+
format: (value) => `${Math.round(4 + value * 76)}px`,
|
|
243
|
+
},
|
|
244
|
+
{ kind: "select", key: "asciiStyle", label: "Style", options: ASCII_STYLES },
|
|
245
|
+
{ kind: "toggle", key: "asciiInvert", label: "Invert" },
|
|
246
|
+
{ kind: "toggle", key: "asciiColor", label: "Use Source Color" },
|
|
247
|
+
percent("asciiRotation", "Edge Rotation"),
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
key: "engraving",
|
|
252
|
+
label: "Engraving",
|
|
253
|
+
showMaster: false,
|
|
254
|
+
palette: "art",
|
|
255
|
+
settings: [
|
|
256
|
+
percent("engravingSpacing", "Spacing"),
|
|
257
|
+
percent("engravingMinThickness", "Min Thickness"),
|
|
258
|
+
percent("engravingMaxThickness", "Max Thickness"),
|
|
259
|
+
degrees("engravingAngle", "Angle", 180),
|
|
260
|
+
percent("engravingContrast", "Contrast"),
|
|
261
|
+
percent("engravingSharpness", "Sharpness"),
|
|
262
|
+
percent("engravingWave", "Wave"),
|
|
263
|
+
percent("engravingWaveFrequency", "Wave Frequency"),
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: "crosshatch",
|
|
268
|
+
label: "Crosshatch",
|
|
269
|
+
showMaster: false,
|
|
270
|
+
palette: "art",
|
|
271
|
+
settings: [
|
|
272
|
+
percent("crosshatchSpacing", "Spacing"),
|
|
273
|
+
percent("crosshatchThickness", "Thickness"),
|
|
274
|
+
degrees("crosshatchAngle", "Angle", 180),
|
|
275
|
+
percent("crosshatchContrast", "Contrast"),
|
|
276
|
+
percent("crosshatchEdges", "Edge Detail"),
|
|
277
|
+
percent("crosshatchLineWeight", "Line Variation"),
|
|
278
|
+
percent("crosshatchWave", "Wave"),
|
|
279
|
+
percent("crosshatchWaveFrequency", "Wave Frequency"),
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: "kuwahara",
|
|
284
|
+
label: "Kuwahara Paint",
|
|
285
|
+
showMaster: false,
|
|
286
|
+
settings: [
|
|
287
|
+
{
|
|
288
|
+
...percent("kuwaharaRadius", "Radius"),
|
|
289
|
+
format: (value) => `${Math.round(2 + value * 14)}px`,
|
|
290
|
+
},
|
|
291
|
+
percent("kuwaharaSharpness", "Sharpness"),
|
|
292
|
+
{
|
|
293
|
+
...percent("kuwaharaSaturation", "Saturation"),
|
|
294
|
+
format: (value) => `${Math.round(value * 200)}%`,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
export const EFFECT_SPECS = EFFECT_GROUPS.flatMap((group) => group.effects);
|
|
303
|
+
const DEFAULT_GRADING = normalizeHfColorGrading("neutral");
|
|
304
|
+
if (!DEFAULT_GRADING) throw new Error("Missing neutral color grading preset");
|
|
305
|
+
export const DEFAULT_EFFECTS = DEFAULT_GRADING.effects;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS,
|
|
8
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS,
|
|
9
|
+
HF_COLOR_GRADING_PALETTES,
|
|
10
|
+
getHfColorGradingCapabilities,
|
|
11
|
+
normalizeHfColorGrading,
|
|
12
|
+
} from "@hyperframes/core/color-grading";
|
|
13
|
+
import {
|
|
14
|
+
activeColorGradingEffectCount,
|
|
15
|
+
FlatEffectsAccessory,
|
|
16
|
+
FlatEffectsSection,
|
|
17
|
+
} from "./propertyPanelFlatEffectsSection";
|
|
18
|
+
import { EFFECT_SPECS } from "./propertyPanelFlatEffectSpecs";
|
|
19
|
+
|
|
20
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.body.innerHTML = "";
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function neutralGrading() {
|
|
27
|
+
const grading = normalizeHfColorGrading("neutral");
|
|
28
|
+
if (!grading) throw new Error("expected neutral grading");
|
|
29
|
+
return grading;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderInto(node: React.ReactElement) {
|
|
33
|
+
const host = document.body.appendChild(document.createElement("div"));
|
|
34
|
+
const root = createRoot(host);
|
|
35
|
+
act(() => root.render(node));
|
|
36
|
+
return { host, root };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sectionProps(
|
|
40
|
+
grading: ReturnType<typeof neutralGrading>,
|
|
41
|
+
onCommitColorGrading: ReturnType<typeof vi.fn> = vi.fn(),
|
|
42
|
+
) {
|
|
43
|
+
return {
|
|
44
|
+
grading,
|
|
45
|
+
previews: {
|
|
46
|
+
status: "ready" as const,
|
|
47
|
+
images: { pixelate: "data:image/png;base64,pixelate" },
|
|
48
|
+
width: 160,
|
|
49
|
+
height: 90,
|
|
50
|
+
},
|
|
51
|
+
presetPreviews: {
|
|
52
|
+
status: "ready" as const,
|
|
53
|
+
images: { "vhs-playback": "data:image/png;base64,vhs" },
|
|
54
|
+
width: 160,
|
|
55
|
+
height: 90,
|
|
56
|
+
},
|
|
57
|
+
onCommitColorGrading,
|
|
58
|
+
onPreviewColorGrading: vi.fn(),
|
|
59
|
+
onRequestEffectPreviews: vi.fn(),
|
|
60
|
+
onRequestPresetPreviews: vi.fn(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("FlatEffectsSection", () => {
|
|
65
|
+
it("places missing composite treatments in their effect family without a Styles section", () => {
|
|
66
|
+
const onCommit = vi.fn();
|
|
67
|
+
const props = sectionProps(neutralGrading(), onCommit);
|
|
68
|
+
const { host, root } = renderInto(<FlatEffectsSection {...props} />);
|
|
69
|
+
expect(host.textContent).not.toContain("Styles");
|
|
70
|
+
expect(props.onRequestPresetPreviews).not.toHaveBeenCalled();
|
|
71
|
+
const retro = host.querySelector<HTMLButtonElement>(
|
|
72
|
+
'[data-flat-effect-group="Retro & Glitch"]',
|
|
73
|
+
);
|
|
74
|
+
act(() => retro?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
75
|
+
expect(props.onRequestPresetPreviews).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(host.querySelector('[data-flat-effect-preset-preview="vhs-playback"]')).not.toBeNull();
|
|
77
|
+
const vhs = host.querySelector<HTMLButtonElement>('[data-flat-effect-preset="vhs-playback"]');
|
|
78
|
+
act(() => vhs?.dispatchEvent(new MouseEvent("pointerover", { bubbles: true })));
|
|
79
|
+
expect(props.onPreviewColorGrading.mock.calls.at(-1)?.[0]).toMatchObject({
|
|
80
|
+
preset: "vhs-playback",
|
|
81
|
+
effects: { tapeDamage: 0.82, scanlineCount: 0.17 },
|
|
82
|
+
});
|
|
83
|
+
expect(onCommit).not.toHaveBeenCalled();
|
|
84
|
+
act(() => vhs?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
85
|
+
expect(onCommit.mock.calls[0][0].preset).toBe("vhs-playback");
|
|
86
|
+
|
|
87
|
+
const print = host.querySelector<HTMLButtonElement>('[data-flat-effect-group="Print"]');
|
|
88
|
+
act(() => print?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
89
|
+
expect(host.querySelector('[data-flat-effect-preset="editorial-halftone"]')).not.toBeNull();
|
|
90
|
+
expect(host.querySelector('[data-flat-effect-preset="two-ink-print"]')).not.toBeNull();
|
|
91
|
+
act(() => root.unmount());
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("offers every shader master once in four intentional families", () => {
|
|
95
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(neutralGrading())} />);
|
|
96
|
+
const renderedKeys: Array<string | null> = [];
|
|
97
|
+
for (const label of ["Essentials", "Retro & Glitch", "Print", "Art"]) {
|
|
98
|
+
const tab = host.querySelector<HTMLButtonElement>(`[data-flat-effect-group="${label}"]`);
|
|
99
|
+
expect(tab).not.toBeNull();
|
|
100
|
+
act(() => tab?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
101
|
+
renderedKeys.push(
|
|
102
|
+
...Array.from(host.querySelectorAll("[data-flat-effect-option]"), (element) =>
|
|
103
|
+
element.getAttribute("data-flat-effect-option"),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
expect(renderedKeys.sort()).toEqual([...HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS].sort());
|
|
108
|
+
const capabilities = new Map(
|
|
109
|
+
getHfColorGradingCapabilities().effects.map((effect) => [effect.key, effect]),
|
|
110
|
+
);
|
|
111
|
+
for (const effect of EFFECT_SPECS) {
|
|
112
|
+
expect(Boolean(effect.palette)).toBe(capabilities.get(effect.key)?.supportsPalette);
|
|
113
|
+
}
|
|
114
|
+
expect(host.querySelectorAll('[data-flat-slider-track="true"]')).toHaveLength(0);
|
|
115
|
+
act(() => root.unmount());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("requests exact cards and auditions a chosen default without persisting", () => {
|
|
119
|
+
const props = sectionProps(neutralGrading());
|
|
120
|
+
const { host, root } = renderInto(<FlatEffectsSection {...props} />);
|
|
121
|
+
expect(props.onRequestEffectPreviews).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(props.onRequestEffectPreviews).toHaveBeenCalledWith(["blur", "pixelate", "bloom"]);
|
|
123
|
+
expect(host.querySelector('[data-flat-effect-preview="pixelate"]')).not.toBeNull();
|
|
124
|
+
const pixelate = host.querySelector<HTMLButtonElement>('[data-flat-effect-option="pixelate"]');
|
|
125
|
+
act(() => pixelate?.dispatchEvent(new MouseEvent("pointerover", { bubbles: true })));
|
|
126
|
+
expect(props.onPreviewColorGrading.mock.calls.at(-1)?.[0].effects.pixelate).toBe(
|
|
127
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.pixelate.pixelate,
|
|
128
|
+
);
|
|
129
|
+
expect(props.onPreviewColorGrading.mock.calls.at(-1)?.[1]).toEqual({
|
|
130
|
+
animatedPreview: { kind: "effects", id: "pixelate" },
|
|
131
|
+
});
|
|
132
|
+
expect(props.onCommitColorGrading).not.toHaveBeenCalled();
|
|
133
|
+
act(() => pixelate?.dispatchEvent(new MouseEvent("pointerout", { bubbles: true })));
|
|
134
|
+
expect(props.onPreviewColorGrading).toHaveBeenLastCalledWith(null);
|
|
135
|
+
act(() => root.unmount());
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("applies a useful canonical default when an effect is chosen", () => {
|
|
139
|
+
const onCommit = vi.fn();
|
|
140
|
+
const grading = { ...neutralGrading(), intensity: 0 };
|
|
141
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(grading, onCommit)} />);
|
|
142
|
+
const pixelate = host.querySelector<HTMLButtonElement>('[data-flat-effect-option="pixelate"]');
|
|
143
|
+
act(() => pixelate?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
144
|
+
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(onCommit.mock.calls[0][0].effects.pixelate).toBe(
|
|
146
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.pixelate.pixelate,
|
|
147
|
+
);
|
|
148
|
+
expect(onCommit.mock.calls[0][0].intensity).toBe(0);
|
|
149
|
+
act(() => root.unmount());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("uses a complete treatment card as a fresh starting point while preserving the LUT", () => {
|
|
153
|
+
const onCommit = vi.fn();
|
|
154
|
+
const base = neutralGrading();
|
|
155
|
+
const grading = {
|
|
156
|
+
...base,
|
|
157
|
+
adjust: { ...base.adjust, exposure: 0.4 },
|
|
158
|
+
effects: { ...base.effects, blur: 0.6 },
|
|
159
|
+
palette: ["#112233", "#ffffff"],
|
|
160
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.5 },
|
|
161
|
+
};
|
|
162
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(grading, onCommit)} />);
|
|
163
|
+
const toggle = host.querySelector<HTMLButtonElement>('[data-flat-effects-add-toggle="true"]');
|
|
164
|
+
act(() => toggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
165
|
+
const retro = host.querySelector<HTMLButtonElement>(
|
|
166
|
+
'[data-flat-effect-group="Retro & Glitch"]',
|
|
167
|
+
);
|
|
168
|
+
act(() => retro?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
169
|
+
const vhs = host.querySelector<HTMLButtonElement>('[data-flat-effect-preset="vhs-playback"]');
|
|
170
|
+
act(() => vhs?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
171
|
+
const committed = onCommit.mock.calls[0][0];
|
|
172
|
+
expect(committed).toMatchObject({
|
|
173
|
+
preset: "vhs-playback",
|
|
174
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.5 },
|
|
175
|
+
});
|
|
176
|
+
expect(committed.effects.tapeDamage).toBeGreaterThan(0);
|
|
177
|
+
expect(committed.effects.blur).toBe(0);
|
|
178
|
+
expect(committed.palette).toBeNull();
|
|
179
|
+
expect(committed.adjust.exposure).not.toBe(0.4);
|
|
180
|
+
act(() => root.unmount());
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("shows only the selected active effect controls and converts degrees to shader units", () => {
|
|
184
|
+
const onCommit = vi.fn();
|
|
185
|
+
const grading = normalizeHfColorGrading({
|
|
186
|
+
effects: HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.chromaticAberration,
|
|
187
|
+
});
|
|
188
|
+
if (!grading) throw new Error("expected chromatic grading");
|
|
189
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(grading, onCommit)} />);
|
|
190
|
+
expect(host.textContent).toContain("Angle");
|
|
191
|
+
const effect = host.querySelector('[data-flat-effect-editor="chromaticAberration"]');
|
|
192
|
+
if (!effect) throw new Error("expected chromatic effect");
|
|
193
|
+
const rows = effect.querySelectorAll('[data-flat-slider-track="true"]');
|
|
194
|
+
const angleTrack = rows[1] as HTMLElement | undefined;
|
|
195
|
+
if (!angleTrack) throw new Error("expected angle slider");
|
|
196
|
+
Object.defineProperty(angleTrack, "getBoundingClientRect", {
|
|
197
|
+
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
198
|
+
});
|
|
199
|
+
act(() => {
|
|
200
|
+
angleTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 50 }));
|
|
201
|
+
angleTrack.dispatchEvent(new MouseEvent("pointerup", { bubbles: true, clientX: 50 }));
|
|
202
|
+
});
|
|
203
|
+
expect(onCommit.mock.calls.at(-1)?.[0].effects.chromaticAngle).toBe(0.5);
|
|
204
|
+
act(() => root.unmount());
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("offers native ASCII styles, binary controls, and a bounded custom palette", () => {
|
|
208
|
+
const onCommit = vi.fn();
|
|
209
|
+
const grading = normalizeHfColorGrading({
|
|
210
|
+
effects: HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.ascii,
|
|
211
|
+
});
|
|
212
|
+
if (!grading) throw new Error("expected ASCII grading");
|
|
213
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(grading, onCommit)} />);
|
|
214
|
+
expect(
|
|
215
|
+
host.querySelector<HTMLSelectElement>('select[aria-label="Style"]')?.options,
|
|
216
|
+
).toHaveLength(8);
|
|
217
|
+
expect(host.querySelectorAll('[data-flat-toggle="true"]')).toHaveLength(2);
|
|
218
|
+
expect(host.querySelector('[data-flat-effect-editor="ascii"]')?.textContent).not.toContain(
|
|
219
|
+
"Mix",
|
|
220
|
+
);
|
|
221
|
+
expect(host.querySelectorAll("[data-flat-effects-palette-preset]")).toHaveLength(
|
|
222
|
+
HF_COLOR_GRADING_PALETTES.length,
|
|
223
|
+
);
|
|
224
|
+
const synthwave = host.querySelector<HTMLButtonElement>(
|
|
225
|
+
'[data-flat-effects-palette-preset="synthwave"]',
|
|
226
|
+
);
|
|
227
|
+
act(() => synthwave?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
228
|
+
expect(onCommit.mock.calls.at(-1)?.[0].palette).toEqual(
|
|
229
|
+
HF_COLOR_GRADING_PALETTES.find(({ id }) => id === "synthwave")?.colors,
|
|
230
|
+
);
|
|
231
|
+
const addPalette = host.querySelector<HTMLButtonElement>(
|
|
232
|
+
'[data-flat-effects-add-palette="true"]',
|
|
233
|
+
);
|
|
234
|
+
act(() => addPalette?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
235
|
+
expect(onCommit.mock.calls.at(-1)?.[0].palette).toEqual(["#000000", "#ffffff"]);
|
|
236
|
+
act(() => root.unmount());
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("resets the selected effect to its authored defaults", () => {
|
|
240
|
+
const onCommit = vi.fn();
|
|
241
|
+
const grading = normalizeHfColorGrading({
|
|
242
|
+
effects: { ...HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.digitalGlitch, digitalGlitch: 0.9 },
|
|
243
|
+
});
|
|
244
|
+
if (!grading) throw new Error("expected digital glitch grading");
|
|
245
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(grading, onCommit)} />);
|
|
246
|
+
expect(
|
|
247
|
+
host.querySelector('[data-flat-effect-editor="digitalGlitch"]')?.textContent,
|
|
248
|
+
).not.toContain("Mix");
|
|
249
|
+
const reset = host.querySelector<HTMLButtonElement>('[title="Reset Digital Glitch"]');
|
|
250
|
+
act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
251
|
+
expect(onCommit.mock.calls.at(-1)?.[0].effects).toMatchObject(
|
|
252
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.digitalGlitch,
|
|
253
|
+
);
|
|
254
|
+
act(() => root.unmount());
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("removes only the selected effect and preserves another active effect", () => {
|
|
258
|
+
const onCommit = vi.fn();
|
|
259
|
+
const grading = normalizeHfColorGrading({
|
|
260
|
+
effects: {
|
|
261
|
+
...HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.blur,
|
|
262
|
+
...HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.pixelate,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
if (!grading) throw new Error("expected multi-effect grading");
|
|
266
|
+
const { host, root } = renderInto(<FlatEffectsSection {...sectionProps(grading, onCommit)} />);
|
|
267
|
+
const remove = host.querySelector<HTMLButtonElement>('[title="Remove Blur"]');
|
|
268
|
+
act(() => remove?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
269
|
+
expect(onCommit.mock.calls.at(-1)?.[0].effects.blur).toBe(0);
|
|
270
|
+
expect(onCommit.mock.calls.at(-1)?.[0].effects.pixelate).toBe(
|
|
271
|
+
HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.pixelate.pixelate,
|
|
272
|
+
);
|
|
273
|
+
act(() => root.unmount());
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("closes the catalog when an already active effect is selected", () => {
|
|
277
|
+
const grading = normalizeHfColorGrading({
|
|
278
|
+
effects: HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS.blur,
|
|
279
|
+
});
|
|
280
|
+
if (!grading) throw new Error("expected blur grading");
|
|
281
|
+
const props = sectionProps(grading);
|
|
282
|
+
const { host, root } = renderInto(<FlatEffectsSection {...props} />);
|
|
283
|
+
expect(props.onRequestEffectPreviews).not.toHaveBeenCalled();
|
|
284
|
+
const toggle = host.querySelector<HTMLButtonElement>('[data-flat-effects-add-toggle="true"]');
|
|
285
|
+
act(() => toggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
286
|
+
expect(toggle?.getAttribute("aria-expanded")).toBe("true");
|
|
287
|
+
expect(props.onRequestEffectPreviews).toHaveBeenCalledWith(["blur", "pixelate", "bloom"]);
|
|
288
|
+
const blur = host.querySelector<HTMLButtonElement>('[data-flat-effect-option="blur"]');
|
|
289
|
+
act(() => blur?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
290
|
+
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
|
|
291
|
+
act(() => root.unmount());
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("requests only the visible family when the catalog tab changes", () => {
|
|
295
|
+
const props = sectionProps(neutralGrading());
|
|
296
|
+
const { host, root } = renderInto(<FlatEffectsSection {...props} />);
|
|
297
|
+
const art = host.querySelector<HTMLButtonElement>('[data-flat-effect-group="Art"]');
|
|
298
|
+
act(() => art?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
299
|
+
expect(props.onRequestEffectPreviews).toHaveBeenLastCalledWith([
|
|
300
|
+
"ascii",
|
|
301
|
+
"engraving",
|
|
302
|
+
"crosshatch",
|
|
303
|
+
"kuwahara",
|
|
304
|
+
]);
|
|
305
|
+
act(() => root.unmount());
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("FlatEffectsAccessory", () => {
|
|
310
|
+
it("counts active masters and resets only effects and palette", () => {
|
|
311
|
+
const base = neutralGrading();
|
|
312
|
+
const grading = {
|
|
313
|
+
...base,
|
|
314
|
+
adjust: { ...base.adjust, contrast: 0.2 },
|
|
315
|
+
effects: { ...base.effects, blur: 0.4, ascii: 1 },
|
|
316
|
+
palette: ["#112233", "#ffffff"],
|
|
317
|
+
};
|
|
318
|
+
expect(activeColorGradingEffectCount(grading)).toBe(2);
|
|
319
|
+
const onCommit = vi.fn();
|
|
320
|
+
const { host, root } = renderInto(
|
|
321
|
+
<FlatEffectsAccessory grading={grading} onCommitColorGrading={onCommit} />,
|
|
322
|
+
);
|
|
323
|
+
const reset = host.querySelector<HTMLButtonElement>('[data-flat-effects-reset="true"]');
|
|
324
|
+
act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
325
|
+
expect(onCommit.mock.calls[0][0].adjust.contrast).toBe(0.2);
|
|
326
|
+
expect(onCommit.mock.calls[0][0].effects.blur).toBe(0);
|
|
327
|
+
expect(onCommit.mock.calls[0][0].effects.ascii).toBe(0);
|
|
328
|
+
expect(onCommit.mock.calls[0][0].palette).toBeNull();
|
|
329
|
+
act(() => root.unmount());
|
|
330
|
+
});
|
|
331
|
+
});
|