> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# SVG Mask Reveal

> A soft token-colored sweep reveals media only through an editable SVG wordmark mask.

# SVG Mask Reveal

A soft token-colored sweep reveals media only through an editable SVG wordmark mask.

**SVG Mask Reveal**

```html svg-mask-reveal.html
<!doctype html>
<!--
  svg-mask-reveal -- HyperFrames video primitive (effects / burst / exhibit)

  Concept: token-colored media is visible only through an SVG wordmark mask.
  A soft alpha band crosses the mask from left to right, making the wordmark
  act as a window into the fill beneath it. One mechanic, one job: exhibiting
  a wordmark through a vector mask reveal.

  Compiled-from evidence: hyperframes-research-css-masks.md, SvgMaskReveal and
  "SVG text as mask over image or video". The implementation follows its
  inline SVG, explicit user-space bounds, namespaced fragment IDs, and soft
  reveal geometry guidance.

  Use when: a logo, product name, or short title should reveal a branded fill
  inside its glyphs. Use a path-based sibling when exact brand glyph outlines
  matter more than editable text.

  Variables (declared in data-composition-variables below):
    - text (string, default "REVEAL"): the wordmark used as the SVG mask.
    - revealProgress (number, default 100): final revealed width, 0 to 100%.
    - featherPx (number, default 24): softness of the traveling mask edge.

  Envelope (burst profile, fixed IN/OUT, elastic HOLD):
    IN_BASE  = 0.90s  the soft band sweeps across the wordmark
    HOLD     = max(0, D - (IN_BASE + OUT_BASE)); final state stays readable
    OUT_BASE = 0.45s  the complete wordmark fades cleanly
    If D is shorter than IN_BASE + OUT_BASE, IN and OUT compress together.

  Sync point: SWEEP_LANDS = 0.80s into an uncompressed IN. It scales with IN
  only when the envelope is compressed and never moves into the elastic HOLD.

  Sound: none. The sweep is designed to accept scene-level sound if desired.

  Mount contract: this is a template-wrapped sub-composition. The root has no
  data-width or data-height, fills the host with inset:0, establishes cqw/cqh
  sizing with container-type:size, and registers one paused GSAP timeline under
  the hardcoded composition id "svg-mask-reveal". The demo mounts this file via
  data-composition-src so it exercises the real transport path.
-->
<html
  lang="en"
  data-composition-variables='[
    { "id": "text", "type": "string", "role": "content", "label": "Wordmark", "description": "Text used as the SVG mask window.", "default": "REVEAL", "maxLength": 16 },
    { "id": "revealProgress", "type": "number", "role": "content", "label": "Reveal progress", "description": "Final revealed width of the wordmark.", "default": 100, "min": 0, "max": 100, "step": 1, "unit": "%" },
    { "id": "featherPx", "type": "number", "role": "style", "label": "Edge feather", "description": "Softness of the traveling reveal edge.", "default": 24, "min": 0, "max": 120, "step": 1, "unit": "px" }
  ]'
>
  <head>
    <meta charset="UTF-8" />
    <title>SVG Mask Reveal</title>
  </head>
  <body>
    <template>
      <div
        id="root"
        data-composition-id="svg-mask-reveal"
        data-start="0"
        data-duration="4"
        data-fps="30"
      >
        <style>
          @property --svg-mask-reveal-sweep-x {
            syntax: "<percentage>";
            inherits: false;
            initial-value: -10%;
          }

          *,
          *::before,
          *::after {
            box-sizing: border-box;
          }

          /* INVARIANT: the host owns dimensions. All visible layout derives
             from this size container, and the root is styled only by #root. */
          #root {
            position: absolute;
            inset: 0;
            container-type: size;
            isolation: isolate;
            overflow: hidden;
            background: var(--bg);
            color: var(--fg);
            font-family: var(--font-body);
          }

          .smr-clip,
          .smr-stage {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
          }

          .smr-stage {
            display: grid;
            place-items: center;
          }

          /* EDIT ZONE: presentation scale. The SVG keeps its 12:5 viewBox
             while these container-relative limits make it fit wide and tall
             hosts without stage-pixel assumptions. */
          .smr-svg {
            display: block;
            width: min(92cqw, 168cqh);
            height: auto;
            overflow: visible;
          }

          .smr-media-start {
            stop-color: var(--brand);
          }

          .smr-media-middle {
            stop-color: var(--accent);
          }

          .smr-media-end {
            stop-color: var(--fg);
          }

          .smr-mask-text {
            fill: var(--fg);
            font-family: var(--font-display);
            font-size: 240px;
            font-weight: 850;
            letter-spacing: 0.02em;
          }

          .smr-media {
            mask-mode: alpha, alpha;
            mask-composite: intersect;
          }

          .smr-outline {
            fill: none;
            stroke: var(--border);
            stroke-width: 2;
            font-family: var(--font-display);
            font-size: 240px;
            font-weight: 850;
            letter-spacing: 0.02em;
          }
        </style>

        <div
          id="svg-mask-reveal-clip"
          class="smr-clip clip"
          data-start="0"
          data-duration="4"
          data-track-index="0"
        >
          <div class="smr-stage">
            <svg
              class="smr-svg"
              viewBox="0 0 1200 500"
              role="img"
              aria-label="REVEAL"
              preserveAspectRatio="xMidYMid meet"
            >
              <defs>
                <!-- IDs carry the full primitive prefix so fragment URLs do
                     not collide with sibling compositions after assembly. -->
                <linearGradient
                  id="svg-mask-reveal-media-gradient"
                  gradientUnits="userSpaceOnUse"
                  x1="80"
                  y1="420"
                  x2="1120"
                  y2="80"
                >
                  <stop class="smr-media-start" offset="0" />
                  <stop class="smr-media-middle" offset="0.52" />
                  <stop class="smr-media-end" offset="1" />
                </linearGradient>

                <!-- The word mask owns only the vector silhouette. The media
                     rect independently intersects it with the CSS sweep. -->
                <mask
                  id="svg-mask-reveal-word-mask"
                  mask-type="alpha"
                  maskUnits="userSpaceOnUse"
                  maskContentUnits="userSpaceOnUse"
                  x="0"
                  y="0"
                  width="1200"
                  height="500"
                >
                  <text
                    id="svg-mask-reveal-text"
                    class="smr-mask-text"
                    x="600"
                    y="260"
                    text-anchor="middle"
                    dominant-baseline="middle"
                    textLength="1000"
                    lengthAdjust="spacingAndGlyphs"
                  >
                    REVEAL
                  </text>
                </mask>
              </defs>

              <!-- The outline stays quiet and complete while the token-colored
                   media itself remains visible only through the word mask. -->
              <text
                id="svg-mask-reveal-outline"
                class="smr-outline"
                x="600"
                y="260"
                text-anchor="middle"
                dominant-baseline="middle"
                textLength="1000"
                lengthAdjust="spacingAndGlyphs"
              >
                REVEAL
              </text>
              <rect
                class="smr-media"
                x="0"
                y="0"
                width="1200"
                height="500"
                fill="url(#svg-mask-reveal-media-gradient)"
              />
            </svg>
          </div>
        </div>

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

            var root = document.getElementById("root");
            var compositionId = "svg-mask-reveal";
            var stage = root.querySelector(".smr-stage");
            var svg = root.querySelector(".smr-svg");
            var maskText = root.querySelector("#svg-mask-reveal-text");
            var outlineText = root.querySelector("#svg-mask-reveal-outline");
            var mediaRect = root.querySelector(".smr-media");

            // EDIT ZONE: declared defaults are merged with per-instance host
            // overrides by the runtime. Missing and zero are distinct here.
            var vars =
              window.__hyperframes && window.__hyperframes.getVariables
                ? window.__hyperframes.getVariables()
                : {};
            var textValue = vars.text == null ? "REVEAL" : String(vars.text);
            var progressValue = vars.revealProgress == null ? NaN : Number(vars.revealProgress);
            var revealProgress = Number.isFinite(progressValue)
              ? Math.max(0, Math.min(100, progressValue))
              : 100;
            var featherValue = vars.featherPx == null ? NaN : Number(vars.featherPx);
            var featherPx = Number.isFinite(featherValue)
              ? Math.max(0, Math.min(120, featherValue))
              : 24;

            maskText.textContent = textValue;
            outlineText.textContent = textValue;
            svg.setAttribute("aria-label", textValue);

            // INVARIANT: the word and sweep masks are independent layers on
            // the media rect. This initialization owns their feather geometry.
            var VIEW_WIDTH = 1200;
            var featherPercent = (featherPx / VIEW_WIDTH) * 100;
            var hiddenSweepPercent = -featherPercent;
            mediaRect.style.maskImage =
              "url(#svg-mask-reveal-word-mask), linear-gradient(to right, black var(--svg-mask-reveal-sweep-x, -10%), transparent calc(var(--svg-mask-reveal-sweep-x, -10%) + " +
              featherPercent +
              "%))";

            // RETIME RANGE: IN_BASE and OUT_BASE define the burst envelope.
            // The sweep uses --ease-standard's GSAP owner, power2.out.
            var IN_BASE = 0.9;
            var OUT_BASE = 0.45;
            var SWEEP_START_BASE = 0.08;
            var SWEEP_LANDS_BASE = 0.8;
            var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
            var envelopeBase = IN_BASE + OUT_BASE;
            var scale = duration < envelopeBase ? duration / envelopeBase : 1;
            var IN = IN_BASE * scale;
            var OUT = OUT_BASE * scale;
            var HOLD = Math.max(0, duration - IN - OUT);
            var OUT_START = IN + HOLD;
            var SWEEP_START = SWEEP_START_BASE * scale;
            var SWEEP_DURATION = (SWEEP_LANDS_BASE - SWEEP_START_BASE) * scale;
            var targetSweepPercent = revealProgress === 0 ? hiddenSweepPercent : revealProgress;

            gsap.set(stage, { opacity: 1 });
            gsap.set(mediaRect, {
              "--svg-mask-reveal-sweep-x": hiddenSweepPercent + "%",
            });

            var tl = gsap.timeline({ paused: true });
            tl.to(
              mediaRect,
              {
                "--svg-mask-reveal-sweep-x": targetSweepPercent + "%",
                duration: SWEEP_DURATION,
                ease: "power2.out",
              },
              SWEEP_START,
            );
            tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
            tl.seek(0);

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

Copy npx hyperframes add svg-mask-reveal to the clipboard

That writes one file: `compositions/components/svg-mask-reveal.html`.

## Paste it into your composition

Open `compositions/components/svg-mask-reveal.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 |
| - | - | - | - |
| `text` | `REVEAL` | string | Text used as the SVG mask window. |
| `revealProgress` | `100` | 0% to 100%, step 1% | Final revealed width of the wordmark. |
| `featherPx` | `24` | 0px to 120px, step 1px | Softness of the traveling reveal edge. |

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
<div
  data-composition-id="svg-mask-reveal"
  data-composition-src="compositions/components/svg-mask-reveal.html"
  data-variable-values='{"text":"REVEAL","revealProgress":100,"featherPx":24}'
></div>
```

{/* hf:generated-footer */}

Tagged `effect` `svg` `mask` `wordmark` `reveal` `exhibit`.

## Related topics

* [Browse the complete Catalog](/catalog.md)
* [Add assets and Catalog items in the browser editor](/studio/assets-and-blocks.md)
* [Put blocks together into one video](/concepts/compositions.md)
