@hyperframes/studio 0.7.72 → 0.7.73

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.
Files changed (35) hide show
  1. package/dist/assets/{hyperframes-player-Bjf2HPzR.js → hyperframes-player-DGlzhf_s.js} +1 -1
  2. package/dist/assets/index-B37rWXo4.css +1 -0
  3. package/dist/assets/{index-Ct_pxETK.js → index-CbVTOJ-E.js} +1 -1
  4. package/dist/assets/{index-Ch1hbJ3e.js → index-CcA5WVJv.js} +1 -1
  5. package/dist/assets/index-DXcLI2wu.js +428 -0
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +4203 -2281
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/components/editor/PropertyPanelFlat.tsx +1 -0
  11. package/src/components/editor/colorGradingFrameAnalysis.test.ts +101 -0
  12. package/src/components/editor/colorGradingFrameAnalysis.ts +203 -0
  13. package/src/components/editor/propertyPanelColorCurveGraph.tsx +549 -0
  14. package/src/components/editor/propertyPanelColorCurves.test.tsx +158 -0
  15. package/src/components/editor/propertyPanelColorCurves.tsx +185 -0
  16. package/src/components/editor/propertyPanelColorGradingControls.tsx +115 -77
  17. package/src/components/editor/propertyPanelColorScopes.tsx +258 -0
  18. package/src/components/editor/propertyPanelColorSecondary.test.tsx +181 -0
  19. package/src/components/editor/propertyPanelColorSecondary.tsx +448 -0
  20. package/src/components/editor/propertyPanelColorWheels.test.tsx +119 -0
  21. package/src/components/editor/propertyPanelColorWheels.tsx +334 -0
  22. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +109 -0
  23. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +168 -3
  24. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +184 -243
  25. package/src/components/editor/propertyPanelGradingNumberField.test.tsx +79 -0
  26. package/src/components/editor/propertyPanelGradingNumberField.tsx +116 -0
  27. package/src/components/editor/useColorGradingController.test.ts +133 -17
  28. package/src/components/editor/useColorGradingController.ts +11 -2
  29. package/src/components/editor/useColorGradingPreviews.ts +54 -11
  30. package/src/components/editor/useColorGradingScopes.test.tsx +143 -0
  31. package/src/components/editor/useColorGradingScopes.ts +62 -0
  32. package/src/components/editor/useInspectorGestureTransaction.ts +30 -1
  33. package/src/icons/SystemIcons.tsx +4 -0
  34. package/dist/assets/index-BgZMle9I.css +0 -1
  35. package/dist/assets/index-DCyWLHnx.js +0 -428
