Merge pull request #2754 from heygen-com/feat/media-treatment-overlays

feat(registry): add media treatment overlays
This commit is contained in:
Ular Kimsanov
2026-07-24 19:52:06 -07:00
committed by GitHub
23 changed files with 1033 additions and 12 deletions
+1 -1
View File
@@ -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 |
@@ -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";');
});
});
+163
View File
@@ -0,0 +1,163 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#ch-hud {
--hf-camcorder-color: #fff;
--hf-camcorder-accent: #f12c2c;
--hf-camcorder-inset: 5vmin;
--hf-camcorder-font-size: 3.7vmin;
position: absolute;
inset: 0;
color: var(--hf-camcorder-color);
font-family: monospace;
font-size: var(--hf-camcorder-font-size);
font-weight: 700;
line-height: 1.25;
pointer-events: none;
-webkit-text-stroke: 0.12vmin rgb(0 0 0 / 92%);
text-shadow: 0 0.2vmin 0.6vmin rgb(0 0 0 / 90%);
}
#ch-hud .ch-corner {
position: absolute;
}
#ch-hud .ch-record {
top: var(--hf-camcorder-inset);
left: var(--hf-camcorder-inset);
display: flex;
align-items: center;
gap: 1.8vmin;
}
#ch-hud .ch-record-dot {
width: 2.2vmin;
height: 2.2vmin;
border-radius: 50%;
background: var(--hf-camcorder-accent);
box-shadow: 0 0 0.9vmin color-mix(in srgb, var(--hf-camcorder-accent) 55%, transparent);
}
#ch-hud .ch-battery {
top: var(--hf-camcorder-inset);
right: var(--hf-camcorder-inset);
width: 7vmin;
height: 3.5vmin;
border: 0.35vmin solid currentcolor;
}
#ch-hud .ch-battery::before {
position: absolute;
top: 0.7vmin;
left: 0.7vmin;
width: 4.7vmin;
height: 1.4vmin;
background: currentcolor;
content: "";
}
#ch-hud .ch-battery::after {
position: absolute;
top: 0.7vmin;
right: -1vmin;
width: 0.7vmin;
height: 1.4vmin;
background: currentcolor;
content: "";
}
#ch-hud .ch-date {
bottom: var(--hf-camcorder-inset);
left: var(--hf-camcorder-inset);
}
#ch-hud .ch-counter {
right: var(--hf-camcorder-inset);
bottom: var(--hf-camcorder-inset);
text-align: right;
}
#ch-hud .ch-mode {
display: block;
margin-bottom: 0.7vmin;
font-size: 2.6vmin;
}
</style>
</head>
<body>
<div
id="camcorder-hud"
data-composition-id="camcorder-hud"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="ch-hud" data-hf-counter-start="0" aria-hidden="true">
<div class="ch-corner ch-record"><span class="ch-record-dot"></span><span>REC</span></div>
<div class="ch-corner ch-battery"></div>
<div class="ch-corner ch-date">JAN 01 2000<br />12:00 AM</div>
<div class="ch-corner ch-counter">
<span class="ch-mode">SP · 16:9</span><span class="ch-counter-value">00:00:00</span>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var hud = document.getElementById("ch-hud");
var counter = hud && hud.querySelector(".ch-counter-value");
var recordDot = hud && hud.querySelector(".ch-record-dot");
var host = hud && hud.closest("[data-composition-id]");
if (!hud || !counter || !recordDot || !host) {
throw new Error("Camcorder HUD could not find its composition host");
}
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var counterStart = Number(hud.getAttribute("data-hf-counter-start")) || 0;
var state = { seconds: 0 };
function formatCounter(value) {
var seconds = Math.max(0, Math.floor(value));
var hours = Math.floor(seconds / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
var remainder = seconds % 60;
return [hours, minutes, remainder]
.map(function (part) {
return String(part).padStart(2, "0");
})
.join(":");
}
function updateHud() {
counter.textContent = formatCounter(counterStart + state.seconds);
recordDot.style.opacity = Math.floor(state.seconds / 0.55) % 2 === 0 ? "1" : "0.25";
}
var tl = gsap.timeline({ paused: true });
tl.to(state, { seconds: duration, duration: duration, ease: "none", onUpdate: updateHud }, 0);
updateHud();
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

+73
View File
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Camcorder HUD - Demo</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
}
html,
body,
#camcorder-hud-demo {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: #050505;
}
#ch-demo-backdrop,
#ch-demo-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
#ch-demo-backdrop {
object-fit: cover;
}
</style>
</head>
<body>
<div
id="camcorder-hud-demo"
data-composition-id="camcorder-hud-demo"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<img
id="ch-demo-backdrop"
class="clip"
src="demo-backdrop.jpg"
data-start="0"
data-duration="4"
data-track-index="0"
alt="Creator recording a video"
/>
<div
id="ch-demo-overlay"
data-composition-id="camcorder-hud"
data-composition-src="camcorder-hud.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
tl.to("#ch-demo-backdrop", { scale: 1.02, duration: 4, ease: "none" }, 0);
window.__timelines = window.__timelines || {};
window.__timelines["camcorder-hud-demo"] = tl;
</script>
</body>
</html>
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

