Install
That writes one file:compositions/components/stitched-text-draw.html.
Paste it into your composition
Opencompositions/components/stitched-text-draw.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 | STITCH | string | Uppercased A-Z 0-9 and space, clamped to 12 characters. |
stitch | fine | fine, coarse | Dash/gap scale, thread width, and jitter amplitude. |
accent | green | green, blue, violet | Thread color: green rides —brand, blue rides —accent, violet rides —accent-2. |
exit | none | none, fade, up | Optional departure. Default none: the stitched word holds until the frame cuts. |
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="stitched-text-draw"
data-composition-src="compositions/components/stitched-text-draw.html"
data-variable-values='{"text":"STITCH","stitch":"fine","accent":"green","exit":"none"}'
></div>
Source
stitched-text-draw.html
stitched-text-draw.html
<!doctype html>
<!--
stitched-text-draw: HyperFrames video primitive (type / craft, Wave M
experiment M6)
Text drawn as thread stitches. Letters come from a single-stroke display
alphabet (A-Z 0-9) authored as a compact stroke table of lines and
elliptical arcs per glyph, flattened to polylines at mount, resampled to
stitch-scale steps, and displaced by seeded perpendicular jitter so every
stitch sits at a slightly different angle (hand-sewn wobble). Each stroke
draws thread-first: a needle dot leads the reveal front, needle-hole dots
appear at every stitch boundary behind it, and each letter finishes with
a tiny thread-tail overshoot (the thread sticks out past the stroke end
along its exit tangent, then is pulled snug).
Draw mechanic (gotcha 8, and gotcha "nested SVG mask doesn't repaint
under seek"): the reveal is ATTRIBUTE-driven stroke-dashoffset on the
stitched path itself, using the double-dash trick instead of an SVG mask.
Each path's dasharray is its stitch pattern (dash, gap, dash, ...) built
to sum EXACTLY to the path length L, followed by one closing gap of L
(total 2L). Animating the stroke-dashoffset attribute from L to 0 sweeps
the reveal front from start to end while the visible dashes settle into
their final stitch positions at offset 0. No <mask>, no <clipPath>, no
CSS dashoffset property (Chrome under-invalidates it on reverse seeks);
the painter primes and updates the attribute via setAttribute.
Variables (declared in data-composition-variables below):
- text (string, default "STITCH"): uppercased; A-Z 0-9 and space,
other characters render as spaces; clamped to 12 characters.
- stitch ("fine" | "coarse", default "fine"): dash/gap scale, thread
width, and jitter amplitude.
- accent ("green" | "blue" | "violet", default "green"): thread color;
green rides --brand, blue rides --accent, violet rides --accent-2.
- exit ("none" | "fade" | "up", default "none").
Envelope, fixed IN and OUT with elastic HOLD only (never timeScale):
IN_BASE = 0.15 lead + 2.9 draw + 0.15 margin
HOLD = max(0, D - IN - OUT), truly still
OUT_BASE = 0.45s only when exit != none
If D < IN_BASE + OUT_BASE the whole envelope compresses together.
Letters draw strictly in sequence; each letter's share of the draw
window is proportional to its total stroke length, with a fixed
needle-jump pause between letters.
Determinism and seek-safety: all geometry, jitter (fixed LCG seed
0x57174c3d), stitch patterns, hole positions, and the per-stroke draw
schedule are computed once at mount. One inert anchor tween spans [0, D];
a painter recomputes every stroke's dashoffset, every hole's visibility,
and the needle/tail pose as pure functions of tl.time() on each update
(zero per-element tweens, gotcha 9). Writes are idempotent; eventful
seeks (suppressEvents=false) land identical frames in any order and
either direction.
Mount contract: the runtime clones only this template. #root fills the
host box (no data-width/data-height, container-type: size, cqmin units),
is styled via #root only, and registers one paused timeline under the
LITERAL "stitched-text-draw" key. getTotalLength drives all dash math
(never the pathLength attribute, never non-scaling-stroke on dashed
paths).
-->
<html
lang="en"
data-composition-id="stitched-text-draw"
data-composition-duration="4"
data-composition-variables='[
{ "id": "text", "type": "string", "role": "content", "label": "Text", "description": "Uppercased A-Z 0-9 and space, clamped to 12 characters.", "default": "STITCH" },
{ "id": "stitch", "type": "enum", "role": "style", "label": "Stitch", "description": "Dash/gap scale, thread width, and jitter amplitude.", "default": "fine", "options": [{ "value": "fine", "label": "Fine" }, { "value": "coarse", "label": "Coarse" }] },
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Thread color: green rides --brand, blue rides --accent, violet rides --accent-2.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
{ "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure. Default none: the stitched word holds until the frame cuts.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Stitched Text Draw</title>
</head>
<body>
<template>
<div id="root" data-composition-id="stitched-text-draw" 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-display, system-ui, sans-serif);
pointer-events: none;
}
.std-clip {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
background: var(--bg, transparent);
}
.std-stage {
display: grid;
place-items: center;
will-change: transform, opacity;
}
.std-svg {
display: block;
width: 92cqw;
height: 56cqh;
}
</style>
<div
id="stitched-text-draw-clip"
class="std-clip clip"
data-start="0"
data-duration="4"
data-track-index="0"
>
<div class="std-stage" role="img">
<svg
class="std-svg"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet"
aria-hidden="true"
></svg>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
"use strict";
var SVG_NS = "http://www.w3.org/2000/svg";
var root = document.getElementById("root");
var stage = root.querySelector(".std-stage");
var svg = root.querySelector(".std-svg");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var text = (vars.text == null || vars.text === "" ? "STITCH" : String(vars.text))
.toUpperCase()
.slice(0, 12);
var stitch = vars.stitch === "coarse" ? "coarse" : "fine";
// Each enum choice routes to a DIFFERENT contract token so the
// variable stays meaningful under a theme.
var accentColors = {
green: "var(--brand, #52525b)",
blue: "var(--accent, #52525b)",
violet: "var(--accent-2, #52525b)",
};
var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
? vars.accent
: "green";
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
root.style.setProperty("--std-accent", accentColors[accent]);
stage.setAttribute("aria-label", text);
// Stitch registers: dash/gap in glyph units (glyph box 70x100).
var registers = {
fine: { dash: 6.5, gap: 4.5, width: 2.6, jitter: 1.5 },
coarse: { dash: 10, gap: 7, width: 4.2, jitter: 2.3 },
};
var reg = registers[stitch];
// ---- Single-stroke display alphabet -------------------------
// Glyph box: width 70 (space 40), cap height 100, y down.
// Each glyph is an array of strokes; a stroke is an array of
// [x, y] points (lines) built from line points and flattened
// elliptical arcs. Curves only need enough samples to read as
// curved once resampled and jittered.
function arcPts(cx, cy, rx, ry, a0, a1) {
var steps = Math.max(6, Math.ceil(Math.abs(a1 - a0) / 18));
var pts = [];
for (var i = 0; i <= steps; i += 1) {
var a = ((a0 + ((a1 - a0) * i) / steps) * Math.PI) / 180;
pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
}
return pts;
}
function join() {
var out = [];
for (var i = 0; i < arguments.length; i += 1) {
out = out.concat(arguments[i]);
}
return out;
}
var GLYPHS = {
A: [
[
[4, 100],
[35, 2],
[66, 100],
],
[
[15, 64],
[55, 64],
],
],
B: [
[
[6, 2],
[6, 98],
],
join([[6, 2]], arcPts(36, 26, 24, 24, -90, 90), [[6, 50]]),
join([[6, 50]], arcPts(38, 74, 26, 24, -90, 90), [[6, 98]]),
],
C: [arcPts(38, 50, 30, 46, -60, -300)],
D: [
join(
[
[6, 2],
[24, 2],
],
arcPts(24, 50, 40, 48, -90, 90),
[
[24, 98],
[6, 98],
[6, 4],
],
),
],
E: [
[
[60, 2],
[8, 2],
[8, 98],
[60, 98],
],
[
[8, 50],
[48, 50],
],
],
F: [
[
[60, 2],
[8, 2],
[8, 98],
],
[
[8, 50],
[46, 50],
],
],
G: [
join(arcPts(38, 50, 30, 46, -60, -305), [
[56, 62],
[38, 62],
]),
],
H: [
[
[8, 2],
[8, 98],
],
[
[62, 2],
[62, 98],
],
[
[8, 50],
[62, 50],
],
],
I: [
[
[35, 2],
[35, 98],
],
[
[20, 2],
[50, 2],
],
[
[20, 98],
[50, 98],
],
],
J: [
join(
[
[58, 2],
[58, 72],
],
arcPts(33, 72, 25, 26, 0, 180),
),
],
K: [
[
[8, 2],
[8, 98],
],
[
[60, 2],
[8, 56],
],
[
[24, 42],
[62, 98],
],
],
L: [
[
[8, 2],
[8, 98],
[60, 98],
],
],
M: [
[
[6, 98],
[6, 2],
[35, 58],
[64, 2],
[64, 98],
],
],
N: [
[
[8, 98],
[8, 2],
[62, 98],
[62, 2],
],
],
O: [arcPts(35, 50, 30, 47, -90, 270)],
P: [
[
[8, 98],
[8, 2],
],
join(
[
[8, 2],
[36, 2],
],
arcPts(36, 27, 26, 25, -90, 90),
[
[36, 52],
[8, 52],
],
),
],
Q: [
arcPts(35, 50, 30, 47, -90, 270),
[
[46, 70],
[66, 96],
],
],
R: [
[
[8, 98],
[8, 2],
],
join(
[
[8, 2],
[36, 2],
],
arcPts(36, 27, 26, 25, -90, 90),
[
[36, 52],
[8, 52],
],
),
[
[30, 52],
[64, 98],
],
],
S: [
[
[58, 14],
[44, 3],
[24, 3],
[10, 14],
[10, 30],
[24, 42],
[46, 56],
[58, 68],
[58, 86],
[44, 97],
[22, 97],
[8, 86],
],
],
T: [
[
[6, 2],
[64, 2],
],
[
[35, 2],
[35, 98],
],
],
U: [
join(
[
[8, 2],
[8, 68],
],
arcPts(35, 68, 27, 29, 180, 360),
[
[62, 68],
[62, 2],
],
),
],
V: [
[
[6, 2],
[35, 98],
[64, 2],
],
],
W: [
[
[4, 2],
[19, 98],
[35, 34],
[51, 98],
[66, 2],
],
],
X: [
[
[8, 2],
[62, 98],
],
[
[62, 2],
[8, 98],
],
],
Y: [
[
[6, 2],
[35, 50],
],
[
[64, 2],
[35, 50],
[35, 98],
],
],
Z: [
[
[8, 2],
[62, 2],
[8, 98],
[62, 98],
],
],
0: [arcPts(35, 50, 27, 47, -90, 270)],
1: [
[
[20, 20],
[38, 4],
[38, 98],
],
],
2: [
join(arcPts(35, 26, 26, 24, -160, 10), [
[8, 98],
[62, 98],
]),
],
3: [join(arcPts(34, 26, 24, 24, -150, 80), arcPts(36, 73, 26, 25, -100, 150))],
4: [
[
[50, 2],
[6, 66],
[66, 66],
],
[
[50, 44],
[50, 98],
],
],
5: [
[
[58, 3],
[14, 3],
[11, 44],
[34, 38],
[55, 48],
[59, 72],
[46, 94],
[20, 96],
[8, 84],
],
],
6: [
join(
[
[52, 4],
[28, 26],
[13, 56],
],
arcPts(35, 72, 24, 26, -180, 180),
),
],
7: [
[
[8, 3],
[62, 3],
[30, 98],
],
],
8: [join(arcPts(35, 27, 21, 24, 90, 450), arcPts(35, 74, 26, 25, -90, 270))],
9: [
join(arcPts(37, 28, 25, 25, 0, 360), [
[62, 28],
[56, 64],
[34, 98],
]),
],
};
var GLYPH_W = 70;
var SPACE_W = 40;
var GAP = 24;
// Fixed-seed LCG: the only randomness source, consumed in one
// deterministic order during this build phase.
var lcgState = 0x57174c3d;
function next() {
lcgState = (Math.imul(1664525, lcgState) + 1013904223) >>> 0;
return lcgState / 4294967296;
}
// Resample a polyline to even stitch-scale steps, then displace
// each point perpendicular to the local direction: seeded angle
// jitter, so every stitch lands at a slightly different angle.
function resampleJitter(points) {
var STEP = 5;
var flat = [points[0]];
for (var i = 1; i < points.length; i += 1) {
var ax = flat[flat.length - 1][0];
var ay = flat[flat.length - 1][1];
var bx = points[i][0];
var by = points[i][1];
var seg = Math.hypot(bx - ax, by - ay);
var n = Math.max(1, Math.round(seg / STEP));
for (var k = 1; k <= n; k += 1) {
flat.push([ax + ((bx - ax) * k) / n, ay + ((by - ay) * k) / n]);
}
}
var out = [];
for (var p = 0; p < flat.length; p += 1) {
var prev = flat[Math.max(0, p - 1)];
var after = flat[Math.min(flat.length - 1, p + 1)];
var dx = after[0] - prev[0];
var dy = after[1] - prev[1];
var len = Math.hypot(dx, dy) || 1;
var edge = p === 0 || p === flat.length - 1 ? 0.5 : 1;
var off = (next() - 0.5) * 2 * reg.jitter * edge;
out.push([flat[p][0] + (-dy / len) * off, flat[p][1] + (dx / len) * off]);
}
return out;
}
// ---- Layout: bake letter x-offsets into the points so every
// path shares ONE flat coordinate space (the global needle can
// then read getPointAtLength coordinates directly). -----------
var characters = Array.from(text);
var letters = [];
var cursor = 0;
for (var ci = 0; ci < characters.length; ci += 1) {
var ch = characters[ci];
var glyph = GLYPHS[ch];
if (!glyph) {
cursor += SPACE_W + GAP;
continue;
}
var strokes = [];
for (var si = 0; si < glyph.length; si += 1) {
var jittered = resampleJitter(glyph[si]);
var shifted = [];
for (var pi = 0; pi < jittered.length; pi += 1) {
shifted.push([jittered[pi][0] + cursor, jittered[pi][1]]);
}
strokes.push({ points: shifted });
}
letters.push({ strokes: strokes });
cursor += GLYPH_W + GAP;
}
var totalWidth = Math.max(GLYPH_W, cursor - GAP);
var PAD = 14;
svg.setAttribute(
"viewBox",
-PAD + " " + -PAD + " " + (totalWidth + 2 * PAD) + " " + (100 + 2 * PAD),
);
// ---- Build DOM + per-stroke dash/hole tables ----------------
function el(name) {
return document.createElementNS(SVG_NS, name);
}
var holeFill = "color-mix(in srgb, var(--fg, #f8fafc) 34%, transparent)";
var totalLength = 0;
for (var li = 0; li < letters.length; li += 1) {
var letter = letters[li];
for (var sj = 0; sj < letter.strokes.length; sj += 1) {
var stroke = letter.strokes[sj];
var d =
"M " +
stroke.points
.map(function (pt) {
return pt[0].toFixed(2) + " " + pt[1].toFixed(2);
})
.join(" L ");
var path = el("path");
path.setAttribute("d", d);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "var(--std-accent)");
path.setAttribute("stroke-width", String(reg.width));
path.setAttribute("stroke-linecap", "round");
svg.appendChild(path);
var L = path.getTotalLength();
stroke.el = path;
stroke.length = L;
totalLength += L;
// Double-dash reveal pattern: alternating dash/gap summing
// EXACTLY to L (ending on a dash element so the appended L
// sits at an odd index and reads as one closing gap).
var pattern = [];
var sum = 0;
while (sum + reg.dash + reg.gap < L) {
pattern.push(reg.dash, reg.gap);
sum += reg.dash + reg.gap;
}
pattern.push(Math.max(0.5, L - sum));
pattern.push(L);
stroke.dasharray = pattern
.map(function (v) {
return v.toFixed(3);
})
.join(" ");
path.setAttribute("stroke-dasharray", stroke.dasharray);
path.setAttribute("stroke-dashoffset", L.toFixed(3));
// Needle holes: one dot at each stitch boundary (dash start
// and dash end), revealed as the front passes.
stroke.holes = [];
var period = reg.dash + reg.gap;
for (var s = 0; s <= L; s += period) {
var boundaries = [s, Math.min(L, s + reg.dash)];
for (var b = 0; b < boundaries.length; b += 1) {
var pt = path.getPointAtLength(boundaries[b]);
var hole = el("circle");
hole.setAttribute("cx", pt.x.toFixed(2));
hole.setAttribute("cy", pt.y.toFixed(2));
hole.setAttribute("r", (reg.width * 0.42).toFixed(2));
hole.setAttribute("fill", holeFill);
hole.setAttribute("opacity", "0");
svg.appendChild(hole);
stroke.holes.push({ el: hole, at: boundaries[b] });
}
}
// End pose for the thread-tail overshoot.
var endPt = path.getPointAtLength(L);
var backPt = path.getPointAtLength(Math.max(0, L - 2));
var tx = endPt.x - backPt.x;
var ty = endPt.y - backPt.y;
var tLen = Math.hypot(tx, ty) || 1;
stroke.end = [endPt.x, endPt.y];
stroke.tangent = [tx / tLen, ty / tLen];
}
}
// Global needle dot + thread tail (letters draw strictly in
// sequence, so one needle serves the whole word).
var tail = el("line");
tail.setAttribute("stroke", "var(--std-accent)");
tail.setAttribute("stroke-width", (reg.width * 0.8).toFixed(2));
tail.setAttribute("stroke-linecap", "round");
tail.setAttribute("opacity", "0");
svg.appendChild(tail);
var needle = el("circle");
needle.setAttribute("r", (reg.width * 1.15).toFixed(2));
needle.setAttribute("fill", "var(--fg, #f8fafc)");
needle.setAttribute("stroke", "color-mix(in srgb, var(--std-accent) 60%, transparent)");
needle.setAttribute("stroke-width", (reg.width * 0.45).toFixed(2));
needle.setAttribute("opacity", "0");
svg.appendChild(needle);
// Envelope: fixed IN/OUT, elastic HOLD, never time-scaled.
var LEAD = 0.15;
var DRAW_BASE = 2.9;
var MARGIN = 0.15;
var PAUSE_BASE = 0.05;
var IN_BASE = LEAD + DRAW_BASE + MARGIN;
var OUT_BASE = exit === "none" ? 0 : 0.45;
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
var totalBase = Math.max(0.001, IN_BASE + OUT_BASE);
var scale = duration < totalBase ? duration / totalBase : 1;
var lead = LEAD * scale;
var DRAW = DRAW_BASE * scale;
var pause = PAUSE_BASE * scale;
var IN = IN_BASE * scale;
var OUT = OUT_BASE * scale;
var HOLD = Math.max(0, duration - (IN + OUT));
var OUT_START = IN + HOLD;
// Schedule: letters sequential, each letter's window sized by
// its share of the total stitch length; strokes sequential
// within the letter by the same rule.
var pauses = Math.max(0, letters.length - 1) * pause;
var drawable = Math.max(0.05, DRAW - pauses);
var clock = lead;
for (var lj = 0; lj < letters.length; lj += 1) {
var lt = letters[lj];
var letterLen = 0;
for (var sk = 0; sk < lt.strokes.length; sk += 1) {
letterLen += lt.strokes[sk].length;
}
var letterTime = totalLength > 0 ? (drawable * letterLen) / totalLength : 0;
for (var sm = 0; sm < lt.strokes.length; sm += 1) {
var st = lt.strokes[sm];
var strokeTime = letterLen > 0 ? (letterTime * st.length) / letterLen : 0;
st.t0 = clock;
st.t1 = clock + Math.max(0.01, strokeTime);
clock = st.t1;
}
lt.t0 = lt.strokes[0].t0;
lt.t1 = clock;
clock += pause;
}
function clamp01(v) {
return v < 0 ? 0 : v > 1 ? 1 : v;
}
// backOut: overshoots ~10% past 1, then settles to exactly 1.
// The overshoot region drives the thread-tail.
function backOut(u) {
var c1 = 1.70158;
var c3 = c1 + 1;
var x = u - 1;
return 1 + c3 * x * x * x + c1 * x * x;
}
var TAIL_MAX = 16;
// Pure per-update painter (zero per-element tweens): dashoffset
// ATTRIBUTE, hole visibility, and the needle/tail pose are all
// functions of tl.time() alone. Idempotent writes.
function applyTime(timeSeconds) {
var needleOn = false;
var needleX = 0;
var needleY = 0;
var tailFrom = null;
var tailTo = null;
for (var i = 0; i < letters.length; i += 1) {
var letter = letters[i];
for (var j = 0; j < letter.strokes.length; j += 1) {
var stroke = letter.strokes[j];
var span = stroke.t1 - stroke.t0;
var u = clamp01(span > 0 ? (timeSeconds - stroke.t0) / span : 1);
var L = stroke.length;
var visible;
// Round linecaps paint a zero-length-dash dot at the exact
// boundary sitting at the path start when dashoffset = L,
// so undrawn strokes are also opacity-gated.
stroke.el.setAttribute("opacity", u <= 0 ? "0" : "1");
if (u <= 0) {
stroke.el.setAttribute("stroke-dashoffset", L.toFixed(3));
visible = 0;
} else if (u >= 1) {
stroke.el.setAttribute("stroke-dashoffset", "0");
visible = L;
} else {
var p = backOut(u);
var front = clamp01(p) * L;
stroke.el.setAttribute("stroke-dashoffset", (L - front).toFixed(3));
visible = front;
needleOn = true;
if (p <= 1) {
var pt = stroke.el.getPointAtLength(front);
needleX = pt.x;
needleY = pt.y;
} else {
// Thread-tail overshoot: the needle carries the
// thread past the stroke end along its exit tangent,
// then the settle pulls it snug.
var over = Math.min((p - 1) * L, TAIL_MAX);
needleX = stroke.end[0] + stroke.tangent[0] * over;
needleY = stroke.end[1] + stroke.tangent[1] * over;
tailFrom = stroke.end;
tailTo = [needleX, needleY];
}
}
for (var h = 0; h < stroke.holes.length; h += 1) {
stroke.holes[h].el.setAttribute(
"opacity",
stroke.holes[h].at <= visible && u > 0 ? "1" : "0",
);
}
}
}
if (needleOn) {
needle.setAttribute("opacity", "1");
needle.setAttribute("cx", needleX.toFixed(2));
needle.setAttribute("cy", needleY.toFixed(2));
} else {
needle.setAttribute("opacity", "0");
}
if (tailFrom) {
tail.setAttribute("opacity", "0.9");
tail.setAttribute("x1", tailFrom[0].toFixed(2));
tail.setAttribute("y1", tailFrom[1].toFixed(2));
tail.setAttribute("x2", tailTo[0].toFixed(2));
tail.setAttribute("y2", tailTo[1].toFixed(2));
} else {
tail.setAttribute("opacity", "0");
}
}
gsap.set(stage, { opacity: 1, y: "0cqh" });
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
applyTime(tl.time());
},
});
// Anchor tween: an inert plain-object tween spanning [0, D] so
// onUpdate fires for every eventful seek and holds never clamp.
tl.to({ p: 0 }, { p: 1, duration: duration, ease: "none" }, 0);
// HOLD: truly still; repeated repaints write identical values.
// OUT: optional departure; exit none holds until the frame cuts.
if (exit === "up") {
tl.to(stage, { y: "-4cqh", duration: OUT, ease: "power2.in" }, OUT_START);
tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
} else if (exit === "fade") {
tl.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
}
tl.seek(0);
applyTime(0);
window.__timelines = window.__timelines || {};
window.__timelines["stitched-text-draw"] = tl;
})();
</script>
</div>
</template>
</body>
</html>
motion-primitive experiment type craft stitch deterministic.