Files
hyperframes/docs/catalog/blocks/loop-splitreveal.mdx
Miao YangandClaude Fable 5.1 44c43fd99f feat(registry): add 249 seamless image-loop blocks (95 motion families, 1:1)
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>
2026-09-09 18:40:32 +08:00

477 lines
39 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Loop Split Reveal"
description: "Paired image panels slide in from opposite edges and meet. 4 image slots, 8s seamless loop, 1:1."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/loop-splitreveal.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/loop-splitreveal.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add loop-splitreveal" item="loop-splitreveal" />
That writes one file: `compositions/loop-splitreveal.html`.
## Add it to your video
It runs for 8 seconds at 1080×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="loop-splitreveal"
data-composition-src="compositions/loop-splitreveal.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1080"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `mediaFit` | `crop` | `natural`, `crop` | Natural keeps each image's own ratio; Crop fills the card box at the media aspect below. |
| `cropAspectRatio` | `1:1` | `16:9`, `9:16`, `1:1`, `4:5`, `3:2`, `3:4`, `2:3` | Card aspect used when Fit is Crop. |
| `backgroundType` | `gradient` | `gradient`, `color`, `image` | Gradient (start → end colours), flat Colour, or your own Image. |
| `backgroundGradientStart` | `#111111` | color | Top colour of the gradient. |
| `backgroundGradientEnd` | `#444444` | color | Bottom colour of the gradient. |
| `backgroundColor` | `#0b0b0f` | color | Flat colour when Type is Color; also the ground under an Image. |
| `backgroundImage` | `` | image | Cover-fitted background when Type is Image. |
| `blurAmount` | `0` | 0px to 60px, step 1px | Blur on the background image, px. |
| `backgroundGradientNoise` | `0` | 0% to 100%, step 1% | Film-grain overlay strength. |
| `backgroundOpacity` | `1` | 0 to 1, step 0.05 | Opacity of the whole background layer (0 = transparent ground). |
| `shadowEnabled` | `false` | `true`, `false` | Shadow Enabled (engine parameter; see the block header comment). |
| `shadowX` | `0` | -40px to 40px, step 0.5px | Shadow X (engine parameter; see the block header comment). |
| `shadowY` | `1` | -40px to 40px, step 0.5px | Shadow Y (engine parameter; see the block header comment). |
| `shadowBlur` | `1.5` | 0px to 40px, step 0.5px | Shadow Blur (engine parameter; see the block header comment). |
| `shadowSpread` | `-0.25` | -20px to 40px, step 0.5px | Shadow Spread (engine parameter; see the block header comment). |
| `shadowOpacity` | `40` | 0% to 100%, step 1% | Shadow Opacity (engine parameter; see the block header comment). |
| `shadowColor` | `#000000` | color | Shadow Color (engine parameter; see the block header comment). |
| `borderRadius` | `32` | 0px to 200px, step 1px | Card corner radius in px at 1080. |
| `easing` | `smooth` | `custom`, `smooth`, `natural`, `slowdown`, `snappy`, `accelerate`, `elastic`, `bounce`, `overshoot`, `impulse`, `swing`, `linear` | Easing family for stepped motion. |
| `easeBezier` | `0.50,0.00,0.50,1.00` | string | Cubic-bezier control points for custom easing. |
| `padding` | `6` | 0px to 100px, step 1px | Padding (engine parameter; see the block header comment). |
| `splitRatio` | `50` | 0 to 100, step 1 | Split Ratio (engine parameter; see the block header comment). |
| `gap` | `2` | 0px to 10px, step 0.1px | Spacing between cards. |
| `image1` | `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` | image | Image on card 1 of 4. Any aspect; fitted per mediaFit. |
| `image2` | `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` | image | Image on card 2 of 4. Any aspect; fitted per mediaFit. |
| `image3` | `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` | image | Image on card 3 of 4. Any aspect; fitted per mediaFit. |
| `image4` | `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` | image | Image on card 4 of 4. Any aspect; fitted per mediaFit. |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="loop-splitreveal"
data-composition-src="compositions/loop-splitreveal.html"
data-variable-values='{"mediaFit":"crop","cropAspectRatio":"1:1","backgroundType":"gradient","backgroundGradientStart":"#111111","backgroundGradientEnd":"#444444","backgroundColor":"#0b0b0f","backgroundImage":"","blurAmount":0,"backgroundGradientNoise":0,"backgroundOpacity":1,"shadowEnabled":"false","shadowX":0,"shadowY":1,"shadowBlur":1.5,"shadowSpread":-0.25,"shadowOpacity":40,"shadowColor":"#000000","borderRadius":32,"easing":"smooth","easeBezier":"0.50,0.00,0.50,1.00","padding":6,"splitRatio":50,"gap":2,"image1":"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","image2":"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","image3":"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","image4":"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"}'
></div>
```
## Source
<Accordion title={`loop-splitreveal.html`}>
```html
<!doctype html>
<html
lang="en"
data-composition-variables='[{"id":"mediaFit","type":"enum","role":"style","label":"Media · Fit","description":"Natural keeps each image&#39;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&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39; width%3D&#39;1400&#39; height%3D&#39;1050&#39; viewBox%3D&#39;0 0 1400 1050&#39;%3E%3Crect width%3D&#39;1400&#39; height%3D&#39;1050&#39; fill%3D&#39;%230b0b0f&#39;%2F%3E%3Crect width%3D&#39;1400&#39; height%3D&#39;1050&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.18)&#39;%2F%3E%3Crect x%3D&#39;2&#39; y%3D&#39;2&#39; width%3D&#39;1396&#39; height%3D&#39;1046&#39; fill%3D&#39;none&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.14)&#39; stroke-width%3D&#39;4&#39;%2F%3E%3Cpath d%3D&#39;M252 778.05H621.6&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39; stroke-width%3D&#39;16.5&#39; stroke-linecap%3D&#39;round&#39;%2F%3E%3Cpath d%3D&#39;M286.65 778.05L379.05 662.55L471.45 778.05Z&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%2F%3E%3Ccircle cx%3D&#39;552.3&#39; cy%3D&#39;674.1&#39; r%3D&#39;34.65&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%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&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39; width%3D&#39;1400&#39; height%3D&#39;788&#39; viewBox%3D&#39;0 0 1400 788&#39;%3E%3Crect width%3D&#39;1400&#39; height%3D&#39;788&#39; fill%3D&#39;%230b0b0f&#39;%2F%3E%3Crect width%3D&#39;1400&#39; height%3D&#39;788&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.18)&#39;%2F%3E%3Crect x%3D&#39;2&#39; y%3D&#39;2&#39; width%3D&#39;1396&#39; height%3D&#39;784&#39; fill%3D&#39;none&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.14)&#39; stroke-width%3D&#39;4&#39;%2F%3E%3Cpath d%3D&#39;M252 583.908H529.376&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39; stroke-width%3D&#39;12.382857142857144&#39; stroke-linecap%3D&#39;round&#39;%2F%3E%3Cpath d%3D&#39;M278.004 583.908L347.348 497.228L416.692 583.908Z&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%2F%3E%3Ccircle cx%3D&#39;477.36800000000005&#39; cy%3D&#39;505.896&#39; r%3D&#39;26.004&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%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&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39; width%3D&#39;1050&#39; height%3D&#39;1400&#39; viewBox%3D&#39;0 0 1050 1400&#39;%3E%3Crect width%3D&#39;1050&#39; height%3D&#39;1400&#39; fill%3D&#39;%230b0b0f&#39;%2F%3E%3Crect width%3D&#39;1050&#39; height%3D&#39;1400&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.18)&#39;%2F%3E%3Crect x%3D&#39;2&#39; y%3D&#39;2&#39; width%3D&#39;1046&#39; height%3D&#39;1396&#39; fill%3D&#39;none&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.14)&#39; stroke-width%3D&#39;4&#39;%2F%3E%3Cpath d%3D&#39;M189 995.05H558.6&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39; stroke-width%3D&#39;16.5&#39; stroke-linecap%3D&#39;round&#39;%2F%3E%3Cpath d%3D&#39;M223.65 995.05L316.05 879.55L408.45 995.05Z&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%2F%3E%3Ccircle cx%3D&#39;489.3&#39; cy%3D&#39;891.1&#39; r%3D&#39;34.65&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%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&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39; width%3D&#39;1400&#39; height%3D&#39;1400&#39; viewBox%3D&#39;0 0 1400 1400&#39;%3E%3Crect width%3D&#39;1400&#39; height%3D&#39;1400&#39; fill%3D&#39;%230b0b0f&#39;%2F%3E%3Crect width%3D&#39;1400&#39; height%3D&#39;1400&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.18)&#39;%2F%3E%3Crect x%3D&#39;2&#39; y%3D&#39;2&#39; width%3D&#39;1396&#39; height%3D&#39;1396&#39; fill%3D&#39;none&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.14)&#39; stroke-width%3D&#39;4&#39;%2F%3E%3Cpath d%3D&#39;M252 1037.4H744.8&#39; stroke%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39; stroke-width%3D&#39;22&#39; stroke-linecap%3D&#39;round&#39;%2F%3E%3Cpath d%3D&#39;M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%2F%3E%3Ccircle cx%3D&#39;652.4000000000001&#39; cy%3D&#39;898.8&#39; r%3D&#39;46.199999999999996&#39; fill%3D&#39;rgba(255%2C255%2C255%2C0.45)&#39;%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>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `loop` `images` `showcase` `gallery` `splitreveal`.
Created by Miao Yang.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)