mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -1,316 +0,0 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user