mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
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:
+103
@@ -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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user