> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes-fix-nested-composition-media-inpoint.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Magnetic

> VFX composition block

export const InstallCommand = ({command, item}) => {
  const [copied, setCopied] = React.useState(false);
  const [tuned, setTuned] = React.useState("");
  React.useEffect(() => {
    if (!item) return;
    const read = () => {
      try {
        const raw = new URLSearchParams(window.location.search).get(`vars-${item}`);
        if (!raw) return setTuned("");
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return setTuned("");
        if (Object.keys(parsed).length === 0) return setTuned("");
        setTuned(` --vars '${JSON.stringify(parsed)}'`);
      } catch {
        setTuned("");
      }
    };
    read();
    window.addEventListener("hf-vars-changed", read);
    window.addEventListener("popstate", read);
    return () => {
      window.removeEventListener("hf-vars-changed", read);
      window.removeEventListener("popstate", read);
    };
  }, [item]);
  const fullCommand = `${command}${tuned}`;
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(fullCommand);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = fullCommand;
        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">
        {fullCommand}
      </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>;
};

<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-magnetic.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-magnetic.png" autoPlay muted loop playsInline />

## Install

<InstallCommand command="npx hyperframes add vfx-magnetic" item="vfx-magnetic" />

That writes one file: `compositions/vfx-magnetic.html`.

<Danger>
  Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
  Without it this item's screen renders black. Rendering from the CLI switches
  it on for you. [How it works](/guides/html-in-canvas)
</Danger>

## Change the colors

Set these CSS variables on the block:

* `--bg-color` — Background. Defaults to `#0c0c0c`.
* `--text-color` — Text color. Defaults to `#e8e8e8`.

## Source

