mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
# radial-surround
|
||||
|
||||
Labeled hairline chips assemble around a centered subject on an elliptical
|
||||
ring: positions baked once via cos/sin, staggered smooth-settle entries. With
|
||||
`close_in` (the default) the chips then converge inward toward the subject
|
||||
while a subtle edge vignette dims the stage. The center NEVER moves: the
|
||||
subject is surrounded, not zoomed. The classic problem-setup beat ("your team,
|
||||
buried under tools").
|
||||
|
||||
## Files
|
||||
|
||||
- `radial-surround.html`: the mountable sub-composition (install target:
|
||||
`compositions/components/radial-surround.html`).
|
||||
- `registry-item.json`: registry metadata and the variables block.
|
||||
- `demo.html`: a 1920x1080 host that mounts the primitive with non-default
|
||||
variables through `data-composition-src`.
|
||||
|
||||
## Variables
|
||||
|
||||
| id | type | default | notes |
|
||||
| -------------- | ------- | ------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `center_label` | string | `Your team` | label on the default center token card |
|
||||
| `chips` | string | `Docs,Tickets,Dashboards,Inbox,Chat,Sheets` | comma list of chip labels; up to 12 render |
|
||||
| `close_in` | boolean | `true` | converge the ring inward with the edge dim; `false` holds the wide ring |
|
||||
| `cues` | string | `""` | comma-separated per-chip entrance seconds; blank entries use the cascade |
|
||||
| `accent` | enum | `green` | `green` maps to `--brand`, `blue` to `--accent`, `violet` to `--accent-2` |
|
||||
| `exit` | enum | `none` | `none` holds the final frame; `fade` and `up` release the stage |
|
||||
|
||||
Envelope: IN = center settle + chip cascade (+ close-in when enabled; the
|
||||
converge lands at 4.50s at defaults), HOLD is the sole elastic phase (dead
|
||||
still), OUT = up to 0.45s only when `exit` is `fade` or `up`. Sync points:
|
||||
`ring-assembled` at 2.95s, `surround-closed` at 4.50s (defaults). Cue values
|
||||
are clamped so every chip lands before the close-in (or the exit) begins.
|
||||
|
||||
## The slot mechanism
|
||||
|
||||
The primitive ships one named slot for the center subject inside its
|
||||
`<template>`:
|
||||
|
||||
```html
|
||||
<div class="rs-center" data-slot="center">...</div>
|
||||
```
|
||||
|
||||
The slot is empty by default: the script generates a token card carrying
|
||||
`center_label`, so an untouched mount still reads as a tasteful setup. To
|
||||
show your own subject, install the component and **place children inside the
|
||||
`[data-slot="center"]` element in your installed copy** (the runtime clones
|
||||
only the primitive's own template on mount, so slot content lives in the
|
||||
component file, not on the host clip). When the slot already has children the
|
||||
default card is skipped; chips, cues, `close_in`, and `exit` still apply.
|
||||
|
||||
Rules:
|
||||
|
||||
- Direct `img`/`video` children are capped at 34cqw x 30cqh with
|
||||
`object-fit: cover` and the contract radius. Arbitrary HTML works too; size
|
||||
it in `cqw`/`cqh` and keep it inside roughly 34cqw x 30cqh so the close-in
|
||||
ring (20cqw x 23cqh radii) clears it.
|
||||
- The center only fades and settles on entrance; it never translates. Do not
|
||||
animate the slot content yourself: the surrounding choreography assumes a
|
||||
still subject.
|
||||
- Leave the chips and vignette alone; they render above the center layer.
|
||||
|
||||
## Worked example
|
||||
|
||||
Install, then point the center at a product shot:
|
||||
|
||||
```bash
|
||||
npx hyperframes add radial-surround
|
||||
```
|
||||
|
||||
In `compositions/components/radial-surround.html`, fill the slot:
|
||||
|
||||
```html
|
||||
<div class="rs-center" data-slot="center">
|
||||
<img src="../../assets/team-photo.png" alt="" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Mount it from a host composition like any sub-composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
class="clip"
|
||||
data-composition-id="radial-surround"
|
||||
data-composition-src="./components/radial-surround.html"
|
||||
data-variable-values='{"chips":"Slack,Email,CRM,Tickets,Docs","close_in":true,"accent":"blue"}'
|
||||
data-start="2"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
```
|
||||
|
||||
Chips pop in around the photo on alternating compass points, close in at
|
||||
~3.3s, and the crowded frame holds still for the rest of the clip window
|
||||
(`exit` defaults to `none`; frame roots own scene transitions).
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
radial-surround demo. This standalone composition mounts the primitive
|
||||
through data-composition-src. Non-default center label, chips, cues, and
|
||||
accent prove the variable path; close_in stays at its default (true) so the
|
||||
signature converge plus edge dim is what QA surfaces render.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Radial Surround Demo</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg, #0b0c0e);
|
||||
}
|
||||
|
||||
#root {
|
||||
--bg: #0b0c0e;
|
||||
--fg: #f8fafc;
|
||||
--surface: #14171c;
|
||||
--border: #343a46;
|
||||
--brand: #71f5a7;
|
||||
--accent: #61a8ff;
|
||||
--accent-2: #c5a3ff;
|
||||
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(
|
||||
ellipse at 50% 44%,
|
||||
var(--bg, #171a20) 0%,
|
||||
var(--bg, #0b0c0e) 72%
|
||||
);
|
||||
}
|
||||
|
||||
.mount-card {
|
||||
width: 88%;
|
||||
height: 86%;
|
||||
left: 6%;
|
||||
top: 7%;
|
||||
container-type: size;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--border, #94a3b8) 20%, transparent);
|
||||
border-radius: 30px;
|
||||
background: color-mix(in srgb, var(--surface, #101318) 60%, transparent);
|
||||
box-shadow: 0 40px 100px color-mix(in srgb, var(--bg, #000000) 48%, transparent);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="radial-surround-demo"
|
||||
data-start="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
data-fps="30"
|
||||
>
|
||||
<div
|
||||
id="radial-surround-mount"
|
||||
class="mount-card clip"
|
||||
data-composition-id="radial-surround"
|
||||
data-composition-src="./radial-surround.html"
|
||||
data-variable-values='{"center_label":"One workspace","chips":"Slack,Email,CRM,Tickets,Docs,Sheets","cues":"0.8,1.05,1.3,1.55,1.8,2.05","accent":"blue"}'
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
data-width="1690"
|
||||
data-height="929"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["radial-surround-demo"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,432 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
radial-surround: HyperFrames video primitive (problem setup)
|
||||
|
||||
Labeled hairline chips assemble around a centered subject on an elliptical
|
||||
ring. Positions are baked exactly once via cos/sin before the timeline is
|
||||
registered, so every seek lands on the same geometry. Chips enter with
|
||||
staggered smooth settles (scale + fade, power3.out, no overshoot). With
|
||||
close_in enabled the chips then converge inward toward the subject while a
|
||||
subtle edge vignette dims the stage: the center NEVER moves. The subject is
|
||||
surrounded, not zoomed.
|
||||
|
||||
Variables (declared in data-composition-variables below):
|
||||
- center_label (string, default "Your team"): label on the default token
|
||||
card. Ignored for layout when the center slot carries custom children.
|
||||
- chips (string, default "Docs,Tickets,Dashboards,Inbox,Chat,Sheets"):
|
||||
comma list of chip labels; up to 12 render, extras are dropped.
|
||||
- close_in (boolean, default true): converge the ring inward with the
|
||||
edge dim after assembly. false holds the wide ring instead.
|
||||
- cues (string, default ""): comma-separated per-chip entrance times in
|
||||
seconds relative to mount start. Blank or invalid entries fall back to
|
||||
that chip's default cascade slot (~0.30s gap). Values are clamped so
|
||||
every chip lands before the close-in (or the exit) begins.
|
||||
- accent (green | blue | violet, default green): center card tint.
|
||||
green routes to --brand, blue to --accent, violet to --accent-2.
|
||||
- exit (none | fade | up, default none): frame roots own transitions;
|
||||
the default crowded hold ends the film.
|
||||
|
||||
Envelope, fixed IN with elastic HOLD (never gsap.timeScale()):
|
||||
IN = center settle + chip cascade (+ close-in and vignette when
|
||||
close_in is true); at defaults the converge lands at 4.50s.
|
||||
HOLD = dead still, elastic: everything after IN until OUT (or D).
|
||||
OUT = 0 when exit is "none"; else up to 0.45s fade / fade-up.
|
||||
Entrance starts compress together when D is too short for the cascade;
|
||||
the close-in window is reserved first so the converge always completes.
|
||||
|
||||
Slot: the runtime clones only this template. A fork that wants a custom
|
||||
subject places its own children inside the [data-slot="center"] element;
|
||||
when that element already has children the default token card is not
|
||||
generated (chips, cues, close_in, and exit still apply, and the center
|
||||
still never moves).
|
||||
|
||||
Mount contract: elastic root, no data-width/data-height, styled via #root
|
||||
only, container-type: size, one paused timeline registered under the
|
||||
literal "radial-surround" key. Deterministic and seek-safe: fromTo
|
||||
entrances, no CSS transitions, no Date.now, no randomness.
|
||||
-->
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-id="radial-surround"
|
||||
data-composition-duration="5"
|
||||
data-composition-variables='[
|
||||
{ "id": "center_label", "type": "string", "role": "content", "label": "Center label", "description": "Label on the default center token card.", "default": "Your team" },
|
||||
{ "id": "chips", "type": "string", "role": "content", "label": "Chips", "description": "Comma-separated chip labels that surround the center (up to 12).", "default": "Docs,Tickets,Dashboards,Inbox,Chat,Sheets" },
|
||||
{ "id": "close_in", "type": "boolean", "role": "timing", "label": "Close in", "description": "Converge the ring inward with a subtle edge dim after assembly. The center never moves.", "default": true },
|
||||
{ "id": "cues", "type": "string", "role": "timing", "label": "Cues", "description": "Comma-separated per-chip entrance times in seconds. Blank entries use the default cascade.", "default": "" },
|
||||
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Accent token family used by the center card tint.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
|
||||
{ "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "How the surrounded stage leaves the frame. Default none: the crowded hold ends the film.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Radial Surround</title>
|
||||
</head>
|
||||
<body>
|
||||
<template>
|
||||
<div id="root" data-composition-id="radial-surround" data-duration="5" data-fps="30">
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
container-type: size;
|
||||
isolation: isolate;
|
||||
background: var(--bg, transparent);
|
||||
color: var(--fg, #f8fafc);
|
||||
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rs-clip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rs-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.rs-center-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.rs-center {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.rs-center > img,
|
||||
.rs-center > video {
|
||||
max-width: 34cqw;
|
||||
max-height: 30cqh;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius, 2.2cqmin);
|
||||
}
|
||||
|
||||
.rs-card {
|
||||
width: 28cqw;
|
||||
min-height: 22cqh;
|
||||
padding: var(--space-2, 2.2cqmin);
|
||||
border: 0.16cqmin solid color-mix(in srgb, var(--rs-accent) 46%, var(--border, #475569));
|
||||
border-radius: var(--radius, 2.2cqmin);
|
||||
background: color-mix(in srgb, var(--surface, #131a24) 94%, var(--rs-accent));
|
||||
box-shadow: 0 3cqh 9cqh color-mix(in srgb, var(--bg, #000000) 40%, transparent);
|
||||
}
|
||||
|
||||
.rs-card-label {
|
||||
overflow: hidden;
|
||||
color: var(--fg, #f8fafc);
|
||||
font-family: var(
|
||||
--font-display,
|
||||
var(--font-body, ui-sans-serif, system-ui, sans-serif)
|
||||
);
|
||||
font-size: var(--rs-center-font, 4cqmin);
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 0.04em;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rs-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* Positioning wrapper: the baked ring offsets live in the --rs-ox
|
||||
and --rs-oy custom properties (plain cq values) and the close-in
|
||||
tweens THOSE, never x/y or a GSAP-written calc() string. GSAP
|
||||
misconverts vertical cq units to px mid-tween when it has to
|
||||
re-init a layout property under a seek, so the calc stays in the
|
||||
stylesheet and only the var strings interpolate; the inner
|
||||
chip's unitless scale and opacity are the only transforms. */
|
||||
.rs-pos {
|
||||
position: absolute;
|
||||
top: calc(50% + var(--rs-oy, 0cqh));
|
||||
left: calc(50% + var(--rs-ox, 0cqw));
|
||||
width: max-content;
|
||||
transform: translate(-50%, -50%);
|
||||
will-change: top, left;
|
||||
}
|
||||
|
||||
.rs-chip {
|
||||
padding: 1.15cqmin 2.1cqmin;
|
||||
border: 0.14cqmin solid var(--border, #475569);
|
||||
border-radius: 0.6cqmin;
|
||||
background: var(--surface, #131a24);
|
||||
color: var(--fg, #f8fafc);
|
||||
font-size: 2.1cqmin;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Close-in edge dim: very subtle, content stays legible. */
|
||||
.rs-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 60%,
|
||||
color-mix(in srgb, var(--bg, #05070c) 34%, transparent) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="radial-surround-clip"
|
||||
class="rs-clip clip"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div class="rs-stage" role="img">
|
||||
<div class="rs-center-layer">
|
||||
<div class="rs-center" data-slot="center"></div>
|
||||
</div>
|
||||
<div class="rs-ring" aria-hidden="true"></div>
|
||||
<div class="rs-vignette"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Literal id, never read off the DOM: the flattening step strips
|
||||
// data-composition-id from the mounted root (see toggle-flip).
|
||||
var compositionId = "radial-surround";
|
||||
var root = document.getElementById("root");
|
||||
var stage = root.querySelector(".rs-stage");
|
||||
var center = root.querySelector('[data-slot="center"]');
|
||||
var ring = root.querySelector(".rs-ring");
|
||||
var vignette = root.querySelector(".rs-vignette");
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
|
||||
var centerLabel =
|
||||
vars.center_label == null || String(vars.center_label).trim() === ""
|
||||
? "Your team"
|
||||
: String(vars.center_label).trim();
|
||||
var chipsValue =
|
||||
vars.chips == null || String(vars.chips).trim() === ""
|
||||
? "Docs,Tickets,Dashboards,Inbox,Chat,Sheets"
|
||||
: String(vars.chips);
|
||||
var labels = chipsValue
|
||||
.split(",")
|
||||
.map(function (token) {
|
||||
return token.trim();
|
||||
})
|
||||
.filter(function (token) {
|
||||
return token !== "";
|
||||
})
|
||||
.slice(0, 12);
|
||||
if (labels.length === 0)
|
||||
labels = ["Docs", "Tickets", "Dashboards", "Inbox", "Chat", "Sheets"];
|
||||
|
||||
var closeIn = !(vars.close_in === false || vars.close_in === "false");
|
||||
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
|
||||
|
||||
// Each enum choice routes to a DIFFERENT contract token so the
|
||||
// variable stays meaningful under a theme.
|
||||
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";
|
||||
root.style.setProperty("--rs-accent", accentColors[accent]);
|
||||
|
||||
// Slot: a fork may author its own subject inside
|
||||
// [data-slot="center"]; then the default token card is skipped.
|
||||
if (center.children.length === 0) {
|
||||
center.classList.add("rs-card");
|
||||
var label = document.createElement("span");
|
||||
label.className = "rs-card-label";
|
||||
label.textContent = centerLabel;
|
||||
center.appendChild(label);
|
||||
}
|
||||
stage.setAttribute("aria-label", centerLabel + ": " + labels.join(", "));
|
||||
|
||||
// Deterministic center-label fitting inside the 28cqw card.
|
||||
var chars = Array.from(centerLabel).length;
|
||||
var fontCqw = Math.min(3.4, 23 / (Math.max(1, chars) * 0.62));
|
||||
root.style.setProperty("--rs-center-font", "min(" + fontCqw.toFixed(3) + "cqw, 6cqh)");
|
||||
|
||||
/* Elliptical ring, baked once (deterministic). FAR is the wide
|
||||
assembly ring; NEAR is the close-in ring, pressing corner chips
|
||||
up against the 28cqw center card so the crowding reads (chips
|
||||
paint above the card). Angles alternate sides from the top so
|
||||
consecutive entries pop on opposite compass points. */
|
||||
var FAR_RX = 30; /* cqw */
|
||||
var FAR_RY = 31; /* cqh */
|
||||
var NEAR_RX = 20;
|
||||
var NEAR_RY = 23;
|
||||
var count = labels.length;
|
||||
var step = 360 / count;
|
||||
function ringPosition(index, rx, ry) {
|
||||
var degrees = -90 + Math.ceil(index / 2) * step * (index % 2 === 1 ? 1 : -1);
|
||||
var a = (degrees * Math.PI) / 180;
|
||||
return {
|
||||
ox: (Math.cos(a) * rx).toFixed(3) + "cqw",
|
||||
oy: (Math.sin(a) * ry).toFixed(3) + "cqh",
|
||||
};
|
||||
}
|
||||
var positions = [];
|
||||
var chips = labels.map(function (text, index) {
|
||||
var pos = document.createElement("div");
|
||||
pos.className = "rs-pos";
|
||||
var far = ringPosition(index, FAR_RX, FAR_RY);
|
||||
pos.style.setProperty("--rs-ox", far.ox);
|
||||
pos.style.setProperty("--rs-oy", far.oy);
|
||||
var chip = document.createElement("div");
|
||||
chip.className = "rs-chip";
|
||||
chip.textContent = text;
|
||||
pos.appendChild(chip);
|
||||
ring.appendChild(pos);
|
||||
positions.push(pos);
|
||||
return chip;
|
||||
});
|
||||
|
||||
// RETIME RANGE: fixed IN cascade (+ close-in), elastic still HOLD,
|
||||
// optional OUT. The close-in window is reserved off the top so the
|
||||
// converge always completes inside the clip.
|
||||
var CENTER_AT = 0.05;
|
||||
var BASE_START = 0.9;
|
||||
var GAP = 0.3;
|
||||
var ENTRY = 0.55;
|
||||
var CLOSE_DUR = 0.75;
|
||||
var CLOSE_STAG = 0.09;
|
||||
var BREATH = 0.35;
|
||||
var OUT_BASE = 0.45;
|
||||
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "5"));
|
||||
var OUT = exit === "none" ? 0 : Math.min(OUT_BASE, duration * 0.2);
|
||||
var avail = duration - OUT;
|
||||
var closeSpan = closeIn ? (count - 1) * CLOSE_STAG + CLOSE_DUR : 0;
|
||||
var latestStart = Math.max(0, avail - ENTRY - closeSpan);
|
||||
// Compress the cascade together when the duration cannot fit it.
|
||||
var gap =
|
||||
count > 1 ? Math.min(GAP, Math.max(0, latestStart - BASE_START) / (count - 1)) : GAP;
|
||||
|
||||
var cueList = String(vars.cues == null ? "" : vars.cues).split(",");
|
||||
var starts = chips.map(function (chip, index) {
|
||||
var cue = Number((cueList[index] || "").trim());
|
||||
var hasCue = (cueList[index] || "").trim() !== "" && Number.isFinite(cue);
|
||||
var start = hasCue ? cue : Math.min(BASE_START, latestStart) + gap * index;
|
||||
return Math.max(0, Math.min(start, latestStart));
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Center seats first: gentle fade + subtle scale settle. After
|
||||
// this the center NEVER moves again (surrounded, not zoomed).
|
||||
gsap.set(center, { opacity: 0, scale: 0.94 });
|
||||
tl.fromTo(
|
||||
center,
|
||||
{ opacity: 0, scale: 0.94 },
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: Math.min(0.7, Math.max(0.2, avail * 0.25)),
|
||||
ease: "power3.out",
|
||||
},
|
||||
Math.min(CENTER_AT, latestStart),
|
||||
);
|
||||
|
||||
// IN: chips assemble on the FAR ring in a measured stagger.
|
||||
// Explicit both-endpoints state so any seek is exact.
|
||||
chips.forEach(function (chip, index) {
|
||||
gsap.set(chip, { scale: 0, opacity: 0 });
|
||||
tl.fromTo(
|
||||
chip,
|
||||
{ scale: 0, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: ENTRY, ease: "power3.out" },
|
||||
starts[index],
|
||||
);
|
||||
});
|
||||
|
||||
// Close-in: chips converge from all compass points toward the
|
||||
// subject while the edges dim slightly. The center holds still.
|
||||
if (closeIn) {
|
||||
var maxEntryEnd =
|
||||
starts.reduce(function (max, start) {
|
||||
return Math.max(max, start);
|
||||
}, 0) + ENTRY;
|
||||
var closeStart = Math.max(
|
||||
maxEntryEnd,
|
||||
Math.min(maxEntryEnd + BREATH, avail - closeSpan),
|
||||
);
|
||||
positions.forEach(function (pos, index) {
|
||||
var far = ringPosition(index, FAR_RX, FAR_RY);
|
||||
var near = ringPosition(index, NEAR_RX, NEAR_RY);
|
||||
tl.fromTo(
|
||||
pos,
|
||||
{ "--rs-ox": far.ox, "--rs-oy": far.oy },
|
||||
{
|
||||
"--rs-ox": near.ox,
|
||||
"--rs-oy": near.oy,
|
||||
duration: CLOSE_DUR,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
closeStart + index * CLOSE_STAG,
|
||||
);
|
||||
});
|
||||
gsap.set(vignette, { opacity: 0 });
|
||||
tl.fromTo(
|
||||
vignette,
|
||||
{ opacity: 0 },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: Math.min(0.9, Math.max(0.2, closeSpan)),
|
||||
ease: "power1.inOut",
|
||||
},
|
||||
closeStart + 0.05,
|
||||
);
|
||||
}
|
||||
|
||||
// HOLD: intentionally still. Crowded, quiet, nothing floats.
|
||||
|
||||
// OUT: the whole surrounded stage leaves together.
|
||||
if (exit !== "none") {
|
||||
var OUT_START = duration - OUT;
|
||||
tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
|
||||
if (exit === "up") {
|
||||
// yPercent, not a cq-unit y: percent transforms seek exactly.
|
||||
tl.to(stage, { yPercent: -4, duration: OUT, ease: "power2.in" }, OUT_START);
|
||||
}
|
||||
}
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines[compositionId] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "radial-surround",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Radial Surround",
|
||||
"description": "Labeled hairline chips assemble around a centered subject on an elliptical ring, then optionally converge inward with a subtle edge dim. The center never moves: surrounded, not zoomed.",
|
||||
"tags": ["motion-primitive", "problem-setup", "radial", "ring", "surround", "converge"],
|
||||
"jobs": ["agitate"],
|
||||
"family": "layout",
|
||||
"profile": "surround",
|
||||
"evidence": "Wave J J2-6: flagship frame 02-problem elliptical ring math (FAR/NEAR radii, per-chip cos/sin angles) generalized to a slotted primitive",
|
||||
"syncPoints": [
|
||||
{ "id": "ring-assembled", "phase": "in", "offset": 2.95 },
|
||||
{ "id": "surround-closed", "phase": "in", "offset": 4.5 }
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "radial-surround.html",
|
||||
"target": "compositions/components/radial-surround.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"id": "center_label",
|
||||
"type": "string",
|
||||
"role": "content",
|
||||
"label": "Center label",
|
||||
"description": "Label on the default center token card.",
|
||||
"default": "Your team"
|
||||
},
|
||||
{
|
||||
"id": "chips",
|
||||
"type": "string",
|
||||
"role": "content",
|
||||
"label": "Chips",
|
||||
"description": "Comma-separated chip labels that surround the center (up to 12).",
|
||||
"default": "Docs,Tickets,Dashboards,Inbox,Chat,Sheets"
|
||||
},
|
||||
{
|
||||
"id": "close_in",
|
||||
"type": "boolean",
|
||||
"role": "timing",
|
||||
"label": "Close in",
|
||||
"description": "Converge the ring inward with a subtle edge dim after assembly. The center never moves.",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "cues",
|
||||
"type": "string",
|
||||
"role": "timing",
|
||||
"label": "Cues",
|
||||
"description": "Comma-separated per-chip entrance times in seconds. Blank entries use the default cascade.",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"id": "accent",
|
||||
"type": "enum",
|
||||
"role": "style",
|
||||
"label": "Accent",
|
||||
"description": "Accent token family used by the center card tint.",
|
||||
"default": "green",
|
||||
"options": [
|
||||
{ "value": "green", "label": "Green" },
|
||||
{ "value": "blue", "label": "Blue" },
|
||||
{ "value": "violet", "label": "Violet" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "exit",
|
||||
"type": "enum",
|
||||
"role": "timing",
|
||||
"label": "Exit",
|
||||
"description": "How the surrounded stage leaves the frame. Default none: the crowded hold ends the film.",
|
||||
"default": "none",
|
||||
"options": [
|
||||
{ "value": "none", "label": "None" },
|
||||
{ "value": "fade", "label": "Fade" },
|
||||
{ "value": "up", "label": "Up" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user