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

# ASCII Render Pass

> A slotted scene renders as live ASCII: a canvas samples the source's luminance grid (rasterized once at mount) and draws glyphs from a density ramp charset; resolution steps coarse to fine as the piece resolves, then holds legible-fine.

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="ascii-render-pass 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/components/ascii-render-pass.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/components/ascii-render-pass.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add ascii-render-pass" />

That writes one file: `compositions/components/ascii-render-pass.html`.

## Paste it into your composition

Open `compositions/components/ascii-render-pass.html` and copy what is inside into your own composition.

A component has no size or duration of its own. It takes both from the composition
you paste it into.

## 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                                                                  |
| ------------ | ------------ | ------------------------- | ----------------------------------------------------------------------------- |
| `charset`    | ` .:-=+*#%@` | string                    | Density ramp, dark to light. A leading space keeps dark cells empty.          |
| `grid`       | `med`        | `coarse`, `med`, `fine`   | Final cell pitch the resolve lands on.                                        |
| `resolve_at` | `1.4`        | 0.6s to 10s, step 0.1s    | Seconds from mount start when the coarse-to-fine resolve begins.              |
| `accent`     | `green`      | `green`, `blue`, `violet` | Glyph ink: green rides --brand, blue rides --accent, violet rides --accent-2. |
| `exit`       | `none`       | `none`, `fade`, `up`      | Optional departure. Default none: the fine render holds until the frame cuts. |

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="ascii-render-pass"
  data-composition-src="compositions/components/ascii-render-pass.html"
  data-variable-values='{"charset":" .:-=+*#%@","grid":"med","resolve_at":1.4,"accent":"green","exit":"none"}'
