feat(studio): add color grading inspector controls#1515
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
Approve. Well-structured UI with clean separation (controls vs. section vs. helpers), proper feature flagging, and correct undo/redo integration via coalesced PatchOperation persistence.
The split-pane inspector refactor is a nice improvement — Design and Layers coexisting is good UX. The debounced live-commit path (handleDomAttributeLiveCommit → 40ms slider tick → 350ms persist) gives smooth slider feedback without hammering persistence. Coalesce keys ensure undo granularity is per-slider-release.
Minor notes (non-blocking):
<input type="range">elements lackaria-label— the visual<span>label isn't programmatically associated. Quick fix: addaria-label={label}to each range input.- The
SLIDERS.length % 2 === 1 && slider.key === "saturation"col-span logic hardcodes the assumption that saturation is last. Usingindex === SLIDERS.length - 1would be more resilient to reordering. commitColorGradingisn't wrapped inuseCallback— creates new function identity on every render during slider drags.postMessage(... , "*")for iframe communication — consider using the actual iframe origin for defense-in-depth.- Component tests for the interactive controls would be valuable before removing the feature flag.
— Miga
vanceingalls
left a comment
There was a problem hiding this comment.
Hello Ular — feat(studio): add color grading inspector controls.
Summary of intent
Layer 3 of the stack: the Studio inspector UI. Gated Color Grading section visible for video/img selections (behind STUDIO_COLOR_GRADING_ENABLED env flag, default off), preset dropdown, .cube upload + project-LUT picker, nine adjustment sliders matching the schema's HfColorGradingAdjustKey set, LUT strength slider, hold-to-compare button, runtime-status pill, reset. Persists via data-color-grading attribute through the existing onSetAttributeLive path; pushes live updates through the bridge's set-color-grading / set-color-grading-compare actions.
Stack chain posture
- Base
feat/color-grading-runtimeat47a55279(= #1514 head). Headafa94767. Mergeable. - Stack-internal diff: schema (
colorGrading.ts) and runtime (runtime/colorGrading.ts) untouched here. Pure consumption. - CI green (Preflight, Preview parity, regression, player-perf — the studio package doesn't gate the windows render shards on this PR's selector).
Per-file findings
packages/studio/src/components/editor/manualEditingAvailability.ts:67-72
STUDIO_COLOR_GRADING_ENABLED defaults to false, opt-in via VITE_STUDIO_ENABLE_COLOR_GRADING=1. Tested at manualEditingAvailability.test.ts:32-42. Good discipline — bug-bash scope matches the PR body.
packages/studio/src/utils/mediaTypes.ts:5
LUT_EXT = /\.cube$/i. Asymmetric note: every other extension constant in this file is a multi-alternation regex (IMAGE_EXT, VIDEO_EXT, etc.). v1 supports only .cube so a single-alt regex is correct; mentioning so a future reader doesn't think it's a typo missing a sibling extension.
packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx (new, 395 lines)
This is the inspector → runtime emission boundary — the most regime-sensitive surface in the stack. I traced every emit site:
toBridgeColorGradingat:84-93serializes exactly the shape the runtime reads (preset,intensity,adjust,lut,colorSpace), skippingenabledbecauseisHfColorGradingActiveshort-circuits on inactive. MatchesnormalizeHfColorGrading's input contract.postColorGradingat:275-289posts{ action: "set-color-grading", target, grading }directly to the iframe, wherebridge.ts:installRuntimeControlBridgehandles it via theset-color-gradingbranch. Bridge contract verified one layer down.commitColorGradingat:318-340serializes for persistence (serializeHfColorGrading) AND posts live for preview. The 350mssetTimeoutdebounce on persistence is reasonable — drag a slider, only one writeback. The cleanup at:265-273flushes any pending value on unmount. Pattern #4 cousin (defer-then-flush) handled correctly.commitColorGradingoverridesintensity: 1at:474and:484(inpropertyPanelColorGradingControls.tsx) and at:333-337here. The schema permits[0, 1]but the inspector emits1unconditionally. Two ways to read this: (a) intentional v1 scoping — top-level "wet/dry" is a follow-up; (b) regime mismatch where the inspector is narrower than the schema. Either reading is defensible; documenting in the PR body that "v1 inspector keeps top-level intensity = 1 because the LUT intensity slider is the only wet/dry control surfaced" would close the loop.postCompareat:291-309hardcodesposition: 1, lineWidth: 0. The bridge contract supports a full split-screen wipe (the shader atruntime/colorGrading.ts:286-298implements it), but the inspector only toggles a "hold to show original" boolean. v1 scope decision — fine to call out in the PR body so reviewers don't think this is a missing control.
packages/studio/src/components/editor/propertyPanelColorGradingControls.tsx (new, 493 lines)
The slider widget is well-contained: local draft state with a 40ms commit debounce (scheduleCommit at :154-166), numeric input with arrow-key nudging, reset-per-slider, LUT picker that lists project assets filtered through LUT_EXT and a + button for upload to assets/luts/ via onImportAssets.
SLIDER_THUMB_SIZE = 10andSLIDER_THUMB_RADIUS = 5are layout constants for the rail/tick alignment. Documented at the top of the file. Good.formatExposureat:48-51divides by 100 to convert the slider's±200range into±2.00stops. Matches the schema'sADJUST_LIMITS.exposure = { min: -2, max: 2 }. Verified.buildSliderTicksat:73-82generates ticks for the rail underlay. Theif (span <= 200) step = 50magic numbers are fine for this fixed slider, but if you ever externalize the slider widget they'd need to be props. Not blocking; mentioning.- LUT upload (
:343-348) auto-applies the first uploaded LUT. Good DX. If a user uploads three LUTs at once, only the first lands asselected, the others sit in the project tree — consistent with how other media imports behave in Studio.
packages/studio/src/components/StudioRightPanel.tsx (+234/-141)
Layout reorganization to let Design and Layers be open together (per PR body). Reads as defensive refactor — separate usePanelLayout hook integration, no visible scope creep beyond what the PR claims. The domEditingDom.ts + domSelection.ts + useDomEditTextCommits.ts deltas extend selection to video/img elements so the inspector can target them. I didn't audit these in depth — they're text/selection plumbing and CI's player-perf + Preview parity are green. If Rames takes the right-panel layout layer, he can complement here.
packages/studio/src/components/editor/manualEditingAvailability.test.ts:32-42
Two new tests covering the env-gate. Good — matches the project's discipline of testing every new env flag.
Multi-layer regime contract status
End-to-end verification: inspector → bridge → runtime.
- Inspector emits:
{ preset: string|null, intensity: number, adjust: Record<key, number>, lut: { src, intensity } | null, colorSpace: string }fromtoBridgeColorGradingat:84-93. - Bridge accepts:
payload.grading ?? null, passes straight through tocolorGrading.setGrading(target, grading)atruntime/init.ts:1818. - Runtime normalizes again via
normalizeHfColorGrading(rawGrading)atruntime/colorGrading.ts:1147, which re-runs the schema-layer predicates. Belt-and-suspenders, which is correct given the bridge accepts arbitraryunknown.
No regime drift between layers 2 and 3. The full schema → runtime → inspector triplet uses one source of truth (@hyperframes/core/color-grading), and the inspector defensively re-normalizes via normalizeHfColorGrading at :243 when reading the persisted attribute. STUDIO-5215's class of bug — where one layer's accepted shape diverges from another's required shape — is absent here.
Dispatch chain audit
COLOR_GRADING_DATA_KEY is the only new dispatch key in studio land, derived from HF_COLOR_GRADING_ATTR.replace(/^data-/, "") at :54. Pattern matches how other attribute-driven sections do their lookups. No EditOp variant; no switch needing a new case. useDomSelection.ts:5-17 extends selection to video/img which is the only "new consumer" of an existing shape, and it's additive — the prior selector set is untouched.
CI
Green. The skipped checks on this PR (Perf shards, regression-shards) are pre-existing studio-only PR behavior — the heavy render-perf matrix runs on PRs touching core/engine/player, not studio-only diffs. Matches how prior studio-only PRs in your history (#1500, #1506, #1508) gated.
Verdict
Clear-with-nits at afa94767. The asks are documentation-shaped rather than code-shaped: (1) a one-line PR-body sentence on the v1-scope decisions for intensity and split-screen-compare; (2) the upstream-PR's MAX_LUT_SIZE consolidation closes a duplicate-constant pattern across all three layers if you decide to do it. None block stamp. Re-verify if HEAD moves before stamp. Handing to Vai for stamp.
Review by Via
47a5527 to
8449708
Compare
afa9476 to
42419d0
Compare
8449708 to
d1162cd
Compare
42419d0 to
1295586
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Hello again Ular — R2 on feat(studio): add color grading inspector controls.
What I checked at the new SHA
- R1 was authored against
afa9476; head is now1295586. Two new commits since R1:3661d51(refactor: simplify color grading inspector) and1295586(refactor: simplify color grading controls). - The two new commits restructure the slider control's internal state — replacing a pair of
useState+useEffectresync with a single{ value, source }discriminator keyed to the parent value. The change reads well:valueRef.currentis the source of truth on each render, the draft is shown only while it is still keyed to the value the user grabbed, and the input field follows the same discipline. ThesetLocalDraftextraction folds the duplicated clamp + format + state-set pattern into one helper. Nothing in the dispatch chain has shifted —onCommitColorGradingcontinues to receive aNormalizedHfColorGrading, and thepropertyPanelColorGradingSection.tsx:37DEFAULT_COLOR_GRADINGconstant remains the canonical zero state. - Regime contract:
NormalizedHfColorGradingis still imported from@hyperframes/corein bothpropertyPanelColorGradingControls.tsx:6andpropertyPanelColorGradingSection.tsx:18. Single source of truth across schema → runtime → inspector continues to hold. STUDIO-5215-class drift remains structurally absent.
What did NOT move
The two R1 nits were PR-body documentation asks, and the PR body is unchanged at 1295586:
intensity = 1is still locked in v1 with no documented rationale. Same restatement — a single line in the PR's "How" section explaining whether v1 deliberately locks intensity (and what the v2 plan is) would close the loop for a future reviewer.- Compare-position defaults remain hardcoded with no rationale. Same restatement — a line on why the chosen default exists and whether future configurability is in scope would suffice.
Both are comment-level. Neither blocks a stamp.
Verdict
Clear with the same doc nits restated. Handing back to @vaivia.
Review by Via
miga-heygen
left a comment
There was a problem hiding this comment.
R2 Approve. All 3 flagged useEffect anti-patterns are fixed. All ponytail findings addressed.
useEffect fixes:
- Effect #1 (slider draft sync): Replaced with inline
{value, source}tuple derivation — no effect, no double render. Whenvalueprop changes,draftcomputes fromvalueautomatically. Textbook fix. - Effect #3 (grading sync from element): Removed. Parent now mounts
<ColorGradingSection key={targetKey}>— component remounts with correct initial state on element switch. One-line parent change, two effects eliminated. - Effect #4 (compare reset on target change): Removed. Same
key={targetKey}pattern handles this —compareEnabledreinitializes tofalseon remount.
All 4 remaining useEffects are legitimate: timer cleanup, iframe load subscription, unmount persist flush, compare cleanup on unmount.
Ponytail fixes:
- YAGNI types removed (
RuntimeColorGradingStatusState,ColorGradingCompareState,RuntimeColorGradingWindow) - Formatters inlined (
formatPercent,formatExposure,fileLabel) - Redundant
mouseuplistener removed (covered bypointerup) buildSliderTickssimplified toArray.from(new Set([min, neutral, max]))commitColorGradingwrapped inuseCallbacktoBridgeColorGradingsimplified with rest-destructure- Col-span logic uses
index === SLIDERS.length - 1instead of hardcoded key check
Clean, responsive refactor. CI green at 1295586.
— Miga
miga-heygen
left a comment
There was a problem hiding this comment.
Re-approve after rebase. HEAD is still 1295586 — no code changes since R2. All useEffect fixes confirmed, all ponytail findings addressed. Approval stands.
— Miga
What
Adds the Studio color grading inspector UI for media elements.
This is PR 3 of 3 in the color grading stack. It is stacked on top of the runtime/render PR.
Why
Studio needs a usable editing surface for the runtime feature: users should be able to adjust media color live, compare before/after, and upload their own 3D
.cubeLUTs for bug bash and real workflows.How
Test plan
Verification run:
bun run --filter @hyperframes/studio test -- src/player/lib/timelineDOM.test.ts src/components/editor/manualEditingAvailability.test.ts