Files
hyperframes/registry/components/svg-mask-reveal/svg-mask-reveal.html
T
Miguel Ángel 9734578e60 feat(registry): bring back the video-primitive moves (#3169)
Restores the 208 catalog items reverted after their previews 404'd in
production, this time on the payload mechanism rather than the .html files
that caused the outage.

The generator no longer writes a preview document to docs/public. That writer,
and the machinery under it, existed only to produce files the docs host
discards, so it is gone rather than bypassed. Items now embed the composition
itself via a payload, which is what the previous change already does for the
items that were already in the catalog.

The variables explorer is parked, not restored: it drove its preview through
the same unpublished .html path, so it would have shown an empty frame. Items
that declare variables get the live player plus the static variables table, and
reconnecting the explorer to payloads is a follow-up.
2026-08-10 18:46:03 -04:00

317 lines
12 KiB
HTML
Vendored

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