<Accordion title={`vfx-magnetic.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=1920, height=1080" />
      <title>Magnetic Cursor — HTML-in-Canvas</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: #0c0c0c;
          overflow: hidden;
          font-family:
            system-ui,
            -apple-system,
            sans-serif;
          color: #e8e8e8;
        }
        #root {
          position: relative;
          width: 1920px;
          height: 1080px;
          overflow: hidden;
          background: #0c0c0c;
        }
        canvas {
          display: block;
        }

        .page-content {
          width: 1920px;
          height: 1080px;
          overflow: hidden;
          font-family:
            system-ui,
            -apple-system,
            sans-serif;
          color: #e8e8e8;
          display: flex;
          align-items: center;
          justify-content: center;
        }
        .page-inner {
          max-width: 900px;
          padding: 0 48px;
        }
        .heading {
          font-size: 64px;
          font-weight: 700;
          line-height: 1.15;
          letter-spacing: -0.03em;
          color: #f0f0f0;
          margin-bottom: 32px;
        }
        .heading em {
          font-style: normal;
          color: #70c7ed;
        }
        .body-text {
          font-size: 22px;
          line-height: 1.7;
          color: rgba(255, 255, 255, 0.6);
          margin-bottom: 24px;
        }
        .body-text:last-of-type {
          margin-bottom: 48px;
        }
        .hint {
          font-size: 15px;
          color: rgba(255, 255, 255, 0.3);
          font-family: ui-monospace, "SF Mono", monospace;
          margin-bottom: 48px;
          letter-spacing: 0.02em;
        }
        .card-grid {
          display: grid;
          grid-template-columns: repeat(3, 1fr);
          gap: 20px;
        }
        .card {
          background: rgba(255, 255, 255, 0.04);
          border: 1px solid rgba(255, 255, 255, 0.08);
          border-radius: 14px;
          padding: 24px;
        }
        .card-icon {
          width: 40px;
          height: 40px;
          border-radius: 10px;
          margin-bottom: 14px;
          display: flex;
          align-items: center;
          justify-content: center;
          font-size: 20px;
          background: rgba(112, 199, 237, 0.12);
        }
        .card-title {
          font-size: 16px;
          font-weight: 600;
          color: rgba(255, 255, 255, 0.85);
          margin-bottom: 8px;
        }
        .card-desc {
          font-size: 14px;
          line-height: 1.5;
          color: rgba(255, 255, 255, 0.4);
        }

        /* Subtitles */
        .subtitle-bar {
          position: absolute;
          bottom: 60px;
          left: 50%;
          transform: translateX(-50%);
          z-index: 10;
          pointer-events: none;
        }
        .subtitle {
          font-size: 28px;
          font-weight: 500;
          color: white;
          text-align: center;
          background: rgba(0, 0, 0, 0.7);
          padding: 12px 32px;
          border-radius: 8px;
          backdrop-filter: blur(8px);
          white-space: nowrap;
          opacity: 0;
          font-family:
            system-ui,
            -apple-system,
            sans-serif;
        }

        /* Cursor dot */
        .cursor-dot {
          position: absolute;
          width: 20px;
          height: 20px;
          border-radius: 50%;
          background: rgba(112, 199, 237, 0.8);
          box-shadow:
            0 0 20px rgba(112, 199, 237, 0.5),
            0 0 40px rgba(112, 199, 237, 0.3);
          z-index: 10;
          pointer-events: none;
          transform: translate(-50%, -50%);
          opacity: 0;
        }
      </style>
    </head>
    <body>
      <div
        id="root"
        data-composition-id="magnetic-cursor"
        data-width="1920"
        data-height="1080"
        data-start="0"
        data-duration="15"
        data-root="true"
      >
        <canvas
          id="gl-canvas"
          layoutsubtree
          width="1920"
          height="1080"
          style="position: absolute; top: 0; left: 0; width: 1920px; height: 1080px"
        >
          <div class="page-content" id="content" style="width: 1920px; height: 1080px">
            <div class="page-inner">
              <h1 class="heading">Pixels bend toward<br />your <em>cursor</em></h1>
              <p class="body-text">
                Move your mouse across this page. Every pixel is drawn through a WebGL shader that
                warps space toward your cursor position, like a magnet pulling iron filings beneath a
                thin rubber sheet.
              </p>
              <p class="body-text">
                The distortion follows a Gaussian falloff — strong at close range, tapering smoothly
                to nothing. The text remains readable even under deformation.
              </p>
              <p class="hint">HTML-in-Canvas API + WebGL2 shader distortion</p>
              <div class="card-grid">
                <div class="card">
                  <div class="card-icon">&#9889;</div>
                  <div class="card-title">Real-time</div>
                  <div class="card-desc">
                    60fps distortion on live DOM content via texElementImage2D
                  </div>
                </div>
                <div class="card">
                  <div class="card-icon">&#128268;</div>
                  <div class="card-title">Native API</div>
                  <div class="card-desc">
                    No html2canvas. Browser's own rasterizer at full fidelity.
                  </div>
                </div>
                <div class="card">
                  <div class="card-icon">&#127912;</div>
                  <div class="card-title">GPU Shaders</div>
                  <div class="card-desc">
                    Gaussian warp + chromatic aberration in one fragment shader pass.
                  </div>
                </div>
              </div>
            </div>
          </div>
        </canvas>

        <!-- Visible cursor dot -->
        <div class="cursor-dot" id="cursor-dot"></div>

        <!-- Subtitles -->
        <div class="subtitle-bar">
          <div class="subtitle" id="sub1">
            The HTML-in-Canvas API renders live DOM as a WebGL texture
          </div>
          <div class="subtitle" id="sub2">A magnetic shader warps every pixel toward the cursor</div>
          <div class="subtitle" id="sub3">
            Chromatic aberration splits RGB channels at the distortion site
          </div>
          <div class="subtitle" id="sub4">
            Impossible without native drawElementImage — no polyfill exists
          </div>
        </div>

        <div
          id="driver"
          class="clip"
          data-start="0"
          data-duration="15"
          data-track-index="0"
          style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
        ></div>
      </div>

      <script>
        var W = 1920,
          H = 1080;
        var canvas = document.getElementById("gl-canvas");
        var content = document.getElementById("content");
        var cursorDot = document.getElementById("cursor-dot");

        function isSupported() {
          var tc = document.createElement("canvas");
          if (!("layoutSubtree" in tc)) return false;
          tc.setAttribute("layoutsubtree", "");
          var ctx = tc.getContext("2d");
          return ctx && typeof ctx.drawElementImage === "function";
        }

        // ── WebGL2 Setup ─────────────────────────────────────────────────
        var gl = canvas.getContext("webgl2", { alpha: false, preserveDrawingBuffer: true });

        function compile(type, src) {
          var s = gl.createShader(type);
          gl.shaderSource(s, src);
          gl.compileShader(s);
          if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s));
          return s;
        }
        function link(vsSrc, fsSrc) {
          var p = gl.createProgram();
          gl.attachShader(p, compile(gl.VERTEX_SHADER, vsSrc));
          gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fsSrc));
          gl.linkProgram(p);
          return p;
        }

        var VS = [
          "#version 300 es",
          "in vec2 a_pos;",
          "out vec2 v_uv;",
          "void main() {",
          "  v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);",
          "  gl_Position = vec4(a_pos, 0.0, 1.0);",
          "}",
        ].join("\n");

        var FS = [
          "#version 300 es",
          "precision highp float;",
          "in vec2 v_uv;",
          "out vec4 fragColor;",
          "uniform sampler2D u_content;",
          "uniform vec2 u_mouse;",
          "uniform vec2 u_resolution;",
          "uniform float u_repel;",
          "uniform float u_strength;",
          "",
          "void main() {",
          "  vec2 uv = v_uv;",
          "  float aspect = u_resolution.x / u_resolution.y;",
          "  vec2 delta = u_mouse - uv;",
          "  vec2 aspectDelta = delta * vec2(aspect, 1.0);",
          "  float dist = length(aspectDelta);",
          "  float strength = exp(-dist * dist * 20.0) * 0.04 * u_strength;",
          "  float direction = mix(1.0, -1.0, u_repel);",
          "  vec2 displaced = uv + delta * strength * direction;",
          "  displaced = clamp(displaced, vec2(0.0), vec2(1.0));",
          "  float aberration = strength * 0.6;",
          "  vec2 aberDir = normalize(delta + 0.0001) * aberration;",
          "  float r = texture(u_content, displaced + aberDir * 0.3).r;",
          "  float g = texture(u_content, displaced).g;",
          "  float b = texture(u_content, displaced - aberDir * 0.3).b;",
          "  fragColor = vec4(r, g, b, 1.0);",
          "}",
        ].join("\n");

        var prog = link(VS, FS);

        // Quad
        var vao = gl.createVertexArray();
        gl.bindVertexArray(vao);
        var buf = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buf);
        gl.bufferData(
          gl.ARRAY_BUFFER,
          new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
          gl.STATIC_DRAW,
        );
        var aPos = gl.getAttribLocation(prog, "a_pos");
        gl.enableVertexAttribArray(aPos);
        gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

        // Texture
        var tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

        // Uniforms
        gl.useProgram(prog);
        var U = {};
        ["u_content", "u_mouse", "u_resolution", "u_repel", "u_strength"].forEach(function (n) {
          U[n] = gl.getUniformLocation(prog, n);
        });

        // ── Capture + Render ─────────────────────────────────────────────
        var captured = false;

        function captureContent() {
          if (!isSupported()) return;
          gl.bindTexture(gl.TEXTURE_2D, tex);
          gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, content);
          captured = true;
        }

        // Also support drawElementImage fallback via 2D context capture
        function captureVia2D() {
          var tc = document.createElement("canvas");
          tc.setAttribute("layoutsubtree", "");
          tc.width = W;
          tc.height = H;
          // Can't use drawElementImage from a different canvas — skip fallback
        }

        function render(mouseX, mouseY, repel, strength) {
          gl.viewport(0, 0, W, H);
          gl.useProgram(prog);
          gl.bindVertexArray(vao);
          gl.activeTexture(gl.TEXTURE0);
          gl.bindTexture(gl.TEXTURE_2D, tex);
          gl.uniform1i(U.u_content, 0);
          gl.uniform2f(U.u_mouse, mouseX, mouseY);
          gl.uniform2f(U.u_resolution, W, H);
          gl.uniform1f(U.u_repel, repel);
          gl.uniform1f(U.u_strength, strength);
          gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        }

        // ── Animation State ──────────────────────────────────────────────
        var S = {
          mouseX: 0.5,
          mouseY: 0.5,
          repel: 0,
          strength: 0,
          progress: 0,
        };

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

        // Driver tween
        tl.to(
          S,
          {
            progress: 1,
            duration: 15,
            ease: "none",
            onUpdate: function () {
              // Update cursor dot position
              cursorDot.style.left = S.mouseX * W + "px";
              cursorDot.style.top = S.mouseY * H + "px";
              render(S.mouseX, S.mouseY, S.repel, S.strength);
            },
          },
          0,
        );

        // Cursor appears
        tl.to(cursorDot, { opacity: 1, duration: 0.3 }, 0.5);
        tl.to(S, { strength: 1, duration: 0.5, ease: "power2.out" }, 0.5);

        // Cursor path: smooth figure-8 across the page
        // Phase 1: sweep across heading
        tl.to(S, { mouseX: 0.35, mouseY: 0.3, duration: 2, ease: "power1.inOut" }, 1);
        tl.to(S, { mouseX: 0.6, mouseY: 0.35, duration: 2, ease: "power1.inOut" }, 3);

        // Phase 2: circle around text
        tl.to(S, { mouseX: 0.4, mouseY: 0.5, duration: 1.5, ease: "sine.inOut" }, 5);
        tl.to(S, { mouseX: 0.55, mouseY: 0.55, duration: 1.5, ease: "sine.inOut" }, 6.5);

        // Phase 3: repel pulse
        tl.to(S, { repel: 1, duration: 0.2, ease: "power4.out" }, 8);
        tl.to(S, { repel: 0, duration: 0.5, ease: "power2.out" }, 8.5);

        // Phase 4: sweep across cards
        tl.to(S, { mouseX: 0.3, mouseY: 0.75, duration: 2, ease: "power1.inOut" }, 9);
        tl.to(S, { mouseX: 0.65, mouseY: 0.72, duration: 2, ease: "power1.inOut" }, 11);

        // Phase 5: exit
        tl.to(S, { mouseX: 0.8, mouseY: 0.2, duration: 1.5, ease: "power2.inOut" }, 13);
        tl.to(S, { strength: 0, duration: 0.5 }, 14);
        tl.to(cursorDot, { opacity: 0, duration: 0.3 }, 14.2);

        // Subtitles
        tl.to("#sub1", { opacity: 1, duration: 0.3 }, 1);
        tl.to("#sub1", { opacity: 0, duration: 0.3 }, 3.5);
        tl.to("#sub2", { opacity: 1, duration: 0.3 }, 4);
        tl.to("#sub2", { opacity: 0, duration: 0.3 }, 6.5);
        tl.to("#sub3", { opacity: 1, duration: 0.3 }, 7.5);
        tl.to("#sub3", { opacity: 0, duration: 0.3 }, 9.5);
        tl.to("#sub4", { opacity: 1, duration: 0.3 }, 10.5);
        tl.to("#sub4", { opacity: 0, duration: 0.3 }, 13);

        window.__timelines["magnetic-cursor"] = tl;

        // Paint event for texture upload
        canvas.onpaint = function () {
          if (!captured) {
            gl.bindTexture(gl.TEXTURE_2D, tex);
            gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, content);
            captured = true;
          }
        };

        setTimeout(function () {
          if (isSupported()) {
            canvas.requestPaint();
          }
          tl.seek(0);
        }, 0);
      </script>
    </body>
  </html>
  ```
</Accordion>

## Usage

After installing, add the block to your host composition:

```html theme={null}
<div data-composition-id="vfx-magnetic" data-composition-src="compositions/vfx-magnetic.html" data-start="0" data-duration="15" data-track-index="1" data-width="1920" data-height="1080"></div>
```

Tagged `html-in-canvas` `webgl`.

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)