+73
View File
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#editorial-flash-demo {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: #080808;
}
#ef-demo-backdrop,
#ef-demo-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
#ef-demo-backdrop {
object-fit: cover;
}
</style>
</head>
<body>
<div
id="editorial-flash-demo"
data-composition-id="editorial-flash-demo"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<img
id="ef-demo-backdrop"
class="clip"
src="./demo-backdrop.jpg"
alt=""
data-start="0"
data-duration="4"
data-track-index="0"
/>
<div
id="ef-demo-overlay"
data-composition-id="editorial-flash-overlay"
data-composition-src="editorial-flash-overlay.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
</div>
<script>
var tl = gsap.timeline({ paused: true });
tl.to("#ef-demo-backdrop", { scale: 1.035, duration: 4, ease: "none" }, 0);
window.__timelines = window.__timelines || {};
window.__timelines["editorial-flash-demo"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#ef-flash {
--hf-editorial-flash-wash: 255 253 250;
--hf-editorial-flash-warm: 255 174 105;
--hf-editorial-flash-cool: 183 218 255;
position: absolute;
inset: -12%;
overflow: hidden;
pointer-events: none;
}
#ef-flash .ef-layer {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity, transform;
}
#ef-wash {
background: rgb(var(--hf-editorial-flash-wash));
}
#ef-core {
inset: -18%;
background: radial-gradient(
ellipse 68% 92% at 37% 47%,
rgb(255 255 255) 0%,
rgb(255 255 255 / 98%) 20%,
rgb(var(--hf-editorial-flash-wash) / 86%) 45%,
rgb(var(--hf-editorial-flash-warm) / 32%) 67%,
transparent 86%
);
mix-blend-mode: screen;
transform-origin: 38% 48%;
}
#ef-sweep {
inset: -28%;
background: linear-gradient(
108deg,
transparent 21%,
rgb(var(--hf-editorial-flash-warm) / 10%) 38%,
rgb(var(--hf-editorial-flash-wash) / 90%) 49%,
rgb(255 255 255 / 98%) 53%,
rgb(var(--hf-editorial-flash-cool) / 24%) 62%,
transparent 79%
);
mix-blend-mode: screen;
}
</style>
</head>
<body>
<div
id="editorial-flash-overlay"
data-composition-id="editorial-flash-overlay"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="ef-flash" aria-hidden="true">
<div id="ef-wash" class="ef-layer"></div>
<div id="ef-core" class="ef-layer" data-layout-allow-overflow></div>
<div id="ef-sweep" class="ef-layer" data-layout-allow-overflow></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var flash = document.getElementById("ef-flash");
var host = flash && flash.closest("[data-composition-id]");
if (!host) throw new Error("Editorial Flash could not find its composition host");
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var hit = Math.max(0.12, duration * 0.58);
var tl = gsap.timeline({ paused: true });
tl.to({}, { duration: duration, ease: "none" }, 0);
tl.to("#ef-wash", { opacity: 0.92, duration: 0.04, ease: "power4.in" }, hit);
tl.fromTo(
"#ef-core",
{ opacity: 0, scale: 0.86, rotation: -5 },
{ opacity: 1, scale: 1, rotation: -5, duration: 0.05, ease: "power4.in" },
hit - 0.01,
);
tl.fromTo(
"#ef-sweep",
{ opacity: 0, xPercent: -12, rotation: -2 },
{ opacity: 0.9, xPercent: 8, rotation: -2, duration: 0.04, ease: "power4.in" },
hit,
);
tl.to("#ef-wash", { opacity: 0, duration: 0.18, ease: "power3.out" }, hit + 0.04);
tl.to(
"#ef-core",
{ opacity: 0, scale: 1.18, rotation: -5, duration: 0.34, ease: "power2.out" },
hit + 0.04,
);
tl.to(
"#ef-sweep",
{ opacity: 0, xPercent: 28, rotation: -2, duration: 0.3, ease: "power2.out" },
hit + 0.04,
);
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

