tegaki 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/FONTS-LICENSE.md +6 -0
  3. package/README.md +22 -4
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.mjs +233 -0
  6. package/dist/cli.mjs.map +1 -0
  7. package/dist/core/index.d.mts +3 -3
  8. package/dist/core/index.mjs +3 -2
  9. package/dist/{core-MwnsJAbN.mjs → core-VIBhSqFM.mjs} +88 -650
  10. package/dist/core-VIBhSqFM.mjs.map +1 -0
  11. package/dist/fonts/amiri/bundle.mjs.map +1 -1
  12. package/dist/fonts/caveat/bundle.mjs.map +1 -1
  13. package/dist/fonts/italianno/bundle.mjs.map +1 -1
  14. package/dist/fonts/klee-one/bundle.mjs.map +1 -1
  15. package/dist/fonts/nanum-pen-script/bundle.d.mts +7144 -0
  16. package/dist/fonts/nanum-pen-script/bundle.mjs +161041 -0
  17. package/dist/fonts/nanum-pen-script/bundle.mjs.map +1 -0
  18. package/dist/fonts/nanum-pen-script/nanum-pen-script-38efadb5.ttf +0 -0
  19. package/dist/fonts/nanum-pen-script/nanum-pen-script.ttf +0 -0
  20. package/dist/fonts/parisienne/bundle.mjs.map +1 -1
  21. package/dist/fonts/suez-one/bundle.mjs.map +1 -1
  22. package/dist/fonts/tangerine/bundle.mjs.map +1 -1
  23. package/dist/fonts/tillana/bundle.mjs.map +1 -1
  24. package/dist/{index-B2rqZbJt.d.mts → index-BmhPyEv9.d.mts} +4 -5
  25. package/dist/{index-BVYJFRMq.d.mts → index-D-lHv505.d.mts} +71 -3
  26. package/dist/index.d.mts +4 -4
  27. package/dist/index.mjs +4 -3
  28. package/dist/react/index.d.mts +4 -4
  29. package/dist/react/index.mjs +4 -3
  30. package/dist/{react-174En_ya.mjs → react-wE5kRPvk.mjs} +4 -2
  31. package/dist/{react-174En_ya.mjs.map → react-wE5kRPvk.mjs.map} +1 -1
  32. package/dist/shaper-harfbuzz/index.d.mts +1 -1
  33. package/dist/shaper-harfbuzz/index.mjs +1 -1
  34. package/dist/shaper-harfbuzz/index.mjs.map +1 -1
  35. package/dist/{shaper-registry-CrfNiB1j.d.mts → shaper-registry-DmlJGnE3.d.mts} +1 -1
  36. package/dist/textToSvg-GTQMM8Le.mjs +906 -0
  37. package/dist/textToSvg-GTQMM8Le.mjs.map +1 -0
  38. package/dist/wc/index.d.mts +3 -3
  39. package/dist/wc/index.mjs +3 -2
  40. package/dist/wc/index.mjs.map +1 -1
  41. package/fonts/nanum-pen-script/bundle.ts +20 -0
  42. package/fonts/nanum-pen-script/glyphData.json +1 -0
  43. package/fonts/nanum-pen-script/nanum-pen-script-38efadb5.ttf +0 -0
  44. package/fonts/nanum-pen-script/nanum-pen-script.ttf +0 -0
  45. package/package.json +31 -20
  46. package/src/cli.ts +293 -0
  47. package/src/core/engine.ts +95 -0
  48. package/src/core/index.ts +1 -0
  49. package/src/lib/svgExport.ts +290 -0
  50. package/src/lib/textToSvg.test.ts +85 -0
  51. package/src/lib/textToSvg.ts +162 -0
  52. package/dist/core-MwnsJAbN.mjs.map +0 -1
  53. package/dist/solid/index.d.mts +0 -20
  54. package/dist/solid/index.mjs +0 -91
  55. package/dist/solid/index.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
+ ## tegaki@0.20.0
2
+
3
+ ### Add `TegakiEngine.toSVG()` and a `canvas` accessor
4
+
5
+ The engine can now serialize its current text to a standalone SVG string via `toSVG({ animated, loop })` — either self-drawing (SMIL mask reveal, variable stroke width) or looping (CSS keyframes, constant width), with the viewBox cropped to the ink. The backing `<canvas>` is also exposed through a `canvas` getter so export tooling can read pixels without reaching through the DOM.
6
+
7
+ ### Add a `tegaki` CLI and a `textToSvg()` export
8
+
9
+ `npx tegaki` renders text to an animated handwriting SVG straight from the command line. The same output is available programmatically through the new `textToSvg()` export, along with its `TextToSvgMode` and `TextToSvgOptions` types.
10
+
1
11
  # tegaki
