Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/concepts/variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,45 @@ The same pattern covers the three media element types:

Pass assets as URL references your composition resolves at render time; don't inline base64. URL-shaped assets travel cleanly through both the local renderer and the Lambda surface — see [Templates on Lambda](/deploy/templates-on-lambda#working-with-large-variables) for the 256 KiB execution-input cap on distributed renders.

### Parameterizing media color grading

Media color grading can also read exact variable references inside
`data-color-grading`. Use `$name` or `${name}` as the entire value for a field;
the runtime resolves it from the current composition's variables before applying
the shader grading:

```html compositions/hero.html
<html data-composition-variables='[
{"id":"gradingPreset","type":"enum","label":"Color grading preset","default":"warm-clean",
"options":[{"value":"warm-clean","label":"Warm Clean"},{"value":"cool-clean","label":"Cool Clean"}]},
{"id":"gradingIntensity","type":"number","label":"Color grading intensity","default":0.75,"min":0,"max":1,"step":0.05},
{"id":"gradingExposure","type":"number","label":"Exposure","default":0,"min":-2,"max":2,"step":0.05}
]'>
<body>
<div data-composition-id="hero" data-width="1920" data-height="1080">
<video
id="hero-video"
src="assets/hero.mp4"
data-start="0"
data-track-index="0"
muted
playsinline
data-color-grading='{
"preset":"$gradingPreset",
"intensity":"$gradingIntensity",
"adjust":{"exposure":"${gradingExposure}"},
"colorSpace":"rec709"
}'
></video>
</div>
</body>
</html>
```

When the same composition is embedded multiple times, each host's
`data-variable-values` can produce different grading without copying or rewriting
the media element's `data-color-grading` JSON.

### Swapping media: do you need to vary duration too?

A common follow-up: if a variable swaps a `<video>` to a different clip, does `data-duration` need to change too? Usually no. `data-duration` is optional on `<video>` and `<audio>` — leave it off and the renderer ffprobes the source and uses its natural length:
Expand Down
16 changes: 16 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
"import": "./src/compiler/index.ts",
"types": "./src/compiler/index.ts"
},
"./color-grading": {
"import": "./src/colorGrading.ts",
"types": "./src/colorGrading.ts"
},
"./color-luts": {
"import": "./src/colorLuts.ts",
"types": "./src/colorLuts.ts"
},
"./runtime": "./dist/hyperframe.runtime.iife.js",
"./runtime/clipTree": {
"import": "./src/runtime/clipTree.ts",
Expand Down Expand Up @@ -129,6 +137,14 @@
"import": "./dist/compiler/index.js",
"types": "./dist/compiler/index.d.ts"
},
"./color-grading": {
"import": "./dist/colorGrading.js",
"types": "./dist/colorGrading.d.ts"
},
"./color-luts": {
"import": "./dist/colorLuts.js",
"types": "./dist/colorLuts.d.ts"
},
"./runtime": "./dist/hyperframe.runtime.iife.js",
"./runtime/lottie-readiness": {
"import": "./dist/lottieReadiness.js",
Expand Down
108 changes: 108 additions & 0 deletions packages/core/src/colorGrading.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, it } from "vitest";
import {
HF_COLOR_GRADING_COLOR_SPACE,
isHfColorGradingActive,
normalizeHfColorGrading,
normalizeHfColorGradingWithVariables,
serializeHfColorGrading,
} from "./colorGrading";

describe("color grading", () => {
it("parses preset shorthand", () => {
const grading = normalizeHfColorGrading("warm-clean");
expect(grading?.preset).toBe("warm-clean");
expect(grading?.colorSpace).toBe(HF_COLOR_GRADING_COLOR_SPACE);
expect(grading?.adjust.temperature).toBeGreaterThan(0);
expect(isHfColorGradingActive(grading)).toBe(true);
});

it("merges manual adjustments over preset values", () => {
const grading = normalizeHfColorGrading({
preset: "warm-clean",
intensity: 0.5,
adjust: { temperature: -0.25, contrast: 0.2 },
});
expect(grading?.intensity).toBe(0.5);
expect(grading?.adjust.temperature).toBe(-0.25);
expect(grading?.adjust.contrast).toBe(0.2);
expect(grading?.adjust.saturation).toBeGreaterThan(0);
});

it("clamps values to supported shader ranges", () => {
const grading = normalizeHfColorGrading({
intensity: 2,
adjust: { exposure: 10, contrast: -5, saturation: 3 },
lut: { src: "looks/test.cube", intensity: 3 },
});
expect(grading?.intensity).toBe(1);
expect(grading?.adjust.exposure).toBe(2);
expect(grading?.adjust.contrast).toBe(-1);
expect(grading?.adjust.saturation).toBe(1);
expect(grading?.lut?.intensity).toBe(1);
});

it("returns null for disabled or invalid grading", () => {
expect(normalizeHfColorGrading({ enabled: false, preset: "warm-clean" })).toBeNull();
expect(normalizeHfColorGrading("{nope")).toBeNull();
expect(normalizeHfColorGrading("")).toBeNull();
});

it("serializes normalized grading for data-color-grading", () => {
const grading = normalizeHfColorGrading({ adjust: { exposure: 0.25 } });
const serialized = serializeHfColorGrading(grading);
expect(serialized).toContain('"exposure":0.25');
expect(normalizeHfColorGrading(serialized)?.adjust.exposure).toBe(0.25);
});

it("treats zero global intensity as inactive even with LUT data", () => {
const grading = normalizeHfColorGrading({
intensity: 0,
adjust: { exposure: 0.5 },
lut: { src: "assets/luts/test.cube", intensity: 1 },
});
expect(isHfColorGradingActive(grading)).toBe(false);
});

it("resolves exact variable references inside color grading JSON", () => {
const grading = normalizeHfColorGradingWithVariables(
JSON.stringify({
preset: "$preset",
intensity: "$gradingIntensity",
adjust: {
exposure: "${exposure}",
saturation: "$saturation",
},
lut: {
src: "$lutSrc",
intensity: "$lutIntensity",
},
}),
{
preset: "warm-clean",
gradingIntensity: 0.6,
exposure: 0.25,
saturation: -0.2,
lutSrc: "assets/luts/warm.cube",
lutIntensity: 0.4,
},
);

expect(grading?.preset).toBe("warm-clean");
expect(grading?.intensity).toBe(0.6);
expect(grading?.adjust.exposure).toBe(0.25);
expect(grading?.adjust.saturation).toBe(-0.2);
expect(grading?.lut).toEqual({ src: "assets/luts/warm.cube", intensity: 0.4 });
});

it("supports a whole grading supplied by one variable", () => {
const grading = normalizeHfColorGradingWithVariables("$colorGrade", {
colorGrade: {
adjust: { contrast: 0.2 },
lut: { src: "assets/luts/natural-boost.cube", intensity: 0.75 },
},
});

expect(grading?.adjust.contrast).toBe(0.2);
expect(grading?.lut).toEqual({ src: "assets/luts/natural-boost.cube", intensity: 0.75 });
});
});
Loading
Loading