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

# Rack Focus

> A focus pull with real aperture bokeh: the focal plane travels from a near subject to a far one, so one resolves as the other blows into hard-edged polygon discs that clip to a cat's eye toward the corners. A focus pull only reads against content at two clearly separated depths, so this block carries its own depth-layered night exterior and puts both subject depths on variables.

# Rack Focus

A focus pull with real aperture bokeh: the focal plane travels from a near subject to a far one, so one resolves as the other blows into hard-edged polygon discs that clip to a cat's eye toward the corners. A focus pull only reads against content at two clearly separated depths, so this block carries its own depth-layered night exterior and puts both subject depths on variables.

**Rack Focus**

```html rack-focus.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <title>Rack Focus</title>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <!--
      RACK FOCUS: a focus pull with real aperture bokeh.

      WHAT THIS NEEDS TO READ AT ALL
      ------------------------------
      A focus pull is a depth effect. It only reads when the frame holds
      content at two clearly separated depths: something near the lens and
      something far behind it. Point it at flat, single-plane content and
      nothing happens, exactly like a dolly zoom on a flat card.

      This block therefore carries its own depth-layered scene, a night
      exterior built from light sources spread from 1.15 m to 90 m, so it
      works standalone. Every depth is a variable: `nearfocus` is where the
      pull starts, `farfocus` is where it ends, and the scene's two subjects
      sit at those depths. Change them and the subjects move with them.

      WHY THIS IS NOT A BLUR
      ----------------------
      A CSS/Gaussian blur softens everything uniformly. A real defocus turns
      each point of light into an image of the APERTURE, scaled by its circle
      of confusion, so a bright point becomes a hard-edged polygon disc whose
      size grows with distance from the focal plane, and which clips to a
      cat's-eye toward the frame corners. Every light in this scene is
      splatted as an aperture-shaped sprite at its own circle of confusion.

      CIRCLE OF CONFUSION, from three.js BokehShader2 (MIT), Martins Upitis
      --------------------------------------------------------------------
      Read from examples/jsm/shaders/BokehShader2.js:

          float CoC = 0.03;              // circle of confusion in mm
                                         // (35mm film = 0.03mm)
          float f = focalLength;         // mm
          float d = fDepth * 1000.0;     // focal plane in mm
          float o = depth  * 1000.0;     // object depth in mm
          float a = (o * f) / (o - f);
          float b = (d * f) / (d - f);
          float c = (d - f) / (d * fstop * CoC);
          blur = abs(a - b) * c;

      `blur` comes out in units of that 0.03 mm acceptable-sharpness circle,
      so the defocus DIAMETER on the sensor is `blur * 0.03` mm, which this
      block converts to pixels with the sensor width. Same constants, same
      formula, independently written, no code copied. This agrees with the
      Zeiss thin-lens form CoC = (f²/N)·|1/S - 1/U| for S >> f.

      Aperture shape is the community-standard regular-polygon boundary
      d(θ) = cos(π/n) / cos(mod(θ, 2π/n) - π/n), n = blade count (real
      irises ship 5, 6, 8 or 9 blades). Cat's-eye clipping is the aperture
      intersected with two barrel openings offset along the radial direction,
      which is the actual mechanism of mechanical vignetting.

      DETERMINISM
      -----------
      State at frame N is computed from N. The focal distance is a closed-form
      function of t, every circle of confusion follows from a static depth and
      that focal distance, and the scene point cloud is built once from a
      seeded PRNG. No accumulation, no clocks, no unseeded randomness.
    -->
    <style>
      *,
      *::before,
      *::after {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        background: #000;
        overflow: hidden;
      }
      #rf-root {
        position: relative;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
      }
      #rf-backdrop {
        position: absolute;
        inset: 0;
        background: #05060a;
      }
      #rf-canvas {
        position: absolute;
        top: 0;
        left: 0;
        width: 1920px;
        height: 1080px;
      }
    </style>
  </head>
  <body>
    <div
      id="rf-root"
      data-composition-id="rack-focus"
      data-root="true"
      data-width="1920"
      data-height="1080"
      data-start="0"
      data-duration="6"
      data-composition-variables='[
        {"id":"nearfocus","type":"number","label":"Near focal distance","default":1.2,"min":0.2,"max":100,"step":0.05,"unit":"m"},
        {"id":"farfocus","type":"number","label":"Far focal distance","default":80,"min":0.3,"max":400,"step":0.5,"unit":"m"},
        {"id":"focallength","type":"number","label":"Focal length","default":85,"min":12,"max":300,"step":1,"unit":"mm"},
        {"id":"aperture","type":"number","label":"Aperture (f-number)","default":1.8,"min":0.95,"max":22,"step":0.05},
        {"id":"blades","type":"number","label":"Aperture blades (bokeh shape)","default":6,"min":3,"max":14,"step":1},
        {"id":"catseye","type":"number","label":"Cat eye clipping at the corners","default":0.62,"min":0,"max":1,"step":0.02},
        {"id":"pullstart","type":"number","label":"Pull start","default":1.2,"min":0,"max":30,"step":0.05,"unit":"s"},
        {"id":"pullduration","type":"number","label":"Pull duration","default":3.2,"min":0.1,"max":30,"step":0.05,"unit":"s"},
        {"id":"pullease","type":"string","label":"Pull easing (GSAP ease)","default":"power2.inOut","placeholder":"power2.inOut"},
        {"id":"bokeh","type":"number","label":"Bokeh exposure","default":1,"min":0,"max":3,"step":0.05},
        {"id":"backdrop","type":"color","label":"Backdrop","default":"#05060a"}
      ]'
    >
      <div id="rf-backdrop"></div>
      <canvas id="rf-canvas" width="1920" height="1080"></canvas>

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

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

        // 36mm-wide sensor, 16:9 active area. Pixels per millimetre is the
        // only thing the projection needs, and it is the same on both axes.
        var SENSOR_W_MM = 36;
        var PX_PER_MM = W / SENSOR_W_MM;

        // BokehShader2's acceptable-sharpness circle, in mm (35mm film).
        var COC_MM = 0.03;

        // Defocus is clamped so a wildly out-of-range focus setting cannot
        // splat sprites the size of the frame. BokehShader2 clamps the same
        // quantity with its `maxblur` uniform.
        var MAX_COC_R_PX = 0.1 * H;
        // Smallest sprite half-width. Below roughly one pixel a splat is an
        // aliasing machine, so points in focus bottom out here.
        var MIN_R_PX = 0.75;

        var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};
        var CS = getComputedStyle(document.getElementById("rf-root"));

        // The runtime defines every declared variable as `--<slug>` on the
        // root (packages/core/src/tokenSlug.ts), so a host stylesheet can
        // override one there too. Read the custom property first, fall back
        // to the declared value when it is unset.
        function raw(id) {
          var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
          return css !== "" ? css : V[id];
        }
        function num(id, fallback) {
          var n = parseFloat(raw(id));
          return isFinite(n) ? n : fallback;
        }

        var NEAR = num("nearfocus", 1.2);
        var FAR = num("farfocus", 80);
        var FOCAL = num("focallength", 85);
        var FSTOP = num("aperture", 1.8);
        var BLADES = Math.max(3, Math.round(num("blades", 6)));
        var CATSEYE = num("catseye", 0.62);
        var PULL_START = num("pullstart", 1.2);
        var PULL_DUR = Math.max(0.001, num("pullduration", 3.2));
        var BOKEH = num("bokeh", 1);
        var EASE_NAME = String(raw("pullease") || "power2.inOut");
        var BACKDROP =
          typeof raw("backdrop") === "string" && raw("backdrop") ? raw("backdrop") : "#05060a";

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

        var EASE = gsap.parseEase(EASE_NAME) || gsap.parseEase("power2.inOut");

        // A focus ring is roughly linear in dioptres, not in metres: a rack
        // from 1.2m to 80m spends its first millimetre of barrel rotation
        // crossing most of the distance. Interpolating 1/distance is what
        // makes the pull travel evenly instead of snapping to the far plane.
        function focusAt(t) {
          var u = Math.min(1, Math.max(0, (t - PULL_START) / PULL_DUR));
          var e = EASE(u);
          var inv = 1 / NEAR + (1 / FAR - 1 / NEAR) * e;
          return 1 / inv;
        }

        // ── Scene ────────────────────────────────────────────────────────
        // A night exterior: a string of practical lights right in front of
        // the lens, a lit city block far behind it, and scattered lights
        // through every depth between so the pull reads as a continuous
        // travel rather than a cut between two planes.

        function mulberry32(a) {
          return function () {
            a |= 0;
            a = (a + 0x6d2b79f5) | 0;
            var t = Math.imul(a ^ (a >>> 15), 1 | a);
            t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
            return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
          };
        }
        var rnd = mulberry32(0x5eed1a3);
        function rr(lo, hi) {
          return lo + (hi - lo) * rnd();
        }

        // x, y in metres (y up, origin on the optical axis), z in metres,
        // r0 = the light's own physical radius in metres, b = peak
        // brightness when perfectly in focus (values above 1 are highlights
        // that clip, which is exactly why they stay visible once spread
        // across a bokeh disc), rgb = colour.
        var P = [];
        function light(x, y, z, r0, b, c) {
          P.push(x, y, z, r0, b, c[0], c[1], c[2]);
        }

        var TUNGSTEN = [1.0, 0.74, 0.45];
        var FILAMENT = [1.0, 0.9, 0.74];
        var WIRE = [0.86, 0.74, 0.6];
        var WARM_WIN = [1.0, 0.79, 0.52];
        var COOL_WIN = [0.6, 0.75, 1.0];
        var SIGN_A = [0.35, 0.95, 1.0];
        var SIGN_B = [1.0, 0.42, 0.72];

        // Half the frame's width, in metres, at depth z. Both subjects are
        // laid out against the reference framing (85mm, 1.2m / 80m) and then
        // scaled by this, so retuning the lens or either focal distance moves
        // the subjects with the frame instead of pushing them out of it.
        function halfW(z) {
          return (0.5 * SENSOR_W_MM * z) / FOCAL;
        }

        // Near subject: a catenary string of bulbs at `nearfocus`. The wire is
        // what makes "sharp" unmistakable, a one-pixel line either resolves
        // or it does not, and the filament inside each bulb is the second cue.
        var NEAR_Z = NEAR;
        var NS = halfW(NEAR_Z) / 0.25412;
        var X_END = 0.4;
        var SAG_A = 0.3;
        var SAG = 0.1;
        var COSH_END = Math.cosh(X_END / SAG_A);
        function stringY(x) {
          var s = (COSH_END - Math.cosh(x / SAG_A)) / (COSH_END - 1);
          return 0.075 - SAG * s - 0.035 * (x / X_END);
        }
        function stringZ(x) {
          return NEAR_Z + 0.05 * (x / X_END);
        }
        for (var i = 0; i < 1100; i++) {
          var wx = -0.42 + (0.84 * i) / 1099;
          light(wx * NS, stringY(wx) * NS, stringZ(wx), 0.0006 * NS, 1.4, WIRE);
        }
        for (var k = -5; k <= 5; k++) {
          var bx = k * 0.085;
          var by = stringY(bx) - 0.011;
          var bz = stringZ(bx);
          light(bx * NS, by * NS, bz, 0.006 * NS, 26, TUNGSTEN);
          light((bx - 0.0022) * NS, by * NS, bz, 0.0006 * NS, 7, FILAMENT);
          light(bx * NS, (by - 0.0022) * NS, bz, 0.0006 * NS, 7, FILAMENT);
          light((bx + 0.0022) * NS, by * NS, bz, 0.0006 * NS, 7, FILAMENT);
        }

        // Far subject: three lit towers plus a dense LED sign strip, sitting
        // around `farfocus`. The sign's pitch is fine enough that it only
        // resolves into separate lamps when focus actually arrives.
        var FAR_Z = FAR;
        var FS = halfW(FAR_Z) / 16.941;
        function tower(cx, hw, topY, botY, cols, rows, z, lit) {
          for (var c = 0; c < cols; c++) {
            for (var r = 0; r < rows; r++) {
              if (rnd() > lit) continue;
              var x = cx - hw + (2 * hw * (c + 0.5)) / cols;
              var y = botY + ((topY - botY) * (r + 0.5)) / rows;
              var cool = rnd() < 0.28;
              light(x, y, z + rr(-0.4, 0.4) * FS, 0.24 * FS, rr(9, 20), cool ? COOL_WIN : WARM_WIN);
            }
          }
        }
        tower(-9.5 * FS, 3.5 * FS, 9.6 * FS, -4.0 * FS, 6, 14, FAR_Z * 1.03, 0.34);
        tower(2.0 * FS, 4.5 * FS, 6.4 * FS, -4.0 * FS, 8, 12, FAR_Z * 0.98, 0.3);
        tower(12.5 * FS, 3.0 * FS, 11.0 * FS, -4.0 * FS, 5, 15, FAR_Z * 1.08, 0.36);
        for (var s = 0; s < 40; s++) {
          var sx = (-5.5 + (11 * s) / 39) * FS;
          var mixc = s / 39;
          var sc = [
            SIGN_A[0] + (SIGN_B[0] - SIGN_A[0]) * mixc,
            SIGN_A[1] + (SIGN_B[1] - SIGN_A[1]) * mixc,
            SIGN_A[2] + (SIGN_B[2] - SIGN_A[2]) * mixc,
          ];
          light(sx, -5.0 * FS, FAR_Z * 0.99, 0.1 * FS, 7, sc);
          light(sx, -5.55 * FS, FAR_Z * 0.99, 0.1 * FS, 7, sc);
        }

        // Everything between. Depth is drawn log-uniform between the two
        // subjects and the screen position is uniform, so the mid-ground
        // stays evenly spread whatever the two focal distances are.
        var Z_LO = Math.min(NEAR_Z, FAR_Z) * 1.9;
        var Z_HI = Math.max(NEAR_Z, FAR_Z) * 0.85;
        for (var m = 0; m < 90; m++) {
          var z = Z_LO * Math.pow(Z_HI / Z_LO, rnd());
          var halfWm = (0.5 * SENSOR_W_MM * z) / FOCAL;
          var halfHm = (halfWm * H) / W;
          var warm = rnd() < 0.66;
          light(
            rr(-1.05, 1.05) * halfWm,
            rr(-1.0, 0.75) * halfHm,
            z,
            rr(0.006, 0.05) * (z / 12),
            rr(5, 18),
            warm ? WARM_WIN : COOL_WIN,
          );
        }

        // ── GL ───────────────────────────────────────────────────────────
        var canvas = document.getElementById("rf-canvas");
        var gl =
          canvas.getContext("webgl", {
            alpha: true,
            antialias: false,
            depth: false,
            stencil: false,
            preserveDrawingBuffer: true,
            powerPreference: "high-performance",
          }) ||
          canvas.getContext("experimental-webgl", {
            alpha: true,
            preserveDrawingBuffer: true,
          });

        var VERT = [
          "precision highp float;",
          "attribute vec2 aCorner;", // -1..1 quad corner
          "attribute vec3 aPos;", // metres, y up, z away from the lens
          "attribute vec2 aSize;", // x = own radius (m), y = in-focus peak
          "attribute vec3 aColor;",
          "uniform vec2 uRes;",
          "uniform float uPxPerMm;",
          "uniform float uFocal;", // mm
          "uniform float uFocus;", // metres
          "uniform float uFstop;",
          "uniform float uCoCmm;",
          "uniform float uMaxR;",
          "uniform float uMinR;",
          "uniform float uCatsEye;",
          "uniform float uBokeh;",
          "varying vec2 vQ;",
          "varying vec3 vColor;",
          "varying float vGain;",
          "varying float vShape;",
          "varying float vAA;",
          "varying vec2 vRadial;",
          "varying float vCat;",
          "void main() {",
          "  float z = max(aPos.z, 0.001);",
          "  float ppm = uPxPerMm * uFocal / z;", // pixels per metre at this depth
          "  vec2 centre = uRes * 0.5 + aPos.xy * ppm;",
          "  float r0 = max(aSize.x * ppm, uMinR);",
          "",
          "  // BokehShader2's circle of confusion, in units of uCoCmm.",
          "  float f = uFocal;",
          "  float d = uFocus * 1000.0;",
          "  float o = z * 1000.0;",
          "  float a = (o * f) / max(o - f, 1e-4);",
          "  float b = (d * f) / max(d - f, 1e-4);",
          "  float c = (d - f) / max(d * uFstop * uCoCmm, 1e-6);",
          "  float blur = abs(a - b) * c;",
          "  // -> defocus diameter in mm -> pixels -> radius.",
          "  float cocR = min(blur * uCoCmm * uPxPerMm * 0.5, uMaxR);",
          "",
          "  // A finite source convolved with the defocus disc: radii add in",
          "  // quadrature. Flux is conserved, so peak brightness falls as the",
          "  // inverse square of the radius. That single term is why an",
          "  // in-focus lamp clips to white and a defocused one is a readable",
          "  // disc instead of a smear.",
          "  float R = sqrt(r0 * r0 + cocR * cocR);",
          "  vGain = aSize.y * uBokeh * (r0 * r0) / (R * R);",
          "  // Near focus the sprite is the lamp (round); far from it the",
          "  // sprite is an image of the aperture (polygonal).",
          "  vShape = (cocR * cocR) / (cocR * cocR + r0 * r0);",
          "  vAA = 1.0 / max(R, 0.5);",
          "  vColor = aColor;",
          "  vQ = aCorner;",
          "",
          "  // Mechanical vignetting: the barrel openings clip the aperture",
          "  // harder the further the sprite sits from the optical axis.",
          "  vec2 off = centre - uRes * 0.5;",
          "  float rad = length(off);",
          "  vRadial = rad > 1.0 ? off / rad : vec2(1.0, 0.0);",
          "  vCat = uCatsEye * min(1.0, rad / (length(uRes) * 0.5));",
          "",
          "  // A light spread thin enough to land under half a display code",
          "  // value contributes nothing but fill rate. Culling it here is what",
          "  // keeps a 900-point wire from splatting 900 invisible discs the",
          "  // moment it goes out of focus.",
          "  if (vGain < 0.0015) {",
          "    gl_Position = vec4(2.0, 2.0, 2.0, 1.0);",
          "    return;",
          "  }",
          "",
          "  vec2 p = centre + aCorner * R;",
          "  gl_Position = vec4((p / uRes) * 2.0 - 1.0, 0.0, 1.0);",
          "}",
        ].join("\n");

        var FRAG = [
          "precision highp float;",
          "varying vec2 vQ;",
          "varying vec3 vColor;",
          "varying float vGain;",
          "varying float vShape;",
          "varying float vAA;",
          "varying vec2 vRadial;",
          "varying float vCat;",
          "uniform float uBlades;",
          "const float PI = 3.14159265;",
          "void main() {",
          "  float r = length(vQ);",
          "  if (r > 1.0) discard;",
          "  float th = r > 1e-5 ? atan(vQ.y, vQ.x) : 0.0;",
          "",
          "  // Regular-polygon aperture boundary: circumradius 1 at a blade",
          "  // vertex, cos(PI/n) at a blade midpoint.",
          "  float n = uBlades;",
          "  float seg = 2.0 * PI / n;",
          "  float poly = cos(PI / n) / cos(mod(th, seg) - PI / n);",
          "  float bound = mix(1.0, poly, vShape);",
          "",
          "  float cov = smoothstep(bound, bound - vAA, r);",
          "  // Two offset barrel openings cut the disc from opposite sides,",
          "  // which is what turns a corner bokeh into a cat's eye.",
          "  float cut = vCat * vShape;",
          "  cov *= smoothstep(1.0, 1.0 - vAA, length(vQ - vRadial * cut));",
          "  cov *= smoothstep(1.0, 1.0 - vAA, length(vQ + vRadial * cut));",
          "",
          "  gl_FragColor = vec4(vColor * (vGain * cov), 1.0);",
          "}",
        ].join("\n");

        var uni = {};
        var ready = false;
        var count = 0;

        function compile(type, src) {
          var sh = gl.createShader(type);
          gl.shaderSource(sh, src);
          gl.compileShader(sh);
          if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
            throw new Error("rack-focus shader: " + gl.getShaderInfoLog(sh));
          }
          return sh;
        }

        function hexToRgb(hex) {
          var h = String(hex).trim().replace("#", "");
          if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
          var v = parseInt(h, 16);
          if (!isFinite(v)) return [0.02, 0.024, 0.039];
          return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255];
        }
        var BG = hexToRgb(BACKDROP);

        if (gl) {
          var prog = gl.createProgram();
          gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
          gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
          gl.linkProgram(prog);
          if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
            throw new Error("rack-focus link: " + gl.getProgramInfoLog(prog));
          }
          gl.useProgram(prog);

          // Six vertices per light: two triangles carrying the same point
          // payload and four distinct corner offsets.
          var CORNERS = [
            [-1, -1],
            [1, -1],
            [1, 1],
            [-1, -1],
            [1, 1],
            [-1, 1],
          ];
          var n = P.length / 8;
          count = n * 6;
          var STRIDE = 10;
          var data = new Float32Array(count * STRIDE);
          var w = 0;
          for (var pi = 0; pi < n; pi++) {
            var o = pi * 8;
            for (var ci = 0; ci < 6; ci++) {
              data[w++] = CORNERS[ci][0];
              data[w++] = CORNERS[ci][1];
              data[w++] = P[o];
              data[w++] = P[o + 1];
              data[w++] = P[o + 2];
              data[w++] = P[o + 3];
              data[w++] = P[o + 4];
              data[w++] = P[o + 5];
              data[w++] = P[o + 6];
              data[w++] = P[o + 7];
            }
          }

          var buf = gl.createBuffer();
          gl.bindBuffer(gl.ARRAY_BUFFER, buf);
          gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
          var BYTES = STRIDE * 4;
          [
            ["aCorner", 2, 0],
            ["aPos", 3, 8],
            ["aSize", 2, 20],
            ["aColor", 3, 28],
          ].forEach(function (spec) {
            var loc = gl.getAttribLocation(prog, spec[0]);
            gl.enableVertexAttribArray(loc);
            gl.vertexAttribPointer(loc, spec[1], gl.FLOAT, false, BYTES, spec[2]);
          });

          [
            "uRes",
            "uPxPerMm",
            "uFocal",
            "uFocus",
            "uFstop",
            "uCoCmm",
            "uMaxR",
            "uMinR",
            "uCatsEye",
            "uBlades",
            "uBokeh",
          ].forEach(function (nm) {
            uni[nm] = gl.getUniformLocation(prog, nm);
          });

          gl.viewport(0, 0, W, H);
          gl.uniform2f(uni.uRes, W, H);
          gl.uniform1f(uni.uPxPerMm, PX_PER_MM);
          gl.uniform1f(uni.uFocal, FOCAL);
          gl.uniform1f(uni.uFstop, FSTOP);
          gl.uniform1f(uni.uCoCmm, COC_MM);
          gl.uniform1f(uni.uMaxR, MAX_COC_R_PX);
          gl.uniform1f(uni.uMinR, MIN_R_PX);
          gl.uniform1f(uni.uCatsEye, CATSEYE);
          gl.uniform1f(uni.uBlades, BLADES);
          gl.uniform1f(uni.uBokeh, BOKEH);

          // Light adds to light. Overlapping bokeh discs are brighter where
          // they cross, which is the whole texture of a bokeh field.
          gl.disable(gl.DEPTH_TEST);
          gl.enable(gl.BLEND);
          gl.blendFunc(gl.ONE, gl.ONE);
          ready = true;
        }

        // Every frame is computed from t alone: the focal distance is a
        // closed-form function of t, and each sprite's circle of confusion
        // falls out of its own static depth and that distance.
        function draw(t) {
          if (!ready) return;
          gl.uniform1f(uni.uFocus, focusAt(t));
          gl.clearColor(BG[0], BG[1], BG[2], 1);
          gl.clear(gl.COLOR_BUFFER_BIT);
          gl.drawArrays(gl.TRIANGLES, 0, count);
          gl.flush();
        }

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

        // The canvas is repainted 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,
        // so this fires on every seek, and hands us the frame time directly.
        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["rack-focus"] = tl;

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

Copy npx hyperframes add rack-focus to the clipboard

That writes one file: `compositions/rack-focus.html`.

## 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="rack-focus"
  data-composition-src="compositions/rack-focus.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.

## Change how it looks

Set these CSS variables on the block:

* `--nearfocus` — Near focal distance in metres (where the pull starts and the near subject sits). Defaults to `1.2`.
* `--farfocus` — Far focal distance in metres (where the pull ends and the far subject sits). Defaults to `80`.
* `--focallength` — Focal length in mm (longer lens = shallower depth of field). Defaults to `85`.
* `--aperture` — Aperture f-number (lower = bigger bokeh). Defaults to `1.8`.
* `--blades` — Aperture blade count, the bokeh polygon's sides. Defaults to `6`.
* `--catseye` — Cat's-eye clipping at the frame corners (mechanical vignetting). Defaults to `0.62`.
* `--pullstart` — Seconds held on the near subject before the pull starts. Defaults to `1.2`.
* `--pullduration` — Pull duration in seconds. Defaults to `3.2`.
* `--pullease` — Pull easing (any GSAP ease name). Defaults to `power2.inOut`.
* `--bokeh` — Bokeh exposure. Defaults to `1`.
* `--backdrop` — Backdrop. Defaults to `#05060a`.