@@ -0,0 +1,185 @@
1
+ import { useState } from "react";
2
+ import type { HfColorCurvePoint, HfHueCurvePoint } from "@hyperframes/core/color-grading";
3
+ import { RotateCcw } from "../../icons/SystemIcons";
4
+ import {
5
+ CurveGraph,
6
+ formatPointValue,
7
+ movePoint,
8
+ pointsFor,
9
+ RGB_IDENTITY,
10
+ TABS,
11
+ type ColorCurveValues,
12
+ type CurveTab,
13
+ withPoints,
14
+ } from "./propertyPanelColorCurveGraph";
15
+ import { GradingNumberField } from "./propertyPanelGradingNumberField";
16
+ import { useInspectorGestureDraft } from "./useInspectorGestureTransaction";
17
+
18
+ export type { ColorCurveValues } from "./propertyPanelColorCurveGraph";
19
+
20
+ export function ColorCurves({
21
+ value,
22
+ disabled,
23
+ onPreview,
24
+ onCommit,
25
+ }: {
26
+ value: ColorCurveValues;
27
+ disabled?: boolean;
28
+ onPreview: (value: ColorCurveValues) => void;
29
+ onCommit: (value: ColorCurveValues) => void;
30
+ }) {
31
+ const [activeKey, setActiveKey] = useState<CurveTab["key"]>("master");
32
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
33
+ const { draft, setDraft, transaction } = useInspectorGestureDraft({
34
+ sourceValue: value,
35
+ onPreview,
36
+ onCommit,
37
+ });
38
+ const tab = TABS.find((candidate) => candidate.key === activeKey) ?? TABS[0];
39
+ if (!tab) throw new Error("Color curve tabs are unavailable");
40
+ const points = pointsFor(draft, tab);
41
+
42
+ const previewPoints = (
43
+ nextPoints: readonly (HfColorCurvePoint | HfHueCurvePoint)[],
44
+ nextSelectedIndex: number,
45
+ ) => {
46
+ setSelectedIndex(nextSelectedIndex);
47
+ transaction.preview(withPoints(draft, tab, nextPoints));
48
+ };
49
+ const resetActive = () => {
50
+ transaction.cancel();
51
+ const next = withPoints(draft, tab, tab.kind === "rgb" ? RGB_IDENTITY : []);
52
+ setDraft(next);
53
+ setSelectedIndex(null);
54
+ onCommit(next);
55
+ };
56
+ const deleteSelected = () => {
57
+ if (selectedIndex === null) return;
58
+ if (tab.kind === "rgb" && (selectedIndex === 0 || selectedIndex === points.length - 1)) return;
59
+ transaction.cancel();
60
+ const nextPoints =
61
+ tab.kind === "hue" && points.length <= 3
62
+ ? []
63
+ : points.filter((_, index) => index !== selectedIndex);
64
+ const next = withPoints(draft, tab, nextPoints);
65
+ setDraft(next);
66
+ setSelectedIndex(null);
67
+ onCommit(next);
68
+ };
69
+ const updateSelected = (axis: "input" | "output", rawValue: number) => {
70
+ if (selectedIndex === null || !Number.isFinite(rawValue)) return;
71
+ const point = points[selectedIndex];
72
+ if (!point) return;
73
+ const moved = movePoint(
74
+ points,
75
+ selectedIndex,
76
+ axis === "input" ? rawValue : point[0],
77
+ axis === "output" ? rawValue : point[1],
78
+ tab,
79
+ );
80
+ previewPoints(moved.points, moved.selected);
81
+ };
82
+ const selectedPoint = selectedIndex === null ? null : points[selectedIndex];
83
+ const endpointSelected =
84
+ tab.kind === "rgb" &&
85
+ selectedIndex !== null &&
86
+ (selectedIndex === 0 || selectedIndex === points.length - 1);
87
+
88
+ return (
89
+ <div data-color-curves="true" className="space-y-2">
90
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-panel-hairline">
91
+ {TABS.map((candidate) => (
92
+ <button
93
+ key={candidate.key}
94
+ type="button"
95
+ data-color-curve-tab={candidate.key}
96
+ aria-pressed={candidate.key === tab.key}
97
+ disabled={disabled}
98
+ onClick={() => {
99
+ transaction.cancel();
100
+ setActiveKey(candidate.key);
101
+ setSelectedIndex(null);
102
+ }}
103
+ className={`border-b-2 px-0.5 py-1 text-[9px] ${
104
+ candidate.key === tab.key
105
+ ? "border-panel-accent text-panel-text-1"
106
+ : "border-transparent text-panel-text-4 hover:text-panel-text-2"
107
+ }`}
108
+ >
109
+ {candidate.label}
110
+ </button>
111
+ ))}
112
+ </div>
113
+ <CurveGraph
114
+ tab={tab}
115
+ points={points}
116
+ selectedIndex={selectedIndex}
117
+ disabled={disabled}
118
+ onBegin={transaction.begin}
119
+ onPreview={previewPoints}
120
+ onSelect={setSelectedIndex}
121
+ onDelete={deleteSelected}
122
+ onSettle={transaction.settle}
123
+ onCancel={transaction.cancel}
124
+ />
125
+ <div className="flex min-h-7 items-end gap-2">
126
+ {selectedPoint ? (
127
+ <>
128
+ <GradingNumberField
129
+ label={tab.kind === "rgb" ? "Input" : "Hue"}
130
+ ariaLabel={`Curve point ${tab.kind === "rgb" ? "input" : "hue"}`}
131
+ value={formatPointValue(selectedPoint[0], tab, "input")}
132
+ min={0}
133
+ max={tab.kind === "rgb" ? 1 : 359.999}
134
+ disabled={disabled || endpointSelected}
135
+ labelClassName="min-w-0 flex-1"
136
+ labelTextClassName="block text-[8px] uppercase text-panel-text-5"
137
+ inputClassName="block w-full"
138
+ onBegin={transaction.begin}
139
+ onPreview={(next) => updateSelected("input", next)}
140
+ onSettle={transaction.settle}
141
+ onCancel={transaction.cancel}
142
+ />
143
+ <GradingNumberField
144
+ label="Output"
145
+ ariaLabel="Curve point output"
146
+ value={formatPointValue(selectedPoint[1], tab, "output")}
147
+ min={tab.min}
148
+ max={tab.max}
149
+ disabled={disabled}
150
+ labelClassName="min-w-0 flex-1"
151
+ labelTextClassName="block text-[8px] uppercase text-panel-text-5"
152
+ inputClassName="block w-full"
153
+ onBegin={transaction.begin}
154
+ onPreview={(next) => updateSelected("output", next)}
155
+ onSettle={transaction.settle}
156
+ onCancel={transaction.cancel}
157
+ />
158
+ <button
159
+ type="button"
160
+ disabled={disabled || endpointSelected}
161
+ onClick={deleteSelected}
162
+ className="pb-0.5 text-[9px] text-panel-text-4 hover:text-panel-text-1 disabled:opacity-30"
163
+ >
164
+ Delete
165
+ </button>
166
+ </>
167
+ ) : (
168
+ <span className="flex-1 text-[9px] text-panel-text-5">
169
+ Click the graph or press Enter to add a point
170
+ </span>
171
+ )}
172
+ <button
173
+ type="button"
174
+ aria-label={`Reset ${tab.label} curve`}
175
+ title={`Reset ${tab.label} curve`}
176
+ disabled={disabled}
177
+ onClick={resetActive}
178
+ className="pb-0.5 text-panel-text-4 hover:text-panel-text-1 disabled:opacity-40"
179
+ >
180
+ <RotateCcw size={11} />
181
+ </button>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
@@ -15,7 +15,7 @@ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
15
15
 
16
16
  const LUT_UPLOAD_DIR = "assets/luts";
17
17
 
18
- const ADJUST_SLIDERS: Array<{
18
+ export const COLOR_GRADING_ADJUST_SLIDERS: Array<{
19
19
  key: HfColorGradingAdjustKey;
20
20
  label: string;
21
21
  min: number;
@@ -60,7 +60,7 @@ const ADJUST_SLIDERS: Array<{
60
60
  { key: "saturation", label: "Saturation", min: -100, max: 100, step: 1, scale: 100, suffix: "%" },
61
61
  ];
62
62
 
63
- const DETAIL_SLIDERS: Array<{
63
+ export const COLOR_GRADING_DETAIL_SLIDERS: Array<{
64
64
  key: HfColorGradingDetailKey;
65
65
  label: string;
66
66
  min: number;
@@ -123,7 +123,7 @@ const DETAIL_SLIDERS: Array<{
123
123
  },
124
124
  ];
125
125
 
126
- type DetailSlider = (typeof DETAIL_SLIDERS)[number];
126
+ type DetailSlider = (typeof COLOR_GRADING_DETAIL_SLIDERS)[number];
127
127
  type SliderSettings = {
128
128
  active?: boolean;
129
129
  label: string;
@@ -143,28 +143,111 @@ const EFFECT_SLIDERS: Array<{
143
143
  { key: "pixelate", label: "Pixelate", min: 0, max: 100, step: 1, scale: 100, suffix: "%" },
144
144
  ];
145
145
 
146
- const AMOUNT_DETAIL_SLIDERS = DETAIL_SLIDERS.filter(
146
+ const AMOUNT_DETAIL_SLIDERS = COLOR_GRADING_DETAIL_SLIDERS.filter(
147
147
  (slider) => slider.key === "vignette" || slider.key === "grain",
148
148
  );
149
- const VIGNETTE_TUNE_SLIDERS = DETAIL_SLIDERS.filter(
149
+ export const VIGNETTE_TUNE_SLIDERS = COLOR_GRADING_DETAIL_SLIDERS.filter(
150
150
  (slider) =>
151
151
  slider.key === "vignetteMidpoint" ||
152
152
  slider.key === "vignetteRoundness" ||
153
153
  slider.key === "vignetteFeather",
154
154
  );
155
- const GRAIN_TUNE_SLIDERS = DETAIL_SLIDERS.filter(
155
+ export const GRAIN_TUNE_SLIDERS = COLOR_GRADING_DETAIL_SLIDERS.filter(
156
156
  (slider) => slider.key === "grainSize" || slider.key === "grainRoughness",
157
157
  );
158
158
 
159
- function normalizedDefaultValue(slider: { defaultValue?: number; scale: number }): number {
159
+ export function normalizedColorGradingDefault(slider: {
160
+ defaultValue?: number;
161
+ scale: number;
162
+ }): number {
160
163
  return (slider.defaultValue ?? 0) / slider.scale;
161
164
  }
162
165
 
163
- function visibleIntensity(grading: NormalizedHfColorGrading): number {
166
+ export function visibleColorGradingIntensity(grading: NormalizedHfColorGrading): number {
164
167
  // Earlier drafts could persist 0% strength; the next manual edit should revive visible grading.
165
168
  return grading.intensity === 0 ? 1 : grading.intensity;
166
169
  }
167
170
 
171
+ function colorGradingWithLut(
172
+ grading: NormalizedHfColorGrading,
173
+ src: string | null,
174
+ intensity = 1,
175
+ ): NormalizedHfColorGrading {
176
+ return {
177
+ ...grading,
178
+ intensity: visibleColorGradingIntensity(grading),
179
+ lut: src ? { src, intensity } : null,
180
+ };
181
+ }
182
+
183
+ export function colorGradingWithDetail(
184
+ grading: NormalizedHfColorGrading,
185
+ key: HfColorGradingDetailKey,
186
+ value: number,
187
+ ): NormalizedHfColorGrading {
188
+ return {
189
+ ...grading,
190
+ intensity: visibleColorGradingIntensity(grading),
191
+ details: { ...grading.details, [key]: value },
192
+ };
193
+ }
194
+
195
+ function colorGradingWithIntensity(
196
+ grading: NormalizedHfColorGrading,
197
+ intensity: number,
198
+ ): NormalizedHfColorGrading {
199
+ return { ...grading, intensity };
200
+ }
201
+
202
+ export function colorGradingWithAdjust(
203
+ grading: NormalizedHfColorGrading,
204
+ key: HfColorGradingAdjustKey,
205
+ value: number,
206
+ ): NormalizedHfColorGrading {
207
+ return {
208
+ ...grading,
209
+ intensity: visibleColorGradingIntensity(grading),
210
+ adjust: { ...grading.adjust, [key]: value },
211
+ };
212
+ }
213
+
214
+ async function importFirstLut(
215
+ files: FileList | null,
216
+ onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>,
217
+ ): Promise<string | null> {
218
+ if (!files?.length || !onImportAssets) return null;
219
+ const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR);
220
+ return uploaded.find((asset) => LUT_EXT.test(asset)) ?? null;
221
+ }
222
+
223
+ export function createColorGradingActions(
224
+ grading: NormalizedHfColorGrading,
225
+ onCommit: (grading: NormalizedHfColorGrading) => void,
226
+ ) {
227
+ const applyLut = (src: string | null, intensity = 1) => {
228
+ onCommit(colorGradingWithLut(grading, src, intensity));
229
+ };
230
+ return {
231
+ setIntensityPercent(value: number) {
232
+ onCommit(colorGradingWithIntensity(grading, value / 100));
233
+ },
234
+ applyLut,
235
+ setLutIntensityPercent(value: number) {
236
+ if (grading.lut) applyLut(grading.lut.src, value / 100);
237
+ },
238
+ async importLut(
239
+ files: FileList | null,
240
+ onImportAssets: ((files: FileList, dir?: string) => Promise<string[]>) | undefined,
241
+ onImported: () => void,
242
+ ) {
243
+ const src = await importFirstLut(files, onImportAssets);
244
+ if (!src) return;
245
+ onImported();
246
+ applyLut(src);
247
+ },
248
+ };
249
+ }
250
+
168
251
  export function ColorGradingControls({
169
252
  grading,
170
253
  assets,
@@ -189,11 +272,14 @@ export function ColorGradingControls({
189
272
  const detailSettingsSliders =
190
273
  detailSettings === "vignette" ? VIGNETTE_TUNE_SLIDERS : GRAIN_TUNE_SLIDERS;
191
274
  const vignetteSettingsActive = VIGNETTE_TUNE_SLIDERS.some(
192
- (slider) => Math.abs(grading.details[slider.key] - normalizedDefaultValue(slider)) > 0.0001,
275
+ (slider) =>
276
+ Math.abs(grading.details[slider.key] - normalizedColorGradingDefault(slider)) > 0.0001,
193
277
  );
194
278
  const grainSettingsActive = GRAIN_TUNE_SLIDERS.some(
195
- (slider) => Math.abs(grading.details[slider.key] - normalizedDefaultValue(slider)) > 0.0001,
279
+ (slider) =>
280
+ Math.abs(grading.details[slider.key] - normalizedColorGradingDefault(slider)) > 0.0001,
196
281
  );
282
+ const actions = createColorGradingActions(grading, onCommitColorGrading);
197
283
 
198
284
  const applyPreset = (preset: string) => {
199
285
  const next = normalizeHfColorGrading({ preset, intensity: 1, lut: grading.lut });
@@ -202,51 +288,13 @@ export function ColorGradingControls({
202
288
  onCommitColorGrading(next);
203
289
  }
204
290
  };
205
- const updateFilterIntensity = (value: number) => {
206
- onCommitColorGrading({
207
- ...grading,
208
- intensity: value / 100,
209
- });
210
- };
211
- const applyLut = (src: string | null, intensity = 1) => {
212
- onCommitColorGrading({
213
- ...grading,
214
- intensity: visibleIntensity(grading),
215
- lut: src ? { src, intensity } : null,
216
- });
217
- };
218
- const updateLutIntensity = (value: number) => {
219
- if (!grading.lut) return;
220
- applyLut(grading.lut.src, value / 100);
221
- };
222
- const importLuts = async (files: FileList | null) => {
223
- if (!files?.length || !onImportAssets) return;
224
- const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR);
225
- const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
226
- if (firstLut) {
227
- track("button", "Import LUT");
228
- applyLut(firstLut, 1);
229
- }
230
- };
231
291
  const commitDetailSlider = (slider: DetailSlider, next: number) => {
232
- onCommitColorGrading({
233
- ...grading,
234
- intensity: visibleIntensity(grading),
235
- details: {
236
- ...grading.details,
237
- [slider.key]: next / slider.scale,
238
- },
239
- });
292
+ onCommitColorGrading(colorGradingWithDetail(grading, slider.key, next / slider.scale));
240
293
  };
241
294
  const resetDetailSlider = (slider: DetailSlider) => {
242
- onCommitColorGrading({
243
- ...grading,
244
- intensity: visibleIntensity(grading),
245
- details: {
246
- ...grading.details,
247
- [slider.key]: normalizedDefaultValue(slider),
248
- },
249
- });
295
+ onCommitColorGrading(
296
+ colorGradingWithDetail(grading, slider.key, normalizedColorGradingDefault(slider)),
297
+ );
250
298
  };
251
299
  const renderDetailSlider = (slider: DetailSlider, settings?: SliderSettings) => {
252
300
  const value = Math.round(grading.details[slider.key] * slider.scale);
@@ -293,8 +341,8 @@ export function ColorGradingControls({
293
341
  neutral={0}
294
342
  suffix="%"
295
343
  displayValue={`${Math.round(grading.intensity * 100)}%`}
296
- onCommit={updateFilterIntensity}
297
- onReset={() => updateFilterIntensity(100)}
344
+ onCommit={actions.setIntensityPercent}
345
+ onReset={() => actions.setIntensityPercent(100)}
298
346
  />
299
347
 
300
348
  <div className="min-w-0 rounded-md border border-panel-border/70 bg-panel-input/15">
@@ -322,7 +370,7 @@ export function ColorGradingControls({
322
370
  onChange={(event) => {
323
371
  const nextSrc = event.target.value;
324
372
  track("select", "Custom LUT");
325
- applyLut(
373
+ actions.applyLut(
326
374
  nextSrc || null,
327
375
  nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1,
328
376
  );
@@ -361,7 +409,9 @@ export function ColorGradingControls({
361
409
  multiple
362
410
  className="hidden"
363
411
  onChange={(event) => {
364
- void importLuts(event.currentTarget.files);
412
+ void actions.importLut(event.currentTarget.files, onImportAssets, () =>
413
+ track("button", "Import LUT"),
414
+ );
365
415
  event.currentTarget.value = "";
366
416
  }}
367
417
  />
@@ -386,8 +436,8 @@ export function ColorGradingControls({
386
436
  neutral={0}
387
437
  suffix="%"
388
438
  displayValue={`${Math.round((grading.lut.intensity ?? 1) * 100)}%`}
389
- onCommit={updateLutIntensity}
390
- onReset={() => updateLutIntensity(100)}
439
+ onCommit={actions.setLutIntensityPercent}
440
+ onReset={() => actions.setLutIntensityPercent(100)}
391
441
  />
392
442
  </div>
393
443
  )}
@@ -398,7 +448,7 @@ export function ColorGradingControls({
398
448
  <div className="grid min-w-0 gap-1.5">
399
449
  <span className={LABEL}>Adjust</span>
400
450
  <div className="grid min-w-0 grid-cols-2 gap-1.5">
401
- {ADJUST_SLIDERS.map((slider) => {
451
+ {COLOR_GRADING_ADJUST_SLIDERS.map((slider) => {
402
452
  const value = grading.adjust[slider.key] * slider.scale;
403
453
  const isExposure = slider.key === "exposure";
404
454
  return (
@@ -418,24 +468,12 @@ export function ColorGradingControls({
418
468
  : `${Math.round(value)}%`
419
469
  }
420
470
  onCommit={(next) => {
421
- onCommitColorGrading({
422
- ...grading,
423
- intensity: visibleIntensity(grading),
424
- adjust: {
425
- ...grading.adjust,
426
- [slider.key]: next / slider.scale,
427
- },
428
- });
471
+ onCommitColorGrading(
472
+ colorGradingWithAdjust(grading, slider.key, next / slider.scale),
473
+ );
429
474
  }}
430
475
  onReset={() => {
431
- onCommitColorGrading({
432
- ...grading,
433
- intensity: visibleIntensity(grading),
434
- adjust: {
435
- ...grading.adjust,
436
- [slider.key]: 0,
437
- },
438
- });
476
+ onCommitColorGrading(colorGradingWithAdjust(grading, slider.key, 0));
439
477
  }}
440
478
  />
441
479
  );
@@ -499,7 +537,7 @@ export function ColorGradingControls({
499
537
  onCommit={(next) => {
500
538
  onCommitColorGrading({
501
539
  ...grading,
502
- intensity: visibleIntensity(grading),
540
+ intensity: visibleColorGradingIntensity(grading),
503
541
  effects: {
504
542
  ...grading.effects,
505
543
  [slider.key]: next / slider.scale,
@@ -509,7 +547,7 @@ export function ColorGradingControls({
509
547
  onReset={() => {
510
548
  onCommitColorGrading({
511
549
  ...grading,
512
- intensity: visibleIntensity(grading),
550
+ intensity: visibleColorGradingIntensity(grading),
513
551
  effects: {
514
552
  ...grading.effects,
515
553
  [slider.key]: 0,