mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
3a7fcd10e0
* feat(registry): add 25 image carousel blocks (5 families × 5 variants) Five carousel families, each with 5 style variants: - Orbit (1–5): image cards on a spinning 3D Fibonacci sphere - Path (1–5): cards following animated CSS motion paths - Circle (1–5): circular carousel layouts - Vision (1–5): Apple Vision-style spatial presentations - Text Circle (1–5): circular carousels with text overlays All blocks are 1920×1080 at 6s, with 12–24 configurable image slots. Includes catalog preview thumbnails for each block. Co-Authored-By: Jake Moran <jake.moran@heygen.com> * refactor(registry): host carousel block images on the CDN The registry is served straight out of this repository (DEFAULT_REGISTRY_URL points at raw.githubusercontent.com), so every byte a block ships is permanent history. The 25 carousel blocks added 421 JPEGs, 27.7 MiB in a checkout, and made the diff 472 files. Only 23 of those images were distinct: the same 12-24 placeholders were copied into every block. files[] entries gain an optional `url`. When set, the installer fetches the bytes from there instead of joining the registry base. `path` does not change and still says where the file lands relative to the item, so composition HTML, target mirroring and `hyperframes add` behave exactly as before. Keys are content-addressed, so the 396 manifest entries resolve to 23 objects, and a changed image gets a new URL rather than a stale one cached behind `immutable, max-age=31536000`. The catalog preview renderer copies an item's directory and renders it, so it needs the same materialisation step. Without it the preview draws every card blank and reports success, which is worse than failing. Also drops registry/catalog/, 25 hand-made thumbnails referenced by nothing; catalog previews are rendered by CI and served from docs/images/catalog. Verified: all 23 objects return 200 from the CDN with hashes matching their keys; `hyperframes add carousel-orbit-1` against a local registry installs 24 real JPEGs; the preview render produces the album art, and produces blank cards when the fetch step is removed. * style(registry): format the carousel composition HTML `oxfmt --check .` covers the whole tree, and these 25 files were never run through it. The pre-commit hook only formats staged files, so nothing local caught it. * feat(catalog): publish the carousel blocks without republishing their images These 25 blocks had no Catalog page. Every other item in the registry has one, so they shipped invisible: installable by name, unfindable by browsing. Generating them naively undid the change they were added by. The Catalog payload copies an item's assets into docs/public/, which is tracked, so the 396 images this PR just removed came back as 43 MB one directory over — worse than the 3 MB they started as, because each block got its own copy. The copy exists because these compositions assemble `img.src` at run time out of a variable value, so there is no `src="..."` in the markup for the payload's asset scan to resolve. An unpredictable path can only be satisfied by serving every file beside it, which is what `needsOwnDirectory` asks for. An absolute URL needs no directory: the scan already skips any `https:` reference. So for the payload path only, hosted files are left undownloaded and the composition's variable defaults are rewritten to their URLs. The preview renderer still downloads them, because it paints real frames and a missing file is a blank card. The explorer posts every value to the preview frame on mount, including untouched ones, so the page's variable list carries the URLs too. Left as local paths they would have overridden the payload's own defaults and asked the frame for a file that was deliberately never published. Result: 25 pages, 25 payloads, zero bytes of image added. Verified: a spike item declaring no assets at all rendered its 24 covers from the CDN, proving the variable-default path; payload generation for a carousel block now writes no item directory and no shared asset; the preview render still produces the album art. mint validate and mint broken-links pass on the new pages. test:scripts is green. * refactor(catalog): split the hosted-asset step out of prepareProjectDir Two functions rather than one: finding the composition and rewriting its variable defaults are separate jobs, and inlining the mode branch pushed prepareProjectDir past the complexity gate it was already sitting on. Behaviour is unchanged. Re-verified both paths after the split: the payload for a carousel block still writes no item directory and no shared asset, and the preview render still produces the album art. * feat(catalog): give the carousels their own shelf 25 image carousels landed in Showcases and were 53% of it, so the scenes that shelf exists for disappeared underneath them. That is the same shape the 24 editor themes made, and it gets the same fix they got. Keyed on the first tag, which is this file's stated grouping rule, rather than on the name. `screen-flow-carousel` leads with `product-demo` and stays on the shelf that says what it is for; a future carousel that is not named `carousel-*` still lands here. Showcases 47 -> 22, Carousels 25, and no existing item changed shelf. * fix(registry): centre the circle-5 carousel path in its composition Its ring was centred at x=3832.6 in a 3840-wide composition, so it sat on the right edge and most of it fell outside the frame. Only a few cards were ever visible, cropped, with two thirds of the composition empty. The exported path carried absolute coordinates from a layout that was never recentred. Shifting the four vertices by (-1912.597, -4.340) puts the ring on the composition centre. Handles are relative, so only the anchor points move and the shape is unchanged. carousel-text-circle-5 shares the identical path and had the identical fault. The other three circle variants sit within 12% of centre, which reads as authored placement rather than the same bug, so they are left alone. * fix(catalog): rebuild the circle-5 payloads after recentring the path The Catalog preview plays the payload, not the composition on disk, so recentring the source changed nothing a reader sees. The payload still carried the old vertices and the ring still hung off the right edge of the frame. Verified the consumer this time, not just the producer: both payloads now resolve to a path centre of x=1920. The other 23 rebuild byte-identical, so the formatting pass did not reach them. * feat(catalog): promote Carousels to its own section It was a shelf inside Scenes & demos, which is where a scene type belongs by kind but not by weight. At 25 items it is larger than Data & charts (17) and Blocks (13), each of which is already a section holding a single shelf, so the catalog's own precedent puts it one level up. Pulling it out also takes the largest section in the catalog from 120 items to 95, which is the reason the shelf was added in the first place. The two circle-5 pages change because their embedded source block carries the recentred path; nothing else in them moved. --------- Co-authored-by: Jake Moran <jake.moran@heygen.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2201 lines
97 KiB
Plaintext
2201 lines
97 KiB
Plaintext
---
|
||
title: "Path Carousel 4"
|
||
description: "Image cards ride an open bezier path (arc, V, diagonal, wave, S-curve) with depth scaling and tangent rotation (Path style 4); 24 image slots."
|
||
---
|
||
|
||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||
|
||
<VariablesExplorer
|
||
previewSrc="/public/catalog/blocks/carousel-path-4.json"
|
||
compositionId="carousel-path-4"
|
||
compositionSrc="compositions/carousel-path-4.html"
|
||
variables={[{"id":"image1","type":"image","role":"content","label":"Card image 1","description":"Image on card 1 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/223d162eed10f08f.jpg"},{"id":"image2","type":"image","role":"content","label":"Card image 2","description":"Image on card 2 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/5964f78a6c7c55f7.jpg"},{"id":"image3","type":"image","role":"content","label":"Card image 3","description":"Image on card 3 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/e402bbb823b45122.jpg"},{"id":"image4","type":"image","role":"content","label":"Card image 4","description":"Image on card 4 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/25fe4d416b4e318e.jpg"},{"id":"image5","type":"image","role":"content","label":"Card image 5","description":"Image on card 5 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/5360277ad88fe1d4.jpg"},{"id":"image6","type":"image","role":"content","label":"Card image 6","description":"Image on card 6 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/2726d9a6cdc429df.jpg"},{"id":"image7","type":"image","role":"content","label":"Card image 7","description":"Image on card 7 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/9e4c4f735ff68582.jpg"},{"id":"image8","type":"image","role":"content","label":"Card image 8","description":"Image on card 8 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/76ac64af4487c9f1.jpg"},{"id":"image9","type":"image","role":"content","label":"Card image 9","description":"Image on card 9 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/f21ff2ce3281c61a.jpg"},{"id":"image10","type":"image","role":"content","label":"Card image 10","description":"Image on card 10 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/4361229f4648df3d.jpg"},{"id":"image11","type":"image","role":"content","label":"Card image 11","description":"Image on card 11 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/636f8577be7b9849.jpg"},{"id":"image12","type":"image","role":"content","label":"Card image 12","description":"Image on card 12 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/74d11304a8ac596c.jpg"},{"id":"image13","type":"image","role":"content","label":"Card image 13","description":"Image on card 13 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/dc265f0c99df9596.jpg"},{"id":"image14","type":"image","role":"content","label":"Card image 14","description":"Image on card 14 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/830389dd85314cef.jpg"},{"id":"image15","type":"image","role":"content","label":"Card image 15","description":"Image on card 15 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/bba30166a86960c3.jpg"},{"id":"image16","type":"image","role":"content","label":"Card image 16","description":"Image on card 16 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/5cd0712a041701c7.jpg"},{"id":"image17","type":"image","role":"content","label":"Card image 17","description":"Image on card 17 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/e92ca3af976fb9bf.jpg"},{"id":"image18","type":"image","role":"content","label":"Card image 18","description":"Image on card 18 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/9e4f27a7562be618.jpg"},{"id":"image19","type":"image","role":"content","label":"Card image 19","description":"Image on card 19 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/4d4530bd67c951b4.jpg"},{"id":"image20","type":"image","role":"content","label":"Card image 20","description":"Image on card 20 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/28b5685774692ce0.jpg"},{"id":"image21","type":"image","role":"content","label":"Card image 21","description":"Image on card 21 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/368947470dbc62d2.jpg"},{"id":"image22","type":"image","role":"content","label":"Card image 22","description":"Image on card 22 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/28b5685774692ce0.jpg"},{"id":"image23","type":"image","role":"content","label":"Card image 23","description":"Image on card 23 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/c7e4be702908c77a.jpg"},{"id":"image24","type":"image","role":"content","label":"Card image 24","description":"Image on card 24 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"https://static.heygen.ai/hyperframes-oss/registry-assets/4ddd90caa12fe2bd.jpg"},{"id":"speed","type":"number","role":"style","label":"Speed","description":"Time scale of the carousel motion. 1 = the designed pace.","default":1,"min":0.25,"max":3,"step":0.05},{"id":"background","type":"color","role":"style","label":"Background","description":"Composition ground.","default":"#EDEDEF"}]}
|
||
>
|
||
|
||
```html carousel-path-4.html
|
||
<!doctype html>
|
||
<html
|
||
lang="en"
|
||
data-composition-variables='[{"id":"image1","type":"image","role":"content","label":"Card image 1","description":"Image on card 1 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-bob-seger.jpg"},{"id":"image2","type":"image","role":"content","label":"Card image 2","description":"Image on card 2 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-dire-straits.jpg"},{"id":"image3","type":"image","role":"content","label":"Card image 3","description":"Image on card 3 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-faces.jpg"},{"id":"image4","type":"image","role":"content","label":"Card image 4","description":"Image on card 4 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-little-feat.jpg"},{"id":"image5","type":"image","role":"content","label":"Card image 5","description":"Image on card 5 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-north-mississippi-allstars.jpg"},{"id":"image6","type":"image","role":"content","label":"Card image 6","description":"Image on card 6 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-sea-level.jpg"},{"id":"image7","type":"image","role":"content","label":"Card image 7","description":"Image on card 7 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-taj-mahal.jpg"},{"id":"image8","type":"image","role":"content","label":"Card image 8","description":"Image on card 8 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-tool.jpg"},{"id":"image9","type":"image","role":"content","label":"Card image 9","description":"Image on card 9 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-van-morrison.jpg"},{"id":"image10","type":"image","role":"content","label":"Card image 10","description":"Image on card 10 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-10-songs.jpg"},{"id":"image11","type":"image","role":"content","label":"Card image 11","description":"Image on card 11 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-bangers.jpg"},{"id":"image12","type":"image","role":"content","label":"Card image 12","description":"Image on card 12 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-bhindi-bhagee.jpg"},{"id":"image13","type":"image","role":"content","label":"Card image 13","description":"Image on card 13 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-chb-2.jpg"},{"id":"image14","type":"image","role":"content","label":"Card image 14","description":"Image on card 14 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-chb.jpg"},{"id":"image15","type":"image","role":"content","label":"Card image 15","description":"Image on card 15 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-homework.jpg"},{"id":"image16","type":"image","role":"content","label":"Card image 16","description":"Image on card 16 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-sleeping.jpg"},{"id":"image17","type":"image","role":"content","label":"Card image 17","description":"Image on card 17 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-y2k.jpg"},{"id":"image18","type":"image","role":"content","label":"Card image 18","description":"Image on card 18 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-back-together.jpg"},{"id":"image19","type":"image","role":"content","label":"Card image 19","description":"Image on card 19 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-bitter-with-the-sweet.jpg"},{"id":"image20","type":"image","role":"content","label":"Card image 20","description":"Image on card 20 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-cleaning-windows.jpg"},{"id":"image21","type":"image","role":"content","label":"Card image 21","description":"Image on card 21 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-dixie-chicken-live-at-the-rainbow-theatre-london-u.jpg"},{"id":"image22","type":"image","role":"content","label":"Card image 22","description":"Image on card 22 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-dweller-on-the-threshold.jpg"},{"id":"image23","type":"image","role":"content","label":"Card image 23","description":"Image on card 23 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-far-east-man.jpg"},{"id":"image24","type":"image","role":"content","label":"Card image 24","description":"Image on card 24 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-funky-shit.jpg"},{"id":"speed","type":"number","role":"style","label":"Speed","description":"Time scale of the carousel motion. 1 = the designed pace.","default":1,"min":0.25,"max":3,"step":0.05},{"id":"background","type":"color","role":"style","label":"Background","description":"Composition ground.","default":"#EDEDEF"}]'
|
||
>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>Image Carousel · Path 4</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||
<style>
|
||
body {
|
||
margin: 0;
|
||
background: #ededef;
|
||
}
|
||
#root {
|
||
position: relative;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
background: var(--background, #ededef);
|
||
}
|
||
.scaler {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 3840px;
|
||
height: 2160px;
|
||
transform-origin: 0 0;
|
||
}
|
||
.clip {
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
.stage {
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
.stage.is-3d {
|
||
transform-style: preserve-3d;
|
||
}
|
||
.card {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
will-change: transform;
|
||
backface-visibility: visible;
|
||
}
|
||
.card-inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: var(--card-radius);
|
||
overflow: hidden;
|
||
box-shadow: var(--card-shadow);
|
||
background: #fff;
|
||
}
|
||
.card-inner img {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.textline {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-family: "Inter", system-ui, sans-serif;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
white-space: nowrap;
|
||
pointer-events: none;
|
||
}
|
||
.textline.above.cards-over-right {
|
||
clip-path: inset(0 50% 0 0);
|
||
}
|
||
.textline.above.cards-over-left {
|
||
clip-path: inset(0 0 0 50%);
|
||
}
|
||
.textdrift {
|
||
display: flex;
|
||
align-items: center;
|
||
will-change: transform;
|
||
}
|
||
.word {
|
||
display: inline-block;
|
||
overflow: hidden;
|
||
padding: 0.06em 0;
|
||
}
|
||
.word + .word {
|
||
margin-left: 0.26em;
|
||
}
|
||
.word > span {
|
||
display: inline-block;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div
|
||
data-hf-id="hf-0m1b"
|
||
id="root"
|
||
data-composition-id="carousel-path-4"
|
||
data-start="0"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
data-duration="6"
|
||
data-fps="30"
|
||
data-layout-allow-overflow=""
|
||
>
|
||
<div id="carousel-path-4-partA" class="clip" data-start="0" data-duration="6">
|
||
<div data-hf-id="hf-elyu" id="carousel-path-4-scaler" class="scaler">
|
||
<div data-hf-id="hf-mi5e" id="carousel-path-4-stage" class="stage"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function (global) {
|
||
"use strict";
|
||
|
||
const lerp = (a, b, t) => a + (b - a) * t;
|
||
const clamp01 = (t) => (t < 0 ? 0 : t > 1 ? 1 : t);
|
||
|
||
const aeLinear = (v, a, b, c, d) => (a === b ? c : lerp(c, d, clamp01((v - a) / (b - a))));
|
||
|
||
const aeEase = (v, a, b, c, d) => {
|
||
const t = clamp01((v - a) / (b - a));
|
||
const e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||
return lerp(c, d, e);
|
||
};
|
||
const wrap01 = (x) => {
|
||
let p = x % 1;
|
||
if (p < 0) p += 1;
|
||
return p;
|
||
};
|
||
const DEG = 180 / Math.PI;
|
||
|
||
function buildPath(vertices, closed, samplesPerSeg, center) {
|
||
samplesPerSeg = samplesPerSeg || 64;
|
||
const n = vertices.length;
|
||
const segCount = closed ? n : n - 1;
|
||
const pts = [];
|
||
for (let s = 0; s < segCount; s++) {
|
||
const a = vertices[s],
|
||
b = vertices[(s + 1) % n];
|
||
const p0 = a.p,
|
||
p1 = [a.p[0] + a.out[0], a.p[1] + a.out[1]];
|
||
const p2 = [b.p[0] + b.in[0], b.p[1] + b.in[1]],
|
||
p3 = b.p;
|
||
const last = s === segCount - 1 ? samplesPerSeg : samplesPerSeg - 1;
|
||
for (let k = 0; k <= last; k++) {
|
||
const t = k / samplesPerSeg,
|
||
mt = 1 - t;
|
||
const x =
|
||
mt * mt * mt * p0[0] +
|
||
3 * mt * mt * t * p1[0] +
|
||
3 * mt * t * t * p2[0] +
|
||
t * t * t * p3[0];
|
||
const y =
|
||
mt * mt * mt * p0[1] +
|
||
3 * mt * mt * t * p1[1] +
|
||
3 * mt * t * t * p2[1] +
|
||
t * t * t * p3[1];
|
||
pts.push([x, y]);
|
||
}
|
||
}
|
||
if (center) {
|
||
let x0 = Infinity,
|
||
x1 = -Infinity,
|
||
y0 = Infinity,
|
||
y1 = -Infinity;
|
||
for (const q of pts) {
|
||
x0 = Math.min(x0, q[0]);
|
||
x1 = Math.max(x1, q[0]);
|
||
y0 = Math.min(y0, q[1]);
|
||
y1 = Math.max(y1, q[1]);
|
||
}
|
||
const sx = center[0] - (x0 + x1) / 2,
|
||
sy = center[1] - (y0 + y1) / 2;
|
||
for (const q of pts) {
|
||
q[0] += sx;
|
||
q[1] += sy;
|
||
}
|
||
}
|
||
const cum = [0];
|
||
for (let i = 1; i < pts.length; i++) {
|
||
const dx = pts[i][0] - pts[i - 1][0],
|
||
dy = pts[i][1] - pts[i - 1][1];
|
||
cum.push(cum[i - 1] + Math.hypot(dx, dy));
|
||
}
|
||
const total = cum[cum.length - 1];
|
||
function locate(pct) {
|
||
const target = pct * total;
|
||
let lo = 0,
|
||
hi = cum.length - 1;
|
||
while (hi - lo > 1) {
|
||
const mid = (lo + hi) >> 1;
|
||
if (cum[mid] <= target) lo = mid;
|
||
else hi = mid;
|
||
}
|
||
const segLen = cum[hi] - cum[lo] || 1;
|
||
return { i: lo, j: hi, f: (target - cum[lo]) / segLen };
|
||
}
|
||
return {
|
||
closed,
|
||
length: total,
|
||
pointOnPath(pct) {
|
||
pct = closed ? wrap01(pct) : clamp01(pct);
|
||
const { i, j, f } = locate(pct);
|
||
return [lerp(pts[i][0], pts[j][0], f), lerp(pts[i][1], pts[j][1], f)];
|
||
},
|
||
tangentOnPath(pct) {
|
||
pct = closed ? wrap01(pct) : clamp01(pct);
|
||
const { i, j } = locate(pct);
|
||
const dx = pts[j][0] - pts[i][0],
|
||
dy = pts[j][1] - pts[i][1];
|
||
const l = Math.hypot(dx, dy) || 1;
|
||
return [dx / l, dy / l];
|
||
},
|
||
};
|
||
}
|
||
|
||
function pathRig(path, ctl, comp, n, u) {
|
||
const spc = ctl.spacing / 100;
|
||
const phase = ctl.phaseOffset / 100;
|
||
const manual = ctl.moveCarousel / 100;
|
||
const dAng = (ctl.depthAngle || 0) / DEG;
|
||
const rotAmt = (ctl.rotationFollow || 0) / 100;
|
||
const cx = comp.width / 2,
|
||
cy = comp.height / 2;
|
||
const maxDist = Math.hypot(cx, cy);
|
||
const out = new Array(n);
|
||
for (let idx = 0; idx < n; idx++) {
|
||
const off = (idx / n) * spc + phase;
|
||
const pct = wrap01(u + off + manual);
|
||
const pos = path.pointOnPath(pct);
|
||
|
||
const dx = pos[0] - cx,
|
||
dy = pos[1] - cy;
|
||
const projected = dx * Math.sin(dAng) + dy * Math.cos(dAng);
|
||
const norm = (projected / maxDist + 1) / 2;
|
||
const depthScale = aeLinear(norm, 0, 1, ctl.scaleMin, 100);
|
||
const mixed = aeLinear(ctl.scaleDepth / 100, 0, 1, 100, depthScale);
|
||
const scale = (mixed * (ctl.globalScale / 100)) / 100;
|
||
|
||
const tang = path.tangentOnPath(pct);
|
||
const rotZ = Math.atan2(tang[1], tang[0]) * DEG * rotAmt;
|
||
|
||
let x = pos[0],
|
||
y = pos[1],
|
||
z = 0;
|
||
if (ctl.parent) {
|
||
const rx = (ctl.xSlider || 0) / DEG,
|
||
ry = (ctl.ySlider || 0) / DEG;
|
||
const a = ctl.parent.anchor,
|
||
P = ctl.parent.position;
|
||
|
||
let l = [pos[0] - a[0], pos[1] - a[1], 0];
|
||
l = [
|
||
l[0] * Math.cos(ry) + l[2] * Math.sin(ry),
|
||
l[1],
|
||
-l[0] * Math.sin(ry) + l[2] * Math.cos(ry),
|
||
];
|
||
l = [
|
||
l[0],
|
||
l[1] * Math.cos(rx) - l[2] * Math.sin(rx),
|
||
l[1] * Math.sin(rx) + l[2] * Math.cos(rx),
|
||
];
|
||
x = P[0] + l[0];
|
||
y = P[1] + l[1];
|
||
z = (P[2] || 0) + l[2];
|
||
}
|
||
|
||
let rotY = 0,
|
||
rotX = 0;
|
||
if (ctl.parent) {
|
||
const camZ = -(comp.cameraDistance || comp.cameraZoom || 5669);
|
||
const vx = cx - x,
|
||
vy = cy - y,
|
||
vz = camZ - z;
|
||
rotY = Math.atan2(vx, -vz) * DEG;
|
||
rotX = -Math.atan2(vy, Math.hypot(vx, vz)) * DEG;
|
||
}
|
||
if (ctl.yAngle3d) {
|
||
const dist = Math.abs(x - cx);
|
||
let r = aeEase(dist, 0, comp.width * (700 / 1920), 0, ctl.yAngle3d);
|
||
if (ctl.mirrorY !== false && x < cx) r = -r;
|
||
rotY += r;
|
||
}
|
||
out[idx] = { x, y, z, scale, rotZ, rotY, rotX, pct, depth: norm };
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function orbitRig(ctl, comp, n, u) {
|
||
const r = ctl.radius;
|
||
const rawCnt = Math.max(1, ctl.elementCount);
|
||
const cnt = Math.floor(rawCnt);
|
||
const off = ctl.offset;
|
||
|
||
const st = ctl.spinTurns || { x: 0, y: 1, z: 0 };
|
||
const rx = ((ctl.rotationX || 0) + u * 360 * (st.x || 0)) / DEG;
|
||
const ry = ((ctl.rotationY || 0) + u * 360 * (st.y || 0)) / DEG;
|
||
const rz = ((ctl.rotationZ || 0) + u * 360 * (st.z || 0)) / DEG;
|
||
const cx = comp.width / 2,
|
||
cy = comp.height / 2;
|
||
const camZ = -(comp.cameraDistance || comp.cameraZoom || 2000);
|
||
const cd = Math.abs(camZ);
|
||
const ga = Math.PI * (3 - Math.sqrt(5));
|
||
const getP = (index, c) => {
|
||
const i = (((index - 0.5 + off) % c) + c) % c;
|
||
const inc = Math.acos(Math.max(-1, Math.min(1, 1 - 2 * (i / c))));
|
||
const azi = ga * i;
|
||
return [
|
||
Math.sin(inc) * Math.cos(azi) * r,
|
||
Math.cos(inc) * r,
|
||
Math.sin(inc) * Math.sin(azi) * r,
|
||
];
|
||
};
|
||
const out = new Array(n);
|
||
for (let k = 0; k < n; k++) {
|
||
const index = k + 1;
|
||
const a = getP(index, cnt),
|
||
b = getP(index, cnt + 1),
|
||
f = rawCnt % 1;
|
||
let p = [lerp(a[0], b[0], f), lerp(a[1], b[1], f), lerp(a[2], b[2], f)];
|
||
|
||
p = [
|
||
p[0],
|
||
p[1] * Math.cos(rx) - p[2] * Math.sin(rx),
|
||
p[1] * Math.sin(rx) + p[2] * Math.cos(rx),
|
||
];
|
||
p = [
|
||
p[0] * Math.cos(ry) + p[2] * Math.sin(ry),
|
||
p[1],
|
||
-p[0] * Math.sin(ry) + p[2] * Math.cos(ry),
|
||
];
|
||
p = [
|
||
p[0] * Math.cos(rz) - p[1] * Math.sin(rz),
|
||
p[0] * Math.sin(rz) + p[1] * Math.cos(rz),
|
||
p[2],
|
||
];
|
||
const jitter = 1 + (index % 2) * 0.01;
|
||
const x = cx + p[0] * jitter,
|
||
y = cy + p[1] * jitter,
|
||
z = p[2] * jitter;
|
||
|
||
const d = Math.hypot(x - cx, y - cy, z - camZ);
|
||
const t = clamp01(aeLinear(d, cd - r, cd + r, 0, 1));
|
||
const s = aeLinear(t, 0, 1, ctl.scaleMax, ctl.scaleMin);
|
||
const finalS = aeLinear(ctl.perspectiveBoost, 0, 100, 100, s);
|
||
|
||
const behindCam = z <= camZ * 0.92;
|
||
|
||
const vx = cx - x,
|
||
vy = cy - y,
|
||
vz = camZ - z;
|
||
const yaw = Math.atan2(vx, -vz) * DEG;
|
||
const pitch = -Math.atan2(vy, Math.hypot(vx, vz)) * DEG;
|
||
out[k] = {
|
||
x,
|
||
y,
|
||
z,
|
||
scale: finalS / 100,
|
||
rotZ: 0,
|
||
rotY: yaw,
|
||
rotX: pitch,
|
||
visible: index <= cnt && !behindCam,
|
||
depth: 1 - t,
|
||
};
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function aeCurve(keys) {
|
||
const segs = [];
|
||
for (let i = 0; i < keys.length - 1; i++) {
|
||
const a = keys[i],
|
||
b = keys[i + 1],
|
||
T = b.time - a.time;
|
||
const linear = !a.easeOut || !b.easeIn || (a.flags && a.flags.startsWith("0101"));
|
||
let P1, P2;
|
||
if (linear) {
|
||
P1 = [a.time + T / 3, a.value + (b.value - a.value) / 3];
|
||
P2 = [a.time + (2 * T) / 3, a.value + (2 * (b.value - a.value)) / 3];
|
||
} else {
|
||
const lo = a.easeOut.influence * T,
|
||
li = b.easeIn.influence * T;
|
||
P1 = [a.time + lo, a.value + a.easeOut.speed * lo];
|
||
P2 = [b.time - li, b.value - b.easeIn.speed * li];
|
||
}
|
||
segs.push({
|
||
t0: a.time,
|
||
t1: b.time,
|
||
P0: [a.time, a.value],
|
||
P1,
|
||
P2,
|
||
P3: [b.time, b.value],
|
||
});
|
||
}
|
||
const bez = (p0, p1, p2, p3, s) => {
|
||
const m = 1 - s;
|
||
return m * m * m * p0 + 3 * m * m * s * p1 + 3 * m * s * s * p2 + s * s * s * p3;
|
||
};
|
||
return {
|
||
first: keys[0].value,
|
||
last: keys[keys.length - 1].value,
|
||
duration: keys[keys.length - 1].time - keys[0].time,
|
||
at(t) {
|
||
if (t <= segs[0].t0) return keys[0].value;
|
||
const sg = segs.find((g) => t <= g.t1) || segs[segs.length - 1];
|
||
if (t >= sg.t1) return sg.P3[1];
|
||
|
||
let s = (t - sg.t0) / (sg.t1 - sg.t0);
|
||
for (let k = 0; k < 8; k++) {
|
||
const x = bez(sg.P0[0], sg.P1[0], sg.P2[0], sg.P3[0], s) - t;
|
||
const m = 1 - s;
|
||
const dx =
|
||
3 * m * m * (sg.P1[0] - sg.P0[0]) +
|
||
6 * m * s * (sg.P2[0] - sg.P1[0]) +
|
||
3 * s * s * (sg.P3[0] - sg.P2[0]);
|
||
if (Math.abs(dx) < 1e-9) break;
|
||
s -= x / dx;
|
||
s = s < 0 ? 0 : s > 1 ? 1 : s;
|
||
}
|
||
return bez(sg.P0[1], sg.P1[1], sg.P2[1], sg.P3[1], s);
|
||
},
|
||
};
|
||
}
|
||
|
||
function chainedDrive(keys, mode) {
|
||
const c = aeCurve(keys),
|
||
T = c.duration,
|
||
delta = c.last - c.first;
|
||
return {
|
||
cycle: T,
|
||
at(t) {
|
||
if (T <= 0) return c.first;
|
||
const k = Math.floor(t / T),
|
||
r = t - k * T;
|
||
if (mode === "pingpong")
|
||
return k % 2 === 0 ? c.at(keys[0].time + r) : c.at(keys[0].time + (T - r));
|
||
return c.at(keys[0].time + r) + k * delta;
|
||
},
|
||
};
|
||
}
|
||
|
||
function buildProgress(opts) {
|
||
const dur = opts.duration,
|
||
cruise = opts.cruise;
|
||
const sign = cruise < 0 ? -1 : 1;
|
||
const c = Math.abs(cruise);
|
||
const cap = opts.boostCap == null ? 0.5 : opts.boostCap;
|
||
const vIn = Math.max(
|
||
Math.min(c * (opts.arriveMul || 4), c + cap),
|
||
opts.arriveMin || 0.16,
|
||
);
|
||
const vOut = Math.max(Math.min(c * (opts.exitMul || 6), c + cap), opts.exitMin || 0.22);
|
||
const tA = opts.arrive || 1.6,
|
||
tX = opts.exit || 0.9;
|
||
const N = Math.max(600, Math.round(dur * 120));
|
||
const lut = new Float64Array(N + 1);
|
||
let acc = 0,
|
||
prevV = null;
|
||
for (let i = 0; i <= N; i++) {
|
||
const t = (i / N) * dur;
|
||
let v;
|
||
if (t < tA) {
|
||
const k = 1 - Math.pow(1 - t / tA, 4);
|
||
v = lerp(vIn, c, k);
|
||
} else if (t > dur - tX) {
|
||
const k = Math.pow((t - (dur - tX)) / tX, 3);
|
||
v = lerp(c, vOut, k);
|
||
} else v = c;
|
||
if (prevV !== null) acc += ((prevV + v) / 2) * (dur / N);
|
||
prevV = v;
|
||
lut[i] = acc * sign;
|
||
}
|
||
return {
|
||
duration: dur,
|
||
travel: lut[N],
|
||
at(t) {
|
||
const f = clamp01(t / dur) * N,
|
||
i = Math.floor(f),
|
||
g = f - i;
|
||
return i >= N ? lut[N] : lerp(lut[i], lut[i + 1], g);
|
||
},
|
||
};
|
||
}
|
||
|
||
global.CarouselRig = {
|
||
buildPath,
|
||
pathRig,
|
||
orbitRig,
|
||
buildProgress,
|
||
aeCurve,
|
||
chainedDrive,
|
||
aeLinear,
|
||
aeEase,
|
||
};
|
||
})(typeof window !== "undefined" ? window : globalThis);
|
||
</script>
|
||
<script>
|
||
const DATA = {
|
||
id: "carousel-path-4",
|
||
family: "path",
|
||
controls: {
|
||
speed: 100,
|
||
spacing: 100,
|
||
phaseOffset: 0,
|
||
globalScale: 53.9,
|
||
scaleDepth: 18.6,
|
||
scaleMin: -663,
|
||
depthAngle: 183.76,
|
||
moveCarousel: 49.1,
|
||
rotationFollow: 57,
|
||
},
|
||
comp: {
|
||
width: 3840,
|
||
height: 2160,
|
||
outWidth: 1920,
|
||
outHeight: 1080,
|
||
cameraZoom: 5669.29,
|
||
cameraDistance: 5669.29,
|
||
duration: 6,
|
||
fps: 30,
|
||
},
|
||
n: 24,
|
||
cards: [
|
||
{ file: "artist-bob-seger.jpg", width: 640, height: 640 },
|
||
{ file: "artist-dire-straits.jpg", width: 640, height: 640 },
|
||
{ file: "artist-faces.jpg", width: 640, height: 500 },
|
||
{ file: "artist-little-feat.jpg", width: 640, height: 640 },
|
||
{ file: "artist-north-mississippi-allstars.jpg", width: 640, height: 640 },
|
||
{ file: "artist-sea-level.jpg", width: 640, height: 495 },
|
||
{ file: "artist-taj-mahal.jpg", width: 640, height: 640 },
|
||
{ file: "artist-tool.jpg", width: 640, height: 640 },
|
||
{ file: "artist-van-morrison.jpg", width: 640, height: 640 },
|
||
{ file: "pl-10-songs.jpg", width: 640, height: 640 },
|
||
{ file: "pl-bangers.jpg", width: 640, height: 640 },
|
||
{ file: "pl-bhindi-bhagee.jpg", width: 640, height: 640 },
|
||
{ file: "pl-chb-2.jpg", width: 640, height: 640 },
|
||
{ file: "pl-chb.jpg", width: 640, height: 640 },
|
||
{ file: "pl-homework.jpg", width: 640, height: 640 },
|
||
{ file: "pl-sleeping.jpg", width: 640, height: 640 },
|
||
{ file: "pl-y2k.jpg", width: 640, height: 640 },
|
||
{ file: "track-back-together.jpg", width: 640, height: 640 },
|
||
{ file: "track-bitter-with-the-sweet.jpg", width: 640, height: 640 },
|
||
{ file: "track-cleaning-windows.jpg", width: 640, height: 640 },
|
||
{
|
||
file: "track-dixie-chicken-live-at-the-rainbow-theatre-london-u.jpg",
|
||
width: 640,
|
||
height: 640,
|
||
},
|
||
{ file: "track-dweller-on-the-threshold.jpg", width: 640, height: 640 },
|
||
{ file: "track-far-east-man.jpg", width: 640, height: 640 },
|
||
{ file: "track-funky-shit.jpg", width: 640, height: 640 },
|
||
],
|
||
path: {
|
||
vertices: [
|
||
{ p: [-168.4, 1484], in: [0, 0], out: [417.3, -68.9] },
|
||
{ p: [1071.4, 687.1], in: [-435, 0], out: [500.2, 0] },
|
||
{ p: [2468, 1345.9], in: [-704.4, 0], out: [726.9, 0] },
|
||
{ p: [4259.6, 680.5], in: [-348.5, -132.9], out: [0, 0] },
|
||
],
|
||
closed: false,
|
||
},
|
||
keys: {
|
||
"Depth Angle": [
|
||
{
|
||
time: 0,
|
||
value: 215.699997,
|
||
flags: "01010000",
|
||
easeIn: { speed: 0, influence: 0 },
|
||
easeOut: { speed: 0, influence: 0 },
|
||
},
|
||
{
|
||
time: 1.56667,
|
||
value: 160.099997,
|
||
flags: "01010000",
|
||
easeIn: { speed: 0, influence: 0 },
|
||
easeOut: { speed: 0, influence: 0.166667 },
|
||
},
|
||
],
|
||
"Move Carousel": [
|
||
{
|
||
time: 0,
|
||
value: 0,
|
||
flags: "02020000",
|
||
easeIn: { speed: 0, influence: 0.333333 },
|
||
easeOut: { speed: 266.290401, influence: 0.157005 },
|
||
},
|
||
{
|
||
time: 1.53333,
|
||
value: 80,
|
||
flags: "02020000",
|
||
easeIn: { speed: 205.452802, influence: 0.146587 },
|
||
easeOut: { speed: 0, influence: 0.333333 },
|
||
},
|
||
],
|
||
},
|
||
motion: {
|
||
arrive: 1.6,
|
||
arriveMul: 4,
|
||
arriveMin: 0.16,
|
||
exit: 0.9,
|
||
exitMul: 6,
|
||
exitMin: 0.22,
|
||
entryStaggerTotal: 0.45,
|
||
entryDuration: 0.55,
|
||
entryLead: 0.25,
|
||
orbitSpinDegPerSec: 24,
|
||
orbitSpinScale: 1,
|
||
boostCap: 0.5,
|
||
speedScale: 1,
|
||
},
|
||
look: {
|
||
background: "#EDEDEF",
|
||
cardRadius: 0,
|
||
cardShadow: "none",
|
||
imageHeight: null,
|
||
centerPath: true,
|
||
endFade: 0.06,
|
||
fitBox: 1500,
|
||
},
|
||
text: null,
|
||
ending: null,
|
||
outro: null,
|
||
ring: null,
|
||
registry: {
|
||
imageVars: [
|
||
"image1",
|
||
"image2",
|
||
"image3",
|
||
"image4",
|
||
"image5",
|
||
"image6",
|
||
"image7",
|
||
"image8",
|
||
"image9",
|
||
"image10",
|
||
"image11",
|
||
"image12",
|
||
"image13",
|
||
"image14",
|
||
"image15",
|
||
"image16",
|
||
"image17",
|
||
"image18",
|
||
"image19",
|
||
"image20",
|
||
"image21",
|
||
"image22",
|
||
"image23",
|
||
"image24",
|
||
],
|
||
textVars: null,
|
||
speedVar: "speed",
|
||
},
|
||
cutAt: null,
|
||
};
|
||
(async function () {
|
||
const R = window.CarouselRig;
|
||
|
||
const REG = DATA.registry || null;
|
||
const V =
|
||
REG && window.__hyperframes && window.__hyperframes.getVariables
|
||
? window.__hyperframes.getVariables() || {}
|
||
: {};
|
||
const varUrl = (v) => (v && typeof v === "object" ? v.url || v.src || "" : v || "");
|
||
if (REG) {
|
||
const srcs = REG.imageVars.map(
|
||
(id, i) => varUrl(V[id]) || DATA.cards[i % DATA.cards.length].file,
|
||
);
|
||
DATA.cards = await Promise.all(
|
||
srcs.map(async (src, i) => {
|
||
const img = new Image();
|
||
img.src = src;
|
||
try {
|
||
await img.decode();
|
||
} catch (e) {}
|
||
return {
|
||
file: src,
|
||
abs: true,
|
||
width: img.naturalWidth || DATA.cards[i % DATA.cards.length].width,
|
||
height: img.naturalHeight || DATA.cards[i % DATA.cards.length].height,
|
||
};
|
||
}),
|
||
);
|
||
if (DATA.text && REG.textVars && REG.textVars.line1 && V[REG.textVars.line1])
|
||
DATA.text.words = String(V[REG.textVars.line1]).trim().split(/\s+/);
|
||
if (DATA.outro && REG.textVars && REG.textVars.line2 && V[REG.textVars.line2])
|
||
DATA.outro.words = String(V[REG.textVars.line2]).trim().split(/\s+/);
|
||
if (REG.speedVar && V[REG.speedVar] != null && isFinite(+V[REG.speedVar]))
|
||
DATA.motion.speedScale = +V[REG.speedVar];
|
||
}
|
||
const { family, controls: ctl, comp, n, cards, motion, look } = DATA;
|
||
const root = document.getElementById("root");
|
||
const stage = document.getElementById(DATA.id + "-stage");
|
||
root.style.setProperty("--card-radius", look.cardRadius + "px");
|
||
root.style.setProperty("--card-shadow", look.cardShadow);
|
||
|
||
document.getElementById(DATA.id + "-scaler").style.transform =
|
||
"scale(" + comp.outWidth / comp.width + ")";
|
||
|
||
const is3d = family === "orbit" || family === "vision";
|
||
if (is3d) {
|
||
stage.style.perspective = comp.cameraZoom + "px";
|
||
stage.style.perspectiveOrigin = "50% 50%";
|
||
}
|
||
|
||
const camShift = (comp.cameraDistance || comp.cameraZoom) - comp.cameraZoom;
|
||
const group = document.createElement("div");
|
||
group.className = "stage" + (is3d ? " is-3d" : "");
|
||
group.id = DATA.id + "-group";
|
||
const gx = [];
|
||
if (look.zoom && look.zoom !== 1) gx.push("scale(" + look.zoom + ")");
|
||
if (camShift) gx.push("translateZ(" + -camShift + "px)");
|
||
if (gx.length) group.style.transform = gx.join(" ");
|
||
|
||
const squeeze = document.createElement("div");
|
||
squeeze.className = "stage" + (is3d ? " is-3d" : "");
|
||
squeeze.id = DATA.id + "-squeeze";
|
||
group.appendChild(squeeze);
|
||
|
||
const TXT = DATA.text || null;
|
||
const textLayers = [];
|
||
if (TXT) {
|
||
for (const cls of ["below", "above"]) {
|
||
const line = document.createElement("div");
|
||
line.className = "textline " + cls + " cards-over-" + (TXT.cardsOverOn || "right");
|
||
line.id = DATA.id + "-text-" + cls;
|
||
line.style.fontSize = TXT.fontSize + "px";
|
||
line.style.color = TXT.color || "#111";
|
||
const drift = document.createElement("div");
|
||
drift.className = "textdrift";
|
||
drift.id = DATA.id + "-drift-" + cls;
|
||
TXT.words.forEach((w, k) => {
|
||
const box = document.createElement("span");
|
||
box.className = "word";
|
||
const inner = document.createElement("span");
|
||
inner.textContent = w;
|
||
inner.id = DATA.id + "-w" + k + "-" + cls;
|
||
box.appendChild(inner);
|
||
drift.appendChild(box);
|
||
});
|
||
line.appendChild(drift);
|
||
textLayers.push(line);
|
||
}
|
||
stage.appendChild(textLayers[0]);
|
||
}
|
||
stage.appendChild(group);
|
||
if (TXT) stage.appendChild(textLayers[1]);
|
||
|
||
const els = [],
|
||
inners = [];
|
||
for (let i = 0; i < n; i++) {
|
||
const c = cards[i];
|
||
let w = c.width,
|
||
h = c.height;
|
||
if (look.imageHeight) {
|
||
w = c.width * (look.imageHeight / c.height);
|
||
h = look.imageHeight;
|
||
} else if (look.fitBox) {
|
||
const k = look.fitBox / Math.max(c.width, c.height);
|
||
w = c.width * k;
|
||
h = c.height * k;
|
||
}
|
||
const el = document.createElement("div");
|
||
el.className = "card";
|
||
el.id = DATA.id + "-card-" + i;
|
||
el.style.width = w + "px";
|
||
el.style.height = h + "px";
|
||
el.style.marginLeft = -w / 2 + "px";
|
||
el.style.marginTop = -h / 2 + "px";
|
||
const inner = document.createElement("div");
|
||
inner.className = "card-inner";
|
||
inner.id = el.id + "-inner";
|
||
const img = document.createElement("img");
|
||
img.src = c.abs ? c.file : "assets/carousel-images/" + c.file;
|
||
img.alt = "";
|
||
|
||
if (!is3d) el.style.zIndex = String(n - i);
|
||
inner.appendChild(img);
|
||
el.appendChild(inner);
|
||
squeeze.appendChild(el);
|
||
els.push(el);
|
||
inners.push(inner);
|
||
}
|
||
|
||
const path = DATA.path
|
||
? R.buildPath(
|
||
DATA.path.vertices,
|
||
DATA.path.closed,
|
||
64,
|
||
look.centerPath ? [comp.width / 2, comp.height / 2] : null,
|
||
)
|
||
: null;
|
||
|
||
const K = DATA.keys || {};
|
||
const drives = {};
|
||
for (const name in K) {
|
||
const cumulative =
|
||
/^(Move Carousel|Rotation [XYZ])$/.test(name) &&
|
||
Math.abs(K[name][K[name].length - 1].value - K[name][0].value) > 1e-6;
|
||
drives[name] = R.chainedDrive(K[name], cumulative ? "accumulate" : "pingpong");
|
||
}
|
||
const speedScale = motion.speedScale == null ? 1 : motion.speedScale;
|
||
const tt = (t) => t * speedScale;
|
||
const baseSpeed = (ctl.speed || 0) / 1000;
|
||
|
||
const fallbackCruise =
|
||
family === "orbit" ? (motion.orbitSpinDegPerSec || 24) / 360 : baseSpeed;
|
||
const prog = R.buildProgress(
|
||
Object.assign({ duration: comp.duration, cruise: fallbackCruise }, motion),
|
||
);
|
||
const hasMove = !!drives["Move Carousel"],
|
||
hasSpin = !!(drives["Rotation X"] || drives["Rotation Y"] || drives["Rotation Z"]);
|
||
function controlsAt(t) {
|
||
const c = Object.assign({}, ctl);
|
||
for (const name in drives) {
|
||
const v = drives[name].at(tt(t));
|
||
if (name === "Move Carousel") c.moveCarousel = v;
|
||
else if (name === "Depth Angle") c.depthAngle = v;
|
||
else if (name === "Rotation Follow") c.rotationFollow = v;
|
||
else if (name === "Rotation X") c.rotationX = v;
|
||
else if (name === "Rotation Y") c.rotationY = v;
|
||
else if (name === "Rotation Z") c.rotationZ = v;
|
||
}
|
||
if (family === "orbit") c.spinTurns = { x: 0, y: 0, z: 0 };
|
||
return c;
|
||
}
|
||
function uAt(t) {
|
||
if (family === "orbit") return hasSpin ? 0 : prog.at(t);
|
||
return hasMove ? baseSpeed * tt(t) : prog.at(t);
|
||
}
|
||
const endFade = path && !path.closed ? (look.endFade ?? 0.06) : 0;
|
||
|
||
const RING = DATA.ring || null;
|
||
const converge = RING && RING.entry === "converge";
|
||
const cvE = converge ? gsap.parseEase(RING.ease || "power3.out") : null;
|
||
let ringStartRef = 0;
|
||
function convergeFactor(i, t) {
|
||
const per = (RING.stagger || 0.5) / n,
|
||
s0 = ringStartRef + i * per,
|
||
d = RING.duration || 0.8;
|
||
const e = t <= s0 ? 0 : t >= s0 + d ? 1 : cvE((t - s0) / d);
|
||
return 1 + ((RING.from || 3) - 1) * (1 - e);
|
||
}
|
||
function render(t) {
|
||
const u = uAt(t),
|
||
c = controlsAt(t);
|
||
const poses =
|
||
family === "orbit" ? R.orbitRig(c, comp, n, u) : R.pathRig(path, c, comp, n, u);
|
||
const cx = comp.width / 2,
|
||
cy = comp.height / 2;
|
||
for (let i = 0; i < n; i++) {
|
||
const p = poses[i],
|
||
el = els[i];
|
||
if (converge) {
|
||
const f = convergeFactor(i, t);
|
||
p.x = cx + (p.x - cx) * f;
|
||
p.y = cy + (p.y - cy) * f;
|
||
}
|
||
let alpha = p.visible === false ? 0 : 1;
|
||
if (endFade) {
|
||
const e = Math.min(p.pct, 1 - p.pct) / endFade;
|
||
alpha *= e < 1 ? e : 1;
|
||
}
|
||
el.style.opacity = alpha;
|
||
el.style.transform =
|
||
"translate3d(" +
|
||
p.x.toFixed(2) +
|
||
"px," +
|
||
p.y.toFixed(2) +
|
||
"px," +
|
||
(-p.z).toFixed(2) +
|
||
"px)" +
|
||
(p.rotY ? " rotateY(" + p.rotY.toFixed(3) + "deg)" : "") +
|
||
(p.rotX ? " rotateX(" + p.rotX.toFixed(3) + "deg)" : "") +
|
||
(p.rotZ ? " rotateZ(" + p.rotZ.toFixed(3) + "deg)" : "") +
|
||
" scale(" +
|
||
p.scale.toFixed(5) +
|
||
")";
|
||
}
|
||
}
|
||
|
||
const clock = { t: 0 };
|
||
const tl = gsap.timeline({ paused: true });
|
||
tl.fromTo(
|
||
clock,
|
||
{ t: 0 },
|
||
{
|
||
t: comp.duration,
|
||
duration: comp.duration,
|
||
ease: "none",
|
||
onUpdate: () => render(clock.t),
|
||
},
|
||
0,
|
||
);
|
||
|
||
let ringStart = 0;
|
||
if (TXT) {
|
||
const wd = TXT.wordDuration,
|
||
ws = TXT.wordStagger;
|
||
TXT.words.forEach((w, k) => {
|
||
const targets = textLayers.map((L) => L.firstChild.children[k].firstChild);
|
||
tl.fromTo(
|
||
targets,
|
||
{ xPercent: 110 },
|
||
{ xPercent: 0, duration: wd, ease: "power3.out", immediateRender: true },
|
||
k * ws,
|
||
);
|
||
});
|
||
ringStart = (TXT.words.length - 1) * ws;
|
||
|
||
if (TXT.driftPx) {
|
||
const d0 = wd * (TXT.driftOverlapAt == null ? 0.61 : TXT.driftOverlapAt);
|
||
const px = TXT.driftPx * (comp.width / comp.outWidth);
|
||
|
||
const DE = TXT.driftEase || { power: 1, linear: 1 };
|
||
const k = DE.linear == null ? 0.35 : DE.linear,
|
||
pw = DE.power || 3;
|
||
const driftEase = (x) => (1 - k) * (1 - Math.pow(1 - x, pw)) + k * x;
|
||
|
||
const xFrom = TXT.driftEndsCentered ? px : 0,
|
||
xTo = TXT.driftEndsCentered ? 0 : -px;
|
||
tl.fromTo(
|
||
textLayers.map((L) => L.firstChild),
|
||
{ x: xFrom },
|
||
{
|
||
x: xTo,
|
||
duration: (DATA.cutAt || comp.duration) - d0,
|
||
ease: driftEase,
|
||
immediateRender: true,
|
||
},
|
||
d0,
|
||
);
|
||
}
|
||
}
|
||
ringStartRef = ringStart;
|
||
const each = motion.entryStaggerTotal / n,
|
||
lead = TXT ? 0 : motion.entryLead || 0,
|
||
eDur = motion.entryDuration;
|
||
const E = gsap.parseEase("power3.out");
|
||
for (let i = 0; i < n; i++) {
|
||
if (converge) break;
|
||
const start = ringStart + i * each - lead;
|
||
if (start >= 0) {
|
||
tl.fromTo(
|
||
inners[i],
|
||
{ autoAlpha: 0, scale: 0.72 },
|
||
{ autoAlpha: 1, scale: 1, duration: eDur, ease: "power3.out", immediateRender: true },
|
||
start,
|
||
);
|
||
continue;
|
||
}
|
||
if (start + eDur <= 0) continue;
|
||
|
||
const p0 = -start / eDur,
|
||
e0 = E(p0);
|
||
const tail = (x) => (E(p0 + x * (1 - p0)) - e0) / (1 - e0);
|
||
tl.fromTo(
|
||
inners[i],
|
||
{ autoAlpha: e0, scale: 0.72 + 0.28 * e0 },
|
||
{ autoAlpha: 1, scale: 1, duration: eDur - -start, ease: tail, immediateRender: true },
|
||
0,
|
||
);
|
||
}
|
||
|
||
const END = DATA.ending || null;
|
||
const cutAt = DATA.cutAt || comp.duration;
|
||
if (END) {
|
||
const tA = END.textScaleAt,
|
||
tB = tA + (END.imagesDelay == null ? 0.2 : END.imagesDelay);
|
||
if (TXT && END.textScaleTo != null)
|
||
tl.fromTo(
|
||
textLayers,
|
||
{ scale: 1 },
|
||
{
|
||
scale: END.textScaleTo,
|
||
duration: cutAt - tA,
|
||
ease: END.ease || "power2.in",
|
||
immediateRender: true,
|
||
},
|
||
tA,
|
||
);
|
||
if (END.imagesTo != null)
|
||
tl.fromTo(
|
||
squeeze,
|
||
{ scale: 1 },
|
||
{
|
||
scale: END.imagesTo,
|
||
duration: cutAt - tB,
|
||
ease: END.ease || "power2.in",
|
||
immediateRender: true,
|
||
},
|
||
tB,
|
||
);
|
||
}
|
||
|
||
const OUT = DATA.outro || null;
|
||
if (OUT) {
|
||
const line = document.getElementById(DATA.id + "-outro-line");
|
||
const driftEl = document.getElementById(DATA.id + "-outro-drift");
|
||
line.style.fontSize = (OUT.fontSize || (TXT && TXT.fontSize) || 190) + "px";
|
||
line.style.color = OUT.color || (TXT && TXT.color) || "#111";
|
||
OUT.words.forEach((w) => {
|
||
const s = document.createElement("span");
|
||
s.className = "word";
|
||
s.textContent = w;
|
||
driftEl.appendChild(s);
|
||
});
|
||
const sd = OUT.scaleDuration || 0.8,
|
||
outEnd = comp.duration;
|
||
tl.fromTo(
|
||
line,
|
||
{ scale: OUT.scaleFrom == null ? 1.4 : OUT.scaleFrom },
|
||
{
|
||
scale: OUT.scaleTo == null ? 1 : OUT.scaleTo,
|
||
duration: sd,
|
||
ease: OUT.ease || "power2.out",
|
||
immediateRender: true,
|
||
},
|
||
cutAt,
|
||
);
|
||
if (OUT.driftPx) {
|
||
const d0 = cutAt + sd * (OUT.driftOverlapAt == null ? 0.61 : OUT.driftOverlapAt);
|
||
const px = OUT.driftPx * (comp.width / comp.outWidth);
|
||
tl.fromTo(
|
||
driftEl,
|
||
{ x: 0 },
|
||
{ x: -px, duration: outEnd - d0, ease: "none", immediateRender: true },
|
||
d0,
|
||
);
|
||
}
|
||
}
|
||
render(0);
|
||
window.__timelines[DATA.id] = tl;
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
</VariablesExplorer>
|
||
|
||
## Install
|
||
|
||
<InstallCommand command="npx hyperframes add carousel-path-4" item="carousel-path-4" />
|
||
|
||
That writes `compositions/carousel-path-4.html`, plus 24 supporting files under `assets/carousel-images/`.
|
||
|
||
## Add it to your video
|
||
|
||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||
|
||
```html index.html
|
||
<div
|
||
data-composition-id="carousel-path-4"
|
||
data-composition-src="compositions/carousel-path-4.html"
|
||
data-start="0"
|
||
data-duration="6"
|
||
data-track-index="1"
|
||
data-width="1920"
|
||
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 |
|
||
| --- | --- | --- | --- |
|
||
| `image1` | `assets/carousel-images/artist-bob-seger.jpg` | image | Image on card 1 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image2` | `assets/carousel-images/artist-dire-straits.jpg` | image | Image on card 2 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image3` | `assets/carousel-images/artist-faces.jpg` | image | Image on card 3 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image4` | `assets/carousel-images/artist-little-feat.jpg` | image | Image on card 4 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image5` | `assets/carousel-images/artist-north-mississippi-allstars.jpg` | image | Image on card 5 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image6` | `assets/carousel-images/artist-sea-level.jpg` | image | Image on card 6 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image7` | `assets/carousel-images/artist-taj-mahal.jpg` | image | Image on card 7 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image8` | `assets/carousel-images/artist-tool.jpg` | image | Image on card 8 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image9` | `assets/carousel-images/artist-van-morrison.jpg` | image | Image on card 9 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image10` | `assets/carousel-images/pl-10-songs.jpg` | image | Image on card 10 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image11` | `assets/carousel-images/pl-bangers.jpg` | image | Image on card 11 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image12` | `assets/carousel-images/pl-bhindi-bhagee.jpg` | image | Image on card 12 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image13` | `assets/carousel-images/pl-chb-2.jpg` | image | Image on card 13 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image14` | `assets/carousel-images/pl-chb.jpg` | image | Image on card 14 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image15` | `assets/carousel-images/pl-homework.jpg` | image | Image on card 15 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image16` | `assets/carousel-images/pl-sleeping.jpg` | image | Image on card 16 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image17` | `assets/carousel-images/pl-y2k.jpg` | image | Image on card 17 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image18` | `assets/carousel-images/track-back-together.jpg` | image | Image on card 18 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image19` | `assets/carousel-images/track-bitter-with-the-sweet.jpg` | image | Image on card 19 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image20` | `assets/carousel-images/track-cleaning-windows.jpg` | image | Image on card 20 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image21` | `assets/carousel-images/track-dixie-chicken-live-at-the-rainbow-theatre-london-u.jpg` | image | Image on card 21 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image22` | `assets/carousel-images/track-dweller-on-the-threshold.jpg` | image | Image on card 22 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image23` | `assets/carousel-images/track-far-east-man.jpg` | image | Image on card 23 of 24. Any aspect; it is fitted into the card box. |
|
||
| `image24` | `assets/carousel-images/track-funky-shit.jpg` | image | Image on card 24 of 24. Any aspect; it is fitted into the card box. |
|
||
| `speed` | `1` | 0.25 to 3, step 0.05 | Time scale of the carousel motion. 1 = the designed pace. |
|
||
| `background` | `#EDEDEF` | color | Composition ground. |
|
||
|
||
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="carousel-path-4"
|
||
data-composition-src="compositions/carousel-path-4.html"
|
||
data-variable-values='{"image1":"assets/carousel-images/artist-bob-seger.jpg","image2":"assets/carousel-images/artist-dire-straits.jpg","image3":"assets/carousel-images/artist-faces.jpg","image4":"assets/carousel-images/artist-little-feat.jpg","image5":"assets/carousel-images/artist-north-mississippi-allstars.jpg","image6":"assets/carousel-images/artist-sea-level.jpg","image7":"assets/carousel-images/artist-taj-mahal.jpg","image8":"assets/carousel-images/artist-tool.jpg","image9":"assets/carousel-images/artist-van-morrison.jpg","image10":"assets/carousel-images/pl-10-songs.jpg","image11":"assets/carousel-images/pl-bangers.jpg","image12":"assets/carousel-images/pl-bhindi-bhagee.jpg","image13":"assets/carousel-images/pl-chb-2.jpg","image14":"assets/carousel-images/pl-chb.jpg","image15":"assets/carousel-images/pl-homework.jpg","image16":"assets/carousel-images/pl-sleeping.jpg","image17":"assets/carousel-images/pl-y2k.jpg","image18":"assets/carousel-images/track-back-together.jpg","image19":"assets/carousel-images/track-bitter-with-the-sweet.jpg","image20":"assets/carousel-images/track-cleaning-windows.jpg","image21":"assets/carousel-images/track-dixie-chicken-live-at-the-rainbow-theatre-london-u.jpg","image22":"assets/carousel-images/track-dweller-on-the-threshold.jpg","image23":"assets/carousel-images/track-far-east-man.jpg","image24":"assets/carousel-images/track-funky-shit.jpg","speed":1,"background":"#EDEDEF"}'
|
||
></div>
|
||
```
|
||
|
||
## Source
|
||
|
||
<Accordion title={`carousel-path-4.html`}>
|
||
|
||
```html
|
||
<!doctype html>
|
||
<html
|
||
lang="en"
|
||
data-composition-variables='[{"id":"image1","type":"image","role":"content","label":"Card image 1","description":"Image on card 1 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-bob-seger.jpg"},{"id":"image2","type":"image","role":"content","label":"Card image 2","description":"Image on card 2 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-dire-straits.jpg"},{"id":"image3","type":"image","role":"content","label":"Card image 3","description":"Image on card 3 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-faces.jpg"},{"id":"image4","type":"image","role":"content","label":"Card image 4","description":"Image on card 4 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-little-feat.jpg"},{"id":"image5","type":"image","role":"content","label":"Card image 5","description":"Image on card 5 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-north-mississippi-allstars.jpg"},{"id":"image6","type":"image","role":"content","label":"Card image 6","description":"Image on card 6 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-sea-level.jpg"},{"id":"image7","type":"image","role":"content","label":"Card image 7","description":"Image on card 7 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-taj-mahal.jpg"},{"id":"image8","type":"image","role":"content","label":"Card image 8","description":"Image on card 8 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-tool.jpg"},{"id":"image9","type":"image","role":"content","label":"Card image 9","description":"Image on card 9 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/artist-van-morrison.jpg"},{"id":"image10","type":"image","role":"content","label":"Card image 10","description":"Image on card 10 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-10-songs.jpg"},{"id":"image11","type":"image","role":"content","label":"Card image 11","description":"Image on card 11 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-bangers.jpg"},{"id":"image12","type":"image","role":"content","label":"Card image 12","description":"Image on card 12 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-bhindi-bhagee.jpg"},{"id":"image13","type":"image","role":"content","label":"Card image 13","description":"Image on card 13 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-chb-2.jpg"},{"id":"image14","type":"image","role":"content","label":"Card image 14","description":"Image on card 14 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-chb.jpg"},{"id":"image15","type":"image","role":"content","label":"Card image 15","description":"Image on card 15 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-homework.jpg"},{"id":"image16","type":"image","role":"content","label":"Card image 16","description":"Image on card 16 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-sleeping.jpg"},{"id":"image17","type":"image","role":"content","label":"Card image 17","description":"Image on card 17 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/pl-y2k.jpg"},{"id":"image18","type":"image","role":"content","label":"Card image 18","description":"Image on card 18 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-back-together.jpg"},{"id":"image19","type":"image","role":"content","label":"Card image 19","description":"Image on card 19 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-bitter-with-the-sweet.jpg"},{"id":"image20","type":"image","role":"content","label":"Card image 20","description":"Image on card 20 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-cleaning-windows.jpg"},{"id":"image21","type":"image","role":"content","label":"Card image 21","description":"Image on card 21 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-dixie-chicken-live-at-the-rainbow-theatre-london-u.jpg"},{"id":"image22","type":"image","role":"content","label":"Card image 22","description":"Image on card 22 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-dweller-on-the-threshold.jpg"},{"id":"image23","type":"image","role":"content","label":"Card image 23","description":"Image on card 23 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-far-east-man.jpg"},{"id":"image24","type":"image","role":"content","label":"Card image 24","description":"Image on card 24 of 24. Any aspect; it is fitted into the card box.","portrays":["subject_image"],"default":"assets/carousel-images/track-funky-shit.jpg"},{"id":"speed","type":"number","role":"style","label":"Speed","description":"Time scale of the carousel motion. 1 = the designed pace.","default":1,"min":0.25,"max":3,"step":0.05},{"id":"background","type":"color","role":"style","label":"Background","description":"Composition ground.","default":"#EDEDEF"}]'
|
||
>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>Image Carousel · Path 4</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||
<style>
|
||
body {
|
||
margin: 0;
|
||
background: #ededef;
|
||
}
|
||
#root {
|
||
position: relative;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
background: var(--background, #ededef);
|
||
}
|
||
.scaler {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 3840px;
|
||
height: 2160px;
|
||
transform-origin: 0 0;
|
||
}
|
||
.clip {
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
.stage {
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
.stage.is-3d {
|
||
transform-style: preserve-3d;
|
||
}
|
||
.card {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
will-change: transform;
|
||
backface-visibility: visible;
|
||
}
|
||
.card-inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: var(--card-radius);
|
||
overflow: hidden;
|
||
box-shadow: var(--card-shadow);
|
||
background: #fff;
|
||
}
|
||
.card-inner img {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.textline {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-family: "Inter", system-ui, sans-serif;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
white-space: nowrap;
|
||
pointer-events: none;
|
||
}
|
||
.textline.above.cards-over-right {
|
||
clip-path: inset(0 50% 0 0);
|
||
}
|
||
.textline.above.cards-over-left {
|
||
clip-path: inset(0 0 0 50%);
|
||
}
|
||
.textdrift {
|
||
display: flex;
|
||
align-items: center;
|
||
will-change: transform;
|
||
}
|
||
.word {
|
||
display: inline-block;
|
||
overflow: hidden;
|
||
padding: 0.06em 0;
|
||
}
|
||
.word + .word {
|
||
margin-left: 0.26em;
|
||
}
|
||
.word > span {
|
||
display: inline-block;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div
|
||
data-hf-id="hf-0m1b"
|
||
id="root"
|
||
data-composition-id="carousel-path-4"
|
||
data-start="0"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
data-duration="6"
|
||
data-fps="30"
|
||
data-layout-allow-overflow=""
|
||
>
|
||
<div id="carousel-path-4-partA" class="clip" data-start="0" data-duration="6">
|
||
<div data-hf-id="hf-elyu" id="carousel-path-4-scaler" class="scaler">
|
||
<div data-hf-id="hf-mi5e" id="carousel-path-4-stage" class="stage"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function (global) {
|
||
"use strict";
|
||
|
||
const lerp = (a, b, t) => a + (b - a) * t;
|
||
const clamp01 = (t) => (t < 0 ? 0 : t > 1 ? 1 : t);
|
||
|
||
const aeLinear = (v, a, b, c, d) => (a === b ? c : lerp(c, d, clamp01((v - a) / (b - a))));
|
||
|
||
const aeEase = (v, a, b, c, d) => {
|
||
const t = clamp01((v - a) / (b - a));
|
||
const e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||
return lerp(c, d, e);
|
||
};
|
||
const wrap01 = (x) => {
|
||
let p = x % 1;
|
||
if (p < 0) p += 1;
|
||
return p;
|
||
};
|
||
const DEG = 180 / Math.PI;
|
||
|
||
function buildPath(vertices, closed, samplesPerSeg, center) {
|
||
samplesPerSeg = samplesPerSeg || 64;
|
||
const n = vertices.length;
|
||
const segCount = closed ? n : n - 1;
|
||
const pts = [];
|
||
for (let s = 0; s < segCount; s++) {
|
||
const a = vertices[s],
|
||
b = vertices[(s + 1) % n];
|
||
const p0 = a.p,
|
||
p1 = [a.p[0] + a.out[0], a.p[1] + a.out[1]];
|
||
const p2 = [b.p[0] + b.in[0], b.p[1] + b.in[1]],
|
||
p3 = b.p;
|
||
const last = s === segCount - 1 ? samplesPerSeg : samplesPerSeg - 1;
|
||
for (let k = 0; k <= last; k++) {
|
||
const t = k / samplesPerSeg,
|
||
mt = 1 - t;
|
||
const x =
|
||
mt * mt * mt * p0[0] +
|
||
3 * mt * mt * t * p1[0] +
|
||
3 * mt * t * t * p2[0] +
|
||
t * t * t * p3[0];
|
||
const y =
|
||
mt * mt * mt * p0[1] +
|
||
3 * mt * mt * t * p1[1] +
|
||
3 * mt * t * t * p2[1] +
|
||
t * t * t * p3[1];
|
||
pts.push([x, y]);
|
||
}
|
||
}
|
||
if (center) {
|
||
let x0 = Infinity,
|
||
x1 = -Infinity,
|
||
y0 = Infinity,
|
||
y1 = -Infinity;
|
||
for (const q of pts) {
|
||
x0 = Math.min(x0, q[0]);
|
||
x1 = Math.max(x1, q[0]);
|
||
y0 = Math.min(y0, q[1]);
|
||
y1 = Math.max(y1, q[1]);
|
||
}
|
||
const sx = center[0] - (x0 + x1) / 2,
|
||
sy = center[1] - (y0 + y1) / 2;
|
||
for (const q of pts) {
|
||
q[0] += sx;
|
||
q[1] += sy;
|
||
}
|
||
}
|
||
const cum = [0];
|
||
for (let i = 1; i < pts.length; i++) {
|
||
const dx = pts[i][0] - pts[i - 1][0],
|
||
dy = pts[i][1] - pts[i - 1][1];
|
||
cum.push(cum[i - 1] + Math.hypot(dx, dy));
|
||
}
|
||
const total = cum[cum.length - 1];
|
||
function locate(pct) {
|
||
const target = pct * total;
|
||
let lo = 0,
|
||
hi = cum.length - 1;
|
||
while (hi - lo > 1) {
|
||
const mid = (lo + hi) >> 1;
|
||
if (cum[mid] <= target) lo = mid;
|
||
else hi = mid;
|
||
}
|
||
const segLen = cum[hi] - cum[lo] || 1;
|
||
return { i: lo, j: hi, f: (target - cum[lo]) / segLen };
|
||
}
|
||
return {
|
||
closed,
|
||
length: total,
|
||
pointOnPath(pct) {
|
||
pct = closed ? wrap01(pct) : clamp01(pct);
|
||
const { i, j, f } = locate(pct);
|
||
return [lerp(pts[i][0], pts[j][0], f), lerp(pts[i][1], pts[j][1], f)];
|
||
},
|
||
tangentOnPath(pct) {
|
||
pct = closed ? wrap01(pct) : clamp01(pct);
|
||
const { i, j } = locate(pct);
|
||
const dx = pts[j][0] - pts[i][0],
|
||
dy = pts[j][1] - pts[i][1];
|
||
const l = Math.hypot(dx, dy) || 1;
|
||
return [dx / l, dy / l];
|
||
},
|
||
};
|
||
}
|
||
|
||
function pathRig(path, ctl, comp, n, u) {
|
||
const spc = ctl.spacing / 100;
|
||
const phase = ctl.phaseOffset / 100;
|
||
const manual = ctl.moveCarousel / 100;
|
||
const dAng = (ctl.depthAngle || 0) / DEG;
|
||
const rotAmt = (ctl.rotationFollow || 0) / 100;
|
||
const cx = comp.width / 2,
|
||
cy = comp.height / 2;
|
||
const maxDist = Math.hypot(cx, cy);
|
||
const out = new Array(n);
|
||
for (let idx = 0; idx < n; idx++) {
|
||
const off = (idx / n) * spc + phase;
|
||
const pct = wrap01(u + off + manual);
|
||
const pos = path.pointOnPath(pct);
|
||
|
||
const dx = pos[0] - cx,
|
||
dy = pos[1] - cy;
|
||
const projected = dx * Math.sin(dAng) + dy * Math.cos(dAng);
|
||
const norm = (projected / maxDist + 1) / 2;
|
||
const depthScale = aeLinear(norm, 0, 1, ctl.scaleMin, 100);
|
||
const mixed = aeLinear(ctl.scaleDepth / 100, 0, 1, 100, depthScale);
|
||
const scale = (mixed * (ctl.globalScale / 100)) / 100;
|
||
|
||
const tang = path.tangentOnPath(pct);
|
||
const rotZ = Math.atan2(tang[1], tang[0]) * DEG * rotAmt;
|
||
|
||
let x = pos[0],
|
||
y = pos[1],
|
||
z = 0;
|
||
if (ctl.parent) {
|
||
const rx = (ctl.xSlider || 0) / DEG,
|
||
ry = (ctl.ySlider || 0) / DEG;
|
||
const a = ctl.parent.anchor,
|
||
P = ctl.parent.position;
|
||
|
||
let l = [pos[0] - a[0], pos[1] - a[1], 0];
|
||
l = [
|
||
l[0] * Math.cos(ry) + l[2] * Math.sin(ry),
|
||
l[1],
|
||
-l[0] * Math.sin(ry) + l[2] * Math.cos(ry),
|
||
];
|
||
l = [
|
||
l[0],
|
||
l[1] * Math.cos(rx) - l[2] * Math.sin(rx),
|
||
l[1] * Math.sin(rx) + l[2] * Math.cos(rx),
|
||
];
|
||
x = P[0] + l[0];
|
||
y = P[1] + l[1];
|
||
z = (P[2] || 0) + l[2];
|
||
}
|
||
|
||
let rotY = 0,
|
||
rotX = 0;
|
||
if (ctl.parent) {
|
||
const camZ = -(comp.cameraDistance || comp.cameraZoom || 5669);
|
||
const vx = cx - x,
|
||
vy = cy - y,
|
||
vz = camZ - z;
|
||
rotY = Math.atan2(vx, -vz) * DEG;
|
||
rotX = -Math.atan2(vy, Math.hypot(vx, vz)) * DEG;
|
||
}
|
||
if (ctl.yAngle3d) {
|
||
const dist = Math.abs(x - cx);
|
||
let r = aeEase(dist, 0, comp.width * (700 / 1920), 0, ctl.yAngle3d);
|
||
if (ctl.mirrorY !== false && x < cx) r = -r;
|
||
rotY += r;
|
||
}
|
||
out[idx] = { x, y, z, scale, rotZ, rotY, rotX, pct, depth: norm };
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function orbitRig(ctl, comp, n, u) {
|
||
const r = ctl.radius;
|
||
const rawCnt = Math.max(1, ctl.elementCount);
|
||
const cnt = Math.floor(rawCnt);
|
||
const off = ctl.offset;
|
||
|
||
const st = ctl.spinTurns || { x: 0, y: 1, z: 0 };
|
||
const rx = ((ctl.rotationX || 0) + u * 360 * (st.x || 0)) / DEG;
|
||
const ry = ((ctl.rotationY || 0) + u * 360 * (st.y || 0)) / DEG;
|
||
const rz = ((ctl.rotationZ || 0) + u * 360 * (st.z || 0)) / DEG;
|
||
const cx = comp.width / 2,
|
||
cy = comp.height / 2;
|
||
const camZ = -(comp.cameraDistance || comp.cameraZoom || 2000);
|
||
const cd = Math.abs(camZ);
|
||
const ga = Math.PI * (3 - Math.sqrt(5));
|
||
const getP = (index, c) => {
|
||
const i = (((index - 0.5 + off) % c) + c) % c;
|
||
const inc = Math.acos(Math.max(-1, Math.min(1, 1 - 2 * (i / c))));
|
||
const azi = ga * i;
|
||
return [
|
||
Math.sin(inc) * Math.cos(azi) * r,
|
||
Math.cos(inc) * r,
|
||
Math.sin(inc) * Math.sin(azi) * r,
|
||
];
|
||
};
|
||
const out = new Array(n);
|
||
for (let k = 0; k < n; k++) {
|
||
const index = k + 1;
|
||
const a = getP(index, cnt),
|
||
b = getP(index, cnt + 1),
|
||
f = rawCnt % 1;
|
||
let p = [lerp(a[0], b[0], f), lerp(a[1], b[1], f), lerp(a[2], b[2], f)];
|
||
|
||
p = [
|
||
p[0],
|
||
p[1] * Math.cos(rx) - p[2] * Math.sin(rx),
|
||
p[1] * Math.sin(rx) + p[2] * Math.cos(rx),
|
||
];
|
||
p = [
|
||
p[0] * Math.cos(ry) + p[2] * Math.sin(ry),
|
||
p[1],
|
||
-p[0] * Math.sin(ry) + p[2] * Math.cos(ry),
|
||
];
|
||
p = [
|
||
p[0] * Math.cos(rz) - p[1] * Math.sin(rz),
|
||
p[0] * Math.sin(rz) + p[1] * Math.cos(rz),
|
||
p[2],
|
||
];
|
||
const jitter = 1 + (index % 2) * 0.01;
|
||
const x = cx + p[0] * jitter,
|
||
y = cy + p[1] * jitter,
|
||
z = p[2] * jitter;
|
||
|
||
const d = Math.hypot(x - cx, y - cy, z - camZ);
|
||
const t = clamp01(aeLinear(d, cd - r, cd + r, 0, 1));
|
||
const s = aeLinear(t, 0, 1, ctl.scaleMax, ctl.scaleMin);
|
||
const finalS = aeLinear(ctl.perspectiveBoost, 0, 100, 100, s);
|
||
|
||
const behindCam = z <= camZ * 0.92;
|
||
|
||
const vx = cx - x,
|
||
vy = cy - y,
|
||
vz = camZ - z;
|
||
const yaw = Math.atan2(vx, -vz) * DEG;
|
||
const pitch = -Math.atan2(vy, Math.hypot(vx, vz)) * DEG;
|
||
out[k] = {
|
||
x,
|
||
y,
|
||
z,
|
||
scale: finalS / 100,
|
||
rotZ: 0,
|
||
rotY: yaw,
|
||
rotX: pitch,
|
||
visible: index <= cnt && !behindCam,
|
||
depth: 1 - t,
|
||
};
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function aeCurve(keys) {
|
||
const segs = [];
|
||
for (let i = 0; i < keys.length - 1; i++) {
|
||
const a = keys[i],
|
||
b = keys[i + 1],
|
||
T = b.time - a.time;
|
||
const linear = !a.easeOut || !b.easeIn || (a.flags && a.flags.startsWith("0101"));
|
||
let P1, P2;
|
||
if (linear) {
|
||
P1 = [a.time + T / 3, a.value + (b.value - a.value) / 3];
|
||
P2 = [a.time + (2 * T) / 3, a.value + (2 * (b.value - a.value)) / 3];
|
||
} else {
|
||
const lo = a.easeOut.influence * T,
|
||
li = b.easeIn.influence * T;
|
||
P1 = [a.time + lo, a.value + a.easeOut.speed * lo];
|
||
P2 = [b.time - li, b.value - b.easeIn.speed * li];
|
||
}
|
||
segs.push({
|
||
t0: a.time,
|
||
t1: b.time,
|
||
P0: [a.time, a.value],
|
||
P1,
|
||
P2,
|
||
P3: [b.time, b.value],
|
||
});
|
||
}
|
||
const bez = (p0, p1, p2, p3, s) => {
|
||
const m = 1 - s;
|
||
return m * m * m * p0 + 3 * m * m * s * p1 + 3 * m * s * s * p2 + s * s * s * p3;
|
||
};
|
||
return {
|
||
first: keys[0].value,
|
||
last: keys[keys.length - 1].value,
|
||
duration: keys[keys.length - 1].time - keys[0].time,
|
||
at(t) {
|
||
if (t <= segs[0].t0) return keys[0].value;
|
||
const sg = segs.find((g) => t <= g.t1) || segs[segs.length - 1];
|
||
if (t >= sg.t1) return sg.P3[1];
|
||
|
||
let s = (t - sg.t0) / (sg.t1 - sg.t0);
|
||
for (let k = 0; k < 8; k++) {
|
||
const x = bez(sg.P0[0], sg.P1[0], sg.P2[0], sg.P3[0], s) - t;
|
||
const m = 1 - s;
|
||
const dx =
|
||
3 * m * m * (sg.P1[0] - sg.P0[0]) +
|
||
6 * m * s * (sg.P2[0] - sg.P1[0]) +
|
||
3 * s * s * (sg.P3[0] - sg.P2[0]);
|
||
if (Math.abs(dx) < 1e-9) break;
|
||
s -= x / dx;
|
||
s = s < 0 ? 0 : s > 1 ? 1 : s;
|
||
}
|
||
return bez(sg.P0[1], sg.P1[1], sg.P2[1], sg.P3[1], s);
|
||
},
|
||
};
|
||
}
|
||
|
||
function chainedDrive(keys, mode) {
|
||
const c = aeCurve(keys),
|
||
T = c.duration,
|
||
delta = c.last - c.first;
|
||
return {
|
||
cycle: T,
|
||
at(t) {
|
||
if (T <= 0) return c.first;
|
||
const k = Math.floor(t / T),
|
||
r = t - k * T;
|
||
if (mode === "pingpong")
|
||
return k % 2 === 0 ? c.at(keys[0].time + r) : c.at(keys[0].time + (T - r));
|
||
return c.at(keys[0].time + r) + k * delta;
|
||
},
|
||
};
|
||
}
|
||
|
||
function buildProgress(opts) {
|
||
const dur = opts.duration,
|
||
cruise = opts.cruise;
|
||
const sign = cruise < 0 ? -1 : 1;
|
||
const c = Math.abs(cruise);
|
||
const cap = opts.boostCap == null ? 0.5 : opts.boostCap;
|
||
const vIn = Math.max(
|
||
Math.min(c * (opts.arriveMul || 4), c + cap),
|
||
opts.arriveMin || 0.16,
|
||
);
|
||
const vOut = Math.max(Math.min(c * (opts.exitMul || 6), c + cap), opts.exitMin || 0.22);
|
||
const tA = opts.arrive || 1.6,
|
||
tX = opts.exit || 0.9;
|
||
const N = Math.max(600, Math.round(dur * 120));
|
||
const lut = new Float64Array(N + 1);
|
||
let acc = 0,
|
||
prevV = null;
|
||
for (let i = 0; i <= N; i++) {
|
||
const t = (i / N) * dur;
|
||
let v;
|
||
if (t < tA) {
|
||
const k = 1 - Math.pow(1 - t / tA, 4);
|
||
v = lerp(vIn, c, k);
|
||
} else if (t > dur - tX) {
|
||
const k = Math.pow((t - (dur - tX)) / tX, 3);
|
||
v = lerp(c, vOut, k);
|
||
} else v = c;
|
||
if (prevV !== null) acc += ((prevV + v) / 2) * (dur / N);
|
||
prevV = v;
|
||
lut[i] = acc * sign;
|
||
}
|
||
return {
|
||
duration: dur,
|
||
travel: lut[N],
|
||
at(t) {
|
||
const f = clamp01(t / dur) * N,
|
||
i = Math.floor(f),
|
||
g = f - i;
|
||
return i >= N ? lut[N] : lerp(lut[i], lut[i + 1], g);
|
||
},
|
||
};
|
||
}
|
||
|
||
global.CarouselRig = {
|
||
buildPath,
|
||
pathRig,
|
||
orbitRig,
|
||
buildProgress,
|
||
aeCurve,
|
||
chainedDrive,
|
||
aeLinear,
|
||
aeEase,
|
||
};
|
||
})(typeof window !== "undefined" ? window : globalThis);
|
||
</script>
|
||
<script>
|
||
const DATA = {
|
||
id: "carousel-path-4",
|
||
family: "path",
|
||
controls: {
|
||
speed: 100,
|
||
spacing: 100,
|
||
phaseOffset: 0,
|
||
globalScale: 53.9,
|
||
scaleDepth: 18.6,
|
||
scaleMin: -663,
|
||
depthAngle: 183.76,
|
||
moveCarousel: 49.1,
|
||
rotationFollow: 57,
|
||
},
|
||
comp: {
|
||
width: 3840,
|
||
height: 2160,
|
||
outWidth: 1920,
|
||
outHeight: 1080,
|
||
cameraZoom: 5669.29,
|
||
cameraDistance: 5669.29,
|
||
duration: 6,
|
||
fps: 30,
|
||
},
|
||
n: 24,
|
||
cards: [
|
||
{ file: "artist-bob-seger.jpg", width: 640, height: 640 },
|
||
{ file: "artist-dire-straits.jpg", width: 640, height: 640 },
|
||
{ file: "artist-faces.jpg", width: 640, height: 500 },
|
||
{ file: "artist-little-feat.jpg", width: 640, height: 640 },
|
||
{ file: "artist-north-mississippi-allstars.jpg", width: 640, height: 640 },
|
||
{ file: "artist-sea-level.jpg", width: 640, height: 495 },
|
||
{ file: "artist-taj-mahal.jpg", width: 640, height: 640 },
|
||
{ file: "artist-tool.jpg", width: 640, height: 640 },
|
||
{ file: "artist-van-morrison.jpg", width: 640, height: 640 },
|
||
{ file: "pl-10-songs.jpg", width: 640, height: 640 },
|
||
{ file: "pl-bangers.jpg", width: 640, height: 640 },
|
||
{ file: "pl-bhindi-bhagee.jpg", width: 640, height: 640 },
|
||
{ file: "pl-chb-2.jpg", width: 640, height: 640 },
|
||
{ file: "pl-chb.jpg", width: 640, height: 640 },
|
||
{ file: "pl-homework.jpg", width: 640, height: 640 },
|
||
{ file: "pl-sleeping.jpg", width: 640, height: 640 },
|
||
{ file: "pl-y2k.jpg", width: 640, height: 640 },
|
||
{ file: "track-back-together.jpg", width: 640, height: 640 },
|
||
{ file: "track-bitter-with-the-sweet.jpg", width: 640, height: 640 },
|
||
{ file: "track-cleaning-windows.jpg", width: 640, height: 640 },
|
||
{
|
||
file: "track-dixie-chicken-live-at-the-rainbow-theatre-london-u.jpg",
|
||
width: 640,
|
||
height: 640,
|
||
},
|
||
{ file: "track-dweller-on-the-threshold.jpg", width: 640, height: 640 },
|
||
{ file: "track-far-east-man.jpg", width: 640, height: 640 },
|
||
{ file: "track-funky-shit.jpg", width: 640, height: 640 },
|
||
],
|
||
path: {
|
||
vertices: [
|
||
{ p: [-168.4, 1484], in: [0, 0], out: [417.3, -68.9] },
|
||
{ p: [1071.4, 687.1], in: [-435, 0], out: [500.2, 0] },
|
||
{ p: [2468, 1345.9], in: [-704.4, 0], out: [726.9, 0] },
|
||
{ p: [4259.6, 680.5], in: [-348.5, -132.9], out: [0, 0] },
|
||
],
|
||
closed: false,
|
||
},
|
||
keys: {
|
||
"Depth Angle": [
|
||
{
|
||
time: 0,
|
||
value: 215.699997,
|
||
flags: "01010000",
|
||
easeIn: { speed: 0, influence: 0 },
|
||
easeOut: { speed: 0, influence: 0 },
|
||
},
|
||
{
|
||
time: 1.56667,
|
||
value: 160.099997,
|
||
flags: "01010000",
|
||
easeIn: { speed: 0, influence: 0 },
|
||
easeOut: { speed: 0, influence: 0.166667 },
|
||
},
|
||
],
|
||
"Move Carousel": [
|
||
{
|
||
time: 0,
|
||
value: 0,
|
||
flags: "02020000",
|
||
easeIn: { speed: 0, influence: 0.333333 },
|
||
easeOut: { speed: 266.290401, influence: 0.157005 },
|
||
},
|
||
{
|
||
time: 1.53333,
|
||
value: 80,
|
||
flags: "02020000",
|
||
easeIn: { speed: 205.452802, influence: 0.146587 },
|
||
easeOut: { speed: 0, influence: 0.333333 },
|
||
},
|
||
],
|
||
},
|
||
motion: {
|
||
arrive: 1.6,
|
||
arriveMul: 4,
|
||
arriveMin: 0.16,
|
||
exit: 0.9,
|
||
exitMul: 6,
|
||
exitMin: 0.22,
|
||
entryStaggerTotal: 0.45,
|
||
entryDuration: 0.55,
|
||
entryLead: 0.25,
|
||
orbitSpinDegPerSec: 24,
|
||
orbitSpinScale: 1,
|
||
boostCap: 0.5,
|
||
speedScale: 1,
|
||
},
|
||
look: {
|
||
background: "#EDEDEF",
|
||
cardRadius: 0,
|
||
cardShadow: "none",
|
||
imageHeight: null,
|
||
centerPath: true,
|
||
endFade: 0.06,
|
||
fitBox: 1500,
|
||
},
|
||
text: null,
|
||
ending: null,
|
||
outro: null,
|
||
ring: null,
|
||
registry: {
|
||
imageVars: [
|
||
"image1",
|
||
"image2",
|
||
"image3",
|
||
"image4",
|
||
"image5",
|
||
"image6",
|
||
"image7",
|
||
"image8",
|
||
"image9",
|
||
"image10",
|
||
"image11",
|
||
"image12",
|
||
"image13",
|
||
"image14",
|
||
"image15",
|
||
"image16",
|
||
"image17",
|
||
"image18",
|
||
"image19",
|
||
"image20",
|
||
"image21",
|
||
"image22",
|
||
"image23",
|
||
"image24",
|
||
],
|
||
textVars: null,
|
||
speedVar: "speed",
|
||
},
|
||
cutAt: null,
|
||
};
|
||
(async function () {
|
||
const R = window.CarouselRig;
|
||
|
||
const REG = DATA.registry || null;
|
||
const V =
|
||
REG && window.__hyperframes && window.__hyperframes.getVariables
|
||
? window.__hyperframes.getVariables() || {}
|
||
: {};
|
||
const varUrl = (v) => (v && typeof v === "object" ? v.url || v.src || "" : v || "");
|
||
if (REG) {
|
||
const srcs = REG.imageVars.map(
|
||
(id, i) => varUrl(V[id]) || DATA.cards[i % DATA.cards.length].file,
|
||
);
|
||
DATA.cards = await Promise.all(
|
||
srcs.map(async (src, i) => {
|
||
const img = new Image();
|
||
img.src = src;
|
||
try {
|
||
await img.decode();
|
||
} catch (e) {}
|
||
return {
|
||
file: src,
|
||
abs: true,
|
||
width: img.naturalWidth || DATA.cards[i % DATA.cards.length].width,
|
||
height: img.naturalHeight || DATA.cards[i % DATA.cards.length].height,
|
||
};
|
||
}),
|
||
);
|
||
if (DATA.text && REG.textVars && REG.textVars.line1 && V[REG.textVars.line1])
|
||
DATA.text.words = String(V[REG.textVars.line1]).trim().split(/\s+/);
|
||
if (DATA.outro && REG.textVars && REG.textVars.line2 && V[REG.textVars.line2])
|
||
DATA.outro.words = String(V[REG.textVars.line2]).trim().split(/\s+/);
|
||
if (REG.speedVar && V[REG.speedVar] != null && isFinite(+V[REG.speedVar]))
|
||
DATA.motion.speedScale = +V[REG.speedVar];
|
||
}
|
||
const { family, controls: ctl, comp, n, cards, motion, look } = DATA;
|
||
const root = document.getElementById("root");
|
||
const stage = document.getElementById(DATA.id + "-stage");
|
||
root.style.setProperty("--card-radius", look.cardRadius + "px");
|
||
root.style.setProperty("--card-shadow", look.cardShadow);
|
||
|
||
document.getElementById(DATA.id + "-scaler").style.transform =
|
||
"scale(" + comp.outWidth / comp.width + ")";
|
||
|
||
const is3d = family === "orbit" || family === "vision";
|
||
if (is3d) {
|
||
stage.style.perspective = comp.cameraZoom + "px";
|
||
stage.style.perspectiveOrigin = "50% 50%";
|
||
}
|
||
|
||
const camShift = (comp.cameraDistance || comp.cameraZoom) - comp.cameraZoom;
|
||
const group = document.createElement("div");
|
||
group.className = "stage" + (is3d ? " is-3d" : "");
|
||
group.id = DATA.id + "-group";
|
||
const gx = [];
|
||
if (look.zoom && look.zoom !== 1) gx.push("scale(" + look.zoom + ")");
|
||
if (camShift) gx.push("translateZ(" + -camShift + "px)");
|
||
if (gx.length) group.style.transform = gx.join(" ");
|
||
|
||
const squeeze = document.createElement("div");
|
||
squeeze.className = "stage" + (is3d ? " is-3d" : "");
|
||
squeeze.id = DATA.id + "-squeeze";
|
||
group.appendChild(squeeze);
|
||
|
||
const TXT = DATA.text || null;
|
||
const textLayers = [];
|
||
if (TXT) {
|
||
for (const cls of ["below", "above"]) {
|
||
const line = document.createElement("div");
|
||
line.className = "textline " + cls + " cards-over-" + (TXT.cardsOverOn || "right");
|
||
line.id = DATA.id + "-text-" + cls;
|
||
line.style.fontSize = TXT.fontSize + "px";
|
||
line.style.color = TXT.color || "#111";
|
||
const drift = document.createElement("div");
|
||
drift.className = "textdrift";
|
||
drift.id = DATA.id + "-drift-" + cls;
|
||
TXT.words.forEach((w, k) => {
|
||
const box = document.createElement("span");
|
||
box.className = "word";
|
||
const inner = document.createElement("span");
|
||
inner.textContent = w;
|
||
inner.id = DATA.id + "-w" + k + "-" + cls;
|
||
box.appendChild(inner);
|
||
drift.appendChild(box);
|
||
});
|
||
line.appendChild(drift);
|
||
textLayers.push(line);
|
||
}
|
||
stage.appendChild(textLayers[0]);
|
||
}
|
||
stage.appendChild(group);
|
||
if (TXT) stage.appendChild(textLayers[1]);
|
||
|
||
const els = [],
|
||
inners = [];
|
||
for (let i = 0; i < n; i++) {
|
||
const c = cards[i];
|
||
let w = c.width,
|
||
h = c.height;
|
||
if (look.imageHeight) {
|
||
w = c.width * (look.imageHeight / c.height);
|
||
h = look.imageHeight;
|
||
} else if (look.fitBox) {
|
||
const k = look.fitBox / Math.max(c.width, c.height);
|
||
w = c.width * k;
|
||
h = c.height * k;
|
||
}
|
||
const el = document.createElement("div");
|
||
el.className = "card";
|
||
el.id = DATA.id + "-card-" + i;
|
||
el.style.width = w + "px";
|
||
el.style.height = h + "px";
|
||
el.style.marginLeft = -w / 2 + "px";
|
||
el.style.marginTop = -h / 2 + "px";
|
||
const inner = document.createElement("div");
|
||
inner.className = "card-inner";
|
||
inner.id = el.id + "-inner";
|
||
const img = document.createElement("img");
|
||
img.src = c.abs ? c.file : "assets/carousel-images/" + c.file;
|
||
img.alt = "";
|
||
|
||
if (!is3d) el.style.zIndex = String(n - i);
|
||
inner.appendChild(img);
|
||
el.appendChild(inner);
|
||
squeeze.appendChild(el);
|
||
els.push(el);
|
||
inners.push(inner);
|
||
}
|
||
|
||
const path = DATA.path
|
||
? R.buildPath(
|
||
DATA.path.vertices,
|
||
DATA.path.closed,
|
||
64,
|
||
look.centerPath ? [comp.width / 2, comp.height / 2] : null,
|
||
)
|
||
: null;
|
||
|
||
const K = DATA.keys || {};
|
||
const drives = {};
|
||
for (const name in K) {
|
||
const cumulative =
|
||
/^(Move Carousel|Rotation [XYZ])$/.test(name) &&
|
||
Math.abs(K[name][K[name].length - 1].value - K[name][0].value) > 1e-6;
|
||
drives[name] = R.chainedDrive(K[name], cumulative ? "accumulate" : "pingpong");
|
||
}
|
||
const speedScale = motion.speedScale == null ? 1 : motion.speedScale;
|
||
const tt = (t) => t * speedScale;
|
||
const baseSpeed = (ctl.speed || 0) / 1000;
|
||
|
||
const fallbackCruise =
|
||
family === "orbit" ? (motion.orbitSpinDegPerSec || 24) / 360 : baseSpeed;
|
||
const prog = R.buildProgress(
|
||
Object.assign({ duration: comp.duration, cruise: fallbackCruise }, motion),
|
||
);
|
||
const hasMove = !!drives["Move Carousel"],
|
||
hasSpin = !!(drives["Rotation X"] || drives["Rotation Y"] || drives["Rotation Z"]);
|
||
function controlsAt(t) {
|
||
const c = Object.assign({}, ctl);
|
||
for (const name in drives) {
|
||
const v = drives[name].at(tt(t));
|
||
if (name === "Move Carousel") c.moveCarousel = v;
|
||
else if (name === "Depth Angle") c.depthAngle = v;
|
||
else if (name === "Rotation Follow") c.rotationFollow = v;
|
||
else if (name === "Rotation X") c.rotationX = v;
|
||
else if (name === "Rotation Y") c.rotationY = v;
|
||
else if (name === "Rotation Z") c.rotationZ = v;
|
||
}
|
||
if (family === "orbit") c.spinTurns = { x: 0, y: 0, z: 0 };
|
||
return c;
|
||
}
|
||
function uAt(t) {
|
||
if (family === "orbit") return hasSpin ? 0 : prog.at(t);
|
||
return hasMove ? baseSpeed * tt(t) : prog.at(t);
|
||
}
|
||
const endFade = path && !path.closed ? (look.endFade ?? 0.06) : 0;
|
||
|
||
const RING = DATA.ring || null;
|
||
const converge = RING && RING.entry === "converge";
|
||
const cvE = converge ? gsap.parseEase(RING.ease || "power3.out") : null;
|
||
let ringStartRef = 0;
|
||
function convergeFactor(i, t) {
|
||
const per = (RING.stagger || 0.5) / n,
|
||
s0 = ringStartRef + i * per,
|
||
d = RING.duration || 0.8;
|
||
const e = t <= s0 ? 0 : t >= s0 + d ? 1 : cvE((t - s0) / d);
|
||
return 1 + ((RING.from || 3) - 1) * (1 - e);
|
||
}
|
||
function render(t) {
|
||
const u = uAt(t),
|
||
c = controlsAt(t);
|
||
const poses =
|
||
family === "orbit" ? R.orbitRig(c, comp, n, u) : R.pathRig(path, c, comp, n, u);
|
||
const cx = comp.width / 2,
|
||
cy = comp.height / 2;
|
||
for (let i = 0; i < n; i++) {
|
||
const p = poses[i],
|
||
el = els[i];
|
||
if (converge) {
|
||
const f = convergeFactor(i, t);
|
||
p.x = cx + (p.x - cx) * f;
|
||
p.y = cy + (p.y - cy) * f;
|
||
}
|
||
let alpha = p.visible === false ? 0 : 1;
|
||
if (endFade) {
|
||
const e = Math.min(p.pct, 1 - p.pct) / endFade;
|
||
alpha *= e < 1 ? e : 1;
|
||
}
|
||
el.style.opacity = alpha;
|
||
el.style.transform =
|
||
"translate3d(" +
|
||
p.x.toFixed(2) +
|
||
"px," +
|
||
p.y.toFixed(2) +
|
||
"px," +
|
||
(-p.z).toFixed(2) +
|
||
"px)" +
|
||
(p.rotY ? " rotateY(" + p.rotY.toFixed(3) + "deg)" : "") +
|
||
(p.rotX ? " rotateX(" + p.rotX.toFixed(3) + "deg)" : "") +
|
||
(p.rotZ ? " rotateZ(" + p.rotZ.toFixed(3) + "deg)" : "") +
|
||
" scale(" +
|
||
p.scale.toFixed(5) +
|
||
")";
|
||
}
|
||
}
|
||
|
||
const clock = { t: 0 };
|
||
const tl = gsap.timeline({ paused: true });
|
||
tl.fromTo(
|
||
clock,
|
||
{ t: 0 },
|
||
{
|
||
t: comp.duration,
|
||
duration: comp.duration,
|
||
ease: "none",
|
||
onUpdate: () => render(clock.t),
|
||
},
|
||
0,
|
||
);
|
||
|
||
let ringStart = 0;
|
||
if (TXT) {
|
||
const wd = TXT.wordDuration,
|
||
ws = TXT.wordStagger;
|
||
TXT.words.forEach((w, k) => {
|
||
const targets = textLayers.map((L) => L.firstChild.children[k].firstChild);
|
||
tl.fromTo(
|
||
targets,
|
||
{ xPercent: 110 },
|
||
{ xPercent: 0, duration: wd, ease: "power3.out", immediateRender: true },
|
||
k * ws,
|
||
);
|
||
});
|
||
ringStart = (TXT.words.length - 1) * ws;
|
||
|
||
if (TXT.driftPx) {
|
||
const d0 = wd * (TXT.driftOverlapAt == null ? 0.61 : TXT.driftOverlapAt);
|
||
const px = TXT.driftPx * (comp.width / comp.outWidth);
|
||
|
||
const DE = TXT.driftEase || { power: 1, linear: 1 };
|
||
const k = DE.linear == null ? 0.35 : DE.linear,
|
||
pw = DE.power || 3;
|
||
const driftEase = (x) => (1 - k) * (1 - Math.pow(1 - x, pw)) + k * x;
|
||
|
||
const xFrom = TXT.driftEndsCentered ? px : 0,
|
||
xTo = TXT.driftEndsCentered ? 0 : -px;
|
||
tl.fromTo(
|
||
textLayers.map((L) => L.firstChild),
|
||
{ x: xFrom },
|
||
{
|
||
x: xTo,
|
||
duration: (DATA.cutAt || comp.duration) - d0,
|
||
ease: driftEase,
|
||
immediateRender: true,
|
||
},
|
||
d0,
|
||
);
|
||
}
|
||
}
|
||
ringStartRef = ringStart;
|
||
const each = motion.entryStaggerTotal / n,
|
||
lead = TXT ? 0 : motion.entryLead || 0,
|
||
eDur = motion.entryDuration;
|
||
const E = gsap.parseEase("power3.out");
|
||
for (let i = 0; i < n; i++) {
|
||
if (converge) break;
|
||
const start = ringStart + i * each - lead;
|
||
if (start >= 0) {
|
||
tl.fromTo(
|
||
inners[i],
|
||
{ autoAlpha: 0, scale: 0.72 },
|
||
{ autoAlpha: 1, scale: 1, duration: eDur, ease: "power3.out", immediateRender: true },
|
||
start,
|
||
);
|
||
continue;
|
||
}
|
||
if (start + eDur <= 0) continue;
|
||
|
||
const p0 = -start / eDur,
|
||
e0 = E(p0);
|
||
const tail = (x) => (E(p0 + x * (1 - p0)) - e0) / (1 - e0);
|
||
tl.fromTo(
|
||
inners[i],
|
||
{ autoAlpha: e0, scale: 0.72 + 0.28 * e0 },
|
||
{ autoAlpha: 1, scale: 1, duration: eDur - -start, ease: tail, immediateRender: true },
|
||
0,
|
||
);
|
||
}
|
||
|
||
const END = DATA.ending || null;
|
||
const cutAt = DATA.cutAt || comp.duration;
|
||
if (END) {
|
||
const tA = END.textScaleAt,
|
||
tB = tA + (END.imagesDelay == null ? 0.2 : END.imagesDelay);
|
||
if (TXT && END.textScaleTo != null)
|
||
tl.fromTo(
|
||
textLayers,
|
||
{ scale: 1 },
|
||
{
|
||
scale: END.textScaleTo,
|
||
duration: cutAt - tA,
|
||
ease: END.ease || "power2.in",
|
||
immediateRender: true,
|
||
},
|
||
tA,
|
||
);
|
||
if (END.imagesTo != null)
|
||
tl.fromTo(
|
||
squeeze,
|
||
{ scale: 1 },
|
||
{
|
||
scale: END.imagesTo,
|
||
duration: cutAt - tB,
|
||
ease: END.ease || "power2.in",
|
||
immediateRender: true,
|
||
},
|
||
tB,
|
||
);
|
||
}
|
||
|
||
const OUT = DATA.outro || null;
|
||
if (OUT) {
|
||
const line = document.getElementById(DATA.id + "-outro-line");
|
||
const driftEl = document.getElementById(DATA.id + "-outro-drift");
|
||
line.style.fontSize = (OUT.fontSize || (TXT && TXT.fontSize) || 190) + "px";
|
||
line.style.color = OUT.color || (TXT && TXT.color) || "#111";
|
||
OUT.words.forEach((w) => {
|
||
const s = document.createElement("span");
|
||
s.className = "word";
|
||
s.textContent = w;
|
||
driftEl.appendChild(s);
|
||
});
|
||
const sd = OUT.scaleDuration || 0.8,
|
||
outEnd = comp.duration;
|
||
tl.fromTo(
|
||
line,
|
||
{ scale: OUT.scaleFrom == null ? 1.4 : OUT.scaleFrom },
|
||
{
|
||
scale: OUT.scaleTo == null ? 1 : OUT.scaleTo,
|
||
duration: sd,
|
||
ease: OUT.ease || "power2.out",
|
||
immediateRender: true,
|
||
},
|
||
cutAt,
|
||
);
|
||
if (OUT.driftPx) {
|
||
const d0 = cutAt + sd * (OUT.driftOverlapAt == null ? 0.61 : OUT.driftOverlapAt);
|
||
const px = OUT.driftPx * (comp.width / comp.outWidth);
|
||
tl.fromTo(
|
||
driftEl,
|
||
{ x: 0 },
|
||
{ x: -px, duration: outEnd - d0, ease: "none", immediateRender: true },
|
||
d0,
|
||
);
|
||
}
|
||
}
|
||
render(0);
|
||
window.__timelines[DATA.id] = tl;
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
{/* hf:generated-footer */}
|
||
|
||
Tagged `carousel` `gallery` `images` `showcase` `path`.
|
||
|
||
Created by Jake Moran.
|
||
|
||
## Related topics
|
||
|
||
- [Browse the complete Catalog](/catalog)
|
||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||
- [Build a richer composition](/go-further)
|