mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -1,288 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
beat-timeline -- HyperFrames video primitive (transitions / sequencing / bridge)
|
||||
|
||||
Concept: a visible orchestration spine made from a paused master GSAP
|
||||
timeline. Each beat row is pinned to an evenly spaced named label, then
|
||||
enters exactly on that mark. One mechanic, one job: bridge a sequence by
|
||||
making its deterministic beat grid readable and copy-and-own-able.
|
||||
|
||||
Compiled-from evidence: launches-motion-vocabulary.md, "Paused, seeked
|
||||
timeline orchestration" census entry (15/18 projects, 134/159 files).
|
||||
|
||||
Use when: a scene needs several titled beats to land in a clear order, or
|
||||
when a copied composition needs a legible master-timeline sequencing spine.
|
||||
|
||||
Variables (declared in data-composition-variables below):
|
||||
- beatCount (number, default 4): integer row count, clamped to 2..6.
|
||||
- interval (number, default 0.6s): base spacing between named beat labels,
|
||||
clamped to 0.2..2s before any short-duration compression.
|
||||
- labels (string): comma-separated row titles. Missing entries fall back
|
||||
to "Beat N" and extra entries are ignored.
|
||||
|
||||
Envelope (holdable orchestration adaptation, never gsap.timeScale()):
|
||||
ENTRANCE_SPAN = (beatCount - 1) * interval + ROW_IN_DURATION.
|
||||
HOLD = max(0, D - (ENTRANCE_SPAN + OUT_BASE)); every landed row remains
|
||||
fully visible and deliberately still during this elastic phase.
|
||||
OUT_BASE = 0.40s; all rows fade together with power2.in at D - OUT_BASE.
|
||||
If D is shorter than ENTRANCE_SPAN + OUT_BASE, interval and each row's
|
||||
entrance duration scale down together so the final row still lands before
|
||||
OUT. Only a host duration at or below OUT_BASE also compresses OUT, which
|
||||
keeps the full lifecycle inside an otherwise impossible time box.
|
||||
|
||||
Sync points: beat labels are fixed offsets inside IN. Every label owns one
|
||||
declared SFX cue at the same offset, never inside elastic HOLD. Beats use
|
||||
"beat-tick" and the last beat uses "beat-tick-final". The primitive only
|
||||
dispatches bubbling hf:sfx events; scene-level audio mixing owns playback.
|
||||
|
||||
Mount contract: this file is a mountable sub-composition. The runtime clones
|
||||
only <template> contents, so all live styles, markup, and scripts are inside
|
||||
the template. #root fills the host-owned box with no data-width/data-height,
|
||||
and all internal geometry is based on its own cqw/cqh container. Variables
|
||||
come from window.__hyperframes.getVariables(), because document.documentElement
|
||||
belongs to the host after mounting.
|
||||
-->
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{ "id": "beatCount", "type": "number", "role": "content", "label": "Beat count", "description": "Number of beat rows in the sequence.", "default": 4, "min": 2, "max": 6, "step": 1 },
|
||||
{ "id": "interval", "type": "number", "role": "layout", "label": "Interval", "description": "Base time between consecutive beat markers.", "default": 0.6, "min": 0.2, "max": 2, "step": 0.05, "unit": "s" },
|
||||
{ "id": "labels", "type": "string", "role": "content", "label": "Labels", "description": "Comma-separated beat titles, one per beat. Extra entries are ignored and missing entries fall back to Beat N.", "default": "One,Two,Three,Four" }
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Beat Timeline</title>
|
||||
<!-- Metadata only. The loader reads variable declarations from <html>,
|
||||
then discards everything outside <template> during mounting. -->
|
||||
</head>
|
||||
<body>
|
||||
<template>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="beat-timeline"
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
data-fps="30"
|
||||
>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Root is styled by ID for mounted CSS scoping. It owns no stage
|
||||
dimensions, only the host box and the container-unit basis. */
|
||||
#root {
|
||||
--ease-standard: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
--ease-exit: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
container-type: size;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
background: var(--bg, Canvas);
|
||||
color: var(--fg, CanvasText);
|
||||
font-family: var(--font-body, system-ui, sans-serif);
|
||||
}
|
||||
|
||||
.bt-clip {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.bt-stack {
|
||||
width: 84cqw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.4cqh;
|
||||
}
|
||||
|
||||
/* EDIT ZONE: row geometry only. Motion timing is owned by the script's
|
||||
RETIME RANGE below, and theme paint remains token-driven here. */
|
||||
.bt-row {
|
||||
min-height: 10cqh;
|
||||
display: grid;
|
||||
grid-template-columns: 6cqw 1fr;
|
||||
align-items: center;
|
||||
column-gap: 2.6cqw;
|
||||
padding: 1.4cqh 2.4cqw;
|
||||
border: 0.12cqw solid var(--border, currentColor);
|
||||
border-radius: var(--radius, 2cqh);
|
||||
background: var(--surface, Canvas);
|
||||
box-shadow: 0 1.2cqh 4cqh color-mix(in srgb, var(--bg, Canvas) 52%, transparent);
|
||||
}
|
||||
|
||||
.bt-index {
|
||||
width: 6cqw;
|
||||
height: 6cqw;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--brand, currentColor);
|
||||
color: var(--bg, Canvas);
|
||||
font-family: var(--font-display, system-ui, sans-serif);
|
||||
font-size: 1.8cqw;
|
||||
font-weight: 750;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bt-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bt-title {
|
||||
overflow: hidden;
|
||||
color: var(--fg, CanvasText);
|
||||
font-family: var(--font-display, system-ui, sans-serif);
|
||||
font-size: min(4.4cqw, 5.2cqh);
|
||||
font-weight: 700;
|
||||
line-height: 1.04;
|
||||
letter-spacing: -0.08cqw;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bt-subtitle {
|
||||
margin-top: 0.7cqh;
|
||||
color: var(--muted, currentColor);
|
||||
font-size: min(1.8cqw, 2.2cqh);
|
||||
font-weight: 550;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 0.04cqw;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="beat-timeline-clip"
|
||||
class="bt-clip clip"
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div class="bt-stack" aria-label="Beat timeline"></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");
|
||||
// Literal by contract: mounting strips data-composition-id from the
|
||||
// flattened inner root, so reading the attribute post-mount would
|
||||
// register under null and fail the timeline lookup.
|
||||
var compositionId = "beat-timeline";
|
||||
var stack = root.querySelector(".bt-stack");
|
||||
|
||||
// EDIT ZONE: declarations live on <html>; this runtime call is the
|
||||
// single owner of declared defaults plus per-instance overrides.
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
|
||||
var parsedBeatCount = Number(vars.beatCount);
|
||||
// INVARIANT: exactly 2..6 integer rows reach the DOM and timeline.
|
||||
var beatCount = Math.max(
|
||||
2,
|
||||
Math.min(6, Math.round(Number.isFinite(parsedBeatCount) ? parsedBeatCount : 4)),
|
||||
);
|
||||
var parsedInterval = Number(vars.interval);
|
||||
// INVARIANT: invalid timing falls back once, valid timing clamps to
|
||||
// the declared range before duration elasticity is calculated.
|
||||
var interval = Math.max(
|
||||
0.2,
|
||||
Math.min(2, Number.isFinite(parsedInterval) ? parsedInterval : 0.6),
|
||||
);
|
||||
// INVARIANT: labels is always a string; blank or missing entries
|
||||
// fall back per row, while authored extra entries stay unused.
|
||||
var labelsValue = vars.labels == null ? "One,Two,Three,Four" : String(vars.labels);
|
||||
var suppliedLabels = labelsValue.split(",").map(function (label) {
|
||||
return label.trim();
|
||||
});
|
||||
|
||||
var parsedDuration = parseFloat(root.dataset.duration || "5.5");
|
||||
// INVARIANT: the lifecycle remains finite even under a malformed
|
||||
// host duration. The authored and demo durations are both 5.5s.
|
||||
var duration = Math.max(0.001, Number.isFinite(parsedDuration) ? parsedDuration : 5.5);
|
||||
|
||||
// RETIME RANGE: these values own the whole pacing decision. The
|
||||
// interval and row entrance compress together, never via timeScale.
|
||||
var ROW_IN_BASE = 0.4;
|
||||
var OUT_BASE = 0.4;
|
||||
var baseEntranceSpan = (beatCount - 1) * interval + ROW_IN_BASE;
|
||||
var OUT = duration <= OUT_BASE ? duration / 2 : OUT_BASE;
|
||||
var entranceBudget = duration - OUT;
|
||||
var scale = baseEntranceSpan > entranceBudget ? entranceBudget / baseEntranceSpan : 1;
|
||||
var timedInterval = interval * scale;
|
||||
var ROW_IN = ROW_IN_BASE * scale;
|
||||
var ENTRANCE_SPAN = (beatCount - 1) * timedInterval + ROW_IN;
|
||||
var HOLD = Math.max(0, duration - (ENTRANCE_SPAN + OUT));
|
||||
var OUT_START = ENTRANCE_SPAN + HOLD;
|
||||
|
||||
function fireSfx(id, t) {
|
||||
root.dispatchEvent(
|
||||
new CustomEvent("hf:sfx", { detail: { id: id, t: t }, bubbles: true }),
|
||||
);
|
||||
}
|
||||
|
||||
var rows = [];
|
||||
for (var i = 0; i < beatCount; i += 1) {
|
||||
var title = suppliedLabels[i] || "Beat " + (i + 1);
|
||||
var row = document.createElement("div");
|
||||
row.className = "bt-row";
|
||||
row.innerHTML =
|
||||
'<div class="bt-index" aria-hidden="true"></div>' +
|
||||
'<div class="bt-copy"><div class="bt-title"></div><div class="bt-subtitle"></div></div>';
|
||||
row.querySelector(".bt-index").textContent = String(i + 1).padStart(2, "0");
|
||||
row.querySelector(".bt-title").textContent = title;
|
||||
row.querySelector(".bt-subtitle").textContent =
|
||||
"Cue " +
|
||||
String(i + 1).padStart(2, "0") +
|
||||
" · " +
|
||||
(i * timedInterval).toFixed(2) +
|
||||
"s";
|
||||
stack.appendChild(row);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
// Explicit entrance and exit endpoints make a direct seek to t=0
|
||||
// correct without ever playing the timeline.
|
||||
gsap.set(rows, { opacity: 0, y: "2cqh", scale: 0.98 });
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// ORCHESTRATION SPINE: every row tween and its declared cue are
|
||||
// pinned to the named label, never to a duplicated numeric offset.
|
||||
for (var beatIndex = 0; beatIndex < beatCount; beatIndex += 1) {
|
||||
var labelName = "beat" + beatIndex;
|
||||
var cueTime = beatIndex * timedInterval;
|
||||
var cueId = beatIndex === beatCount - 1 ? "beat-tick-final" : "beat-tick";
|
||||
tl.addLabel(labelName, cueTime);
|
||||
tl.to(
|
||||
rows[beatIndex],
|
||||
{ opacity: 1, y: "0cqh", scale: 1, duration: ROW_IN, ease: "power2.out" },
|
||||
labelName,
|
||||
);
|
||||
// SFX CUE (IN, beat N): one scene-mixed cue at this beat label.
|
||||
tl.call(fireSfx, [cueId, cueTime], labelName);
|
||||
}
|
||||
|
||||
// HOLD is deliberately still. OUT accelerates every landed row away
|
||||
// together, asymmetrically paired with the power2.out entrances.
|
||||
tl.to(rows, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
|
||||
|
||||
tl.seek(0);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines[compositionId] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user