diff --git a/docs/guides/color-grading.mdx b/docs/guides/color-grading.mdx
index 6961d959ff..56216efbdd 100644
--- a/docs/guides/color-grading.mdx
+++ b/docs/guides/color-grading.mdx
@@ -322,7 +322,7 @@ When grading a video, animate opacity on a wrapper element instead of directly o
| Build a terminal/editorial character treatment | `effects.ascii` + `effects.asciiSize` + an optional two-color `palette` |
| Build a restrained multi-color pixel treatment | `effects.dither` + `effects.ditherSize` + a two-to-six-color `palette` |
| Add an organic light leak | Install the finite `organic-light-leak-overlay` Registry block |
-| Build a freeze-frame cutout | Existing background removal + the `freeze-frame-cutout` Registry overlay block + host GSAP |
+| Build a freeze-frame cutout | Existing background removal + the `freeze-frame-dressing` Registry overlay block + host GSAP |
| Make a presenter float over graphics | Existing [Remove Background](/guides/remove-background) workflow |
| Put text behind a presenter | Existing `remove-background --background-output` workflow |
| Render HDR delivery files | Existing [HDR Rendering](/guides/hdr) workflow |
diff --git a/packages/cli/src/registry/registryBlocks.test.ts b/packages/cli/src/registry/registryBlocks.test.ts
index 05f0fb3974..b21e79784c 100644
--- a/packages/cli/src/registry/registryBlocks.test.ts
+++ b/packages/cli/src/registry/registryBlocks.test.ts
@@ -1,6 +1,8 @@
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { bundleToSingleHtml } from "@hyperframes/core/compiler";
+import { parseHTML } from "linkedom";
import { describe, expect, it } from "vitest";
const blocksDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../registry/blocks");
@@ -29,7 +31,7 @@ function findMissingLocalScripts(itemDir: string, manifest: RegistryManifest): s
return missing;
}
-describe("registry block manifests", () => {
+describe("registry blocks", () => {
it("installs every local script referenced by a block composition", () => {
const missing: string[] = [];
@@ -48,4 +50,18 @@ describe("registry block manifests", () => {
expect(missing).toEqual([]);
});
+
+ it("keeps the Camcorder HUD seekable inside a differently named host composition", async () => {
+ const bundled = await bundleToSingleHtml(resolve(blocksDir, "camcorder-hud"), {
+ entryFile: "demo.html",
+ });
+ const { document } = parseHTML(bundled);
+ const demo = document.getElementById("camcorder-hud-demo");
+ const hud = document.getElementById("ch-demo-overlay");
+
+ expect(demo?.getAttribute("data-composition-id")).toBe("camcorder-hud-demo");
+ expect(hud?.getAttribute("data-composition-id")).toBe("camcorder-hud");
+ expect(hud?.hasAttribute("data-composition-src")).toBe(false);
+ expect(bundled).toContain('var __hfTimelineCompId = "camcorder-hud";');
+ });
});
diff --git a/registry/blocks/camcorder-hud/camcorder-hud.html b/registry/blocks/camcorder-hud/camcorder-hud.html
new file mode 100644
index 0000000000..f5bb2d024a
--- /dev/null
+++ b/registry/blocks/camcorder-hud/camcorder-hud.html
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
REC
+
+
JAN 01 2000
12:00 AM
+
+ SP ยท 16:900:00:00
+
+
+
+
+
+
+
diff --git a/registry/blocks/camcorder-hud/demo-backdrop.jpg b/registry/blocks/camcorder-hud/demo-backdrop.jpg
new file mode 100644
index 0000000000..63bd79fb30
Binary files /dev/null and b/registry/blocks/camcorder-hud/demo-backdrop.jpg differ
diff --git a/registry/blocks/camcorder-hud/demo.html b/registry/blocks/camcorder-hud/demo.html
new file mode 100644
index 0000000000..2b5e68cc74
--- /dev/null
+++ b/registry/blocks/camcorder-hud/demo.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+ Camcorder HUD - Demo
+
+
+
+
+
+

+
+
+
+
+
+
diff --git a/registry/blocks/camcorder-hud/registry-item.json b/registry/blocks/camcorder-hud/registry-item.json
new file mode 100644
index 0000000000..41ebe3722e
--- /dev/null
+++ b/registry/blocks/camcorder-hud/registry-item.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
+ "name": "camcorder-hud",
+ "type": "hyperframes:block",
+ "title": "Camcorder HUD",
+ "description": "Responsive REC, battery, editable date placeholder, and timeline-driven counter overlay for creator-camera and camcorder treatments",
+ "tags": [
+ "camcorder",
+ "camera",
+ "recording",
+ "hud",
+ "overlay",
+ "creator",
+ "media-treatment-overlay"
+ ],
+ "dimensions": {
+ "width": 1920,
+ "height": 1080
+ },
+ "duration": 4,
+ "files": [
+ {
+ "path": "camcorder-hud.html",
+ "target": "compositions/camcorder-hud.html",
+ "type": "hyperframes:composition"
+ }
+ ],
+ "preview": {
+ "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.png",
+ "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.mp4"
+ }
+}
diff --git a/registry/blocks/editorial-flash-overlay/demo-backdrop.jpg b/registry/blocks/editorial-flash-overlay/demo-backdrop.jpg
new file mode 100644
index 0000000000..077705a002
Binary files /dev/null and b/registry/blocks/editorial-flash-overlay/demo-backdrop.jpg differ
diff --git a/registry/blocks/editorial-flash-overlay/demo.html b/registry/blocks/editorial-flash-overlay/demo.html
new file mode 100644
index 0000000000..4705557b2c
--- /dev/null
+++ b/registry/blocks/editorial-flash-overlay/demo.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
diff --git a/registry/blocks/editorial-flash-overlay/editorial-flash-overlay.html b/registry/blocks/editorial-flash-overlay/editorial-flash-overlay.html
new file mode 100644
index 0000000000..9b2c8d4502
--- /dev/null
+++ b/registry/blocks/editorial-flash-overlay/editorial-flash-overlay.html
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/registry/blocks/editorial-flash-overlay/registry-item.json b/registry/blocks/editorial-flash-overlay/registry-item.json
new file mode 100644
index 0000000000..35f1ea891a
--- /dev/null
+++ b/registry/blocks/editorial-flash-overlay/registry-item.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
+ "name": "editorial-flash-overlay",
+ "type": "hyperframes:block",
+ "title": "Editorial Flash Overlay",
+ "description": "Finite neutral-warm light layers for a seek-safe camera-flash cut or social reveal",
+ "tags": [
+ "flash",
+ "editorial",
+ "social",
+ "light",
+ "transition",
+ "overlay",
+ "media-treatment-overlay"
+ ],
+ "dimensions": {
+ "width": 1920,
+ "height": 1080
+ },
+ "duration": 4,
+ "files": [
+ {
+ "path": "editorial-flash-overlay.html",
+ "target": "compositions/editorial-flash-overlay.html",
+ "type": "hyperframes:composition"
+ }
+ ],
+ "preview": {
+ "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.png",
+ "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.mp4"
+ }
+}
diff --git a/registry/blocks/freeze-frame-dressing/demo-background.jpg b/registry/blocks/freeze-frame-dressing/demo-background.jpg
new file mode 100644
index 0000000000..4f944ae6b4
Binary files /dev/null and b/registry/blocks/freeze-frame-dressing/demo-background.jpg differ
diff --git a/registry/blocks/freeze-frame-dressing/demo-cutout.webp b/registry/blocks/freeze-frame-dressing/demo-cutout.webp
new file mode 100644
index 0000000000..383d554f34
Binary files /dev/null and b/registry/blocks/freeze-frame-dressing/demo-cutout.webp differ
diff --git a/registry/blocks/freeze-frame-dressing/demo.html b/registry/blocks/freeze-frame-dressing/demo.html
new file mode 100644
index 0000000000..cd0f2e5c84
--- /dev/null
+++ b/registry/blocks/freeze-frame-dressing/demo.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+

+

+
+
+
+
+
+
diff --git a/registry/blocks/freeze-frame-dressing/freeze-frame-dressing.html b/registry/blocks/freeze-frame-dressing/freeze-frame-dressing.html
new file mode 100644
index 0000000000..458db7e892
--- /dev/null
+++ b/registry/blocks/freeze-frame-dressing/freeze-frame-dressing.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/registry/blocks/freeze-frame-dressing/registry-item.json b/registry/blocks/freeze-frame-dressing/registry-item.json
new file mode 100644
index 0000000000..b2596eb7a7
--- /dev/null
+++ b/registry/blocks/freeze-frame-dressing/registry-item.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
+ "name": "freeze-frame-dressing",
+ "type": "hyperframes:block",
+ "title": "Freeze-Frame Dressing",
+ "description": "Timeline-driven paper, tape, and flash dressing for a freeze-frame or background-removed subject",
+ "tags": [
+ "freeze-frame",
+ "cutout",
+ "scrapbook",
+ "paper",
+ "social",
+ "overlay",
+ "media-treatment-overlay"
+ ],
+ "dimensions": {
+ "width": 1920,
+ "height": 1080
+ },
+ "duration": 4,
+ "files": [
+ {
+ "path": "freeze-frame-dressing.html",
+ "target": "compositions/freeze-frame-dressing.html",
+ "type": "hyperframes:composition"
+ }
+ ],
+ "preview": {
+ "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.png",
+ "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.mp4"
+ }
+}
diff --git a/registry/blocks/organic-light-leak-overlay/demo-backdrop.jpg b/registry/blocks/organic-light-leak-overlay/demo-backdrop.jpg
new file mode 100644
index 0000000000..e7f2ba592a
Binary files /dev/null and b/registry/blocks/organic-light-leak-overlay/demo-backdrop.jpg differ
diff --git a/registry/blocks/organic-light-leak-overlay/demo.html b/registry/blocks/organic-light-leak-overlay/demo.html
new file mode 100644
index 0000000000..f8d15dbaea
--- /dev/null
+++ b/registry/blocks/organic-light-leak-overlay/demo.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
diff --git a/registry/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html b/registry/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html
new file mode 100644
index 0000000000..206e92f847
--- /dev/null
+++ b/registry/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/registry/blocks/organic-light-leak-overlay/registry-item.json b/registry/blocks/organic-light-leak-overlay/registry-item.json
new file mode 100644
index 0000000000..1c5d2b2611
--- /dev/null
+++ b/registry/blocks/organic-light-leak-overlay/registry-item.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
+ "name": "organic-light-leak-overlay",
+ "type": "hyperframes:block",
+ "title": "Organic Light Leak Overlay",
+ "description": "Finite CSS light-leak overlay for memory beats and motivated transitions",
+ "tags": ["light-leak", "film", "memory", "transition", "overlay", "media-treatment-overlay"],
+ "dimensions": {
+ "width": 1920,
+ "height": 1080
+ },
+ "duration": 4,
+ "files": [
+ {
+ "path": "organic-light-leak-overlay.html",
+ "target": "compositions/organic-light-leak-overlay.html",
+ "type": "hyperframes:composition"
+ }
+ ],
+ "preview": {
+ "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.png",
+ "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.mp4"
+ }
+}
diff --git a/registry/registry.json b/registry/registry.json
index 5513973ee9..19d8d68ae7 100644
--- a/registry/registry.json
+++ b/registry/registry.json
@@ -75,6 +75,22 @@
"name": "grain-overlay",
"type": "hyperframes:component"
},
+ {
+ "name": "camcorder-hud",
+ "type": "hyperframes:block"
+ },
+ {
+ "name": "editorial-flash-overlay",
+ "type": "hyperframes:block"
+ },
+ {
+ "name": "organic-light-leak-overlay",
+ "type": "hyperframes:block"
+ },
+ {
+ "name": "freeze-frame-dressing",
+ "type": "hyperframes:block"
+ },
{
"name": "shimmer-sweep",
"type": "hyperframes:component"
diff --git a/scripts/generate-catalog-previews.ts b/scripts/generate-catalog-previews.ts
index 70ec84f624..d4fecf2d97 100644
--- a/scripts/generate-catalog-previews.ts
+++ b/scripts/generate-catalog-previews.ts
@@ -42,6 +42,7 @@ import {
createRenderJob,
executeRenderJob,
} from "../packages/producer/src/index.js";
+import { compileForRender } from "../packages/producer/src/services/htmlCompiler.js";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, "..");
@@ -90,10 +91,12 @@ function discoverItems(kindFilter: ItemKind | null, nameFilter: string | null):
const manifestPath = join(sourceDir, "registry-item.json");
if (!existsSync(manifestPath)) continue;
- // Blocks: find the first composition file. Components: use demo.html.
+ // Authored demos show transparent overlays against representative media.
let entryFile: string;
- if (kind === "component") {
+ if (existsSync(join(sourceDir, "demo.html"))) {
entryFile = "demo.html";
+ } else if (kind === "component") {
+ continue;
} else {
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
const compFile = manifest.files?.find(
@@ -123,7 +126,7 @@ function outputDir(kind: ItemKind): string {
return resolve(repoRoot, "docs/images/catalog", typeDir);
}
-function prepareProjectDir(item: CatalogItem): string {
+async function prepareProjectDir(item: CatalogItem): Promise {
const tmpDir = join(tmpdir(), `hf-catalog-${item.name}-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
cpSync(item.sourceDir, tmpDir, { recursive: true });
@@ -170,7 +173,6 @@ function prepareProjectDir(item: CatalogItem): string {
}
}
writeFileSync(join(tmpDir, "index.html"), content, "utf-8");
- return tmpDir;
}
}
if (!existsSync(join(tmpDir, "index.html"))) {
@@ -217,6 +219,13 @@ function prepareProjectDir(item: CatalogItem): string {
writeFileSync(join(tmpDir, "index.html"), wrapper, "utf-8");
}
+ const indexPath = join(tmpDir, "index.html");
+ const indexHtml = readFileSync(indexPath, "utf-8");
+ if (indexHtml.includes("data-composition-src")) {
+ const compiled = await compileForRender(tmpDir, indexPath, join(tmpDir, "_downloads"));
+ writeFileSync(indexPath, compiled.html, "utf-8");
+ }
+
return tmpDir;
}
@@ -259,9 +268,7 @@ async function generateThumbnail(item: CatalogItem, projectDir: string): Promise
duration = 5;
}
- // Capture at 40% of duration for a representative frame
- // Capture at 60% of duration so the animation is well underway.
- // Cap at 3s to avoid overly-late captures on long compositions.
+ // Capture after the treatment appears, capped for long compositions.
const captureTime = Math.min(3.0, duration * 0.6);
const result = await captureFrame(session, 0, captureTime);
cpSync(result.path, join(outDir, `${item.name}.png`));
@@ -327,7 +334,7 @@ async function main(): Promise {
for (const item of items) {
console.log(`[${item.kind}] ${item.name}`);
- const projectDir = prepareProjectDir(item);
+ const projectDir = await prepareProjectDir(item);
try {
await generateThumbnail(item, projectDir);
if (!skipVideo) {
diff --git a/skills-manifest.json b/skills-manifest.json
index 2dec1c9972..eaf2d70205 100644
--- a/skills-manifest.json
+++ b/skills-manifest.json
@@ -46,7 +46,7 @@
"files": 10
},
"media-use": {
- "hash": "1234dcf993bbe1fc",
+ "hash": "470beb651f5a5068",
"files": 151
},
"motion-graphics": {
diff --git a/skills/media-use/references/media-treatment-recipes.md b/skills/media-use/references/media-treatment-recipes.md
index 2a92a34436..37903ab58f 100644
--- a/skills/media-use/references/media-treatment-recipes.md
+++ b/skills/media-use/references/media-treatment-recipes.md
@@ -826,7 +826,7 @@ Extract the exact deterministic source frame first, then remove its background:
ffmpeg -ss -i -frames:v 1 -y .media/generated/freeze-source.png
npx hyperframes remove-background .media/generated/freeze-source.png \
-o .media/generated/freeze-cutout.png --json
-npx hyperframes add freeze-frame-cutout --no-clipboard
+npx hyperframes add freeze-frame-dressing --no-clipboard
```
Add the transparent result as a direct-root timed media layer and insert the