Files
hyperframes/registry/components/per-word-rise/per-word-rise.html
T
Miguel Ángel 9734578e60 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.
2026-08-10 18:46:03 -04:00

317 lines
13 KiB
HTML
Vendored

<!doctype html>
<!--
per-word-rise: HyperFrames video primitive (text effects / reveal)
Words, or characters through the split variable, rise into place with a
controlled cascade and a small blur-to-sharp settle. Each unit lands with
a short overshoot-free settle tail (the final few pixels glide in over the
closing frames instead of stopping dead), then the finished line holds
truly still until the frame cuts (exit none) or departs with the optional
exit variable.
Variables:
- text (string, default "WORDS IN MOTION"): displayed text.
- split (word | char, default "word"): animated unit size.
- cues (string, default ""): comma-separated seconds (from mount start)
for each unit's landing. Empty keeps the authored cascade rhythm.
- accent (green | blue | violet, default "green"): text color.
- exit (none | fade | up, default "none"): optional departure.
Envelope, fixed IN and OUT with elastic HOLD only:
IN_BASE = 1.45s, the full staggered rise and sharp settle (cues can
stretch the rise phase past IN_BASE when they land later)
HOLD = max(0, D - IN - OUT), truly still: no drift
OUT_BASE = 0.50s staggered departure when exit is fade or up, 0s when
exit is none (the default: the line holds until the cut)
If D is shorter than IN_BASE + OUT_BASE, IN and OUT 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 per-word-rise key.
-->
<html
lang="en"
data-composition-id="per-word-rise"
data-composition-duration="3.5"
data-composition-variables='[
{ "id": "text", "type": "string", "role": "content", "label": "Text", "description": "Text whose words or characters rise into place.", "default": "WORDS IN MOTION" },
{ "id": "split", "type": "enum", "role": "content", "label": "Split", "description": "Animate the text as words or individual characters.", "default": "word", "options": [{ "value": "word", "label": "Word" }, { "value": "char", "label": "Character" }] },
{ "id": "cues", "type": "string", "role": "timing", "label": "Cues", "description": "Comma-separated seconds (from mount start) for each unit landing. Empty keeps the authored cascade rhythm.", "default": "" },
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Contract accent token used for the text color.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
{ "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure. Default none: the line holds until the frame cuts.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Per Word Rise</title>
</head>
<body>
<template>
<div id="root" data-composition-id="per-word-rise" data-duration="3.5" 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-display, Inter, system-ui, sans-serif);
pointer-events: none;
}
.pwr-clip {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
background: var(--bg, #0b0c0e);
}
.pwr-stage {
display: flex;
width: 88cqw;
max-width: 88cqw;
align-items: baseline;
justify-content: center;
flex-wrap: wrap;
column-gap: var(--space-1, 1.8cqw);
row-gap: var(--space-2, 2.8cqh);
color: var(--pwr-accent);
font-size: var(--pwr-font-size);
font-weight: 780;
line-height: 1.08;
letter-spacing: -0.035em;
text-align: center;
}
.pwr-stage.pwr-char {
column-gap: 0.045em;
letter-spacing: 0;
}
.pwr-unit {
display: inline-block;
flex: 0 0 auto;
filter: blur(var(--pwr-blur, 0cqh));
will-change: transform, filter, opacity;
}
.pwr-space {
width: 0.36em;
}
</style>
<div
id="per-word-rise-clip"
class="pwr-clip clip"
data-start="0"
data-duration="3.5"
data-track-index="0"
>
<div class="pwr-stage" role="img"></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 = root.querySelector(".pwr-stage");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var text = vars.text == null ? "WORDS IN MOTION" : String(vars.text);
var split = vars.split === "char" ? "char" : "word";
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";
var cuesValue = vars.cues == null ? "" : String(vars.cues);
var cues = cuesValue.split(",").reduce(function (result, token) {
var trimmed = token.trim();
if (trimmed === "") return result;
var seconds = Number(trimmed);
if (!Number.isFinite(seconds) || seconds < 0) return result;
result.push(seconds);
return result;
}, []);
root.style.setProperty("--pwr-accent", accentColors[accent]);
stage.classList.toggle("pwr-char", split === "char");
stage.setAttribute("aria-label", text);
var characterCount = Math.max(1, Array.from(text).length);
var fittedCqw = Math.min(9.5, 126 / characterCount);
root.style.setProperty(
"--pwr-font-size",
"min(" + fittedCqw.toFixed(3) + "cqw, 15cqh)",
);
var segments = split === "char" ? Array.from(text) : text.trim().split(/\s+/);
if (segments.length === 0) segments = [""];
var units = segments.map(function (segment, index) {
var unit = document.createElement("span");
unit.id = "per-word-rise-unit-" + index;
unit.className = "pwr-unit";
if (split === "char" && /\s/.test(segment)) {
unit.classList.add("pwr-space");
unit.textContent = "\u00a0";
} else {
unit.textContent = segment;
}
unit.setAttribute("aria-hidden", "true");
stage.appendChild(unit);
return unit;
});
var IN_BASE = 1.45;
var OUT_BASE = exit === "none" ? 0 : 0.5;
var RISE_BASE = 0.74;
var STAGGER_BASE = 0.62;
var EXIT_STAGGER_BASE = 0.14;
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "3.5"));
var totalBase = Math.max(0.001, IN_BASE + OUT_BASE);
var scale = duration < totalBase ? duration / totalBase : 1;
var IN = IN_BASE * scale;
var OUT = OUT_BASE * scale;
var RISE = RISE_BASE * scale;
var STAGGER = units.length > 1 ? STAGGER_BASE * scale : 0;
var EXIT_STAGGER = units.length > 1 ? EXIT_STAGGER_BASE * scale : 0;
var EXIT_MOVE = Math.max(0.001, OUT - EXIT_STAGGER);
// Landing ease: overshoot-free decel with a settle tail. A plain
// power3.out slams from fast to imperceptible in ~2 frames and the
// words stop dead; this piecewise cubic keeps ~7% of the travel for
// the last 22% of the duration and is C1-continuous at the seam,
// so the word glides its final few pixels over the closing frames
// (a settle) with monotonic velocity: no stall, no hop, no
// overshoot. Pure function of progress, so it stays seek-safe.
var LAND_SPLIT = 0.78; // progress where the settle tail begins
var LAND_RESIDUAL = 0.07; // fraction of travel left for the tail
var LAND_SEAM_SLOPE = ((1 - LAND_RESIDUAL) / LAND_SPLIT) * 0.5; // shared velocity at the seam
function landEase(p) {
if (p <= 0) return 0;
if (p >= 1) return 1;
if (p < LAND_SPLIT) {
var x = p / LAND_SPLIT;
var inv = 1 - x;
// Half cubic-out, half linear: keeps end velocity for the seam.
return (1 - LAND_RESIDUAL) * (0.5 * (1 - inv * inv * inv) + 0.5 * x);
}
var u = (p - LAND_SPLIT) / (1 - LAND_SPLIT);
var b = LAND_SEAM_SLOPE * (1 - LAND_SPLIT);
var c = 3 * LAND_RESIDUAL - 2 * b;
var d = b - 2 * LAND_RESIDUAL;
return 1 - LAND_RESIDUAL + b * u + c * u * u + d * u * u * u;
}
// Cues own the word landings when provided; the authored cascade
// otherwise. Landings beyond the cue list extrapolate at the cue
// list's own gap, and every landing clamps ahead of any exit.
var riseEnd = IN;
var landings;
if (cues.length > 0) {
var lastCue = cues[cues.length - 1];
var cueGap =
cues.length >= 2
? Math.max(0.05, (lastCue - cues[0]) / (cues.length - 1))
: RISE * 0.4;
var latestLanding = Math.max(RISE * 0.5, duration - OUT);
landings = units.map(function (unit, index) {
var landing =
index < cues.length
? cues[index]
: lastCue + (index - (cues.length - 1)) * cueGap;
return Math.min(latestLanding, Math.max(0.05, landing));
});
riseEnd = Math.min(duration - OUT, Math.max(IN, Math.max.apply(null, landings)));
} else {
// Authored cascade: same left-to-right wave the stagger drew,
// expressed as explicit landings so every unit gets the settle.
var step = units.length > 1 ? STAGGER / (units.length - 1) : 0;
landings = units.map(function (unit, index) {
return index * step + RISE;
});
}
var HOLD_START = riseEnd;
var HOLD = Math.max(0, duration - (HOLD_START + OUT));
var OUT_START = HOLD_START + HOLD;
gsap.set(units, { opacity: 0, y: "8cqh", "--pwr-blur": "1.4cqh" });
var tl = gsap.timeline({ paused: true });
units.forEach(function (unit, index) {
var landing = landings[index];
var startAt = Math.max(0, landing - RISE);
tl.to(
unit,
{
opacity: 1,
y: "0cqh",
"--pwr-blur": "0cqh",
duration: Math.max(0.08, landing - startAt),
ease: landEase,
},
startAt,
);
});
// TRUE hold: the finished line is dead still until the exit or
// the cut. The old 0.55cqh drift was sub-perceptual at 1080p.
// OUT: optional departure; exit none holds until the frame cuts.
if (exit === "up") {
tl.to(
units,
{
opacity: 0,
y: "-4.5cqh",
"--pwr-blur": "0.7cqh",
duration: EXIT_MOVE,
stagger: { amount: EXIT_STAGGER, from: "start" },
ease: "power2.in",
},
OUT_START,
);
} else if (exit === "fade") {
tl.to(
units,
{
opacity: 0,
duration: EXIT_MOVE,
stagger: { amount: EXIT_STAGGER, from: "start" },
ease: "power2.in",
},
OUT_START,
);
}
tl.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines["per-word-rise"] = tl;
})();
</script>
</div>
</template>
</body>
</html>