Skip to content

Commit 3f30de7

Browse files
authored
Merge pull request #2911 from heygen-com/docs/color-grading-chapter
docs(prompting): add colour grading and film effects chapter
2 parents 343c025 + 0c7c9bb commit 3f30de7

4 files changed

Lines changed: 351 additions & 3 deletions

File tree

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"prompting/overlays-and-lower-thirds",
112112
"prompting/captions-catalog",
113113
"prompting/generated-artwork",
114+
"prompting/color-grading",
114115
"prompting/vfx-and-liquid-glass",
115116
"prompting/runtimes-and-3d"
116117
]

docs/guides/color-grading.mdx

Lines changed: 180 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ skin from a broad creative grade.
123123

124124
HyperFrames supports up to four ordered selections. These are static
125125
media-level qualifiers: they do not include object tracking, rotoscoping,
126-
facial recognition, or spatial masks.
126+
facial recognition, or spatial masks. To confine a grade to a region of the
127+
frame rather than a range of colors, see
128+
[Limiting a Grade to Part of the Frame](#limiting-a-grade-to-part-of-the-frame).
127129

128130
### Scopes
129131

@@ -179,6 +181,13 @@ tested preset, then add small, explainable adjustments. Avoid inventing many
179181
unrelated curve and secondary values: they are difficult to review and easy to
180182
overcook.
181183

184+
For worked examples — ten graded A/B renders, each with the plain-language
185+
prompt that produced it and the payload it compiled to — see
186+
[Color grading and film effects](/prompting/color-grading) in the Prompt Guide.
187+
It covers the failures this page cannot: choosing a source that actually has
188+
something for the treatment to remove, and separating a subject so one part of
189+
the frame can be graded while the rest is protected.
190+
182191
## Low-Level HTML Contract
183192

184193
The CLI and Studio persist the resolved grade in `data-color-grading`:
@@ -237,6 +246,175 @@ npx hyperframes media-treatment --all --json
237246

238247
Use `--all` only for exhaustive tooling or contract inspection.
239248

249+
## Animating a Grade
250+
251+
Nine grading properties are exposed as CSS custom properties and can be tweened
252+
directly:
253+
254+
```text
255+
--hf-color-grading-ascii --hf-color-grading-bloom --hf-color-grading-blur
256+
--hf-color-grading-dither --hf-color-grading-exposure --hf-color-grading-intensity
257+
--hf-color-grading-kuwahara --hf-color-grading-pixelate
258+
--hf-color-grading-lut-intensity
259+
```
260+
261+
```js
262+
tl.to("#plate", { "--hf-color-grading-pixelate": 0.5, duration: 3, ease: "power2.inOut" }, 0.3);
263+
```
264+
265+
Start the value at identity in the payload and in an inline `style`, so frame 0
266+
is the ungraded reference and every intermediate value is rendered rather than
267+
just the endpoints.
268+
269+
Two things this list does **not** give you:
270+
271+
- **`--hf-color-grading-intensity` scales the primary grade only.** The shader
272+
mixes between the ungraded sample and the graded one at that value, so it
273+
ramps `adjust`, `wheels`, `curves`, `hueCurves`, `secondaries` and the LUT.
274+
`details` and `effects` sit outside that mix entirely and it scales neither.
275+
Some of them shape the sampled media *before* the mix — CRT warp, blur,
276+
Kuwahara, pixelate, the tape family, chromatic aberration, digital glitch —
277+
and so are already present on both sides of it. Others run *after* it —
278+
grain, halftone, bloom, scanlines, vignette. Either way intensity does not
279+
reach them, so it is not a master "ramp the whole look" dial when the look is
280+
effect-based; animate the specific effect instead.
281+
- **Every other property — `halftone`, `twoInkPrint`, `tapeDamage`, hue curves,
282+
secondaries — has no custom property**, so it cannot be tweened this way.
283+
284+
For some of those, you can drive the payload itself from the timeline.
285+
Rewriting `data-color-grading` re-applies the grade:
286+
287+
```js
288+
const plate = document.getElementById("plate");
289+
const v = { halftone: 0, bloom: 0 };
290+
291+
tl.to(v, {
292+
halftone: 0.72,
293+
bloom: 0.7,
294+
duration: 3.4,
295+
ease: "power2.inOut",
296+
onUpdate: () => {
297+
plate.setAttribute("data-color-grading", JSON.stringify({
298+
intensity: 1,
299+
effects: {
300+
halftone: v.halftone, halftoneSize: 0.38,
301+
bloom: v.bloom, bloomRadius: 18,
302+
},
303+
}));
304+
},
305+
}, 0.3);
306+
```
307+
308+
This stays deterministic: the value is derived from timeline position, never
309+
from wall-clock time, so seeking to a frame always produces the same result.
310+
It costs a payload parse per tick, so prefer a custom property when one exists.
311+
312+
<Warning>
313+
**This does not work for every effect.** It is verified working for `halftone`
314+
and `twoInkPrint`. It is verified *not* working for `crtCurvature`,
315+
`scanlines`, `chromaBleed`, and `chromaticAberration` — the identical payload
316+
applied statically renders correctly, but the same values driven through
317+
`setAttribute` produce no visible change, and seeding them non-zero at init
318+
does not help. Measure the effect you intend to animate before committing to
319+
it, and fall back to a static treatment if the ramp does not move.
320+
</Warning>
321+
322+
## Limiting a Grade to Part of the Frame
323+
324+
Grading qualifies pixels **by value, never by position**. Selections key on hue,
325+
saturation, and luma; the one spatially-varying control, `vignette`, is locked
326+
to the frame centre and cannot be moved or reshaped into a power window. There
327+
are no masks, shapes, or tracked regions in the contract.
328+
329+
A grade also applies to a whole media element. So to treat part of the frame,
330+
split that part into its own layer and grade the layer:
331+
332+
| You want | Build |
333+
| --- | --- |
334+
| Subject graded, background clean | Matted cutout on top, graded; original clip underneath, clean |
335+
| Background graded, subject clean | Original clip underneath, graded; matted cutout on top, clean |
336+
| One region of the subject graded | Two copies of the cutout — clean underneath, graded copy on top clipped to the region |
337+
338+
Two rules that are easy to get wrong:
339+
340+
- **Use the original clip as the background plate, not a subject-removed
341+
plate.** A subject-removed plate is a hole where the subject was, and a
342+
feathered cutout composited over it produces a dark rim.
343+
- **If your cutout ships premultiplied alpha**, the browser composites it as
344+
straight and edge pixels get multiplied twice, giving a black outline. Rebuild
345+
it from the original plus its matte:
346+
`ffmpeg -i clip.mp4 -i matte.mp4 -filter_complex "[1:v]format=gray[m];[0:v][m]alphamerge" -c:v libvpx-vp9 out.webm`
347+
348+
### Worked Example: Redacting a Face
349+
350+
Only the face is pixelated. The rest of the subject and the entire room are
351+
untouched. Three layers, and the region comes from `clip-path` on the third:
352+
353+
```html index.html
354+
<div class="band">
355+
<!-- 1. the room, clean -->
356+
<video src="media/room.mp4" muted
357+
data-start="0" data-duration="4" data-track-index="2"></video>
358+
359+
<!-- 2. the subject, clean -->
360+
<video src="media/subject.webm" muted class="fg"
361+
data-start="0" data-duration="4" data-track-index="3"></video>
362+
363+
<!-- 3. the same subject, graded, clipped to the head -->
364+
<video src="media/subject.webm" muted class="fx"
365+
data-start="0" data-duration="4" data-track-index="4"
366+
style="clip-path: ellipse(9.8% 15.5% at 50.7% 14.5%)"
367+
data-color-grading='{"intensity":1,"effects":{"pixelate":0.5}}'></video>
368+
</div>
369+
```
370+
371+
```css
372+
.band .fg { z-index: 2; }
373+
.band .fx { z-index: 3; }
374+
```
375+
376+
Everything outside the clip falls through to the clean copy beneath. Because the
377+
graded layer *is* the cutout, the ellipse can overshoot the head without
378+
touching the room — there is nothing outside the silhouette to paint — so size
379+
it generously rather than tightly around the features.
380+
381+
Pixelate preserves alpha, so the block grid clips to the silhouette instead of
382+
filling a rectangle.
383+
384+
<Warning>
385+
A fixed `clip-path` is only valid for footage where the subject barely moves.
386+
Measure before relying on it. Anything with real subject movement needs a
387+
tracked matte produced outside HyperFrames — there is no tracking in the
388+
grading pipeline.
389+
</Warning>
390+
391+
The prompt that produces the composition above:
392+
393+
```text
394+
Take room.mp4 and its matted cutout subject.webm and build a 4-second
395+
1920x1080 composition that redacts only the subject's face.
396+
397+
Three stacked layers inside one band, all playing the same 4 seconds:
398+
1. room.mp4, no grading at all
399+
2. subject.webm, no grading at all
400+
3. subject.webm again, on top, carrying the grade
401+
402+
Layer 3 is the only graded element: pixelate at 0.5, held constant for the
403+
whole shot — no ramp and no fade-up. A redaction that animates on reads as an
404+
effect; one that is simply on reads as policy. Mid strength should leave a
405+
recognisable human shape without a readable face.
406+
407+
Limit layer 3 to the head with a CSS clip-path ellipse. Do not try to do this
408+
with the grading payload — it has no spatial masking. Size the ellipse
409+
generously so it takes in the hairline, ears, and jaw rather than sitting tight
410+
on the features; it can overshoot the head safely, because outside the
411+
silhouette the cutout is transparent and there is nothing to paint.
412+
413+
Use the original room clip as the background plate rather than a
414+
subject-removed plate, and if the cutout has premultiplied alpha, rebuild it
415+
with ffmpeg alphamerge first so the edges do not come out with a black rim.
416+
```
417+
240418
## Custom LUTs
241419

242420
HyperFrames supports project-local 3D `.cube` LUTs:
@@ -275,7 +453,7 @@ validation, and rendered comparisons to evaluate it.
275453
| HDR delivery render | Separate workflow | The native HDR compositor preserves HDR source pixels and does not apply this SDR grading/effects pipeline to native HDR layers |
276454
| LOG camera footage | Partial | Requires a known matching transform/LUT; no automatic camera profile management |
277455
| Full-scene grade including DOM/text | Not supported | Current grading targets individual media elements |
278-
| Face or region tracking | Not supported | Use a separate isolated media layer or external tracking/masking workflow |
456+
| Face or region tracking | Not supported | No masks or tracking in the contract. Isolate the region as its own layer — see [Limiting a Grade to Part of the Frame](#limiting-a-grade-to-part-of-the-frame) |
279457
| Remote media | Partial | Requires compatible CORS headers; project-local assets are reliable |
280458
| ACES/OCIO finishing | Not supported | Outside the current browser shader pipeline |
281459

0 commit comments

Comments
 (0)