@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.
Files changed (62) hide show
  1. package/dist/assets/{hyperframes-player-DpKeYLEo.js → hyperframes-player-Bjf2HPzR.js} +1 -1
  2. package/dist/assets/index-BgZMle9I.css +1 -0
  3. package/dist/assets/{index-DXjAllII.js → index-Ch1hbJ3e.js} +1 -1
  4. package/dist/assets/{index-CbX-xoud.js → index-Ct_pxETK.js} +1 -1
  5. package/dist/assets/index-DCyWLHnx.js +428 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +5383 -3804
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +4 -4
  12. package/src/components/StudioLeftSidebar.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +10 -4
  14. package/src/components/editor/AnimationCard.test.tsx +100 -0
  15. package/src/components/editor/AnimationCard.tsx +5 -2
  16. package/src/components/editor/EaseCurveSection.test.tsx +348 -0
  17. package/src/components/editor/EaseCurveSection.tsx +444 -237
  18. package/src/components/editor/EaseParamFields.test.tsx +188 -0
  19. package/src/components/editor/EaseParamFields.tsx +246 -0
  20. package/src/components/editor/PropertyPanel.test.tsx +9 -7
  21. package/src/components/editor/PropertyPanelFlat.tsx +74 -88
  22. package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
  23. package/src/components/editor/easeCurveSvg.tsx +65 -0
  24. package/src/components/editor/easePresetLibrary.test.ts +114 -0
  25. package/src/components/editor/easePresetLibrary.ts +49 -0
  26. package/src/components/editor/gsapAnimationConstants.ts +15 -13
  27. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
  28. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
  29. package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
  30. package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
  31. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
  32. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
  33. package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
  34. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
  35. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +5 -5
  36. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
  37. package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
  38. package/src/components/editor/propertyPanelTypes.ts +13 -0
  39. package/src/components/editor/useColorGradingController.test.ts +235 -30
  40. package/src/components/editor/useColorGradingController.ts +46 -88
  41. package/src/components/editor/useColorGradingPreviews.ts +307 -0
  42. package/src/components/nle/NLEContext.tsx +1 -1
  43. package/src/contexts/PanelLayoutContext.tsx +3 -6
  44. package/src/hooks/useBlockCatalog.ts +31 -13
  45. package/src/hooks/useBlockHandlers.ts +22 -0
  46. package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
  47. package/src/hooks/useGsapScriptCommits.ts +14 -1
  48. package/src/hooks/usePanelLayout.test.ts +68 -1
  49. package/src/hooks/usePanelLayout.ts +72 -34
  50. package/src/player/components/PlayerControls.tsx +68 -229
  51. package/src/player/components/ShortcutsPanel.tsx +2 -4
  52. package/src/player/components/SpeedMenu.tsx +1 -2
  53. package/src/telemetry/events.test.ts +9 -0
  54. package/src/telemetry/events.ts +5 -0
  55. package/src/utils/blockInstaller.test.ts +67 -0
  56. package/src/utils/blockInstaller.ts +162 -109
  57. package/src/utils/studioUiPreferences.test.ts +5 -0
  58. package/src/utils/studioUiPreferences.ts +8 -0
  59. package/dist/assets/index-CAeU317U.js +0 -428
  60. package/dist/assets/index-Ceyz8Qt2.css +0 -1
  61. package/src/player/components/PlayerControls.test.ts +0 -20
  62. package/src/player/components/useSeekBarDrag.ts +0 -169
@@ -215,39 +215,98 @@ function neutralPropsBase() {
215
215
  grading: neutralGrading(),
216
216
  assets: [] as string[],
217
217
  onCommitColorGrading: vi.fn(),
218
+ onPreviewColorGrading: vi.fn(),
218
219
  applyScope: "source-file" as const,
219
220
  applyBusy: false,
220
221
  onSetApplyScope: vi.fn(),
221
222
  onApplyToScope: vi.fn(),
222
223
  onApplyScopeAvailable: true,
223
224
  mediaMetadata: null,
225
+ presetPreviews: {
226
+ status: "ready" as const,
227
+ images: { "bright-pop": "data:image/png;base64,bright" },
228
+ width: 160,
229
+ height: 90,
230
+ },
231
+ onRequestPresetPreviews: vi.fn(),
224
232
  };
225
233
  }
226
234
 