2
12
 
13
+ ## 0.19.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 68ee58f: Support Korean (Hangul) writing system and add Nanum Pen Script as a built-in font. The bundle ships ~650 most-frequent precomposed Hangul syllables (KS X 1001 common band ∪ top-N Korean-Wikipedia-frequency, capped by the Google Fonts subsetting ceiling) plus the 40 modern compatibility jamo (19 consonants ㄱ–ㅎ + 21 vowels ㅏ–ㅣ) and the Latin baseline. Hangul is precomposed in Unicode, so no shaper is required.
18
+
19
+ ### Patch Changes
20
+
21
+ - 29e111e: Fix Next.js App Router / Turbopack compatibility. Two build/packaging issues prevented `tegaki` from being used in a Next.js (App Router) app:
22
+
23
+ - The bundler dropped the `'use client'` directive from the React adapter's output chunk, so importing `TegakiRenderer` into a Server Component failed with "You're importing a module that depends on `useState` into a React Server Component". The directive is now preserved in the built output.
24
+ - The font subpath exports (`tegaki/fonts/*`) resolved the `node` condition to the raw TypeScript source, which uses `import ... with { type: 'url' }` import attributes that Turbopack/webpack can't process. The `node` condition now points to the pre-built `.mjs` bundle (which loads the font via `new URL('./font.ttf', import.meta.url)`), so it works in any Node-based bundler.
25
+
3
26
  ## 0.18.0
4
27
 
5
28
  ### Minor Changes
package/FONTS-LICENSE.md CHANGED
@@ -51,6 +51,12 @@ All fonts are licensed under the [SIL Open Font License, Version 1.1](https://op
51
51
  - **Copyright**: Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com)
52
52
  - **License**: SIL Open Font License, Version 1.1
53
53
 
54
+ ## Nanum Pen Script
55
+
56
+ - **Designer**: Sandoll Communications Inc. for NHN Corporation
57
+ - **Copyright**: Copyright © 2010 NHN Corporation. All rights reserved. Font designed by Sandoll Communications Inc.
58
+ - **License**: SIL Open Font License, Version 1.1
59
+
54
60
  ---
55
61
 
56
62
  ## SIL Open Font License, Version 1.1
package/README.md CHANGED
@@ -6,12 +6,12 @@ Tegaki (手書き) turns any font into animated handwriting.
6
6
  No manual path authoring. No native dependencies. Just pick a font.
7
7
 
