mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
c8040d6747
One variant per family, curated from a 249-component loop-template collection generated by Miao Yang. Selection covers 24 well-developed multi-variant families (marquee, carousel, 3d, deck, orbit, globe, flicker, spiral, frames, flip, proximity, wheel, book, carousel3d, grid, field, scale, spin, stack, stories, wipe, gravity, parallax, pages) plus 11 best-in-class single-variant families (bento, corridor, coverflow, fold, tunnel, filmstrip, ring, shelf, vortexspin, swirl, cardtoss). Code-only: each block ships registry-item.json + HTML composition, no preview media. registry.json updated with all 35 entries, kept alphabetically sorted within the hyperframes:block group. Validation: scripts/lint-registry-items.mjs passed, 434 item(s), 0 errors. Co-Authored-By: Miao Yang <miao.yang@heygen.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
323 lines
33 KiB
HTML
Vendored
323 lines
33 KiB
HTML
Vendored
<!doctype html>
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[{"id":"mediaFit","type":"enum","role":"style","label":"Media · Fit","description":"Natural keeps each image's own ratio; Crop fills the card box at the media aspect below.","default":"natural","options":[{"value":"natural","label":"Natural (original ratio)"},{"value":"crop","label":"Crop"}]},{"id":"cropAspectRatio","type":"enum","role":"style","label":"Media · Aspect","description":"Card aspect used when Fit is Crop.","default":"1:1","options":[{"value":"16:9","label":"16:9"},{"value":"9:16","label":"9:16"},{"value":"1:1","label":"1:1"},{"value":"4:5","label":"4:5"},{"value":"3:2","label":"3:2"},{"value":"3:4","label":"3:4"},{"value":"2:3","label":"2:3"}]},{"id":"backgroundType","type":"enum","role":"style","label":"Background · Type","description":"Gradient (start → end colours), flat Colour, or your own Image.","default":"gradient","options":[{"value":"gradient","label":"Gradient"},{"value":"color","label":"Color"},{"value":"image","label":"Image"}]},{"id":"backgroundGradientStart","type":"color","role":"style","label":"Background · Gradient start","description":"Top colour of the gradient.","default":"#111111"},{"id":"backgroundGradientEnd","type":"color","role":"style","label":"Background · Gradient end","description":"Bottom colour of the gradient.","default":"#444444"},{"id":"backgroundColor","type":"color","role":"style","label":"Background · Color","description":"Flat colour when Type is Color; also the ground under an Image.","default":"#0b0b0f"},{"id":"backgroundImage","type":"image","role":"style","label":"Background · Image","description":"Cover-fitted background when Type is Image.","portrays":["background_image"],"default":""},{"id":"blurAmount","type":"number","role":"style","label":"Background · Blur","description":"Blur on the background image, px.","default":0,"min":0,"max":60,"step":1,"unit":"px"},{"id":"backgroundGradientNoise","type":"number","role":"style","label":"Background · Grain","description":"Film-grain overlay strength.","default":0,"min":0,"max":100,"step":1,"unit":"%"},{"id":"backgroundOpacity","type":"number","role":"style","label":"Background · Opacity","description":"Opacity of the whole background layer (0 = transparent ground).","default":1,"min":0,"max":1,"step":0.05},{"id":"borderRadius","type":"number","role":"style","label":"Appearance · Border radius","description":"Card corner radius in px at 1080.","default":0,"min":0,"max":200,"step":1,"unit":"px"},{"id":"cycles","type":"number","role":"style","label":"Animation · Cycles","description":"Number of motion cycles per loop.","default":1,"min":0,"max":10,"step":0.1},{"id":"stagger","type":"number","role":"style","label":"Animation · Stagger","description":"Stagger (engine parameter; see the block header comment).","default":0.06666666666666667,"min":0,"max":2,"step":0.01},{"id":"delay","type":"number","role":"style","label":"Animation · Delay","description":"Hold time between steps, seconds.","default":0,"min":0,"max":2,"step":0.01,"unit":"s"},{"id":"direction","type":"enum","role":"style","label":"Animation · Direction","description":"Direction of travel.","default":"down","options":[{"value":"down","label":"Down"},{"value":"up","label":"Up"}]},{"id":"planeSize","type":"number","role":"style","label":"Template options · Media size","description":"Card size in scene units.","default":38,"min":0,"max":76,"step":1},{"id":"visible","type":"number","role":"style","label":"Template options · Visible","description":"Visible (engine parameter; see the block header comment).","default":3,"min":0,"max":12,"step":0.1},{"id":"perspective","type":"number","role":"style","label":"Template options · Perspective","description":"Camera focal length: smaller = stronger perspective.","default":73,"min":0,"max":246,"step":10},{"id":"zoom","type":"number","role":"style","label":"Template options · Zoom","description":"Zoom (engine parameter; see the block header comment).","default":150,"min":0,"max":534,"step":10},{"id":"offsetX","type":"number","role":"style","label":"Template options · Offset X","description":"Horizontal offset of the formation, % of width.","default":0,"min":-100,"max":100,"step":1,"unit":"%"},{"id":"offsetY","type":"number","role":"style","label":"Template options · Offset Y","description":"Vertical offset of the formation, % of height.","default":-2.5,"min":-100,"max":100,"step":1,"unit":"%"},{"id":"image1","type":"image","role":"content","label":"Media · Image 1","description":"Image on card 1 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image2","type":"image","role":"content","label":"Media · Image 2","description":"Image on card 2 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image3","type":"image","role":"content","label":"Media · Image 3","description":"Image on card 3 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image4","type":"image","role":"content","label":"Media · Image 4","description":"Image on card 4 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image5","type":"image","role":"content","label":"Media · Image 5","description":"Image on card 5 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image6","type":"image","role":"content","label":"Media · Image 6","description":"Image on card 6 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image7","type":"image","role":"content","label":"Media · Image 7","description":"Image on card 7 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image8","type":"image","role":"content","label":"Media · Image 8","description":"Image on card 8 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"}]'
|
|
>
|
|
<head>
|
|
<title>loop-stack-1</title>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=1080, height=1080" />
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
html,
|
|
body {
|
|
width: 1080px;
|
|
height: 1080px;
|
|
overflow: hidden;
|
|
background: #0b0b0f;
|
|
}
|
|
#loop-stack-1-bg {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
#loop-stack-1-stage {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.card {
|
|
position: absolute;
|
|
overflow: hidden;
|
|
}
|
|
.card img {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div
|
|
id="loop-stack-1-root"
|
|
data-composition-id="loop-stack-1"
|
|
data-start="0"
|
|
data-width="1080"
|
|
data-height="1080"
|
|
data-duration="11"
|
|
>
|
|
<div
|
|
id="loop-stack-1-loop"
|
|
class="clip"
|
|
data-start="0"
|
|
data-duration="11"
|
|
data-track-index="1"
|
|
data-layout-allow-overflow
|
|
>
|
|
<div id="loop-stack-1-bg"></div>
|
|
<div id="loop-stack-1-stage"></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// Shared runtime for the the reference tool loop ports. Loaded by every family engine.
|
|
// Everything here is a pure function of the HyperFrames variables + t.
|
|
//
|
|
// Reference constants (measured on the 512x512 preview library, 2026-09-08):
|
|
// Marquee planeSize -> 0.3% of frame height per unit (7 variants, all 1.54px/unit @512)
|
|
// Marquee gap -> 0.2% of frame height per unit
|
|
// 3D perspective -> CSS focal length f = 1000px * (100/perspective) at H=1080
|
|
// (3D01/02/08/11: card widths & far-card spacing match to <3%)
|
|
// Both are calibrated on a SQUARE canvas; for 16:9 we scale by H. ponytail: if the
|
|
// dashboard 16:9 reference shows width-relative scaling, flip REF below to W.
|
|
window.RF = (() => {
|
|
// Variables are injected by the inline script via RF.use(): HyperFrames wraps INLINE scripts in a
|
|
// per-composition scope where getVariables() resolves; this external file only sees the host window.
|
|
const V = {},
|
|
MEDIA = [];
|
|
const use = (vars, media) => {
|
|
Object.assign(V, vars || {});
|
|
MEDIA.splice(0, MEDIA.length, ...(media || []));
|
|
};
|
|
const root = document.querySelector("[data-composition-id]");
|
|
const W = +root.dataset.width,
|
|
H = +root.dataset.height;
|
|
const REF = H;
|
|
const num = (k, d) => (V[k] !== undefined && V[k] !== "" && !isNaN(+V[k]) ? +V[k] : d);
|
|
const str = (k, d) => (V[k] !== undefined && V[k] !== "" ? String(V[k]) : d);
|
|
const on = (k, d) => /^(true|on|1)$/i.test(str(k, d ? "on" : "off")); // spec booleans arrive as "true"/"false", enums as on/off
|
|
|
|
function pick(n) {
|
|
n = Math.max(1, Math.min(MEDIA.length, n | 0));
|
|
return MEDIA.slice(0, n);
|
|
}
|
|
// aspect (w/h) a card takes for a media item under mediaFit + cropAspectRatio
|
|
function aspect(m, fit, cropAR) {
|
|
if (fit === "natural") return m.w / m.h;
|
|
const [a, b] = String(cropAR || "1:1")
|
|
.split(":")
|
|
.map(Number);
|
|
return a && b ? a / b : 1;
|
|
}
|
|
function card(m, w, h, radius) {
|
|
const el = document.createElement("div");
|
|
el.className = "card";
|
|
el.style.cssText = `position:absolute;width:${w}px;height:${h}px;overflow:hidden;border-radius:${radius}px;`;
|
|
const img = document.createElement("img");
|
|
img.src = m.src;
|
|
img.width = Math.round(w);
|
|
img.height = Math.round(h);
|
|
img.style.cssText = `display:block;width:${w}px;height:${h}px;object-fit:cover;`;
|
|
el.appendChild(img);
|
|
return el;
|
|
}
|
|
function background(el) {
|
|
// the editor's Background panel: type gradient | color | image, gradient start/end, grain (noise %), opacity, image blur
|
|
const type = str("backgroundType", "gradient");
|
|
const c = str("backgroundColor", "#0b0b0f");
|
|
el.style.background = c;
|
|
if (type === "gradient") {
|
|
const g1 = str("backgroundGradientStart", "#111111"),
|
|
g2 = str("backgroundGradientEnd", "#444444");
|
|
el.style.background = `linear-gradient(180deg, ${g1}, ${g2})`;
|
|
} else if (type === "image") {
|
|
const im = V.backgroundImage,
|
|
src = im && typeof im === "object" ? im.url || im.src || "" : im || "";
|
|
if (src) {
|
|
el.style.background = `${c} url("${src}") center / cover no-repeat`;
|
|
const b = num("blurAmount", 0);
|
|
if (b > 0) el.style.filter = `blur(${b}px)`;
|
|
}
|
|
}
|
|
el.style.opacity = String(Math.max(0, Math.min(1, num("backgroundOpacity", 1))));
|
|
const noise = num("backgroundGradientNoise", 0);
|
|
if (noise > 0) {
|
|
const g = document.createElement("div");
|
|
g.style.cssText = `position:absolute;inset:0;pointer-events:none;mix-blend-mode:overlay;opacity:${((noise / 100) * 0.7).toFixed(3)};background:url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'256'%20height%3D'256'%3E%3Cfilter%20id%3D'n'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.9'%20numOctaves%3D'3'%20stitchTiles%3D'stitch'%20seed%3D'7'%2F%3E%3CfeColorMatrix%20values%3D'0%200%200%200%200.5%200%200%200%200%200.5%200%200%200%200%200.5%200%200%200%201%200'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'256'%20height%3D'256'%20filter%3D'url(%23n)'%2F%3E%3C%2Fsvg%3E") 0 0 / 256px 256px repeat`;
|
|
el.appendChild(g);
|
|
}
|
|
}
|
|
const expoInOut = (x) =>
|
|
x <= 0
|
|
? 0
|
|
: x >= 1
|
|
? 1
|
|
: x < 0.5
|
|
? Math.pow(2, 20 * x - 10) / 2
|
|
: (2 - Math.pow(2, -20 * x + 10)) / 2;
|
|
const frac = (x) => x - Math.floor(x);
|
|
return {
|
|
V,
|
|
use,
|
|
W,
|
|
H,
|
|
REF,
|
|
num,
|
|
str,
|
|
on,
|
|
MEDIA,
|
|
pick,
|
|
aspect,
|
|
card,
|
|
background,
|
|
expoInOut,
|
|
frac,
|
|
};
|
|
})();
|
|
</script>
|
|
<script>
|
|
(async () => {
|
|
// Stack loop family — "Cards deal onto a receding stack."
|
|
// Measured on the Stack01 preview (11s, 8 images -> 41.25f steps), 2026-09-08:
|
|
// front card 292px square = planeSize*zoom/100 % of H (38*1.5 = 57% -> 292 @512), `visible` cards
|
|
// recede behind it, peeking out above (direction down) with sizes 292/264/226 = S*(1 - .079L - .017L^2)
|
|
// and top edges 134/108/80 (27px = 5.3% of H higher per level); the whole group's bbox is centred
|
|
// (80..426; offsetY -2.5 -> -2.5px, Stack03 -7.5 -> -19px: used 0.4% of H per unit).
|
|
// Stack03 (perspective 123, visible 6): front 328 = 64% H, tops every 20.7px, sizes
|
|
// 1/.957/.909/.854/.787/.689 -> quadratic a=.038 b=.0048 (max err 1.7%); Stack01 a=.079 b=.017.
|
|
// Step: ONE smooth ease (bezier .5,0,.5,1) over the step moves every card one level toward the
|
|
// camera (front 292 -> 304 at f20, card2 274 -> 284 -> 288 at f21/24/27, model within 2px); the
|
|
// front card fades out around e = 0.5 over `stagger` of the step (0.0667*41.25 = 2.75f; luma
|
|
// 74 -> 93 -> 97 -> 101 at f20..23), the new back card fades in over tau 0.635..0.855 (luma
|
|
// 27,43,53,61,65,68,70 at f26..32, ease-out shape).
|
|
// Guesses/knobs: recession constants (a, b, rise) interpolated linearly in perspective between the two
|
|
// measured variants; direction up mirrors the peek side (Stack02: peeks below, sheet agrees);
|
|
// cycles = steps per loop multiplier; delay = seconds of phase.
|
|
// media list [{src,w,h}] is inlined by build.mjs; HyperFrames scopes inline scripts, so hand both to the shared lib explicitly
|
|
const __V = Object.assign(
|
|
{},
|
|
(window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: null) || {},
|
|
{ loopDuration: 11, loopRepeat: 2, imageCount: 8 },
|
|
);
|
|
const __PH = [
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
];
|
|
const __MEDIA = await Promise.all(
|
|
["image1", "image2", "image3", "image4", "image5", "image6", "image7", "image8"].map(
|
|
async (id, i) => {
|
|
const v = __V[id];
|
|
const src = (v && typeof v === "object" ? v.url || v.src : v) || __PH[i];
|
|
const img = new Image();
|
|
img.src = src;
|
|
try {
|
|
await img.decode();
|
|
} catch (e) {}
|
|
return { src, w: img.naturalWidth || 1400, h: img.naturalHeight || 1050 };
|
|
},
|
|
),
|
|
);
|
|
RF.use(__V, __MEDIA);
|
|
const { W, H, REF, num, str, pick, aspect, background, frac } = RF;
|
|
const DUR = num("loopDuration", 11);
|
|
const FIT = str("mediaFit", "natural");
|
|
const CROP_AR = str("cropAspectRatio", "1:1");
|
|
const S = ((num("planeSize", 38) * num("zoom", 150)) / 100 / 100) * REF;
|
|
const VIS = Math.max(1, num("visible", 3) | 0);
|
|
const OX = num("offsetX", 0) * 0.004 * REF,
|
|
OY = num("offsetY", 0) * 0.004 * REF; // 0.4% of H per unit (S01 -2.5px, S03 -19px @512)
|
|
const PERSP = num("perspective", 73);
|
|
// recession constants: measured on Stack01 (perspective 73) and Stack03 (123), interpolated linearly in perspective (knob)
|
|
const lerpP = (a73, a123) => a73 + ((PERSP - 73) * (a123 - a73)) / 50;
|
|
const RA = Math.max(0.005, lerpP(0.079, 0.038)),
|
|
RB = Math.max(0, lerpP(0.017, 0.0048));
|
|
const CYCLES = Math.max(1, num("cycles", 1) | 0);
|
|
const STAG = num("stagger", 0.0667);
|
|
const DELAY = num("delay", 0);
|
|
const UP = str("direction", "down") === "up" ? -1 : 1; // +1: back cards peek out above
|
|
const RADIUS = (num("borderRadius", 0) * REF) / 1080;
|
|
const IMAGES = pick(num("imageCount", 16));
|
|
const N = IMAGES.length;
|
|
const STEPS = N * CYCLES;
|
|
const RISE = lerpP(0.053, 0.0405) * REF; // top edge climb per level (S01 27px, S03 20.7px @512)
|
|
const bezier = (x1, y1, x2, y2) => (x) => {
|
|
if (x <= 0) return 0;
|
|
if (x >= 1) return 1;
|
|
let lo = 0,
|
|
hi = 1,
|
|
s = 0.5;
|
|
for (let i = 0; i < 24; i++) {
|
|
s = (lo + hi) / 2;
|
|
const bx = 3 * (1 - s) * (1 - s) * s * x1 + 3 * (1 - s) * s * s * x2 + s * s * s;
|
|
if (bx < x) lo = s;
|
|
else hi = s;
|
|
}
|
|
return 3 * (1 - s) * (1 - s) * s * y1 + 3 * (1 - s) * s * s * y2 + s * s * s;
|
|
};
|
|
const smooth = bezier(0.5, 0, 0.5, 1);
|
|
const sizeAt = (L) => S * Math.max(0.2, 1 - RA * L - RB * L * L);
|
|
|
|
document.getElementById("loop-stack-1-loop").dataset.duration = DUR;
|
|
background(document.getElementById("loop-stack-1-bg"));
|
|
const stage = document.getElementById("loop-stack-1-stage");
|
|
// group bbox at rest: front bottom .. back top; centre it
|
|
const backTop = -S / 2 - RISE * (VIS - 1) + (S - sizeAt(VIS - 1)) * 0; // top of back card relative to front centre
|
|
const groupCentre = (backTop + S / 2) / 2; // front bottom = +S/2
|
|
const yShift = -groupCentre * UP + OY;
|
|
const cards = [];
|
|
for (let i = 0; i <= VIS; i++) {
|
|
// VIS + 1: the fading front plus the stack
|
|
const el = document.createElement("div");
|
|
el.className = "card";
|
|
el.style.borderRadius = RADIUS + "px";
|
|
const img = document.createElement("img");
|
|
el.appendChild(img);
|
|
stage.appendChild(el);
|
|
cards.push({ el, img, src: null });
|
|
}
|
|
function place(c, m, L, op) {
|
|
const size = sizeAt(L);
|
|
const a = aspect(m, FIT, CROP_AR);
|
|
const w = size * Math.min(1, a),
|
|
h = size * Math.min(1, 1 / a);
|
|
if (c.src !== m.src) {
|
|
c.img.src = m.src;
|
|
c.src = m.src;
|
|
}
|
|
c.el.style.display = op <= 0 ? "none" : "";
|
|
c.el.style.opacity = Math.min(1, op);
|
|
c.el.style.width = w + "px";
|
|
c.el.style.height = h + "px";
|
|
// level L: top edge RISE*L above the front's top edge (down) / below its bottom edge (up)
|
|
const top = UP > 0 ? -S / 2 - RISE * L : S / 2 + RISE * L - h;
|
|
c.el.style.transform = `translate(${OX - w / 2}px, ${top + yShift}px)`;
|
|
}
|
|
function renderAt(t) {
|
|
const g = frac(t + DELAY / DUR) * STEPS,
|
|
k = Math.floor(g),
|
|
tau = g - k;
|
|
const e = smooth(tau);
|
|
// draw back to front: level VIS (fading in), VIS-1 .. 1, then the leaving front (level -e)
|
|
const fin = Math.max(0, Math.min(1, (tau - 0.635) / 0.22));
|
|
const opIn = 1 - (1 - fin) * (1 - fin);
|
|
place(cards[0], IMAGES[(k + VIS) % N], VIS - e, opIn);
|
|
for (let m = VIS - 1; m >= 1; m--) place(cards[VIS - m], IMAGES[(k + m) % N], m - e, 1);
|
|
const opOut = 1 - Math.max(0, Math.min(1, (e - 0.5 + STAG / 2) / Math.max(STAG, 0.005)));
|
|
place(cards[VIS], IMAGES[k % N], -e, opOut);
|
|
}
|
|
// one paused linear tween drives t; every frame is recomputed from t alone (seek-safe)
|
|
const state = { t: 0 };
|
|
const tl = gsap.timeline({ paused: true });
|
|
tl.to(state, { t: 1, duration: DUR, ease: "none", onUpdate: () => renderAt(state.t) }, 0);
|
|
renderAt(0);
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["loop-stack-1"] = tl;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|