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

# Halftone Field

> A full-bleed halftone light field: a grid of dots whose size and colour track a slow flowing simplex-noise field, so broad bands of light sweep across a mostly-black frame like a giant LED wall showing a plasma. Built to run under type.

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

## Install

<InstallCommand command="npx hyperframes add halftone-field" />

That writes one file: `compositions/halftone-field.html`.

## Add it to your video

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

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

* `--frequency` — Field frequency (1 = broad swells, 10 = fine mottle). Defaults to `2`.
* `--speed` — Flow speed (0 freezes the field). Defaults to `5`.
* `--cellsize` — Cell size (dot pitch, 1 = 6px, 100 = 60px). Defaults to `54`.
* `--gamma` — Gamma: midtone crush, the whole look. Low values flatten it to an even grey dot field. Defaults to `12`.
* `--palettebias` — Palette bias: floor under every dot's size and palette position. 0 lets the darks go fully black. Defaults to `2`.
* `--palette1` — Palette stop 1 (lows). Defaults to `#00AAFF`.
* `--palette2` — Palette stop 2. Defaults to `#C2FF67`.
* `--palette3` — Palette stop 3. Defaults to `#6600FF`.
* `--palette4` — Palette stop 4 (crests). Defaults to `#000000`.
* `--background` — Background. Defaults to `#000000`.
* `--quantize` — Quantise to 30fps for a stepped LED-wall look. Defaults to `false`. Options: `false`, `true`.

## 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 |
| ------------- | --------- | ----------------- | ------------ |
| `frequency`   | `2`       | 1 to 10, step 0.1 |              |
| `speed`       | `5`       | 0 to 10, step 0.1 |              |
| `cellSize`    | `54`      | 1 to 100, step 1  |              |
| `gamma`       | `12`      | 1 to 20, step 0.1 |              |
| `paletteBias` | `2`       | 0 to 20, step 0.5 |              |
| `palette1`    | `#00AAFF` | color             |              |
| `palette2`    | `#C2FF67` | color             |              |
| `palette3`    | `#6600FF` | color             |              |
| `palette4`    | `#000000` | color             |              |
| `background`  | `#000000` | color             |              |
| `quantize`    | `false`   | boolean           |              |

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="halftone-field"
  data-composition-src="compositions/halftone-field.html"
  data-variable-values='{"frequency":2,"speed":5,"cellSize":54,"gamma":12,"paletteBias":2,"palette1":"#00AAFF","palette2":"#C2FF67","palette3":"#6600FF","palette4":"#000000","background":"#000000","quantize":false}'
