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

# Annotate Cursor

> A marker pen glides onto the frame and hand-draws a loose loop, an underline, or an arrow around a region, the ink laid exactly under its nib, then lifts and leaves.

**Annotate Cursor**

```html cursor-annotate.html theme={null}
<!doctype html>
<!--
  Annotate Cursor (actor primitive), mountable sub-composition

  concept: a marker pen glides to a region of the host UI, touches down and hand-draws
  one annotation around it (a loose loop, an underline, or an arrow), then lifts and
  leaves. The ink is revealed exactly under the nib: both read the same path length.

  mount contract: loaded by a host through data-composition-src; only <template> is
  cloned. #root fills the host clip (inset:0). See demo.html.

  variables:
    shape (circle|underline|arrow)   the mark the pen draws
    color                            marker ink
    target_x, target_y, target_w, target_h (% of host box)  the region being marked

  envelope (default 2.95s): glide in 0-0.5s, touch down, draw 0.7-1.8s, lift and step
  clear of the mark, hold, exit in the last 0.35s. Shorter durations compress every beat evenly.
  The ink is a tapered marker stroke: thick where the pen moves slowly, thin where it is fast.
-->
<html
  lang="en"
  data-composition-id="cursor-annotate"
  data-composition-duration="2.95"
  data-composition-variables='[
    { "id": "shape", "type": "enum", "role": "style", "label": "Mark", "default": "circle", "options": [ { "value": "circle", "label": "Loose loop" }, { "value": "underline", "label": "Underline" }, { "value": "arrow", "label": "Arrow" } ] },
    { "id": "color", "type": "color", "role": "style", "label": "Ink", "default": "#f43f5e" },
    { "id": "target_x", "type": "number", "role": "layout", "label": "Region left", "default": 40, "min": 0, "max": 95, "step": 1, "unit": "%" },
    { "id": "target_y", "type": "number", "role": "layout", "label": "Region top", "default": 38, "min": 0, "max": 95, "step": 1, "unit": "%" },
    { "id": "target_w", "type": "number", "role": "layout", "label": "Region width", "default": 20, "min": 2, "max": 90, "step": 1, "unit": "%" },
    { "id": "target_h", "type": "number", "role": "layout", "label": "Region height", "default": 16, "min": 2, "max": 90, "step": 1, "unit": "%" }
  ]'
>
  <head>
    <meta charset="UTF-8" />
    <title>Annotate Cursor</title>
  </head>
  <body>
    <template>
      <style>
        #root {
          position: absolute;
          inset: 0;
          overflow: hidden;
          isolation: isolate;
          pointer-events: none;
        }
        .ca-ink {
          position: absolute;
          inset: 0;
          width: 100%;
          height: 100%;
          overflow: visible;
        }
        .ca-ink path {
          fill: var(--ca-ink, #f43f5e);
        }
        .ca-pen {
          position: absolute;
          left: 0;
          top: 0;
          width: 0;
          height: 0;
          will-change: transform;
        }
        /* The nib sits at the svg origin, so the wrapper's translate places the nib. */
        .ca-pen svg {
          position: absolute;
          left: -40px;
          top: -200px;
          width: 240px;
          height: 240px;
          overflow: visible;
        }
      </style>

      <div id="root" data-composition-id="cursor-annotate" data-duration="2.95">
        <svg class="ca-ink" id="ca-ink" aria-hidden="true"></svg>
        <div class="ca-pen" id="ca-pen" aria-hidden="true">
          <svg viewBox="-40 -200 240 240" aria-hidden="true">
            <g id="ca-pen-body">
              <g transform="rotate(-50)">
                <path d="M0 0 L15 -5.5 L15 5.5 Z" fill="#1d1d22" />
                <path id="ca-pen-tip" d="M3 -1.4 L15 -5.5 L15 5.5 L3 1.4 Z" />
                <rect x="15" y="-7.5" width="14" height="15" rx="2" fill="#2a2a31" />
                <rect
                  x="29"
                  y="-9"
                  width="62"
                  height="18"
                  rx="4"
                  fill="#ffffff"
                  stroke="#141414"
                  stroke-width="2"
                />
                <rect x="36" y="-9" width="8" height="18" id="ca-pen-band" />
                <rect x="91" y="-9.5" width="16" height="19" rx="6" fill="#1d1d22" />
              </g>
            </g>
          </svg>
        </div>
      </div>

      <script>
        (function () {
          "use strict";

          var html = document.documentElement;
          var root = document.getElementById("root");
          var ink = document.getElementById("ca-ink");
          var pen = document.getElementById("ca-pen");
          var penBody = document.getElementById("ca-pen-body");
          var SVG_NS = "http://www.w3.org/2000/svg";

          var DEFAULTS = {};
          try {
            JSON.parse(html.getAttribute("data-composition-variables") || "[]").forEach(
              function (variable) {
                DEFAULTS[variable.id] = variable.default;
              },
            );
          } catch (error) {
            /* malformed declaration falls back to the inline defaults below */
          }
          var scopedVariables = {};
          try {
            if (window.__hyperframes && typeof window.__hyperframes.getVariables === "function") {
              scopedVariables = window.__hyperframes.getVariables() || {};
            }
          } catch (error) {
            /* no scoped variables API in this context */
          }
          var vars = Object.assign({}, DEFAULTS, window.__hfVariables || {}, scopedVariables);

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

          // Hardcoded: the compositor rewrites the wrapper's attributes on mount.
          var compositionId = "cursor-annotate";
          var duration = Math.max(
            0.001,
            parseFloat(
              root.dataset.duration || html.getAttribute("data-composition-duration") || "2.95",
            ),
          );
          var shape =
            ["circle", "underline", "arrow"].indexOf(vars.shape) >= 0 ? vars.shape : "circle";
          var color = /^#[0-9a-f]{3,8}$/i.test(String(vars.color)) ? vars.color : "#f43f5e";
          var W = root.clientWidth || 1920;
          var H = root.clientHeight || 1080;
          var unit = W / 1920;
          var box = {
            x: (num(vars.target_x, 40, 0, 100) / 100) * W,
            y: (num(vars.target_y, 38, 0, 100) / 100) * H,
            w: (num(vars.target_w, 20, 1, 100) / 100) * W,
            h: (num(vars.target_h, 16, 1, 100) / 100) * H,
          };

          ink.setAttribute("viewBox", "0 0 " + W + " " + H);
          root.style.setProperty("--ca-ink", color);
          document.getElementById("ca-pen-tip").setAttribute("fill", color);
          document.getElementById("ca-pen-band").setAttribute("fill", color);

          // Each mark is one or more strokes; a stroke maps u in [0,1] to a point on the page.
          // A hand loop: starts upper-left, runs a little past its start and wider on the way out.
          function loopStroke() {
            var cx = box.x + box.w / 2;
            var cy = box.y + box.h / 2;
            var rx = box.w * 0.62 + 18 * unit;
            var ry = box.h * 0.66 + 16 * unit;
            var tilt = (-5 * Math.PI) / 180;
            return [
              function (u) {
                var a = -2.55 + (Math.PI * 2 + 0.5) * u;
                var r = (1 + 0.035 * Math.sin(3 * a + 1.1)) * (1 + 0.08 * u);
                var ex = Math.cos(a) * rx * r;
                var ey = Math.sin(a) * ry * r;
                return [
                  cx + ex * Math.cos(tilt) - ey * Math.sin(tilt),
                  cy + ex * Math.sin(tilt) + ey * Math.cos(tilt),
                ];
              },
            ];
          }

          function underlineStroke() {
            var y = box.y + box.h + 14 * unit;
            var x0 = box.x - box.w * 0.04;
            var span = box.w * 1.08;
            return [
              function (u) {
                return [
                  x0 + span * u,
                  y + 4 * unit * Math.sin(u * Math.PI * 1.3) - 12 * unit * u * u,
                ];
              },
            ];
          }

          function smooth(x) {
            return x * x * (3 - 2 * x);
          }
          function lerp(a, b, u) {
            return [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
          }

          // Shaft from below-left, curving onto the region; the head is one V that slows into its point.
          function arrowStrokes() {
            var tip = [box.x - 10 * unit, box.y + box.h + 8 * unit];
            var from = [
              tip[0] - Math.max(160 * unit, box.w * 0.7),
              tip[1] + Math.max(120 * unit, box.h * 0.9),
            ];
            var ctrl = [from[0] + (tip[0] - from[0]) * 0.15, tip[1] + (from[1] - tip[1]) * 0.05];
            var back = Math.atan2(ctrl[1] - tip[1], ctrl[0] - tip[0]);
            var len = 30 * unit;
            var barb = function (spread) {
              return [
                tip[0] + Math.cos(back + spread) * len,
                tip[1] + Math.sin(back + spread) * len,
              ];
            };
            return [
              function (u) {
                var v = 1 - u;
                return [
                  v * v * from[0] + 2 * v * u * ctrl[0] + u * u * tip[0],
                  v * v * from[1] + 2 * v * u * ctrl[1] + u * u * tip[1],
                ];
              },
              function (u) {
                return u < 0.5
                  ? lerp(barb(0.5), tip, smooth(u * 2))
                  : lerp(tip, barb(-0.5), smooth(u * 2 - 1));
              },
            ];
          }

          var paths =
            shape === "underline"
              ? underlineStroke()
              : shape === "arrow"
                ? arrowStrokes()
                : loopStroke();

          // Beats at the default 2.95s; a shorter composition compresses them evenly.
          var squeeze = Math.min(1, duration / 2.95);
          var T_LAND = 0.5 * squeeze;
          var T_DRAW = 0.7 * squeeze;
          var HOP = 0.2 * squeeze;
          var ASIDE_TIME = 0.35 * squeeze;
          var EXIT_TIME = 0.35 * squeeze;
          var drawTotal = (shape === "circle" ? 1.1 : shape === "arrow" ? 0.95 : 0.6) * squeeze;

          var glide = gsap.parseEase("power3.out");
          var strokeEase = gsap.parseEase("sine.inOut");
          var hop = gsap.parseEase("power2.inOut");
          var aside = gsap.parseEase("power2.out");
          var leave = gsap.parseEase("power2.in");

          function clamp01(u) {
            return u < 0 ? 0 : u > 1 ? 1 : u;
          }
          function arcLength(fn) {
            var sum = 0;
            var prev = fn(0);
            for (var i = 1; i <= 200; i++) {
              var p = fn(i / 200);
              sum += Math.hypot(p[0] - prev[0], p[1] - prev[1]);
              prev = p;
            }
            return sum;
          }

          // Samples at even steps of stroke time, so spacing between samples is the pen's speed.
          // Width follows speed (7u slow, 3u fast) and thins back to 3u as the pen lifts.
          var STEPS = 160;
          var MIN_W = 3 * unit;
          var MAX_W = 7 * unit;
          var strokes = paths.map(function (fn) {
            var pts = [];
            for (var k = 0; k <= STEPS; k++) {
              var u = strokeEase(k / STEPS);
              var p = fn(u);
              var a = fn(Math.max(0, u - 1e-4));
              var b = fn(Math.min(1, u + 1e-4));
              var tlen = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
              pts.push({ x: p[0], y: p[1], nx: -(b[1] - a[1]) / tlen, ny: (b[0] - a[0]) / tlen });
            }
            pts.forEach(function (p, k) {
              var a = pts[Math.max(0, k - 1)];
              var b = pts[Math.min(STEPS, k + 1)];
              p.speed = Math.hypot(b.x - a.x, b.y - a.y);
            });
            return { pts: pts, length: arcLength(fn) };
          });
          var topSpeed = 0;
          strokes.forEach(function (s) {
            s.pts.forEach(function (p) {
              topSpeed = Math.max(topSpeed, p.speed);
            });
          });
          strokes.forEach(function (s) {
            s.pts.forEach(function (p, k) {
              var lift = 1 - smooth(clamp01((k / STEPS - 0.82) / 0.18));
              p.w = MIN_W + (MAX_W - MIN_W) * (1 - p.speed / topSpeed) * lift;
            });
            s.el = document.createElementNS(SVG_NS, "path");
            ink.appendChild(s.el);
          });

          var totalLength = strokes.reduce(function (sum, s) {
            return sum + s.length;
          }, 0);
          var windows = [];
          var cursorT = T_DRAW;
          strokes.forEach(function (s, i) {
            var span = (drawTotal - HOP * (strokes.length - 1)) * (s.length / totalLength);
            windows.push({ start: cursorT, end: cursorT + span });
            cursorT += span + (i < strokes.length - 1 ? HOP : 0);
          });
          var T_LIFT = cursorT;
          var T_EXIT = Math.max(T_LIFT + ASIDE_TIME, duration - EXIT_TIME);

          // The stroke as drawn by stroke-time f: its samples up to f, the last one interpolated.
          function drawnTo(s, f) {
            var x = clamp01(f) * STEPS;
            var k = Math.floor(x);
            var out = s.pts.slice(0, k + 1);
            if (k < STEPS && x > k) {
              var a = s.pts[k];
              var b = s.pts[k + 1];
              var m = x - k;
              out.push({
                x: a.x + (b.x - a.x) * m,
                y: a.y + (b.y - a.y) * m,
                nx: a.nx + (b.nx - a.nx) * m,
                ny: a.ny + (b.ny - a.ny) * m,
                w: a.w + (b.w - a.w) * m,
              });
            }
            return out;
          }

          // Filled outline of a variable-width stroke with round ends.
          function outline(pts) {
            if (pts.length < 2) return "";
            function side(p, dir) {
              var h = (p.w / 2) * dir;
              return (p.x + p.nx * h).toFixed(1) + " " + (p.y + p.ny * h).toFixed(1);
            }
            function cap(p) {
              var r = (p.w / 2).toFixed(2);
              return " A" + r + " " + r + " 0 0 0 ";
            }
            var last = pts[pts.length - 1];
            var d = "M" + side(pts[0], 1);
            for (var i = 1; i < pts.length; i++) d += " L" + side(pts[i], 1);
            d += cap(last) + side(last, -1);
            for (var j = pts.length - 2; j >= 0; j--) d += " L" + side(pts[j], -1);
            return d + cap(pts[0]) + side(pts[0], 1) + " Z";
          }

          function nibOf(s, f) {
            var pts = drawnTo(s, f);
            var p = pts[pts.length - 1];
            return { x: p.x, y: p.y };
          }
          var first = nibOf(strokes[0], 0);
          var endPoint = nibOf(strokes[strokes.length - 1], 1);

          // After the lift the nib steps straight away from the region until it clears the mark
          // by 40u, then keeps going that way off the frame.
          var inkBox = { l: box.x, t: box.y, r: box.x + box.w, b: box.y + box.h };
          strokes.forEach(function (s) {
            s.pts.forEach(function (p) {
              inkBox.l = Math.min(inkBox.l, p.x);
              inkBox.t = Math.min(inkBox.t, p.y);
              inkBox.r = Math.max(inkBox.r, p.x);
              inkBox.b = Math.max(inkBox.b, p.y);
            });
          });
          var away = { x: endPoint.x - (box.x + box.w / 2), y: endPoint.y - (box.y + box.h / 2) };
          var awayLen = Math.hypot(away.x, away.y) || 1;
          away = { x: away.x / awayLen, y: away.y / awayLen };
          function reach(p, l, t, r, b) {
            var hits = [];
            if (away.x > 1e-6) hits.push((r - p.x) / away.x);
            if (away.x < -1e-6) hits.push((l - p.x) / away.x);
            if (away.y > 1e-6) hits.push((b - p.y) / away.y);
            if (away.y < -1e-6) hits.push((t - p.y) / away.y);
            return Math.max(0, Math.min.apply(null, hits));
          }
          var clear = 40 * unit;
          var step = reach(
            endPoint,
            inkBox.l - clear,
            inkBox.t - clear,
            inkBox.r + clear,
            inkBox.b + clear,
          );
          var ASIDE = {
            x: Math.max(40 * unit, Math.min(W - 100 * unit, endPoint.x + away.x * step)),
            y: Math.max(100 * unit, Math.min(H - 40 * unit, endPoint.y + away.y * step)),
          };
          var off = reach(ASIDE, 0, 0, W, H) + 180 * unit;
          var END = { x: ASIDE.x + away.x * off, y: ASIDE.y + away.y * off };
          var START = { x: first.x + 0.3 * W, y: H * 1.25 };

          function mix(a, b, u) {
            return { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u };
          }

          // Returns the nib position and how far off the paper it is (0 = touching).
          function penAt(t) {
            if (t <= T_LAND) return { p: mix(START, first, glide(clamp01(t / T_LAND))), lift: 1 };
            if (t <= T_DRAW)
              return { p: first, lift: 1 - clamp01((t - T_LAND) / (T_DRAW - T_LAND)) };
            for (var i = 0; i < windows.length; i++) {
              var w = windows[i];
              if (t <= w.end)
                return { p: nibOf(strokes[i], (t - w.start) / (w.end - w.start)), lift: 0 };
              var next = windows[i + 1];
              if (next && t < next.start) {
                var u = (t - w.end) / (next.start - w.end);
                return {
                  p: mix(nibOf(strokes[i], 1), nibOf(strokes[i + 1], 0), hop(u)),
                  lift: Math.sin(Math.PI * u),
                };
              }
            }
            if (t <= T_EXIT) {
              var a = clamp01((t - T_LIFT) / ASIDE_TIME);
              return { p: mix(endPoint, ASIDE, aside(a)), lift: Math.min(1, a * 2.5) };
            }
            return {
              p: mix(ASIDE, END, leave(clamp01((t - T_EXIT) / (duration - T_EXIT)))),
              lift: 1,
            };
          }

          function render(t) {
            strokes.forEach(function (s, i) {
              var w = windows[i];
              s.el.setAttribute("d", outline(drawnTo(s, (t - w.start) / (w.end - w.start))));
            });
            var state = penAt(t);
            var lift = state.lift;
            pen.style.transform =
              "translate(" + state.p.x.toFixed(2) + "px," + state.p.y.toFixed(2) + "px)";
            penBody.setAttribute(
              "transform",
              "scale(" +
                ((0.85 + 0.07 * lift) * unit).toFixed(4) +
                ") rotate(" +
                (-4 * lift).toFixed(2) +
                ")",
            );
            pen.style.filter =
              "drop-shadow(0 " +
              ((3 + 7 * lift) * unit).toFixed(1) +
              "px " +
              ((4 + 8 * lift) * unit).toFixed(1) +
              "px rgba(0,0,0," +
              (0.3 - 0.1 * lift).toFixed(3) +
              "))";
          }

          var clock = { t: 0 };
          var tl = gsap.timeline({ paused: true });
          tl.to(
            clock,
            {
              t: duration,
              duration: duration,
              ease: "none",
              onUpdate: function () {
                render(clock.t);
              },
            },
            0,
          );
          render(0);
          tl.seek(0);
          window.__timelines = window.__timelines || {};
          window.__timelines[compositionId] = tl;
        })();
      </script>
    </template>
  </body>
</html>
```