227
235
  describe("FlatColorGradingSection — Preset + LUT", () => {
228
- it("renders the Preset dropdown with id/label pairs and fires onCommitColorGrading on change", () => {
236
+ it("previews looks without committing, restores on leave, and commits only on click", () => {
229
237
  const onCommitColorGrading = vi.fn();
238
+ const onPreviewColorGrading = vi.fn();
239
+ const base = neutralGrading();
240
+ const grading = {
241
+ ...base,
242
+ intensity: 0.4,
243
+ adjust: { ...base.adjust, contrast: 0.3 },
244
+ details: { ...base.details, vignette: 0.2 },
245
+ effects: { ...base.effects, pixelate: 0.5 },
246
+ palette: ["#112233", "#ffffff"],
247
+ lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
248
+ };
230
249
  const { host, root } = renderInto(
231
250
  <FlatColorGradingSection
232
251
  {...neutralPropsBase()}
252
+ grading={grading}
233
253
  onCommitColorGrading={onCommitColorGrading}
254
+ onPreviewColorGrading={onPreviewColorGrading}
234
255
  />,
235
256
  );
236
- const presetSelect = host.querySelector<HTMLSelectElement>(
237
- '[data-flat-grade-preset="true"] select',
257
+ const brightPop = host.querySelector<HTMLButtonElement>(
258
+ '[data-flat-grade-preset="bright-pop"]',
238
259
  );
239
- if (!presetSelect) throw new Error("expected a preset select");
240
- expect(presetSelect.value).toBe("neutral");
241
- // The visible "Preset" label is a sibling span outside FlatSelectRow
242
- // (label="" there, to avoid rendering it twice) — the select still
243
- // needs its own accessible name via the dedicated ariaLabel prop.
244
- expect(presetSelect.getAttribute("aria-label")).toBe("Preset");
260
+ if (!brightPop) throw new Error("expected a Bright Pop look button");
245
261
  act(() => {
246
- presetSelect.value = "fresh-pop";
247
- presetSelect.dispatchEvent(new Event("change", { bubbles: true }));
262
+ brightPop.dispatchEvent(new MouseEvent("pointerover", { bubbles: true }));
248
263
  });
264
+ expect(onPreviewColorGrading.mock.calls.at(-1)?.[0].preset).toBe("bright-pop");
265
+ expect(onPreviewColorGrading.mock.calls.at(-1)?.[1]).toEqual({
266
+ animatedPreview: { kind: "presets", id: "bright-pop" },
267
+ });
268
+ expect(onCommitColorGrading).not.toHaveBeenCalled();
269
+ act(() => brightPop.dispatchEvent(new MouseEvent("pointerout", { bubbles: true })));
270
+ expect(onPreviewColorGrading).toHaveBeenLastCalledWith(null);
271
+ act(() => brightPop.dispatchEvent(new MouseEvent("click", { bubbles: true })));
249
272
  expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
250
- expect(onCommitColorGrading.mock.calls[0][0].preset).toBe("fresh-pop");
273
+ expect(onCommitColorGrading.mock.calls[0][0]).toMatchObject({
274
+ preset: "bright-pop",
275
+ intensity: 1,
276
+ effects: { pixelate: 0.5 },
277
+ palette: ["#112233", "#ffffff"],
278
+ lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
279
+ });
280
+ expect(onCommitColorGrading.mock.calls[0][0].adjust.contrast).not.toBe(0.3);
281
+ expect(onCommitColorGrading.mock.calls[0][0].details.vignette).not.toBe(0.2);
282
+ act(() => root.unmount());
283
+ });
284
+
285
+ it("keeps effect-bearing saved styles out of Grade", () => {
286
+ const { host, root } = renderInto(<FlatColorGradingSection {...neutralPropsBase()} />);
287
+ const presets = host.querySelector('[data-flat-grade-preset-group="presets"]');
288
+ expect(presets?.querySelector('[data-flat-grade-preset="bright-pop"]')).not.toBeNull();
289
+ expect(presets?.querySelector('[data-flat-grade-preset="vhs-playback"]')).toBeNull();
290
+ expect(presets?.querySelector('[data-flat-grade-preset="editorial-halftone"]')).toBeNull();
291
+ expect(host.querySelector('[data-flat-grade-preset="neutral"]')?.textContent).toContain(
292
+ "Neutral",
293
+ );
294
+ act(() => root.unmount());
295
+ });
296
+
297
+ it("shows exact selected-media preview images and requests a fresh batch on mount", () => {
298
+ const onRequestPresetPreviews = vi.fn();
299
+ const { host, root } = renderInto(
300
+ <FlatColorGradingSection
301
+ {...neutralPropsBase()}
302
+ onRequestPresetPreviews={onRequestPresetPreviews}
303
+ />,
304
+ );
305
+ expect(onRequestPresetPreviews).toHaveBeenCalledTimes(1);
306
+ expect(
307
+ host.querySelector<HTMLImageElement>('[data-flat-grade-preview="bright-pop"]')?.src,
308
+ ).toContain("data:image/png;base64,bright");
309
+ expect(host.textContent).not.toContain("VHS Playback");
251
310
  act(() => root.unmount());
252
311
  });
253
312
 
@@ -410,7 +469,7 @@ describe("FlatColorGradingSection — Adjust sliders", () => {
410
469
  act(() => root.unmount());
411
470
  });
412
471
 
413
- it("does NOT force intensity to revive when the Strength slider itself is dragged — it writes the value directly", () => {
472
+ it("does NOT force intensity to revive when the Amount slider itself is dragged — it writes the value directly", () => {
414
473
  const onCommitColorGrading = vi.fn();
415
474
  const grading = { ...neutralGrading(), intensity: 0 };
416
475
  const { host, root } = renderInto(
@@ -420,7 +479,7 @@ describe("FlatColorGradingSection — Adjust sliders", () => {
420
479
  onCommitColorGrading={onCommitColorGrading}
421
480
  />,
422
481
  );
423
- const strengthRow = findRowByText(host, "div", "Strength", "startsWith");
482
+ const strengthRow = findRowByText(host, "div", "Amount", "startsWith");
424
483
  // min=0, max=100, step=1, ratio=0.4 -> raw=40 -> commit(40) -> intensity = 40/100 = 0.4
425
484
  dragSliderTrack(strengthRow, 40, 100);
426
485
  expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
@@ -492,32 +551,6 @@ describe("FlatColorGradingSection — Vignette and Grain", () => {
492
551
  });
493
552
  });
494
553
 
495
- describe("FlatColorGradingSection — Effects", () => {
496
- it("renders Blur and Pixelate sliders under an Effects micro-label", () => {
497
- const { host, root } = renderInto(<FlatColorGradingSection {...neutralPropsBase()} />);
498
- expect(host.textContent).toContain("Effects");
499
- const rows = host.querySelectorAll('[data-flat-grade-effect="true"]');
500
- expect(rows).toHaveLength(2);
501
- act(() => root.unmount());
502
- });
503
-
504
- it("commits a dragged Pixelate value on slider track pointerdown, scaled from percent to the 0..1 effect range", () => {
505
- const onCommitColorGrading = vi.fn();
506
- const { host, root } = renderInto(
507
- <FlatColorGradingSection
508
- {...neutralPropsBase()}
509
- onCommitColorGrading={onCommitColorGrading}
510
- />,
511
- );
512
- const pixelateRow = findRowByText(host, '[data-flat-grade-effect="true"]', "Pixelate");
513
- // min=0, max=100, step=1, ratio=0.75 -> raw=75 -> commit(75) -> effects.pixelate = 75/100 = 0.75
514
- dragSliderTrack(pixelateRow, 75, 100);
515
- expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
516
- expect(onCommitColorGrading.mock.calls[0][0].effects.pixelate).toBe(0.75);
517
- act(() => root.unmount());
518
- });
519
- });
520
-
521
554
  describe("FlatColorGradingSection — HDR banner and Apply scope", () => {
522
555
  it("shows the HDR banner only when mediaMetadata reports an HDR source", () => {
523
556
  const { host, root } = renderInto(
@@ -1,19 +1,23 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
- HF_COLOR_GRADING_PRESETS,
3
+ HF_COLOR_GRADING_GRADE_PRESETS,
4
4
  isHfColorGradingActive,
5
5
  normalizeHfColorGrading,
6
6
  type HfColorGradingAdjustKey,
7
7
  type HfColorGradingDetailKey,
8
- type HfColorGradingEffectKey,
9
8
  type NormalizedHfColorGrading,
10
9
  } from "@hyperframes/core/color-grading";
11
10
  import { Compare, Plus, RotateCcw, Settings } from "../../icons/SystemIcons";
12
11
  import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
13
12
  import { LUT_EXT } from "../../utils/mediaTypes";
14
- import { FlatSelectRow, FlatSlider } from "./propertyPanelFlatPrimitives";
15
- import { resolveValueTier } from "./propertyPanelValueTier";
16
- import type { ColorGradingControllerState, MediaMetadata } from "./useColorGradingController";
13
+ import { FLAT_PREVIEW_GRID, FlatSlider } from "./propertyPanelFlatPrimitives";
14
+ import type {
15
+ ColorGradingControllerState,
16
+ ColorGradingPresetPreviews,
17
+ ColorGradingPreviewOptions,
18
+ MediaMetadata,
19
+ } from "./useColorGradingController";
20
+ import { presetPreviewHandlers } from "./propertyPanelPresetPreview";
17
21
 
18
22
  const STATUS_DOT_CLASS: Record<ColorGradingControllerState["runtimeStatus"]["state"], string> = {
19
23
  active: "bg-emerald-400",
@@ -123,8 +127,6 @@ export function FlatColorGradingAccessory({
123
127
  );
124
128
  }
125
129
 
126
- const PRESET_OPTIONS = HF_COLOR_GRADING_PRESETS.map((p) => ({ value: p.id, label: p.label }));
127
-
128
130
  const ADJUST_SLIDERS: Array<{
129
131
  key: HfColorGradingAdjustKey;
130
132
  label: string;
@@ -182,11 +184,6 @@ const VIGNETTE_TUNE_KEYS: HfColorGradingDetailKey[] = [
182
184
  ];
183
185
  const GRAIN_TUNE_KEYS: HfColorGradingDetailKey[] = ["grainSize", "grainRoughness"];
184
186
 
185
- const EFFECT_SLIDERS: Array<{ key: HfColorGradingEffectKey; label: string }> = [
186
- { key: "blur", label: "Blur" },
187
- { key: "pixelate", label: "Pixelate" },
188
- ];
189
-
190
187
  function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
191
188
  if (metadata?.color.dynamicRange !== "hdr") return null;
192
189
  const details = [
@@ -231,23 +228,32 @@ export function FlatColorGradingSection({
231
228
  assets,
232
229
  onImportAssets,
233
230
  onCommitColorGrading,
231
+ onPreviewColorGrading,
234
232
  applyScope,
235
233
  applyBusy,
236
234
  onSetApplyScope,
237
235
  onApplyToScope,
238
236
  onApplyScopeAvailable,
239
237
  mediaMetadata,
238
+ presetPreviews,
239
+ onRequestPresetPreviews,
240
240
  }: {
241
241
  grading: NormalizedHfColorGrading;
242
242
  assets: string[];
243
243
  onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
244
244
  onCommitColorGrading: (next: NormalizedHfColorGrading) => void;
245
+ onPreviewColorGrading: (
246
+ next: NormalizedHfColorGrading | null,
247
+ options?: ColorGradingPreviewOptions,
248
+ ) => void;
245
249
  applyScope: "source-file" | "project";
246
250
  applyBusy: boolean;
247
251
  onSetApplyScope: (scope: "source-file" | "project") => void;
248
252
  onApplyToScope: () => void;
249
253
  onApplyScopeAvailable: boolean;
250
254
  mediaMetadata: MediaMetadata | null;
255
+ presetPreviews: ColorGradingPresetPreviews;
256
+ onRequestPresetPreviews: () => void;
251
257
  }) {
252
258
  const track = useTrackDesignInput();
253
259
  const lutInputRef = useRef<HTMLInputElement>(null);
@@ -260,10 +266,21 @@ export function FlatColorGradingSection({
260
266
  const lut = grading.lut;
261
267
  const selectedLutName = lut?.src ? (lut.src.split("/").pop() ?? lut.src) : null;
262
268
 
263
- const applyPreset = (presetId: string) => {
264
- const next = normalizeHfColorGrading({ preset: presetId, intensity: 1, lut: grading.lut });
265
- if (next) onCommitColorGrading(next);
269
+ useEffect(() => {
270
+ if (
271
+ presetPreviews.status !== "loading" &&
272
+ HF_COLOR_GRADING_GRADE_PRESETS.some((preset) => !presetPreviews.images[preset.id])
273
+ ) {
274
+ onRequestPresetPreviews();
275
+ }
276
+ }, [onRequestPresetPreviews, presetPreviews.images, presetPreviews.status]);
277
+
278
+ const resolvePreset = (presetId: string) => {
279
+ const resolved = normalizeHfColorGrading({ preset: presetId, lut: grading.lut });
280
+ return resolved ? { ...resolved, effects: grading.effects, palette: grading.palette } : grading;
266
281
  };
282
+
283
+ useEffect(() => () => onPreviewColorGrading(null), [onPreviewColorGrading]);
267
284
  const updateIntensity = (value: number) => {
268
285
  onCommitColorGrading({ ...grading, intensity: value / 100 });
269
286
  };
@@ -319,22 +336,60 @@ export function FlatColorGradingSection({
319
336
  return (
320
337
  <div className="space-y-1.5">
321
338
  <HdrBanner metadata={mediaMetadata} />
322
- <div data-flat-grade-preset="true" className="flex min-h-[30px] items-center justify-between">
323
- <span className="text-[11px] text-panel-text-2">Preset</span>
324
- <FlatSelectRow
325
- label=""
326
- ariaLabel="Preset"
327
- value={grading.preset ?? "neutral"}
328
- options={PRESET_OPTIONS}
329
- tier={resolveValueTier(
330
- grading.preset === "neutral" ? undefined : (grading.preset ?? undefined),
331
- "neutral",
332
- )}
333
- onChange={applyPreset}
334
- />
339
+ <div data-flat-grade-presets="true" className="space-y-1.5">
340
+ <div data-flat-grade-preset-group="presets" className={FLAT_PREVIEW_GRID}>
341
+ {HF_COLOR_GRADING_GRADE_PRESETS.map((preset) => {
342
+ const label = preset.label;
343
+ const selected = grading.preset === preset.id;
344
+ const preview = presetPreviews.images[preset.id];
345
+ return (
346
+ <button
347
+ key={preset.id}
348
+ type="button"
349
+ data-flat-grade-preset={preset.id}
350
+ aria-pressed={selected}
351
+ {...presetPreviewHandlers({
352
+ id: preset.id,
353
+ label,
354
+ resolve: () => resolvePreset(preset.id),
355
+ onPreview: onPreviewColorGrading,
356
+ onCommit: onCommitColorGrading,
357
+ onTrack: (name) => track("button", `Apply ${name}`),
358
+ })}
359
+ className={`min-w-0 overflow-hidden border text-left text-[10px] transition-colors ${
360
+ selected
361
+ ? "border-panel-accent bg-panel-accent/10 text-panel-text-0"
362
+ : "border-panel-hairline bg-panel-bg-soft text-panel-text-3 hover:border-panel-border-input hover:text-panel-text-1"
363
+ }`}
364
+ >
365
+ <span
366
+ data-flat-grade-preview-frame={preset.id}
367
+ className="flex w-full items-center justify-center overflow-hidden bg-black/20"
368
+ style={{ aspectRatio: `${presetPreviews.width} / ${presetPreviews.height}` }}
369
+ >
370
+ {preview ? (
371
+ <img
372
+ data-flat-grade-preview={preset.id}
373
+ src={preview}
374
+ alt=""
375
+ draggable={false}
376
+ className="block h-full w-full object-contain"
377
+ />
378
+ ) : (
379
+ <span
380
+ data-flat-grade-preview-placeholder={presetPreviews.status}
381
+ className="h-full w-full bg-panel-bg-soft"
382
+ />
383
+ )}
384
+ </span>
385
+ <span className="block truncate px-2 py-1.5">{label}</span>
386
+ </button>
387
+ );
388
+ })}
389
+ </div>
335
390
  </div>
336
391
  <FlatSlider
337
- label="Strength"
392
+ label="Amount"
338
393
  value={Math.round(grading.intensity * 100)}
339
394
  min={0}
340
395
  max={100}
@@ -462,7 +517,7 @@ export function FlatColorGradingSection({
462
517
 
463
518
  <div className="space-y-1.5 border-t border-panel-hairline pt-1.5">
464
519
  <div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
465
- Finishing
520
+ Finish
466
521
  </div>
467
522
  <div className="flex items-center gap-1.5">
468
523
  <div className="flex-1">{renderDetailSlider("vignette")}</div>
@@ -497,42 +552,6 @@ export function FlatColorGradingSection({
497
552
  )}
498
553
  </div>
499
554
 
500
- <div className="space-y-0.5 border-t border-panel-hairline pt-1.5">
501
- <div className="mb-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
502
- Effects
503
- </div>
504
- {EFFECT_SLIDERS.map((slider) => {
505
- const value = grading.effects[slider.key];
506
- const isSet = value > 1e-6;
507
- return (
508
- <div key={slider.key} data-flat-grade-effect="true">
509
- <FlatSlider
510
- label={slider.label}
511
- value={Math.round(value * 100)}
512
- min={0}
513
- max={100}
514
- tier={isSet ? "explicitCustom" : "default"}
515
- displayValue={`${Math.round(value * 100)}%`}
516
- onCommit={(next) =>
517
- onCommitColorGrading({
518
- ...grading,
519
- intensity: visibleIntensity(grading),
520
- effects: { ...grading.effects, [slider.key]: next / 100 },
521
- })
522
- }
523
- onReset={() =>
524
- onCommitColorGrading({
525
- ...grading,
526
- intensity: visibleIntensity(grading),
527
- effects: { ...grading.effects, [slider.key]: 0 },
528
- })
529
- }
530
- />
531
- </div>
532
- );
533
- })}
534
- </div>
535
-
536
555
  {onApplyScopeAvailable && (
537
556
  <div className="flex items-center justify-between gap-2 border-t border-panel-hairline pt-1.5">
538
557
  <span className="flex items-center gap-1.5 text-[11px] text-panel-text-2">
@@ -0,0 +1,96 @@
1
+ import {
2
+ HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS,
3
+ type HfColorGradingEffectKey,
4
+ type NormalizedHfColorGrading,
5
+ } from "@hyperframes/core/color-grading";
6
+ import { FlatSelectRow } from "./propertyPanelFlatSelectRow";
7
+ import { FlatSlider } from "./propertyPanelFlatPrimitives";
8
+ import { FlatToggle } from "./propertyPanelFlatToggle";
9
+ import {
10
+ DEFAULT_EFFECTS,
11
+ type EffectControl,
12
+ type EffectSpec,
13
+ } from "./propertyPanelFlatEffectSpecs";
14
+
15
+ type CommitEffect = (key: HfColorGradingEffectKey, value: number) => void;
16
+
17
+ function defaultValueFor(control: EffectControl, effect: EffectSpec): number {
18
+ return (
19
+ HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS[effect.key][control.key] ?? DEFAULT_EFFECTS[control.key]
20
+ );
21
+ }
22
+
23
+ function EffectSliderControl({
24
+ control,
25
+ value,
26
+ defaultValue,
27
+ onCommit,
28
+ }: {
29
+ control: Extract<EffectControl, { kind: "slider" }>;
30
+ value: number;
31
+ defaultValue: number;
32
+ onCommit: CommitEffect;
33
+ }) {
34
+ const scale = control.scale ?? 100;
35
+ const sliderValue = value * scale;
36
+ const displayValue = control.format
37
+ ? control.format(value)
38
+ : `${Number.isInteger(sliderValue) ? sliderValue : sliderValue.toFixed(1)}${control.unit ?? "%"}`;
39
+ return (
40
+ <FlatSlider
41
+ label={control.label}
42
+ value={sliderValue}
43
+ min={control.min ?? 0}
44
+ max={control.max ?? 100}
45
+ step={control.step ?? 1}
46
+ tier={Math.abs(value - defaultValue) > 0.0001 ? "explicitCustom" : "default"}
47
+ displayValue={displayValue}
48
+ onCommit={(next) => onCommit(control.key, next / scale)}
49
+ onReset={() => onCommit(control.key, defaultValue)}
50
+ />
51
+ );
52
+ }
53
+
54
+ export function FlatEffectControl({
55
+ control,
56
+ effect,
57
+ effects,
58
+ onCommit,
59
+ }: {
60
+ control: EffectControl;
61
+ effect: EffectSpec;
62
+ effects: NormalizedHfColorGrading["effects"];
63
+ onCommit: CommitEffect;
64
+ }) {
65
+ const value = effects[control.key];
66
+ const defaultValue = defaultValueFor(control, effect);
67
+ if (control.kind === "toggle") {
68
+ return (
69
+ <FlatToggle
70
+ label={control.label}
71
+ checked={value >= 0.5}
72
+ onChange={(next) => onCommit(control.key, next ? 1 : 0)}
73
+ />
74
+ );
75
+ }
76
+ if (control.kind === "select") {
77
+ return (
78
+ <FlatSelectRow
79
+ label={control.label}
80
+ value={String(Math.round(value))}
81
+ options={control.options}
82
+ tier={value === defaultValue ? "default" : "explicitCustom"}
83
+ onChange={(next) => onCommit(control.key, Number.parseInt(next, 10))}
84
+ onReset={() => onCommit(control.key, defaultValue)}
85
+ />
86
+ );
87
+ }
88
+ return (
89
+ <EffectSliderControl
90
+ control={control}
91
+ value={value}
92
+ defaultValue={defaultValue}
93
+ onCommit={onCommit}
94
+ />
95
+ );
96
+ }