></div>
```

## Source

<Accordion title={`halftone-field.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=1920, height=1080" />
      <title>Halftone Field</title>
      <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
      <style>
        *,
        *::before,
        *::after {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        body {
          background: #000;
          overflow: hidden;
        }
        #hlf-root {
          position: relative;
          width: 1920px;
          height: 1080px;
          overflow: hidden;
        }
        #hlf-backdrop {
          position: absolute;
          inset: 0;
          background: #000000;
        }
        #hlf-canvas {
          position: absolute;
          top: 0;
          left: 0;
          width: 1920px;
          height: 1080px;
        }
      </style>
    </head>
    <body>
      <div
        id="hlf-root"
        data-composition-id="halftone-field"
        data-root="true"
        data-width="1920"
        data-height="1080"
        data-start="0"
        data-duration="10"
        data-composition-variables='[
          {"id":"frequency","type":"number","label":"Field frequency","default":2,"min":1,"max":10,"step":0.1},
          {"id":"speed","type":"number","label":"Flow speed","default":5,"min":0,"max":10,"step":0.1},
          {"id":"cellSize","type":"number","label":"Cell size","default":54,"min":1,"max":100,"step":1},
          {"id":"gamma","type":"number","label":"Gamma (midtone crush)","default":12,"min":1,"max":20,"step":0.1},
          {"id":"paletteBias","type":"number","label":"Palette bias","default":2,"min":0,"max":20,"step":0.5},
          {"id":"palette1","type":"color","label":"Palette stop 1 (lows)","default":"#00AAFF"},
          {"id":"palette2","type":"color","label":"Palette stop 2","default":"#C2FF67"},
          {"id":"palette3","type":"color","label":"Palette stop 3","default":"#6600FF"},
          {"id":"palette4","type":"color","label":"Palette stop 4 (crests)","default":"#000000"},
          {"id":"background","type":"color","label":"Background","default":"#000000"},
          {"id":"quantize","type":"boolean","label":"Quantise to 30fps (stepped)","default":false}
        ]'
      >
        <div id="hlf-backdrop"></div>
        <canvas id="hlf-canvas" width="1920" height="1080"></canvas>

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

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

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

          // The runtime defines every declared variable as `--<id>` on the root,
          // 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;
          }
          function bool(id) {
            var v = raw(id);
            return v === true || /^(true|1|on|yes)$/i.test(String(v));
          }
          // #RGB / #RRGGBB -> [r, g, b] in 0..1. Anything else falls back.
          function rgb(id, fallback) {
            var s = String(raw(id) || "").trim();
            var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
            if (!m) s = fallback;
            else s = "#" + m[1];
            var hex = s.slice(1);
            if (hex.length === 3) {
              hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
            }
            var n = parseInt(hex, 16);
            return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
          }
          // The reference exposes 1-10 / 1-100 / 1-20 dials and maps them onto
          // the ranges the shader actually wants.
          function mapLinear(x, a1, a2, b1, b2) {
            return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);
          }

          var FREQ = mapLinear(num("frequency", 2), 1, 10, 0.3, 6);
          var SPEED = num("speed", 5) * 0.05;
          var CELL = mapLinear(num("cellSize", 54), 1, 100, 6, 60);
          var GAMMA = mapLinear(num("gamma", 12), 1, 20, 0.5, 8);
          var BIAS = num("paletteBias", 2) * 0.05;
          var PAL = [
            rgb("palette1", "#00AAFF"),
            rgb("palette2", "#C2FF67"),
            rgb("palette3", "#6600FF"),
            rgb("palette4", "#000000"),
          ];
          var BG = rgb("background", "#000000");
          var QUANTIZE = bool("quantize");

          document.getElementById("hlf-backdrop").style.background =
            "rgb(" +
            BG.map(function (c) {
              return Math.round(c * 255);
            }).join(",") +
            ")";

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

          var VERT = "attribute vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }";

          // Single pass. The reference renders the colour field into a target and
          // samples it at each cell centre, but the field is analytic, so
          // evaluating it at the cell centre here is the same value without the
          // round trip through a texture.
          var FRAG = `
  precision highp float;
  uniform vec2 uRes;
  uniform float uTime;
  uniform float uFreq;
  uniform float uSpeed;
  uniform float uCell;
  uniform float uGamma;
  uniform float uBias;
  uniform vec3 uPal0;
  uniform vec3 uPal1;
  uniform vec3 uPal2;
  uniform vec3 uPal3;
  uniform vec3 uBg;

  // 3D simplex noise — Ashima Arts / Stefan Gustavson (MIT).
  vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
  vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
  vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
  vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }

  float snoise(vec3 v) {
    const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
    const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);

    vec3 i = floor(v + dot(v, C.yyy));
    vec3 x0 = v - i + dot(i, C.xxx);

    vec3 g = step(x0.yzx, x0.xyz);
    vec3 l = 1.0 - g;
    vec3 i1 = min(g.xyz, l.zxy);
    vec3 i2 = max(g.xyz, l.zxy);

    vec3 x1 = x0 - i1 + C.xxx;
    vec3 x2 = x0 - i2 + C.yyy;
    vec3 x3 = x0 - D.yyy;

    i = mod289(i);
    vec4 p = permute(permute(permute(
        i.z + vec4(0.0, i1.z, i2.z, 1.0)) +
        i.y + vec4(0.0, i1.y, i2.y, 1.0)) +
        i.x + vec4(0.0, i1.x, i2.x, 1.0));

    float n_ = 0.142857142857;
    vec3 ns = n_ * D.wyz - D.xzx;

    vec4 j = p - 49.0 * floor(p * ns.z * ns.z);

    vec4 x_ = floor(j * ns.z);
    vec4 y_ = floor(j - 7.0 * x_);

    vec4 x = x_ * ns.x + ns.yyyy;
    vec4 y = y_ * ns.x + ns.yyyy;
    vec4 h = 1.0 - abs(x) - abs(y);

    vec4 b0 = vec4(x.xy, y.xy);
    vec4 b1 = vec4(x.zw, y.zw);

    vec4 s0 = floor(b0) * 2.0 + 1.0;
    vec4 s1 = floor(b1) * 2.0 + 1.0;
    vec4 sh = -step(h, vec4(0.0));

    vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
    vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;

    vec3 p0 = vec3(a0.xy, h.x);
    vec3 p1 = vec3(a0.zw, h.y);
    vec3 p2 = vec3(a1.xy, h.z);
    vec3 p3 = vec3(a1.zw, h.w);

    vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
    p0 *= norm.x;
    p1 *= norm.y;
    p2 *= norm.z;
    p3 *= norm.w;

    vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
    m = m * m;
    return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
  }

  // Full-saturation, full-value HSV: only the hue ramp survives.
  vec3 hue2rgb(float h) {
    return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
  }

  // Piecewise-linear ramp across the four palette stops.
  vec3 ramp(float v) {
    float x = clamp(v, 0.0, 1.0) * 3.0;
    vec3 c = mix(uPal0, uPal1, clamp(x, 0.0, 1.0));
    c = mix(c, uPal2, clamp(x - 1.0, 0.0, 1.0));
    c = mix(c, uPal3, clamp(x - 2.0, 0.0, 1.0));
    return c;
  }

  void main() {
    // One sample per cell, taken at the cell centre.
    vec2 cell = floor(gl_FragCoord.xy / uCell);
    vec2 centre = (cell + 0.5) * uCell;

    vec2 st = centre / uRes;
    st.x *= uRes.x / uRes.y;

    float hue = abs(snoise(vec3(st * uFreq, uTime * uSpeed)));
    vec3 field = hue2rgb(hue);

    // The gamma is the whole look: it crushes the midtones so most of the frame
    // stays background and only the crests of the field open up.
    float gray = pow(dot(field, vec3(0.2126, 0.7152, 0.0722)), uGamma);
    float v = clamp(gray + uBias, 0.0, 1.0);

    float radius = v * 0.5 * uCell;
    float d = length(gl_FragCoord.xy - centre);
    float mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, d);

    gl_FragColor = vec4(mix(uBg, ramp(v), mask), 1.0);
  }
  `;

          var uni = {};
          var ready = false;

          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("halftone-field shader: " + gl.getShaderInfoLog(sh));
            }
            return sh;
          }

          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("halftone-field link: " + gl.getProgramInfoLog(prog));
            }
            gl.useProgram(prog);

            var buf = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, buf);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
            var loc = gl.getAttribLocation(prog, "aPos");
            gl.enableVertexAttribArray(loc);
            gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

            [
              "uRes",
              "uTime",
              "uFreq",
              "uSpeed",
              "uCell",
              "uGamma",
              "uBias",
              "uPal0",
              "uPal1",
              "uPal2",
              "uPal3",
              "uBg",
            ].forEach(function (n) {
              uni[n] = gl.getUniformLocation(prog, n);
            });

            gl.viewport(0, 0, W, H);
            gl.uniform2f(uni.uRes, W, H);
            gl.uniform1f(uni.uFreq, FREQ);
            gl.uniform1f(uni.uSpeed, SPEED);
            gl.uniform1f(uni.uCell, CELL);
            gl.uniform1f(uni.uGamma, GAMMA);
            gl.uniform1f(uni.uBias, BIAS);
            gl.uniform3f(uni.uPal0, PAL[0][0], PAL[0][1], PAL[0][2]);
            gl.uniform3f(uni.uPal1, PAL[1][0], PAL[1][1], PAL[1][2]);
            gl.uniform3f(uni.uPal2, PAL[2][0], PAL[2][1], PAL[2][2]);
            gl.uniform3f(uni.uPal3, PAL[3][0], PAL[3][1], PAL[3][2]);
            gl.uniform3f(uni.uBg, BG[0], BG[1], BG[2]);
            ready = true;
          }

          // Every frame is computed from t alone — the field is a closed-form
          // function of position and time. No accumulator, no clock, no PRNG.
          // `quantize` snaps t to a 30fps grid for the stepped LED-wall look.
          function draw(t) {
            if (!ready) return;
            gl.uniform1f(uni.uTime, QUANTIZE ? Math.floor(t * 30) / 30 : t);
            gl.drawArrays(gl.TRIANGLES, 0, 3);
            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["halftone-field"] = tl;

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

Tagged `webgl` `shader` `background` `texture` `halftone`.

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

- [Halftone Dissolve](/catalog/components/halftone-dissolve.md)
- [Grain Field](/catalog/components/grain-field.md)
- [Apply media effects](/guides/media-effects.md)
