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

# Gallery Tunnel

> An infinite corridor of panels: a square tunnel receding to a vanishing point, its floor, ceiling and walls tiled with saturated colour slabs and caller-supplied images that swell toward camera and exit past you, the far end dissolving into fog.

export const InstallCommand = ({command}) => {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(command);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = command;
        scratch.setAttribute("readonly", "");
        scratch.style.position = "fixed";
        scratch.style.opacity = "0";
        document.body.appendChild(scratch);
        scratch.select();
        document.execCommand("copy");
        document.body.removeChild(scratch);
        previous?.focus?.();
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch {}
  };
  return <div className="hf-install-command not-prose my-4 flex items-stretch overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <code className="flex-1 overflow-x-auto whitespace-nowrap border-r border-zinc-200 px-4 py-3 font-mono text-sm text-zinc-800 dark:border-zinc-800 dark:text-zinc-100">
        {command}
      </code>
      <button type="button" onClick={copy} data-copied={copied ? "true" : "false"} aria-label={`Copy ${command} to the clipboard`} className="hf-install-copy">
        <svg className="hf-install-copy-clipboard" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" />
          <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" />
        </svg>
        <svg className="hf-install-copy-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.75 9.5L6.5 13.25L15.25 4.5" />
        </svg>
      </button>
      <span className="hf-install-copy-status" role="status" aria-live="polite">
        {copied ? "Copied" : ""}
      </span>
    </div>;
};

<iframe className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800" title="gallery-tunnel preview" loading="lazy" srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/gallery-tunnel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gallery-tunnel.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add gallery-tunnel" />

That writes one file: `compositions/gallery-tunnel.html`.

## Add it to your video

It runs for 12 seconds at 1920×1080. Paste this into your composition:

```html index.html theme={null}
<div
  data-composition-id="gallery-tunnel"
  data-composition-src="compositions/gallery-tunnel.html"
  data-start="0"
  data-duration="12"
  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.

## Change how it looks

Set these CSS variables on the block:

* `--images` — Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png). Defaults to \`\`.
* `--imagemix` — Image share of filled slots (0 = all colour, 1 = all images). Defaults to `0.5`.
* `--speed` — Flight speed (segments per second). Defaults to `3`.
* `--palette` — Slab palette: comma-separated hex colours. Defaults to `#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200`.
* `--grid` — Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment). Defaults to `2`.
* `--fill` — Slot fill rate. Defaults to `0.5`.
* `--fog` — Fog distance in segments: depth at which the corridor reaches pure backdrop. Defaults to `14.25`.
* `--edgecolor` — Edge line colour. Defaults to `#B0B0B0`.
* `--edgeopacity` — Edge line opacity. Defaults to `0.5`.
* `--backdrop` — Backdrop and fog colour. Defaults to `#000000`.

## 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 |
| ------------- | ------------------------------------------------- | ---------------------------------------------- | ------------ |
| `images`      | \`\`                                              | string                                         |              |
| `imageMix`    | `0.5`                                             | 0 to 1, step 0.05                              |              |
| `speed`       | `3`                                               | 0segments/s to 8segments/s, step 0.1segments/s |              |
| `palette`     | `#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200` | string                                         |              |
| `grid`        | `2`                                               | 1 to 4, step 1                                 |              |
| `fill`        | `0.5`                                             | 0 to 1, step 0.05                              |              |
| `fog`         | `14.25`                                           | 3segments to 14.5segments, step 0.25segments   |              |
| `edgeColor`   | `#B0B0B0`                                         | color                                          |              |
| `edgeOpacity` | `0.5`                                             | 0 to 1, step 0.05                              |              |
| `backdrop`    | `#000000`                                         | color                                          |              |

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="gallery-tunnel"
  data-composition-src="compositions/gallery-tunnel.html"
  data-variable-values='{"images":"","imageMix":0.5,"speed":3,"palette":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200","grid":2,"fill":0.5,"fog":14.25,"edgeColor":"#B0B0B0","edgeOpacity":0.5,"backdrop":"#000000"}'
