@hyperframes/studio 0.7.71 → 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 (79) hide show
  1. package/dist/assets/{hyperframes-player-C1X90psg.js → hyperframes-player-DGlzhf_s.js} +1 -1
  2. package/dist/assets/index-B37rWXo4.css +1 -0
  3. package/dist/assets/{index-DzxDHPR1.js → index-CbVTOJ-E.js} +1 -1
  4. package/dist/assets/{index-nY4lLBqM.js → index-CcA5WVJv.js} +1 -1
  5. package/dist/assets/index-DXcLI2wu.js +428 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7762 -4261
  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 +75 -88
  22. package/src/components/editor/colorGradingFrameAnalysis.test.ts +101 -0
  23. package/src/components/editor/colorGradingFrameAnalysis.ts +203 -0
  24. package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
  25. package/src/components/editor/easeCurveSvg.tsx +65 -0
  26. package/src/components/editor/easePresetLibrary.test.ts +114 -0
  27. package/src/components/editor/easePresetLibrary.ts +49 -0
  28. package/src/components/editor/gsapAnimationConstants.ts +15 -13
  29. package/src/components/editor/propertyPanelColorCurveGraph.tsx +549 -0
  30. package/src/components/editor/propertyPanelColorCurves.test.tsx +158 -0
  31. package/src/components/editor/propertyPanelColorCurves.tsx +185 -0
  32. package/src/components/editor/propertyPanelColorGradingControls.tsx +115 -77
  33. package/src/components/editor/propertyPanelColorScopes.tsx +258 -0
  34. package/src/components/editor/propertyPanelColorSecondary.test.tsx +181 -0
  35. package/src/components/editor/propertyPanelColorSecondary.tsx +448 -0
  36. package/src/components/editor/propertyPanelColorWheels.test.tsx +119 -0
  37. package/src/components/editor/propertyPanelColorWheels.tsx +334 -0
  38. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +109 -0
  39. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +239 -41
  40. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +260 -300
  41. package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
  42. package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
  43. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
  44. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
  45. package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
  46. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
  47. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
  48. package/src/components/editor/propertyPanelGradingNumberField.test.tsx +79 -0
  49. package/src/components/editor/propertyPanelGradingNumberField.tsx +116 -0
  50. package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
  51. package/src/components/editor/propertyPanelTypes.ts +13 -0
  52. package/src/components/editor/useColorGradingController.test.ts +331 -10
  53. package/src/components/editor/useColorGradingController.ts +57 -90
  54. package/src/components/editor/useColorGradingPreviews.ts +350 -0
  55. package/src/components/editor/useColorGradingScopes.test.tsx +143 -0
  56. package/src/components/editor/useColorGradingScopes.ts +62 -0
  57. package/src/components/editor/useInspectorGestureTransaction.ts +30 -1
  58. package/src/components/nle/NLEContext.tsx +1 -1
  59. package/src/contexts/PanelLayoutContext.tsx +3 -6
  60. package/src/hooks/useBlockCatalog.ts +31 -13
  61. package/src/hooks/useBlockHandlers.ts +22 -0
  62. package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
  63. package/src/hooks/useGsapScriptCommits.ts +14 -1
  64. package/src/hooks/usePanelLayout.test.ts +68 -1
  65. package/src/hooks/usePanelLayout.ts +72 -34
  66. package/src/icons/SystemIcons.tsx +4 -0
  67. package/src/player/components/PlayerControls.tsx +68 -229
  68. package/src/player/components/ShortcutsPanel.tsx +2 -4
  69. package/src/player/components/SpeedMenu.tsx +1 -2
  70. package/src/telemetry/events.test.ts +9 -0
  71. package/src/telemetry/events.ts +5 -0
  72. package/src/utils/blockInstaller.test.ts +67 -0
  73. package/src/utils/blockInstaller.ts +162 -109
  74. package/src/utils/studioUiPreferences.test.ts +5 -0
  75. package/src/utils/studioUiPreferences.ts +8 -0
  76. package/dist/assets/index-CSCh2Vrp.js +0 -428
  77. package/dist/assets/index-Ceyz8Qt2.css +0 -1
  78. package/src/player/components/PlayerControls.test.ts +0 -20
  79. package/src/player/components/useSeekBarDrag.ts +0 -169
@@ -7,7 +7,10 @@ import {
7
7
  FlatColorGradingAccessory,
8
8
  FlatColorGradingSection,
9
9
  } from "./propertyPanelFlatColorGradingSection";
10
- import { normalizeHfColorGrading } from "@hyperframes/core/color-grading";
10
+ import {
11
+ normalizeHfColorGrading,
12
+ type NormalizedHfColorGrading,
13
+ } from "@hyperframes/core/color-grading";
11
14
 
12
15
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
13
16
 