></div>
```

## Source

<Accordion title={`ascii-render-pass.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    ascii-render-pass: HyperFrames video primitive (Wave M experiment, texture)

    A slotted scene renders as live ASCII: the source is rasterized ONCE at
    mount to an offscreen canvas, its luminance grid is sampled per cell, and
    each cell draws a glyph from a density ramp charset. Resolution animates
    coarse to fine on a stepped pitch ladder as the piece resolves, then
    holds legible-fine. The DOM source itself never displays; the canvas is
    the whole picture.

    The scene is a SLOT. Callers supply content by placing an inert template
    anywhere in the HOST page (templates never render, and the runtime wipes
    the host clip's own children on mount):

      <template data-slot="ascii-render-pass-scene"> ... </template>

    Slot content is rasterized through an SVG foreignObject snapshot, so it
    must be self-contained: inline styles or rules carried by host <style>
    tags (which are copied into the snapshot), data-URI images only, system
    or already-active fonts. External resources do not load inside the
    snapshot. When no slot exists, the primitive rasterizes a token monogram
    tile drawn synchronously from contract tokens (no snapshot involved).

    Variables (declared in data-composition-variables below):
      - charset (string): density ramp, dark to light, first glyph may be a
        space so dark cells stay empty. Default " .:-=+*#%@".
      - grid ("coarse" | "med" | "fine", default "med"): final cell pitch.
      - resolve_at (seconds): when the coarse-to-fine resolve begins.
      - accent ("green" | "blue" | "violet", default "green"): glyph ink;
        green rides --brand, blue rides --accent, violet rides --accent-2.
      - exit ("none" | "fade" | "up", default "none"): frame roots own
        transitions; the default holds the fine render to the cut.

    Envelope, fixed segments with elastic HOLD only (never timeScale):
      0.00s        canvas fades up over 0.5s at the coarsest pitch
      resolve_at   pitch ladder steps coarse to fine over 1.2s (5 steps,
                   powers of sqrt(2)); a per-cell hash shimmer rides the
                   steps and lands at zero on the final step
      then         legible-fine HOLD, truly still
      OUT = 0.45s  only when exit is not none

    Determinism (canvas 2D law): the raster, its luminance summed-area
    table, and every resolved color are fixed once at mount. Every painted
    frame is a pure function of (that raster, the timeline's current time):
    the timeline's onUpdate clears the canvas and redraws every glyph from
    scratch. Cell shimmer uses a stateless integer hash of (column, row,
    ladder step), not a stream; there is no Math.random, no wall clock, no
    incremental state. Eventful seeks (suppressEvents false, the engine's
    render path) land identical frames in any order and either direction.
    The no-slot monogram raster is drawn synchronously before the timeline
    registers; a slotted raster arrives from the snapshot decode and
    repaints the current frame, after which all frames are pure f(t).
  -->
  <html
    lang="en"
    data-composition-id="ascii-render-pass"
    data-composition-duration="4"
    data-composition-variables='[
      { "id": "charset", "type": "string", "role": "style", "label": "Charset", "description": "Density ramp, dark to light. A leading space keeps dark cells empty.", "default": " .:-=+*#%@" },
      { "id": "grid", "type": "enum", "role": "style", "label": "Grid", "description": "Final cell pitch the resolve lands on.", "default": "med", "options": [{ "value": "coarse", "label": "Coarse" }, { "value": "med", "label": "Medium" }, { "value": "fine", "label": "Fine" }] },
      { "id": "resolve_at", "type": "number", "role": "timing", "label": "Resolve at", "description": "Seconds from mount start when the coarse-to-fine resolve begins.", "default": 1.4, "min": 0.6, "max": 10, "step": 0.1, "unit": "s" },
      { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Glyph ink: green rides --brand, blue rides --accent, violet rides --accent-2.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
      { "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure. Default none: the fine render holds until the frame cuts.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>ASCII Render Pass</title>
    </head>
    <body>
      <template>
        <div id="root" data-composition-id="ascii-render-pass" data-duration="4" data-fps="30">
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            #root {
              position: absolute;
              inset: 0;
              overflow: hidden;
              container-type: size;
              isolation: isolate;
              color: var(--fg, #f8fafc);
              font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
              pointer-events: none;
            }

            .arp-clip {
              position: absolute;
              inset: 0;
              overflow: hidden;
              background: var(--bg, transparent);
            }

            .arp-stage {
              position: absolute;
              inset: 0;
              will-change: transform, opacity;
            }

            .arp-canvas {
              position: absolute;
              inset: 0;
              width: 100%;
              height: 100%;
            }
          </style>

          <div
            id="ascii-render-pass-clip"
            class="arp-clip clip"
            data-start="0"
            data-duration="4"
            data-track-index="0"
          >
            <div class="arp-stage">
              <canvas class="arp-canvas" aria-hidden="true"></canvas>
            </div>
          </div>

          <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
          <script>
            (function () {
              "use strict";

              var root = document.getElementById("root");
              var stage = root.querySelector(".arp-stage");
              var canvas = root.querySelector(".arp-canvas");
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              function num(value, fallback, min, max) {
                var n = Number(value);
                if (!isFinite(n)) n = fallback;
                return Math.max(min, Math.min(max, n));
              }

              var charset =
                typeof vars.charset === "string" && vars.charset.length >= 2
                  ? vars.charset
                  : " .:-=+*#%@";
              var grid = vars.grid === "coarse" || vars.grid === "fine" ? vars.grid : "med";
              var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
              // Each enum choice routes to a DIFFERENT contract token so the
              // variable stays meaningful under a theme.
              var accentColors = {
                green: "var(--brand, #71f5a7)",
                blue: "var(--accent, #61a8ff)",
                violet: "var(--accent-2, #c5a3ff)",
              };
              var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
                ? vars.accent
                : "green";
              // Bundler mirrors composition variables as scoped CSS custom
              // props; when this unit's accent variable shadows the --accent
              // token ("blue" is a valid CSS color), fall back to the literal.
              var computedAccent = getComputedStyle(root).getPropertyValue("--accent").trim();
              if (
                computedAccent === "green" ||
                computedAccent === "blue" ||
                computedAccent === "violet"
              ) {
                accentColors.blue = "#61a8ff";
              }

              // Envelope.
              var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
              var OUT = exit === "none" ? 0 : 0.45;
              var RESOLVE = Math.min(1.2, Math.max(0.5, duration - OUT - 1.0));
              var FADE_IN = 0.5;
              var maxAt = Math.max(0.6, duration - RESOLVE - OUT - 0.05);
              var resolveAt = num(vars.resolve_at, 1.4, 0.6, maxAt);
              var OUT_START = duration - OUT;

              // Canvas raster basis, fixed once at mount. Elastic root: the
              // host box decides the resolution.
              var dpr = Math.min(2, Math.max(1, Number(window.devicePixelRatio) || 1));
              var box = root.getBoundingClientRect();
              var cssW = Math.max(1, Math.round(box.width) || 640);
              var cssH = Math.max(1, Math.round(box.height) || 360);
              canvas.width = Math.round(cssW * dpr);
              canvas.height = Math.round(cssH * dpr);
              var ctx = canvas.getContext("2d");

              // Resolve tokens to concrete values once at mount so nothing in
              // the paint path depends on style recalc during seeks.
              var hostDoc = root.ownerDocument;
              function resolveColor(cssColor, fallback) {
                var probe = hostDoc.createElement("span");
                probe.style.color = cssColor;
                root.appendChild(probe);
                var out = getComputedStyle(probe).color || fallback;
                probe.remove();
                return out;
              }
              var bgColor = resolveColor("var(--bg, #0b0c0e)", "#0b0c0e");
              var inkColor = resolveColor(accentColors[accent], "#71f5a7");
              var probeFont = hostDoc.createElement("span");
              probeFont.style.fontFamily =
                'var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace)';
              root.appendChild(probeFont);
              var monoFamily = getComputedStyle(probeFont).fontFamily || "monospace";
              probeFont.remove();

              // Source raster: cssW x cssH capped to 1280 wide, composited
              // over the resolved bg so luminance never depends on alpha.
              var rasterScale = Math.min(1, 1280 / cssW);
              var rw = Math.max(1, Math.round(cssW * rasterScale));
              var rh = Math.max(1, Math.round(cssH * rasterScale));
              var raster = hostDoc.createElement("canvas");
              raster.width = rw;
              raster.height = rh;
              var rctx = raster.getContext("2d");

              // Luminance summed-area table so any cell's average is O(1) and
              // a pure function of the fixed raster.
              var sat = null; // Float64Array (rw+1)*(rh+1)
              function buildSat() {
                var data = rctx.getImageData(0, 0, rw, rh).data;
                var table = new Float64Array((rw + 1) * (rh + 1));
                for (var y = 0; y < rh; y += 1) {
                  var rowSum = 0;
                  for (var x = 0; x < rw; x += 1) {
                    var i = (y * rw + x) * 4;
                    rowSum += (0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]) / 255;
                    table[(y + 1) * (rw + 1) + (x + 1)] = table[y * (rw + 1) + (x + 1)] + rowSum;
                  }
                }
                sat = table;
              }
              function cellLuma(x0, y0, x1, y1) {
                // Raster-space integer rect, clamped.
                if (x1 <= x0 || y1 <= y0) return 0;
                var s = rw + 1;
                var sum = sat[y1 * s + x1] - sat[y0 * s + x1] - sat[y1 * s + x0] + sat[y0 * s + x0];
                return sum / ((x1 - x0) * (y1 - y0));
              }

              function paintMonogram(target, w, h) {
                // Synchronous token monogram raster: a surface tile with one
                // display glyph, same vocabulary as the slot-less siblings.
                var m = Math.min(w, h);
                target.fillStyle = bgColor;
                target.fillRect(0, 0, w, h);
                var tile = m * 0.52;
                var x = (w - tile) / 2;
                var y = (h - tile) / 2;
                var r = m * 0.05;
                target.fillStyle = resolveColor(
                  "color-mix(in srgb, var(--surface, #14171c) 72%, " + accentColors[accent] + ")",
                  "#1a2a22",
                );
                target.beginPath();
                if (target.roundRect) target.roundRect(x, y, tile, tile, r);
                else target.rect(x, y, tile, tile);
                target.fill();
                target.fillStyle = resolveColor(
                  "color-mix(in srgb, var(--fg, #f8fafc) 30%, " + accentColors[accent] + ")",
                  "#a8f5c8",
                );
                target.font = "600 " + Math.round(m * 0.3) + "px " + monoFamily;
                target.textAlign = "center";
                target.textBaseline = "middle";
                target.fillText("H", w / 2, h / 2 + m * 0.012);
              }

              // SLOT. Rasterized through an SVG foreignObject snapshot of the
              // slot markup plus every host <style> tag and the resolved
              // contract tokens, so host-authored rules and theme values
              // apply inside the snapshot.
              var slotTemplate = null;
              try {
                slotTemplate = hostDoc.querySelector('template[data-slot="ascii-render-pass-scene"]');
              } catch (error) {
                slotTemplate = null;
              }

              var rasterReady = false;
              function finishRaster() {
                buildSat();
                rasterReady = true;
                root.setAttribute("data-arp-raster", "ready");
                paint(tl ? tl.time() : 0);
              }

              if (slotTemplate) {
                var tokenNames = [
                  "--bg",
                  "--fg",
                  "--muted",
                  "--surface",
                  "--border",
                  "--brand",
                  "--accent",
                  "--accent-2",
                  "--font-display",
                  "--font-body",
                  "--font-mono",
                  "--radius",
                  "--space-1",
                  "--space-2",
                  "--space-3",
                  "--dur-beat",
                  "--ease-standard",
                  "--ease-emphasis",
                ];
                var rootStyle = getComputedStyle(root);
                var tokenDecl = "";
                for (var ti = 0; ti < tokenNames.length; ti += 1) {
                  var tv = rootStyle.getPropertyValue(tokenNames[ti]).trim();
                  if (tv) tokenDecl += tokenNames[ti] + ":" + tv + ";";
                }
                var styleTexts = "";
                var styleTags = hostDoc.querySelectorAll("style");
                for (var si = 0; si < styleTags.length; si += 1) {
                  styleTexts += styleTags[si].textContent || "";
                }
                var wrapper = hostDoc.createElement("div");
                wrapper.setAttribute(
                  "style",
                  "width:" +
                    rw +
                    "px;height:" +
                    rh +
                    "px;overflow:hidden;position:relative;" +
                    "display:grid;place-items:center;background:" +
                    bgColor +
                    ";" +
                    tokenDecl,
                );
                wrapper.appendChild(hostDoc.importNode(slotTemplate.content, true));
                var styleEl = hostDoc.createElement("style");
                styleEl.textContent = styleTexts;
                wrapper.insertBefore(styleEl, wrapper.firstChild);
                var serialized = new XMLSerializer().serializeToString(wrapper);
                var svgMarkup =
                  // No xmlns on foreignObject: a default xmlns declaration
                  // applies to the declaring element itself and would eject
                  // foreignObject from the SVG namespace (renders blank). The
                  // serialized wrapper div already carries the XHTML namespace.
                  '<svg xmlns="http://www.w3.org/2000/svg" width="' +
                  rw +
                  '" height="' +
                  rh +
                  '"><foreignObject width="' +
                  rw +
                  '" height="' +
                  rh +
                  '">' +
                  serialized +
                  "</foreignObject></svg>";
                var snapshot = new Image();
                snapshot.onload = function () {
                  rctx.fillStyle = bgColor;
                  rctx.fillRect(0, 0, rw, rh);
                  rctx.drawImage(snapshot, 0, 0, rw, rh);
                  try {
                    finishRaster();
                  } catch (error) {
                    // Tainted or unreadable snapshot: fall back to the monogram.
                    paintMonogram(rctx, rw, rh);
                    finishRaster();
                  }
                };
                snapshot.onerror = function () {
                  paintMonogram(rctx, rw, rh);
                  finishRaster();
                };
                snapshot.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svgMarkup);
              } else {
                paintMonogram(rctx, rw, rh);
              }

              // Final cell pitch (display px) per grid enum; the ladder steps
              // down from 4x on powers of sqrt(2). Stepped, not continuous:
              // each step is a stable re-sample, the digital resolve.
              var rowsFinal = { coarse: 22, med: 36, fine: 54 };
              var cellFinal = Math.max(4, cssH / rowsFinal[grid]);
              var LADDER = [4, 2.83, 2, 1.41, 1];
              var LAST = LADDER.length - 1;

              function clamp01(v) {
                return v < 0 ? 0 : v > 1 ? 1 : v;
              }
              // Stateless integer hash for per-cell shimmer: pure function of
              // (col, row, step), no stream, no state.
              function hash01(cx, cy, step) {
                var h =
                  Math.imul(cx + 1, 0x9e3779b1) ^
                  Math.imul(cy + 1, 0x85ebca6b) ^
                  Math.imul(step + 1, 0xc2b2ae35);
                h = Math.imul(h ^ (h >>> 15), 0x27d4eb2f);
                h = (h ^ (h >>> 13)) >>> 0;
                return h / 4294967296;
              }

              var glyphs = charset.split("");
              var GN = glyphs.length;

              // Pure repaint from (raster tables, t).
              function paint(timeSeconds) {
                ctx.fillStyle = bgColor;
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                if (!rasterReady) return;
                var t = timeSeconds;
                var fade = clamp01(t / FADE_IN);
                fade = 1 - (1 - fade) * (1 - fade); // power2.out
                if (fade <= 0) return;
                var rp = clamp01((t - resolveAt) / RESOLVE);
                rp = rp * rp * (3 - 2 * rp); // smoothstep across the ladder
                var step = Math.min(LAST, Math.floor(rp * LAST + 1e-6));
                if (rp >= 1) step = LAST;
                var cell = cellFinal * LADDER[step];
                var cols = Math.ceil(cssW / cell);
                var rows = Math.ceil(cssH / cell);
                var cellDev = cell * dpr;
                ctx.font = "600 " + cellDev * 0.92 + "px " + monoFamily;
                ctx.textAlign = "center";
                ctx.textBaseline = "middle";
                ctx.fillStyle = inkColor;
                // Shimmer amplitude decays to exactly zero on the final step
                // so the HOLD is truly still.
                var shimmerAmp = step === LAST && rp >= 1 ? 0 : 0.16;
                for (var cy = 0; cy < rows; cy += 1) {
                  for (var cx = 0; cx < cols; cx += 1) {
                    var x0 = Math.round(cx * cell * rasterScale);
                    var y0 = Math.round(cy * cell * rasterScale);
                    var x1 = Math.min(rw, Math.round((cx + 1) * cell * rasterScale));
                    var y1 = Math.min(rh, Math.round((cy + 1) * cell * rasterScale));
                    var luma = cellLuma(x0, Math.min(y0, rh), x1, Math.min(y1, rh));
                    var idx = Math.round(luma * (GN - 1));
                    if (idx <= 0 && glyphs[0] === " ") continue;
                    var alpha = (0.3 + 0.7 * luma) * fade;
                    if (shimmerAmp > 0) {
                      alpha *= 1 - shimmerAmp + shimmerAmp * hash01(cx, cy, step);
                    }
                    if (alpha <= 0.01) continue;
                    ctx.globalAlpha = clamp01(alpha);
                    ctx.fillText(glyphs[idx], (cx + 0.5) * cellDev, (cy + 0.5) * cellDev);
                  }
                }
                ctx.globalAlpha = 1;
              }

              gsap.set(stage, { opacity: 1, y: "0cqh" });

              var tl = gsap.timeline({
                paused: true,
                onUpdate: function () {
                  paint(tl.time());
                },
              });

              // Anchor tween: spans the full authored duration so onUpdate
              // (the canvas repaint) fires for every eventful seek in [0, D]
              // and holds keep tl.time() honest.
              tl.to({ t: 0 }, { t: 1, duration: duration, ease: "none" }, 0);

              // HOLD: legible-fine, truly still (shimmer amplitude is zero
              // past the final ladder step; the raster never changes).

              // OUT: optional departure; exit none holds until the frame cuts.
              if (exit === "up") {
                tl.to(stage, { y: "-4cqh", duration: OUT, ease: "power2.in" }, OUT_START);
                tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
              } else if (exit === "fade") {
                tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
              }

              tl.seek(0);
              paint(0);
              window.__timelines = window.__timelines || {};
              window.__timelines["ascii-render-pass"] = tl;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `motion-primitive` `experiment` `texture` `ascii` `quantize`.

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

- [ASCII Trail Reveal](/catalog/components/ascii-trail-reveal.md)
- [Light Sweep Pass](/catalog/components/light-sweep-pass.md)
- [Render templates on Lambda](/deploy/templates-on-lambda.md)
