Install
That writes one file:compositions/components/ascii-trail-reveal.html.
Paste it into your composition
Opencompositions/components/ascii-trail-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 |
|---|---|---|---|
label | The reveal | string | Text shown on the revealed panel. |
accent | green | green, blue, violet | Color used by the trail and revealed panel. |
trail_width | narrow | narrow, wide | Width of the cell reveal tail around the authored path. |
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:
<div
data-composition-id="ascii-trail-reveal"
data-composition-src="compositions/components/ascii-trail-reveal.html"
data-variable-values='{"label":"The reveal","accent":"green","trail_width":"narrow"}'
></div>
Source
ascii-trail-reveal.html
ascii-trail-reveal.html
<!doctype html>
<!--
ascii-trail-reveal: HyperFrames video primitive (intros and reveals)
Concept: a luminous actor follows an authored S-curve across an ASCII
signal grid. Cells flip away behind the actor to reveal the real panel,
while the lagging edge dissolves in a deterministic cascade.
Variables:
- label (string, default "The reveal"): text on the revealed panel.
- accent (green | blue | violet, default "green"): trail and panel accent.
- trail_width (narrow | wide, default "narrow"): width of the reveal tail.
Envelope (fixed IN and OUT, elastic HOLD only, never timeScale):
IN_BASE = 3.20s the actor crosses the authored path, rests briefly,
then dissolves as the final cells complete their flip
HOLD = elastic = max(0, D - IN_BASE), fully revealed content drifts
subtly and remains legible
OUT_BASE = 0.00s the revealed panel is the required end state
If D is shorter than IN_BASE, IN compresses to D and HOLD is zero.
Determinism: glyphs, cell timing jitter, and reveal order come only from
row, column, and index math. No clock or random source participates.
-->
<html
lang="en"
data-composition-id="ascii-trail-reveal"
data-composition-duration="4"
data-composition-variables='[
{ "id": "label", "type": "string", "role": "content", "label": "Panel label", "description": "Text shown on the revealed panel.", "default": "The reveal" },
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Color used by the trail and revealed panel.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
{ "id": "trail_width", "type": "enum", "role": "layout", "label": "Trail width", "description": "Width of the cell reveal tail around the authored path.", "default": "narrow", "options": [{ "value": "narrow", "label": "Narrow" }, { "value": "wide", "label": "Wide" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>ASCII Trail Reveal</title>
</head>
<body>
<template>
<div id="root" data-composition-id="ascii-trail-reveal" data-duration="4" data-fps="30">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
#root {
position: absolute;
inset: 0;
overflow: hidden;
container-type: size;
isolation: isolate;
color: var(--fg, #f8fafc);
font-family: var(--font-body, Inter, system-ui, sans-serif);
pointer-events: none;
}
.atr-clip {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
}
.atr-panel {
position: relative;
width: 88cqw;
height: 76cqh;
overflow: hidden;
border: 0.16cqmin solid color-mix(in srgb, var(--atr-accent) 52%, #475569);
border-radius: 3cqmin;
background: #111820;
box-shadow:
0 4cqh 12cqh rgba(0, 0, 0, 0.45),
inset 0 0 0 0.12cqmin rgba(255, 255, 255, 0.04);
perspective: 80cqmin;
}
.atr-content {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 8cqw;
background:
radial-gradient(
circle at 50% 45%,
color-mix(in srgb, var(--atr-accent) 24%, transparent),
transparent 48%
),
linear-gradient(145deg, #17212c, #0c1219);
transform-origin: 50% 50%;
}
.atr-label {
max-width: 76cqw;
color: #f8fafc;
font-family: var(--font-display, Inter, system-ui, sans-serif);
font-size: clamp(24px, 10cqmin, 112px);
font-weight: 760;
line-height: 0.96;
letter-spacing: -0.045em;
text-align: center;
text-wrap: balance;
text-shadow: 0 0 4cqmin color-mix(in srgb, var(--atr-accent) 28%, transparent);
}
.atr-grid {
--atr-columns: 24;
--atr-rows: 14;
position: absolute;
inset: 0;
z-index: 2;
display: grid;
grid-template-columns: repeat(var(--atr-columns), 1fr);
grid-template-rows: repeat(var(--atr-rows), 1fr);
overflow: hidden;
background: #0b1016;
transform-style: preserve-3d;
}
.atr-cell {
display: grid;
place-items: center;
min-width: 0;
min-height: 0;
overflow: hidden;
border-right: 0.06cqmin solid rgba(148, 163, 184, 0.08);
border-bottom: 0.06cqmin solid rgba(148, 163, 184, 0.08);
background: color-mix(in srgb, #111820 94%, var(--atr-accent));
color: color-mix(in srgb, var(--atr-accent) 72%, #dbeafe);
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace);
font-size: clamp(7px, 2.35cqmin, 22px);
font-weight: 650;
line-height: 1;
transform-origin: 50% 50%;
backface-visibility: hidden;
will-change: transform, opacity;
}
.atr-path-layer {
position: absolute;
inset: 0;
z-index: 3;
width: 100%;
height: 100%;
overflow: visible;
}
.atr-path-guide {
fill: none;
stroke: color-mix(in srgb, var(--atr-accent) 34%, transparent);
stroke-width: 2;
stroke-linecap: round;
stroke-dasharray: 3 14;
}
.atr-actor-halo {
fill: color-mix(in srgb, var(--atr-accent) 22%, transparent);
}
.atr-actor-core {
fill: #f8fafc;
stroke: var(--atr-accent);
stroke-width: 7;
filter: drop-shadow(0 0 18px var(--atr-accent));
}
</style>
<div
id="ascii-trail-reveal-clip"
class="atr-clip clip"
data-start="0"
data-duration="4"
data-track-index="0"
>
<div class="atr-panel">
<div class="atr-content">
<div class="atr-label"></div>
</div>
<div class="atr-grid" aria-hidden="true"></div>
<svg
class="atr-path-layer"
viewBox="0 0 1000 600"
preserveAspectRatio="none"
aria-hidden="true"
>
<path class="atr-path-guide"></path>
<g id="ascii-trail-reveal-actor">
<circle class="atr-actor-halo" r="42"></circle>
<circle class="atr-actor-core" r="13"></circle>
</g>
</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 content = root.querySelector(".atr-content");
var label = root.querySelector(".atr-label");
var grid = root.querySelector(".atr-grid");
var guide = root.querySelector(".atr-path-guide");
var actor = root.querySelector("#ascii-trail-reveal-actor");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var labelText = vars.label == null ? "The reveal" : String(vars.label);
var accent = vars.accent === "blue" || vars.accent === "violet" ? vars.accent : "green";
var trailWidth = vars.trail_width === "wide" ? "wide" : "narrow";
var accentColors = {
green: "var(--brand, #4ade80)",
blue: "var(--accent, #38bdf8)",
violet: "var(--accent-2, #a78bfa)",
};
label.textContent = labelText;
root.style.setProperty("--atr-accent", accentColors[accent]);
var COLS = 24;
var ROWS = 14;
var GLYPHS = " .:-=+*#%@01/\\<>[]{}";
var PATH_POINTS = [
[-80, 460],
[340, 460],
[660, 140],
[1080, 140],
];
var cells = [];
function sCurvePoint(progress) {
var inverse = 1 - progress;
var startWeight = inverse * inverse * inverse;
var firstWeight = 3 * inverse * inverse * progress;
var secondWeight = 3 * inverse * progress * progress;
var endWeight = progress * progress * progress;
return {
x:
startWeight * PATH_POINTS[0][0] +
firstWeight * PATH_POINTS[1][0] +
secondWeight * PATH_POINTS[2][0] +
endWeight * PATH_POINTS[3][0],
y:
startWeight * PATH_POINTS[0][1] +
firstWeight * PATH_POINTS[1][1] +
secondWeight * PATH_POINTS[2][1] +
endWeight * PATH_POINTS[3][1],
};
}
guide.setAttribute(
"d",
"M " +
PATH_POINTS[0].join(" ") +
" C " +
PATH_POINTS.slice(1)
.map(function (point) {
return point.join(" ");
})
.join(" "),
);
for (var row = 0; row < ROWS; row += 1) {
for (var column = 0; column < COLS; column += 1) {
var index = row * COLS + column;
var cell = document.createElement("span");
var glyphIndex = (index * 17 + row * 11 + column * 7) % GLYPHS.length;
cell.className = "atr-cell";
cell.textContent = GLYPHS.charAt(glyphIndex);
grid.appendChild(cell);
cells.push({ element: cell, row: row, column: column, index: index });
}
}
var IN_BASE = 3.2;
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
var IN = Math.min(IN_BASE, duration);
var HOLD = Math.max(0, duration - IN);
var phaseScale = IN / IN_BASE;
var actorState = { progress: 0 };
function updateActor() {
var point = sCurvePoint(actorState.progress);
actor.setAttribute("transform", "translate(" + point.x + " " + point.y + ")");
}
gsap.set(content, { scale: 0.985 });
gsap.set(guide, { opacity: 0 });
gsap.set(actor, { opacity: 0 });
updateActor();
var tl = gsap.timeline({ paused: true });
tl.to(content, { scale: 1, duration: IN, ease: "power2.out" }, 0);
tl.to(
guide,
{ opacity: 0.72, duration: 0.24 * phaseScale, ease: "power2.out" },
0.18 * phaseScale,
);
tl.to(
actor,
{ opacity: 1, duration: 0.24 * phaseScale, ease: "power2.out" },
0.22 * phaseScale,
);
tl.to(
actorState,
{
progress: 1,
duration: 2.35 * phaseScale,
ease: "power2.out",
onUpdate: updateActor,
},
0.34 * phaseScale,
);
cells.forEach(function (cellData) {
var progress = (cellData.column + 0.5) / COLS;
var pathPoint = sCurvePoint(progress);
var cellY = ((cellData.row + 0.5) / ROWS) * 600;
var distanceFromPath = Math.abs(cellY - pathPoint.y) / 600;
var distanceLag = distanceFromPath * (trailWidth === "wide" ? 0.5 : 0.8);
var indexJitter = ((cellData.index * 37) % 11) * 0.009;
var revealAt = (0.42 + progress * 1.92 + distanceLag + indexJitter) * phaseScale;
var flipDuration = (0.2 + ((cellData.index * 13) % 5) * 0.018) * phaseScale;
tl.to(
cellData.element,
{
rotationY: 88,
scaleX: 0.3,
opacity: 0,
duration: flipDuration,
ease: "power2.in",
},
revealAt,
);
});
tl.to(
guide,
{ opacity: 0, duration: 0.24 * phaseScale, ease: "power2.in" },
2.82 * phaseScale,
);
tl.to(
actor,
{ opacity: 0, duration: 0.24 * phaseScale, ease: "power2.in" },
2.92 * phaseScale,
);
if (HOLD > 0) {
tl.to(
content,
{
scale: 1.008,
duration: HOLD / 2,
ease: "sine.inOut",
yoyo: true,
repeat: 1,
},
IN,
);
}
tl.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines["ascii-trail-reveal"] = tl;
})();
</script>
</div>
</template>
</body>
</html>
motion-primitive intro reveal ascii trail grid.