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

# Cosmic Orb

> An iridescent soap-bubble planet: a slowly spinning sphere of nebula bands, dust lanes and twinkling stars wrapped in a refracting glass limb with chromatic aberration at the edge.

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

## Install

<InstallCommand command="npx hyperframes add cosmic-orb" />

That writes one file: `compositions/cosmic-orb.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="cosmic-orb"
  data-composition-src="compositions/cosmic-orb.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:

* `--hue` — Nebula hue. Defaults to `268`.
* `--accent` — Accent hue. Defaults to `196`.
* `--spin` — Spin rate (turns per second). Defaults to `0.035`.
* `--stars` — Star density. Defaults to `1`.
* `--glow` — Glow intensity. Defaults to `1`.
* `--size` — Orb size (fraction of frame height). Defaults to `0.86`.
* `--pulse` — Beat pulse (0-1, flat value when no envelope is set). Defaults to `0`.
* `--pulseenvelope` — Beat pulse envelope: comma-separated 0-1 samples spread evenly across the clip. Defaults to \`\`.
* `--backdrop` — Backdrop. Defaults to `#04040a`.

## 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 |
| --------------- | --------- | ----------------------------------------- | ------------ |
| `hue`           | `268`     | 0deg to 360deg, step 1deg                 |              |
| `accent`        | `196`     | 0deg to 360deg, step 1deg                 |              |
| `spin`          | `0.035`   | 0turns/s to 0.5turns/s, step 0.005turns/s |              |
| `stars`         | `1`       | 0 to 2, step 0.05                         |              |
| `glow`          | `1`       | 0 to 2, step 0.05                         |              |
| `size`          | `0.86`    | 0.2 to 1.2, step 0.01                     |              |
| `pulse`         | `0`       | 0 to 1, step 0.01                         |              |
| `pulseEnvelope` | \`\`      | string                                    |              |
| `backdrop`      | `#04040a` | color                                     |              |

Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:

```html wrap theme={null}
<div
  data-composition-id="cosmic-orb"
  data-composition-src="compositions/cosmic-orb.html"
  data-variable-values='{"hue":268,"accent":196,"spin":0.035,"stars":1,"glow":1,"size":0.86,"pulse":0,"pulseEnvelope":"","backdrop":"#04040a"}'
></div>
```

## Source

