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

# Ordered Dither Pass

> Bayer-matrix ordered dithering quantizes a slotted scene (rasterized once at mount): the image emerges from pure 2-tone noise to clean, or dissolves the reverse. Stateless per frame, never error diffusion.

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

## Install

<InstallCommand command="npx hyperframes add ordered-dither-pass" />

That writes one file: `compositions/components/ordered-dither-pass.html`.

## Paste it into your composition

Open `compositions/components/ordered-dither-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                                                                                             |
| ----------- | ------- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
| `matrix`    | `4`     | `2`, `4`, `8`             | Bayer matrix size: 2 is the coarsest crosshatch, 8 the smoothest ordered grain.                          |
| `direction` | `in`    | `in`, `out`               | in resolves noise to clean; out dissolves clean to noise and holds it.                                   |
| `levels`    | `2`     | 2 to 6, step 1            | Quantization levels per color channel during the dithered phase.                                         |
| `accent`    | `green` | `green`, `blue`, `violet` | Light tone of the 2-tone noise phase: green rides --brand, blue rides --accent, violet rides --accent-2. |
| `exit`      | `none`  | `none`, `fade`, `up`      | Optional departure. Default none: the final state 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="ordered-dither-pass"
  data-composition-src="compositions/components/ordered-dither-pass.html"
  data-variable-values='{"matrix":"4","direction":"in","levels":2,"accent":"green","exit":"none"}'
></div>
```

