Revert "feat(registry): the video-primitive moves, documented and customisable (#3090)" (#3162)

This reverts commit 3b53bfd2f7.
This commit is contained in:
Miguel Ángel
2026-08-10 14:47:47 -04:00
committed by GitHub
parent 3b53bfd2f7
commit c86d4013f5
2328 changed files with 5906 additions and 458525 deletions
@@ -1,45 +0,0 @@
# stitched-text-draw
Wave M experiment (M6). Text drawn as thread stitches: letters draw thread-first with a needle dot leading each stroke's reveal front, needle-hole dots appearing at every stitch boundary behind it, and a tiny thread-tail overshoot as each letter finishes (the thread sticks out past the stroke end along its exit tangent, then is pulled snug).
4s authored, elastic HOLD, exit `none` by default.
## Letter path approach
A single-stroke display alphabet (A-Z 0-9, plus space) authored as a compact stroke table inside the primitive: each glyph is a list of strokes, each stroke built from line points and flattened elliptical arcs in a 70x100 glyph box. At mount every stroke is resampled to even stitch-scale steps and displaced by seeded perpendicular jitter (fixed LCG seed `0x57174c3d`), so every stitch sits at a slightly different angle: the hand-sewn wobble. Letter x-offsets are baked into the points so all paths share one flat SVG coordinate space and a single global needle can read `getPointAtLength` coordinates directly.
## Draw mechanic (attribute dashoffset, no mask)
The reveal is ATTRIBUTE-driven `stroke-dashoffset` on the stitched path itself (gotcha 8: the CSS property leaves stale paint under reverse seeks; and SVG masks do not reliably repaint under seeks, so no `<mask>`/`<clipPath>`). Each path's dasharray is its stitch pattern (dash, gap, dash, ...) built to sum exactly to the measured path length `L` (via `getTotalLength`, never the `pathLength` attribute, never `non-scaling-stroke` on dashed paths), followed by one closing gap of `L` (total `2L`). Animating the dashoffset attribute from `L` to `0` sweeps the reveal front from start to end; the visible stitches slide the last few units into their final positions as the offset reaches zero, which reads as the thread being pulled through.
## Variables
| id | type | default | notes |
| -------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `text` | string | `STITCH` | Uppercased; A-Z 0-9 and space, other characters render as spaces; clamped to 12 characters. |
| `stitch` | enum | `fine` | `fine` (dash 6.5 / gap 4.5 / width 2.6) or `coarse` (dash 10 / gap 7 / width 4.2) in glyph units, with matching jitter amplitude. |
| `accent` | enum | `green` | Thread color: green rides `--brand`, blue rides `--accent`, violet rides `--accent-2`. |
| `exit` | enum | `none` | `none` holds until the cut; `fade` departs opacity-only; `up` rises out. |
## Mount
```html
<div
class="clip"
data-composition-id="stitched-text-draw"
data-composition-src="./stitched-text-draw.html"
data-variable-values='{"text":"SEWN 24","stitch":"coarse","accent":"violet"}'
data-start="0"
data-duration="4"
data-track-index="0"
></div>
```
Elastic root: no `data-width`/`data-height`; it fills whatever box the host clip gives it. Timeline registers under the literal `stitched-text-draw` key.
## Notes
- Zero per-element tweens (gotcha 9): one inert anchor tween spans the authored duration; a painter recomputes every stroke's dashoffset attribute, every hole's visibility, and the needle/tail pose as pure functions of `tl.time()` on each update. Idempotent writes; eventful seeks (`suppressEvents=false`) land identical frames in any order and either direction.
- Letters draw strictly in sequence (windows proportional to stitch length, fixed needle-jump pause between letters), so one global needle serves the whole word.
- The per-stroke ease is a backOut: the ~10% overshoot region past the stroke end is what drives the thread-tail (up to 16 glyph units), which retracts to zero as the ease settles.
- The hold is truly still: every stroke sits at dashoffset 0, all holes visible, needle and tail at opacity 0.
-85
View File
@@ -1,85 +0,0 @@
<!doctype html>
<!--
stitched-text-draw demo. FULL-BLEED light mount: the primitive fills the
whole 1920x1080 frame on a light token block. Non-default text, stitch,
and accent values prove the variable path.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Stitched Text Draw Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--bg, #eef0f3);
}
#root {
--bg: #eef0f3;
--fg: #14161b;
--muted: #5b6472;
--surface: #ffffff;
--border: #d3d8e0;
--brand: #3f3f46;
--accent: #3f3f46;
--accent-2: #3f3f46;
--font-display: "Inter", system-ui, sans-serif;
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: radial-gradient(ellipse at 50% 42%, #f7f8fa 0%, var(--bg, #eef0f3) 72%);
}
.mount-bleed {
width: 100%;
height: 100%;
left: 0;
top: 0;
container-type: size;
overflow: hidden;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="stitched-text-draw-demo"
data-start="0"
data-width="1920"
data-height="1080"
data-duration="4"
data-fps="30"
>
<div
id="stitched-text-draw-mount"
class="mount-bleed clip"
data-composition-id="stitched-text-draw"
data-composition-src="./stitched-text-draw.html"
data-variable-values='{"text":"SEWN 24","stitch":"coarse","accent":"violet"}'
data-start="0"
data-duration="4"
data-track-index="0"
data-width="1920"
data-height="1080"
></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["stitched-text-draw-demo"] = gsap.timeline({ paused: true });
</script>
</body>
</html>
@@ -1,68 +0,0 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "stitched-text-draw",
"type": "hyperframes:component",
"title": "Stitched Text Draw",
"description": "Text drawn as thread stitches: a single-stroke display alphabet draws via attribute-driven dashoffset with stitch-tuned dash patterns, seeded angle jitter, needle-hole dots, a leading needle, and a thread-tail overshoot per letter.",
"tags": ["motion-primitive", "experiment", "type", "craft", "stitch", "deterministic"],
"jobs": ["reveal"],
"family": "text-effects",
"profile": "thread-stitch",
"evidence": "Wave M experiment M6: wild-scout stitched-line text draw (score 16), thread-stitch wordmark reveal for craft/authenticity beats",
"syncPoints": [{ "id": "stitch-done", "phase": "in", "offset": 3.05 }],
"files": [
{
"path": "stitched-text-draw.html",
"target": "compositions/components/stitched-text-draw.html",
"type": "hyperframes:snippet"
}
],
"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" }
]
}
]
}
@@ -1,865 +0,0 @@
<!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>