+102
View File
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#freeze-frame-demo {
width: 960px;
height: 540px;
margin: 0;
overflow: hidden;
background: #111;
}
#freeze-demo-background,
#freeze-demo-cutout,
#freeze-demo-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
#freeze-demo-background {
object-fit: cover;
filter: saturate(0.28) contrast(1.1) brightness(0.72);
transform: scale(1.04) rotate(-0.4deg);
}
#freeze-demo-cutout {
z-index: 10;
object-fit: contain;
filter: drop-shadow(3px 0 0 #fff) drop-shadow(-3px 0 0 #fff) drop-shadow(0 3px 0 #fff)
drop-shadow(0 -3px 0 #fff) drop-shadow(14px 18px 8px rgb(16 18 23 / 46%));
}
#freeze-demo-overlay {
z-index: 20;
}
</style>
</head>
<body>
<div
id="freeze-frame-demo"
data-composition-id="freeze-frame-demo"
data-start="0"
data-duration="4"
data-width="960"
data-height="540"
>
<img
id="freeze-demo-background"
class="clip"
src="./demo-background.jpg"
alt=""
data-start="0"
data-duration="4"
data-track-index="0"
/>
<img
id="freeze-demo-cutout"
class="clip"
src="./demo-cutout.webp"
alt=""
data-start="0"
data-duration="4"
data-track-index="1"
/>
<div
id="freeze-demo-overlay"
data-composition-id="freeze-frame-dressing"
data-composition-src="freeze-frame-dressing.html"
data-start="0"
data-duration="4"
data-track-index="2"
data-width="960"
data-height="540"
></div>
</div>
<script>
var tl = gsap.timeline({ paused: true });
tl.fromTo(
"#freeze-demo-cutout",
{ y: 42, scale: 0.86, rotation: -2 },
{ y: 0, scale: 1, rotation: 0.4, duration: 0.5, ease: "back.out(1.35)" },
1,
);
tl.to({}, { duration: 4, ease: "none" }, 0);
window.__timelines = window.__timelines || {};
window.__timelines["freeze-frame-demo"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#hf-freeze-paper {
position: absolute;
inset: 0;
opacity: 0;
background:
linear-gradient(115deg, rgb(248 84 73 / 86%) 0 16%, transparent 16%),
linear-gradient(345deg, rgb(255 221 82 / 92%) 0 17%, transparent 17%),
linear-gradient(rgb(244 240 224 / 78%), rgb(244 240 224 / 78%));
clip-path: polygon(2% 8%, 97% 2%, 94% 94%, 6% 98%);
mix-blend-mode: screen;
pointer-events: none;
}
.hf-freeze-tape {
position: absolute;
width: 12.5vmin;
height: 2.9vmin;
opacity: 0;
background: rgb(255 236 168 / 76%);
border: 1px solid rgb(110 91 55 / 18%);
box-shadow: 0 0.3vmin 0.8vmin rgb(20 16 10 / 18%);
pointer-events: none;
}
#hf-freeze-tape-a {
top: 8%;
left: 12.5%;
transform: rotate(-8deg);
}
#hf-freeze-tape-b {
right: 12.5%;
bottom: 7%;
transform: rotate(7deg);
}
#hf-freeze-flash {
position: absolute;
inset: 0;
opacity: 0;
background: #fff;
pointer-events: none;
}
</style>
</head>
<body>
<div
id="freeze-frame-dressing"
data-composition-id="freeze-frame-dressing"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="hf-freeze-paper" aria-hidden="true"></div>
<div id="hf-freeze-tape-a" class="hf-freeze-tape" aria-hidden="true"></div>
<div id="hf-freeze-tape-b" class="hf-freeze-tape" aria-hidden="true"></div>
<div id="hf-freeze-flash" aria-hidden="true"></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var paper = document.getElementById("hf-freeze-paper");
var host = paper && paper.closest("[data-composition-id]");
if (!host) throw new Error("Freeze-Frame Dressing could not find its composition host");
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var hit = Math.max(0.1, duration * 0.24);
var exit = Math.max(hit + 0.7, duration - 0.3);
var tl = gsap.timeline({ paused: true });
tl.to({}, { duration: duration, ease: "none" }, 0);
tl.to("#hf-freeze-flash", { opacity: 0.95, duration: 0.06, ease: "power2.out" }, hit);
tl.to("#hf-freeze-flash", { opacity: 0, duration: 0.24, ease: "power2.in" }, hit + 0.06);
tl.to("#hf-freeze-paper", { opacity: 0.64, duration: 0.18, ease: "power2.out" }, hit + 0.04);
tl.fromTo(
"#hf-freeze-tape-a",
{ opacity: 0, scale: 1.18 },
{ opacity: 1, scale: 1, duration: 0.24, ease: "power2.out" },
hit + 0.3,
);
tl.fromTo(
"#hf-freeze-tape-b",
{ opacity: 0, scale: 1.18 },
{ opacity: 1, scale: 1, duration: 0.24, ease: "power2.out" },
hit + 0.42,
);
tl.to(
["#hf-freeze-paper", "#hf-freeze-tape-a", "#hf-freeze-tape-b"],
{ opacity: 0, duration: 0.24, ease: "power2.in" },
exit,
);
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+77
View File
@@ -0,0 +1,77 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#organic-light-leak-demo {
width: 960px;
height: 540px;
margin: 0;
overflow: hidden;
background: #090909;
}
#light-leak-demo-backdrop,
#light-leak-demo-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
#light-leak-demo-backdrop {
object-fit: cover;
}
#light-leak-demo-overlay {
z-index: 20;
}
</style>
</head>
<body>
<div
id="organic-light-leak-demo"
data-composition-id="organic-light-leak-demo"
data-start="0"
data-duration="4"
data-width="960"
data-height="540"
>
<img
id="light-leak-demo-backdrop"
class="clip"
src="./demo-backdrop.jpg"
alt=""
data-start="0"
data-duration="4"
data-track-index="0"
/>
<div
id="light-leak-demo-overlay"
data-composition-id="organic-light-leak-overlay"
data-composition-src="organic-light-leak-overlay.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="960"
data-height="540"
></div>
</div>
<script>
var tl = gsap.timeline({ paused: true });
tl.to("#light-leak-demo-backdrop", { scale: 1.025, duration: 4, ease: "none" }, 0);
window.__timelines = window.__timelines || {};
window.__timelines["organic-light-leak-demo"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,125 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#hf-light-leak {
position: absolute;
inset: -16%;
opacity: 0;
overflow: hidden;
pointer-events: none;
mix-blend-mode: screen;
}
#hf-light-leak .hf-leak-layer {
position: absolute;
inset: -20%;
will-change: opacity, transform;
}
#hf-light-leak .hf-leak-amber {
background:
radial-gradient(ellipse 52% 110% at 10% 58%, rgb(255 241 197 / 92%), transparent 45%),
radial-gradient(ellipse 72% 120% at 24% 54%, rgb(255 126 57 / 78%), transparent 66%);
filter: blur(2.8vmin);
}
#hf-light-leak .hf-leak-red {
background: radial-gradient(
ellipse 58% 105% at 78% 34%,
rgb(255 79 61 / 72%),
rgb(255 44 92 / 28%) 48%,
transparent 76%
);
filter: blur(4vmin);
}
#hf-light-leak .hf-leak-gold {
background: linear-gradient(
104deg,
transparent 24%,
rgb(255 144 62 / 30%) 40%,
rgb(255 247 205 / 72%) 51%,
rgb(255 124 48 / 22%) 62%,
transparent 78%
);
filter: blur(2vmin);
}
</style>
</head>
<body>
<div
id="organic-light-leak-overlay"
data-composition-id="organic-light-leak-overlay"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="hf-light-leak" aria-hidden="true">
<div class="hf-leak-layer hf-leak-amber"></div>
<div class="hf-leak-layer hf-leak-red"></div>
<div class="hf-leak-layer hf-leak-gold"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var leak = document.getElementById("hf-light-leak");
var host = leak && leak.closest("[data-composition-id]");
if (!host) throw new Error("Organic Light Leak could not find its composition host");
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var tl = gsap.timeline({ paused: true });
tl.to({}, { duration: duration, ease: "none" }, 0);
tl.fromTo(
leak,
{ opacity: 0 },
{ opacity: 0.9, duration: duration * 0.24, ease: "sine.out" },
duration * 0.08,
);
tl.to(leak, { opacity: 0.46, duration: duration * 0.2, ease: "sine.inOut" }, duration * 0.42);
tl.to(leak, { opacity: 0, duration: duration * 0.26, ease: "sine.in" }, duration * 0.68);
tl.fromTo(
".hf-leak-amber",
{ xPercent: -20, scale: 0.92, rotation: -4 },
{ xPercent: 18, scale: 1.1, rotation: 2, duration: duration, ease: "sine.inOut" },
0,
);
tl.fromTo(
".hf-leak-red",
{ xPercent: 20, yPercent: -8, scale: 1.08 },
{ xPercent: -16, yPercent: 8, scale: 0.94, duration: duration, ease: "sine.inOut" },
0,
);
tl.fromTo(
".hf-leak-gold",
{ xPercent: -28, rotation: -6 },
{ xPercent: 24, rotation: 4, duration: duration, ease: "sine.inOut" },
0,
);
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
@@ -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"
}
}
+16
View File
@@ -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"
+15 -8
View File
@@ -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<string> {
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<void> {
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) {
+1 -1
View File
@@ -46,7 +46,7 @@
"files": 10
},
"media-use": {
"hash": "1234dcf993bbe1fc",
"hash": "470beb651f5a5068",
"files": 151
},
"motion-graphics": {
@@ -826,7 +826,7 @@ Extract the exact deterministic source frame first, then remove its background:
ffmpeg -ss <seconds> -i <source-video> -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