## Source

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

    Bayer-matrix ordered dithering quantizes a slotted scene. The source is
    rasterized ONCE at mount to a reduced-resolution offscreen buffer; every
    frame re-derives each output pixel from (source pixel, Bayer threshold,
    the animated dither amount), so the pass is stateless per frame. With
    direction "in" the image emerges from pure 2-tone noise (theme bg vs
    accent) into posterized color and finally clean; "out" runs the same
    ramp in reverse and holds the noise. Explicitly ordered (parallel,
    frame-independent) dithering, never error diffusion.

    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="ordered-dither-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. When no slot exists, the primitive rasterizes
    a token monogram tile drawn synchronously from contract tokens.

    Variables (declared in data-composition-variables below):
      - matrix ("2" | "4" | "8", default "4"): Bayer matrix size; 2 is the
        coarsest crosshatch, 8 the smoothest ordered grain.
      - direction ("in" | "out", default "in"): in resolves noise to clean;
        out dissolves clean to noise and holds it.
      - levels (2-6, default 2): quantization levels per color channel
        during the dithered phase.
      - accent ("green" | "blue" | "violet", default "green"): the light
        tone of the 2-tone noise phase; green rides --brand, blue rides
        --accent, violet rides --accent-2.
      - exit ("none" | "fade" | "up", default "none"): frame roots own
        transitions; the default holds the final state to the cut.

    Envelope, fixed segments with elastic HOLD only (never timeScale):
      0.00s   the field fades up over 0.25s in its start state
      0.40s   the dither amount ramps across 1.6s (power1.inOut)
      then    HOLD, truly still (clean for "in", pure noise for "out")
      OUT = 0.45s only when exit is not none

    Determinism (canvas 2D law): the source buffer, the Bayer table, and
    every resolved color are fixed once at mount. Every painted frame is a
    pure function of (those tables, the timeline's current time): onUpdate
    rewrites one reused ImageData end to end and blits it, scaled with
    image smoothing off (the chunky retro pixel is the point). 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="ordered-dither-pass"
    data-composition-duration="3.5"
    data-composition-variables='[
      { "id": "matrix", "type": "enum", "role": "style", "label": "Matrix", "description": "Bayer matrix size: 2 is the coarsest crosshatch, 8 the smoothest ordered grain.", "default": "4", "options": [{ "value": "2", "label": "2 x 2" }, { "value": "4", "label": "4 x 4" }, { "value": "8", "label": "8 x 8" }] },
      { "id": "direction", "type": "enum", "role": "timing", "label": "Direction", "description": "in resolves noise to clean; out dissolves clean to noise and holds it.", "default": "in", "options": [{ "value": "in", "label": "Noise to clean" }, { "value": "out", "label": "Clean to noise" }] },
      { "id": "levels", "type": "number", "role": "style", "label": "Levels", "description": "Quantization levels per color channel during the dithered phase.", "default": 2, "min": 2, "max": 6, "step": 1 },
      { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Light tone of the 2-tone noise phase: 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 final state 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>Ordered Dither Pass</title>
    </head>
    <body>
      <template>
        <div id="root" data-composition-id="ordered-dither-pass" data-duration="3.5" 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-display, "Inter", system-ui, sans-serif);
              pointer-events: none;
            }

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

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

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

          <div
            id="ordered-dither-pass-clip"
            class="odp-clip clip"
            data-start="0"
            data-duration="3.5"
            data-track-index="0"
          >
            <div class="odp-stage">
              <canvas class="odp-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(".odp-stage");
              var canvas = root.querySelector(".odp-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 matrixSize = vars.matrix === "2" || vars.matrix === "8" ? Number(vars.matrix) : 4;
              var direction = vars.direction === "out" ? "out" : "in";
              var levels = Math.round(num(vars.levels, 2, 2, 6));
              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 || "3.5"));
              var OUT = exit === "none" ? 0 : 0.45;
              var RAMP_AT = 0.4;
              var RAMP = Math.min(1.6, Math.max(0.6, duration - OUT - RAMP_AT - 0.4));
              var FADE_IN = 0.25;
              var OUT_START = duration - OUT;

              // Display canvas basis, fixed once at mount.
              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");
              ctx.imageSmoothingEnabled = false;

              // Reduced-resolution pixel buffer: one dither pixel is about 3
              // display px (capped 640 wide), the chunky retro read AND the
              // per-frame loop budget.
              var bufW = Math.max(8, Math.min(640, Math.round(cssW / 3)));
              var bufH = Math.max(8, Math.round(bufW * (cssH / cssW)));

              // Resolve tokens to concrete values once at mount.
              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;
              }
              function parseRgb(color, fallback) {
                var m = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(color);
                return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : fallback;
              }
              var bgColor = resolveColor("var(--bg, #0b0c0e)", "#0b0c0e");
              var bgRgb = parseRgb(bgColor, [11, 12, 14]);
              var accentRgb = parseRgb(
                resolveColor(accentColors[accent], "#71f5a7"),
                [113, 245, 167],
              );
              var monoFamily = (function () {
                var probe = hostDoc.createElement("span");
                probe.style.fontFamily =
                  'var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace)';
                root.appendChild(probe);
                var out = getComputedStyle(probe).fontFamily || "monospace";
                probe.remove();
                return out;
              })();

              // Bayer matrix, generated recursively; thresholds normalized to
              // (i + 0.5) / n^2 in [0, 1).
              var bayer = (function () {
                var m = [[0]];
                var size = 1;
                while (size < matrixSize) {
                  var next = [];
                  for (var y = 0; y < size * 2; y += 1) next.push(new Array(size * 2));
                  for (var yy = 0; yy < size; yy += 1) {
                    for (var xx = 0; xx < size; xx += 1) {
                      var v = 4 * m[yy][xx];
                      next[yy][xx] = v;
                      next[yy][xx + size] = v + 2;
                      next[yy + size][xx] = v + 3;
                      next[yy + size][xx + size] = v + 1;
                    }
                  }
                  m = next;
                  size *= 2;
                }
                var n2 = matrixSize * matrixSize;
                var flat = new Float64Array(n2);
                for (var by = 0; by < matrixSize; by += 1) {
                  for (var bx = 0; bx < matrixSize; bx += 1) {
                    flat[by * matrixSize + bx] = (m[by][bx] + 0.5) / n2;
                  }
                }
                return flat;
              })();

              // Source raster at buffer resolution, composited over the bg.
              var raster = hostDoc.createElement("canvas");
              raster.width = bufW;
              raster.height = bufH;
              var rctx = raster.getContext("2d");

              var srcData = null; // Uint8ClampedArray, fixed after raster
              var rasterReady = false;

              function paintMonogram(target, w, h) {
                var m = Math.min(w, h);
                target.fillStyle = bgColor;
                target.fillRect(0, 0, w, h);
                var tile = m * 0.62;
                var x = (w - tile) / 2;
                var y = (h - tile) / 2;
                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, m * 0.06);
                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.36) + "px " + monoFamily;
                target.textAlign = "center";
                target.textBaseline = "middle";
                target.fillText("H", w / 2, h / 2 + m * 0.014);
              }

              function finishRaster() {
                srcData = rctx.getImageData(0, 0, bufW, bufH).data;
                rasterReady = true;
                root.setAttribute("data-odp-raster", "ready");
                paint(tl ? tl.time() : 0);
              }

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

              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 || "";
                }
                // Snapshot at css size for faithful layout, then downsample
                // into the buffer raster.
                var wrapper = hostDoc.createElement("div");
                wrapper.setAttribute(
                  "style",
                  "width:" +
                    cssW +
                    "px;height:" +
                    cssH +
                    "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="' +
                  cssW +
                  '" height="' +
                  cssH +
                  '"><foreignObject width="' +
                  cssW +
                  '" height="' +
                  cssH +
                  '">' +
                  serialized +
                  "</foreignObject></svg>";
                var snapshot = new Image();
                snapshot.onload = function () {
                  rctx.fillStyle = bgColor;
                  rctx.fillRect(0, 0, bufW, bufH);
                  rctx.drawImage(snapshot, 0, 0, bufW, bufH);
                  try {
                    finishRaster();
                  } catch (error) {
                    // Tainted or unreadable snapshot: fall back to the monogram.
                    paintMonogram(rctx, bufW, bufH);
                    finishRaster();
                  }
                };
                snapshot.onerror = function () {
                  paintMonogram(rctx, bufW, bufH);
                  finishRaster();
                };
                snapshot.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svgMarkup);
              } else {
                paintMonogram(rctx, bufW, bufH);
                srcData = rctx.getImageData(0, 0, bufW, bufH).data;
                rasterReady = true;
                root.setAttribute("data-odp-raster", "ready");
              }

              var outImage = rctx.createImageData(bufW, bufH);

              function clamp01(v) {
                return v < 0 ? 0 : v > 1 ? 1 : v;
              }

              // The whole pass is one scalar a(t) in [0, 1]: 1 = pure 2-tone
              // noise, 0 = clean source.
              //   contrast = clamp01((1 - a) * 1.6)  signal emerges under dither
              //   cleanMix = clamp01(((1 - a) - 0.55) / 0.45)  dither releases
              //   duoMix   = clamp01((a - 0.55) / 0.45)  noise phase is duotone
              function amountAt(t) {
                var p = clamp01((t - RAMP_AT) / RAMP);
                // power1.inOut
                p = p < 0.5 ? 2 * p * p : 1 - 2 * (1 - p) * (1 - p);
                return direction === "in" ? 1 - p : p;
              }

              // Pure repaint from (srcData, bayer, t): every output pixel is
              // rewritten, no state survives between frames.
              function paint(timeSeconds) {
                ctx.fillStyle = bgColor;
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                if (!rasterReady) return;
                var fade = clamp01(timeSeconds / FADE_IN);
                var a = amountAt(timeSeconds);
                var contrast = clamp01((1 - a) * 1.6);
                var cleanMix = clamp01((1 - a - 0.55) / 0.45);
                var duoMix = clamp01((a - 0.55) / 0.45);
                var L1 = levels - 1;
                var out = outImage.data;
                for (var y = 0; y < bufH; y += 1) {
                  var brow = (y % matrixSize) * matrixSize;
                  for (var x = 0; x < bufW; x += 1) {
                    var th = bayer[brow + (x % matrixSize)];
                    var i = (y * bufW + x) * 4;
                    var vr = 0.5 + (srcData[i] / 255 - 0.5) * contrast;
                    var vg = 0.5 + (srcData[i + 1] / 255 - 0.5) * contrast;
                    var vb = 0.5 + (srcData[i + 2] / 255 - 0.5) * contrast;
                    var qr = Math.floor(vr * L1 + th) / L1;
                    var qg = Math.floor(vg * L1 + th) / L1;
                    var qb = Math.floor(vb * L1 + th) / L1;
                    qr = qr < 0 ? 0 : qr > 1 ? 1 : qr;
                    qg = qg < 0 ? 0 : qg > 1 ? 1 : qg;
                    qb = qb < 0 ? 0 : qb > 1 ? 1 : qb;
                    // Noise phase is a BINARY luminance threshold, independent
                    // of levels: odd level counts put flat mid-gray exactly on
                    // a quantization step, which would render the "pure noise"
                    // pole as a flat field instead of the Bayer pattern.
                    var lum = 0.299 * vr + 0.587 * vg + 0.114 * vb;
                    var tone = lum > th ? 1 : 0;
                    var dr = bgRgb[0] + (accentRgb[0] - bgRgb[0]) * tone;
                    var dg = bgRgb[1] + (accentRgb[1] - bgRgb[1]) * tone;
                    var db = bgRgb[2] + (accentRgb[2] - bgRgb[2]) * tone;
                    var r = qr * 255 + (dr - qr * 255) * duoMix;
                    var g = qg * 255 + (dg - qg * 255) * duoMix;
                    var b = qb * 255 + (db - qb * 255) * duoMix;
                    r += (srcData[i] - r) * cleanMix;
                    g += (srcData[i + 1] - g) * cleanMix;
                    b += (srcData[i + 2] - b) * cleanMix;
                    out[i] = r;
                    out[i + 1] = g;
                    out[i + 2] = b;
                    out[i + 3] = 255;
                  }
                }
                rctx.putImageData(outImage, 0, 0);
                ctx.globalAlpha = fade;
                ctx.drawImage(raster, 0, 0, bufW, bufH, 0, 0, canvas.width, canvas.height);
                ctx.globalAlpha = 1;
                // putImageData reuses the raster canvas as a blit surface;
                // srcData is a detached copy, so the source stays pristine.
              }

              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: truly still (a(t) is constant past the ramp, the source
              // 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["ordered-dither-pass"] = tl;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `motion-primitive` `experiment` `texture` `dither` `transition` `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

- [Apply media effects](/guides/media-effects.md)
- [Color grading and film effects](/prompting/color-grading.md)
- [Stagger Cascade](/catalog/components/stagger-cascade.md)