## 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 |
| - | - | - | - |
| `nearfocus` | `1.2` | 0.2m to 100m, step 0.05m | |
| `farfocus` | `80` | 0.3m to 400m, step 0.5m | |
| `focallength` | `85` | 12mm to 300mm, step 1mm | |
| `aperture` | `1.8` | 0.95 to 22, step 0.05 | |
| `blades` | `6` | 3 to 14, step 1 | |
| `catseye` | `0.62` | 0 to 1, step 0.02 | |
| `pullstart` | `1.2` | 0s to 30s, step 0.05s | |
| `pullduration` | `3.2` | 0.1s to 30s, step 0.05s | |
| `pullease` | `power2.inOut` | string | |
| `bokeh` | `1` | 0 to 3, step 0.05 | |
| `backdrop` | `#05060a` | 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
<div
  data-composition-id="rack-focus"
  data-composition-src="compositions/rack-focus.html"
  data-variable-values='{"nearfocus":1.2,"farfocus":80,"focallength":85,"aperture":1.8,"blades":6,"catseye":0.62,"pullstart":1.2,"pullduration":3.2,"pullease":"power2.inOut","bokeh":1,"backdrop":"#05060a"}'
></div>
```

{/* hf:generated-footer */}

Tagged `webgl` `shader` `camera` `depth` `cinematic` `showcase`.

## 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)
