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

# Avatar Cloud

> Lettermark avatars populate a loose elliptical cloud while fine SVG links draw between the community and its central proof label.

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

## Install

<InstallCommand command="npx hyperframes add avatar-cloud" />

That writes one file: `compositions/components/avatar-cloud.html`.

## Paste it into your composition

Open `compositions/components/avatar-cloud.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                                       |
| ------------- | -------------- | --------------- | -------------------------------------------------- |
| `avatarCount` | `10`           | 6 to 16, step 1 | Number of lettermark avatars in the network cloud. |
| `showLinks`   | `yes`          | `yes`, `no`     | Show or hide the connecting network lines.         |
| `label`       | `50k builders` | string          | Community proof copy shown in the center hub.      |

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="avatar-cloud"
  data-composition-src="compositions/components/avatar-cloud.html"
  data-variable-values='{"avatarCount":10,"showLinks":"yes","label":"50k builders"}'
></div>
```

## Source

<Accordion title={`avatar-cloud.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    avatar-cloud: HyperFrames video primitive (effects / holdable / prove)

    Concept: lettermark avatars populate a loose elliptical cloud around a
    central proof label. Fine SVG links draw from the hub to every avatar, plus
    selected avatar-to-avatar edges, so the beat communicates connection rather
    than quantity alone. One mechanic, one job: prove community participation.

    Compiled-from evidence: consumer-sim fixture plus the
    avatar-cloud-network animation rule. The rule establishes the elliptical
    cluster, layered hub, staggered avatar arrival, outward SVG link draw, and
    readable climax dwell.

    Use when: a scene needs to show an active builder, customer, creator, or
    collaborator community without external photos or brand assets.

    Variables (declared in data-composition-variables below):
      - avatarCount (number, default 10, range 6 to 16): visible lettermarks.
      - showLinks ("yes" | "no", default "yes"): include network links.
      - label (string, default "50k builders"): proof copy in the center hub.

    Envelope (fixed IN/OUT, elastic HOLD only, never gsap.timeScale()):
      IN_BASE  = 2.20s  hub settles, avatars cascade in, then links draw.
      HOLD     = elastic = max(0, D - (IN_BASE + OUT_BASE)); deterministic,
                 out-of-phase sine drift keeps the formed network alive.
      OUT_BASE = 0.50s  the complete proof beat accelerates into a clean fade.
      If D is shorter than IN_BASE + OUT_BASE, IN and OUT scale together so
      IN + OUT equals D and HOLD equals 0.

    Sync point: network-formed at 2.15s into IN. It is a fixed offset before the
    elastic HOLD and scales proportionally only when the envelope is compressed.

    Sound cue: none. The distributed community assembly has no truthful single
    impact, so this primitive does not dispatch an hf:sfx event.

    Mount contract: this file is a mountable sub-composition. A host loads it via
    data-composition-src, and the runtime clones only <template> contents. Styles,
    markup, GSAP, and registration therefore live inside <template>. #root fills
    the host with inset:0 and container-type:size and has no data-width or
    data-height. The hardcoded id "avatar-cloud" matches the host id and timeline
    key after mount flattening. Variables come from
    window.__hyperframes.getVariables(), which merges declarations with host
    data-variable-values overrides.
  -->
  <html
    lang="en"
    data-composition-variables='[
      { "id": "avatarCount", "type": "number", "role": "layout", "label": "Avatar count", "description": "Number of lettermark avatars in the network cloud.", "default": 10, "min": 6, "max": 16, "step": 1 },
      { "id": "showLinks", "type": "enum", "role": "content", "label": "Show links", "description": "Show or hide the connecting network lines.", "default": "yes", "options": [{ "value": "yes", "label": "Yes" }, { "value": "no", "label": "No" }] },
      { "id": "label", "type": "string", "role": "content", "label": "Proof label", "description": "Community proof copy shown in the center hub.", "default": "50k builders" }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Avatar Cloud</title>
      <!-- Metadata only. Mounting discards everything outside <template>. -->
    </head>
    <body>
      <template>
        <div
          id="root"
          data-composition-id="avatar-cloud"
          data-start="0"
          data-duration="5"
          data-fps="30"
        >
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            /* INVARIANT: the host owns the box. The primitive owns one isolated,
               position-blind size container and no viewport-relative geometry. */
            #root {
              position: absolute;
              inset: 0;
              container-type: size;
              isolation: isolate;
              overflow: hidden;
              color: var(--fg, #f8fafc);
              font-family: var(--font-body, Inter, system-ui, sans-serif);
            }

            .ac-clip,
            .ac-stage,
            .ac-avatar-layer,
            .ac-links {
              position: absolute;
              inset: 0;
              width: 100%;
              height: 100%;
            }

            .ac-clip {
              overflow: hidden;
              background:
                radial-gradient(
                  ellipse at 50% 47.5%,
                  color-mix(in srgb, var(--fg, #f8fafc) 8%, transparent),
                  transparent 48%
                ),
                var(--bg, #0b0c10);
            }

            .ac-stage {
              overflow: hidden;
              transform-origin: center;
            }

            /* EDIT ZONE: lines use the same token pair as the avatars and hub.
               The SVG viewBox owns all endpoints, so links and positions cannot
               derive independently. */
            .ac-links {
              z-index: 1;
              overflow: visible;
              pointer-events: none;
            }

            .ac-link {
              fill: none;
              stroke: color-mix(in srgb, var(--fg, #f8fafc) 22%, transparent);
              stroke-width: 1.8;
              vector-effect: non-scaling-stroke;
            }

            .ac-link-secondary {
              stroke: color-mix(in srgb, var(--fg, #f8fafc) 14%, transparent);
              stroke-width: 1.2;
            }

            .ac-avatar-layer {
              z-index: 2;
            }

            .ac-avatar-position {
              position: absolute;
              transform: translate(-50%, -50%);
            }

            .ac-avatar {
              display: grid;
              place-items: center;
              width: min(calc(var(--ac-size) * 1cqw), calc(var(--ac-size) * 1.65cqh));
              aspect-ratio: 1;
              border: 0.16cqw solid color-mix(in srgb, var(--fg, #f8fafc) 32%, transparent);
              border-radius: 999cqw;
              background: var(--ac-color, color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent));
              color: var(--bg, #0b0c10);
              font-family: var(--font-display, Inter, system-ui, sans-serif);
              font-size: min(calc(var(--ac-size) * 0.35cqw), calc(var(--ac-size) * 0.58cqh));
              font-weight: 780;
              letter-spacing: -0.04em;
              line-height: 1;
              box-shadow:
                0 1.2cqh 2.8cqh color-mix(in srgb, var(--bg, #0b0c10) 48%, transparent),
                inset 0 0.12cqw 0 color-mix(in srgb, var(--fg, #f8fafc) 26%, transparent);
              will-change: transform, opacity;
            }

            /* Tones are depth steps of the ink, not hues: near faces sit at L1,
               mid at L2, far at L3. Value alone separates the cloud. */
            .ac-avatar[data-tone="0"] {
              --ac-color: color-mix(in srgb, var(--fg, #f8fafc) 72%, transparent);
            }

            .ac-avatar[data-tone="1"] {
              --ac-color: color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent);
            }

            .ac-avatar[data-tone="2"] {
              --ac-color: color-mix(in srgb, var(--fg, #f8fafc) 72%, transparent);
            }

            .ac-avatar[data-tone="3"] {
              --ac-color: color-mix(in srgb, var(--fg, #f8fafc) 30%, transparent);
            }

            .ac-avatar[data-tone="4"] {
              --ac-color: color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent);
            }

            .ac-hub {
              position: absolute;
              left: 50%;
              top: 47.5%;
              z-index: 5;
              display: flex;
              flex-direction: column;
              align-items: center;
              justify-content: center;
              min-width: 24cqw;
              max-width: 48cqw;
              min-height: 16cqh;
              padding: var(--space-2, 2cqh) var(--space-3, 3cqw);
              overflow: hidden;
              border: 0.16cqw solid color-mix(in srgb, var(--border, #334155) 82%, transparent);
              border-radius: var(--radius, 2.4cqw);
              background: var(--surface, #1e293b);
              box-shadow:
                0 2cqh 5cqh color-mix(in srgb, var(--bg, #0b0c10) 56%, transparent),
                inset 0 0.14cqw 0 color-mix(in srgb, var(--fg, #f8fafc) 18%, transparent);
              transform: translate(-50%, -50%);
              transform-origin: center;
            }

            .ac-kicker {
              color: var(--muted, #8b93a3);
              font-family: var(--font-display, Inter, system-ui, sans-serif);
              font-size: min(1.6cqw, 2.8cqh);
              font-weight: 720;
              letter-spacing: 0.16em;
              line-height: 1;
              text-transform: uppercase;
            }

            .ac-label {
              max-width: 40cqw;
              overflow: hidden;
              color: var(--fg, #f8fafc);
              font-family: var(--font-display, Inter, system-ui, sans-serif);
              font-size: min(5.2cqw, 9cqh);
              font-weight: 820;
              letter-spacing: -0.045em;
              line-height: 1.05;
              text-align: center;
              text-overflow: ellipsis;
              white-space: nowrap;
            }
          </style>

          <div
            id="avatar-cloud-clip"
            class="ac-clip clip"
            data-start="0"
            data-duration="5"
            data-track-index="0"
          >
            <section class="ac-stage" aria-label="Community network proof">
              <svg
                class="ac-links"
                viewBox="0 0 1000 600"
                preserveAspectRatio="none"
                aria-hidden="true"
                focusable="false"
              ></svg>
              <div class="ac-avatar-layer" aria-hidden="true"></div>
              <div class="ac-hub">
                <span class="ac-kicker">Community</span>
                <span class="ac-label"></span>
              </div>
            </section>
          </div>

          <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
          <script>
            (function () {
              "use strict";

              var root = document.currentScript.closest("#root") || document.getElementById("root");
              var compositionId = "avatar-cloud";
              var stage = root.querySelector(".ac-stage");
              var linksLayer = root.querySelector(".ac-links");
              var avatarLayer = root.querySelector(".ac-avatar-layer");
              var hub = root.querySelector(".ac-hub");
              var labelElement = root.querySelector(".ac-label");

              // EDIT ZONE: getVariables is the only owner of declared defaults
              // plus host overrides. Values are normalized exactly once here.
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              var rawCount = Number(vars.avatarCount);
              var avatarCount = Number.isFinite(rawCount) ? Math.round(rawCount) : 10;
              avatarCount = Math.max(6, Math.min(16, avatarCount));
              var showLinks = vars.showLinks === "no" ? "no" : "yes";
              var label = vars.label == null ? "50k builders" : String(vars.label);
              labelElement.textContent = label;

              // One normalized coordinate table owns both DOM placement and SVG
              // endpoints. Index-based sine offsets make the ellipse loose while
              // remaining identical under every seek and render.
              var CENTER = { x: 500, y: 285 };
              var positions = [];
              var avatars = [];
              var lettermarks = [
                "AK",
                "BM",
                "CR",
                "DL",
                "ES",
                "FT",
                "GN",
                "HP",
                "IV",
                "JO",
                "KQ",
                "LU",
                "MW",
                "NX",
                "OY",
                "PZ",
              ];
              var avatarFragment = document.createDocumentFragment();

              for (var index = 0; index < avatarCount; index += 1) {
                var angle = -Math.PI / 2 + (index / avatarCount) * Math.PI * 2;
                var radiusX = 356 + Math.sin((index + 1) * 2.37) * 24;
                var radiusY = 194 + Math.sin((index + 1) * 1.61) * 18;
                var x = CENTER.x + Math.cos(angle) * radiusX;
                var y = CENTER.y + Math.sin(angle) * radiusY;
                var size = 7.5 + Math.sin((index + 1) * 1.93) * 0.8;

                positions.push({ x: x, y: y });

                var positionElement = document.createElement("div");
                positionElement.className = "ac-avatar-position";
                positionElement.style.left = x / 10 + "%";
                positionElement.style.top = y / 6 + "%";

                var avatar = document.createElement("div");
                avatar.className = "ac-avatar";
                avatar.dataset.tone = String(index % 5);
                avatar.style.setProperty("--ac-size", size.toFixed(2));
                avatar.textContent = lettermarks[index];
                positionElement.appendChild(avatar);
                avatarFragment.appendChild(positionElement);
                avatars.push(avatar);
              }

              avatarLayer.replaceChildren(avatarFragment);

              var lines = [];

              function appendLine(from, to, secondary) {
                var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
                var length = Math.hypot(to.x - from.x, to.y - from.y);
                line.setAttribute("x1", String(from.x));
                line.setAttribute("y1", String(from.y));
                line.setAttribute("x2", String(to.x));
                line.setAttribute("y2", String(to.y));
                line.setAttribute("class", secondary ? "ac-link ac-link-secondary" : "ac-link");
                line.style.strokeDasharray = length + " " + length;
                line.dataset.length = String(length);
                linksLayer.appendChild(line);
                lines.push(line);
              }

              if (showLinks === "yes") {
                positions.forEach(function (position) {
                  appendLine(CENTER, position, false);
                });

                positions.forEach(function (position, positionIndex) {
                  if (positionIndex % 3 === 0) {
                    appendLine(position, positions[(positionIndex + 2) % avatarCount], true);
                  }
                });
              }

              // RETIME RANGE: these values own the complete lifecycle. The link
              // wave fits the maximum 22-line network before the fixed sync point.
              var IN_BASE = 2.2;
              var OUT_BASE = 0.5;
              var HUB_START_BASE = 0.05;
              var HUB_DURATION_BASE = 0.5;
              var AVATAR_START_BASE = 0.24;
              var AVATAR_DURATION_BASE = 0.5;
              var AVATAR_STAGGER_BASE = 0.055;
              var LINES_START_BASE = 1.02;
              var LINES_DURATION_BASE = 0.55;
              var LINE_STAGGER_BASE = 0.025;

              var durationValue = Number(root.dataset.duration);
              var duration = Number.isFinite(durationValue) && durationValue > 0 ? durationValue : 5;
              var envelopeScale = duration < IN_BASE + OUT_BASE ? duration / (IN_BASE + OUT_BASE) : 1;
              var IN = IN_BASE * envelopeScale;
              var OUT = OUT_BASE * envelopeScale;
              var HOLD = Math.max(0, duration - IN - OUT);
              var HOLD_START = IN;
              var OUT_START = IN + HOLD;
              var HUB_START = HUB_START_BASE * envelopeScale;
              var HUB_DURATION = HUB_DURATION_BASE * envelopeScale;
              var AVATAR_START = AVATAR_START_BASE * envelopeScale;
              var AVATAR_DURATION = AVATAR_DURATION_BASE * envelopeScale;
              var AVATAR_STAGGER = AVATAR_STAGGER_BASE * envelopeScale;
              var LINES_START = LINES_START_BASE * envelopeScale;
              var LINES_DURATION = LINES_DURATION_BASE * envelopeScale;
              var LINE_STAGGER = LINE_STAGGER_BASE * envelopeScale;

              gsap.set(stage, { opacity: 1, scale: 1 });
              gsap.set(hub, { opacity: 0, scale: 0.94 });
              gsap.set(avatars, { opacity: 0, scale: 0.92, x: 0, y: 0 });
              lines.forEach(function (line) {
                gsap.set(line, { opacity: 0, strokeDashoffset: Number(line.dataset.length) });
              });

              var timeline = gsap.timeline({ paused: true });

              // IN: --ease-standard maps to GSAP power2.out in the shared easing
              // vocabulary. No entrance starts at scale zero.
              timeline.to(
                hub,
                { opacity: 1, scale: 1, duration: HUB_DURATION, ease: "power2.out" },
                HUB_START,
              );
              timeline.to(
                avatars,
                {
                  opacity: 1,
                  scale: 1,
                  duration: AVATAR_DURATION,
                  ease: "power2.out",
                  stagger: { each: AVATAR_STAGGER, from: "start" },
                },
                AVATAR_START,
              );

              lines.forEach(function (line, lineIndex) {
                timeline.to(
                  line,
                  {
                    opacity: 1,
                    strokeDashoffset: 0,
                    duration: LINES_DURATION,
                    ease: "power2.out",
                  },
                  LINES_START + lineIndex * LINE_STAGGER,
                );
              });

              // HOLD: deterministic index-derived endpoints with --ease-drift's
              // sine.inOut curve. One finite leg fills the elastic hold exactly.
              if (HOLD > 0) {
                avatars.forEach(function (avatar, avatarIndex) {
                  var driftX = Math.sin((avatarIndex + 1) * 1.73) * 0.85;
                  var driftY = Math.sin((avatarIndex + 1) * 2.41) * 1.15;
                  timeline.fromTo(
                    avatar,
                    { x: -driftX + "cqw", y: -driftY + "cqh" },
                    {
                      x: driftX + "cqw",
                      y: driftY + "cqh",
                      duration: HOLD,
                      ease: "sine.inOut",
                      immediateRender: false,
                    },
                    HOLD_START,
                  );
                });
              }

              // OUT: the complete network leaves once with the asymmetric exit
              // curve. HOLD motion resolves before this phase begins.
              timeline.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);

              timeline.seek(0);

              window.__timelines = window.__timelines || {};
              window.__timelines[compositionId] = timeline;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `effects` `avatars` `community` `network` `social-proof` `prove`.

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

- [Avatar Group Hover](/catalog/components/avatar-group-hover.md)
- [Add an avatar presenter](/guides/avatar-presenter.md)
- [Cloud rendering](/deploy/cloud.md)
