mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -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.
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user