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.
379 lines
17 KiB
HTML
Vendored
379 lines
17 KiB
HTML
Vendored
<!doctype html>
|
|
<!--
|
|
touch-indicator -- HyperFrames video primitive (pointers / interaction / demonstrate)
|
|
|
|
CONCEPT: a translucent contact-circle that represents a fingertip touching
|
|
glass. It never hovers: every appearance resolves into a touch-down, every
|
|
touch causes a same-frame response on its target, every gesture ends with
|
|
a physical lift. One mechanic (tap or swipe, picked by a variable at
|
|
setup), one file. This is the mobile counterpart to an oversized cursor,
|
|
but it obeys touch physics, not pointer physics.
|
|
|
|
MOUNTABLE SUB-COMPOSITION: this file is loaded by a host via
|
|
data-composition-src, not pasted inline. The runtime only clones
|
|
<template> contents (see skills/hyperframes-core/references/sub-compositions.md),
|
|
so every style, the markup, and the script that builds and registers the
|
|
timeline all live inside <template> below. The file is self-driving: it
|
|
builds its own paused GSAP timeline and registers it under
|
|
window.__timelines["touch-indicator"] on load, matching the root's own
|
|
data-composition-id. A host mounts it with a matching
|
|
data-composition-id="touch-indicator" data-composition-src="./touch-indicator.html".
|
|
|
|
COMPILED FROM: hyperframes-corpus-data/mobile/gesture-actor-spec.md (the
|
|
contact-never-hover law, contact-circle sizing, tap anatomy, swipe law,
|
|
lift/exit law, same-frame target-reaction contract) with supporting
|
|
constants cross-checked against gesture-physics-recipes.md. Evidence
|
|
status: spec ready, no rendered corpus clips yet.
|
|
|
|
USE WHEN: a mobile UI scene needs a visible tap or swipe to cause the next
|
|
animation beat (button press, list scroll, card swipe, sheet reveal).
|
|
Skip it for cursor/pointer UI (use a cursor primitive) and for gestures
|
|
whose drawn path is itself the subject (use a trail primitive instead).
|
|
|
|
VARIABLES (declared below via data-composition-variables):
|
|
- gesture enum "tap" | "swipe", default "tap"
|
|
- polarity enum "light" | "dark", default "light"
|
|
- targetX number 0-100 (percent of host width), default 50
|
|
- targetY number 0-100 (percent of host height), default 55
|
|
|
|
ENVELOPE (seconds, 60fps frame-quantized, gesture = "tap"):
|
|
IN 0.000 - 0.167 approach (0.083) + touch-down compress (0.083)
|
|
HOLD 0.167 - 0.183 elastic dwell at compressed scale (stretch here only)
|
|
OUT 0.183 - 0.467 release (0.167) + lift (0.117), lift starts the
|
|
instant release ends (0s gap, within the spec's
|
|
<=0.04s allowance)
|
|
|
|
ENVELOPE (seconds, 60fps frame-quantized, gesture = "swipe"):
|
|
IN 0.000 - 0.167 approach (0.083) + touch-down compress (0.083)
|
|
HOLD 0.167 - 0.500 elastic travel: accelerate 41% / decelerate 59% of
|
|
the stretchable swipe duration (default 0.333)
|
|
OUT 0.500 - 0.617 lift only (0.117); a swiping finger does not settle
|
|
back to rest scale, it leaves mid-motion
|
|
|
|
SOUND CUES (fixed offsets, never inside the elastic HOLD):
|
|
touch-down IN + 0.083s -> soft fingertip contact
|
|
lift-off tap: OUT + 0.167s -> physical release tick
|
|
swipe: OUT + 0s -> physical release tick (no bounce)
|
|
|
|
BUILT-IN RESPONSE: the primitive has to read standalone in a slot with no
|
|
host UI to react to it, so touch-down also drives its own contact-blur
|
|
ripple (EDIT ZONE: .hf-touch-indicator__contact, unchanged from the
|
|
original spec) plus a quiet radial pulse centered on the root
|
|
(.hf-touch-indicator__pulse, new). Both are token-painted and low-opacity
|
|
so they read as a same-frame acknowledgement, not a second effect
|
|
competing with a host's own reaction wiring.
|
|
-->
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[
|
|
{"id":"gesture","type":"enum","label":"Gesture","default":"tap","options":[{"value":"tap","label":"Tap"},{"value":"swipe","label":"Swipe"}]},
|
|
{"id":"polarity","type":"enum","label":"Polarity","default":"light","options":[{"value":"light","label":"Light (on dark UI)"},{"value":"dark","label":"Dark (on light UI)"}]},
|
|
{"id":"targetX","type":"number","label":"Target X","default":50,"min":0,"max":100,"unit":"%"},
|
|
{"id":"targetY","type":"number","label":"Target Y","default":55,"min":0,"max":100,"unit":"%"}
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Touch Indicator</title>
|
|
<!-- Head is metadata for the source file only; the mount runtime clones
|
|
only <template> contents and discards everything else, including
|
|
this head. See sub-compositions.md, "Pitfall 1".
|
|
|
|
The data-composition-variables declared on <html> above duplicates
|
|
the same schema on the #root div inside <template> below (the
|
|
"dual-carrier contract", core/src/runtime/compositionLoader.ts and
|
|
packages/parsers/src/htmlParser.ts both read declared defaults from
|
|
<html> only on the lazy external-load / CLI-metadata paths, while
|
|
the render-time bundler reads the template's root div). Keep both
|
|
copies in sync if the variable schema changes. -->
|
|
</head>
|
|
<body>
|
|
<template>
|
|
<style>
|
|
/* INVARIANT: root fills the host's box, position-blind, elastic to
|
|
whatever size a host slot gives it. No data-width/data-height on
|
|
the root -- the host slot owns the size (see mount contract).
|
|
Root is styled by #root, never a class (sub-compositions.md,
|
|
Pitfall 3). */
|
|
#root {
|
|
position: absolute;
|
|
inset: 0;
|
|
container-type: inline-size;
|
|
isolation: isolate;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hf-touch-indicator__pulse {
|
|
/* Root-level built-in response: a quiet radial flash centered on
|
|
the touch point, so the gesture reads even with no host UI to
|
|
react to it. Token-painted, low peak opacity -- see BUILT-IN
|
|
RESPONSE above. */
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: clamp(120px, 30cqw, 320px);
|
|
height: clamp(120px, 30cqw, 320px);
|
|
border-radius: 50%;
|
|
background: radial-gradient(
|
|
circle,
|
|
color-mix(in srgb, var(--fg, #14181f) 30%, transparent) 0%,
|
|
transparent 72%
|
|
);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
.hf-touch-indicator__actor {
|
|
/* EDIT ZONE: sizing law from the spec, full-frame law (contact
|
|
circle target 5cqw of the host box). RETIME RANGE: floor
|
|
clamped to 46px (the spec's absolute floor, never smaller at
|
|
delivery scale) since cqw now resolves against whatever box a
|
|
host slot gives this mountable primitive, which can be much
|
|
narrower than a full 1920-wide frame. */
|
|
--hf-gesture-size: clamp(46px, 5cqw, 111px);
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: var(--hf-gesture-size);
|
|
height: var(--hf-gesture-size);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
pointer-events: none;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
.hf-touch-indicator__core {
|
|
position: absolute;
|
|
inset: 0;
|
|
box-sizing: border-box;
|
|
/* INVARIANT: circle geometry, hardcoded 50%, not var(--radius) -
|
|
that token is for rectangular UI corners, not this actor's
|
|
fixed disc. */
|
|
border-radius: 50%;
|
|
background: color-mix(in srgb, var(--bg, #ffffff) 68%, transparent);
|
|
border: 2px solid color-mix(in srgb, var(--fg, #14181f) 72%, transparent);
|
|
box-shadow: 0 4px 12px color-mix(in srgb, var(--fg, #14181f) 22%, transparent);
|
|
transform-origin: 50% 50%;
|
|
will-change: transform;
|
|
}
|
|
|
|
.hf-touch-indicator__actor--dark .hf-touch-indicator__core {
|
|
background: color-mix(in srgb, var(--fg, #14181f) 64%, transparent);
|
|
border-color: color-mix(in srgb, var(--bg, #ffffff) 86%, transparent);
|
|
}
|
|
|
|
.hf-touch-indicator__contact {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 54%;
|
|
width: 62%;
|
|
height: 22%;
|
|
border-radius: 50%;
|
|
background: color-mix(in srgb, var(--fg, #14181f) 32%, transparent);
|
|
filter: blur(5px);
|
|
opacity: 0;
|
|
/* INVARIANT: centering (xPercent/yPercent -50) and scaleX are set
|
|
by the FIRST GSAP tween below, not here - GSAP overwrites the
|
|
whole transform, so a static translate()+scaleX() here would be
|
|
discarded the instant the timeline renders
|
|
(gsap_css_transform_conflict). */
|
|
transform-origin: 50% 50%;
|
|
will-change: transform, opacity;
|
|
}
|
|
</style>
|
|
|
|
<div
|
|
id="root"
|
|
data-composition-id="touch-indicator"
|
|
data-composition-variables='[
|
|
{"id":"gesture","type":"enum","label":"Gesture","default":"tap","options":[{"value":"tap","label":"Tap"},{"value":"swipe","label":"Swipe"}]},
|
|
{"id":"polarity","type":"enum","label":"Polarity","default":"light","options":[{"value":"light","label":"Light (on dark UI)"},{"value":"dark","label":"Dark (on light UI)"}]},
|
|
{"id":"targetX","type":"number","label":"Target X","default":50,"min":0,"max":100,"unit":"%"},
|
|
{"id":"targetY","type":"number","label":"Target Y","default":55,"min":0,"max":100,"unit":"%"}
|
|
]'
|
|
>
|
|
<div class="hf-touch-indicator__pulse"></div>
|
|
<div class="hf-touch-indicator__actor">
|
|
<div class="hf-touch-indicator__contact"></div>
|
|
<div class="hf-touch-indicator__core"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const FPS = 60;
|
|
const q = (s) => Math.max(1, Math.round(s * FPS)) / FPS;
|
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
|
|
// Coordinate law from the spec: the actor wrapper owns x/y
|
|
// (absolute position), the core owns press scale and the
|
|
// depth/lift offset, the contact shadow owns only its own opacity
|
|
// and scaleX, the root pulse owns only its own position/opacity/
|
|
// scale. Never tween two meanings on one transform channel.
|
|
function buildTouchIndicator(tl, root, { at = 0, elasticity = 1 } = {}) {
|
|
const defaults = { gesture: "tap", polarity: "light", targetX: 50, targetY: 55 };
|
|
// Per-instance overrides (a host's data-variable-values) land in
|
|
// window.__hfVariablesByComp, scoped by composition id. Reading
|
|
// window.__hfVariables directly here would silently ignore every
|
|
// host override -- window.__hyperframes.getVariables() is the
|
|
// scoped accessor, per the registry convention (see
|
|
// lower-third-bild.html, caption-texture.html).
|
|
const resolved =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: window.__hfVariables || {};
|
|
const vars = Object.assign({}, defaults, resolved);
|
|
const pulse = root.querySelector(".hf-touch-indicator__pulse");
|
|
const actor = root.querySelector(".hf-touch-indicator__actor");
|
|
const core = root.querySelector(".hf-touch-indicator__core");
|
|
const contact = root.querySelector(".hf-touch-indicator__contact");
|
|
if (vars.polarity === "dark") actor.classList.add("hf-touch-indicator__actor--dark");
|
|
|
|
// Compute layout geometry once at setup, never inside an ease/callback.
|
|
const hostW = root.clientWidth || 1;
|
|
const hostH = root.clientHeight || 1;
|
|
const x0 = (vars.targetX / 100) * hostW;
|
|
const y0 = (vars.targetY / 100) * hostH;
|
|
tl.set(pulse, { x: x0, y: y0, xPercent: -50, yPercent: -50 }, at);
|
|
|
|
// --- IN: fixed, never stretched. ---
|
|
const approach = q(0.09);
|
|
const down = q(0.08);
|
|
tl.fromTo(
|
|
actor,
|
|
{ x: x0, y: y0, xPercent: -50, yPercent: -50, autoAlpha: 0 },
|
|
{
|
|
x: x0,
|
|
y: y0,
|
|
xPercent: -50,
|
|
yPercent: -50,
|
|
autoAlpha: 1,
|
|
duration: approach,
|
|
ease: "power2.out",
|
|
immediateRender: false,
|
|
},
|
|
at,
|
|
);
|
|
tl.fromTo(
|
|
core,
|
|
{ y: -10, scale: 0.72 },
|
|
{ y: 0, scale: 1, duration: approach, ease: "power2.out", immediateRender: false },
|
|
at,
|
|
);
|
|
|
|
const touchDown = at + approach; // sync point: fixed +0.083s into IN
|
|
const coreDownScale = vars.gesture === "swipe" ? 0.86 : 0.82;
|
|
tl.fromTo(
|
|
core,
|
|
{ scale: 1 },
|
|
{ scale: coreDownScale, duration: down, ease: "power2.in", immediateRender: false },
|
|
touchDown,
|
|
);
|
|
tl.fromTo(
|
|
contact,
|
|
{ autoAlpha: 0, scaleX: 0.62, xPercent: -50, yPercent: -50 },
|
|
{
|
|
autoAlpha: 0.34,
|
|
scaleX: 1,
|
|
xPercent: -50,
|
|
yPercent: -50,
|
|
duration: down,
|
|
ease: "power2.in",
|
|
immediateRender: false,
|
|
},
|
|
touchDown,
|
|
);
|
|
// Built-in root pulse: same-frame acknowledgement so the
|
|
// gesture reads even with no host UI wired to react to it.
|
|
tl.fromTo(
|
|
pulse,
|
|
{ autoAlpha: 0, scale: 0.4 },
|
|
{
|
|
autoAlpha: 0.35,
|
|
scale: 1,
|
|
duration: down,
|
|
ease: "power2.out",
|
|
immediateRender: false,
|
|
},
|
|
touchDown,
|
|
);
|
|
tl.to(
|
|
pulse,
|
|
{ autoAlpha: 0, scale: 1.4, duration: q(0.24), ease: "power2.out" },
|
|
touchDown + down,
|
|
);
|
|
|
|
const inEnd = at + approach + down;
|
|
|
|
// --- HOLD: the only elastic phase. Stretch it, never gsap.timeScale(). ---
|
|
let holdEnd;
|
|
if (vars.gesture === "swipe") {
|
|
// RETIME RANGE: 0.28s - 0.52s per the swipe law. elasticity scales pacing.
|
|
const swipeDuration = q(clamp(0.34 * elasticity, 0.28, 0.52));
|
|
const travel = hostH * 0.4; // vertical scroll, 40% of host height (32-68% allowed)
|
|
const accel = q(swipeDuration * 0.41);
|
|
const decel = swipeDuration - accel;
|
|
const bendY = y0 - travel * 0.41;
|
|
const endY = y0 - travel;
|
|
tl.fromTo(
|
|
actor,
|
|
{ x: x0, y: y0 },
|
|
{ x: x0, y: bendY, duration: accel, ease: "power2.in", immediateRender: false },
|
|
inEnd,
|
|
);
|
|
tl.to(actor, { x: x0, y: endY, duration: decel, ease: "power3.out" }, inEnd + accel);
|
|
holdEnd = inEnd + swipeDuration;
|
|
} else {
|
|
// RETIME RANGE: 0.02s - 0.10s. A brief contact dwell, never a
|
|
// long-press (long-press is a distinct gesture with its own
|
|
// progress-ring recognizer).
|
|
const holdDuration = q(clamp(0.02 * elasticity, 0.02, 0.1));
|
|
holdEnd = inEnd + holdDuration;
|
|
}
|
|
|
|
// --- OUT: fixed, never stretched. ---
|
|
if (vars.gesture === "swipe") {
|
|
const liftDuration = q(0.12);
|
|
tl.to(contact, { autoAlpha: 0, duration: q(0.08), ease: "power2.out" }, holdEnd);
|
|
tl.to(
|
|
core,
|
|
{ y: -10, scale: 0.74, duration: liftDuration, ease: "power2.in" },
|
|
holdEnd,
|
|
);
|
|
tl.to(actor, { autoAlpha: 0, duration: liftDuration, ease: "power2.in" }, holdEnd);
|
|
return { touchDown, holdStart: inEnd, holdEnd, settle: holdEnd + liftDuration };
|
|
}
|
|
|
|
const release = q(0.16);
|
|
tl.to(core, { scale: 1, duration: release, ease: "power2.out" }, holdEnd);
|
|
tl.to(
|
|
contact,
|
|
{ autoAlpha: 0, scaleX: 0.72, duration: q(0.12), ease: "power2.out" },
|
|
holdEnd,
|
|
);
|
|
const liftStart = holdEnd + release; // 0s gap, within the spec's <=0.04s allowance
|
|
const liftDuration = q(0.12);
|
|
tl.to(
|
|
core,
|
|
{ y: -10, scale: 0.74, duration: liftDuration, ease: "power2.in" },
|
|
liftStart,
|
|
);
|
|
tl.to(actor, { autoAlpha: 0, duration: liftDuration, ease: "power2.in" }, liftStart);
|
|
return { touchDown, holdStart: inEnd, holdEnd, settle: liftStart + liftDuration };
|
|
}
|
|
|
|
// Self-driving mount: build the paused timeline and register it
|
|
// under this file's own data-composition-id, matching the host's
|
|
// data-composition-src wiring (sub-compositions.md, Pitfall 2).
|
|
const root = document.querySelector('[data-composition-id="touch-indicator"]');
|
|
const tl = gsap.timeline({ paused: true });
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["touch-indicator"] = tl;
|
|
buildTouchIndicator(tl, root, { at: 0 });
|
|
})();
|
|
</script>
|
|
</template>
|
|
</body>
|
|
</html>
|