8
8
  [![npm](https://img.shields.io/npm/v/tegaki)](https://www.npmjs.com/package/tegaki)
9
- [![license](https://img.shields.io/npm/l/tegaki)](https://github.com/KurtGokhan/tegaki/blob/main/LICENSE)
9
+ [![license](https://img.shields.io/npm/l/tegaki)](https://github.com/gkurt/tegaki/blob/main/LICENSE)
10
10
 
11
11
  <br clear="both" />
12
12
 
13
13
  <p align="center">
14
- <img src="media/hello-world.svg" alt="Hello World handwriting animation" width="500" />
14
+ <img src="media/hello-world.svg" alt="Tegaki is awesome handwriting animation" width="560" />
15
15
  </p>
16
16
 
17
17
  ---
@@ -41,6 +41,23 @@ function App() {
41
41
 
42
42
  That's it. The text draws itself stroke by stroke with natural timing.
43
43
 
44
+ ## Command Line
45
+
46
+ Don't want to wire up a component? Generate an animated handwriting SVG straight from your terminal — nothing to install:
47
+
48
+ ```bash
49
+ npx tegaki "Tegaki is awesome"
50
+ ```
51
+
52
+ This writes a self-drawing, looping `tegaki-is-awesome.svg` — drop it into a README, a slide, or any page. Pick a font, mode, size, or color:
53
+
54
+ ```bash
55
+ npx tegaki "Hello World" --font tangerine --mode once -o hello.svg
56
+ npx tegaki "ABC" --stagger 80% --size 140 --color "#222"
57
+ ```
58
+
59
+ `--mode` is `loop` (repeats forever, the default), `once` (draws itself a single time), or `static` (finished artwork). Run `npx tegaki --help` for every option and `--list-fonts` for the bundled fonts. The CLI emits SVG only — for PNG, GIF, or WebM use the [interactive studio](https://gkurt.com/tegaki/studio/).
60
+
44
61
  ## Framework Support
45
62
 
46
63
  Tegaki works with all major frameworks:
@@ -75,8 +92,9 @@ Several handwriting fonts are bundled and ready to use:
75
92
  - **Amiri** — `tegaki/fonts/amiri` _(Arabic + Latin)_
76
93
  - **Tillana** — `tegaki/fonts/tillana` _(Devanagari + Latin)_
77
94
  - **Klee One** — `tegaki/fonts/klee-one` _(Japanese: kana + Kyōiku grade 1–2 kanji + Latin)_
95
+ - **Nanum Pen Script** — `tegaki/fonts/nanum-pen-script` _(Korean: Hangul syllables + jamo + Latin)_
78
96
 
79
- For other fonts, use the [interactive generator](https://gkurt.com/tegaki/generator/) to create a custom bundle.
97
+ For other fonts, use the [interactive studio](https://gkurt.com/tegaki/studio/) to create a custom bundle.
80
98
 
81
99
  ## Documentation
82
100
 
@@ -90,7 +108,7 @@ Visit **[gkurt.com/tegaki](https://gkurt.com/tegaki)** for full documentation:
90
108
  ## Integrations
91
109
 
92
110
  - [Sli.dev](https://sli.dev/) - Create presentations using markdown. [See Tegaki integration example](https://andreas-taranetz.github.io/slidev-addon-animated-text/)
93
- - [Remotion](https://www.remotion.dev/) - Create videos programmatically with React. [See Tegaki integration example](https://github.com/KurtGokhan/tegaki/blob/gokhan/support-font-features/examples/remotion/src/Root.tsx)
111
+ - [Remotion](https://www.remotion.dev/) - Create videos programmatically with React. [See Tegaki integration example](https://github.com/gkurt/tegaki/blob/gokhan/support-font-features/examples/remotion/src/Root.tsx)
94
112
 
95
113
  ## License
96
114
 
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import { t as textToSvg } from "./textToSvg-GTQMM8Le.mjs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ //#region src/cli.ts
5
+ /**
6
+ * `tegaki` — turn text into an animated handwriting SVG from the command line.
7
+ *
8
+ * npx tegaki "Tegaki is awesome" # → tegaki-is-awesome.svg (looping)
9
+ * npx tegaki "Hello" -f tangerine -o hi.svg
10
+ *
11
+ * SVG is the only format emitted: it's the one the renderer can produce with
12
+ * zero native dependencies (no canvas, no headless browser). Raster/video
13
+ * export (PNG/GIF/WebM) lives in the browser studio at gkurt.com/tegaki/studio.
14
+ */
15
+ /** Bundled fonts the CLI can load, keyed by `--font` name. */
16
+ const FONTS = {
17
+ caveat: "Caveat (Latin)",
18
+ italianno: "Italianno (Latin)",
19
+ tangerine: "Tangerine (Latin)",
20
+ parisienne: "Parisienne (Latin)",
21
+ "suez-one": "Suez One (Hebrew + Latin)",
22
+ amiri: "Amiri (Arabic + Latin)",
23
+ tillana: "Tillana (Devanagari + Latin)",
24
+ "klee-one": "Klee One (Japanese + Latin)",
25
+ "nanum-pen-script": "Nanum Pen Script (Korean + Latin)"
26
+ };
27
+ /** Fonts whose scripts need shaping the headless CLI does not apply (RTL / complex GPOS). */
28
+ const NEEDS_SHAPING = /* @__PURE__ */ new Set([
29
+ "suez-one",
30
+ "amiri",
31
+ "tillana"
32
+ ]);
33
+ const MODES = [
34
+ "loop",
35
+ "once",
36
+ "static"
37
+ ];
38
+ const HELP = `tegaki — animated handwriting SVG generator
39
+
40
+ Usage:
41
+ tegaki <text...> [options]
42
+
43
+ Options:
44
+ -o, --output <file> Output path (default: <slug>.svg; "-" writes to stdout)
45
+ -f, --font <name> Bundled font (default: caveat). See --list-fonts.
46
+ -m, --mode <mode> loop | once | static (default: loop)
47
+ loop self-drawing, repeats forever (README hero)
48
+ once draws itself once, then stays complete
49
+ static finished artwork, no animation
50
+ --size <px> Font size in px (default: 100)
51
+ --color <css> Stroke color (default: #1a1a1a)
52
+ --line-height <px> Line height in px (default: from font metrics)
53
+ --stagger <advance> Overlap glyphs instead of drawing them in sequence.
54
+ e.g. "80%" (of the previous glyph) or "0.3" (seconds)
55
+ --stagger-duration <s|auto> Per-glyph duration when staggering (default: auto)
56
+ --pressure <0-1> Variable stroke width for once/static (default: 1)
57
+ --smoothing Smooth strokes onto a spline
58
+ --segment-size <px> Stroke subdivision threshold (default: 2 when applicable)
59
+ -h, --help Show this help
60
+ -v, --version Show version
61
+ --list-fonts List bundled fonts
62
+
63
+ Examples:
64
+ tegaki "Tegaki is awesome"
65
+ tegaki "Hello World" --font tangerine --mode once -o hello.svg
66
+ tegaki "ABC" --stagger 80% --size 140 --color "#222"
67
+ `;
68
+ function fail(message) {
69
+ process.stderr.write(`tegaki: ${message}\n`);
70
+ process.exit(1);
71
+ }
72
+ /** Slugify text into a safe base filename. */
73
+ function slug(text) {
74
+ return text.trim().slice(0, 40).replace(/\s+/g, "-").replace(/[^\w-]/g, "").toLowerCase() || "tegaki";
75
+ }
76
+ function parseArgs(argv) {
77
+ const words = [];
78
+ const opts = {
79
+ text: "",
80
+ output: null,
81
+ font: "caveat",
82
+ fontSize: 100,
83
+ color: "#1a1a1a",
84
+ mode: "loop",
85
+ smoothing: false
86
+ };
87
+ const expectValue = (flag, inline, i) => {
88
+ if (inline !== void 0) return inline;
89
+ const next = argv[++i.v];
90
+ if (next === void 0) fail(`option ${flag} requires a value`);
91
+ return next;
92
+ };
93
+ const idx = { v: 0 };
94
+ for (idx.v = 0; idx.v < argv.length; idx.v++) {
95
+ const arg = argv[idx.v];
96
+ if (!arg.startsWith("-") || arg === "-") {
97
+ words.push(arg);
98
+ continue;
99
+ }
100
+ const eq = arg.indexOf("=");
101
+ const flag = eq >= 0 ? arg.slice(0, eq) : arg;
102
+ const inline = eq >= 0 ? arg.slice(eq + 1) : void 0;
103
+ switch (flag) {
104
+ case "-h":
105
+ case "--help":
106
+ process.stdout.write(HELP);
107
+ process.exit(0);
108
+ break;
109
+ case "-v":
110
+ case "--version":
111
+ process.stdout.write(`${cachedVersion}\n`);
112
+ process.exit(0);
113
+ break;
114
+ case "--list-fonts":
115
+ listFonts();
116
+ process.exit(0);
117
+ break;
118
+ case "-o":
119
+ case "--output":
120
+ opts.output = expectValue(flag, inline, idx);
121
+ break;
122
+ case "-f":
123
+ case "--font":
124
+ opts.font = expectValue(flag, inline, idx).toLowerCase();
125
+ break;
126
+ case "-m":
127
+ case "--mode": {
128
+ const m = expectValue(flag, inline, idx);
129
+ if (!MODES.includes(m)) fail(`unknown mode "${m}" (expected: ${MODES.join(", ")})`);
130
+ opts.mode = m;
131
+ break;
132
+ }
133
+ case "--size":
134
+ opts.fontSize = numeric(flag, expectValue(flag, inline, idx));
135
+ break;
136
+ case "--line-height":
137
+ opts.lineHeight = numeric(flag, expectValue(flag, inline, idx));
138
+ break;
139
+ case "--color":
140
+ opts.color = expectValue(flag, inline, idx);
141
+ break;
142
+ case "--stagger":
143
+ opts.stagger = expectValue(flag, inline, idx);
144
+ break;
145
+ case "--stagger-duration": {
146
+ const v = expectValue(flag, inline, idx);
147
+ opts.staggerDuration = v === "auto" ? "auto" : numeric(flag, v);
148
+ break;
149
+ }
150
+ case "--pressure":
151
+ opts.pressure = numeric(flag, expectValue(flag, inline, idx));
152
+ break;
153
+ case "--smoothing":
154
+ opts.smoothing = true;
155
+ break;
156
+ case "--segment-size":
157
+ opts.segmentSize = numeric(flag, expectValue(flag, inline, idx));
158
+ break;
159
+ default: fail(`unknown option "${flag}" (try --help)`);
160
+ }
161
+ }
162
+ opts.text = words.join(" ");
163
+ return opts;
164
+ }
165
+ function numeric(flag, value) {
166
+ const n = Number(value);
167
+ if (!Number.isFinite(n)) fail(`option ${flag} expects a number, got "${value}"`);
168
+ return n;
169
+ }
170
+ function listFonts() {
171
+ process.stdout.write("Bundled fonts:\n");
172
+ for (const [name, label] of Object.entries(FONTS)) process.stdout.write(` ${name.padEnd(18)} ${label}\n`);
173
+ }
174
+ let cachedVersion = "unknown";
175
+ async function resolveVersion() {
176
+ try {
177
+ const url = new URL("../package.json", import.meta.url);
178
+ const pkg = JSON.parse(await readFile(url, "utf-8"));
179
+ if (pkg.version) cachedVersion = pkg.version;
180
+ } catch {}
181
+ }
182
+ /**
183
+ * Load a built font bundle by name. Tries the shipped `dist/fonts/<name>` first
184
+ * (the `npx` path), then the source `fonts/<name>` (running from a checkout).
185
+ */
186
+ async function loadBundle(name) {
187
+ if (!(name in FONTS)) fail(`unknown font "${name}". Run \`tegaki --list-fonts\` to see the ${Object.keys(FONTS).length} bundled fonts.`);
188
+ const candidates = [new URL(`./fonts/${name}/bundle.mjs`, import.meta.url), new URL(`../fonts/${name}/bundle.ts`, import.meta.url)];
189
+ for (const url of candidates) try {
190
+ const mod = await import(url.href);
191
+ if (mod?.default) return mod.default;
192
+ } catch {}
193
+ fail(`could not load font bundle "${name}".`);
194
+ }
195
+ async function main() {
196
+ await resolveVersion();
197
+ const opts = parseArgs(process.argv.slice(2));
198
+ if (!opts.text) {
199
+ process.stdout.write(HELP);
200
+ process.exit(0);
201
+ }
202
+ if (NEEDS_SHAPING.has(opts.font)) process.stderr.write(`tegaki: note — "${opts.font}" is a complex/RTL script. The CLI lays glyphs out by advance width without shaping, so joins and direction may be wrong. Use the browser studio (gkurt.com/tegaki/studio) for shaped output.\n`);
203
+ const font = await loadBundle(opts.font);
204
+ const timing = opts.stagger !== void 0 ? { stagger: {
205
+ advance: /%$/.test(opts.stagger) ? opts.stagger : Number(opts.stagger),
206
+ duration: opts.staggerDuration ?? "auto"
207
+ } } : void 0;
208
+ const svg = textToSvg(opts.text, font, {
209
+ fontSize: opts.fontSize,
210
+ lineHeight: opts.lineHeight,
211
+ color: opts.color,
212
+ mode: opts.mode,
213
+ pressure: opts.pressure,
214
+ smoothing: opts.smoothing,
215
+ segmentSize: opts.segmentSize,
216
+ timing
217
+ });
218
+ if (opts.output === "-") {
219
+ process.stdout.write(svg);
220
+ return;
221
+ }
222
+ const outPath = opts.output ?? `${slug(opts.text)}.svg`;
223
+ await writeFile(outPath, svg, "utf-8");
224
+ const kb = (Buffer.byteLength(svg, "utf-8") / 1024).toFixed(1);
225
+ process.stderr.write(`tegaki: wrote ${outPath} (${opts.mode}, ${FONTS[opts.font]}, ${kb} KB)\n`);
226
+ }
227
+ main().catch((err) => {
228
+ fail(err instanceof Error ? err.message : String(err));
229
+ });
230
+ //#endregion
231
+ export {};
232
+
233
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `tegaki` — turn text into an animated handwriting SVG from the command line.\n *\n * npx tegaki \"Tegaki is awesome\" # → tegaki-is-awesome.svg (looping)\n * npx tegaki \"Hello\" -f tangerine -o hi.svg\n *\n * SVG is the only format emitted: it's the one the renderer can produce with\n * zero native dependencies (no canvas, no headless browser). Raster/video\n * export (PNG/GIF/WebM) lives in the browser studio at gkurt.com/tegaki/studio.\n */\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { type TextToSvgMode, textToSvg } from './lib/textToSvg.ts';\nimport type { TegakiBundle } from './types.ts';\n\n/** Bundled fonts the CLI can load, keyed by `--font` name. */\nconst FONTS: Record<string, string> = {\n caveat: 'Caveat (Latin)',\n italianno: 'Italianno (Latin)',\n tangerine: 'Tangerine (Latin)',\n parisienne: 'Parisienne (Latin)',\n 'suez-one': 'Suez One (Hebrew + Latin)',\n amiri: 'Amiri (Arabic + Latin)',\n tillana: 'Tillana (Devanagari + Latin)',\n 'klee-one': 'Klee One (Japanese + Latin)',\n 'nanum-pen-script': 'Nanum Pen Script (Korean + Latin)',\n};\n\n/** Fonts whose scripts need shaping the headless CLI does not apply (RTL / complex GPOS). */\nconst NEEDS_SHAPING = new Set(['suez-one', 'amiri', 'tillana']);\n\nconst MODES: TextToSvgMode[] = ['loop', 'once', 'static'];\n\ninterface CliOptions {\n text: string;\n output: string | null; // null → derive from text; '-' → stdout\n font: string;\n fontSize: number;\n lineHeight?: number;\n color: string;\n mode: TextToSvgMode;\n stagger?: string;\n staggerDuration?: number | 'auto';\n pressure?: number;\n smoothing: boolean;\n segmentSize?: number;\n}\n\nconst HELP = `tegaki — animated handwriting SVG generator\n\nUsage:\n tegaki <text...> [options]\n\nOptions:\n -o, --output <file> Output path (default: <slug>.svg; \"-\" writes to stdout)\n -f, --font <name> Bundled font (default: caveat). See --list-fonts.\n -m, --mode <mode> loop | once | static (default: loop)\n loop self-drawing, repeats forever (README hero)\n once draws itself once, then stays complete\n static finished artwork, no animation\n --size <px> Font size in px (default: 100)\n --color <css> Stroke color (default: #1a1a1a)\n --line-height <px> Line height in px (default: from font metrics)\n --stagger <advance> Overlap glyphs instead of drawing them in sequence.\n e.g. \"80%\" (of the previous glyph) or \"0.3\" (seconds)\n --stagger-duration <s|auto> Per-glyph duration when staggering (default: auto)\n --pressure <0-1> Variable stroke width for once/static (default: 1)\n --smoothing Smooth strokes onto a spline\n --segment-size <px> Stroke subdivision threshold (default: 2 when applicable)\n -h, --help Show this help\n -v, --version Show version\n --list-fonts List bundled fonts\n\nExamples:\n tegaki \"Tegaki is awesome\"\n tegaki \"Hello World\" --font tangerine --mode once -o hello.svg\n tegaki \"ABC\" --stagger 80% --size 140 --color \"#222\"\n`;\n\nfunction fail(message: string): never {\n process.stderr.write(`tegaki: ${message}\\n`);\n process.exit(1);\n}\n\n/** Slugify text into a safe base filename. */\nfunction slug(text: string): string {\n const s = text\n .trim()\n .slice(0, 40)\n .replace(/\\s+/g, '-')\n .replace(/[^\\w-]/g, '')\n .toLowerCase();\n return s || 'tegaki';\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n const words: string[] = [];\n const opts: CliOptions = {\n text: '',\n output: null,\n font: 'caveat',\n fontSize: 100,\n color: '#1a1a1a',\n mode: 'loop',\n smoothing: false,\n };\n\n // Pull the value for a flag, supporting both `--flag value` and `--flag=value`.\n const expectValue = (flag: string, inline: string | undefined, i: { v: number }): string => {\n if (inline !== undefined) return inline;\n const next = argv[++i.v];\n if (next === undefined) fail(`option ${flag} requires a value`);\n return next;\n };\n\n const idx = { v: 0 };\n for (idx.v = 0; idx.v < argv.length; idx.v++) {\n const arg = argv[idx.v]!;\n if (!arg.startsWith('-') || arg === '-') {\n words.push(arg);\n continue;\n }\n const eq = arg.indexOf('=');\n const flag = eq >= 0 ? arg.slice(0, eq) : arg;\n const inline = eq >= 0 ? arg.slice(eq + 1) : undefined;\n\n switch (flag) {\n case '-h':\n case '--help':\n process.stdout.write(HELP);\n process.exit(0);\n break;\n case '-v':\n case '--version':\n process.stdout.write(`${cachedVersion}\\n`);\n process.exit(0);\n break;\n case '--list-fonts':\n listFonts();\n process.exit(0);\n break;\n case '-o':\n case '--output':\n opts.output = expectValue(flag, inline, idx);\n break;\n case '-f':\n case '--font':\n opts.font = expectValue(flag, inline, idx).toLowerCase();\n break;\n case '-m':\n case '--mode': {\n const m = expectValue(flag, inline, idx) as TextToSvgMode;\n if (!MODES.includes(m)) fail(`unknown mode \"${m}\" (expected: ${MODES.join(', ')})`);\n opts.mode = m;\n break;\n }\n case '--size':\n opts.fontSize = numeric(flag, expectValue(flag, inline, idx));\n break;\n case '--line-height':\n opts.lineHeight = numeric(flag, expectValue(flag, inline, idx));\n break;\n case '--color':\n opts.color = expectValue(flag, inline, idx);\n break;\n case '--stagger':\n opts.stagger = expectValue(flag, inline, idx);\n break;\n case '--stagger-duration': {\n const v = expectValue(flag, inline, idx);\n opts.staggerDuration = v === 'auto' ? 'auto' : numeric(flag, v);\n break;\n }\n case '--pressure':\n opts.pressure = numeric(flag, expectValue(flag, inline, idx));\n break;\n case '--smoothing':\n opts.smoothing = true;\n break;\n case '--segment-size':\n opts.segmentSize = numeric(flag, expectValue(flag, inline, idx));\n break;\n default:\n fail(`unknown option \"${flag}\" (try --help)`);\n }\n }\n\n opts.text = words.join(' ');\n return opts;\n}\n\nfunction numeric(flag: string, value: string): number {\n const n = Number(value);\n if (!Number.isFinite(n)) fail(`option ${flag} expects a number, got \"${value}\"`);\n return n;\n}\n\nfunction listFonts(): void {\n process.stdout.write('Bundled fonts:\\n');\n for (const [name, label] of Object.entries(FONTS)) {\n process.stdout.write(` ${name.padEnd(18)} ${label}\\n`);\n }\n}\n\n// Resolved once at startup (before args are parsed) so `--version` can print synchronously.\nlet cachedVersion = 'unknown';\nasync function resolveVersion(): Promise<void> {\n try {\n const url = new URL('../package.json', import.meta.url);\n const pkg = JSON.parse(await readFile(url, 'utf-8')) as { version?: string };\n if (pkg.version) cachedVersion = pkg.version;\n } catch {\n // leave default\n }\n}\n\n/**\n * Load a built font bundle by name. Tries the shipped `dist/fonts/<name>` first\n * (the `npx` path), then the source `fonts/<name>` (running from a checkout).\n */\nasync function loadBundle(name: string): Promise<TegakiBundle> {\n if (!(name in FONTS)) {\n fail(`unknown font \"${name}\". Run \\`tegaki --list-fonts\\` to see the ${Object.keys(FONTS).length} bundled fonts.`);\n }\n const candidates = [\n new URL(`./fonts/${name}/bundle.mjs`, import.meta.url), // built: dist/cli.mjs → dist/fonts/<name>/bundle.mjs\n new URL(`../fonts/${name}/bundle.ts`, import.meta.url), // dev: src/cli.ts → fonts/<name>/bundle.ts\n ];\n for (const url of candidates) {\n try {\n const mod = (await import(url.href)) as { default?: TegakiBundle };\n if (mod?.default) return mod.default;\n } catch {\n // try next candidate\n }\n }\n fail(`could not load font bundle \"${name}\".`);\n}\n\nasync function main(): Promise<void> {\n await resolveVersion();\n const opts = parseArgs(process.argv.slice(2));\n\n if (!opts.text) {\n process.stdout.write(HELP);\n process.exit(0);\n }\n\n if (NEEDS_SHAPING.has(opts.font)) {\n process.stderr.write(\n `tegaki: note — \"${opts.font}\" is a complex/RTL script. The CLI lays glyphs out by ` +\n `advance width without shaping, so joins and direction may be wrong. ` +\n `Use the browser studio (gkurt.com/tegaki/studio) for shaped output.\\n`,\n );\n }\n\n const font = await loadBundle(opts.font);\n\n const timing =\n opts.stagger !== undefined\n ? {\n stagger: {\n advance: /%$/.test(opts.stagger) ? (opts.stagger as `${number}%`) : Number(opts.stagger),\n duration: opts.staggerDuration ?? ('auto' as const),\n },\n }\n : undefined;\n\n const svg = textToSvg(opts.text, font, {\n fontSize: opts.fontSize,\n lineHeight: opts.lineHeight,\n color: opts.color,\n mode: opts.mode,\n pressure: opts.pressure,\n smoothing: opts.smoothing,\n segmentSize: opts.segmentSize,\n timing,\n });\n\n if (opts.output === '-') {\n process.stdout.write(svg);\n return;\n }\n\n const outPath = opts.output ?? `${slug(opts.text)}.svg`;\n await writeFile(outPath, svg, 'utf-8');\n const kb = (Buffer.byteLength(svg, 'utf-8') / 1024).toFixed(1);\n process.stderr.write(`tegaki: wrote ${outPath} (${opts.mode}, ${FONTS[opts.font]}, ${kb} KB)\\n`);\n}\n\nmain().catch((err: unknown) => {\n fail(err instanceof Error ? err.message : String(err));\n});\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { S as TimedPoint, _ as TegakiEffectName, a as BBox, b as TegakiMultiEffectName, c as CSSLength, d as LineCap, f as PathCommand, g as TegakiEffectConfigs, h as TegakiBundle, i as ShapedGlyph, l as FontOutput, m as Stroke, o as BUNDLE_VERSION, p as Point, r as BundleShaper, s as COMPATIBLE_BUNDLE_VERSIONS, t as ShaperFactory, u as GlyphData, v as TegakiEffects, x as TegakiSingletonEffectName, y as TegakiGlyphData } from "../shaper-registry-CrfNiB1j.mjs";
2
- import { A as TimelineEntry, C as resolveEffects, D as computeTextLayout, E as computeLayoutBbox, M as computeTimeline, O as Timeline, S as hasRenderHooks, T as TextLayout, _ as RenderStageContext, a as CreateElementFn, b as findEffects, c as TimeControlMode, d as getBundle, f as registerBundle, g as EffectDefinition, h as drawGlyph, i as TegakiEngine, j as TimelineStaggerConfig, k as TimelineConfig, l as TimeControlProp, m as ensureFontFace, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as ResolvedEffect, w as LayoutBBox, x as getEffectDefinition, y as findEffect } from "../index-BVYJFRMq.mjs";
3
- export { BBox, BUNDLE_VERSION, BundleShaper, COMPATIBLE_BUNDLE_VERSIONS, CSSLength, CreateElementFn, EffectDefinition, FontOutput, GlyphData, LayoutBBox, LineCap, PathCommand, Point, RenderStageContext, ResolvedEffect, ShapedGlyph, ShaperFactory, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiQuality, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, TimelineStaggerConfig, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects };
1
+ import { S as TimedPoint, _ as TegakiEffectName, a as BBox, b as TegakiMultiEffectName, c as CSSLength, d as LineCap, f as PathCommand, g as TegakiEffectConfigs, h as TegakiBundle, i as ShapedGlyph, l as FontOutput, m as Stroke, o as BUNDLE_VERSION, p as Point, r as BundleShaper, s as COMPATIBLE_BUNDLE_VERSIONS, t as ShaperFactory, u as GlyphData, v as TegakiEffects, x as TegakiSingletonEffectName, y as TegakiGlyphData } from "../shaper-registry-DmlJGnE3.mjs";
2
+ import { A as computeTextLayout, C as findEffects, D as LayoutBBox, E as resolveEffects, F as computeTimeline, M as TimelineConfig, N as TimelineEntry, O as TextLayout, P as TimelineStaggerConfig, S as findEffect, T as hasRenderHooks, _ as ensureFontFace, a as CreateElementFn, b as RenderStageContext, c as TimeControlMode, d as getBundle, f as registerBundle, g as textToSvg, h as TextToSvgOptions, i as TegakiEngine, j as Timeline, k as computeLayoutBbox, l as TimeControlProp, m as TextToSvgMode, n as buildRootProps, o as TegakiEngineOptions, p as resolveBundle, r as domCreateElement, s as TegakiQuality, t as buildChildren, u as createBundle, v as drawGlyph, w as getEffectDefinition, x as ResolvedEffect, y as EffectDefinition } from "../index-D-lHv505.mjs";
3
+ export { type BBox, BUNDLE_VERSION, type BundleShaper, COMPATIBLE_BUNDLE_VERSIONS, type CSSLength, type CreateElementFn, type EffectDefinition, type FontOutput, type GlyphData, type LayoutBBox, type LineCap, type PathCommand, type Point, type RenderStageContext, type ResolvedEffect, type ShapedGlyph, type ShaperFactory, type Stroke, type TegakiBundle, type TegakiEffectConfigs, type TegakiEffectName, type TegakiEffects, TegakiEngine, type TegakiEngineOptions, type TegakiGlyphData, type TegakiMultiEffectName, type TegakiQuality, type TegakiSingletonEffectName, type TextLayout, type TextToSvgMode, type TextToSvgOptions, type TimeControlMode, type TimeControlProp, type TimedPoint, type Timeline, type TimelineConfig, type TimelineEntry, type TimelineStaggerConfig, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects, textToSvg };
@@ -1,2 +1,3 @@
1
- import { _ as findEffect, a as createBundle, b as hasRenderHooks, c as resolveBundle, d as computeTimeline, f as computeLayoutBbox, h as drawGlyph, i as domCreateElement, l as BUNDLE_VERSION, m as ensureFontFace, n as buildChildren, o as getBundle, p as computeTextLayout, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS, v as findEffects, x as resolveEffects, y as getEffectDefinition } from "../core-MwnsJAbN.mjs";
2
- export { BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, TegakiEngine, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects };
1
+ import { _ as getEffectDefinition, a as createBundle, c as resolveBundle, d as computeLayoutBbox, f as computeTextLayout, g as findEffects, h as findEffect, i as domCreateElement, l as BUNDLE_VERSION, m as drawGlyph, n as buildChildren, o as getBundle, p as ensureFontFace, r as buildRootProps, s as registerBundle, t as TegakiEngine, u as COMPATIBLE_BUNDLE_VERSIONS, v as hasRenderHooks, y as resolveEffects } from "../core-VIBhSqFM.mjs";
2
+ import { n as computeTimeline, t as textToSvg } from "../textToSvg-GTQMM8Le.mjs";
3
+ export { BUNDLE_VERSION, COMPATIBLE_BUNDLE_VERSIONS, TegakiEngine, buildChildren, buildRootProps, computeLayoutBbox, computeTextLayout, computeTimeline, createBundle, domCreateElement, drawGlyph, ensureFontFace, findEffect, findEffects, getBundle, getEffectDefinition, hasRenderHooks, registerBundle, resolveBundle, resolveEffects, textToSvg };