Setting the file. One moment. Seam Stamp · Motion Doctrine · heygen-com/hyperframes · Skills Docsscripts/seam-stamp.mjs
scripts/seam-stamp.mjs
JavaScript·143 lines·6 KB
15// blur — Z-seam blur px (default 18 full-frame; use 10 for text-scale)
16
17import { readFileSync, writeFileSync } from "node:fs";
18
19const argv = process.argv.slice(2);
20const flag = (n, d) => {
21 const i = argv.indexOf("--" + n);
22 return i >= 0 ? argv[i + 1] : d;
23};
24const ledger = JSON.parse(readFileSync(flag("ledger", "ledger.json"), "utf8"));
25
26const round = (n) => +n.toFixed(3);
27const lines = [];
28const emit = (s) => lines.push(" " + s);
29
30// ---------- scene inventory (order of appearance) + base states ----------
31const scenes = [];
32const zEntries = new Map(); // selector -> {scale, blur} preset for Z arrivals
33for (const seam of ledger.seams) {
34 for (const sel of [seam.exit?.selector, seam.entry?.selector]) {
35 if (sel && !scenes.includes(sel)) scenes.push(sel);
36 }
37 if (seam.entry?.axis === "z") {
38 const blur = seam.blur ?? 18;
39 zEntries.set(
40 seam.entry.selector,
41 seam.entry.dir === -1
42 ? { scale: 1.25, blur } // pull: arrives oversized
43 : { scale: 0.78, blur },
44 ); // push: arrives small, growing
45 }
46}
47
48emit(`// <seams:auto> — generated by seam-stamp.mjs from ledger.json; do not hand-edit.`);
49emit(
50 `// Regenerate: node <motion-doctrine>/scripts/seam-stamp.mjs --ledger ledger.json --write index.html`,
51);
52if (scenes.length) {
53 const first = scenes[0];
54 const rest = scenes.slice(1).filter((s) => !zEntries.has(s));
55 emit(
56 `gsap.set("${first}", { autoAlpha: 1, xPercent: 0, yPercent: 0, scale: 1, filter: "blur(0px)", transformOrigin: "50% 50%" });`,
57 );
58 if (rest.length)
59 emit(
60 `gsap.set([${rest.map((s) => `"${s}"`).join(",")}], { autoAlpha: 0, xPercent: 0, yPercent: 0, scale: 1, filter: "blur(0px)", transformOrigin: "50% 50%" });`,
61 );
62 for (const [sel, p] of zEntries)
63 emit(
64 `gsap.set("${sel}", { autoAlpha: 0, scale: ${p.scale}, filter: "blur(${p.blur}px)", xPercent: 0, yPercent: 0, transformOrigin: "50% 50%" });`,
65 );
66}
67emit(``);
68
69// ---------- per-seam stamping ----------
70for (const seam of ledger.seams) {
71 const cut = seam.cut,
72 type = seam.type || "cut";
73 emit(`// SEAM — ${seam.id} : ${seam.technique || type} (cut @${cut})`);
74
75 if (type !== "cut") {
76 if (seam.exit?.selector) emit(`tl.set("${seam.exit.selector}", { autoAlpha: 0 }, ${cut});`);
77 if (seam.entry?.selector) emit(`tl.set("${seam.entry.selector}", { autoAlpha: 1 }, ${cut});`);
78 emit(
79 `// ${type}: carrier handoff is Tier-A — author it by hand and keep the carrier row in ledger.json`,
80 );
81 emit(``);
82 continue;
83 }
84
85 const ex = seam.exit,
86 en = seam.entry;
87 if (ex.axis !== en.axis || ex.dir !== en.dir)
88 throw new Error(
89 `ledger row "${seam.id}" mismatched (${ex.axis}${ex.dir} vs ${en.axis}${en.dir}) — fix the PLAN, not the stamp`,
90 );
91
92 if (ex.axis === "z") {
93 const blur = seam.blur ?? 18;
94 const exDur = ex.dur ?? 0.21,
95 enDur = en.dur ?? 0.5;
96 const exScale = ex.dir === -1 ? 0.8 : 1.18;
97 const enFrom = ex.dir === -1 ? 1.25 : 0.78;
98 emit(
99 `tl.to("${ex.selector}", { scale: ${exScale}, filter: "blur(${blur}px)", duration: ${exDur}, ease: "power3.in" }, ${round(cut - exDur)});`,
100 );
101 emit(
102 `tl.to("${ex.selector}", { autoAlpha: 0, duration: ${exDur}, ease: "none" }, ${round(cut - exDur)});`,
103 );
104 emit(`tl.set("${ex.selector}", { autoAlpha: 0 }, ${cut});`);
105 emit(
106 `tl.fromTo("${en.selector}", { autoAlpha: 0.15, scale: ${enFrom}, filter: "blur(${blur}px)" }, { autoAlpha: 1, scale: 1.0, filter: "blur(0px)", duration: ${enDur}, ease: "expo.out", immediateRender: false }, ${cut});`,
107 );
108 } else {
109 const prop = ex.axis === "x" ? "xPercent" : "yPercent";
110 const exDur = ex.dur ?? 0.34,
111 enDur = en.dur ?? 0.42;
112 const travel = en.travel ?? 10;
113 emit(
114 `tl.to("${ex.selector}", { ${prop}: ${12 * ex.dir}, autoAlpha: 0, duration: ${exDur}, ease: "power3.in" }, ${round(cut - exDur)});`,
115 );
116 emit(`tl.set("${ex.selector}", { autoAlpha: 0 }, ${cut});`);
117 emit(
118 `tl.fromTo("${en.selector}", { ${prop}: ${-travel * en.dir}, autoAlpha: 0.35 }, { ${prop}: 0, autoAlpha: 1, duration: ${enDur}, ease: "power4.out", immediateRender: false }, ${cut});`,
119 );
120 }
121 emit(``);
122}
123emit(`// </seams:auto>`);
124
125const block = lines.join("\n");
126const target = flag("write", null);
127if (!target) {
128 console.log(block);
129} else {
130 let html = readFileSync(target, "utf8");
131 const re = /[ \t]*\/\/ <seams:auto>[\s\S]*?\/\/ <\/seams:auto>/;
132 if (re.test(html)) {
133 html = html.replace(re, block);
134 } else {
135 // insert after the master timeline registration line
136 const anchor = /(window\.__timelines\["main"\]\s*=\s*tl;\s*\n)/;
137 if (!anchor.test(html))
138 throw new Error('no <seams:auto> markers and no window.__timelines["main"] anchor found');
139 html = html.replace(anchor, `$1\n${block}\n`);
140 }
141 writeFileSync(target, html);
142 console.log(`stamped ${ledger.seams.length} seams into ${target}`);
143}