Files
hyperframes/docs/catalog/blocks/loop-shelf.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

427 lines
49 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 Shelf"
description: "The focused image card stands upright while its neighbours fall back on a shelf. 8 image slots, 14s 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-shelf.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/loop-shelf.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add loop-shelf" item="loop-shelf" />
That writes one file: `compositions/loop-shelf.html`.
## Add it to your video
It runs for 14 seconds at 1080×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="loop-shelf"
data-composition-src="compositions/loop-shelf.html"
data-start="0"
data-duration="14"
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` | `natural` | `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). |
| `scaleIntensity` | `0.645` | 0 to 1, step 0.05 | Strength of the size change. |
| `borderRadius` | `18` | 0px to 200px, step 1px | Card corner radius in px at 1080. |
| `direction` | `rightToLeft` | `rightToLeft`, `leftToRight` | Direction of travel. |
| `easing` | `smooth` | `smooth`, `snappy`, `elastic`, `linear` | Easing family for stepped motion. |
| `spacing` | `8` | 0 to 16, step 0.1 | Spacing (engine parameter; see the block header comment). |
| `sideTilt` | `8` | 0 to 16, step 0.1 | Side Tilt (engine parameter; see the block header comment). |
| `dim` | `40` | 0 to 80, step 1 | Dim (engine parameter; see the block header comment). |
| `visible` | `2` | 0 to 10, step 0.1 | Visible (engine parameter; see the block header comment). |
| `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 8. 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 8. 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 8. 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 8. Any aspect; fitted per mediaFit. |
| `image5` | `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 5 of 8. Any aspect; fitted per mediaFit. |
| `image6` | `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 6 of 8. Any aspect; fitted per mediaFit. |
| `image7` | `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 7 of 8. Any aspect; fitted per mediaFit. |
| `image8` | `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 8 of 8. 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-shelf"
data-composition-src="compositions/loop-shelf.html"
data-variable-values='{"mediaFit":"natural","cropAspectRatio":"1:1","backgroundType":"gradient","backgroundGradientStart":"#111111","backgroundGradientEnd":"#444444","backgroundColor":"#0b0b0f","backgroundImage":"","blurAmount":0,"backgroundGradientNoise":0,"backgroundOpacity":1,"scaleIntensity":0.645,"borderRadius":18,"direction":"rightToLeft","easing":"smooth","spacing":8,"sideTilt":8,"dim":40,"visible":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","image5":"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","image6":"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","image7":"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","image8":"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-shelf.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":"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":"scaleIntensity","type":"number","role":"style","label":"Appearance · Media scale","description":"Strength of the size change.","default":0.645,"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":18,"min":0,"max":200,"step":1,"unit":"px"},{"id":"direction","type":"enum","role":"style","label":"Animation · Direction","description":"Direction of travel.","default":"rightToLeft","options":[{"value":"rightToLeft","label":"Right To Left"},{"value":"leftToRight","label":"Left To Right"}]},{"id":"easing","type":"enum","role":"style","label":"Animation · Easing","description":"Easing family for stepped motion.","default":"smooth","options":[{"value":"smooth","label":"Smooth"},{"value":"snappy","label":"Snappy"},{"value":"elastic","label":"Elastic"},{"value":"linear","label":"Linear"}]},{"id":"spacing","type":"number","role":"style","label":"Template options · Spacing","description":"Spacing (engine parameter; see the block header comment).","default":8,"min":0,"max":16,"step":0.1},{"id":"sideTilt","type":"number","role":"style","label":"Template options · Side Tilt","description":"Side Tilt (engine parameter; see the block header comment).","default":8,"min":0,"max":16,"step":0.1},{"id":"dim","type":"number","role":"style","label":"Template options · Dim","description":"Dim (engine parameter; see the block header comment).","default":40,"min":0,"max":80,"step":1},{"id":"visible","type":"number","role":"style","label":"Template options · Visible","description":"Visible (engine parameter; see the block header comment).","default":2,"min":0,"max":10,"step":0.1},{"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&#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 8. 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 8. 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 8. 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"},{"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&#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":"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&#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":"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&#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":"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&#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-shelf</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-shelf-bg {
position: absolute;
inset: 0;
}
#loop-shelf-stage {
position: absolute;
inset: 0;
perspective-origin: 50% 50%;
}
#loop-shelf-row {
position: absolute;
left: 50%;
top: 50%;
width: 0;
height: 0;
transform-style: preserve-3d;
}
.card img {
display: block;
}
.dim {
position: absolute;
inset: 0;
background: #000;
}
</style>
</head>
<body>
<div
id="loop-shelf-root"
data-composition-id="loop-shelf"
data-start="0"
data-width="1080"
data-height="1080"
data-duration="14"
>
<div
id="loop-shelf-loop"
class="clip"
data-start="0"
data-duration="14"
data-track-index="1"
data-layout-allow-overflow
>
<div id="loop-shelf-bg"></div>
<div id="loop-shelf-stage"><div id="loop-shelf-row"></div></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 () => {
// Shelf loop family — the focused card stands upright, neighbours fall back (translateZ) behind it,
// tilt outward in 2D (rotateZ, outer edge DOWN) and dim; the focused card sits on top (closer in z).
// Measured @512 (2026-09-08): focused card 330px = 0.645 H (= scaleIntensity·H, no padding); neighbours: column
// height constant 244 with the top edge sloping 11px over 80px -> pure rotateZ 8deg (= sideTilt), no rotateY
// foreshortening; size 241.6 = 0.732 S. Scale follows a perspective law k(d) = 1/(1 + 0.366·d): outgoing and
// incoming card scales at the same frame sum to d_in + d_out ~ 0.96-0.98 under it (linear lerp gives 1.08).
// Screen x = d·P·k(d) with world pitch P ~ 0.75·S (248px, from 5 mid-transition frames; noisy ±10%); the
// neighbour is 80% hidden behind the focused card (visible 0-91px at rest, matches). Dim: luma ×0.6 = 1-dim%.
// Timing: step = DUR/N; motion detected on frames 3-18 of a 52.5-frame step -> transition = 0.35·step
// (0.6s); `smooth` = easeInOutCubic (fitted on FocusSlider/Slats). Knobs: `tilt` (12) has no visible
// effect in the preview (no rotateY/rotateX foreshortening measurable) -> not consumed; `visible` hides
// cards beyond that many slots; elastic falls back to smooth; snappy = (0.4,0,0.2,1) guess.
// 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: 14, 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, card, background, frac } = RF;
const DUR = num("loopDuration", 14);
const FIT = str("mediaFit", "natural");
const CROP_AR = str("cropAspectRatio", "1:1");
const SI = num("scaleIntensity", 0.645);
const SPACING = num("spacing", 8); // unconsumed: no gap is visible (neighbours overlap behind)
const SIDE = num("sideTilt", 8);
const DIM = num("dim", 40) / 100;
const VISIBLE = Math.max(1, num("visible", 2));
const FWD = str("direction", "rightToLeft") === "rightToLeft" ? 1 : -1;
const RADIUS = (num("borderRadius", 18) * REF) / 1080;
const IMAGES = pick(Math.min(24, num("imageCount", 16)));
const N = IMAGES.length;
const TRANS = 0.35;
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 EASES = {
smooth: [0.65, 0, 0.35, 1],
linear: [0, 0, 1, 1],
snappy: [0.4, 0, 0.2, 1],
};
const ease = bezier(...(EASES[str("easing", "smooth")] || EASES.smooth));
document.getElementById("loop-shelf-loop").dataset.duration = DUR;
background(document.getElementById("loop-shelf-bg"));
const S = SI * REF; // focused card height
const F = (1000 * REF) / 1080,
Z1 = 0.366 * F; // push-back per slot (only Z1/F matters)
document.getElementById("loop-shelf-stage").style.perspective = F + "px";
const row = document.getElementById("loop-shelf-row");
const cards = [],
dims = [],
widths = [];
for (let i = 0; i < N; i++) {
const m = IMAGES[i],
a = aspect(m, FIT, CROP_AR),
w = S * a;
const c = card(m, w, S, RADIUS);
c.style.left = -w / 2 + "px";
c.style.top = -S / 2 + "px";
const d = document.createElement("div");
d.className = "dim";
c.appendChild(d);
row.appendChild(c);
cards.push(c);
dims.push(d);
widths.push(w);
}
// world positions: pitch between neighbours = 0.75 × mean of their widths
const P = [0];
for (let i = 1; i <= N; i++)
P.push(P[i - 1] + (0.75 * (widths[i - 1] + widths[i % N])) / 2);
const TOTAL = P[N],
PITCH = TOTAL / N;
function renderAt(t) {
const u = frac(t) * N,
k = Math.floor(u),
g = u - k;
const e = ease(Math.min(1, g / TRANS));
const cam =
FWD > 0 ? P[k] + e * (P[k + 1] - P[k]) : TOTAL - (P[k] + e * (P[k + 1] - P[k]));
for (let i = 0; i < N; i++) {
let off = P[i] - cam;
off -= TOTAL * Math.round(off / TOTAL);
const d = off / PITCH,
ad = Math.abs(d),
c = cards[i];
if (ad > VISIBLE + 0.5) {
c.style.display = "none";
continue;
}
c.style.display = "";
const z = -ad * Z1; // screen scale = F/(F - z) = 1/(1+0.366·ad)
c.style.transform = `translate3d(${off}px,0,${z}px) rotateZ(${Math.sign(d) * Math.min(1, ad) * SIDE}deg)`;
dims[i].style.opacity = String(DIM * Math.min(1, ad));
}
}
// 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-shelf"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `loop` `images` `showcase` `gallery` `shelf` `3d`.
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)