Repository navigation
fix(core): color grading applies to canvas compositions - #5860
Conversation
data-color-grading on a <canvas> was ignored by the runtime and refused by lint, the Studio panel and the CLI writer. One source-tag list in the grading contract now feeds all of them. A canvas is sized from its drawing buffer and re-uploaded on every seek and transport tick.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
somanshreddy
left a comment
There was a problem hiding this comment.
Clean at f8bdb30c: no blocker.
- All four selection sites (the runtime selector, lint, the editing affordance and
media-treatment) now read the oneCOLOR_GRADING_SOURCE_TAGSlist.readSourceSizeuses a canvas's drawing buffer (width/height), not its CSS box, and a 0×0 canvas is treated as not drawable. redrawsOnTransportTickkeeps the old video and animated-property behaviour unchanged and adds only "a canvas always redraws".- The runtime's own output canvas can't be picked up by
refresh(): it never carriesdata-color-grading, and the selector requires that attribute. - A tainted canvas (one that drew a cross-origin image without CORS) fails open.
texImage2Dthrows insiderenderGradedFrame, underdrawEntry's try, andhideSourceElementruns only after a successful draw, so the source stays visible and ungraded.
Non-blocking:
isColorGradingMediaElementnow accepts any canvas, including the runtime's own[data-hf-color-grading-canvas]output. Through the live API'sresolveTarget, a target with nohfId/id, such as{selector: "canvas", selectorIndex: 1}, can land on the output canvas that was inserted after a graded canvas, and grade the grading canvas. So can the id__hf_color_grading_<id>. ExcludingCOLOR_GRADING_CANVAS_ATTRin that guard closes it.- A tainted canvas also hits
swallow("runtime.colorGrading.drawEntry")on every transport tick, because canvases always redraw. It may be worth latchingdrawErrorso it stops retrying. Commentsis red, though not required: the comment ratchet flags +2 lines inruntime/colorGrading.tsand +1 incolorGradingContract.ts.
— Somu
Edit accuracy: accurate 271 (base branch 271), smooth 190 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (2)
|
somanshreddy
left a comment
There was a problem hiding this comment.
Re-approve at 7c54a9ca, the delta from my reviewed head f8bdb30c. It's one commit that deletes 3 comment lines, two in runtime/colorGrading.ts and one in colorGradingContract.ts, with no code change. My review at f8bdb30c stands, including its non-blocking notes 1 and 2. Note 3 (the comment ratchet) is resolved by this commit.
— Somu
What
A
<canvas>withdata-color-gradingis now graded in preview and inhyperframes render, the same way an<img>or<video>is, and the grade follows every frame the canvas draws. Before this, the attribute on a canvas did nothing at runtime, and lint reported it as an error.Why
Hand-drawn and generative compositions paint into a canvas. Grading only worked on
<img>and<video>, so those compositions could not be graded at all.Related work
No open or merged PR covers canvas grading.
How
COLOR_GRADING_SOURCE_TAGS(video,img,canvas) lives in the grading contract (@hyperframes/parsers/color-grading-contract). Every place that picks a grading source reads it: the runtime refresh selector andisColorGradingMediaElement, thecolor_grading_non_medialint rule, the editing affordance that shows the grading section, andhyperframes media-treatment.readSourceSizeis now the single owner for all three kinds. A canvas is measured by its drawing buffer (width/height), not its CSS box, and is drawable once both are above zero.requestVideoFrameCallback. Every seek already ends incolorGrading.redraw(), which re-uploads every graded source. In preview this runs straight after the timeline andhf-seeklisteners; in render it runs throughwaitForActiveLutsafter the page's seek handlers settle. So a canvas drawn inhf-seekor a GSAPonUpdateis graded at the current frame. During playback the transport tick callsredrawAnimated(), which skipped any entry without animated grading properties; it now always redraws canvas sources (redrawsOnTransportTick).currentSrc, so its seed comes from its id or position.Test plan
Unit tests (vitest, one file per run, each run 3 times in a row):
core/src/runtime/colorGrading.test.tsa graded canvastests fail:expected false to be true(not graded) andexpected { state: 'missing' } to match { state: 'pending' }core/src/editing/affordances.test.tscanvas: colorGradingfailslint/src/rules/media.test.tsaccepts color grading on a canvasfails (color_grading_non_mediareported)cli/src/commands/media-treatment.test.tsColor grading requires an <img> or <video>; selector matched <canvas>parsers/src/colorGradingContract.test.ts,.docs.test.tsMutation checks on the runtime: removing the canvas case from the transport tick fails the tick test (
expected +0 to be 1); measuring a canvas as 0x0 fails both canvas tests. Typecheck of core, lint, parsers and cli is green, as are the oxlint, oxfmt and fallow pre-commit hooks.End to end. The fixture is a 640x360, 3 s composition with a 2D canvas painted on
hf-seek: a grey patch, a patch that is red before 1.5 s and blue after, and a white bar that moves with time. Its grade is{"adjust":{"exposure":0.8,"temperature":0.6},"effects":{"bloom":0.5}}. I rendered it as a 10 fps PNG sequence with this branch's CLI and with the published 0.8.172 CLI. Between 0.8.172 and this branch's base, the runtime grading code, the lint rule and the affordances are unchanged. Mean RGB of each patch:At t=2.5 s the branch shows the blue patch and the bar at its late position, graded. The bar's early position (x=150) reads as graded background, so the render is not reusing an old frame.
Preview. In the branch's preview server,
__player.seek(2.5)gave a pixel of 82,70,255 on the grading canvas. Afterplay()for 2 s (t=2.00), the bar was graded white at its t=2.00 position and its old spot was background, so the playback tick updates the grade.WebGL source. The same check with a WebGL canvas cleared on
hf-seek, once withpreserveDrawingBuffer: falseand once withtrue: both renders were graded and current (t=0: 235,223,77; t=0.75 s: 101,223,255).Before / After
Left: published 0.8.172 (canvas ignored). Right: this branch. Frames from the fixture render at t=0.5 s and t=2.5 s.
Known limits
preserveDrawingBuffer. The WebGL spec lets such a canvas clear its buffer once it is presented. The macOS render read the current frame either way. A redraw that runs outside the frame the source painted in (for example a window resize in preview, before the next seek) could upload a cleared buffer. I did not reproduce this;preserveDrawingBuffer: trueavoids it.hf-seek(throughwaitUntil). In preview they are re-graded on the next redraw, not when their promise settles. Render waits for those handlers before grading.