mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-02 12:08:50 +00:00
Merge pull request #2754 from heygen-com/feat/media-treatment-overlays
feat(registry): add media treatment overlays
This commit is contained in:
@@ -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
@@ -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
@@ -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 |
@@ -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
@@ -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 |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user