feat(registry): curtain-reveal transition

Agents often search for "curtain" or "stage reveal" and find nothing built
for it in the catalog. Adds a component where two pleated stage curtains
gather outward from center, like real velvet bunching at the tieback, to
reveal slotted content underneath.
This commit is contained in:
Miguel Angel Simon Sierra
2026-09-23 07:25:03 -04:00
committed by Miguel Ángel
parent 29378b6c7f
commit 548de3fdc3
11 changed files with 933 additions and 5 deletions
+400
View File
@@ -0,0 +1,400 @@
---
title: "Curtain Reveal"
description: "Two pleated stage curtains gather outward from center, the way real velvet bunches at the tieback, revealing the stage underneath. A static pelmet frames the top; a soft spotlight can bloom in as they part."
mode: "frame"
---
import { CatalogDetail, CatalogSlot } from "/snippets/catalog-detail.jsx";
import { InstallCommand } from "/snippets/install-command.jsx";
<CatalogDetail
previewSrc="/public/catalog/components/curtain-reveal.json"
compositionId="curtain-reveal"
compositionSrc="compositions/components/curtain-reveal.html"
title="Curtain Reveal"
description="Two pleated stage curtains gather outward from center, the way real velvet bunches at the tieback, revealing the stage underneath. A static pelmet frames the top; a soft spotlight can bloom in as they part."
variables={[{"id":"tone","type":"enum","role":"style","label":"Curtain tone","description":"Curtain color.","default":"red","options":[{"value":"red","label":"Red"},{"value":"burgundy","label":"Burgundy"},{"value":"navy","label":"Navy"},{"value":"forest","label":"Forest"}]},{"id":"pelmet","type":"enum","role":"style","label":"Pelmet","description":"Static valance framing the top of the stage.","default":"on","options":[{"value":"on","label":"On"},{"value":"off","label":"Off"}]},{"id":"spotlight","type":"enum","role":"style","label":"Spotlight","description":"Soft glow blooming over the stage as the curtains part.","default":"on","options":[{"value":"on","label":"On"},{"value":"off","label":"Off"}]}]}
meta={{"category":"Effects","badge":"Stable","codeLines":319}}
attribution={{"path":"registry/components/curtain-reveal","tags":["curtain","stage reveal","transition","motion-primitive"]}}
hasCode
rawUrl="https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/components/curtain-reveal/curtain-reveal.html"
>
<CatalogSlot slot="code">
```html curtain-reveal.html
<!doctype html>
<!--
curtain-reveal: HyperFrames video primitive (transitions / stage reveal).
Two pleated stage curtains meet at center, covering the frame. On cue they
gather outward -- sliding apart while narrowing slightly, the way real
velvet bunches at the tieback -- revealing the stage underneath. An
optional pelmet frames the top edge and never moves; an optional soft
spotlight blooms in as the curtains part.
The stage is a SLOT. Callers supply it by placing an inert template
anywhere in the HOST page (templates never render, and the runtime wipes
the host clip's own children on mount, so the slot lives at document level):
<template data-slot="curtain-reveal-stage"> ... </template>
Slot content may be an <img>, a muted <video>, or arbitrary HTML; direct
img/video children are stretched to cover the stage (packages/producer
drives a slotted <video>'s own data-start/data-duration exactly like any
other clip -- this primitive never reads or sets currentTime itself). With
no slot, the stage renders a token spotlight card so the reveal reads on
its own.
Variables (declared in data-composition-variables below):
- tone ("red" | "burgundy" | "navy" | "forest", default "red"): curtain
color.
- pelmet ("on" | "off", default "on"): static valance framing the top.
- spotlight ("on" | "off", default "on"): soft glow blooming over the
stage as the curtains part.
Envelope, fixed IN with elastic HOLD only (never timeScale):
IN_BASE = 1.1s the curtains gather and part
HOLD = max(0, D - IN); the stage stands revealed, still
If D < IN_BASE, IN compresses to D (no hold).
Determinism: no randomness anywhere in this primitive. The pleat texture
is a static repeating-gradient, the scalloped hem and pelmet trim are a
closed-form sine wave sampled at a fixed point count, and both curtains
move on one authored GSAP tween each with explicit fromTo endpoints. Any
seek order lands identical frames.
Mount contract: the runtime clones only this template. #root fills the
host box (no data-width/data-height, container-type: size, cqmin units),
is styled via #root only, and registers one paused timeline under the
LITERAL "curtain-reveal" key.
-->
<html
lang="en"
data-composition-id="curtain-reveal"
data-composition-duration="2.4"
data-composition-variables='[
{ "id": "tone", "type": "enum", "role": "style", "label": "Curtain tone", "description": "Curtain color.", "default": "red", "options": [{ "value": "red", "label": "Red" }, { "value": "burgundy", "label": "Burgundy" }, { "value": "navy", "label": "Navy" }, { "value": "forest", "label": "Forest" }] },
{ "id": "pelmet", "type": "enum", "role": "style", "label": "Pelmet", "description": "Static valance framing the top of the stage.", "default": "on", "options": [{ "value": "on", "label": "On" }, { "value": "off", "label": "Off" }] },
{ "id": "spotlight", "type": "enum", "role": "style", "label": "Spotlight", "description": "Soft glow blooming over the stage as the curtains part.", "default": "on", "options": [{ "value": "on", "label": "On" }, { "value": "off", "label": "Off" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Curtain Reveal</title>
</head>
<body>
<template>
<div id="root" data-composition-id="curtain-reveal" data-duration="2.4" data-fps="30">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
#root {
position: absolute;
inset: 0;
overflow: hidden;
container-type: size;
isolation: isolate;
color: var(--fg, #1d1b16);
font-family: var(--font-display, "Inter", system-ui, sans-serif);
background: #060607;
}
.cvr-clip {
position: absolute;
inset: 0;
overflow: hidden;
}
.cvr-stage {
position: absolute;
inset: 0;
overflow: hidden;
}
.cvr-stage > img,
.cvr-stage > video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.cvr-stage-default {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: radial-gradient(60% 60% at 50% 42%, #2a2a2e 0%, #0c0c0d 78%);
}
.cvr-stage-default-mark {
font-size: 4.5cqmin;
font-weight: 700;
letter-spacing: 0.32em;
text-transform: uppercase;
color: #fff;
opacity: 0.4;
}
.cvr-spotlight {
position: absolute;
inset: 0;
background: radial-gradient(
38% 55% at 50% 30%,
rgba(255, 244, 214, 0.32) 0%,
rgba(255, 244, 214, 0) 70%
);
mix-blend-mode: screen;
opacity: 0;
pointer-events: none;
}
.cvr-curtain {
position: absolute;
top: 0;
bottom: 0;
width: 54%;
transform-origin: 50% 50%;
}
.cvr-curtain-left {
left: 0;
clip-path: var(--cvr-hem-left);
}
.cvr-curtain-right {
right: 0;
clip-path: var(--cvr-hem-right);
}
.cvr-curtain-fabric {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(0, 0, 0, 0.35) 0%, transparent 10%, transparent 90%, rgba(
0,
0,
0,
0.5
) 100%),
repeating-linear-gradient(
90deg,
var(--cvr-fold-dark) 0cqw,
var(--cvr-fold-mid) 1.1cqw,
var(--cvr-fold-light) 2.1cqw,
var(--cvr-fold-mid) 3cqw,
var(--cvr-fold-dark) 4.1cqw
);
box-shadow: inset 0 0 6cqmin rgba(0, 0, 0, 0.45);
}
.cvr-pelmet {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 11%;
background: var(--cvr-pelmet-color, #4a0e1f);
box-shadow: 0 0.6cqmin 1.4cqmin rgba(0, 0, 0, 0.55);
clip-path: var(--cvr-pelmet-hem);
}
</style>
<div
id="curtain-reveal-clip"
class="cvr-clip clip"
data-start="0"
data-duration="2.4"
data-track-index="0"
>
<div class="cvr-stage" id="cvr-stage">
<div class="cvr-stage-default">
<span class="cvr-stage-default-mark" aria-hidden="true">Stage</span>
</div>
</div>
<div class="cvr-spotlight" id="cvr-spotlight"></div>
<div class="cvr-curtain cvr-curtain-left" id="cvr-curtain-left">
<div class="cvr-curtain-fabric"></div>
</div>
<div class="cvr-curtain cvr-curtain-right" id="cvr-curtain-right">
<div class="cvr-curtain-fabric"></div>
</div>
<div class="cvr-pelmet" id="cvr-pelmet"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
"use strict";
var root = document.getElementById("root");
var stage = document.getElementById("cvr-stage");
var spotlight = document.getElementById("cvr-spotlight");
var curtainLeft = document.getElementById("cvr-curtain-left");
var curtainRight = document.getElementById("cvr-curtain-right");
var pelmet = document.getElementById("cvr-pelmet");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var toneTable = {
red: { fold: ["#3d0810", "#8a1a26", "#b23345"], pelmet: "#4a0e1a" },
burgundy: { fold: ["#2b0912", "#5c1a2c", "#7d2c42"], pelmet: "#3a0f1e" },
navy: { fold: ["#070d1f", "#152a5c", "#274a8f"], pelmet: "#0c1836" },
forest: { fold: ["#0c1f13", "#1f4a2b", "#356b3f"], pelmet: "#12291a" },
};
var tone = Object.prototype.hasOwnProperty.call(toneTable, vars.tone)
? vars.tone
: "red";
var palette = toneTable[tone];
root.style.setProperty("--cvr-fold-dark", palette.fold[0]);
root.style.setProperty("--cvr-fold-mid", palette.fold[1]);
root.style.setProperty("--cvr-fold-light", palette.fold[2]);
root.style.setProperty("--cvr-pelmet-color", palette.pelmet);
var pelmetOn = vars.pelmet !== "off";
var spotlightOn = vars.spotlight !== "off";
pelmet.style.display = pelmetOn ? "" : "none";
// Slot. Caller templates live at the host document level (the
// mount wipes host-clip children, and templates never render).
var hostDoc = root.ownerDocument;
var slotTemplate = null;
try {
slotTemplate = hostDoc.querySelector('template[data-slot="curtain-reveal-stage"]');
} catch (error) {
slotTemplate = null;
}
if (slotTemplate) {
var defaultMark = stage.querySelector(".cvr-stage-default");
if (defaultMark) defaultMark.remove();
stage.appendChild(hostDoc.importNode(slotTemplate.content, true));
}
// Closed-form scalloped hem: a fixed point count sampled from a
// sine wave, never randomised, so the trim is identical on every
// render of every instance.
function hemPolygon(scallops, ampPct, baseY) {
var STEPS = 24;
var pts = ["0% 0%", "100% 0%"];
for (var i = 0; i <= STEPS; i += 1) {
var t = i / STEPS;
var y = baseY + Math.sin(t * Math.PI * scallops) * ampPct;
pts.push((100 - t * 100).toFixed(2) + "% " + y.toFixed(2) + "%");
}
return "polygon(" + pts.join(", ") + ")";
}
var curtainHem = hemPolygon(3.4, 2.6, 97);
root.style.setProperty("--cvr-hem-left", curtainHem);
root.style.setProperty("--cvr-hem-right", curtainHem);
root.style.setProperty("--cvr-pelmet-hem", hemPolygon(5, 14, 78));
// Envelope: fixed IN, elastic HOLD, never time-scaled.
var IN_BASE = 1.1;
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "2.4"));
var IN = Math.min(IN_BASE, duration);
gsap.set(spotlight, { opacity: 0 });
var tl = gsap.timeline({ paused: true });
tl.fromTo(
curtainLeft,
{ xPercent: 0, scaleX: 1 },
{ xPercent: -128, scaleX: 0.82, duration: IN, ease: "power2.inOut" },
0,
);
tl.fromTo(
curtainRight,
{ xPercent: 0, scaleX: 1 },
{ xPercent: 128, scaleX: 0.82, duration: IN, ease: "power2.inOut" },
0,
);
if (spotlightOn) {
// Clamped to end by IN: uncapped, start (IN * 0.25) + duration
// (IN * 0.85) lands at IN * 1.10 -- fine while the hold covers
// it, but a short composition that compresses the hold away
// would push GSAP's auto-extended timeline past data-duration.
var spotlightStart = IN * 0.25;
var spotlightDuration = Math.min(IN * 0.85, IN - spotlightStart);
tl.fromTo(
spotlight,
{ opacity: 0 },
{ opacity: 0.65, duration: spotlightDuration, ease: "power1.out" },
spotlightStart,
);
}
tl.set({}, {}, duration);
tl.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines["curtain-reveal"] = tl;
})();
</script>
</div>
</template>
</body>
</html>
```
</CatalogSlot>
<CatalogSlot slot="install">
<InstallCommand command="npx hyperframes add curtain-reveal" item="curtain-reveal" />
That writes one file: `compositions/components/curtain-reveal.html`.
</CatalogSlot>
<CatalogSlot slot="docs">
## Paste it into your composition
Open `compositions/components/curtain-reveal.html` and copy what is inside into your own composition.
A component has no size or duration of its own. It takes both from the composition
you paste it into.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `tone` | `red` | `red`, `burgundy`, `navy`, `forest` | Curtain color. |
| `pelmet` | `on` | `on`, `off` | Static valance framing the top of the stage. |
| `spotlight` | `on` | `on`, `off` | Soft glow blooming over the stage as the curtains part. |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="curtain-reveal"
data-composition-src="compositions/components/curtain-reveal.html"
data-variable-values='{"tone":"red","pelmet":"on","spotlight":"on"}'
></div>
```
{/* hf:generated-footer */}
Tagged `curtain` `stage reveal` `transition` `motion-primitive`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
</CatalogSlot>
</CatalogDetail>
+3 -2
View File
@@ -652,7 +652,7 @@
},
{
"group": "Motion & effects",
"tag": "94",
"tag": "95",
"expanded": false,
"pages": [
{
@@ -694,7 +694,7 @@
},
{
"group": "Effects",
"tag": "53",
"tag": "54",
"expanded": false,
"pages": [
"catalog/components/avatar-cloud",
@@ -703,6 +703,7 @@
"catalog/components/camera-rig-depth-stack",
"catalog/components/comparison-split",
"catalog/components/confetti",
"catalog/components/curtain-reveal",
"catalog/components/cut-the-curve",
"catalog/components/device-frame-stage",
"catalog/components/directional-wipe",
+14
View File
@@ -1584,6 +1584,20 @@
"href": "/catalog/components/cursor-glyph-trail",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/cursor-glyph-trail.png"
},
{
"name": "curtain-reveal",
"type": "component",
"title": "Curtain Reveal",
"description": "Two pleated stage curtains gather outward from center, the way real velvet bunches at the tieback, revealing the stage underneath. A static pelmet frames the top; a soft spotlight can bloom in as they part.",
"tags": [
"curtain",
"stage reveal",
"transition",
"motion-primitive"
],
"href": "/catalog/components/curtain-reveal",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/curtain-reveal.png"
},
{
"name": "cut-the-curve",
"type": "component",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+2 -1
View File
@@ -2,7 +2,7 @@
"model": "bge-small-en-v1.5",
"modelRevision": "ea104dacec62c0de699686887e3f920caeb4f3e3",
"dimensions": 384,
"revision": "01f77564cabdcd54733e8251051a721b644be2096c1aba9d62f392c7d66d277e",
"revision": "2c3b540afc2d8269393bf89f99386e2478305c35e21dcd6d497d9ba6dec257ea",
"names": [
"ai-chat-reveal",
"animated-bar-chart",
@@ -109,6 +109,7 @@
"cta-lockup",
"cuboid-carousel",
"cursor-glyph-trail",
"curtain-reveal",
"cut-the-curve",
"data-chart",
"decline-chart",
+320
View File
@@ -0,0 +1,320 @@
<!doctype html>
<!--
curtain-reveal: HyperFrames video primitive (transitions / stage reveal).
Two pleated stage curtains meet at center, covering the frame. On cue they
gather outward -- sliding apart while narrowing slightly, the way real
velvet bunches at the tieback -- revealing the stage underneath. An
optional pelmet frames the top edge and never moves; an optional soft
spotlight blooms in as the curtains part.
The stage is a SLOT. Callers supply it by placing an inert template
anywhere in the HOST page (templates never render, and the runtime wipes
the host clip's own children on mount, so the slot lives at document level):
<template data-slot="curtain-reveal-stage"> ... </template>
Slot content may be an <img>, a muted <video>, or arbitrary HTML; direct
img/video children are stretched to cover the stage (packages/producer
drives a slotted <video>'s own data-start/data-duration exactly like any
other clip -- this primitive never reads or sets currentTime itself). With
no slot, the stage renders a token spotlight card so the reveal reads on
its own.
Variables (declared in data-composition-variables below):
- tone ("red" | "burgundy" | "navy" | "forest", default "red"): curtain
color.
- pelmet ("on" | "off", default "on"): static valance framing the top.
- spotlight ("on" | "off", default "on"): soft glow blooming over the
stage as the curtains part.
Envelope, fixed IN with elastic HOLD only (never timeScale):
IN_BASE = 1.1s the curtains gather and part
HOLD = max(0, D - IN); the stage stands revealed, still
If D < IN_BASE, IN compresses to D (no hold).
Determinism: no randomness anywhere in this primitive. The pleat texture
is a static repeating-gradient, the scalloped hem and pelmet trim are a
closed-form sine wave sampled at a fixed point count, and both curtains
move on one authored GSAP tween each with explicit fromTo endpoints. Any
seek order lands identical frames.
Mount contract: the runtime clones only this template. #root fills the
host box (no data-width/data-height, container-type: size, cqmin units),
is styled via #root only, and registers one paused timeline under the
LITERAL "curtain-reveal" key.
-->
<html
lang="en"
data-composition-id="curtain-reveal"
data-composition-duration="2.4"
data-composition-variables='[
{ "id": "tone", "type": "enum", "role": "style", "label": "Curtain tone", "description": "Curtain color.", "default": "red", "options": [{ "value": "red", "label": "Red" }, { "value": "burgundy", "label": "Burgundy" }, { "value": "navy", "label": "Navy" }, { "value": "forest", "label": "Forest" }] },
{ "id": "pelmet", "type": "enum", "role": "style", "label": "Pelmet", "description": "Static valance framing the top of the stage.", "default": "on", "options": [{ "value": "on", "label": "On" }, { "value": "off", "label": "Off" }] },
{ "id": "spotlight", "type": "enum", "role": "style", "label": "Spotlight", "description": "Soft glow blooming over the stage as the curtains part.", "default": "on", "options": [{ "value": "on", "label": "On" }, { "value": "off", "label": "Off" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Curtain Reveal</title>
</head>
<body>
<template>
<div id="root" data-composition-id="curtain-reveal" data-duration="2.4" data-fps="30">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
#root {
position: absolute;
inset: 0;
overflow: hidden;
container-type: size;
isolation: isolate;
color: var(--fg, #1d1b16);
font-family: var(--font-display, "Inter", system-ui, sans-serif);
background: #060607;
}
.cvr-clip {
position: absolute;
inset: 0;
overflow: hidden;
}
.cvr-stage {
position: absolute;
inset: 0;
overflow: hidden;
}
.cvr-stage > img,
.cvr-stage > video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.cvr-stage-default {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: radial-gradient(60% 60% at 50% 42%, #2a2a2e 0%, #0c0c0d 78%);
}
.cvr-stage-default-mark {
font-size: 4.5cqmin;
font-weight: 700;
letter-spacing: 0.32em;
text-transform: uppercase;
color: #fff;
opacity: 0.4;
}
.cvr-spotlight {
position: absolute;
inset: 0;
background: radial-gradient(
38% 55% at 50% 30%,
rgba(255, 244, 214, 0.32) 0%,
rgba(255, 244, 214, 0) 70%
);
mix-blend-mode: screen;
opacity: 0;
pointer-events: none;
}
.cvr-curtain {
position: absolute;
top: 0;
bottom: 0;
width: 54%;
transform-origin: 50% 50%;
}
.cvr-curtain-left {
left: 0;
clip-path: var(--cvr-hem-left);
}
.cvr-curtain-right {
right: 0;
clip-path: var(--cvr-hem-right);
}
.cvr-curtain-fabric {
position: absolute;
inset: 0;
background:
linear-gradient(
180deg,
rgba(0, 0, 0, 0.35) 0%,
transparent 10%,
transparent 90%,
rgba(0, 0, 0, 0.5) 100%
),
repeating-linear-gradient(
90deg,
var(--cvr-fold-dark) 0cqw,
var(--cvr-fold-mid) 1.1cqw,
var(--cvr-fold-light) 2.1cqw,
var(--cvr-fold-mid) 3cqw,
var(--cvr-fold-dark) 4.1cqw
);
box-shadow: inset 0 0 6cqmin rgba(0, 0, 0, 0.45);
}
.cvr-pelmet {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 11%;
background: var(--cvr-pelmet-color, #4a0e1f);
box-shadow: 0 0.6cqmin 1.4cqmin rgba(0, 0, 0, 0.55);
clip-path: var(--cvr-pelmet-hem);
}
</style>
<div
id="curtain-reveal-clip"
class="cvr-clip clip"
data-start="0"
data-duration="2.4"
data-track-index="0"
>
<div class="cvr-stage" id="cvr-stage">
<div class="cvr-stage-default">
<span class="cvr-stage-default-mark" aria-hidden="true">Stage</span>
</div>
</div>
<div class="cvr-spotlight" id="cvr-spotlight"></div>
<div class="cvr-curtain cvr-curtain-left" id="cvr-curtain-left">
<div class="cvr-curtain-fabric"></div>
</div>
<div class="cvr-curtain cvr-curtain-right" id="cvr-curtain-right">
<div class="cvr-curtain-fabric"></div>
</div>
<div class="cvr-pelmet" id="cvr-pelmet"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
"use strict";
var root = document.getElementById("root");
var stage = document.getElementById("cvr-stage");
var spotlight = document.getElementById("cvr-spotlight");
var curtainLeft = document.getElementById("cvr-curtain-left");
var curtainRight = document.getElementById("cvr-curtain-right");
var pelmet = document.getElementById("cvr-pelmet");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var toneTable = {
red: { fold: ["#3d0810", "#8a1a26", "#b23345"], pelmet: "#4a0e1a" },
burgundy: { fold: ["#2b0912", "#5c1a2c", "#7d2c42"], pelmet: "#3a0f1e" },
navy: { fold: ["#070d1f", "#152a5c", "#274a8f"], pelmet: "#0c1836" },
forest: { fold: ["#0c1f13", "#1f4a2b", "#356b3f"], pelmet: "#12291a" },
};
var tone = Object.prototype.hasOwnProperty.call(toneTable, vars.tone)
? vars.tone
: "red";
var palette = toneTable[tone];
root.style.setProperty("--cvr-fold-dark", palette.fold[0]);
root.style.setProperty("--cvr-fold-mid", palette.fold[1]);
root.style.setProperty("--cvr-fold-light", palette.fold[2]);
root.style.setProperty("--cvr-pelmet-color", palette.pelmet);
var pelmetOn = vars.pelmet !== "off";
var spotlightOn = vars.spotlight !== "off";
pelmet.style.display = pelmetOn ? "" : "none";
// Slot. Caller templates live at the host document level (the
// mount wipes host-clip children, and templates never render).
var hostDoc = root.ownerDocument;
var slotTemplate = null;
try {
slotTemplate = hostDoc.querySelector('template[data-slot="curtain-reveal-stage"]');
} catch (error) {
slotTemplate = null;
}
if (slotTemplate) {
var defaultMark = stage.querySelector(".cvr-stage-default");
if (defaultMark) defaultMark.remove();
stage.appendChild(hostDoc.importNode(slotTemplate.content, true));
}
// Closed-form scalloped hem: a fixed point count sampled from a
// sine wave, never randomised, so the trim is identical on every
// render of every instance.
function hemPolygon(scallops, ampPct, baseY) {
var STEPS = 24;
var pts = ["0% 0%", "100% 0%"];
for (var i = 0; i <= STEPS; i += 1) {
var t = i / STEPS;
var y = baseY + Math.sin(t * Math.PI * scallops) * ampPct;
pts.push((100 - t * 100).toFixed(2) + "% " + y.toFixed(2) + "%");
}
return "polygon(" + pts.join(", ") + ")";
}
var curtainHem = hemPolygon(3.4, 2.6, 97);
root.style.setProperty("--cvr-hem-left", curtainHem);
root.style.setProperty("--cvr-hem-right", curtainHem);
root.style.setProperty("--cvr-pelmet-hem", hemPolygon(5, 14, 78));
// Envelope: fixed IN, elastic HOLD, never time-scaled.
var IN_BASE = 1.1;
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "2.4"));
var IN = Math.min(IN_BASE, duration);
gsap.set(spotlight, { opacity: 0 });
var tl = gsap.timeline({ paused: true });
tl.fromTo(
curtainLeft,
{ xPercent: 0, scaleX: 1 },
{ xPercent: -128, scaleX: 0.82, duration: IN, ease: "power2.inOut" },
0,
);
tl.fromTo(
curtainRight,
{ xPercent: 0, scaleX: 1 },
{ xPercent: 128, scaleX: 0.82, duration: IN, ease: "power2.inOut" },
0,
);
if (spotlightOn) {
// Clamped to end by IN: uncapped, start (IN * 0.25) + duration
// (IN * 0.85) lands at IN * 1.10 -- fine while the hold covers
// it, but a short composition that compresses the hold away
// would push GSAP's auto-extended timeline past data-duration.
var spotlightStart = IN * 0.25;
var spotlightDuration = Math.min(IN * 0.85, IN - spotlightStart);
tl.fromTo(
spotlight,
{ opacity: 0 },
{ opacity: 0.65, duration: spotlightDuration, ease: "power1.out" },
spotlightStart,
);
}
tl.set({}, {}, duration);
tl.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines["curtain-reveal"] = tl;
})();
</script>
</div>
</template>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
<!doctype html>
<!--
curtain-reveal demo. This standalone composition mounts the primitive
FULL-BLEED through data-composition-src, navy tone with the pelmet off to
prove the non-default variable path, and slots a procedural scene so the
curtains part onto a real stage rather than the bare token card the
primitive falls back to.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Curtain Reveal Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #060607;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #060607;
}
.mount-bleed {
position: absolute;
inset: 0;
container-type: size;
overflow: hidden;
}
.demo-scene {
position: absolute;
inset: 0;
}
.demo-scene img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.demo-caption {
position: absolute;
left: 64px;
bottom: 64px;
padding: 14px 22px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 700;
font-size: 30px;
letter-spacing: 0.04em;
color: #fff;
background: rgba(10, 10, 12, 0.35);
border-radius: 8px;
backdrop-filter: blur(6px);
}
</style>
</head>
<body>
<template data-slot="curtain-reveal-stage">
<div class="demo-scene">
<img alt="" src="" id="demo-scene-img" />
<div class="demo-caption">Now announcing</div>
</div>
</template>
<div
id="root"
data-composition-id="curtain-reveal-demo"
data-start="0"
data-width="1920"
data-height="1080"
data-duration="2.4"
data-fps="30"
>
<div
id="curtain-reveal-mount"
class="mount-bleed clip"
data-composition-id="curtain-reveal"
data-composition-src="./curtain-reveal.html"
data-variable-values='{"tone":"navy","pelmet":"off","spotlight":"on"}'
data-start="0"
data-duration="2.4"
data-track-index="0"
data-width="1920"
data-height="1080"
></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
// A procedural SVG scene, so the demo is deterministic and ships
// nothing licensed (same approach as grade-split-reveal).
function stageScene() {
return (
'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">' +
'<defs><radialGradient id="ss-pool" cx="0.5" cy="0.6" r="0.34">' +
'<stop offset="0" stop-color="#4a3a22"/><stop offset="1" stop-color="#0c0810"/></radialGradient></defs>' +
'<rect width="1600" height="900" fill="#0a0710"/>' +
'<ellipse cx="800" cy="760" rx="420" ry="90" fill="url(#ss-pool)"/>' +
'<rect y="640" width="1600" height="260" fill="#150c1e"/>' +
'<g fill="#050308">' +
'<rect x="80" y="560" width="70" height="340"/><rect x="230" y="560" width="70" height="340"/>' +
'<rect x="1300" y="560" width="70" height="340"/><rect x="1450" y="560" width="70" height="340"/>' +
"</g>" +
'<ellipse cx="800" cy="470" rx="46" ry="52" fill="#e9c9a0"/>' +
'<path d="M740 520 Q800 500 860 520 L850 760 L750 760 Z" fill="#7a1220"/>' +
"</svg>"
);
}
// Elements inside an inert <template> are not in the live document,
// so they must be reached through the template's own content
// fragment, never document.getElementById.
document
.querySelector('template[data-slot="curtain-reveal-stage"]')
.content.getElementById("demo-scene-img").src =
"data:image/svg+xml;charset=utf-8," + encodeURIComponent(stageScene());
window.__timelines = window.__timelines || {};
window.__timelines["curtain-reveal-demo"] = gsap.timeline({ paused: true });
})();
</script>
</body>
</html>
@@ -0,0 +1,55 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "curtain-reveal",
"type": "hyperframes:component",
"title": "Curtain Reveal",
"description": "Two pleated stage curtains gather outward from center, the way real velvet bunches at the tieback, revealing the stage underneath. A static pelmet frames the top; a soft spotlight can bloom in as they part.",
"tags": ["curtain", "stage reveal", "transition", "motion-primitive"],
"files": [
{
"path": "curtain-reveal.html",
"target": "compositions/components/curtain-reveal.html",
"type": "hyperframes:snippet"
}
],
"variables": [
{
"id": "tone",
"type": "enum",
"role": "style",
"label": "Curtain tone",
"description": "Curtain color.",
"default": "red",
"options": [
{ "value": "red", "label": "Red" },
{ "value": "burgundy", "label": "Burgundy" },
{ "value": "navy", "label": "Navy" },
{ "value": "forest", "label": "Forest" }
]
},
{
"id": "pelmet",
"type": "enum",
"role": "style",
"label": "Pelmet",
"description": "Static valance framing the top of the stage.",
"default": "on",
"options": [
{ "value": "on", "label": "On" },
{ "value": "off", "label": "Off" }
]
},
{
"id": "spotlight",
"type": "enum",
"role": "style",
"label": "Spotlight",
"description": "Soft glow blooming over the stage as the curtains part.",
"default": "on",
"options": [
{ "value": "on", "label": "On" },
{ "value": "off", "label": "Off" }
]
}
]
}
+5 -1
View File
@@ -1578,9 +1578,13 @@
{
"name": "warm-grain",
"type": "hyperframes:example"
},
{
"name": "curtain-reveal",
"type": "hyperframes:component"
}
],
"catalogArtifact": {
"revision": "01f77564cabdcd54733e8251051a721b644be2096c1aba9d62f392c7d66d277e"
"revision": "2c3b540afc2d8269393bf89f99386e2478305c35e21dcd6d497d9ba6dec257ea"
}
}