<Accordion title={`cosmic-orb.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=1920, height=1080" />
      <title>Cosmic Orb</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;
        }
        #co-root {
          position: relative;
          width: 1920px;
          height: 1080px;
          overflow: hidden;
        }
        #co-backdrop {
          position: absolute;
          inset: 0;
          background: #04040a;
        }
        #co-canvas {
          position: absolute;
          top: 0;
          left: 0;
          width: 1920px;
          height: 1080px;
        }
      </style>
    </head>
    <body>
      <div
        id="co-root"
        data-composition-id="cosmic-orb"
        data-root="true"
        data-width="1920"
        data-height="1080"
        data-start="0"
        data-duration="10"
        data-composition-variables='[
          {"id":"hue","type":"number","label":"Nebula hue","default":268,"min":0,"max":360,"step":1,"unit":"deg"},
          {"id":"accent","type":"number","label":"Accent hue","default":196,"min":0,"max":360,"step":1,"unit":"deg"},
          {"id":"spin","type":"number","label":"Spin rate","default":0.035,"min":0,"max":0.5,"step":0.005,"unit":"turns/s"},
          {"id":"stars","type":"number","label":"Star density","default":1,"min":0,"max":2,"step":0.05},
          {"id":"glow","type":"number","label":"Glow intensity","default":1,"min":0,"max":2,"step":0.05},
          {"id":"size","type":"number","label":"Orb size","default":0.86,"min":0.2,"max":1.2,"step":0.01},
          {"id":"pulse","type":"number","label":"Beat pulse","default":0,"min":0,"max":1,"step":0.01},
          {"id":"pulseEnvelope","type":"string","label":"Beat pulse envelope (comma-separated 0-1 samples)","default":"","placeholder":"0,0.2,1,0.4,0"},
          {"id":"backdrop","type":"color","label":"Backdrop","default":"#04040a"}
        ]'
      >
        <div id="co-backdrop"></div>
        <canvas id="co-canvas" width="1920" height="1080"></canvas>

        <!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
        <div
          id="co-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 ROOT = document.getElementById("co-root");
          var CS = getComputedStyle(ROOT);

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

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

          var HUE = num("hue");
          var ACCENT = num("accent");
          var SPIN = num("spin");
          var STARS = num("stars");
          var GLOW = num("glow");
          var SIZE = num("size");
          var PULSE = num("pulse");
          var BACKDROP = str("backdrop");

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

          // Beat hook. A precomputed envelope (comma-separated 0-1 samples spread
          // evenly across the clip) is sampled closed-form at time t — no audio
          // analysis, no accumulator. Empty envelope => the flat `pulse` value.
          var ENV = str("pulseEnvelope")
            .split(",")
            .map(function (s) {
              return parseFloat(s);
            })
            .filter(function (n) {
              return isFinite(n);
            });

          function pulseAt(t) {
            if (ENV.length === 0) return PULSE;
            if (ENV.length === 1) return ENV[0];
            var x = Math.min(1, Math.max(0, t / DUR)) * (ENV.length - 1);
            var i = Math.floor(x);
            var j = Math.min(i + 1, ENV.length - 1);
            return ENV[i] + (ENV[j] - ENV[i]) * (x - i);
          }

          var canvas = document.getElementById("co-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 = [
            "attribute vec2 aPos;",
            "void main() { gl_Position = vec4(aPos, 0.0, 1.0); }",
          ].join("\n");

          var FRAG = [
            "precision highp float;",
            "uniform vec2 uRes;",
            "uniform float uTime;",
            "uniform float uRadius;",
            "uniform float uSpin;",
            "uniform float uHue;",
            "uniform float uAccent;",
            "uniform float uStars;",
            "uniform float uGlow;",
            "uniform float uPulse;",
            "const float TAU = 6.28318531;",
            "",
            "// One cheap hash for every random-looking thing in here.",
            "float h1(float x) { return fract(sin(x * 127.1) * 43758.5453); }",
            "float h2(vec2 p) { return h1(dot(p, vec2(1.0, 157.31))); }",
            "",
            "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);",
            "}",
            "",
            "vec3 rotX(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z); }",
            "vec3 rotY(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z); }",
            "vec3 rotZ(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }",
            "",
            "vec3 safeDir(vec3 v) {",
            "  float l = dot(v, v);",
            "  if (l < 1e-6) return vec3(0.0, 0.0, -1.0);",
            "  return v * inversesqrt(l);",
            "}",
            "",
            "// Fixed axial tilt, then spin about the tilted polar axis.",
            "vec3 toBody(vec3 n, float spin) { return rotY(rotZ(rotX(n, -0.34), 0.42), -spin); }",
            "vec2 lonlat(vec3 d) { return vec2(atan(d.x, d.z), asin(clamp(d.y, -1.0, 1.0))); }",
            "",
            "float dens(float base) { return clamp(1.0 - (1.0 - base) * uStars, 0.0, 1.0); }",
            "",
            "// One star size-class: cell grid, seeded jitter, soft round core.",
            "float starField(vec2 q, float freq, float base, float rad) {",
            "  vec2 g = q * freq;",
            "  vec2 id = floor(g);",
            "  float h = h2(id);",
            "  if (h < dens(base)) return 0.0;",
            "  vec2 c = vec2(h2(id + vec2(3.7, 1.3)), h2(id + vec2(9.1, 5.5)));",
            "  float d = length(fract(g) - c);",
            "  float core = smoothstep(rad, 0.0, d);",
            "  // Twinkle: a per-star phase driven straight off uTime, no accumulator.",
            "  float tw = 0.62 + 0.38 * sin(uTime * 2.4 + h * 53.7);",
            "  return core * core * (0.45 + 0.55 * h1(h * 91.7)) * tw;",
            "}",
            "",
            "// 4-point diffraction glare, only worth paying for on the brightest class.",
            "float starGlare(vec2 q, float freq, float base) {",
            "  vec2 g = q * freq;",
            "  vec2 id = floor(g);",
            "  float h = h2(id);",
            "  if (h < dens(base)) return 0.0;",
            "  vec2 c = vec2(h2(id + vec2(3.7, 1.3)), h2(id + vec2(9.1, 5.5)));",
            "  vec2 dv = abs(fract(g) - c);",
            "  float hx = exp(-dv.x * 70.0) * exp(-dv.y * 9.0);",
            "  float hy = exp(-dv.y * 70.0) * exp(-dv.x * 9.0);",
            "  return max(hx, hy) * 0.45;",
            "}",
            "",
            "// Three size-classes at ~6 / 11 / 19 cell frequency.",
            "float starsAll(vec2 q, float gain) {",
            "  float s = starField(q, 6.0, 0.958, 0.20);",
            "  s += starGlare(q, 6.0, 0.958);",
            "  s += starField(q, 11.0, 0.920, 0.18) * 0.95;",
            "  s += starField(q, 19.0, 0.865, 0.17) * 0.75;",
            "  return s * gain;",
            "}",
            "",
            "vec3 nebula(vec2 q) {",
            "  // Two sin-field turbulence layers.",
            "  float t1 = sin(q.x * 3.1 + sin(q.y * 2.7) * 1.9) * sin(q.y * 4.3 + sin(q.x * 1.7) * 2.3);",
            "  float t2 = sin(q.x * 7.9 + sin(q.y * 5.1) * 1.3) * sin(q.y * 9.7 + sin(q.x * 4.4) * 1.1);",
            "  float turb = t1 * 0.66 + t2 * 0.34;",
            "",
            "  // Galaxy bands, warped by the turbulence.",
            "  float band = sin(q.y * 3.2 + turb * 1.7 + q.x * 0.55);",
            "  float bands = pow(max(0.0, band * 0.5 + 0.5), 3.2);",
            "",
            "  // Dust lanes: broad darkened swathes, not a thin curve.",
            "  float lane = sin(q.y * 2.9 + q.x * 1.3 + t1 * 1.6);",
            "  float dust = 0.22 + 0.78 * smoothstep(-0.45, 0.75, lane);",
            "",
            "  // Core bulge.",
            "  float dc = length(vec2((q.x - 0.55) * 0.5, (q.y + 0.12) * 1.1));",
            "  float core = exp(-dc * dc * 2.6);",
            "",
            "  // Two pocket glows.",
            "  float dA = length(vec2((q.x + 1.95) * 0.62, (q.y - 0.52) * 1.45));",
            "  float dB = length(vec2((q.x - 2.35) * 0.55, (q.y + 0.78) * 1.30));",
            "  float pA = exp(-dA * dA * 4.0);",
            "  float pB = exp(-dB * dB * 5.0);",
            "",
            "  vec3 cNeb = hue2rgb(uHue / 360.0);",
            "  vec3 cAcc = hue2rgb(uAccent / 360.0);",
            "  vec3 cWarm = hue2rgb(uHue / 360.0 + 0.07);",
            "",
            "  vec3 col = mix(cNeb, cWarm, 0.5 + 0.5 * turb) * bands * dust * 0.17;",
            "  col += mix(cNeb, vec3(1.0), 0.50) * core * dust * 0.13;",
            "  col += mix(cAcc, vec3(1.0), 0.60) * pA * 0.30 * uGlow;",
            "  col += mix(cNeb, vec3(1.0), 0.50) * pB * 0.24 * uGlow;",
            "  return col;",
            "}",
            "",
            "void main() {",
            "  vec2 p = (gl_FragCoord.xy - uRes * 0.5) / uRadius;",
            "  float r = length(p);",
            "  float aa = 1.4 / uRadius;",
            "  float spin = TAU * uSpin * uTime;",
            "",
            "  vec3 cNeb = hue2rgb(uHue / 360.0);",
            "  vec3 cAcc = hue2rgb(uAccent / 360.0);",
            "",
            "  float mask = smoothstep(1.0 + aa, 1.0 - aa, r);",
            "  float halo = exp(-max(0.0, r - 1.0) * 13.0) * (1.0 - mask);",
            "",
            "  vec3 body = vec3(0.0);",
            "  if (mask > 0.001) {",
            "    float rc = min(r, 1.0);",
            "    // Analytic sphere: the normal falls straight out of z = sqrt(1 - r^2).",
            "    float z = sqrt(max(0.0, 1.0 - rc * rc));",
            "    vec3 N = vec3(p.x, p.y, z);",
            "    vec2 q = lonlat(toBody(safeDir(N), spin));",
            "",
            "    float gain = 0.9 + 2.6 * uPulse;",
            "    body = nebula(q);",
            "    body += vec3(1.0) * starsAll(q, gain) * (0.95 + 0.15 * uGlow);",
            "",
            "    // Limb darkening.",
            "    body *= mix(0.32, 1.0, pow(z, 0.5));",
            "",
            "    // Refract-sampled back layer, split per channel: the chromatic",
            "    // aberration that reads as glass at the limb.",
            "    vec3 I = vec3(0.0, 0.0, -1.0);",
            "    vec3 dR = safeDir(refract(I, N, 1.0 / 1.09));",
            "    vec3 dG = safeDir(refract(I, N, 1.0 / 1.13));",
            "    vec3 dB = safeDir(refract(I, N, 1.0 / 1.17));",
            "    vec2 qR = lonlat(toBody(dR, spin));",
            "    vec2 qG = lonlat(toBody(dG, spin));",
            "    vec2 qB = lonlat(toBody(dB, spin));",
            "    vec3 back = vec3(starsAll(qR, gain), starsAll(qG, gain), starsAll(qB, gain));",
            "    float fres = pow(1.0 - z, 1.8);",
            "    body += back * fres * 2.2;",
            "    body += nebula(qG) * fres * 0.8;",
            "",
            "    // Aurora sheen riding the glass shell.",
            "    float ang = atan(p.y, p.x);",
            "    float sheen = smoothstep(0.72, 0.99, r) * smoothstep(1.0, 0.90, r);",
            "    float w = 0.5 + 0.5 * sin(ang * 2.0 + spin * 1.6);",
            "    body += (mix(cNeb, cAcc, w) * 0.55 + vec3(0.16)) * sheen * 0.30 * uGlow;",
            "",
            "    // Three speculars, fixed in screen space: the key light does not spin.",
            "    vec2 a1 = p - vec2(-0.34, 0.62);",
            "    vec2 a2 = p - vec2(0.58, 0.60);",
            "    vec2 a3 = p - vec2(0.24, -0.80);",
            "    float sp = exp(-dot(a1, a1) * 30.0) * 0.30;",
            "    sp += exp(-dot(a2, a2) * 150.0) * 0.80;",
            "    sp += exp(-dot(a3, a3) * 70.0) * 0.30;",
            "    body += vec3(1.0) * sp * (0.30 + 0.70 * pow(1.0 - z, 1.5)) * uGlow;",
            "",
            "    // Fresnel void glow hugging the inside of the rim.",
            "    body += mix(cAcc, vec3(1.0), 0.4) * pow(1.0 - z, 6.0) * 0.30 * uGlow;",
            "  }",
            "",
            "  vec3 haloCol = mix(cNeb, cAcc, 0.35);",
            "  vec3 outCol = body * mask + haloCol * halo * 0.22 * uGlow;",
            "  float outA = clamp(mask + halo * 0.16 * uGlow, 0.0, 1.0);",
            "  gl_FragColor = vec4(clamp(outCol, 0.0, 4.0), outA);",
            "}",
          ].join("\n");

          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("cosmic-orb 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("cosmic-orb 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",
              "uRadius",
              "uSpin",
              "uHue",
              "uAccent",
              "uStars",
              "uGlow",
              "uPulse",
            ].forEach(function (n) {
              uni[n] = gl.getUniformLocation(prog, n);
            });

            gl.viewport(0, 0, W, H);
            gl.uniform2f(uni.uRes, W, H);
            gl.uniform1f(uni.uRadius, (SIZE * H) / 2);
            gl.uniform1f(uni.uSpin, SPIN);
            gl.uniform1f(uni.uHue, HUE);
            gl.uniform1f(uni.uAccent, ACCENT);
            gl.uniform1f(uni.uStars, STARS);
            gl.uniform1f(uni.uGlow, GLOW);
            ready = true;
          }

          // Every frame is computed from t alone: spin is spin_rate * t, the
          // pulse is sampled from the envelope at t. No accumulators, no clocks.
          function draw(t) {
            if (!ready) return;
            gl.uniform1f(uni.uTime, t);
            gl.uniform1f(uni.uPulse, pulseAt(t));
            gl.clearColor(0, 0, 0, 0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            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["cosmic-orb"] = tl;

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

Tagged `webgl` `shader` `background` `showcase` `3d`.

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

- [Locked Nucleus Orbit](/catalog/components/locked-nucleus-orbit.md)
- [Three Orbiting Cards](/catalog/components/three-orbiting-cards.md)
- [Recreating something you saw](/prompting/recreating-references.md)
