Skip to content

Commit c9ed6ac

Browse files
committed
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.
1 parent 29378b6 commit c9ed6ac

9 files changed

Lines changed: 930 additions & 3 deletions

File tree

‎docs/catalog/components/curtain-reveal.mdx‎

Lines changed: 400 additions & 0 deletions
Large diffs are not rendered by default.

‎docs/docs.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@
652652
},
653653
{
654654
"group": "Motion & effects",
655-
"tag": "94",
655+
"tag": "95",
656656
"expanded": false,
657657
"pages": [
658658
{
@@ -694,7 +694,7 @@
694694
},
695695
{
696696
"group": "Effects",
697-
"tag": "53",
697+
"tag": "54",
698698
"expanded": false,
699699
"pages": [
700700
"catalog/components/avatar-cloud",
@@ -703,6 +703,7 @@
703703
"catalog/components/camera-rig-depth-stack",
704704
"catalog/components/comparison-split",
705705
"catalog/components/confetti",
706+
"catalog/components/curtain-reveal",
706707
"catalog/components/cut-the-curve",
707708
"catalog/components/device-frame-stage",
708709
"catalog/components/directional-wipe",

‎docs/public/catalog-index.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,20 @@
15841584
"href": "/catalog/components/cursor-glyph-trail",
15851585
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/cursor-glyph-trail.png"
15861586
},
1587+
{
1588+
"name": "curtain-reveal",
1589+
"type": "component",
1590+
"title": "Curtain Reveal",
1591+
"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.",
1592+
"tags": [
1593+
"curtain",
1594+
"stage reveal",
1595+
"transition",
1596+
"motion-primitive"
1597+
],
1598+
"href": "/catalog/components/curtain-reveal",
1599+
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/curtain-reveal.png"
1600+
},
15871601
{
15881602
"name": "cut-the-curve",
15891603
"type": "component",

‎docs/public/catalog/components/curtain-reveal.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎docs/snippets/catalog-gallery-data.mdx‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
<!doctype html>
2+
<!--
3+
curtain-reveal: HyperFrames video primitive (transitions / stage reveal).
4+
5+
Two pleated stage curtains meet at center, covering the frame. On cue they
6+
gather outward -- sliding apart while narrowing slightly, the way real
7+
velvet bunches at the tieback -- revealing the stage underneath. An
8+
optional pelmet frames the top edge and never moves; an optional soft
9+
spotlight blooms in as the curtains part.
10+
11+
The stage is a SLOT. Callers supply it by placing an inert template
12+
anywhere in the HOST page (templates never render, and the runtime wipes
13+
the host clip's own children on mount, so the slot lives at document level):
14+
15+
<template data-slot="curtain-reveal-stage"> ... </template>
16+
17+
Slot content may be an <img>, a muted <video>, or arbitrary HTML; direct
18+
img/video children are stretched to cover the stage (packages/producer
19+
drives a slotted <video>'s own data-start/data-duration exactly like any
20+
other clip -- this primitive never reads or sets currentTime itself). With
21+
no slot, the stage renders a token spotlight card so the reveal reads on
22+
its own.
23+
24+
Variables (declared in data-composition-variables below):
25+
- tone ("red" | "burgundy" | "navy" | "forest", default "red"): curtain
26+
color.
27+
- pelmet ("on" | "off", default "on"): static valance framing the top.
28+
- spotlight ("on" | "off", default "on"): soft glow blooming over the
29+
stage as the curtains part.
30+
31+
Envelope, fixed IN with elastic HOLD only (never timeScale):
32+
IN_BASE = 1.1s the curtains gather and part
33+
HOLD = max(0, D - IN); the stage stands revealed, still
34+
If D < IN_BASE, IN compresses to D (no hold).
35+
36+
Determinism: no randomness anywhere in this primitive. The pleat texture
37+
is a static repeating-gradient, the scalloped hem and pelmet trim are a
38+
closed-form sine wave sampled at a fixed point count, and both curtains
39+
move on one authored GSAP tween each with explicit fromTo endpoints. Any
40+
seek order lands identical frames.
41+
42+
Mount contract: the runtime clones only this template. #root fills the
43+
host box (no data-width/data-height, container-type: size, cqmin units),
44+
is styled via #root only, and registers one paused timeline under the
45+
LITERAL "curtain-reveal" key.
46+
-->
47+
<html
48+
lang="en"
49+
data-composition-id="curtain-reveal"
50+
data-composition-duration="2.4"
51+
data-composition-variables='[
52+
{ "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" }] },
53+
{ "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" }] },
54+
{ "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" }] }
55+
]'
56+
>
57+
<head>
58+
<meta charset="UTF-8" />
59+
<title>Curtain Reveal</title>
60+
</head>
61+
<body>
62+
<template>
63+
<div id="root" data-composition-id="curtain-reveal" data-duration="2.4" data-fps="30">
64+
<style>
65+
*,
66+
*::before,
67+
*::after {
68+
box-sizing: border-box;
69+
}
70+
71+
#root {
72+
position: absolute;
73+
inset: 0;
74+
overflow: hidden;
75+
container-type: size;
76+
isolation: isolate;
77+
color: var(--fg, #1d1b16);
78+
font-family: var(--font-display, "Inter", system-ui, sans-serif);
79+
background: #060607;
80+
}
81+
82+
.cvr-clip {
83+
position: absolute;
84+
inset: 0;
85+
overflow: hidden;
86+
}
87+
88+
.cvr-stage {
89+
position: absolute;
90+
inset: 0;
91+
overflow: hidden;
92+
}
93+
94+
.cvr-stage > img,
95+
.cvr-stage > video {
96+
display: block;
97+
width: 100%;
98+
height: 100%;
99+
object-fit: cover;
100+
}
101+
102+
.cvr-stage-default {
103+
position: absolute;
104+
inset: 0;
105+
display: grid;
106+
place-items: center;
107+
background: radial-gradient(60% 60% at 50% 42%, #2a2a2e 0%, #0c0c0d 78%);
108+
}
109+
110+
.cvr-stage-default-mark {
111+
font-size: 4.5cqmin;
112+
font-weight: 700;
113+
letter-spacing: 0.32em;
114+
text-transform: uppercase;
115+
color: #fff;
116+
opacity: 0.4;
117+
}
118+
119+
.cvr-spotlight {
120+
position: absolute;
121+
inset: 0;
122+
background: radial-gradient(
123+
38% 55% at 50% 30%,
124+
rgba(255, 244, 214, 0.32) 0%,
125+
rgba(255, 244, 214, 0) 70%
126+
);
127+
mix-blend-mode: screen;
128+
opacity: 0;
129+
pointer-events: none;
130+
}
131+
132+
.cvr-curtain {
133+
position: absolute;
134+
top: 0;
135+
bottom: 0;
136+
width: 54%;
137+
transform-origin: 50% 50%;
138+
}
139+
.cvr-curtain-left {
140+
left: 0;
141+
clip-path: var(--cvr-hem-left);
142+
}
143+
.cvr-curtain-right {
144+
right: 0;
145+
clip-path: var(--cvr-hem-right);
146+
}
147+
148+
.cvr-curtain-fabric {
149+
position: absolute;
150+
inset: 0;
151+
background:
152+
linear-gradient(
153+
180deg,
154+
rgba(0, 0, 0, 0.35) 0%,
155+
transparent 10%,
156+
transparent 90%,
157+
rgba(0, 0, 0, 0.5) 100%
158+
),
159+
repeating-linear-gradient(
160+
90deg,
161+
var(--cvr-fold-dark) 0cqw,
162+
var(--cvr-fold-mid) 1.1cqw,
163+
var(--cvr-fold-light) 2.1cqw,
164+
var(--cvr-fold-mid) 3cqw,
165+
var(--cvr-fold-dark) 4.1cqw
166+
);
167+
box-shadow: inset 0 0 6cqmin rgba(0, 0, 0, 0.45);
168+
}
169+
170+
.cvr-pelmet {
171+
position: absolute;
172+
top: 0;
173+
left: 0;
174+
right: 0;
175+
height: 11%;
176+
background: var(--cvr-pelmet-color, #4a0e1f);
177+
box-shadow: 0 0.6cqmin 1.4cqmin rgba(0, 0, 0, 0.55);
178+
clip-path: var(--cvr-pelmet-hem);
179+
}
180+
</style>
181+
182+
<div
183+
id="curtain-reveal-clip"
184+
class="cvr-clip clip"
185+
data-start="0"
186+
data-duration="2.4"
187+
data-track-index="0"
188+
>
189+
<div class="cvr-stage" id="cvr-stage">
190+
<div class="cvr-stage-default">
191+
<span class="cvr-stage-default-mark" aria-hidden="true">Stage</span>
192+
</div>
193+
</div>
194+
<div class="cvr-spotlight" id="cvr-spotlight"></div>
195+
196+
<div class="cvr-curtain cvr-curtain-left" id="cvr-curtain-left">
197+
<div class="cvr-curtain-fabric"></div>
198+
</div>
199+
<div class="cvr-curtain cvr-curtain-right" id="cvr-curtain-right">
200+
<div class="cvr-curtain-fabric"></div>
201+
</div>
202+
203+
<div class="cvr-pelmet" id="cvr-pelmet"></div>
204+
</div>
205+
206+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
207+
<script>
208+
(function () {
209+
"use strict";
210+
211+
var root = document.getElementById("root");
212+
var stage = document.getElementById("cvr-stage");
213+
var spotlight = document.getElementById("cvr-spotlight");
214+
var curtainLeft = document.getElementById("cvr-curtain-left");
215+
var curtainRight = document.getElementById("cvr-curtain-right");
216+
var pelmet = document.getElementById("cvr-pelmet");
217+
218+
var vars =
219+
window.__hyperframes && window.__hyperframes.getVariables
220+
? window.__hyperframes.getVariables()
221+
: {};
222+
223+
var toneTable = {
224+
red: { fold: ["#3d0810", "#8a1a26", "#b23345"], pelmet: "#4a0e1a" },
225+
burgundy: { fold: ["#2b0912", "#5c1a2c", "#7d2c42"], pelmet: "#3a0f1e" },
226+
navy: { fold: ["#070d1f", "#152a5c", "#274a8f"], pelmet: "#0c1836" },
227+
forest: { fold: ["#0c1f13", "#1f4a2b", "#356b3f"], pelmet: "#12291a" },
228+
};
229+
var tone = Object.prototype.hasOwnProperty.call(toneTable, vars.tone)
230+
? vars.tone
231+
: "red";
232+
var palette = toneTable[tone];
233+
root.style.setProperty("--cvr-fold-dark", palette.fold[0]);
234+
root.style.setProperty("--cvr-fold-mid", palette.fold[1]);
235+
root.style.setProperty("--cvr-fold-light", palette.fold[2]);
236+
root.style.setProperty("--cvr-pelmet-color", palette.pelmet);
237+
238+
var pelmetOn = vars.pelmet !== "off";
239+
var spotlightOn = vars.spotlight !== "off";
240+
pelmet.style.display = pelmetOn ? "" : "none";
241+
242+
// Slot. Caller templates live at the host document level (the
243+
// mount wipes host-clip children, and templates never render).
244+
var hostDoc = root.ownerDocument;
245+
var slotTemplate = null;
246+
try {
247+
slotTemplate = hostDoc.querySelector('template[data-slot="curtain-reveal-stage"]');
248+
} catch (error) {
249+
slotTemplate = null;
250+
}
251+
if (slotTemplate) {
252+
var defaultMark = stage.querySelector(".cvr-stage-default");
253+
if (defaultMark) defaultMark.remove();
254+
stage.appendChild(hostDoc.importNode(slotTemplate.content, true));
255+
}
256+
257+
// Closed-form scalloped hem: a fixed point count sampled from a
258+
// sine wave, never randomised, so the trim is identical on every
259+
// render of every instance.
260+
function hemPolygon(scallops, ampPct, baseY) {
261+
var STEPS = 24;
262+
var pts = ["0% 0%", "100% 0%"];
263+
for (var i = 0; i <= STEPS; i += 1) {
264+
var t = i / STEPS;
265+
var y = baseY + Math.sin(t * Math.PI * scallops) * ampPct;
266+
pts.push((100 - t * 100).toFixed(2) + "% " + y.toFixed(2) + "%");
267+
}
268+
return "polygon(" + pts.join(", ") + ")";
269+
}
270+
271+
var curtainHem = hemPolygon(3.4, 2.6, 97);
272+
root.style.setProperty("--cvr-hem-left", curtainHem);
273+
root.style.setProperty("--cvr-hem-right", curtainHem);
274+
root.style.setProperty("--cvr-pelmet-hem", hemPolygon(5, 14, 78));
275+
276+
// Envelope: fixed IN, elastic HOLD, never time-scaled.
277+
var IN_BASE = 1.1;
278+
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "2.4"));
279+
var IN = Math.min(IN_BASE, duration);
280+
281+
gsap.set(spotlight, { opacity: 0 });
282+
283+
var tl = gsap.timeline({ paused: true });
284+
tl.fromTo(
285+
curtainLeft,
286+
{ xPercent: 0, scaleX: 1 },
287+
{ xPercent: -128, scaleX: 0.82, duration: IN, ease: "power2.inOut" },
288+
0,
289+
);
290+
tl.fromTo(
291+
curtainRight,
292+
{ xPercent: 0, scaleX: 1 },
293+
{ xPercent: 128, scaleX: 0.82, duration: IN, ease: "power2.inOut" },
294+
0,
295+
);
296+
if (spotlightOn) {
297+
// Clamped to end by IN: uncapped, start (IN * 0.25) + duration
298+
// (IN * 0.85) lands at IN * 1.10 -- fine while the hold covers
299+
// it, but a short composition that compresses the hold away
300+
// would push GSAP's auto-extended timeline past data-duration.
301+
var spotlightStart = IN * 0.25;
302+
var spotlightDuration = Math.min(IN * 0.85, IN - spotlightStart);
303+
tl.fromTo(
304+
spotlight,
305+
{ opacity: 0 },
306+
{ opacity: 0.65, duration: spotlightDuration, ease: "power1.out" },
307+
spotlightStart,
308+
);
309+
}
310+
tl.set({}, {}, duration);
311+
tl.seek(0);
312+
313+
window.__timelines = window.__timelines || {};
314+
window.__timelines["curtain-reveal"] = tl;
315+
})();
316+
</script>
317+
</div>
318+
</template>
319+
</body>
320+
</html>

0 commit comments

Comments
 (0)