> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# Orbit Carousel 4

> Image cards on a spinning 3D Fibonacci sphere, billboarded to the camera, sized by camera distance (Orbit style 4); 24 image slots.

# Orbit Carousel 4

Image cards on a spinning 3D Fibonacci sphere, billboarded to the camera, sized by camera distance (Orbit style 4); 24 image slots.

**Orbit Carousel 4**

```html carousel-orbit-4.html theme={null}
<!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 · Orbit 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-orbit-4"
      data-start="0"
      data-width="1920"
      data-height="1080"
      data-duration="6"
      data-fps="30"
      data-layout-allow-overflow=""
    >
      <div id="carousel-orbit-4-partA" class="clip" data-start="0" data-duration="6">
        <div data-hf-id="hf-elyu" id="carousel-orbit-4-scaler" class="scaler">
          <div data-hf-id="hf-mi5e" id="carousel-orbit-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-orbit-4",
        family: "orbit",
        controls: {
          radius: 613,
          elementCount: 25,
          offset: 13.8,
          rotationX: 33,
          rotationY: -41,
          rotationZ: 10,
          perspectiveBoost: 82,
          scaleMin: -20.7,
          scaleMax: -0.4,
          spinDegPerSec: 159.33333333333334,
          spinTurns: { x: 0.047671936316275434, y: -1, z: 0 },
        },
        comp: {
          width: 3840,
          height: 2160,
          outWidth: 1920,
          outHeight: 1080,
          cameraZoom: 1022.2222,
          cameraDistance: 1022.2222,
          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: null,
        keys: {
          "Rotation X": [
            {
              time: 0,
              value: 33,
              flags: "01010000",
              easeIn: { speed: 0, influence: 0 },
              easeOut: { speed: 0, influence: 0 },
            },
            {
              time: 1.56667,
              value: 44.9,
              flags: "01010000",
              easeIn: { speed: 0, influence: 0 },
              easeOut: { speed: 0, influence: 0.166667 },
            },
          ],
          "Rotation Y": [
            {
              time: 0,
              value: -41,
              flags: "02020000",
              easeIn: { speed: 0, influence: 0.333333 },
              easeOut: { speed: -708.913926, influence: 0.178046 },
            },
            {
              time: 1.5,
              value: -280,
              flags: "02020000",
              easeIn: { speed: -2018.614608, influence: 0.069091 },
              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]) || "assets/carousel-images/" + 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>
```

Copy npx hyperframes add carousel-orbit-4 to the clipboard

That writes `compositions/carousel-orbit-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 theme={null}
<div
  data-composition-id="carousel-orbit-4"
  data-composition-src="compositions/carousel-orbit-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.md) 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 theme={null}
<div
  data-composition-id="carousel-orbit-4"
  data-composition-src="compositions/carousel-orbit-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>
```

Tagged `carousel` `gallery` `images` `showcase` `orbit` `3d`.

Created by Jake Moran.

## Related topics

* [Browse the complete Catalog](/catalog.md)
* [Add assets and Catalog items in the browser editor](/studio/assets-and-blocks.md)
* [Put blocks together into one video](/concepts/compositions.md)


## Related topics

- [Orbit Carousel 1](/catalog/blocks/carousel-orbit-1.md)
- [Orbit Carousel 2](/catalog/blocks/carousel-orbit-2.md)
- [Orbit Carousel 3](/catalog/blocks/carousel-orbit-3.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.