Copy npx hyperframes add cursor-annotate to the clipboard

That writes one file: `compositions/components/cursor-annotate.html`.

## Paste it into your composition

Open `compositions/components/cursor-annotate.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 |
| - | - | - | - |
| `shape` | `circle` | `circle`, `underline`, `arrow` | |
| `color` | `#f43f5e` | color | |
| `target_x` | `40` | 0% to 95%, step 1% | |
| `target_y` | `38` | 0% to 95%, step 1% | |
| `target_w` | `20` | 2% to 90%, step 1% | |
| `target_h` | `16` | 2% to 90%, step 1% | |

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="cursor-annotate"
  data-composition-src="compositions/components/cursor-annotate.html"
  data-variable-values='{"shape":"circle","color":"#f43f5e","target_x":40,"target_y":38,"target_w":20,"target_h":16}'
></div>
```

Tagged `cursor` `motion-primitive` `pen` `annotation` `draw-on` `highlight`.

## Related topics

* [Browse the complete Catalog](https://hyperframes.heygen.com/catalog.md)
* [Add assets and Catalog items in Studio](https://hyperframes.heygen.com/studio/assets-and-blocks.md)
* [Build a richer composition](https://hyperframes.heygen.com/go-further.md)


## Related topics

- [Vox Annotate](https://hyperframes.heygen.com/catalog/components/vox-annotate.md)
- [Multiplayer Cursors](https://hyperframes.heygen.com/catalog/components/multiplayer-cursors.md)
- [Oversized Cursor](https://hyperframes.heygen.com/catalog/components/oversized-cursor.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.