mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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.
368 lines
14 KiB
HTML
Vendored
368 lines
14 KiB
HTML
Vendored
<!doctype html>
|
||
<!--
|
||
logo-brand-close: HyperFrames video primitive (brand close)
|
||
|
||
The identity close that ends films. The wordmark lands at display scale,
|
||
a chars-aware fit sizing it to roughly three quarters of the frame width.
|
||
Letters cascade left to right (per-letter fade plus rise with a long
|
||
expo tail) while the whole wordmark settles from a slightly larger scale,
|
||
the accent brand period arrives last with a decisive pop, a readable
|
||
tagline settles beneath, a wide-tracked mono URL breathes in below, then
|
||
the finished lockup holds dead still to the end. Distinct from cta-close:
|
||
identity, not action. No button, no cursor.
|
||
|
||
Variables:
|
||
- wordmark (string): the brand name that cascades into place.
|
||
- tagline (string): optional line beneath the wordmark; empty hides it.
|
||
- url (string): optional mono URL line; empty hides it.
|
||
- accent (green | blue | violet): color of the brand period.
|
||
- exit (none | fade | up, default none): films end on the hold.
|
||
|
||
Envelope, fixed IN with elastic HOLD only (OUT exists only when exit is
|
||
not none):
|
||
IN_BASE = 2.6s, letter cascade + scale settle, period pop, tagline
|
||
settle, URL tracking settle
|
||
HOLD = max(0, D - IN - OUT), completely still
|
||
OUT_BASE = 0.5s only when exit is fade or up, otherwise 0
|
||
If D is shorter than IN_BASE + OUT_BASE, both compress together.
|
||
The timeline is never time-scaled.
|
||
|
||
Mount contract: the runtime clones only this template. #root fills the host
|
||
box, establishes the container query basis, has no data-width or data-height,
|
||
and registers one paused timeline under the literal logo-brand-close key.
|
||
-->
|
||
<html
|
||
lang="en"
|
||
data-composition-id="logo-brand-close"
|
||
data-composition-duration="4"
|
||
data-composition-variables='[
|
||
{ "id": "wordmark", "type": "string", "role": "content", "label": "Wordmark", "description": "Brand name whose letters cascade into the centered lockup.", "default": "HYPERFRAMES" },
|
||
{ "id": "tagline", "type": "string", "role": "content", "label": "Tagline", "description": "Optional tagline that settles beneath the wordmark. Empty hides it.", "default": "Write HTML. Render video." },
|
||
{ "id": "url", "type": "string", "role": "content", "label": "URL", "description": "Optional mono URL line beneath the tagline. Empty hides it.", "default": "hyperframes.heygen.com" },
|
||
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Contract color used by the brand period at the end of the wordmark.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
|
||
{ "id": "exit", "type": "enum", "role": "style", "label": "Exit", "description": "Optional departure. Films end on the still hold, so the default is none.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
|
||
]'
|
||
>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>Logo Brand Close</title>
|
||
</head>
|
||
<body>
|
||
<template>
|
||
<div id="root" data-composition-id="logo-brand-close" data-duration="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, #f8fafc);
|
||
font-family: var(--font-body, Inter, system-ui, sans-serif);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.lbc-clip {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
overflow: hidden;
|
||
background: var(--bg, #0b0c0e);
|
||
}
|
||
|
||
.lbc-lockup {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 100cqw;
|
||
text-align: center;
|
||
will-change: transform, opacity;
|
||
}
|
||
|
||
/* Display scale: the JS chars-aware fit targets ~76cqw of set
|
||
width; 40cqh caps it in short, wide mounts. */
|
||
.lbc-wordmark {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
margin: 0;
|
||
color: var(--fg, #f8fafc);
|
||
font-family: var(--font-display, Inter, system-ui, sans-serif);
|
||
font-size: var(--lbc-wordmark-size, min(10.5cqw, 40cqh));
|
||
font-weight: 600;
|
||
line-height: 1.04;
|
||
letter-spacing: -0.035em;
|
||
white-space: nowrap;
|
||
will-change: transform;
|
||
}
|
||
|
||
.lbc-letter {
|
||
display: inline-block;
|
||
flex: 0 0 auto;
|
||
will-change: transform, opacity;
|
||
}
|
||
|
||
.lbc-space {
|
||
width: 0.34em;
|
||
}
|
||
|
||
.lbc-period {
|
||
color: var(--lbc-accent);
|
||
}
|
||
|
||
.lbc-tagline {
|
||
max-width: 88cqw;
|
||
margin: var(--space-2, 3cqmin) 0 0;
|
||
color: var(--fg, #f8fafc);
|
||
font-size: min(3.4cqw, 5.4cqmin);
|
||
font-weight: 500;
|
||
line-height: 1.3;
|
||
letter-spacing: 0.01em;
|
||
will-change: transform, opacity;
|
||
}
|
||
|
||
.lbc-url {
|
||
margin: var(--space-3, 4.6cqmin) 0 0;
|
||
color: var(--muted, #94a3b8);
|
||
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
|
||
font-size: min(2.4cqw, 3.2cqmin);
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
letter-spacing: 0.28em;
|
||
will-change: opacity;
|
||
}
|
||
</style>
|
||
|
||
<div
|
||
id="logo-brand-close-clip"
|
||
class="lbc-clip clip"
|
||
data-start="0"
|
||
data-duration="4"
|
||
data-track-index="0"
|
||
>
|
||
<div class="lbc-lockup">
|
||
<h2 class="lbc-wordmark"></h2>
|
||
<p class="lbc-tagline"></p>
|
||
<p class="lbc-url"></p>
|
||
</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 lockup = root.querySelector(".lbc-lockup");
|
||
var wordmarkElement = root.querySelector(".lbc-wordmark");
|
||
var taglineElement = root.querySelector(".lbc-tagline");
|
||
var urlElement = root.querySelector(".lbc-url");
|
||
var vars =
|
||
window.__hyperframes && window.__hyperframes.getVariables
|
||
? window.__hyperframes.getVariables()
|
||
: {};
|
||
|
||
var wordmark =
|
||
vars.wordmark == null
|
||
? "HYPERFRAMES"
|
||
: String(vars.wordmark).trim().replace(/\s+/g, " ");
|
||
if (wordmark.length === 0) wordmark = "HYPERFRAMES";
|
||
var tagline =
|
||
vars.tagline == null
|
||
? "Write HTML. Render video."
|
||
: String(vars.tagline).trim().replace(/\s+/g, " ");
|
||
var url = vars.url == null ? "hyperframes.heygen.com" : String(vars.url).trim();
|
||
var accentColors = {
|
||
green: "var(--brand, #71f5a7)",
|
||
blue: "var(--accent, #61a8ff)",
|
||
violet: "var(--accent-2, #c5a3ff)",
|
||
};
|
||
var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
|
||
? vars.accent
|
||
: "green";
|
||
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
|
||
|
||
root.style.setProperty("--lbc-accent", accentColors[accent]);
|
||
wordmarkElement.setAttribute("aria-label", wordmark);
|
||
wordmarkElement.setAttribute("role", "img");
|
||
|
||
// The brand period: an accent dot ends the wordmark. If the
|
||
// wordmark already ends with "." that period takes the accent
|
||
// instead of appending a second one.
|
||
var characters = Array.from(wordmark);
|
||
if (characters[characters.length - 1] !== ".") characters.push(".");
|
||
|
||
var letters = characters.map(function (character, index) {
|
||
var letter = document.createElement("span");
|
||
letter.id = "logo-brand-close-letter-" + index;
|
||
letter.className = "lbc-letter";
|
||
if (/\s/.test(character)) {
|
||
letter.classList.add("lbc-space");
|
||
letter.textContent = " ";
|
||
} else {
|
||
letter.textContent = character;
|
||
}
|
||
if (index === characters.length - 1) letter.classList.add("lbc-period");
|
||
letter.setAttribute("aria-hidden", "true");
|
||
wordmarkElement.appendChild(letter);
|
||
return letter;
|
||
});
|
||
var periodLetter = letters[letters.length - 1];
|
||
var cascadeLetters = letters.slice(0, -1);
|
||
|
||
// Deterministic chars-aware fit: probe the set width at a fixed
|
||
// 100px font (letter-spacing rides in em, so the em ratio is a
|
||
// pure font metric), then size the wordmark so it lands at
|
||
// ~76cqw. The 40cqh term caps it in short, wide mounts. If the
|
||
// probe cannot measure (detached render), fall back to a
|
||
// per-glyph advance estimate.
|
||
wordmarkElement.style.fontSize = "100px";
|
||
var probedWidth = wordmarkElement.getBoundingClientRect().width;
|
||
wordmarkElement.style.fontSize = "";
|
||
var setWidthEm;
|
||
if (probedWidth > 0) {
|
||
setWidthEm = probedWidth / 100;
|
||
} else {
|
||
setWidthEm = characters.reduce(function (total, character) {
|
||
if (/\s/.test(character)) return total + 0.34;
|
||
if (/[.,:;!'’|]/.test(character)) return total + 0.3;
|
||
if (/[Iijlftr]/.test(character)) return total + 0.42;
|
||
if (/[MWmw@]/.test(character)) return total + 0.92;
|
||
return total + 0.64;
|
||
}, 0);
|
||
setWidthEm -= characters.length * 0.035;
|
||
}
|
||
setWidthEm = Math.max(0.3, setWidthEm);
|
||
var wordmarkSize = 76 / setWidthEm;
|
||
root.style.setProperty(
|
||
"--lbc-wordmark-size",
|
||
"min(" + wordmarkSize.toFixed(3) + "cqw, 40cqh)",
|
||
);
|
||
|
||
if (tagline.length === 0) taglineElement.remove();
|
||
else taglineElement.textContent = tagline;
|
||
if (url.length === 0) urlElement.remove();
|
||
else urlElement.textContent = url;
|
||
|
||
var IN_BASE = 2.6;
|
||
var OUT_BASE = exit === "none" ? 0 : 0.5;
|
||
var CASCADE_RISE_BASE = 1.15;
|
||
var CASCADE_STAGGER_BASE = 0.7;
|
||
var SETTLE_BASE = 2.4;
|
||
var PERIOD_AT_BASE = 0.95;
|
||
var PERIOD_DURATION_BASE = 0.9;
|
||
var TAGLINE_AT_BASE = 1.35;
|
||
var TAGLINE_DURATION_BASE = 0.9;
|
||
var URL_AT_BASE = 1.7;
|
||
var URL_DURATION_BASE = 0.85;
|
||
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 STAGGER = cascadeLetters.length > 1 ? CASCADE_STAGGER_BASE * scale : 0;
|
||
var HOLD = Math.max(0, duration - (IN + OUT));
|
||
var OUT_START = IN + HOLD;
|
||
|
||
var tl = gsap.timeline({ paused: true });
|
||
|
||
// IN: per-letter fade plus rise with a long expo tail, while the
|
||
// whole wordmark settles down from a slightly larger scale.
|
||
if (cascadeLetters.length > 0) {
|
||
tl.fromTo(
|
||
cascadeLetters,
|
||
{ opacity: 0, y: "0.62em" },
|
||
{
|
||
opacity: 1,
|
||
y: "0em",
|
||
duration: CASCADE_RISE_BASE * scale,
|
||
stagger: { amount: STAGGER, from: "start" },
|
||
ease: "expo.out",
|
||
},
|
||
0,
|
||
);
|
||
}
|
||
|
||
tl.fromTo(
|
||
wordmarkElement,
|
||
{ scale: 1.04, transformOrigin: "50% 100%" },
|
||
{ scale: 1, duration: SETTLE_BASE * scale, ease: "expo.out" },
|
||
0,
|
||
);
|
||
|
||
// The brand period lands last: a decisive accent pop.
|
||
tl.fromTo(
|
||
periodLetter,
|
||
{ opacity: 0, scale: 0.2, y: "0.08em", transformOrigin: "50% 65%" },
|
||
{
|
||
opacity: 1,
|
||
scale: 1,
|
||
y: "0em",
|
||
duration: PERIOD_DURATION_BASE * scale,
|
||
ease: "back.out(1.8)",
|
||
},
|
||
PERIOD_AT_BASE * scale,
|
||
);
|
||
|
||
if (taglineElement.isConnected) {
|
||
tl.fromTo(
|
||
taglineElement,
|
||
{ opacity: 0, y: "0.9em" },
|
||
{
|
||
opacity: 1,
|
||
y: "0em",
|
||
duration: TAGLINE_DURATION_BASE * scale,
|
||
ease: "power3.out",
|
||
},
|
||
TAGLINE_AT_BASE * scale,
|
||
);
|
||
}
|
||
|
||
if (urlElement.isConnected) {
|
||
tl.fromTo(
|
||
urlElement,
|
||
// scaleX, not letterSpacing: tracking is a layout property, so
|
||
// the browser snaps it to integer device pixels and the
|
||
// ease-out tail stutters under seek-by-frame capture. The
|
||
// widening reads the same and stays on the compositor.
|
||
{ opacity: 0, scaleX: 1.06 },
|
||
{
|
||
opacity: 1,
|
||
scaleX: 1,
|
||
duration: URL_DURATION_BASE * scale,
|
||
ease: "power2.out",
|
||
},
|
||
URL_AT_BASE * scale,
|
||
);
|
||
}
|
||
|
||
// HOLD: dead still. No drift, no breath; the confidence is the
|
||
// stillness, and by default the film ends here.
|
||
|
||
if (exit === "fade") {
|
||
tl.to(lockup, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
|
||
} else if (exit === "up") {
|
||
tl.to(
|
||
lockup,
|
||
{ opacity: 0, y: "-5cqh", duration: OUT, ease: "power2.in" },
|
||
OUT_START,
|
||
);
|
||
}
|
||
|
||
tl.seek(0);
|
||
window.__timelines = window.__timelines || {};
|
||
window.__timelines["logo-brand-close"] = tl;
|
||
})();
|
||
</script>
|
||
</div>
|
||
</template>
|
||
</body>
|
||
</html>
|