@@ -215,39 +218,260 @@ function neutralPropsBase() {
215
218
  grading: neutralGrading(),
216
219
  assets: [] as string[],
217
220
  onCommitColorGrading: vi.fn(),
221
+ onPreviewColorGrading: vi.fn(),
218
222
  applyScope: "source-file" as const,
219
223
  applyBusy: false,
220
224
  onSetApplyScope: vi.fn(),
221
225
  onApplyToScope: vi.fn(),
222
226
  onApplyScopeAvailable: true,
223
227
  mediaMetadata: null,
228
+ presetPreviews: {
229
+ status: "ready" as const,
230
+ images: { "bright-pop": "data:image/png;base64,bright" },
231
+ width: 160,
232
+ height: 90,
233
+ },
234
+ onRequestPresetPreviews: vi.fn(),
235
+ captureGradedFrame: vi.fn(async () => null),
224
236
  };
225
237
  }
226
238
 
227
239
  describe("FlatColorGradingSection — Preset + LUT", () => {
228
- it("renders the Preset dropdown with id/label pairs and fires onCommitColorGrading on change", () => {
240
+ it("previews looks without committing, restores on leave, and commits only on click", () => {
229
241
  const onCommitColorGrading = vi.fn();
242
+ const onPreviewColorGrading = vi.fn();
243
+ const base = neutralGrading();
244
+ const grading = {
245
+ ...base,
246
+ intensity: 0.4,
247
+ adjust: { ...base.adjust, contrast: 0.3 },
248
+ wheels: {
249
+ ...base.wheels,
250
+ shadows: { hue: 210, amount: 0.12, level: 0.04 },
251
+ },
252
+ curves: {
253
+ ...base.curves,
254
+ master: [
255
+ [0, 0],
256
+ [0.5, 0.58],
257
+ [1, 1],
258
+ ] as const,
259
+ },
260
+ hueCurves: {
261
+ ...base.hueCurves,
262
+ hueVsSaturation: [
263
+ [0, 0],
264
+ [120, 0.1],
265
+ [240, 0],
266
+ ] as const,
267
+ },
268
+ secondaries:
269
+ normalizeHfColorGrading({
270
+ secondaries: [
271
+ {
272
+ key: { hue: { center: 30, range: 15 } },
273
+ correction: { saturation: 0.08 },
274
+ },
275
+ ],
276
+ })?.secondaries ?? [],
277
+ details: { ...base.details, vignette: 0.2 },
278
+ effects: { ...base.effects, pixelate: 0.5 },
279
+ palette: ["#112233", "#ffffff"],
280
+ lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
281
+ };
230
282
  const { host, root } = renderInto(
231
283
  <FlatColorGradingSection
232
284
  {...neutralPropsBase()}
285
+ grading={grading}
233
286
  onCommitColorGrading={onCommitColorGrading}
287
+ onPreviewColorGrading={onPreviewColorGrading}
234
288
  />,
235
289
  );
236
- const presetSelect = host.querySelector<HTMLSelectElement>(
237
- '[data-flat-grade-preset="true"] select',
290
+ const brightPop = host.querySelector<HTMLButtonElement>(
291
+ '[data-flat-grade-preset="bright-pop"]',
238
292
  );
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");
293
+ if (!brightPop) throw new Error("expected a Bright Pop look button");
245
294
  act(() => {
246
- presetSelect.value = "bright-pop";
247
- presetSelect.dispatchEvent(new Event("change", { bubbles: true }));
295
+ brightPop.dispatchEvent(new MouseEvent("pointerover", { bubbles: true }));
296
+ });
297
+ expect(onPreviewColorGrading.mock.calls.at(-1)?.[0].preset).toBe("bright-pop");
298
+ expect(onPreviewColorGrading.mock.calls.at(-1)?.[1]).toEqual({
299
+ animatedPreview: { kind: "presets", id: "bright-pop" },
248
300
  });
301
+ expect(onCommitColorGrading).not.toHaveBeenCalled();
302
+ act(() => brightPop.dispatchEvent(new MouseEvent("pointerout", { bubbles: true })));
303
+ expect(onPreviewColorGrading).toHaveBeenLastCalledWith(null);
304
+ act(() => brightPop.dispatchEvent(new MouseEvent("click", { bubbles: true })));
249
305
  expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
250
- expect(onCommitColorGrading.mock.calls[0][0].preset).toBe("bright-pop");
306
+ expect(onCommitColorGrading.mock.calls[0][0]).toMatchObject({
307
+ preset: "bright-pop",
308
+ intensity: 1,
309
+ effects: { pixelate: 0.5 },
310
+ palette: ["#112233", "#ffffff"],
311
+ lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
312
+ });
313
+ expect(onCommitColorGrading.mock.calls[0][0].wheels.shadows.amount).toBe(0);
314
+ expect(onCommitColorGrading.mock.calls[0][0].curves.master).toEqual([
315
+ [0, 0],
316
+ [1, 1],
317
+ ]);
318
+ expect(onCommitColorGrading.mock.calls[0][0].hueCurves.hueVsSaturation).toEqual([]);
319
+ expect(onCommitColorGrading.mock.calls[0][0].secondaries).toEqual([]);
320
+ expect(onCommitColorGrading.mock.calls[0][0].adjust.contrast).not.toBe(0.3);
321
+ expect(onCommitColorGrading.mock.calls[0][0].details.vignette).not.toBe(0.2);
322
+ act(() => root.unmount());
323
+ });
324
+
325
+ it("orders manual controls like the shader pipeline", () => {
326
+ const { host, root } = renderInto(<FlatColorGradingSection {...neutralPropsBase()} />);
327
+ const elements = [
328
+ host.querySelector('[data-flat-grade-adjust="true"]'),
329
+ host.querySelector('[data-color-wheels="true"]'),
330
+ host.querySelector('[data-color-curves="true"]'),
331
+ host.querySelector('[data-flat-grade-secondary="true"]'),
332
+ host.querySelector('[data-flat-grade-lut-toggle="true"]'),
333
+ ];
334
+ expect(elements.every(Boolean)).toBe(true);
335
+ for (let index = 0; index < elements.length - 1; index += 1) {
336
+ const current = elements[index];
337
+ const next = elements[index + 1];
338
+ if (!current || !next) throw new Error("expected all grading sections");
339
+ expect(current.compareDocumentPosition(next) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0);
340
+ }
341
+ act(() => root.unmount());
342
+ });
343
+
344
+ it("samples the full-strength signal entering the secondary stage", async () => {
345
+ const base = neutralGrading();
346
+ const grading = normalizeHfColorGrading({
347
+ ...base,
348
+ preset: "warm-polished",
349
+ intensity: 0.35,
350
+ adjust: { ...base.adjust, exposure: 0.2 },
351
+ wheels: {
352
+ ...base.wheels,
353
+ shadows: { hue: 210, amount: 0.2, level: 0.05 },
354
+ },
355
+ curves: {
356
+ ...base.curves,
357
+ master: [
358
+ [0, 0],
359
+ [0.5, 0.6],
360
+ [1, 1],
361
+ ],
362
+ },
363
+ hueCurves: {
364
+ ...base.hueCurves,
365
+ hueVsSaturation: [
366
+ [0, 0],
367
+ [120, 0.1],
368
+ [240, 0],
369
+ ],
370
+ },
371
+ secondaries: [{ key: { hue: { center: 30, range: 15 } }, correction: {} }],
372
+ details: { ...base.details, vignette: 0.4, grain: 0.2 },
373
+ effects: { ...base.effects, pixelate: 0.5 },
374
+ lut: { src: "assets/luts/custom.cube", intensity: 0.8 },
375
+ });
376
+ if (!grading) throw new Error("expected a secondary grade");
377
+ const captureGradedFrame = vi.fn(
378
+ async (_options?: { grading?: NormalizedHfColorGrading }) => null,
379
+ );
380
+ const { host, root } = renderInto(
381
+ <FlatColorGradingSection
382
+ {...neutralPropsBase()}
383
+ grading={grading}
384
+ captureGradedFrame={captureGradedFrame}
385
+ />,
386
+ );
387
+
388
+ await act(async () => {
389
+ const sample = Array.from(host.querySelectorAll<HTMLButtonElement>("button")).find((button) =>
390
+ button.textContent?.includes("Sample color from frame"),
391
+ );
392
+ sample?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
393
+ await Promise.resolve();
394
+ });
395
+
396
+ expect(captureGradedFrame).toHaveBeenCalledTimes(1);
397
+ const options = captureGradedFrame.mock.calls[0]?.[0];
398
+ const sampledGrading = options?.grading;
399
+ if (!sampledGrading) throw new Error("expected the sampled grading input");
400
+ expect(sampledGrading).toMatchObject({
401
+ preset: null,
402
+ intensity: 1,
403
+ adjust: grading.adjust,
404
+ wheels: grading.wheels,
405
+ curves: grading.curves,
406
+ hueCurves: grading.hueCurves,
407
+ secondaries: [],
408
+ lut: null,
409
+ });
410
+ expect(sampledGrading.details.vignette).toBe(0);
411
+ expect(sampledGrading.details.grain).toBe(0);
412
+ expect(sampledGrading.effects.pixelate).toBe(0);
413
+ act(() => root.unmount());
414
+ });
415
+
416
+ it("keeps effect-bearing saved styles out of Grade", () => {
417
+ const { host, root } = renderInto(<FlatColorGradingSection {...neutralPropsBase()} />);
418
+ const presets = host.querySelector('[data-flat-grade-preset-group="presets"]');
419
+ expect(presets?.querySelector('[data-flat-grade-preset="bright-pop"]')).not.toBeNull();
420
+ expect(presets?.querySelector('[data-flat-grade-preset="vhs-playback"]')).toBeNull();
421
+ expect(presets?.querySelector('[data-flat-grade-preset="editorial-halftone"]')).toBeNull();
422
+ expect(host.querySelector('[data-flat-grade-preset="neutral"]')?.textContent).toContain(
423
+ "Neutral",
424
+ );
425
+ act(() => root.unmount());
426
+ });
427
+
428
+ it("shows ready selected-media preview images without requesting a duplicate batch", () => {
429
+ const onRequestPresetPreviews = vi.fn();
430
+ const { host, root } = renderInto(
431
+ <FlatColorGradingSection
432
+ {...neutralPropsBase()}
433
+ onRequestPresetPreviews={onRequestPresetPreviews}
434
+ />,
435
+ );
436
+ expect(onRequestPresetPreviews).not.toHaveBeenCalled();
437
+ expect(
438
+ host.querySelector<HTMLImageElement>('[data-flat-grade-preview="bright-pop"]')?.src,
439
+ ).toContain("data:image/png;base64,bright");
440
+ expect(host.textContent).not.toContain("VHS Playback");
441
+ act(() => root.unmount());
442
+ });
443
+
444
+ it("requests an idle preview batch once", () => {
445
+ const onRequestPresetPreviews = vi.fn();
446
+ const props = neutralPropsBase();
447
+ const { root } = renderInto(
448
+ <FlatColorGradingSection
449
+ {...props}
450
+ presetPreviews={{ ...props.presetPreviews, status: "idle", images: {} }}
451
+ onRequestPresetPreviews={onRequestPresetPreviews}
452
+ />,
453
+ );
454
+ expect(onRequestPresetPreviews).toHaveBeenCalledTimes(1);
455
+ act(() => root.unmount());
456
+ });
457
+
458
+ it("requires an explicit retry after a preview failure", () => {
459
+ const onRequestPresetPreviews = vi.fn();
460
+ const props = neutralPropsBase();
461
+ const { host, root } = renderInto(
462
+ <FlatColorGradingSection
463
+ {...props}
464
+ presetPreviews={{ ...props.presetPreviews, status: "unavailable", images: {} }}
465
+ onRequestPresetPreviews={onRequestPresetPreviews}
466
+ />,
467
+ );
468
+ expect(onRequestPresetPreviews).not.toHaveBeenCalled();
469
+ const retry = Array.from(host.querySelectorAll("button")).find(
470
+ (button) => button.textContent === "Retry look previews",
471
+ );
472
+ if (!retry) throw new Error("expected an explicit preview retry");
473
+ act(() => retry.dispatchEvent(new MouseEvent("click", { bubbles: true })));
474
+ expect(onRequestPresetPreviews).toHaveBeenCalledTimes(1);
251
475
  act(() => root.unmount());
252
476
  });
253
477
 
@@ -410,7 +634,7 @@ describe("FlatColorGradingSection — Adjust sliders", () => {
410
634
  act(() => root.unmount());
411
635
  });
412
636
 
413
- it("does NOT force intensity to revive when the Strength slider itself is dragged — it writes the value directly", () => {
637
+ it("does NOT force intensity to revive when the Amount slider itself is dragged — it writes the value directly", () => {
414
638
  const onCommitColorGrading = vi.fn();
415
639
  const grading = { ...neutralGrading(), intensity: 0 };
416
640
  const { host, root } = renderInto(
@@ -420,7 +644,7 @@ describe("FlatColorGradingSection — Adjust sliders", () => {
420
644
  onCommitColorGrading={onCommitColorGrading}
421
645
  />,
422
646
  );
423
- const strengthRow = findRowByText(host, "div", "Strength", "startsWith");
647
+ const strengthRow = findRowByText(host, "div", "Amount", "startsWith");
424
648
  // min=0, max=100, step=1, ratio=0.4 -> raw=40 -> commit(40) -> intensity = 40/100 = 0.4
425
649
  dragSliderTrack(strengthRow, 40, 100);
426
650
  expect(onCommitColorGrading).toHaveBeenCalledTimes(1);
@@ -492,32 +716,6 @@ describe("FlatColorGradingSection — Vignette and Grain", () => {
492
716
  });
493
717
  });
494
718
 
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
719
  describe("FlatColorGradingSection — HDR banner and Apply scope", () => {
522
720
  it("shows the HDR banner only when mediaMetadata reports an HDR source", () => {
523
721
  const { host, root } = renderInto(