mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 22:08:03 +00:00
One block per loop variant (loop-<family>[-<n>], 1080x1080), one hand-written engine per family, driven by a single paused GSAP timeline that recomputes every card transform from t. Every engine parameter is exposed through data-composition-variables, grouped like a template editor's panel (Media / Background / Appearance / Animation / Template options), plus image1..N content slots that default to monochrome SVG placeholders. No third-party media ships. Validation: scripts/lint-registry-items.mjs 249/249, oxfmt clean, hyperframes check on one variant per family (95/95), registry.json + docs/catalog pages regenerated by the repo scripts. Previews are rendered locally (720^2) and need the docs-images upload. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
377 lines
28 KiB
HTML
Vendored
377 lines
28 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":"crop","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":"shadowEnabled","type":"enum","role":"style","label":"Appearance · Shadow","description":"Shadow Enabled (engine parameter; see the block header comment).","default":"false","options":[{"value":"true","label":"On"},{"value":"false","label":"Off"}]},{"id":"shadowX","type":"number","role":"style","label":"Appearance · Shadow X","description":"Shadow X (engine parameter; see the block header comment).","default":0,"min":-40,"max":40,"step":0.5,"unit":"px"},{"id":"shadowY","type":"number","role":"style","label":"Appearance · Shadow Y","description":"Shadow Y (engine parameter; see the block header comment).","default":1,"min":-40,"max":40,"step":0.5,"unit":"px"},{"id":"shadowBlur","type":"number","role":"style","label":"Appearance · Shadow blur","description":"Shadow Blur (engine parameter; see the block header comment).","default":1.5,"min":0,"max":40,"step":0.5,"unit":"px"},{"id":"shadowSpread","type":"number","role":"style","label":"Appearance · Shadow spread","description":"Shadow Spread (engine parameter; see the block header comment).","default":-0.25,"min":-20,"max":40,"step":0.5,"unit":"px"},{"id":"shadowOpacity","type":"number","role":"style","label":"Appearance · Shadow opacity","description":"Shadow Opacity (engine parameter; see the block header comment).","default":40,"min":0,"max":100,"step":1,"unit":"%"},{"id":"shadowColor","type":"color","role":"style","label":"Appearance · Shadow colour","description":"Shadow Color (engine parameter; see the block header comment).","default":"#000000"},{"id":"borderRadius","type":"number","role":"style","label":"Appearance · Border radius","description":"Card corner radius in px at 1080.","default":32,"min":0,"max":200,"step":1,"unit":"px"},{"id":"easing","type":"enum","role":"style","label":"Animation · Easing","description":"Easing family for stepped motion.","default":"smooth","options":[{"value":"custom","label":"Custom"},{"value":"smooth","label":"Smooth"},{"value":"natural","label":"Natural"},{"value":"slowdown","label":"Slowdown"},{"value":"snappy","label":"Snappy"},{"value":"accelerate","label":"Accelerate"},{"value":"elastic","label":"Elastic"},{"value":"bounce","label":"Bounce"},{"value":"overshoot","label":"Overshoot"},{"value":"impulse","label":"Impulse"},{"value":"swing","label":"Swing"},{"value":"linear","label":"Linear"}]},{"id":"easeBezier","type":"string","role":"style","label":"Animation · Ease Bezier","description":"Cubic-bezier control points for custom easing.","default":"0.50,0.00,0.50,1.00"},{"id":"padding","type":"number","role":"style","label":"Template options · Padding","description":"Padding (engine parameter; see the block header comment).","default":6,"min":0,"max":100,"step":1,"unit":"px"},{"id":"splitRatio","type":"number","role":"style","label":"Template options · Split Ratio","description":"Split Ratio (engine parameter; see the block header comment).","default":50,"min":0,"max":100,"step":1},{"id":"gap","type":"number","role":"style","label":"Template options · Gap","description":"Spacing between cards.","default":2,"min":0,"max":10,"step":0.1,"unit":"px"},{"id":"image1","type":"image","role":"content","label":"Media · Image 1","description":"Image on card 1 of 4. 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 4. 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 4. 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 4. 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-splitreveal</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-splitreveal-bg {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
.panel {
|
|
position: absolute;
|
|
overflow: hidden;
|
|
}
|
|
.layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: none;
|
|
overflow: hidden;
|
|
}
|
|
.card img {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div
|
|
id="loop-splitreveal-root"
|
|
data-composition-id="loop-splitreveal"
|
|
data-start="0"
|
|
data-width="1080"
|
|
data-height="1080"
|
|
data-duration="8"
|
|
>
|
|
<div
|
|
id="loop-splitreveal-loop"
|
|
class="clip"
|
|
data-start="0"
|
|
data-duration="8"
|
|
data-track-index="1"
|
|
data-layout-allow-overflow
|
|
>
|
|
<div id="loop-splitreveal-bg"></div>
|
|
<div id="loop-splitreveal-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 () => {
|
|
// SplitReveal loop family — "Paired panels slide in from opposite edges": two
|
|
// rounded panels side by side (split by `splitRatio`, `gap` between), each showing an image; per period the
|
|
// next pair slides in — the left panel's new image from the LEFT edge, the right panel's from the RIGHT edge,
|
|
// the right panel lagging the left.
|
|
// Measured on the 512 preview (2026-09-08): card = frame inset 6% of H (padding 6); panels x 31..250 and
|
|
// 261..480 (gap 10px = 2% of H for gap 2 => gap unit % of H, not px); the preview cycles only TWO pairs
|
|
// (placeholder greys of images 3,4 -> 1,2 -> 3,4), i.e. it was rendered with 4 images: period 120f = DUR/2.
|
|
// Left panel coverage f12:.03 f18:.10 f24:.25 f30:.50 f36:.74 f42:.89 f48:.96 f54:.995 => cubic-bezier
|
|
// (.5,0,.5,1) over 48f starting at f6; right panel identical but starting f15 (9f later).
|
|
// Knobs (guesses): with N images the loop shows N/2 pairs, period = DUR/(N/2); timings as fractions of the
|
|
// period (start 0.05, slide 0.40, right lag 0.075); the new image covers the resting one (a push looks the
|
|
// same on uniform placeholders); shadow* (shadowEnabled off in the spec) implemented as a box-shadow on the
|
|
// sliding image when enabled; easing names other than smooth/custom/linear mapped to GSAP eases.
|
|
// 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: 8, loopRepeat: 2, imageCount: 4 },
|
|
);
|
|
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",
|
|
];
|
|
const __MEDIA = await Promise.all(
|
|
["image1", "image2", "image3", "image4"].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, on, pick, aspect, card, background, frac } = RF;
|
|
const DUR = num("loopDuration", 8);
|
|
const FIT = str("mediaFit", "crop");
|
|
const CROP_AR = str("cropAspectRatio", "1:1");
|
|
const SPLIT = Math.min(95, Math.max(5, num("splitRatio", 50))) / 100;
|
|
const GAP = (num("gap", 2) / 100) * REF;
|
|
const PAD = (num("padding", 6) / 100) * REF;
|
|
const RADIUS = (num("borderRadius", 32) * REF) / 1080;
|
|
const EASING = str("easing", "smooth"),
|
|
BEZ = str("easeBezier", "0.50,0.00,0.50,1.00");
|
|
const SHADOW = on("shadowEnabled", false);
|
|
const SH = {
|
|
x: num("shadowX", 0),
|
|
y: num("shadowY", 1),
|
|
blur: num("shadowBlur", 1.5),
|
|
spread: num("shadowSpread", -0.25),
|
|
op: num("shadowOpacity", 40) / 100,
|
|
col: str("shadowColor", "#000000"),
|
|
};
|
|
const IMAGES = pick(num("imageCount", 16));
|
|
const N = IMAGES.length;
|
|
const PAIRS = Math.max(1, Math.floor(N / 2));
|
|
const START = 0.05,
|
|
SLIDE = 0.4,
|
|
LAG = 0.075; // fractions of the per-pair period
|
|
|
|
document.getElementById("loop-splitreveal-loop").dataset.duration = DUR;
|
|
background(document.getElementById("loop-splitreveal-bg"));
|
|
const stage = document.getElementById("loop-splitreveal-stage");
|
|
const CW = W - 2 * PAD,
|
|
CH = H - 2 * PAD;
|
|
const LW = (CW - GAP) * SPLIT,
|
|
RW = CW - GAP - LW;
|
|
|
|
function bezier(p1x, p1y, p2x, p2y) {
|
|
const cx = 3 * p1x,
|
|
bx = 3 * (p2x - p1x) - cx,
|
|
ax = 1 - cx - bx,
|
|
cy = 3 * p1y,
|
|
by = 3 * (p2y - p1y) - cy,
|
|
ay = 1 - cy - by;
|
|
const X = (t) => ((ax * t + bx) * t + cx) * t,
|
|
Y = (t) => ((ay * t + by) * t + cy) * t,
|
|
DX = (t) => (3 * ax * t + 2 * bx) * t + cx;
|
|
return (x) => {
|
|
if (x <= 0) return 0;
|
|
if (x >= 1) return 1;
|
|
let t = x;
|
|
for (let i = 0; i < 8; i++) {
|
|
const d = DX(t);
|
|
if (Math.abs(d) < 1e-6) break;
|
|
t -= (X(t) - x) / d;
|
|
}
|
|
return Y(Math.min(1, Math.max(0, t)));
|
|
};
|
|
}
|
|
function easeFn(name, bez) {
|
|
const b = (bez || "").split(",").map(Number);
|
|
const table = {
|
|
smooth: [0.5, 0, 0.5, 1],
|
|
natural: [0.25, 0.1, 0.25, 1],
|
|
slowdown: [0, 0, 0.2, 1],
|
|
snappy: [0.7, 0, 0.3, 1],
|
|
accelerate: [0.4, 0, 1, 1],
|
|
};
|
|
if (name === "linear") return (x) => Math.min(1, Math.max(0, x));
|
|
if (name === "custom" && b.length === 4 && b.every((v) => !isNaN(v))) return bezier(...b);
|
|
if (table[name]) return bezier(...table[name]);
|
|
const g = {
|
|
elastic: "elastic.out(1,0.5)",
|
|
bounce: "bounce.out",
|
|
overshoot: "back.out(1.7)",
|
|
impulse: "expo.out",
|
|
swing: "sine.inOut",
|
|
}[name];
|
|
return g ? gsap.parseEase(g) : bezier(0.5, 0, 0.5, 1);
|
|
}
|
|
const EASE = easeFn(EASING, BEZ);
|
|
|
|
function makePanel(x, w, imgs) {
|
|
const p = document.createElement("div");
|
|
p.className = "panel";
|
|
p.style.cssText += `left:${x}px;top:${PAD}px;width:${w}px;height:${CH}px;border-radius:${RADIUS}px;`;
|
|
const layers = imgs.map((m) => {
|
|
const a = aspect(m, FIT, CROP_AR);
|
|
let cw, ch;
|
|
if (FIT === "natural") {
|
|
if (w / CH > a) {
|
|
ch = CH;
|
|
cw = CH * a;
|
|
} else {
|
|
cw = w;
|
|
ch = w / a;
|
|
}
|
|
} else {
|
|
if (w / CH > a) {
|
|
cw = w;
|
|
ch = w / a;
|
|
} else {
|
|
ch = CH;
|
|
cw = CH * a;
|
|
}
|
|
}
|
|
const L = document.createElement("div");
|
|
L.className = "layer";
|
|
const c = card(m, cw, ch, 0);
|
|
c.style.left = (w - cw) / 2 + "px";
|
|
c.style.top = (CH - ch) / 2 + "px";
|
|
if (SHADOW)
|
|
L.style.boxShadow = `${(SH.x / 100) * REF}px ${(SH.y / 100) * REF}px ${(SH.blur / 100) * REF}px ${(SH.spread / 100) * REF}px ${SH.col}${Math.round(
|
|
SH.op * 255,
|
|
)
|
|
.toString(16)
|
|
.padStart(2, "0")}`;
|
|
L.appendChild(c);
|
|
p.appendChild(L);
|
|
return L;
|
|
});
|
|
stage.appendChild(p);
|
|
return { el: p, layers, w };
|
|
}
|
|
const left = makePanel(PAD, LW, IMAGES.filter((m, i) => i % 2 === 0).slice(0, PAIRS));
|
|
const right = makePanel(
|
|
PAD + LW + GAP,
|
|
RW,
|
|
IMAGES.filter((m, i) => i % 2 === 1).slice(0, PAIRS),
|
|
);
|
|
|
|
function drive(panel, k, q, fromLeft) {
|
|
const outI = k % PAIRS,
|
|
inI = (k + 1) % PAIRS;
|
|
panel.layers.forEach((L, i) => {
|
|
if (i !== outI && i !== inI) L.style.display = "none";
|
|
});
|
|
const out = panel.layers[outI],
|
|
inn = panel.layers[inI];
|
|
out.style.display = "block";
|
|
out.style.zIndex = "1";
|
|
out.style.transform = "";
|
|
if (q <= 0 || PAIRS === 1) {
|
|
inn.style.display = inI === outI ? "block" : "none";
|
|
return;
|
|
}
|
|
inn.style.display = "block";
|
|
inn.style.zIndex = "2";
|
|
inn.style.transform = `translate3d(${(fromLeft ? -1 : 1) * (1 - q) * panel.w}px,0,0)`;
|
|
}
|
|
function renderAt(t) {
|
|
const s = frac(t) * PAIRS,
|
|
k = Math.floor(s),
|
|
u = s - k;
|
|
drive(left, k, EASE((u - START) / SLIDE), true);
|
|
drive(right, k, EASE((u - START - LAG) / SLIDE), false);
|
|
}
|
|
// 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-splitreveal"] = tl;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|