></div>
```

## Source

<Accordion title={`gallery-tunnel.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=1920, height=1080" />
      <title>Gallery Tunnel</title>
      <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
      <style>
        *,
        *::before,
        *::after {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        body {
          background: #000;
          overflow: hidden;
        }
        #gt-root {
          position: relative;
          width: 1920px;
          height: 1080px;
          overflow: hidden;
        }
        #gt-backdrop {
          position: absolute;
          inset: 0;
          background: #000000;
        }
        #gt-canvas {
          position: absolute;
          top: 0;
          left: 0;
          width: 1920px;
          height: 1080px;
        }
        /* Off-screen decode well: the <img> elements live in the DOM so the page
           load event waits for them, but they are never composited themselves —
           WebGL reads them as textures. The well is clipped to 1px; the images
           inside keep their natural layout size on purpose, because texImage2D
           uploads an <img> at its LAID-OUT size — sizing them down with CSS
           uploads a 1x1 texture and every panel samples one dark pixel. */
        #gt-assets {
          position: absolute;
          top: 0;
          left: 0;
          width: 1px;
          height: 1px;
          overflow: hidden;
          opacity: 0;
          pointer-events: none;
        }
      </style>
    </head>
    <body>
      <!--
        Content slots. Every panel in the corridor is a slot, and `images` fills a
        share of them with your own artwork — drop the files in the project's
        assets/ and list them, either by editing the `images` default below or from
        the host composition:

          <div
            data-composition-id="gallery-tunnel"
            data-composition-src="compositions/gallery-tunnel.html"
            data-start="0" data-duration="12" data-track-index="0"
            data-width="1920" data-height="1080"
            style="--images: assets/shot-1.png, assets/shot-2.png, assets/shot-3.png; --imagemix: 0.7"
          ></div>

        Any number of images works; they are dealt to slots by hash, so one image
        recurs at different depths rather than appearing once. Each is contain-fit
        inside its slot, so a 16:9 screenshot keeps its aspect. `imageMix` sets the
        image-vs-colour split (1 = every filled slot is an image), `fill` how many
        slots are filled at all.
      -->
      <div
        id="gt-root"
        data-composition-id="gallery-tunnel"
        data-root="true"
        data-width="1920"
        data-height="1080"
        data-start="0"
        data-duration="12"
        data-composition-variables='[
          {"id":"images","type":"string","label":"Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png)","default":"","placeholder":"assets/shot-1.png,assets/shot-2.png"},
          {"id":"imageMix","type":"number","label":"Image share of filled slots (0 = all colour, 1 = all images)","default":0.5,"min":0,"max":1,"step":0.05},
          {"id":"speed","type":"number","label":"Flight speed","default":3,"min":0,"max":8,"step":0.1,"unit":"segments/s"},
          {"id":"palette","type":"string","label":"Slab palette: comma-separated hex colours","default":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200"},
          {"id":"grid","type":"number","label":"Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment)","default":2,"min":1,"max":4,"step":1},
          {"id":"fill","type":"number","label":"Slot fill rate","default":0.5,"min":0,"max":1,"step":0.05},
          {"id":"fog","type":"number","label":"Fog distance: depth at which the corridor reaches pure backdrop","default":14.25,"min":3,"max":14.5,"step":0.25,"unit":"segments"},
          {"id":"edgeColor","type":"color","label":"Edge line colour","default":"#B0B0B0"},
          {"id":"edgeOpacity","type":"number","label":"Edge line opacity","default":0.5,"min":0,"max":1,"step":0.05},
          {"id":"backdrop","type":"color","label":"Backdrop and fog colour","default":"#000000"}
        ]'
      >
        <div id="gt-backdrop"></div>
        <canvas id="gt-canvas" width="1920" height="1080"></canvas>
        <div id="gt-assets" aria-hidden="true" data-layout-allow-overflow></div>

        <!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
        <div
          id="gt-drv"
          class="clip"
          data-start="0"
          data-duration="12"
          data-track-index="0"
          style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
        ></div>
      </div>

      <script>
        (function () {
          var DUR = 12;
          var W = 1920;
          var H = 1080;

          // Fixed tunnel geometry, measured off the reference.
          var TUNNEL_WIDTH = 2;
          var TUNNEL_HEIGHT = 1.8;
          var SEGMENT_DEPTH = 1;
          var NUM_SEGMENTS = 15;
          var LINE_RADIUS = 0.003;
          var FOV = 75;
          var INSET = 0.94; // slab gutter inside its cell, so tiles read as discrete
          var SKIN = 0.002; // push slabs just behind the wall plane so edges stay on top

          var ROOT = document.getElementById("gt-root");
          var CS = getComputedStyle(ROOT);

          // `data-composition-variables` is the single owner of every declared
          // default — parse it rather than repeating each default a second time
          // in JS. `window.__hyperframes` is NOT guaranteed to exist yet when this
          // inline script runs, so it can only ever be an override, never the
          // source. Precedence: host CSS custom property, runtime variable bag,
          // declared default.
          var DECL = {};
          JSON.parse(ROOT.getAttribute("data-composition-variables") || "[]").forEach(function (v) {
            DECL[v.id] = v.default;
          });
          var HF = window.__hyperframes;
          var V = (HF && HF.getVariables && HF.getVariables()) || {};

          function raw(id) {
            var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
            if (css !== "") return css;
            if (V[id] !== undefined && V[id] !== "") return V[id];
            return DECL[id];
          }
          function num(id) {
            var n = parseFloat(raw(id));
            return isFinite(n) ? n : 0;
          }
          function str(id) {
            var s = raw(id);
            return typeof s === "string" ? s.trim() : "";
          }
          function list(id) {
            return str(id)
              .split(",")
              .map(function (s) {
                return s.trim();
              })
              .filter(function (s) {
                return s !== "";
              });
          }

          var SPEED = num("speed");
          var GRID = Math.max(1, Math.min(4, Math.round(num("grid"))));
          var FILL = num("fill");
          var IMAGE_MIX = num("imageMix");
          var FOG_FAR = Math.max(3, Math.min(14.5, num("fog")));
          var EDGE_OPACITY = num("edgeOpacity");
          var EDGE_COLOR = str("edgeColor");
          var BACKDROP = str("backdrop");
          var PALETTE = list("palette");
          var IMAGE_SRCS = list("images");

          document.getElementById("gt-backdrop").style.background = BACKDROP;

          // How far behind the camera a segment travels before it recycles. Tying
          // it to the fog distance is the whole trick: the recycle boundary lands
          // at exactly FOG_FAR, so a segment reappears already fogged to pure
          // backdrop and nothing ever pops in at the far end. One owner for the
          // invariant — move the fog and the boundary follows.
          var BACK = Math.max(0, Math.min(1.5, NUM_SEGMENTS * SEGMENT_DEPTH - FOG_FAR));

          // Deterministic 2D integer hash — the only source of "randomness".
          // Content at frame N is a pure function of (absolute segment, slot).
          function hash2(a, b) {
            var h = Math.imul(a | 0, 0x27d4eb2d) ^ Math.imul(b | 0, 0x165667b1);
            h = Math.imul(h ^ (h >>> 15), 0x2545f491);
            h ^= h >>> 13;
            return (h >>> 0) / 4294967296;
          }

          var THREEJS = window.THREE;
          var canvas = document.getElementById("gt-canvas");
          var renderer = new THREEJS.WebGLRenderer({
            canvas: canvas,
            antialias: true,
            alpha: false,
            preserveDrawingBuffer: true,
          });
          renderer.setPixelRatio(1);
          renderer.setSize(W, H, false);
          renderer.setClearColor(new THREEJS.Color(BACKDROP), 1);

          var scene = new THREEJS.Scene();
          scene.fog = new THREEJS.Fog(new THREEJS.Color(BACKDROP), 1, FOG_FAR * SEGMENT_DEPTH);

          // Camera sits still at the origin looking down -z; the corridor moves.
          // Same picture as a moving camera, and the modulo keeps every coordinate
          // bounded no matter how long the clip runs.
          var camera = new THREEJS.PerspectiveCamera(FOV, W / H, 0.05, 40);
          camera.position.set(0, 0, 0);
          camera.lookAt(0, 0, -1);

          var PLANE = new THREEJS.PlaneGeometry(1, 1);

          var COLOR_MATS = PALETTE.map(function (hex) {
            return new THREEJS.MeshBasicMaterial({ color: new THREEJS.Color(hex), fog: true });
          });
          if (COLOR_MATS.length === 0) {
            COLOR_MATS.push(new THREEJS.MeshBasicMaterial({ color: 0xffffff, fog: true }));
          }

          // Caller-supplied content slots. Real <img> elements (so the page load
          // event waits for the decode) used directly as WebGL textures.
          var assets = document.getElementById("gt-assets");
          var IMAGES = IMAGE_SRCS.map(function (src, k) {
            var el = document.createElement("img");
            el.id = "gt-img-" + k;
            el.alt = "";
            el.src = src;
            assets.appendChild(el);

            var tex = new THREEJS.Texture(el);
            tex.minFilter = THREEJS.LinearFilter;
            tex.magFilter = THREEJS.LinearFilter;
            tex.generateMipmaps = false;
            var rec = {
              aspect: 0,
              mat: new THREEJS.MeshBasicMaterial({ map: tex, fog: true }),
            };
            function ready() {
              if (!el.naturalWidth) return;
              rec.aspect = el.naturalWidth / el.naturalHeight;
              tex.needsUpdate = true;
            }
            el.addEventListener("load", ready);
            ready(); // a cache hit can complete before the listener is attached
            return rec;
          });

          var edgeMat = new THREEJS.MeshBasicMaterial({
            color: new THREEJS.Color(EDGE_COLOR),
            fog: true,
            transparent: true,
            opacity: EDGE_OPACITY,
          });

          var HW = TUNNEL_WIDTH / 2;
          var HH = TUNNEL_HEIGHT / 2;
          var cellZ = SEGMENT_DEPTH / GRID;
          var cellX = TUNNEL_WIDTH / GRID;
          var cellY = TUNNEL_HEIGHT / GRID;

          // Four faces of a segment box. `place` maps a cell to a world position
          // inside the segment (origin at the segment's near boundary, growing
          // toward -z); `rot` orients the unit plane into that face, inward-facing.
          // `cw`/`ch` are the cell's extent in the plane's own local axes.
          var FACES = [
            {
              // floor
              rot: [-Math.PI / 2, 0, 0],
              cw: cellX,
              ch: cellZ,
              place: function (iu, iz) {
                return [-HW + (iu + 0.5) * cellX, -HH - SKIN, -(iz + 0.5) * cellZ];
              },
            },
            {
              // ceiling
              rot: [Math.PI / 2, 0, 0],
              cw: cellX,
              ch: cellZ,
              place: function (iu, iz) {
                return [-HW + (iu + 0.5) * cellX, HH + SKIN, -(iz + 0.5) * cellZ];
              },
            },
            {
              // left wall
              rot: [0, Math.PI / 2, 0],
              cw: cellZ,
              ch: cellY,
              place: function (iu, iz) {
                return [-HW - SKIN, -HH + (iu + 0.5) * cellY, -(iz + 0.5) * cellZ];
              },
            },
            {
              // right wall
              rot: [0, -Math.PI / 2, 0],
              cw: cellZ,
              ch: cellY,
              place: function (iu, iz) {
                return [HW + SKIN, -HH + (iu + 0.5) * cellY, -(iz + 0.5) * cellZ];
              },
            },
          ];

          var UP = new THREEJS.Vector3(0, 1, 0);
          // Edges are solid tube geometry, not lines: a cylinder thickens with
          // perspective the way a real moulding does, which is the whole reason
          // the corridor reads as built rather than drawn.
          function tube(group, ax, ay, az, bx, by, bz) {
            var dx = bx - ax,
              dy = by - ay,
              dz = bz - az;
            var len = Math.sqrt(dx * dx + dy * dy + dz * dz);
            if (len < 1e-6) return;
            var mesh = new THREEJS.Mesh(
              new THREEJS.CylinderGeometry(LINE_RADIUS, LINE_RADIUS, len, 6, 1),
              edgeMat,
            );
            mesh.position.set((ax + bx) / 2, (ay + by) / 2, (az + bz) / 2);
            mesh.quaternion.setFromUnitVectors(UP, new THREEJS.Vector3(dx / len, dy / len, dz / len));
            group.add(mesh);
          }

          function buildEdges(group) {
            var D = SEGMENT_DEPTH;
            var k, j, u, z;
            // The four corner runs (and any interior wall divisions) come from the
            // side walls; floor/ceiling only contribute their interior lines, so
            // corners are drawn once and never double-blend at 50% opacity.
            for (k = 1; k < GRID; k++) {
              u = -HW + k * cellX;
              tube(group, u, -HH, 0, u, -HH, -D);
              tube(group, u, HH, 0, u, HH, -D);
            }
            for (k = 0; k <= GRID; k++) {
              u = -HH + k * cellY;
              tube(group, -HW, u, 0, -HW, u, -D);
              tube(group, HW, u, 0, HW, u, -D);
            }
            // Ring lines at each depth boundary. The far boundary belongs to the
            // next segment, so stop one short and the tiling stays seamless.
            for (j = 0; j < GRID; j++) {
              z = -j * cellZ;
              tube(group, -HW, -HH, z, HW, -HH, z);
              tube(group, -HW, HH, z, HW, HH, z);
              tube(group, -HW, -HH, z, -HW, HH, z);
              tube(group, HW, -HH, z, HW, HH, z);
            }
          }

          var segs = [];
          for (var i = 0; i < NUM_SEGMENTS; i++) {
            var group = new THREEJS.Group();
            var panels = [];
            for (var f = 0; f < FACES.length; f++) {
              var face = FACES[f];
              for (var iz = 0; iz < GRID; iz++) {
                for (var iu = 0; iu < GRID; iu++) {
                  var mesh = new THREEJS.Mesh(PLANE, COLOR_MATS[0]);
                  var p = face.place(iu, iz);
                  mesh.position.set(p[0], p[1], p[2]);
                  mesh.rotation.set(face.rot[0], face.rot[1], face.rot[2]);
                  mesh.visible = false;
                  group.add(mesh);
                  panels.push({ mesh: mesh, cw: face.cw * INSET, ch: face.ch * INSET });
                }
              }
            }
            buildEdges(group);
            scene.add(group);
            segs.push({ group: group, panels: panels });
          }

          // Contain-fit so a supplied screenshot keeps its aspect inside the slot
          // instead of being squashed to the cell (wall cells are portrait).
          function fit(p, ar) {
            if (!ar) {
              p.mesh.scale.set(p.cw, p.ch, 1);
              return;
            }
            if (ar > p.cw / p.ch) p.mesh.scale.set(p.cw, p.cw / ar, 1);
            else p.mesh.scale.set(p.ch * ar, p.ch, 1);
          }

          // Everything about frame t is computed from t alone. `d` is the distance
          // flown in segments; `a` is the ABSOLUTE segment number occupying pool
          // slot i right now, which is what content hashes and the every-other
          // rhythm key off — so a segment carries the same panels every time it is
          // looked at, no matter how the timeline is scrubbed.
          function draw(t) {
            var d = SPEED * t;
            for (var i = 0; i < NUM_SEGMENTS; i++) {
              var seg = segs[i];
              var a = i + NUM_SEGMENTS * Math.ceil((d - i - BACK) / NUM_SEGMENTS);
              seg.group.position.z = -(a - d) * SEGMENT_DEPTH;

              var lit = a % 2 === 0;
              var panels = seg.panels;
              for (var s = 0; s < panels.length; s++) {
                var p = panels[s];
                if (!lit || hash2(a, s * 4 + 1) >= FILL) {
                  p.mesh.visible = false;
                  continue;
                }
                p.mesh.visible = true;
                if (IMAGES.length > 0 && hash2(a, s * 4 + 2) < IMAGE_MIX) {
                  var im = IMAGES[Math.floor(hash2(a, s * 4 + 4) * IMAGES.length) % IMAGES.length];
                  p.mesh.material = im.mat;
                  fit(p, im.aspect);
                } else {
                  var ci = Math.floor(hash2(a, s * 4 + 3) * COLOR_MATS.length) % COLOR_MATS.length;
                  p.mesh.material = COLOR_MATS[ci];
                  p.mesh.scale.set(p.cw, p.ch, 1);
                }
              }
            }
            renderer.render(scene, camera);
          }

          window.__timelines = window.__timelines || {};
          var tl = gsap.timeline({ paused: true });

          // Repaint from a property SETTER, not from onUpdate: gsap's seek(t)
          // suppresses events by default, so an onUpdate callback silently never
          // fires on a scrub and the canvas freezes on frame 0. Tweened values are
          // always written during render, suppressed or not.
          var driver = { _t: 0 };
          Object.defineProperty(driver, "t", {
            get: function () {
              return this._t;
            },
            set: function (v) {
              this._t = v;
              draw(v);
            },
          });
          tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
          window.__timelines["gallery-tunnel"] = tl;

          draw(0);
        })();
      </script>
    </body>
  </html>
  ```
</Accordion>

Tagged `webgl` `3d` `background` `showcase` `depth` `loop`.

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)


## Related topics

- [CLI](/packages/cli.md)
- [Changelog](/changelog.md)
- [Verified example prompts](/prompting/examples.md)
