feat(registry): bring back the video-primitive moves (#3169)

Restores the 208 catalog items reverted after their previews 404'd in
production, this time on the payload mechanism rather than the .html files
that caused the outage.

The generator no longer writes a preview document to docs/public. That writer,
and the machinery under it, existed only to produce files the docs host
discards, so it is gone rather than bypassed. Items now embed the composition
itself via a payload, which is what the previous change already does for the
items that were already in the catalog.

The variables explorer is parked, not restored: it drove its preview through
the same unpublished .html path, so it would have shown an empty frame. Items
that declare variables get the live player plus the static variables table, and
reconnecting the explorer to payloads is a follow-up.
This commit is contained in:
Miguel Ángel
2026-08-10 18:46:03 -04:00
committed by GitHub
parent 0f76305191
commit 9734578e60
1383 changed files with 255573 additions and 5760 deletions
+103
View File
@@ -0,0 +1,103 @@
<!doctype html>
<!--
stagger-cascade demo: standalone 1920x1080 host composition. It mounts the
primitive through data-composition-src into a definite card instead of
inlining primitive markup or motion. The non-default count, stagger, and
direction overrides prove that per-instance variables cross the mount.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Stagger Cascade Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
#root {
--bg: #090d16;
--fg: #f4f7fb;
--muted: #8d99aa;
--surface: #171e2d;
--border: rgba(244, 247, 251, 0.14);
--brand: #79a8ff;
--accent: #8be0c4;
--font-display: Inter, system-ui, sans-serif;
--font-body: Inter, system-ui, sans-serif;
--font-mono: ui-monospace, monospace;
--radius: 16px;
--space-1: 8px;
--space-2: 16px;
--space-3: 32px;
--dur-beat: 0.4s;
--ease-standard: cubic-bezier(0.215, 0.61, 0.355, 1);
--ease-emphasis: cubic-bezier(0.19, 1, 0.22, 1);
--ease-exit: cubic-bezier(0.55, 0.055, 0.675, 0.19);
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
display: grid;
place-items: center;
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
}
.mount-card {
width: 50%;
height: 60%;
left: 25%;
top: 20%;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 28px;
background: var(--surface);
box-shadow: 0 40px 100px color-mix(in srgb, var(--fg) 12%, transparent);
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="stagger-cascade-demo"
data-start="0"
data-width="1920"
data-height="1080"
data-duration="4"
data-fps="30"
>
<div
id="stagger-cascade-mount"
class="mount-card clip"
data-composition-id="stagger-cascade"
data-composition-src="./stagger-cascade.html"
data-variable-values='{"itemCount":8,"stagger":90,"direction":"left"}'
data-start="0"
data-duration="4"
data-track-index="0"
data-width="960"
data-height="648"
></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
// The mounted primitive owns all visible motion. This standalone host
// registers only the empty paused timeline required for its own id.
window.__timelines = window.__timelines || {};
window.__timelines["stagger-cascade-demo"] = gsap.timeline({ paused: true });
</script>
</body>
</html>
@@ -0,0 +1,58 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "stagger-cascade",
"type": "hyperframes:component",
"title": "Stagger Cascade",
"description": "A responsive grid of tile cards that fades and travels into place with an evenly spaced per-item GSAP stagger. The ordered cascade is the only visual mechanic.",
"tags": ["effects", "grid", "stagger", "entrance", "exhibit"],
"jobs": ["exhibit"],
"family": "effects",
"profile": "burst",
"evidence": "production census (launches-motion-vocabulary.md)",
"files": [
{
"path": "stagger-cascade.html",
"target": "compositions/components/stagger-cascade.html",
"type": "hyperframes:snippet"
}
],
"variables": [
{
"id": "itemCount",
"type": "number",
"role": "layout",
"label": "Item count",
"description": "Number of tiles in the responsive grid.",
"default": 6,
"min": 3,
"max": 12,
"step": 1
},
{
"id": "stagger",
"type": "number",
"role": "motion",
"label": "Stagger",
"description": "Delay between consecutive tile entrances.",
"default": 60,
"min": 20,
"max": 150,
"step": 1,
"unit": "ms"
},
{
"id": "direction",
"type": "enum",
"role": "motion",
"label": "Direction",
"description": "Direction each tile travels into its resting position.",
"default": "up",
"options": [
{ "value": "up", "label": "Up" },
{ "value": "down", "label": "Down" },
{ "value": "left", "label": "Left" },
{ "value": "right", "label": "Right" }
]
}
]
}
+258
View File
@@ -0,0 +1,258 @@
<!doctype html>
<!--
stagger-cascade: HyperFrames video primitive (effects / burst / exhibit)
Concept: a responsive grid of plain tile cards enters in DOM order with one
visible, evenly spaced GSAP stagger. Each tile fades from opacity 0 to 1 and
travels along the selected axis from a container-relative offset to rest.
One mechanic, one job: exhibit a group through a readable cascade.
Compiled-from evidence: production census (launches-motion-vocabulary.md),
where stagger cascades appear in 13 of 18 launches and observed steps center
around 0.04 to 0.08 seconds, with 0.14 to 0.18 seconds used for deliberate
item-by-item reveals.
Use when: a set of cards, features, rows, or related objects needs to arrive
as an ordered group without adding a competing hero motion.
Variables (declared in data-composition-variables below):
- itemCount (number, default 6, range 3 to 12): tiles in the grid.
- stagger (number, default 60, range 20 to 150 ms): delay per tile.
- direction (up | down | left | right, default up): travel direction into
the resting grid. Up starts below, left starts to the right, and so on.
Envelope (fixed IN/OUT, elastic HOLD only, never gsap.timeScale()):
IN_BASE = 2.40s cascade budget, including the slowest valid stagger.
HOLD = elastic = max(0, D - (IN_BASE + OUT_BASE)); deliberately still
so the ordered entrance remains the only mechanic.
OUT_BASE = 0.40s clean group fade using --ease-exit (power2.in).
If D < IN_BASE + OUT_BASE, IN and OUT scale down together so IN + OUT == D
and HOLD == 0. The stagger step scales only in that compressed envelope.
Sync point: none. The mechanic distributes arrivals across the IN phase, so
there is no single fixed cue that represents the cascade honestly.
Sound cue: none. A distributed cascade has no meaningful single-frame sound
event, so this primitive does not dispatch hf:sfx.
Mount contract: this file is a MOUNTABLE SUB-COMPOSITION. A host loads it via
data-composition-src, and the runtime clones only <template> contents. Styles,
markup, GSAP, and timeline registration therefore live inside <template>.
#root has position:absolute; inset:0; container-type:size and no data-width or
data-height, so the host owns the box. The composition id is hardcoded as
"stagger-cascade" because mount flattening strips it from the live root.
Variables come from window.__hyperframes.getVariables(), which supplies the
declared defaults merged with host data-variable-values overrides.
-->
<html
lang="en"
data-composition-variables='[
{ "id": "itemCount", "type": "number", "role": "layout", "label": "Item count", "description": "Number of tiles in the responsive grid.", "default": 6, "min": 3, "max": 12, "step": 1 },
{ "id": "stagger", "type": "number", "role": "motion", "label": "Stagger", "description": "Delay between consecutive tile entrances.", "default": 60, "min": 20, "max": 150, "step": 1, "unit": "ms" },
{ "id": "direction", "type": "enum", "role": "motion", "label": "Direction", "description": "Direction each tile travels into its resting position.", "default": "up", "options": [{ "value": "up", "label": "Up" }, { "value": "down", "label": "Down" }, { "value": "left", "label": "Left" }, { "value": "right", "label": "Right" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Stagger Cascade</title>
<!-- Metadata only. The mount loader reads variable declarations from
<html>, then discards everything outside <template>. -->
</head>
<body>
<template>
<div
id="root"
data-composition-id="stagger-cascade"
data-start="0"
data-duration="4"
data-fps="30"
>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Root fills the host box. Every internal measurement uses cqw/cqh,
and every painted color comes from the host theme contract. */
#root {
position: absolute;
inset: 0;
container-type: size;
isolation: isolate;
overflow: hidden;
color: var(--fg);
font-family: var(--font-body);
}
.sc-clip {
width: 100%;
height: 100%;
display: grid;
place-items: center;
overflow: hidden;
background: var(--bg);
}
.sc-grid {
width: 82cqw;
height: 70cqh;
display: grid;
grid-template-columns: repeat(var(--sc-columns), minmax(0, 1fr));
grid-auto-rows: minmax(0, 1fr);
gap: min(2.2cqw, 3.8cqh);
}
.sc-tile {
position: relative;
min-width: 0;
min-height: 0;
display: grid;
place-items: center;
overflow: hidden;
border: 0.16cqw solid var(--border);
border-radius: min(2.4cqw, 4cqh);
background: color-mix(in srgb, var(--surface) 92%, var(--brand));
color: var(--fg);
}
.sc-tile::after {
content: "";
position: absolute;
left: 14%;
right: 14%;
bottom: 12%;
height: min(0.55cqw, 0.95cqh);
border-radius: 50cqw;
background: var(--brand);
opacity: 0.72;
}
.sc-tile-index {
display: block;
font-family: var(--font-display);
font-size: min(5cqw, 8cqh);
font-weight: 700;
line-height: 1;
color: var(--fg);
}
</style>
<div
id="stagger-cascade-clip"
class="sc-clip clip"
data-start="0"
data-duration="4"
data-track-index="0"
>
<div class="sc-grid" aria-label="Cascading tile grid"></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 compositionId = "stagger-cascade";
var grid = root.querySelector(".sc-grid");
// EDIT ZONE: declarations above and clamped reads below are the
// single source of truth for the primitive's three public knobs.
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var rawItemCount = Number(vars.itemCount);
var itemCount = Number.isFinite(rawItemCount) ? Math.round(rawItemCount) : 6;
itemCount = Math.max(3, Math.min(12, itemCount));
var rawStagger = Number(vars.stagger);
var staggerMs = Number.isFinite(rawStagger) ? rawStagger : 60;
staggerMs = Math.max(20, Math.min(150, staggerMs));
// INVARIANT: only these four mappings can reach GSAP. Each option
// has a distinct axis or sign at preview size.
var offsets = {
up: { x: "0cqw", y: "12cqh" },
down: { x: "0cqw", y: "-12cqh" },
left: { x: "8cqw", y: "0cqh" },
right: { x: "-8cqw", y: "0cqh" },
};
var offset = Object.prototype.hasOwnProperty.call(offsets, vars.direction)
? offsets[vars.direction]
: offsets.up;
// itemCount owns both DOM count and the responsive grid shape.
var fragment = document.createDocumentFragment();
for (var index = 0; index < itemCount; index += 1) {
var tile = document.createElement("div");
var number = document.createElement("span");
tile.className = "sc-tile";
tile.setAttribute("aria-hidden", "true");
number.className = "sc-tile-index";
number.textContent = String(index + 1).padStart(2, "0");
tile.appendChild(number);
fragment.appendChild(tile);
}
grid.replaceChildren(fragment);
grid.style.setProperty(
"--sc-columns",
String(Math.min(4, Math.ceil(Math.sqrt(itemCount)))),
);
var tiles = root.querySelectorAll(".sc-tile");
// RETIME RANGE: change these four base values together. IN_BASE is
// sized so 12 items at 150ms plus the entrance tween finish exactly
// at its boundary. HOLD alone absorbs longer host durations.
var IN_BASE = 2.4;
var OUT_BASE = 0.4;
var CASCADE_START_BASE = 0.1;
var ENTRY_DURATION_BASE = 0.65;
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
var totalBase = IN_BASE + OUT_BASE;
var scale = duration < totalBase ? duration / totalBase : 1;
var IN = IN_BASE * scale;
var OUT = OUT_BASE * scale;
var HOLD = Math.max(0, duration - (IN + OUT));
var OUT_START = IN + HOLD;
var CASCADE_START = CASCADE_START_BASE * scale;
var ENTRY_DURATION = ENTRY_DURATION_BASE * scale;
var STAGGER_SECONDS = (staggerMs / 1000) * scale;
var timeline = gsap.timeline({ paused: true });
// IN: the sole mechanic. Explicit endpoints keep mounted re-seeks
// deterministic. power2.out is --ease-standard.
timeline.fromTo(
tiles,
{ opacity: 0, x: offset.x, y: offset.y },
{
opacity: 1,
x: "0cqw",
y: "0cqh",
duration: ENTRY_DURATION,
ease: "power2.out",
stagger: { each: STAGGER_SECONDS, from: "start" },
},
CASCADE_START,
);
// HOLD: deliberately unanimated. OUT: one clean group fade with
// power2.in, the GSAP form of --ease-exit.
timeline.to(grid, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
timeline.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines[compositionId] = timeline;
})();
</script>
</div>
</template>
</body>
</html>