@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,108 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { RegistryItem } from "@hyperframes/core/registry";
|
|
3
|
+
import { useBlockCatalog } from "../../hooks/useBlockCatalog";
|
|
4
|
+
import { Film, Plus } from "../../icons/SystemIcons";
|
|
5
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
6
|
+
import type { DomEditSelection } from "./domEditing";
|
|
7
|
+
import { FLAT_PREVIEW_GRID } from "./propertyPanelFlatPrimitives";
|
|
8
|
+
import type { ElementTiming } from "./propertyPanelFlatTimingDerivation";
|
|
9
|
+
|
|
10
|
+
export const MEDIA_TREATMENT_OVERLAY_TAG = "media-treatment-overlay";
|
|
11
|
+
|
|
12
|
+
export function filterMediaTreatmentOverlays(items: readonly RegistryItem[]): RegistryItem[] {
|
|
13
|
+
return items.filter(
|
|
14
|
+
(item) =>
|
|
15
|
+
item.type === "hyperframes:block" &&
|
|
16
|
+
item.tags?.includes(MEDIA_TREATMENT_OVERLAY_TAG) === true,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveMediaOverlayPlacement(
|
|
21
|
+
element: Pick<DomEditSelection, "dataAttributes" | "sourceFile">,
|
|
22
|
+
timing: Pick<ElementTiming, "start" | "duration">,
|
|
23
|
+
) {
|
|
24
|
+
const authoredTrack = Number.parseInt(element.dataAttributes["track-index"] ?? "", 10);
|
|
25
|
+
return {
|
|
26
|
+
start: timing.start,
|
|
27
|
+
...(timing.duration > 0 ? { duration: timing.duration } : {}),
|
|
28
|
+
...(Number.isFinite(authoredTrack) ? { track: authoredTrack + 1 } : {}),
|
|
29
|
+
compositionPath: element.sourceFile,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FlatOverlaysSection({
|
|
34
|
+
onAddOverlay,
|
|
35
|
+
}: {
|
|
36
|
+
onAddOverlay: (name: string) => Promise<void>;
|
|
37
|
+
}) {
|
|
38
|
+
const track = useTrackDesignInput();
|
|
39
|
+
const { blocks, loading, error } = useBlockCatalog();
|
|
40
|
+
const overlays = filterMediaTreatmentOverlays(blocks);
|
|
41
|
+
const [adding, setAdding] = useState<string | null>(null);
|
|
42
|
+
const [previewing, setPreviewing] = useState<string | null>(null);
|
|
43
|
+
|
|
44
|
+
if (loading) {
|
|
45
|
+
return <div className="py-4 text-center text-[10px] text-panel-text-4">Loading overlays…</div>;
|
|
46
|
+
}
|
|
47
|
+
if (error) {
|
|
48
|
+
return <div className="py-4 text-center text-[10px] text-red-300">{error}</div>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const busy = adding !== null;
|
|
52
|
+
return (
|
|
53
|
+
<div data-flat-overlays="true" className={FLAT_PREVIEW_GRID}>
|
|
54
|
+
{overlays.map((overlay) => (
|
|
55
|
+
<button
|
|
56
|
+
key={overlay.name}
|
|
57
|
+
type="button"
|
|
58
|
+
data-flat-overlay={overlay.name}
|
|
59
|
+
aria-label={`Add ${overlay.title}`}
|
|
60
|
+
disabled={busy}
|
|
61
|
+
title={overlay.description}
|
|
62
|
+
onPointerEnter={() => setPreviewing(overlay.name)}
|
|
63
|
+
onPointerLeave={() => setPreviewing(null)}
|
|
64
|
+
onFocus={() => setPreviewing(overlay.name)}
|
|
65
|
+
onBlur={() => setPreviewing(null)}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
track("button", `Add ${overlay.title}`);
|
|
68
|
+
setAdding(overlay.name);
|
|
69
|
+
void onAddOverlay(overlay.name).finally(() => setAdding(null));
|
|
70
|
+
}}
|
|
71
|
+
className="group min-w-0 overflow-hidden border border-panel-hairline bg-panel-bg-soft text-left transition-colors hover:border-panel-border-input hover:bg-panel-bg disabled:cursor-wait disabled:opacity-50"
|
|
72
|
+
>
|
|
73
|
+
<span className="relative flex aspect-video w-full items-center justify-center overflow-hidden bg-black/20">
|
|
74
|
+
{previewing === overlay.name && overlay.preview?.video ? (
|
|
75
|
+
<video
|
|
76
|
+
src={overlay.preview.video}
|
|
77
|
+
poster={overlay.preview.poster}
|
|
78
|
+
autoPlay
|
|
79
|
+
muted
|
|
80
|
+
loop
|
|
81
|
+
playsInline
|
|
82
|
+
className="block h-full w-full object-cover"
|
|
83
|
+
/>
|
|
84
|
+
) : overlay.preview?.poster ? (
|
|
85
|
+
<img
|
|
86
|
+
data-flat-overlay-preview={overlay.name}
|
|
87
|
+
src={overlay.preview.poster}
|
|
88
|
+
alt=""
|
|
89
|
+
draggable={false}
|
|
90
|
+
loading="lazy"
|
|
91
|
+
className="block h-full w-full object-cover"
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<Film size={15} className="text-panel-text-4" />
|
|
95
|
+
)}
|
|
96
|
+
<Plus
|
|
97
|
+
size={12}
|
|
98
|
+
className="absolute right-1.5 top-1.5 text-white opacity-70 drop-shadow group-hover:text-panel-accent group-hover:opacity-100"
|
|
99
|
+
/>
|
|
100
|
+
</span>
|
|
101
|
+
<span className="block truncate px-2 py-1.5 text-[10px] text-panel-text-2">
|
|
102
|
+
{adding === overlay.name ? "Adding…" : overlay.title}
|
|
103
|
+
</span>
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -1159,20 +1159,20 @@ describe("FlatSelectRow — label/value options", () => {
|
|
|
1159
1159
|
const { host, root } = renderInto(
|
|
1160
1160
|
<FlatSelectRow
|
|
1161
1161
|
label="Preset"
|
|
1162
|
-
value="
|
|
1162
|
+
value="clean-studio"
|
|
1163
1163
|
options={[
|
|
1164
1164
|
{ value: "neutral", label: "Neutral" },
|
|
1165
|
-
{ value: "
|
|
1166
|
-
{ value: "
|
|
1165
|
+
{ value: "clean-studio", label: "Clean Studio" },
|
|
1166
|
+
{ value: "bright-pop", label: "Bright Pop" },
|
|
1167
1167
|
]}
|
|
1168
1168
|
tier="explicitCustom"
|
|
1169
1169
|
onChange={vi.fn()}
|
|
1170
1170
|
/>,
|
|
1171
1171
|
);
|
|
1172
1172
|
const select = host.querySelector("select");
|
|
1173
|
-
expect(select?.value).toBe("
|
|
1173
|
+
expect(select?.value).toBe("clean-studio");
|
|
1174
1174
|
const options = Array.from(host.querySelectorAll("option")).map((o) => o.textContent);
|
|
1175
|
-
expect(options).toEqual(["Neutral", "
|
|
1175
|
+
expect(options).toEqual(["Neutral", "Clean Studio", "Bright Pop"]);
|
|
1176
1176
|
act(() => root.unmount());
|
|
1177
1177
|
});
|
|
1178
1178
|
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type PropertyValueTier,
|
|
9
9
|
} from "./propertyPanelValueTier";
|
|
10
10
|
|
|
11
|
+
export const FLAT_PREVIEW_GRID = "grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-1";
|
|
12
|
+
|
|
11
13
|
/* ------------------------------------------------------------------ */
|
|
12
14
|
/* FlatRow — single-column label/value property row */
|
|
13
15
|
/* ------------------------------------------------------------------ */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NormalizedHfColorGrading } from "@hyperframes/core/color-grading";
|
|
2
|
+
import type { ColorGradingPreviewOptions } from "./useColorGradingController";
|
|
3
|
+
|
|
4
|
+
export function presetPreviewHandlers({
|
|
5
|
+
id,
|
|
6
|
+
label,
|
|
7
|
+
resolve,
|
|
8
|
+
onPreview,
|
|
9
|
+
onCommit,
|
|
10
|
+
onTrack,
|
|
11
|
+
}: {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
resolve: () => NormalizedHfColorGrading;
|
|
15
|
+
onPreview: (
|
|
16
|
+
grading: NormalizedHfColorGrading | null,
|
|
17
|
+
options?: ColorGradingPreviewOptions,
|
|
18
|
+
) => void;
|
|
19
|
+
onCommit: (grading: NormalizedHfColorGrading) => void;
|
|
20
|
+
onTrack: (label: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
return {
|
|
23
|
+
title: `Preview ${label}`,
|
|
24
|
+
onPointerEnter: () =>
|
|
25
|
+
onPreview(resolve(), {
|
|
26
|
+
animatedPreview: { kind: "presets" as const, id },
|
|
27
|
+
}),
|
|
28
|
+
onPointerLeave: () => onPreview(null),
|
|
29
|
+
onFocus: () => onPreview(resolve()),
|
|
30
|
+
onBlur: () => onPreview(null),
|
|
31
|
+
onClick: () => {
|
|
32
|
+
onTrack(label);
|
|
33
|
+
onCommit(resolve());
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -19,6 +19,18 @@ export interface BackgroundRemovalResult {
|
|
|
19
19
|
provider?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface MediaOverlayPlacement {
|
|
23
|
+
start: number;
|
|
24
|
+
duration?: number;
|
|
25
|
+
track?: number;
|
|
26
|
+
compositionPath?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AddMediaOverlayHandler = (
|
|
30
|
+
blockName: string,
|
|
31
|
+
placement: MediaOverlayPlacement,
|
|
32
|
+
) => Promise<void>;
|
|
33
|
+
|
|
22
34
|
export interface PropertyPanelProps {
|
|
23
35
|
projectId: string;
|
|
24
36
|
projectDir: string | null;
|
|
@@ -69,6 +81,7 @@ export interface PropertyPanelProps {
|
|
|
69
81
|
onAskAgent: () => void;
|
|
70
82
|
onToggleElementHidden?: (elementKey: string, hidden: boolean) => void | Promise<void>;
|
|
71
83
|
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
84
|
+
onAddMediaOverlay?: AddMediaOverlayHandler;
|
|
72
85
|
fontAssets?: ImportedFontAsset[];
|
|
73
86
|
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
74
87
|
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
@@ -7,15 +7,15 @@ import { normalizeHfColorGrading } from "@hyperframes/core/color-grading";
|
|
|
7
7
|
import { useColorGradingController } from "./useColorGradingController";
|
|
8
8
|
import type { DomEditSelection } from "./domEditing";
|
|
9
9
|
|
|
10
|
-
function
|
|
11
|
-
const next = normalizeHfColorGrading({ preset: "
|
|
12
|
-
if (!next) throw new Error("expected
|
|
10
|
+
function brightPopGrading() {
|
|
11
|
+
const next = normalizeHfColorGrading({ preset: "bright-pop", intensity: 1 });
|
|
12
|
+
if (!next) throw new Error("expected bright-pop preset to normalize");
|
|
13
13
|
return next;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
const next = normalizeHfColorGrading({ preset: "
|
|
18
|
-
if (!next) throw new Error("expected
|
|
16
|
+
function cleanStudioGrading() {
|
|
17
|
+
const next = normalizeHfColorGrading({ preset: "clean-studio", intensity: 1 });
|
|
18
|
+
if (!next) throw new Error("expected clean-studio preset to normalize");
|
|
19
19
|
return next;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -60,14 +60,17 @@ function HookHost({
|
|
|
60
60
|
onState,
|
|
61
61
|
onSetAttributeLive,
|
|
62
62
|
element,
|
|
63
|
+
previewIframeRef,
|
|
63
64
|
}: {
|
|
64
65
|
onState: (state: ReturnType<typeof useColorGradingController>) => void;
|
|
65
66
|
onSetAttributeLive: (attr: string, value: string | null) => void;
|
|
66
67
|
element: DomEditSelection;
|
|
68
|
+
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
67
69
|
}) {
|
|
68
70
|
const state = useColorGradingController({
|
|
69
71
|
projectId: "proj",
|
|
70
72
|
element,
|
|
73
|
+
previewIframeRef,
|
|
71
74
|
onSetAttributeLive,
|
|
72
75
|
});
|
|
73
76
|
onState(state);
|
|
@@ -77,6 +80,7 @@ function HookHost({
|
|
|
77
80
|
function renderHook(
|
|
78
81
|
onSetAttributeLive: (attr: string, value: string | null) => void,
|
|
79
82
|
initialElement: DomEditSelection = makeElement(),
|
|
83
|
+
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>,
|
|
80
84
|
) {
|
|
81
85
|
const host = document.createElement("div");
|
|
82
86
|
document.body.append(host);
|
|
@@ -89,6 +93,7 @@ function renderHook(
|
|
|
89
93
|
onState: (s: ReturnType<typeof useColorGradingController>) => (latest = s),
|
|
90
94
|
onSetAttributeLive,
|
|
91
95
|
element,
|
|
96
|
+
previewIframeRef,
|
|
92
97
|
}),
|
|
93
98
|
);
|
|
94
99
|
});
|
|
@@ -107,6 +112,43 @@ function renderHook(
|
|
|
107
112
|
};
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
type PreviewWindow = Window & {
|
|
116
|
+
__hf?: {
|
|
117
|
+
colorGrading?: {
|
|
118
|
+
renderPreviews?: ReturnType<typeof vi.fn>;
|
|
119
|
+
startPreviewPlayback?: ReturnType<typeof vi.fn>;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
__player?: { play: ReturnType<typeof vi.fn> };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function createPreviewFrame() {
|
|
126
|
+
const iframe = document.body.appendChild(document.createElement("iframe"));
|
|
127
|
+
const contentWindow = iframe.contentWindow as PreviewWindow | null;
|
|
128
|
+
if (!contentWindow) throw new Error("expected iframe contentWindow");
|
|
129
|
+
return { contentWindow, iframe };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function flushPreviewRequest() {
|
|
133
|
+
act(() => vi.advanceTimersByTime(0));
|
|
134
|
+
await act(async () => {
|
|
135
|
+
await Promise.resolve();
|
|
136
|
+
await Promise.resolve();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function colorGradingMessages(calls: ReadonlyArray<ReadonlyArray<unknown>>) {
|
|
141
|
+
return calls
|
|
142
|
+
.map(([message]) => message)
|
|
143
|
+
.filter(
|
|
144
|
+
(message): message is { action: string; grading: unknown } =>
|
|
145
|
+
typeof message === "object" &&
|
|
146
|
+
message !== null &&
|
|
147
|
+
"action" in message &&
|
|
148
|
+
message.action === "set-color-grading",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
110
152
|
describe("useColorGradingController", () => {
|
|
111
153
|
it("starts with the neutral (inactive) grading and idle compare state", () => {
|
|
112
154
|
const { root, getState } = renderHook(vi.fn());
|
|
@@ -115,14 +157,85 @@ describe("useColorGradingController", () => {
|
|
|
115
157
|
act(() => root.unmount());
|
|
116
158
|
});
|
|
117
159
|
|
|
160
|
+
it("requests one exact preset batch from the selected media runtime", async () => {
|
|
161
|
+
vi.useFakeTimers();
|
|
162
|
+
const devicePixelRatio = vi.spyOn(window, "devicePixelRatio", "get").mockReturnValue(2);
|
|
163
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
164
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
165
|
+
width: 160,
|
|
166
|
+
height: 90,
|
|
167
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,bright" }],
|
|
168
|
+
});
|
|
169
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
170
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
171
|
+
|
|
172
|
+
act(() => getState().requestPresetPreviews());
|
|
173
|
+
await flushPreviewRequest();
|
|
174
|
+
|
|
175
|
+
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(renderPreviews.mock.calls[0]?.[1]).toHaveLength(18);
|
|
177
|
+
expect(renderPreviews.mock.calls[0]?.[2]).toEqual({ maxDimension: 320 });
|
|
178
|
+
expect(getState().presetPreviews).toEqual({
|
|
179
|
+
status: "ready",
|
|
180
|
+
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
181
|
+
width: 160,
|
|
182
|
+
height: 90,
|
|
183
|
+
});
|
|
184
|
+
act(() => root.unmount());
|
|
185
|
+
devicePixelRatio.mockRestore();
|
|
186
|
+
vi.useRealTimers();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("requests exact effect families and retains earlier family images", async () => {
|
|
190
|
+
vi.useFakeTimers();
|
|
191
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
192
|
+
const renderPreviews = vi
|
|
193
|
+
.fn()
|
|
194
|
+
.mockImplementation(async (_target: unknown, candidates: Array<{ id: string }>) => ({
|
|
195
|
+
width: 160,
|
|
196
|
+
height: 90,
|
|
197
|
+
images: candidates.map(({ id }) => ({ id, dataUrl: `data:image/png;base64,${id}` })),
|
|
198
|
+
}));
|
|
199
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
200
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
201
|
+
|
|
202
|
+
act(() => getState().requestEffectPreviews(["blur", "pixelate", "bloom"]));
|
|
203
|
+
await flushPreviewRequest();
|
|
204
|
+
|
|
205
|
+
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
206
|
+
expect(renderPreviews.mock.calls[0]?.[1].map(({ id }: { id: string }) => id)).toEqual([
|
|
207
|
+
"blur",
|
|
208
|
+
"pixelate",
|
|
209
|
+
"bloom",
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
act(() => getState().requestEffectPreviews(["kuwahara"]));
|
|
213
|
+
await flushPreviewRequest();
|
|
214
|
+
|
|
215
|
+
expect(renderPreviews).toHaveBeenCalledTimes(2);
|
|
216
|
+
expect(getState().effectPreviews).toEqual({
|
|
217
|
+
status: "ready",
|
|
218
|
+
images: {
|
|
219
|
+
blur: "data:image/png;base64,blur",
|
|
220
|
+
pixelate: "data:image/png;base64,pixelate",
|
|
221
|
+
bloom: "data:image/png;base64,bloom",
|
|
222
|
+
kuwahara: "data:image/png;base64,kuwahara",
|
|
223
|
+
},
|
|
224
|
+
width: 160,
|
|
225
|
+
height: 90,
|
|
226
|
+
});
|
|
227
|
+
act(() => root.unmount());
|
|
228
|
+
vi.useRealTimers();
|
|
229
|
+
});
|
|
230
|
+
|
|
118
231
|
it("commitColorGrading updates grading state synchronously and schedules a debounced persist", async () => {
|
|
119
232
|
vi.useFakeTimers();
|
|
120
233
|
const onSetAttributeLive = vi.fn();
|
|
121
234
|
const { root, getState } = renderHook(onSetAttributeLive);
|
|
122
235
|
act(() => {
|
|
123
|
-
getState().commitColorGrading(
|
|
236
|
+
getState().commitColorGrading(brightPopGrading());
|
|
124
237
|
});
|
|
125
|
-
expect(getState().grading.preset).toBe("
|
|
238
|
+
expect(getState().grading.preset).toBe("bright-pop");
|
|
126
239
|
expect(onSetAttributeLive).not.toHaveBeenCalled();
|
|
127
240
|
act(() => {
|
|
128
241
|
vi.advanceTimersByTime(400);
|
|
@@ -130,7 +243,87 @@ describe("useColorGradingController", () => {
|
|
|
130
243
|
expect(onSetAttributeLive).toHaveBeenCalledTimes(1);
|
|
131
244
|
const [attr, value] = onSetAttributeLive.mock.calls[0] as [string, string];
|
|
132
245
|
expect(attr).toBe("color-grading");
|
|
133
|
-
expect(value).toContain("
|
|
246
|
+
expect(value).toContain("bright-pop");
|
|
247
|
+
act(() => root.unmount());
|
|
248
|
+
vi.useRealTimers();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("previews through the runtime only and restores the committed grade without persisting", () => {
|
|
252
|
+
vi.useFakeTimers();
|
|
253
|
+
const onSetAttributeLive = vi.fn();
|
|
254
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
255
|
+
const postMessage = vi.spyOn(contentWindow, "postMessage");
|
|
256
|
+
const { root, getState } = renderHook(onSetAttributeLive, makeElement(), {
|
|
257
|
+
current: iframe,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
act(() => getState().previewColorGrading(brightPopGrading()));
|
|
261
|
+
act(() => getState().previewColorGrading(null));
|
|
262
|
+
const gradingMessages = colorGradingMessages(postMessage.mock.calls);
|
|
263
|
+
expect(gradingMessages).toHaveLength(2);
|
|
264
|
+
expect(gradingMessages[0]?.grading).toMatchObject({ preset: "bright-pop" });
|
|
265
|
+
expect(gradingMessages[1]?.grading).toBeNull();
|
|
266
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
267
|
+
act(() => vi.advanceTimersByTime(500));
|
|
268
|
+
expect(onSetAttributeLive).not.toHaveBeenCalled();
|
|
269
|
+
|
|
270
|
+
act(() => root.unmount());
|
|
271
|
+
vi.useRealTimers();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("animates only the selected video card without starting the project player", async () => {
|
|
275
|
+
vi.useFakeTimers();
|
|
276
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
277
|
+
const stopPlayback = vi.fn();
|
|
278
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
279
|
+
width: 320,
|
|
280
|
+
height: 180,
|
|
281
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,animated" }],
|
|
282
|
+
});
|
|
283
|
+
const startPreviewPlayback = vi.fn(() => stopPlayback);
|
|
284
|
+
contentWindow.__hf = { colorGrading: { renderPreviews, startPreviewPlayback } };
|
|
285
|
+
contentWindow.__player = { play: vi.fn() };
|
|
286
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
287
|
+
|
|
288
|
+
act(() =>
|
|
289
|
+
getState().previewColorGrading(brightPopGrading(), {
|
|
290
|
+
animatedPreview: { kind: "presets", id: "bright-pop" },
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
await act(async () => {
|
|
294
|
+
await Promise.resolve();
|
|
295
|
+
await Promise.resolve();
|
|
296
|
+
});
|
|
297
|
+
expect(startPreviewPlayback).toHaveBeenCalledTimes(1);
|
|
298
|
+
expect(renderPreviews).toHaveBeenCalledWith(
|
|
299
|
+
expect.anything(),
|
|
300
|
+
[{ id: "bright-pop", grading: expect.objectContaining({ preset: "bright-pop" }) }],
|
|
301
|
+
{ maxDimension: 160, useMediaTime: true },
|
|
302
|
+
);
|
|
303
|
+
expect(contentWindow.__player.play).not.toHaveBeenCalled();
|
|
304
|
+
expect(getState().presetPreviews.images["bright-pop"]).toBe("data:image/png;base64,animated");
|
|
305
|
+
|
|
306
|
+
act(() => getState().previewColorGrading(null));
|
|
307
|
+
expect(stopPlayback).toHaveBeenCalledTimes(1);
|
|
308
|
+
|
|
309
|
+
act(() => root.unmount());
|
|
310
|
+
vi.useRealTimers();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("restores a just-committed look even before React renders the new state", () => {
|
|
314
|
+
vi.useFakeTimers();
|
|
315
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
316
|
+
const postMessage = vi.spyOn(contentWindow, "postMessage");
|
|
317
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
318
|
+
const brightPop = brightPopGrading();
|
|
319
|
+
|
|
320
|
+
act(() => {
|
|
321
|
+
getState().commitColorGrading(brightPop);
|
|
322
|
+
getState().previewColorGrading(null);
|
|
323
|
+
});
|
|
324
|
+
const gradingMessages = colorGradingMessages(postMessage.mock.calls);
|
|
325
|
+
expect(gradingMessages.at(-1)?.grading).toMatchObject({ preset: "bright-pop" });
|
|
326
|
+
|
|
134
327
|
act(() => root.unmount());
|
|
135
328
|
vi.useRealTimers();
|
|
136
329
|
});
|
|
@@ -149,9 +342,9 @@ describe("useColorGradingController", () => {
|
|
|
149
342
|
);
|
|
150
343
|
const { root, getState } = renderHook(onSetAttributeLive);
|
|
151
344
|
act(() => {
|
|
152
|
-
getState().commitColorGrading(
|
|
345
|
+
getState().commitColorGrading(brightPopGrading());
|
|
153
346
|
});
|
|
154
|
-
expect(getState().grading.preset).toBe("
|
|
347
|
+
expect(getState().grading.preset).toBe("bright-pop");
|
|
155
348
|
act(() => {
|
|
156
349
|
vi.advanceTimersByTime(400);
|
|
157
350
|
});
|
|
@@ -170,9 +363,9 @@ describe("useColorGradingController", () => {
|
|
|
170
363
|
const onSetAttributeLive = vi.fn().mockRejectedValue(new Error("disk full"));
|
|
171
364
|
const { root, getState } = renderHook(onSetAttributeLive);
|
|
172
365
|
act(() => {
|
|
173
|
-
getState().commitColorGrading(
|
|
366
|
+
getState().commitColorGrading(brightPopGrading());
|
|
174
367
|
});
|
|
175
|
-
expect(getState().grading.preset).toBe("
|
|
368
|
+
expect(getState().grading.preset).toBe("bright-pop");
|
|
176
369
|
act(() => {
|
|
177
370
|
vi.advanceTimersByTime(400);
|
|
178
371
|
});
|
|
@@ -182,7 +375,7 @@ describe("useColorGradingController", () => {
|
|
|
182
375
|
await Promise.resolve();
|
|
183
376
|
});
|
|
184
377
|
// Reverted to "neutral" (the last confirmed-good value, from before this
|
|
185
|
-
// commit) instead of permanently showing "
|
|
378
|
+
// commit) instead of permanently showing "bright-pop" as if it had saved.
|
|
186
379
|
expect(getState().grading.preset).toBe("neutral");
|
|
187
380
|
expect(getState().runtimeStatus.state).toBe("unavailable");
|
|
188
381
|
act(() => root.unmount());
|
|
@@ -206,7 +399,7 @@ describe("useColorGradingController", () => {
|
|
|
206
399
|
makeElement({ id: "s1-bg" }),
|
|
207
400
|
);
|
|
208
401
|
act(() => {
|
|
209
|
-
getState().commitColorGrading(
|
|
402
|
+
getState().commitColorGrading(brightPopGrading());
|
|
210
403
|
});
|
|
211
404
|
// Let the debounce fire while still on s1-bg — the persist call is now
|
|
212
405
|
// genuinely in flight (its promise won't settle until resolveA() below).
|
|
@@ -243,7 +436,7 @@ describe("useColorGradingController", () => {
|
|
|
243
436
|
let capturedOnSettledA: ((ok: boolean) => void) | undefined;
|
|
244
437
|
const onSetAttributeLive = vi
|
|
245
438
|
.fn()
|
|
246
|
-
// Edit A (
|
|
439
|
+
// Edit A (bright-pop): captures its onSettled and never resolves until
|
|
247
440
|
// resolveA() is called below — simulates a slow persist.
|
|
248
441
|
.mockImplementationOnce(
|
|
249
442
|
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
@@ -253,7 +446,7 @@ describe("useColorGradingController", () => {
|
|
|
253
446
|
});
|
|
254
447
|
},
|
|
255
448
|
)
|
|
256
|
-
// Edit B (
|
|
449
|
+
// Edit B (clean-studio): settles immediately and successfully.
|
|
257
450
|
.mockImplementationOnce(
|
|
258
451
|
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
259
452
|
onSettled?.(true);
|
|
@@ -263,7 +456,7 @@ describe("useColorGradingController", () => {
|
|
|
263
456
|
const { root, getState } = renderHook(onSetAttributeLive);
|
|
264
457
|
|
|
265
458
|
act(() => {
|
|
266
|
-
getState().commitColorGrading(
|
|
459
|
+
getState().commitColorGrading(brightPopGrading());
|
|
267
460
|
});
|
|
268
461
|
act(() => {
|
|
269
462
|
vi.advanceTimersByTime(400);
|
|
@@ -272,13 +465,13 @@ describe("useColorGradingController", () => {
|
|
|
272
465
|
|
|
273
466
|
// B commits on the SAME element before A's persist has settled.
|
|
274
467
|
act(() => {
|
|
275
|
-
getState().commitColorGrading(
|
|
468
|
+
getState().commitColorGrading(cleanStudioGrading());
|
|
276
469
|
});
|
|
277
470
|
act(() => {
|
|
278
471
|
vi.advanceTimersByTime(400);
|
|
279
472
|
});
|
|
280
473
|
expect(onSetAttributeLive).toHaveBeenCalledTimes(2); // B's persist has already settled (mock resolves sync)
|
|
281
|
-
expect(getState().grading.preset).toBe("
|
|
474
|
+
expect(getState().grading.preset).toBe("clean-studio");
|
|
282
475
|
|
|
283
476
|
// NOW A's stale persist finally settles as a FAILURE — must not revert
|
|
284
477
|
// `grading` (which now correctly shows B's newer edit) back to the
|
|
@@ -292,20 +485,32 @@ describe("useColorGradingController", () => {
|
|
|
292
485
|
await Promise.resolve();
|
|
293
486
|
await Promise.resolve();
|
|
294
487
|
});
|
|
295
|
-
expect(getState().grading.preset).toBe("
|
|
488
|
+
expect(getState().grading.preset).toBe("clean-studio");
|
|
296
489
|
act(() => root.unmount());
|
|
297
490
|
vi.useRealTimers();
|
|
298
491
|
});
|
|
299
492
|
|
|
300
|
-
it("resetGrading
|
|
493
|
+
it("resetGrading resets Grade fields without clearing Effects or Palette", () => {
|
|
301
494
|
const { root, getState } = renderHook(vi.fn());
|
|
495
|
+
const grading = normalizeHfColorGrading({
|
|
496
|
+
preset: "bright-pop",
|
|
497
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
|
|
498
|
+
effects: { pixelate: 0.5 },
|
|
499
|
+
palette: ["#112233", "#ffffff"],
|
|
500
|
+
});
|
|
501
|
+
if (!grading) throw new Error("expected grading to normalize");
|
|
302
502
|
act(() => {
|
|
303
|
-
getState().commitColorGrading(
|
|
503
|
+
getState().commitColorGrading(grading);
|
|
304
504
|
});
|
|
305
505
|
act(() => {
|
|
306
506
|
getState().resetGrading();
|
|
307
507
|
});
|
|
308
|
-
expect(getState().grading
|
|
508
|
+
expect(getState().grading).toMatchObject({
|
|
509
|
+
preset: "neutral",
|
|
510
|
+
lut: null,
|
|
511
|
+
effects: { pixelate: 0.5 },
|
|
512
|
+
palette: ["#112233", "#ffffff"],
|
|
513
|
+
});
|
|
309
514
|
act(() => root.unmount());
|
|
310
515
|
});
|
|
311
516
|
|
|
@@ -315,9 +520,9 @@ describe("useColorGradingController", () => {
|
|
|
315
520
|
makeElement({ id: "s1-bg" }),
|
|
316
521
|
);
|
|
317
522
|
act(() => {
|
|
318
|
-
getState().commitColorGrading(
|
|
523
|
+
getState().commitColorGrading(brightPopGrading());
|
|
319
524
|
});
|
|
320
|
-
expect(getState().grading.preset).toBe("
|
|
525
|
+
expect(getState().grading.preset).toBe("bright-pop");
|
|
321
526
|
// A different element, with no persisted grading of its own — without a
|
|
322
527
|
// reset, this hook (unlike the legacy component it was extracted from,
|
|
323
528
|
// which remounts via a `key={selectionIdentityKey}`) would keep showing
|
|
@@ -337,9 +542,9 @@ describe("useColorGradingController", () => {
|
|
|
337
542
|
makeElement({ id: "bg", sourceFile: "index.html" }),
|
|
338
543
|
);
|
|
339
544
|
act(() => {
|
|
340
|
-
getState().commitColorGrading(
|
|
545
|
+
getState().commitColorGrading(brightPopGrading());
|
|
341
546
|
});
|
|
342
|
-
expect(getState().grading.preset).toBe("
|
|
547
|
+
expect(getState().grading.preset).toBe("bright-pop");
|
|
343
548
|
rerenderWithElement(makeElement({ id: "bg", sourceFile: "sub-comp.html" }));
|
|
344
549
|
expect(getState().grading.preset).toBe("neutral");
|
|
345
550
|
act(() => root.unmount());
|
|
@@ -353,7 +558,7 @@ describe("useColorGradingController", () => {
|
|
|
353
558
|
makeElement({ id: "s1-bg" }),
|
|
354
559
|
);
|
|
355
560
|
act(() => {
|
|
356
|
-
getState().commitColorGrading(
|
|
561
|
+
getState().commitColorGrading(brightPopGrading());
|
|
357
562
|
});
|
|
358
563
|
// Switch selection before the 350ms debounce fires — the in-flight edit
|
|
359
564
|
// must be written immediately (targeting the OUTGOING element's own
|
|
@@ -366,7 +571,7 @@ describe("useColorGradingController", () => {
|
|
|
366
571
|
expect(onSetAttributeLive).toHaveBeenCalledTimes(1);
|
|
367
572
|
const [attr, value] = onSetAttributeLive.mock.calls[0] as [string, string];
|
|
368
573
|
expect(attr).toBe("color-grading");
|
|
369
|
-
expect(value).toContain("
|
|
574
|
+
expect(value).toContain("bright-pop");
|
|
370
575
|
// And it must not ALSO fire again once the (now-cleared) original timer
|
|
371
576
|
// window would have elapsed.
|
|
372
577
|
act(() => {
|