mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -1,105 +0,0 @@
|
||||
# spring-stack-shuffle
|
||||
|
||||
A stack of 3 to 5 cards reshuffles on cues: on each cue the card at the back
|
||||
throws over the top of the stack and lands at the front with real mass (one
|
||||
small sanctioned landing overshoot, quiet register), while the other cards
|
||||
step one slot deeper and take a brief compress-resettle as the flyer lands.
|
||||
An accent ring rides the active (front) card across every shuffle.
|
||||
|
||||
The signature is law L1, interruptible springs: when a cue fires while a
|
||||
previous shuffle is still in flight, every moving card redirects to its new
|
||||
slot from its current position at its current velocity. No snap-to-zero, no
|
||||
pop. The default cue rhythm (`0.9, 1.7, 1.95`) fires its third cue mid-throw
|
||||
of the second on purpose, so the redirect is part of the authored default
|
||||
choreography.
|
||||
|
||||
The redirect is not a physics simulation: motion compiles at mount into
|
||||
per-card segment lists (explicit endpoints, pure ease functions). A cue that
|
||||
interrupts a segment cuts it analytically (value from the ease curve,
|
||||
velocity from a fixed finite difference) and continues with a Hermite ease
|
||||
whose start slope equals the cut velocity. Every emitted tween is a `fromTo`
|
||||
with both endpoints authored, so seeks in either direction always reproduce
|
||||
the same frame.
|
||||
|
||||
## Files
|
||||
|
||||
- `spring-stack-shuffle.html`: the mountable sub-composition (install target:
|
||||
`compositions/components/spring-stack-shuffle.html`).
|
||||
- `registry-item.json`: registry metadata and the variables block.
|
||||
- `demo.html`: standalone 1920x1080 host mounting the primitive with
|
||||
non-default variables.
|
||||
|
||||
## Variables
|
||||
|
||||
| id | type | default | notes |
|
||||
| -------- | ------ | ---------------- | ------------------------------------------------------------------------- |
|
||||
| `cards` | number | `4` | stack size, clamped to 3..5 |
|
||||
| `cues` | string | `0.9, 1.7, 1.95` | comma seconds from mount start, one shuffle per cue; empty keeps default |
|
||||
| `accent` | enum | `green` | `green` maps to `--brand`, `blue` to `--accent`, `violet` to `--accent-2` |
|
||||
| `exit` | enum | `none` | `none` holds the settled stack; `fade` and `up` release the stage |
|
||||
|
||||
Cues are clamped into `[0.85, D - exit - 0.9]` so every throw lands before an
|
||||
exit begins. Cues closer together than a throw (0.85s) interrupt the flight;
|
||||
that is the point, not an error. Envelope: staggered entrances finish by
|
||||
~0.8s, HOLD is the only elastic phase, OUT is 0.45s only when `exit` is
|
||||
`fade` or `up`.
|
||||
|
||||
## The slot mechanism
|
||||
|
||||
The primitive ships five named slot panels inside its `<template>`, one per
|
||||
card, front to back at mount:
|
||||
|
||||
```html
|
||||
<div class="sss-slot" data-slot="card-1">...</div>
|
||||
...
|
||||
<div class="sss-slot" data-slot="card-5">...</div>
|
||||
```
|
||||
|
||||
Each slot's default children are a numbered token skeleton card, so an
|
||||
untouched mount still reads as a tasteful shuffle. To show your own content,
|
||||
install the component and **replace the children of each `[data-slot]`
|
||||
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). Slots beyond the `cards` count are removed at mount.
|
||||
|
||||
Rules:
|
||||
|
||||
- Direct `img`/`video` children are automatically sized to cover the card
|
||||
(`object-fit: cover`). Arbitrary HTML works too; size it in `cqw`/`cqh`.
|
||||
- Leave the `.sss-veil` and `.sss-ring` siblings alone; they are the
|
||||
timeline-driven depth dim and active ring.
|
||||
|
||||
## Worked example
|
||||
|
||||
Install, then fill the slots with three screens and shuffle between them:
|
||||
|
||||
```bash
|
||||
npx hyperframes add spring-stack-shuffle
|
||||
```
|
||||
|
||||
In `compositions/components/spring-stack-shuffle.html`, replace each slot's
|
||||
default block:
|
||||
|
||||
```html
|
||||
<div class="sss-slot" data-slot="card-1">
|
||||
<img src="../../assets/screen-dashboard.png" alt="" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Mount it from a host composition like any sub-composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
class="clip"
|
||||
data-composition-id="spring-stack-shuffle"
|
||||
data-composition-src="./components/spring-stack-shuffle.html"
|
||||
data-variable-values='{"cards":3,"cues":"1.0, 1.9","accent":"blue"}'
|
||||
data-start="2"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
```
|
||||
|
||||
Two shuffles fire at 1.0s and 1.9s after mount; the second lands just as the
|
||||
first settles. Bring the cues within 0.85s of each other to see the L1
|
||||
redirect: the in-flight card bends to its new slot without ever stopping.
|
||||
@@ -1,88 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
spring-stack-shuffle demo. This standalone composition mounts the primitive
|
||||
through data-composition-src. Non-default cards, cues, and accent prove the
|
||||
variable path; the tight 1.5 / 1.75 cue pair keeps the L1 mid-flight
|
||||
redirect inside the demo choreography.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Spring Stack Shuffle 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;
|
||||
--muted: #94a3b8;
|
||||
--surface: #14171c;
|
||||
--border: #343a46;
|
||||
--brand: #71f5a7;
|
||||
--accent: #61a8ff;
|
||||
--accent-2: #c5a3ff;
|
||||
--font-body: Inter, system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(ellipse at 50% 42%, #12151b 0%, var(--bg, #0b0c0e) 74%);
|
||||
}
|
||||
|
||||
.mount-card {
|
||||
position: absolute;
|
||||
width: 62%;
|
||||
height: 82%;
|
||||
left: 19%;
|
||||
top: 9%;
|
||||
container-type: size;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="spring-stack-shuffle-demo"
|
||||
data-start="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="4"
|
||||
data-fps="30"
|
||||
>
|
||||
<div
|
||||
id="spring-stack-shuffle-mount"
|
||||
class="mount-card clip"
|
||||
data-composition-id="spring-stack-shuffle"
|
||||
data-composition-src="./spring-stack-shuffle.html"
|
||||
data-variable-values='{"cards":5,"cues":"0.9, 1.5, 1.75, 2.6","accent":"violet"}'
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
data-width="1190"
|
||||
data-height="886"
|
||||
></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["spring-stack-shuffle-demo"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "spring-stack-shuffle",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Spring Stack Shuffle",
|
||||
"description": "A stack of 3 to 5 slotted cards reshuffles on cues: the back card throws over the top and lands at the front with real mass, the rest compress and resettle; cues that land mid-flight redirect the moving cards with velocity preserved.",
|
||||
"tags": ["motion-primitive", "product-demo", "physics", "stack", "interruptible-spring"],
|
||||
"jobs": ["demonstrate"],
|
||||
"family": "product-demo",
|
||||
"profile": "stack",
|
||||
"evidence": "Wave K K7: interruptible-spring stack from the X expert study demand slate; L1 redirect authored into the default cue rhythm",
|
||||
"syncPoints": [{ "id": "first-landing", "phase": "in", "offset": 1.75 }],
|
||||
"files": [
|
||||
{
|
||||
"path": "spring-stack-shuffle.html",
|
||||
"target": "compositions/components/spring-stack-shuffle.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"id": "cards",
|
||||
"type": "number",
|
||||
"role": "content",
|
||||
"label": "Cards",
|
||||
"description": "Number of cards in the stack, 3 to 5.",
|
||||
"default": 4,
|
||||
"min": 3,
|
||||
"max": 5,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"id": "cues",
|
||||
"type": "string",
|
||||
"role": "timing",
|
||||
"label": "Shuffle cues",
|
||||
"description": "Comma-separated seconds from mount start; each cue throws the back card to the front. Cues inside a running throw redirect it mid-flight, velocity preserved. Empty keeps the default rhythm.",
|
||||
"default": "0.9, 1.7, 1.95"
|
||||
},
|
||||
{
|
||||
"id": "accent",
|
||||
"type": "enum",
|
||||
"role": "style",
|
||||
"label": "Accent",
|
||||
"description": "Active-card ring color from the contract token map.",
|
||||
"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 settled stack holds until the frame cuts.",
|
||||
"default": "none",
|
||||
"options": [
|
||||
{ "value": "none", "label": "None" },
|
||||
{ "value": "fade", "label": "Fade" },
|
||||
{ "value": "up", "label": "Up" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,607 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
spring-stack-shuffle: HyperFrames video primitive (product demo / physics)
|
||||
|
||||
A stack of 3 to 5 cards reshuffles on cues: on each cue the card at the
|
||||
back of the stack throws over the top and lands at the front with real
|
||||
mass (small landing overshoot, the quiet register), while the other cards
|
||||
step one slot deeper and give a brief compress-resettle as the flyer
|
||||
lands.
|
||||
|
||||
SIGNATURE (law L1, interruptible springs): when a cue fires while a
|
||||
previous shuffle is still in flight, every in-flight card REDIRECTS to
|
||||
its new slot preserving its instantaneous velocity. No snap-to-zero, no
|
||||
pop. The default cue rhythm (0.9, 1.7, 1.95) deliberately fires the third
|
||||
cue mid-throw of the second so the redirect is part of the authored
|
||||
choreography.
|
||||
|
||||
How the redirect stays deterministic and seek-safe: motion is compiled
|
||||
at mount into per-card, per-property segment lists (explicit from/to and
|
||||
a pure ease function each). A redirect cuts the covering segment
|
||||
analytically (value from the ease, velocity from a fixed finite
|
||||
difference), truncates it with a renormalized copy of the same curve, and
|
||||
continues with a cubic Hermite ease whose start slope equals the cut
|
||||
velocity and whose end slope is zero. Nothing simulates: every emitted
|
||||
tween is a fromTo with both endpoints authored, so any seek in either
|
||||
direction reproduces the same frame.
|
||||
|
||||
Slots (see README.md for a worked example):
|
||||
- [data-slot="card-1"] ... [data-slot="card-5"]: card bodies, front to
|
||||
back at mount. Replace the children of a slot in your installed copy.
|
||||
Defaults: numbered token skeleton cards. Direct img/video children
|
||||
cover the card area.
|
||||
|
||||
Variables (declared in data-composition-variables below):
|
||||
- cards (number 3..5, default 4): how many cards are in the stack.
|
||||
- cues (string, default "0.9, 1.7, 1.95"): comma-separated seconds,
|
||||
relative to mount start; each cue fires one shuffle. Cues closer
|
||||
together than a throw (0.85s) interrupt the flight and exercise the
|
||||
L1 redirect. Empty keeps the authored default rhythm.
|
||||
- accent (green | blue | violet, default green): active-card ring via
|
||||
the contract token map.
|
||||
- exit (none | fade | up, default none): frame roots own transitions;
|
||||
holds end films.
|
||||
|
||||
Envelope, fixed IN and OUT with elastic HOLD only (never time-scaled):
|
||||
IN = staggered card entrances, back to front, done by ~0.8s
|
||||
CUES = clamped into [0.85, D - OUT - 0.9] so every throw lands before
|
||||
any exit begins
|
||||
HOLD = still after the last landing bump settles
|
||||
OUT = 0s for exit none, otherwise 0.45s
|
||||
|
||||
Mount contract: MOUNTABLE SUB-COMPOSITION. The runtime clones only
|
||||
<template> contents; #root fills the host box (inset:0, container-type:
|
||||
size), has no data-width/data-height, and registers one paused timeline
|
||||
under the literal "spring-stack-shuffle" key. Stack geometry is measured
|
||||
in px from the mounted root once (cq-unit transform tweens are not
|
||||
seek-safe); variables come from window.__hyperframes.getVariables().
|
||||
-->
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-id="spring-stack-shuffle"
|
||||
data-composition-duration="4"
|
||||
data-composition-variables='[
|
||||
{ "id": "cards", "type": "number", "role": "content", "label": "Cards", "description": "Number of cards in the stack, 3 to 5.", "default": 4, "min": 3, "max": 5, "step": 1 },
|
||||
{ "id": "cues", "type": "string", "role": "timing", "label": "Shuffle cues", "description": "Comma-separated seconds from mount start; each cue throws the back card to the front. Cues inside a running throw redirect it mid-flight, velocity preserved. Empty keeps the default rhythm.", "default": "0.9, 1.7, 1.95" },
|
||||
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Active-card ring color from the contract token map.", "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 settled stack 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>Spring Stack Shuffle</title>
|
||||
</head>
|
||||
<body>
|
||||
<template>
|
||||
<div id="root" data-composition-id="spring-stack-shuffle" data-duration="4" 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, #0b0c0e);
|
||||
color: var(--fg, #f8fafc);
|
||||
font-family: var(--font-body, Inter, system-ui, sans-serif);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sss-clip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sss-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.sss-card {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 58%;
|
||||
width: 46cqw;
|
||||
height: 42cqh;
|
||||
margin-left: -23cqw;
|
||||
margin-top: -21cqh;
|
||||
border: 0.16cqmin solid var(--border, #343a46);
|
||||
border-radius: var(--radius, 2.2cqmin);
|
||||
background: var(--surface, #14171c);
|
||||
box-shadow: 0 2.2cqh 6cqh color-mix(in srgb, var(--bg, #000000) 55%, transparent);
|
||||
overflow: hidden;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Active-card ring: opacity is timeline-driven so the front card
|
||||
reads as the live one across every shuffle. */
|
||||
.sss-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 0.4cqmin solid var(--sss-accent, #71f5a7);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Depth veil: timeline-driven dim; deeper cards sit darker. */
|
||||
.sss-veil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--bg, #0b0c0e);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sss-slot {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.sss-slot > img,
|
||||
.sss-slot > video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sss-default {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: var(--space-3, 3.2cqmin);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2, 2cqmin);
|
||||
}
|
||||
|
||||
.sss-chip {
|
||||
align-self: flex-start;
|
||||
padding: 0.9cqmin 1.8cqmin;
|
||||
border: 0.14cqmin solid var(--border, #343a46);
|
||||
border-radius: 999cqmin;
|
||||
color: var(--muted, #94a3b8);
|
||||
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
|
||||
font-size: min(1.7cqw, 3cqh);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sss-bar {
|
||||
width: 46%;
|
||||
height: 8%;
|
||||
border-radius: 999cqmin;
|
||||
background: color-mix(in srgb, var(--fg, #f8fafc) 22%, var(--surface, #14171c));
|
||||
}
|
||||
|
||||
.sss-line {
|
||||
width: 84%;
|
||||
height: 5%;
|
||||
border-radius: 999cqmin;
|
||||
background: color-mix(in srgb, var(--muted, #94a3b8) 34%, var(--surface, #14171c));
|
||||
}
|
||||
|
||||
.sss-line + .sss-line {
|
||||
width: 70%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="spring-stack-shuffle-clip"
|
||||
class="sss-clip clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div class="sss-stage">
|
||||
<section class="sss-card" aria-label="Card 1">
|
||||
<div class="sss-slot" data-slot="card-1">
|
||||
<!-- SLOT "card-1": replace the children of this element with
|
||||
your own content (img, video, or HTML). -->
|
||||
<div class="sss-default" aria-hidden="true">
|
||||
<div class="sss-chip">01</div>
|
||||
<div class="sss-bar"></div>
|
||||
<div class="sss-line"></div>
|
||||
<div class="sss-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sss-veil"></div>
|
||||
<div class="sss-ring"></div>
|
||||
</section>
|
||||
<section class="sss-card" aria-label="Card 2">
|
||||
<div class="sss-slot" data-slot="card-2">
|
||||
<!-- SLOT "card-2": replace the children of this element with
|
||||
your own content (img, video, or HTML). -->
|
||||
<div class="sss-default" aria-hidden="true">
|
||||
<div class="sss-chip">02</div>
|
||||
<div class="sss-bar"></div>
|
||||
<div class="sss-line"></div>
|
||||
<div class="sss-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sss-veil"></div>
|
||||
<div class="sss-ring"></div>
|
||||
</section>
|
||||
<section class="sss-card" aria-label="Card 3">
|
||||
<div class="sss-slot" data-slot="card-3">
|
||||
<!-- SLOT "card-3": replace the children of this element with
|
||||
your own content (img, video, or HTML). -->
|
||||
<div class="sss-default" aria-hidden="true">
|
||||
<div class="sss-chip">03</div>
|
||||
<div class="sss-bar"></div>
|
||||
<div class="sss-line"></div>
|
||||
<div class="sss-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sss-veil"></div>
|
||||
<div class="sss-ring"></div>
|
||||
</section>
|
||||
<section class="sss-card" aria-label="Card 4">
|
||||
<div class="sss-slot" data-slot="card-4">
|
||||
<!-- SLOT "card-4": replace the children of this element with
|
||||
your own content (img, video, or HTML). -->
|
||||
<div class="sss-default" aria-hidden="true">
|
||||
<div class="sss-chip">04</div>
|
||||
<div class="sss-bar"></div>
|
||||
<div class="sss-line"></div>
|
||||
<div class="sss-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sss-veil"></div>
|
||||
<div class="sss-ring"></div>
|
||||
</section>
|
||||
<section class="sss-card" aria-label="Card 5">
|
||||
<div class="sss-slot" data-slot="card-5">
|
||||
<!-- SLOT "card-5": replace the children of this element with
|
||||
your own content (img, video, or HTML). -->
|
||||
<div class="sss-default" aria-hidden="true">
|
||||
<div class="sss-chip">05</div>
|
||||
<div class="sss-bar"></div>
|
||||
<div class="sss-line"></div>
|
||||
<div class="sss-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sss-veil"></div>
|
||||
<div class="sss-ring"></div>
|
||||
</section>
|
||||
</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 id: mount flattening strips data-composition-id from
|
||||
// the live root before this timeline registers.
|
||||
var compositionId = "spring-stack-shuffle";
|
||||
var stage = root.querySelector(".sss-stage");
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
|
||||
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("--sss-accent", accentColors[accent]);
|
||||
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
|
||||
|
||||
var cardCountRaw = Number(vars.cards);
|
||||
var cardCount = Number.isFinite(cardCountRaw)
|
||||
? Math.max(3, Math.min(5, Math.round(cardCountRaw)))
|
||||
: 4;
|
||||
var cardEls = Array.prototype.slice.call(root.querySelectorAll(".sss-card"));
|
||||
cardEls.slice(cardCount).forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
cardEls = cardEls.slice(0, cardCount);
|
||||
|
||||
// Stack geometry in px measured once from the mounted root:
|
||||
// cq-unit transform tweens are not seek-safe, px numbers are.
|
||||
var rect = root.getBoundingClientRect();
|
||||
var W = Math.max(1, rect.width);
|
||||
var H = Math.max(1, rect.height);
|
||||
var STEP_Y = -0.055 * H; // deeper cards peek above the front card
|
||||
var STEP_S = 0.05;
|
||||
var APEX_Y = STEP_Y * (cardCount - 1) - 0.09 * H; // arc above the stack
|
||||
var KICK_X = 0.12 * W; // lateral kick during the throw
|
||||
var BUMP_Y = 0.012 * H; // stack compress as the flyer lands
|
||||
|
||||
function poseFor(depth) {
|
||||
return {
|
||||
y: depth * STEP_Y,
|
||||
s: 1 - depth * STEP_S,
|
||||
veil: Math.min(0.5, depth * 0.15),
|
||||
};
|
||||
}
|
||||
|
||||
// RETIME RANGE: fixed IN and OUT; HOLD is the only elastic span.
|
||||
var THROW = 0.85;
|
||||
var RISE = THROW * 0.4;
|
||||
var LAND = THROW * 0.6;
|
||||
var SHIFT = 0.55;
|
||||
var OUT = exit === "none" ? 0 : 0.45;
|
||||
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
|
||||
var OUT_START = duration - OUT;
|
||||
var CUE_MIN = 0.85;
|
||||
var CUE_MAX = Math.max(CUE_MIN, OUT_START - THROW - 0.05);
|
||||
|
||||
var cues = String(
|
||||
vars.cues == null || String(vars.cues).trim() === "" ? "0.9, 1.7, 1.95" : vars.cues,
|
||||
)
|
||||
.split(",")
|
||||
.map(function (token) {
|
||||
return Number(token);
|
||||
})
|
||||
.filter(function (value) {
|
||||
return Number.isFinite(value);
|
||||
})
|
||||
.map(function (value) {
|
||||
return Math.min(CUE_MAX, Math.max(CUE_MIN, value));
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
/* Author-time motion compiler. A track is one (element, property)
|
||||
pair holding non-overlapping segments; every segment is a pure
|
||||
curve with explicit endpoints. cutAt() is the L1 primitive: it
|
||||
evaluates value and velocity at the cut analytically, truncates
|
||||
the covering segment to a renormalized copy of its own curve,
|
||||
and drops everything later. redirect() continues from the cut
|
||||
with a Hermite ease whose start slope IS the cut velocity. */
|
||||
function easeFn(ease) {
|
||||
return typeof ease === "function" ? ease : gsap.parseEase(ease);
|
||||
}
|
||||
|
||||
function makeTrack(el, prop, initial) {
|
||||
return { el: el, prop: prop, initial: initial, segs: [] };
|
||||
}
|
||||
|
||||
function endValue(track) {
|
||||
return track.segs.length > 0 ? track.segs[track.segs.length - 1].to : track.initial;
|
||||
}
|
||||
|
||||
function cutAt(track, time) {
|
||||
var segs = track.segs;
|
||||
while (segs.length > 0 && segs[segs.length - 1].t0 >= time) segs.pop();
|
||||
if (segs.length === 0) return { value: track.initial, velocity: 0 };
|
||||
var last = segs[segs.length - 1];
|
||||
if (last.t1 <= time) return { value: last.to, velocity: 0 };
|
||||
var curve = easeFn(last.ease);
|
||||
var span = last.t1 - last.t0;
|
||||
var f = (time - last.t0) / span;
|
||||
var lo = Math.max(0, f - 0.001);
|
||||
var hi = Math.min(1, f + 0.001);
|
||||
var slope = (curve(hi) - curve(lo)) / (hi - lo);
|
||||
var progress = curve(f);
|
||||
var value = last.from + (last.to - last.from) * progress;
|
||||
var velocity = ((last.to - last.from) * slope) / span;
|
||||
last.t1 = time;
|
||||
last.to = value;
|
||||
if (progress > 1e-6) {
|
||||
last.ease = (function (fullCurve, cutFraction, cutProgress) {
|
||||
return function (u) {
|
||||
return fullCurve(u * cutFraction) / cutProgress;
|
||||
};
|
||||
})(curve, f, progress);
|
||||
}
|
||||
return { value: value, velocity: velocity };
|
||||
}
|
||||
|
||||
// Cubic Hermite: h(0)=0, h(1)=1, h'(0)=m, h'(1)=0. A hot start
|
||||
// slope overshoots past the target and eases back, which is the
|
||||
// spring read; the clamp keeps it in the quiet register (L2).
|
||||
function hermite(m0) {
|
||||
var m = Math.max(-4, Math.min(4, m0));
|
||||
return function (u) {
|
||||
return u * u * u * (m - 2) + u * u * (3 - 2 * m) + m * u;
|
||||
};
|
||||
}
|
||||
|
||||
var smooth = hermite(0); // smoothstep: zero slope both ends
|
||||
|
||||
// Landing ease: zero-slope start (ballistic hang at the apex),
|
||||
// small overshoot past the target, zero-slope settle. C1 all the
|
||||
// way through, pure function of progress.
|
||||
function landEase(overshoot) {
|
||||
return function (u) {
|
||||
if (u < 0.62) {
|
||||
var a = u / 0.62;
|
||||
return (1 + overshoot) * a * a * (3 - 2 * a);
|
||||
}
|
||||
var b = (u - 0.62) / 0.38;
|
||||
return 1 + overshoot - overshoot * b * b * (3 - 2 * b);
|
||||
};
|
||||
}
|
||||
|
||||
function addSeg(track, t0, dur, to, ease) {
|
||||
track.segs.push({ t0: t0, t1: t0 + dur, from: endValue(track), to: to, ease: ease });
|
||||
}
|
||||
|
||||
function redirect(track, time, dur, to) {
|
||||
var cut = cutAt(track, time);
|
||||
var delta = to - cut.value;
|
||||
var ease = Math.abs(delta) < 1e-4 ? smooth : hermite((cut.velocity * dur) / delta);
|
||||
track.segs.push({ t0: time, t1: time + dur, from: cut.value, to: to, ease: ease });
|
||||
}
|
||||
|
||||
var powerOut = gsap.parseEase("power3.out");
|
||||
var cards = cardEls.map(function (el, index) {
|
||||
var pose = poseFor(index);
|
||||
return {
|
||||
el: el,
|
||||
depth: index,
|
||||
landing: null,
|
||||
x: makeTrack(el, "x", 0),
|
||||
y: makeTrack(el, "y", pose.y + 0.05 * H),
|
||||
s: makeTrack(el, "s", pose.s * 0.985),
|
||||
veil: makeTrack(el.querySelector(".sss-veil"), "veil", pose.veil),
|
||||
ring: makeTrack(el.querySelector(".sss-ring"), "ring", 0),
|
||||
};
|
||||
});
|
||||
var zEvents = [];
|
||||
|
||||
// IN: staggered entrances, back card first, front card last.
|
||||
cards.forEach(function (card, index) {
|
||||
var pose = poseFor(index);
|
||||
var tIn = (cardCount - 1 - index) * 0.07;
|
||||
addSeg(card.y, tIn, 0.5, pose.y, powerOut);
|
||||
addSeg(card.s, tIn, 0.5, pose.s, powerOut);
|
||||
if (index === 0) addSeg(card.ring, 0.45, 0.3, 0.9, smooth);
|
||||
});
|
||||
|
||||
// SHUFFLES: one per cue. The back card throws over the top; the
|
||||
// rest step one slot deeper. Any card still in flight when a cue
|
||||
// fires is cut and redirected, velocity preserved (L1).
|
||||
cues.forEach(function (cue) {
|
||||
var back = cards.reduce(function (a, b) {
|
||||
return b.depth > a.depth ? b : a;
|
||||
});
|
||||
cards.forEach(function (card) {
|
||||
if (card.landing && card.landing.t > cue) card.landing.canceled = true;
|
||||
card.landing = null;
|
||||
card.depth = card === back ? 0 : card.depth + 1;
|
||||
zEvents.push({
|
||||
t: cue,
|
||||
el: card.el,
|
||||
z: card === back ? 60 : 40 - card.depth,
|
||||
canceled: false,
|
||||
});
|
||||
});
|
||||
|
||||
cards.forEach(function (card) {
|
||||
var pose = poseFor(card.depth);
|
||||
if (card === back) {
|
||||
// Throw arc: velocity-matched rise to a ballistic apex,
|
||||
// then a landing with one small sanctioned overshoot.
|
||||
redirect(card.x, cue, RISE, KICK_X);
|
||||
addSeg(card.x, cue + RISE, LAND, 0, smooth);
|
||||
redirect(card.y, cue, RISE, APEX_Y);
|
||||
addSeg(card.y, cue + RISE, LAND, pose.y, landEase(0.06));
|
||||
redirect(card.s, cue, RISE, 1.05);
|
||||
addSeg(card.s, cue + RISE, LAND, pose.s, landEase(0.04));
|
||||
redirect(card.veil, cue, THROW, pose.veil);
|
||||
redirect(card.ring, cue, THROW, 0.9);
|
||||
var landing = { t: cue + THROW, el: card.el, z: 40, canceled: false };
|
||||
zEvents.push(landing);
|
||||
card.landing = landing;
|
||||
} else {
|
||||
redirect(card.x, cue, SHIFT, 0);
|
||||
redirect(card.y, cue, SHIFT, pose.y);
|
||||
redirect(card.s, cue, SHIFT, pose.s);
|
||||
redirect(card.veil, cue, SHIFT, pose.veil);
|
||||
redirect(card.ring, cue, SHIFT, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Compress-resettle: the stack takes the flyer's weight.
|
||||
cards.forEach(function (card) {
|
||||
if (card === back) return;
|
||||
var rest = endValue(card.y);
|
||||
addSeg(card.y, cue + THROW - 0.05, 0.16, rest + BUMP_Y, smooth);
|
||||
addSeg(card.y, cue + THROW + 0.11, 0.22, rest, smooth);
|
||||
});
|
||||
});
|
||||
|
||||
// EMIT: initial states, then every compiled segment as an
|
||||
// explicit fromTo. Segments never overlap per track, both
|
||||
// endpoints are authored, eases are pure functions: seek-safe.
|
||||
var propFor = {
|
||||
x: function (value) {
|
||||
return { x: value };
|
||||
},
|
||||
y: function (value) {
|
||||
return { y: value };
|
||||
},
|
||||
s: function (value) {
|
||||
return { scale: value };
|
||||
},
|
||||
veil: function (value) {
|
||||
return { opacity: value };
|
||||
},
|
||||
ring: function (value) {
|
||||
return { opacity: value };
|
||||
},
|
||||
};
|
||||
|
||||
gsap.set(stage, { opacity: 1, y: 0 });
|
||||
cards.forEach(function (card, index) {
|
||||
gsap.set(card.el, { opacity: 0, zIndex: 40 - index });
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
cards.forEach(function (card, index) {
|
||||
var tIn = (cardCount - 1 - index) * 0.07;
|
||||
tl.fromTo(
|
||||
card.el,
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.4, ease: "power2.out" },
|
||||
tIn,
|
||||
);
|
||||
["x", "y", "s", "veil", "ring"].forEach(function (prop) {
|
||||
var track = card[prop];
|
||||
var toProps = propFor[prop];
|
||||
gsap.set(track.el, toProps(track.initial));
|
||||
track.segs.forEach(function (seg) {
|
||||
var dur = seg.t1 - seg.t0;
|
||||
if (dur < 1 / 120) {
|
||||
tl.set(track.el, toProps(seg.to), seg.t1);
|
||||
return;
|
||||
}
|
||||
var to = toProps(seg.to);
|
||||
to.duration = dur;
|
||||
to.ease = easeFn(seg.ease);
|
||||
tl.fromTo(track.el, toProps(seg.from), to, seg.t0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
zEvents.forEach(function (event) {
|
||||
if (event.canceled) return;
|
||||
tl.set(event.el, { zIndex: event.z }, event.t);
|
||||
});
|
||||
|
||||
// HOLD: completely still after the last landing bump.
|
||||
|
||||
// OUT: optional departure; exit none holds until the frame cuts.
|
||||
if (exit === "fade") {
|
||||
tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
|
||||
} else if (exit === "up") {
|
||||
tl.to(
|
||||
stage,
|
||||
{ opacity: 0, y: -0.07 * H, 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