mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +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.
374 lines
15 KiB
HTML
Vendored
374 lines
15 KiB
HTML
Vendored
<!doctype html>
|
|
<!--
|
|
press-ripple: HyperFrames video primitive (product demo / payoff)
|
|
|
|
A cursor decel-arrives from off-stage (power2.out, it arrives, never passes
|
|
through), lands a few px off-center on a caller-positioned target zone, the
|
|
target and cursor compress in lockstep, release with two ink ripple rings,
|
|
then the cursor exits off-stage the way it came. The pressed state and the
|
|
ripple aftermath carry the hold; the cursor never camps on the label.
|
|
|
|
The target is a slot: the element marked data-slot="target" is the press
|
|
surface. Its default content is a token-styled pill carrying the label
|
|
variable; replace the slot's children to press anything else. The slot
|
|
wrapper flips data-state="idle" to data-state="pressed" at the press, so
|
|
custom slot content can style its own pressed look.
|
|
|
|
Variables:
|
|
label: text inside the default pill (ignored when the slot is replaced)
|
|
target_x, target_y: zone center as a percent of the host box
|
|
press_at: seconds from mount start when the compression begins
|
|
cursor: light or dark pointer
|
|
accent: green, blue, or violet ink (ripple rings + pressed pill fill)
|
|
exit: none, fade, or up (default none; holds end films)
|
|
|
|
Envelope (elastic HOLD, never time-scaled):
|
|
IN = entrance settle, cursor arrival, press, release ripple, cursor exit
|
|
HOLD = the rest of D, completely still on the pressed state
|
|
OUT = none by default; 0.45s fade or fade-up when exit is set
|
|
press_at clamps into the window that keeps the whole gesture inside D.
|
|
|
|
Sync point: the press lands at press_at (default 1.4s).
|
|
|
|
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 press-ripple key.
|
|
-->
|
|
<html
|
|
lang="en"
|
|
data-composition-id="press-ripple"
|
|
data-composition-duration="3"
|
|
data-composition-variables='[
|
|
{ "id": "label", "type": "string", "role": "content", "label": "Label", "description": "Text inside the default pill target.", "default": "Get started" },
|
|
{ "id": "target_x", "type": "number", "role": "layout", "label": "Target X", "description": "Zone center, percent of host width.", "default": 50, "min": 8, "max": 92, "step": 1, "unit": "%" },
|
|
{ "id": "target_y", "type": "number", "role": "layout", "label": "Target Y", "description": "Zone center, percent of host height.", "default": 50, "min": 8, "max": 92, "step": 1, "unit": "%" },
|
|
{ "id": "press_at", "type": "number", "role": "timing", "label": "Press at", "description": "Seconds from mount start when the press begins.", "default": 1.4, "min": 0.9, "max": 10, "step": 0.1, "unit": "s" },
|
|
{ "id": "cursor", "type": "enum", "role": "style", "label": "Cursor", "description": "Pointer tone.", "default": "light", "options": [{ "value": "light", "label": "Light" }, { "value": "dark", "label": "Dark" }] },
|
|
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Contract color for the ripple ink and pressed fill.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
|
|
{ "id": "exit", "type": "enum", "role": "style", "label": "Exit", "description": "How the frame leaves. None holds the pressed state.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Fade up" }] }
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Press Ripple</title>
|
|
</head>
|
|
<body>
|
|
<template>
|
|
<div id="root" data-composition-id="press-ripple" data-duration="3" 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;
|
|
}
|
|
|
|
.pr-clip {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
background: var(--bg, #0b0c0e);
|
|
}
|
|
|
|
.pr-stage {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
/* Caller-positioned target zone; --pr-x / --pr-y come from the
|
|
target_x / target_y variables. */
|
|
.pr-zone {
|
|
position: absolute;
|
|
left: calc(var(--pr-x, 50) * 1cqw);
|
|
top: calc(var(--pr-y, 50) * 1cqh);
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
/* Ink ripple rings, centered under the slot, hidden until release.
|
|
The ink mixes the accent toward --fg so the rings carry real
|
|
weight against the host background on both light and dark
|
|
themes; thickness reads as drawn ink, not a hairline. */
|
|
.pr-ring {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 20cqmin;
|
|
height: 20cqmin;
|
|
margin: -10cqmin 0 0 -10cqmin;
|
|
border: 0.85cqmin solid
|
|
color-mix(in srgb, var(--pr-accent, #71f5a7) 72%, var(--fg, #f8fafc));
|
|
border-radius: 50%;
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
/* GSAP owns this wrapper's opacity/y (entrance) and scale (press). */
|
|
.pr-actor {
|
|
position: relative;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
.pr-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: var(--space-2, 2.6cqmin) calc(var(--space-2, 2.6cqmin) * 2.1);
|
|
border: 0.22cqmin solid var(--border, #343a46);
|
|
border-radius: 999px;
|
|
background: var(--surface, #14171c);
|
|
color: var(--fg, #f8fafc);
|
|
font-family: var(--font-body, Inter, system-ui, sans-serif);
|
|
font-size: 4.6cqmin;
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
letter-spacing: -0.015em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pr-target-slot[data-state="pressed"] .pr-pill {
|
|
border-color: color-mix(in srgb, var(--pr-accent, #71f5a7) 72%, var(--fg, #f8fafc));
|
|
background: var(--pr-accent, #71f5a7);
|
|
color: var(--bg, #0b0c0e);
|
|
}
|
|
|
|
/* Pointer: SVG tip sits exactly at this box's top-left corner, so
|
|
the box position places the tip itself. GSAP moves it with x/y
|
|
transforms only (never left/top: layout props snap to integer
|
|
device pixels under the seek-by-frame capture engine) and scales
|
|
it about the tip (press). */
|
|
.pr-cursor {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 7cqmin;
|
|
height: 7cqmin;
|
|
transform-origin: 0 0;
|
|
filter: drop-shadow(0 0.4cqmin 0.9cqmin rgba(2, 6, 23, 0.45));
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
.pr-cursor svg {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|
|
|
|
<div
|
|
id="press-ripple-clip"
|
|
class="pr-clip clip"
|
|
data-start="0"
|
|
data-duration="3"
|
|
data-track-index="0"
|
|
>
|
|
<div class="pr-stage">
|
|
<div class="pr-zone">
|
|
<div class="pr-ring" aria-hidden="true"></div>
|
|
<div class="pr-ring" aria-hidden="true"></div>
|
|
<div class="pr-actor">
|
|
<div class="pr-target-slot" data-slot="target" data-state="idle">
|
|
<span class="pr-pill"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="pr-cursor" aria-hidden="true">
|
|
<svg viewBox="0 0 20 20">
|
|
<path
|
|
class="pr-cursor-path"
|
|
d="M0.6 0.6 L0.6 14.6 L4.4 11.3 L6.7 16.8 L9.4 15.6 L7.1 10.2 L12.4 9.8 Z"
|
|
stroke-width="1.1"
|
|
stroke-linejoin="round"
|
|
></path>
|
|
</svg>
|
|
</div>
|
|
</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(".pr-stage");
|
|
var zone = root.querySelector(".pr-zone");
|
|
var rings = root.querySelectorAll(".pr-ring");
|
|
var actor = root.querySelector(".pr-actor");
|
|
var slot = root.querySelector(".pr-target-slot");
|
|
var pill = root.querySelector(".pr-pill");
|
|
var cursor = root.querySelector(".pr-cursor");
|
|
var cursorPath = root.querySelector(".pr-cursor-path");
|
|
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
function num(value, fallback, min, max) {
|
|
var n = Number(value);
|
|
if (!isFinite(n)) n = fallback;
|
|
return Math.min(max, Math.max(min, n));
|
|
}
|
|
|
|
var labelText =
|
|
vars.label == null ? "Get started" : String(vars.label).trim().replace(/\s+/g, " ");
|
|
var targetX = num(vars.target_x, 50, 8, 92);
|
|
var targetY = num(vars.target_y, 50, 8, 92);
|
|
var cursorTone = vars.cursor === "dark" ? "dark" : "light";
|
|
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
|
|
|
|
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";
|
|
// The bundler mirrors composition variables as scoped CSS custom
|
|
// props, so this unit's own accent variable shadows the contract
|
|
// --accent token inside the subtree ("blue" is a valid CSS color
|
|
// and would render pure blue). When the shadow is present, fall
|
|
// back to the literal contract value instead of var(--accent).
|
|
var computedAccent = getComputedStyle(root).getPropertyValue("--accent").trim();
|
|
if (
|
|
computedAccent === "green" ||
|
|
computedAccent === "blue" ||
|
|
computedAccent === "violet"
|
|
) {
|
|
accentColors.blue = "#61a8ff";
|
|
}
|
|
|
|
pill.textContent = labelText;
|
|
root.style.setProperty("--pr-accent", accentColors[accent]);
|
|
root.style.setProperty("--pr-x", String(targetX));
|
|
root.style.setProperty("--pr-y", String(targetY));
|
|
if (cursorTone === "dark") {
|
|
cursorPath.setAttribute("fill", "#111827");
|
|
cursorPath.setAttribute("stroke", "#f8fafc");
|
|
} else {
|
|
cursorPath.setAttribute("fill", "#f8fafc");
|
|
cursorPath.setAttribute("stroke", "#0f172a");
|
|
}
|
|
|
|
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "3"));
|
|
var OUT = exit === "none" ? 0 : 0.45;
|
|
// The whole press tail (compress + release ripple + cursor exit)
|
|
// is ~1.05s; keep it inside D, then keep the OUT window after it.
|
|
var pressAt = num(vars.press_at, 1.4, 0.9, Math.max(0.9, duration - 1.05 - OUT));
|
|
// Human aim: the tip lands a few px off the zone's true center.
|
|
var landX = targetX + 0.55;
|
|
var landY = targetY + 0.85;
|
|
var arriveEnd = pressAt - 0.16;
|
|
var arriveStart = Math.max(0, arriveEnd - 0.78);
|
|
|
|
// Motion runs on transforms only (x/y px), never left/top: layout
|
|
// props snap to integer device pixels under the seek-by-frame
|
|
// capture engine. Stage size is read once, synchronously, at load
|
|
// (deterministic: fixed viewport per render) to convert the
|
|
// percent-based land/off-stage positions into px.
|
|
var stageW = root.clientWidth || 1920;
|
|
var stageH = root.clientHeight || 1080;
|
|
function xAt(percent) {
|
|
return (percent / 100) * stageW;
|
|
}
|
|
function yAt(percent) {
|
|
return (percent / 100) * stageH;
|
|
}
|
|
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
// IN: target settles in.
|
|
tl.fromTo(
|
|
actor,
|
|
{ opacity: 0, y: "2.6cqh" },
|
|
{ opacity: 1, y: 0, duration: 0.5, ease: "power3.out" },
|
|
0,
|
|
);
|
|
|
|
// Cursor decel-arrives from off-stage bottom-right. power2.out to
|
|
// an exact endpoint: it arrives, it never passes through.
|
|
tl.fromTo(
|
|
cursor,
|
|
{ x: xAt(114), y: yAt(112) },
|
|
{
|
|
x: xAt(landX),
|
|
y: yAt(landY),
|
|
duration: arriveEnd - arriveStart,
|
|
ease: "power2.out",
|
|
},
|
|
arriveStart,
|
|
);
|
|
|
|
// Press: target and cursor compress in lockstep (cursor about its
|
|
// tip), then release with a soft spring.
|
|
tl.to([actor, cursor], { scale: 0.93, duration: 0.12, ease: "power1.in" }, pressAt);
|
|
tl.set(slot, { attr: { "data-state": "pressed" } }, pressAt + 0.12);
|
|
tl.to(
|
|
[actor, cursor],
|
|
{ scale: 1, duration: 0.42, ease: "back.out(1.8)" },
|
|
pressAt + 0.14,
|
|
);
|
|
|
|
// Ink ripple rings from under the slot: real weight (thick ink,
|
|
// fg-mixed contrast, wide travel), still just two rings.
|
|
tl.fromTo(
|
|
rings,
|
|
{ scale: 0.16, autoAlpha: 0.95 },
|
|
{
|
|
scale: 2.6,
|
|
autoAlpha: 0,
|
|
duration: 0.66,
|
|
ease: "power2.out",
|
|
stagger: 0.11,
|
|
immediateRender: false,
|
|
},
|
|
pressAt + 0.1,
|
|
);
|
|
|
|
// The cursor exits off-stage after the release (accelerating,
|
|
// power2.in) so it never occludes the label during the hold; the
|
|
// pressed state and the ripple aftermath carry the frame. Both
|
|
// endpoints are explicit so reverse seeks restore the landing.
|
|
tl.fromTo(
|
|
cursor,
|
|
{ x: xAt(landX), y: yAt(landY) },
|
|
{
|
|
x: xAt(112),
|
|
y: yAt(110),
|
|
duration: 0.55,
|
|
ease: "power2.in",
|
|
immediateRender: false,
|
|
},
|
|
pressAt + 0.5,
|
|
);
|
|
|
|
// HOLD: dead-still on the pressed state; elastic with D.
|
|
|
|
// OUT: only when the host opts in; frame roots own transitions.
|
|
if (exit !== "none") {
|
|
var outStart = Math.max(pressAt + 1.05, duration - OUT);
|
|
var outVars = { opacity: 0, duration: OUT, ease: "power2.in" };
|
|
if (exit === "up") outVars.y = "-4cqh";
|
|
tl.to(stage, outVars, outStart);
|
|
}
|
|
|
|
tl.seek(0);
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["press-ripple"] = tl;
|
|
})();
|
|
</script>
|
|
</div>
|
|
</template>
|
|
</body>
|
|
</html>
|