@logomarks/core 0.1.9 → 0.2.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 (133) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +10 -1
  3. package/package.json +59 -4
  4. package/src/batch/args.d.ts +18 -0
  5. package/src/batch/args.js +44 -0
  6. package/src/batch/index.d.ts +7 -0
  7. package/src/batch/index.js +10 -0
  8. package/src/batch/run.d.ts +53 -0
  9. package/src/batch/run.js +92 -0
  10. package/src/batch/slug.d.ts +2 -0
  11. package/src/batch/slug.js +12 -0
  12. package/src/batch/source.d.ts +12 -0
  13. package/src/batch/source.js +23 -0
  14. package/src/fit/index.d.ts +4 -0
  15. package/src/fit/index.js +29 -5
  16. package/src/font/glyphs.d.ts +31 -0
  17. package/src/font/glyphs.js +228 -0
  18. package/src/font/index.d.ts +10 -0
  19. package/src/font/index.js +10 -0
  20. package/src/font/measure.d.ts +16 -0
  21. package/src/font/measure.js +201 -0
  22. package/src/font/moves.d.ts +42 -0
  23. package/src/font/moves.js +263 -0
  24. package/src/forms/discs.d.ts +20 -0
  25. package/src/forms/discs.js +114 -0
  26. package/src/forms/emblems.d.ts +22 -0
  27. package/src/forms/emblems.js +109 -0
  28. package/src/forms/flow.d.ts +21 -0
  29. package/src/forms/flow.js +80 -0
  30. package/src/forms/folds.d.ts +18 -0
  31. package/src/forms/folds.js +147 -0
  32. package/src/forms/geo.d.ts +46 -0
  33. package/src/forms/geo.js +233 -0
  34. package/src/forms/hooks.d.ts +40 -0
  35. package/src/forms/hooks.js +204 -0
  36. package/src/forms/index.d.ts +22 -0
  37. package/src/forms/index.js +24 -0
  38. package/src/forms/jog.d.ts +33 -0
  39. package/src/forms/jog.js +162 -0
  40. package/src/forms/lines.d.ts +32 -0
  41. package/src/forms/lines.js +233 -0
  42. package/src/forms/ref.d.ts +41 -0
  43. package/src/forms/ref.js +246 -0
  44. package/src/forms/shatter.d.ts +40 -0
  45. package/src/forms/shatter.js +341 -0
  46. package/src/forms/slats.d.ts +35 -0
  47. package/src/forms/slats.js +78 -0
  48. package/src/forms/swash.d.ts +23 -0
  49. package/src/forms/swash.js +74 -0
  50. package/src/forms/tech.d.ts +40 -0
  51. package/src/forms/tech.js +293 -0
  52. package/src/forms/tracks.d.ts +36 -0
  53. package/src/forms/tracks.js +121 -0
  54. package/src/geometry/index.d.ts +1 -0
  55. package/src/geometry/index.js +1 -0
  56. package/src/geometry/mitre.d.ts +8 -0
  57. package/src/geometry/mitre.js +64 -0
  58. package/src/geometry/modular.d.ts +1 -1
  59. package/src/index.js +2 -1
  60. package/src/iso/index.d.ts +5 -0
  61. package/src/iso/index.js +13 -0
  62. package/src/iso/objects.d.ts +168 -0
  63. package/src/iso/objects.js +1042 -0
  64. package/src/iso/planes.d.ts +27 -0
  65. package/src/iso/planes.js +138 -0
  66. package/src/iso/voxels.d.ts +43 -0
  67. package/src/iso/voxels.js +255 -0
  68. package/src/letters/blackletter.d.ts +31 -0
  69. package/src/letters/blackletter.js +199 -0
  70. package/src/letters/dotstroke.d.ts +16 -0
  71. package/src/letters/dotstroke.js +77 -0
  72. package/src/letters/echo.d.ts +42 -0
  73. package/src/letters/echo.js +246 -0
  74. package/src/letters/frame.d.ts +6 -0
  75. package/src/letters/frame.js +14 -0
  76. package/src/letters/index.d.ts +16 -0
  77. package/src/letters/index.js +18 -0
  78. package/src/letters/inktrap.d.ts +66 -0
  79. package/src/letters/inktrap.js +244 -0
  80. package/src/letters/mono.d.ts +145 -0
  81. package/src/letters/mono.js +684 -0
  82. package/src/letters/pillar.d.ts +31 -0
  83. package/src/letters/pillar.js +257 -0
  84. package/src/letters/serif.d.ts +109 -0
  85. package/src/letters/serif.js +666 -0
  86. package/src/letters/stair.d.ts +8 -0
  87. package/src/letters/stair.js +43 -0
  88. package/src/letters/stem.d.ts +29 -0
  89. package/src/letters/stem.js +205 -0
  90. package/src/objects/animals.d.ts +105 -0
  91. package/src/objects/animals.js +469 -0
  92. package/src/objects/bio.d.ts +14 -0
  93. package/src/objects/bio.js +76 -0
  94. package/src/objects/devglyphs.d.ts +34 -0
  95. package/src/objects/devglyphs.js +226 -0
  96. package/src/objects/devices.d.ts +31 -0
  97. package/src/objects/devices.js +114 -0
  98. package/src/objects/faces.d.ts +94 -0
  99. package/src/objects/faces.js +344 -0
  100. package/src/objects/food.d.ts +12 -0
  101. package/src/objects/food.js +74 -0
  102. package/src/objects/index.d.ts +13 -0
  103. package/src/objects/index.js +18 -0
  104. package/src/objects/rosettes.d.ts +14 -0
  105. package/src/objects/rosettes.js +64 -0
  106. package/src/objects/sport.d.ts +8 -0
  107. package/src/objects/sport.js +37 -0
  108. package/src/objects/weapons.d.ts +31 -0
  109. package/src/objects/weapons.js +179 -0
  110. package/src/parts/index.d.ts +2 -0
  111. package/src/parts/index.js +2 -0
  112. package/src/parts/pixels.d.ts +11 -0
  113. package/src/parts/pixels.js +74 -0
  114. package/src/parts/run.js +4 -2
  115. package/src/parts/span.d.ts +13 -0
  116. package/src/parts/span.js +100 -0
  117. package/src/pixels/dither.d.ts +78 -0
  118. package/src/pixels/dither.js +399 -0
  119. package/src/pixels/emote.d.ts +6 -0
  120. package/src/pixels/emote.js +76 -0
  121. package/src/pixels/index.d.ts +7 -0
  122. package/src/pixels/index.js +8 -0
  123. package/src/sdf/fields.js +135 -0
  124. package/src/sdf/gap.js +42 -0
  125. package/src/sdf/index.d.ts +58 -0
  126. package/src/sdf/index.js +14 -0
  127. package/src/sdf/ops.js +102 -0
  128. package/src/sdf/trace.js +90 -0
  129. package/src/sdf/y2k.d.ts +12 -0
  130. package/src/sdf/y2k.js +80 -0
  131. package/src/skill/bundle.js +1 -1
  132. package/src/util/vec.d.ts +14 -0
  133. package/src/util/vec.js +61 -0
@@ -0,0 +1,684 @@
1
+ /**
2
+ * Monogram letters: the letter-building geometry the monogram pairs are drawn with, so each pair draws with one build of
3
+ * each letter instead of a copy. Four styles:
4
+ * - Fused: solid letters, stems W (84), cap 360 (y T0 76 to T1 436), square ends;
5
+ * - Track: centrelines drawn as two tracks (26 wide, 48 apart) with filleted corners, on a cap TH (360) from y 0;
6
+ * - Badge: monoline letters (BT 52 wide, cap BH 212 before the knockout's fit) knocked out of a house badge;
7
+ * - Fan: two Fan letters on one grid of square and quarter-disc tiles (Modular).
8
+ * Also Puff (balloon strokes), Linked (woven bands) and `clean()`, which tidies a finished shape for writing.
9
+ * From the Logomarks tools (monoletters.mjs, batches 99 and 102–104).
10
+ */
11
+ import { C, GAP as G, U, D, I, circle, annulus, rect, bar, poly, sector, arcBand, weave, balloon, sample, stroke, dedupe, fillet, tracks, knockout, linePts, arcPts, joinPts } from '../geometry/index.js';
12
+
13
+ /** @typedef {import('../geometry/types.js').Point} Point */
14
+ /** @typedef {import('../geometry/types.js').Polygon} Polygon */
15
+ /** @typedef {import('../geometry/types.js').Shape} Shape */
16
+
17
+ /* ---------- Fused: solid letters, stems 84, cap 360 (y 76–436) ---------- */
18
+
19
+ /** The Fused stem width: 84. */
20
+ export const W = 84;
21
+
22
+ /** The Fused cap line: y 76. */
23
+ export const T0 = 76;
24
+
25
+ /** The Fused baseline: y 436. */
26
+ export const T1 = 436;
27
+
28
+ /** Halfway between the cap line and the baseline: y 256. */
29
+ export const MID = (T0 + T1) / 2;
30
+
31
+ /**
32
+ * A Fused stem.
33
+ *
34
+ * @param {number} x Its left edge.
35
+ * @param {number} [y0] Its top (T0).
36
+ * @param {number} [y1] Its foot (T1).
37
+ * @returns {Polygon} The stem.
38
+ */
39
+ export const stem = (x, y0 = T0, y1 = T1) => rect(x, y0, W, y1 - y0);
40
+
41
+ /**
42
+ * A Fused bar.
43
+ *
44
+ * @param {number} x0 Its left end.
45
+ * @param {number} x1 Its right end.
46
+ * @param {number} y Its top edge.
47
+ * @returns {Polygon} The bar.
48
+ */
49
+ export const hbar = (x0, x1, y) => rect(x0, y, x1 - x0, W);
50
+
51
+ /**
52
+ * Clips a shape flat at the cap and base lines.
53
+ *
54
+ * @param {import('../geometry/types.js').Geom} g The shape.
55
+ * @param {number} [y0] The top line (T0).
56
+ * @param {number} [y1] The bottom line (T1).
57
+ * @returns {Shape} It, clipped.
58
+ */
59
+ export const clipH = (g, y0 = T0, y1 = T1) => I(g, rect(-400, y0, 1400, y1 - y0));
60
+
61
+ /**
62
+ * A D-shaped bowl to the right of a stem.
63
+ *
64
+ * @param {number} x The stem's left edge.
65
+ * @param {number} y0 The bowl's top.
66
+ * @param {number} y1 Its foot.
67
+ * @param {number} [reach=60] How far its straight runs reach past the stem before the curve.
68
+ * @returns {Shape} The bowl (the stem's height between y0 and y1 included).
69
+ */
70
+ export const bowl = (x, y0, y1, reach = 60) => {
71
+ const h = y1 - y0, r = h / 2;
72
+ return D(U(rect(x, y0, W + reach, h), circle(x + W + reach, y0 + r, r)), U(rect(x + W, y0 + W, reach, h - 2 * W), circle(x + W + reach, y0 + r, r - W)));
73
+ };
74
+
75
+ /* ---------- centreline helpers (Track, Badge, Puff) ---------- */
76
+
77
+ /**
78
+ * A straight run of points, about one every 8 units.
79
+ *
80
+ * @param {readonly number[]} a From.
81
+ * @param {readonly number[]} b To.
82
+ * @param {number} [n] The number of points (by the length, by default).
83
+ * @returns {Point[]} The run.
84
+ */
85
+ export const L_ = (a, b, n) => linePts(a, b, n || Math.max(2, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1]) / 8)));
86
+
87
+ /**
88
+ * A circular arc's points, about one every 3°.
89
+ *
90
+ * @param {number} cx The centre's x.
91
+ * @param {number} cy The centre's y.
92
+ * @param {number} r The radius.
93
+ * @param {number} a0 The start angle (degrees, 0 = right, 90 = down).
94
+ * @param {number} a1 The end angle.
95
+ * @returns {Point[]} The arc.
96
+ */
97
+ export const A_ = (cx, cy, r, a0, a1) => arcPts(cx, cy, r, a0, a1, Math.max(12, Math.ceil(Math.abs(a1 - a0) / 3)));
98
+
99
+ /**
100
+ * Joins runs end to start (each run's first point dropped after the first).
101
+ *
102
+ * @param {...Point[]} p The runs.
103
+ * @returns {Point[]} One run.
104
+ */
105
+ export const J_ = (...p) => joinPts(...p);
106
+
107
+ /**
108
+ * Drops the in-between points of straight runs, so fillet() sees each corner between two long legs.
109
+ *
110
+ * @param {readonly Point[]} p The polyline.
111
+ * @returns {Point[]} Its corners and ends.
112
+ */
113
+ export const straighten = (p) => p.filter((pt, i) => {
114
+ if (!i || i === p.length - 1) return true;
115
+ const a = p[i - 1], b = p[i + 1];
116
+ const t1 = Math.atan2(pt[1] - a[1], pt[0] - a[0]), t2 = Math.atan2(b[1] - pt[1], b[0] - pt[0]);
117
+ let d = Math.abs(t2 - t1);
118
+ if (d > Math.PI) d = 2 * Math.PI - d;
119
+ return d > 0.017;
120
+ });
121
+
122
+ /**
123
+ * Trims a polyline by d from its start, or its end.
124
+ *
125
+ * @param {readonly Point[]} p The polyline.
126
+ * @param {number} d How much to trim.
127
+ * @param {boolean} [atEnd] Trim the end instead.
128
+ * @returns {Point[]} The trimmed polyline.
129
+ */
130
+ export const trim = (p, d, atEnd) => {
131
+ const q = atEnd ? p.slice().reverse() : p.slice();
132
+ let left = d;
133
+ while (q.length > 2) { const L = Math.hypot(q[1][0] - q[0][0], q[1][1] - q[0][1]); if (L > left) { const t = left / L; q[0] = [q[0][0] + (q[1][0] - q[0][0]) * t, q[0][1] + (q[1][1] - q[0][1]) * t]; break; } left -= L; q.shift(); }
134
+ return atEnd ? q.reverse() : q;
135
+ };
136
+
137
+ /** The two tracks' width (the Duo rule): 26. */
138
+ export const TW = 26;
139
+
140
+ /** The two tracks' pitch: 48, so a 74 band with a 22 gap between the tracks. */
141
+ export const PITCH = 48;
142
+
143
+ /**
144
+ * Two tracks along a centreline whose sharp corners are filleted.
145
+ *
146
+ * @param {readonly Point[]} pts The centreline.
147
+ * @param {number | number[]} [r=60] The fillet (or one per corner).
148
+ * @param {object} [o] More tracks() options.
149
+ * @returns {Shape} The tracks.
150
+ */
151
+ export const track2 = (pts, r = 60, o = {}) => tracks(dedupe(fillet(straighten(pts), r)), { n: 2, pitch: PITCH, w: TW, ...o });
152
+
153
+ /**
154
+ * Three tracks (22 wide, 44 apart) along a filleted centreline.
155
+ *
156
+ * @param {readonly Point[]} pts The centreline.
157
+ * @param {number | number[]} [r=70] The fillet (or one per corner).
158
+ * @returns {Shape} The tracks.
159
+ */
160
+ export const track3 = (pts, r = 70) => tracks(dedupe(fillet(straighten(pts), r)), { n: 3, pitch: 44, w: 22 });
161
+
162
+ /**
163
+ * A monoline stroke with square ends.
164
+ *
165
+ * @param {readonly Point[]} pts The centreline.
166
+ * @param {number} w The width.
167
+ * @returns {Shape} The stroke.
168
+ */
169
+ export const mline = (pts, w) => stroke(dedupe(pts), w, { chunk: 8 });
170
+
171
+ /**
172
+ * An ellipse arc's points, about one every 3°.
173
+ *
174
+ * @param {number} cx The centre's x.
175
+ * @param {number} cy The centre's y.
176
+ * @param {number} rx The x radius.
177
+ * @param {number} ry The y radius.
178
+ * @param {number} a0 The start angle (degrees, 0 = right, 90 = down).
179
+ * @param {number} a1 The end angle.
180
+ * @returns {Point[]} The arc.
181
+ */
182
+ export const E_ = (cx, cy, rx, ry, a0, a1) => sample((t) => [cx + rx * Math.cos((t * Math.PI) / 180), cy + ry * Math.sin((t * Math.PI) / 180)], a0, a1, Math.max(16, Math.ceil(Math.abs(a1 - a0) / 3)));
183
+
184
+ /**
185
+ * The top ring of a Fused S's centreline (cap 360): from a0 back round to the spine.
186
+ *
187
+ * @param {number} sx The S's centre x.
188
+ * @param {number} rx The ring's x radius.
189
+ * @param {number} [a0=-25] Where the terminal starts (degrees).
190
+ * @param {number} [ry] The ring's y radius (a quarter of the cap less a stem).
191
+ * @returns {Point[]} The centreline.
192
+ */
193
+ export const sTop = (sx, rx, a0 = -25, ry = (T1 - T0 - W) / 4) => E_(sx, T0 + W / 2 + ry, rx, ry, a0, -270);
194
+
195
+ /**
196
+ * The lower ring of a Fused S's centreline: from the spine to a1.
197
+ *
198
+ * @param {number} sx The S's centre x.
199
+ * @param {number} rx The ring's x radius.
200
+ * @param {number} [a1=160] Where the terminal ends (degrees).
201
+ * @param {number} [ry] The ring's y radius.
202
+ * @returns {Point[]} The centreline.
203
+ */
204
+ export const sLow = (sx, rx, a1 = 160, ry = (T1 - T0 - W) / 4) => E_(sx, T1 - W / 2 - ry, rx, ry, -90, a1);
205
+
206
+ /**
207
+ * A centreline stroked solid (square ends).
208
+ *
209
+ * @param {readonly Point[]} pts The centreline.
210
+ * @param {number} [w] The width (W).
211
+ * @returns {Shape} The stroke.
212
+ */
213
+ export const solid = (pts, w = W) => stroke(dedupe(pts), w, { chunk: 8 });
214
+
215
+ /* ---------- solid letters ---------- */
216
+
217
+ /**
218
+ * A Fused A's two legs (stroke 96, like the set's A's), clipped to the cap.
219
+ *
220
+ * @param {number} ax The apex's x.
221
+ * @param {number} w How far the feet spread.
222
+ * @returns {Shape} The legs.
223
+ */
224
+ export const aLegs = (ax, w) => clipH(U(bar([ax - w / 2 - 30, T1 + 60], [ax, T0 - 40], 96), bar([ax, T0 - 40], [ax + w / 2 + 30, T1 + 60], 96)));
225
+
226
+ /**
227
+ * A Fused M: two stems and a V whose bars meet at (mid, v); the notch their square ends leave is filled, so the V ends in
228
+ * one clean point.
229
+ *
230
+ * @param {number} x The left stem's left edge.
231
+ * @param {number} xr The right stem's left edge.
232
+ * @param {number} [v=340] The V's point's height.
233
+ * @returns {Shape} The M.
234
+ */
235
+ export const mLetter = (x, xr, v = 340) => {
236
+ const V = [(x + xr) / 2 + 42, v], ends = [[x + 42, 30], [xr + 42, 30]].flatMap(([px, py]) => { const L = Math.hypot(V[0] - px, V[1] - py), n = [-(V[1] - py) / L * 48, (V[0] - px) / L * 48]; return [[V[0] + n[0], V[1] + n[1]], [V[0] - n[0], V[1] - n[1]]]; });
237
+ const hull = ends.sort((a, b) => Math.atan2(a[1] - V[1], a[0] - V[0]) - Math.atan2(b[1] - V[1], b[0] - V[0]));
238
+ return U(stem(x), stem(xr), clipH(U(bar([x + 42, 30], V, 96), bar([xr + 42, 30], V, 96), poly(hull))));
239
+ };
240
+
241
+ /**
242
+ * A Fused A's crossbar, kept inside the legs.
243
+ *
244
+ * @param {number} ax The apex's x.
245
+ * @param {number} aw How far the feet spread (as aLegs()).
246
+ * @param {number} y The bar's top edge.
247
+ * @returns {Shape} The bar.
248
+ */
249
+ export const aBar = (ax, aw, y) => I(hbar(ax - 300, ax + 300, y), U(aLegs(ax, aw), poly([[ax, T0 - 40], [ax - aw / 2 - 30, T1 + 60], [ax + aw / 2 + 30, T1 + 60]])));
250
+
251
+ /* ---------- Badge: monoline letters (52), knocked out of a house badge ---------- */
252
+
253
+ /** The Badge letters' monoline width: 52. */
254
+ export const BT = 52;
255
+
256
+ /** The Badge letters' centreline cap height: 212. */
257
+ export const BH = 212;
258
+
259
+ /**
260
+ * A Badge J's centreline: a stem at x, hooking to the left.
261
+ *
262
+ * @param {number} x The stem's centreline.
263
+ * @param {number} [r=50] The hook's radius.
264
+ * @param {number} [H] The cap height (BH).
265
+ * @returns {Point[]} The centreline.
266
+ */
267
+ export const bJ = (x, r = 50, H = BH) => J_(L_([x, -BT], [x, H - r]), A_(x - r, H - r, r, 0, 180), L_([x - 2 * r, H - r], [x - 2 * r, H - r - 24]));
268
+
269
+ /**
270
+ * A Badge bowl's centreline off a stem: out along y0, round, and back along y1.
271
+ *
272
+ * @param {number} x The stem's centreline.
273
+ * @param {number} y0 The bowl's top.
274
+ * @param {number} y1 Its foot.
275
+ * @param {number} w How far it reaches.
276
+ * @returns {Point[]} The centreline.
277
+ */
278
+ export const bBowl = (x, y0, y1, w) => { const r = (y1 - y0) / 2; return J_(L_([x, y0], [x + w - r, y0]), A_(x + w - r, y0 + r, r, -90, 90), L_([x + w - r, y1], [x, y1])); };
279
+
280
+ /**
281
+ * A Badge B's centrelines: the stem and two bowls.
282
+ *
283
+ * @param {number} x The stem's centreline.
284
+ * @param {number} [w=128] The lower bowl's reach (the upper reaches 12 less).
285
+ * @param {number} [H] The cap height (BH).
286
+ * @param {number} [k=0.48] Where the bowls meet, as a share of the cap.
287
+ * @returns {Point[][]} The strokes.
288
+ */
289
+ export const bB = (x, w = 128, H = BH, k = 0.48) => [L_([x, -BT], [x, H + BT]), bBowl(x, 0, H * k, w - 12), bBowl(x, H * k, H, w)];
290
+
291
+ /**
292
+ * A Badge M's centreline: up, down to the V, up, down.
293
+ *
294
+ * @param {number} x The left stem's centreline.
295
+ * @param {number} [w=190] The width.
296
+ * @param {number} [H] The cap height (BH).
297
+ * @param {number} [v=0.72] The V's depth, as a share of the cap.
298
+ * @returns {Point[][]} The stroke.
299
+ */
300
+ export const bM = (x, w = 190, H = BH, v = 0.72) => [J_(L_([x, H + BT], [x, 0]), L_([x, 0], [x + w / 2, H * v]), L_([x + w / 2, H * v], [x + w, 0]), L_([x + w, 0], [x + w, H + BT]))];
301
+
302
+ /**
303
+ * A Badge H's centrelines.
304
+ *
305
+ * @param {number} x The left stem's centreline.
306
+ * @param {number} [w=150] The width.
307
+ * @param {number} [H] The cap height (BH).
308
+ * @returns {Point[][]} The strokes.
309
+ */
310
+ export const bH = (x, w = 150, H = BH) => [L_([x, -BT], [x, H + BT]), L_([x + w, -BT], [x + w, H + BT]), L_([x, H / 2], [x + w, H / 2])];
311
+
312
+ /**
313
+ * A Badge S's centreline: two rings on a quarter of the cap each.
314
+ *
315
+ * @param {number} sx The S's centre x.
316
+ * @param {number} [rx=64] The rings' x radius.
317
+ * @param {number} [a0=-25] Where the top terminal starts (degrees).
318
+ * @param {number} [a1=160] Where the bottom terminal ends.
319
+ * @param {number} [H] The cap height (BH).
320
+ * @returns {Point[][]} The stroke.
321
+ */
322
+ export const bS = (sx, rx = 64, a0 = -25, a1 = 160, H = BH) => { const ry = H / 4; return [J_(E_(sx, ry, rx, ry, a0, -270), E_(sx, 3 * ry, rx, ry, -90, a1))]; };
323
+
324
+ /**
325
+ * A Badge C's centreline.
326
+ *
327
+ * @param {number} cx The centre's x.
328
+ * @param {number} [r] The radius (half the cap).
329
+ * @param {number} [a0=42] Where it starts (degrees).
330
+ * @param {number} [a1=318] Where it ends.
331
+ * @returns {Point[][]} The stroke.
332
+ */
333
+ export const bC = (cx, r = BH / 2, a0 = 42, a1 = 318) => [A_(cx, BH / 2, r, a0, a1)];
334
+
335
+ /**
336
+ * A Badge P's centrelines.
337
+ *
338
+ * @param {number} x The stem's centreline.
339
+ * @param {number} [w=130] The bowl's reach.
340
+ * @param {number} [H] The cap height (BH).
341
+ * @returns {Point[][]} The strokes.
342
+ */
343
+ export const bP = (x, w = 130, H = BH) => [L_([x, -BT], [x, H + BT]), bBowl(x, 0, H * 0.56, w)];
344
+
345
+ /**
346
+ * A Badge R's centrelines: stem, bowl and leg.
347
+ *
348
+ * @param {number} x The stem's centreline.
349
+ * @param {number} [w=136] The width (the bowl reaches 6 less).
350
+ * @param {number} [H] The cap height (BH).
351
+ * @returns {Point[][]} The strokes.
352
+ */
353
+ export const bR = (x, w = 136, H = BH) => [L_([x, -BT], [x, H + BT]), bBowl(x, 0, H * 0.54, w - 6), L_([x + w * 0.42, H * 0.54], [x + w + 30, H + 60])];
354
+
355
+ /**
356
+ * A Badge A's centrelines: the legs and the bar.
357
+ *
358
+ * @param {number} ax The apex's x.
359
+ * @param {number} [w=200] The width.
360
+ * @param {number} [H] The cap height (BH).
361
+ * @param {number} [bar=0.66] The bar's height, as a share of the cap.
362
+ * @returns {Point[][]} The strokes.
363
+ */
364
+ export const bA = (ax, w = 200, H = BH, bar = 0.66) => [J_(L_([ax - w / 2 - 40, H + 90], [ax, 0]), L_([ax, 0], [ax + w / 2 + 40, H + 90])), L_([ax - w * 0.3, H * bar], [ax + w * 0.3, H * bar])];
365
+
366
+ /**
367
+ * A Badge D's centreline.
368
+ *
369
+ * @param {number} x The stem's centreline.
370
+ * @param {number} [w=160] The bowl's reach.
371
+ * @param {number} [H] The cap height (BH).
372
+ * @returns {Point[][]} The stroke.
373
+ */
374
+ export const bD = (x, w = 160, H = BH) => [J_(L_([x, H / 2], [x, -0]), bBowl(x, 0, H, w), L_([x, H], [x, H / 2]))];
375
+
376
+ /**
377
+ * The top-bar ligature as a monoline: a bar from the J's left across to x1, the J's stem at jx with its hook.
378
+ *
379
+ * @param {number} jx The J's stem's centreline.
380
+ * @param {number} x1 Where the bar ends.
381
+ * @param {number} [r=46] The hook's radius.
382
+ * @returns {Point[][]} The strokes.
383
+ */
384
+ export const bJbar = (jx, x1, r = 46) => [L_([jx - 2 * r - 20, 0], [x1, 0]), J_(L_([jx, 0], [jx, BH - r]), A_(jx - r, BH - r, r, 0, 180), L_([jx - 2 * r, BH - r], [jx - 2 * r, BH - r - 24]))];
385
+
386
+ /**
387
+ * Strokes (point lists) as one monoline, square ends, clipped flat at the cap and base lines.
388
+ *
389
+ * @param {readonly (readonly Point[])[]} parts The centrelines.
390
+ * @param {number} [T] The width (BT).
391
+ * @param {number} [H] The cap height (BH).
392
+ * @returns {Shape} The monoline.
393
+ */
394
+ export const mono = (parts, T = BT, H = BH) => I(U(...parts.map((p) => mline(p, T))), rect(-800, -T / 2, 2000, H + T));
395
+
396
+ /**
397
+ * The pair as a monoline knocked out of a house badge.
398
+ *
399
+ * @param {import('../geometry/badges.js').BadgeKind} kind The badge.
400
+ * @param {readonly (readonly Point[])[]} parts The centrelines.
401
+ * @returns {Shape} The badge.
402
+ */
403
+ export const badge = (kind, parts) => knockout(kind, mono(parts));
404
+
405
+ /* ---------- Modular: Fan tiles (a square, a quarter disc centred on one corner, or empty) ---------- */
406
+
407
+ /** The Fan cell: 140. */
408
+ export const FC = 140;
409
+
410
+ /** The Fan gutter: 26. */
411
+ export const FG = 26;
412
+
413
+ /**
414
+ * A grid of Fan tiles, centred on the canvas.
415
+ *
416
+ * @param {readonly (readonly string[])[]} rows The tiles, row by row: 'S' a square, '.' empty, 'tl', 'tr', 'bl' or 'br'
417
+ * a quarter disc centred on that corner of the cell.
418
+ * @returns {Shape} The grid.
419
+ */
420
+ export const fan = (rows) => {
421
+ const out = [], h = rows.length, w = rows[0].length, x0 = C - (w * FC) / 2, y0 = C - (h * FC) / 2;
422
+ rows.forEach((row, r) => row.forEach((c, k) => {
423
+ if (c === '.') return;
424
+ const x = x0 + k * FC + FG / 2, y = y0 + r * FC + FG / 2, s = FC - FG;
425
+ if (c === 'S') { out.push(rect(x, y, s, s)); return; }
426
+ const cx = c[1] === 'l' ? x : x + s, cy = c[0] === 't' ? y : y + s;
427
+ out.push(I(circle(cx, cy, s, 160), rect(x, y, s, s)));
428
+ }));
429
+ return U(...out);
430
+ };
431
+
432
+ /* ---------- more solid letters ---------- */
433
+
434
+ /**
435
+ * A Fused C opening off a stem: monoBowl() with its right side cut away by a wedge (half-angle a), so the terminals are
436
+ * radial.
437
+ *
438
+ * @param {number} x The stem's left edge.
439
+ * @param {number} [reach=0] The bowl's straight runs past the stem.
440
+ * @param {number} [a=42] The opening's half-angle (degrees).
441
+ * @param {number} [y0] The top (T0).
442
+ * @param {number} [y1] The foot (T1).
443
+ * @returns {Shape} The C.
444
+ */
445
+ export const cStem = (x, reach = 0, a = 42, y0 = T0, y1 = T1) => { const r = (y1 - y0) / 2; return D(monoBowl(x, y0, y1, reach), sector(x + W + reach, y0 + r, 600, -a, a)); };
446
+
447
+ /**
448
+ * bowl() kept to the right of its stem's left edge: a bowl taller than twice its reach plus the stem would otherwise
449
+ * bulge out left.
450
+ *
451
+ * @param {number} x The stem's left edge.
452
+ * @param {number} y0 The bowl's top.
453
+ * @param {number} y1 Its foot.
454
+ * @param {number} [reach=0] Its straight runs past the stem.
455
+ * @returns {Shape} The bowl.
456
+ */
457
+ export const monoBowl = (x, y0, y1, reach = 0) => I(bowl(x, y0, y1, reach), rect(x, y0 - 10, 2000, y1 - y0 + 20));
458
+
459
+ /**
460
+ * A Fused A whose right leg stands upright as a stem. Its crossbar: I(hbar(…), rightOf(...aUpLeg(sx))).
461
+ *
462
+ * @param {number} sx The stem's left edge.
463
+ * @returns {Shape} The A.
464
+ */
465
+ export const aUp = (sx) => clipH(U(bar(...aUpLeg(sx), 96), stem(sx, T0 - 100, T1)));
466
+
467
+ /**
468
+ * The half-plane right of the line from a to b (y down): clips an A's crossbar to its inside.
469
+ *
470
+ * @param {readonly number[]} a A point on the line.
471
+ * @param {readonly number[]} b Another.
472
+ * @returns {Polygon} The half-plane, as a big quad.
473
+ */
474
+ export const rightOf = (a, b) => { const dx = b[0] - a[0], dy = b[1] - a[1], k = 3000 / Math.hypot(dx, dy); return poly([[a[0] - dx * k, a[1] - dy * k], [b[0] + dx * k, b[1] + dy * k], [3000, b[1] + dy * k], [3000, a[1] - dy * k]]); };
475
+
476
+ /**
477
+ * The left leg's centreline of aUp(sx), foot to apex, landing on the stem's left edge so the counter stays open.
478
+ *
479
+ * @param {number} sx The stem's left edge.
480
+ * @returns {[Point, Point]} The foot and the apex.
481
+ */
482
+ export const aUpLeg = (sx) => [[sx - 180, T1 + 34], [sx - 10, T0 - 36]];
483
+
484
+ /**
485
+ * A Fused J's hook: an arc band curling left off a stem.
486
+ *
487
+ * @param {number} jx The stem's left edge.
488
+ * @returns {Shape} The hook.
489
+ */
490
+ export const jHook = (jx) => arcBand(jx - 58, T1 - 100 - W / 2, 100, W, 0, 180);
491
+
492
+ /* ---------- Track letters: centrelines at cap TH (y 0 to TH), as two tracks ---------- */
493
+
494
+ /** The Track cap height: 360. */
495
+ export const TH = 360;
496
+
497
+ /** The Track S's ring radius: a quarter of the cap. */
498
+ export const TR = TH / 4;
499
+
500
+ /** Half a two-track band: 37 (a track stopping short of another clears it by this plus the gap). */
501
+ export const TB2 = 37;
502
+
503
+ /** How far a track's round end reaches past its centreline: 13. */
504
+ export const TCAP = 13;
505
+
506
+ /**
507
+ * A Track S entered from the left along the base: the lower ring up its right side to the spine, the upper ring round to
508
+ * its top terminal.
509
+ *
510
+ * @param {number} sx The S's centre x.
511
+ * @param {number} [r] The rings' radius (TR).
512
+ * @param {number} [end=325] Where the top terminal ends (degrees).
513
+ * @returns {Point[]} The centreline.
514
+ */
515
+ export const sFromBase = (sx, r = TR, end = 325) => J_(A_(sx, 3 * r, r, 90, -90), A_(sx, r, r, 90, end));
516
+
517
+ /**
518
+ * A Track S from its bottom terminal up to its top point, leaving to the right along the cap line.
519
+ *
520
+ * @param {number} sx The S's centre x.
521
+ * @param {number} [r] The rings' radius (TR).
522
+ * @param {number} [start=145] Where the bottom terminal starts (degrees).
523
+ * @returns {Point[]} The centreline.
524
+ */
525
+ export const sToTop = (sx, r = TR, start = 145) => J_(A_(sx, 3 * r, r, start, -90), A_(sx, r, r, 90, 270));
526
+
527
+ /**
528
+ * A Track M from its first stem's foot to its last stem's foot.
529
+ *
530
+ * @param {number} x The first stem's centreline.
531
+ * @param {number} [w=270] The width.
532
+ * @param {number} [v=0.62] Where the V meets, as a share of the cap.
533
+ * @returns {Point[]} The centreline.
534
+ */
535
+ export const tM = (x, w = 270, v = 0.62) => J_(L_([x, TH], [x, 0]), L_([x, 0], [x + w / 2, TH * v]), L_([x + w / 2, TH * v], [x + w, 0]), L_([x + w, 0], [x + w, TH]));
536
+
537
+ /**
538
+ * A Track R's stem and bowl from the stem's foot: the bowl comes back a gap short of the stem.
539
+ *
540
+ * @param {number} x The stem's centreline.
541
+ * @param {number} [bw=230] The bowl's width.
542
+ * @param {number} [bh=190] Its height.
543
+ * @returns {Point[]} The centreline.
544
+ */
545
+ export const tRbowl = (x, bw = 230, bh = 190) => J_(L_([x, TH], [x, 0]), L_([x, 0], [x + bw - bh / 2, 0]), A_(x + bw - bh / 2, bh / 2, bh / 2, -90, 90), L_([x + bw - bh / 2, bh], [x + 72 + G + 4, bh]));
546
+
547
+ /**
548
+ * A Track R's leg, standing free a gap below the bowl, down to the base line.
549
+ *
550
+ * @param {number} x The stem's centreline.
551
+ * @param {number} [kx=120] Where the leg starts, from the stem.
552
+ * @param {number} [lx=250] Where it lands, from the stem.
553
+ * @param {number} [bh=190] The bowl's height.
554
+ * @returns {Point[]} The centreline.
555
+ */
556
+ export const tRleg = (x, kx = 120, lx = 250, bh = 190) => L_([x + kx, bh + 74 + G], [x + lx, TH]);
557
+
558
+ /**
559
+ * A Track house A: upright legs, a peaked roof from the shoulders to the apex, from its left foot to its right foot.
560
+ *
561
+ * @param {number} x The left leg's centreline.
562
+ * @param {number} [aw=280] The width.
563
+ * @param {number} [k=130] How far down the shoulders sit.
564
+ * @returns {Point[]} The centreline.
565
+ */
566
+ export const tA = (x, aw = 280, k = 130) => J_(L_([x, TH], [x, k]), L_([x, k], [x + aw / 2, 0]), L_([x + aw / 2, 0], [x + aw, k]), L_([x + aw, k], [x + aw, TH]));
567
+
568
+ /**
569
+ * The house A's bar, stopping a gap short of both legs (round ends showing).
570
+ *
571
+ * @param {number} x The left leg's centreline.
572
+ * @param {number} [aw=280] The width.
573
+ * @param {number} [y=228] The bar's height.
574
+ * @returns {Point[]} The centreline.
575
+ */
576
+ export const tAbar = (x, aw = 280, y = 228) => L_([x + TB2 + G + TCAP, y], [x + aw - TB2 - G - TCAP, y]);
577
+
578
+ /**
579
+ * A Track J's hook and stem, from the hook's tip up to the stem's top at (x, 0).
580
+ *
581
+ * @param {number} x The stem's centreline.
582
+ * @param {number} [rj=76] The hook's radius.
583
+ * @returns {Point[]} The centreline.
584
+ */
585
+ export const tJ = (x, rj = 76) => J_(L_([x - 2 * rj, TH - rj - 40], [x - 2 * rj, TH - rj]), A_(x - rj, TH - rj, rj, 180, 0), L_([x, TH - rj], [x, 0]));
586
+
587
+ /* ---------- lowercase (x-height 176 on the 360 cap) ---------- */
588
+
589
+ /** The lowercase x-height: 176. */
590
+ export const XH = 176;
591
+
592
+ /**
593
+ * A Fused n-arch standing from the x-height to the base.
594
+ *
595
+ * @param {number} xl The left stem's left edge.
596
+ * @param {number} [rA=74] The arch's centreline radius.
597
+ * @param {number} [xh] The x-height (XH).
598
+ * @param {boolean} [both=true] Draw the left stem too.
599
+ * @returns {Shape} The arch.
600
+ */
601
+ export const arch = (xl, rA = 74, xh = XH, both = true) => U(arcBand(xl + W / 2 + rA, xh + W / 2 + rA, rA, W, 180, 360), ...(both ? [rect(xl, xh + W / 2 + rA, W, T1 - xh - W / 2 - rA)] : []), rect(xl + 2 * rA, xh + W / 2 + rA, W, T1 - xh - W / 2 - rA));
602
+
603
+ /**
604
+ * A Fused lowercase e: a ring band W wide, the bar across its middle, the ring open at its lower right.
605
+ *
606
+ * @param {number} cx The centre's x.
607
+ * @param {number} cy The centre's y.
608
+ * @param {number} r The radius to the band's centre.
609
+ * @param {number[]} [open] The opening's angles (degrees, [8, 42]).
610
+ * @returns {Shape} The e.
611
+ */
612
+ export const eLow = (cx, cy, r, open = [8, 42]) => U(D(annulus(cx, cy, r, W), sector(cx, cy, r + W, open[0], open[1])), rect(cx - r, cy - W / 2, 2 * r, W));
613
+
614
+ /* ---------- Puff: balloon strokes (100 at the ends, 134 in the middle), one per stroke ---------- */
615
+
616
+ /**
617
+ * Puff letters: each stroke a balloon, 100 wide at its ends and 134 in the middle.
618
+ *
619
+ * @param {...Point[]} strokes The centrelines.
620
+ * @returns {Shape} The letters.
621
+ */
622
+ export const puff = (...strokes) => U(...strokes.map((p) => balloon(dense(p, 12), 100, 134)));
623
+
624
+ /* ---------- Linked: letters as bands woven over and under ---------- */
625
+
626
+ /** The Linked band's width: 46. */
627
+ export const LBW = 46;
628
+
629
+ /** The Linked gap at a crossing: 12. */
630
+ export const LBG = 12;
631
+
632
+ /**
633
+ * Densifies a polyline (closed or open) to a step.
634
+ *
635
+ * @param {readonly Point[]} pts The polyline.
636
+ * @param {number} [step=6] The spacing.
637
+ * @param {boolean} [closed=false] Whether it closes.
638
+ * @returns {Point[]} The dense polyline.
639
+ */
640
+ export const dense = (pts, step = 6, closed = false) => {
641
+ const out = [];
642
+ const m = closed ? pts.length : pts.length - 1;
643
+ for (let i = 0; i < m; i++) { const a = pts[i], b = pts[(i + 1) % pts.length], n = Math.max(1, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1]) / step)); for (let k = 0; k < n; k++) out.push([a[0] + ((b[0] - a[0]) * k) / n, a[1] + ((b[1] - a[1]) * k) / n]); }
644
+ if (!closed) out.push(pts.at(-1));
645
+ return dedupe(out);
646
+ };
647
+
648
+ /**
649
+ * Woven letters: bands over and under where they cross.
650
+ *
651
+ * @param {readonly [readonly Point[], boolean?][]} strands Each strand's points and whether it closes.
652
+ * @param {number} [w] The band (LBW).
653
+ * @param {number} [g] The gap at a crossing (LBG).
654
+ * @returns {Shape} The letters.
655
+ */
656
+ export const linked = (strands, w = LBW, g = LBG) => weave(strands.map(([pts, closed]) => ({ pts: dense(pts, 6, !!closed), closed: !!closed })), w, g);
657
+
658
+ /* ---------- a finished shape ---------- */
659
+
660
+ /**
661
+ * Drops the zero-width leftovers polygon-clipping can leave where square-ended strokes meet at a sharp turn: spike
662
+ * vertices (an edge running straight back along the one before it) and rings with next to no area. Nothing visible
663
+ * changes. Every ring comes back closed (its last point repeating the first).
664
+ *
665
+ * @param {Shape} shape The shape.
666
+ * @returns {Shape} It, cleaned.
667
+ */
668
+ export const clean = (shape) => {
669
+ const area = (r) => r.reduce((a, p, i) => { const q = r[(i + 1) % r.length]; return a + p[0] * q[1] - q[0] * p[1]; }, 0) / 2;
670
+ const despike = (ring) => {
671
+ let r = ring.slice();
672
+ if (r.length > 1 && Math.hypot(r[0][0] - r.at(-1)[0], r[0][1] - r.at(-1)[1]) < 1e-9) r.pop();
673
+ for (let changed = true; changed && r.length > 3; ) {
674
+ changed = false;
675
+ for (let i = 0; i < r.length && r.length > 3; i++) {
676
+ const a = r[(i - 1 + r.length) % r.length], b = r[i], c = r[(i + 1) % r.length];
677
+ const u = [a[0] - b[0], a[1] - b[1]], v = [c[0] - b[0], c[1] - b[1]], lu = Math.hypot(...u), lv = Math.hypot(...v);
678
+ if (lu < 0.05 || lv < 0.05 || (u[0] * v[0] + u[1] * v[1]) / (lu * lv) > 0.998) { r.splice(i, 1); changed = true; i--; }
679
+ }
680
+ }
681
+ return [...r, r[0]];
682
+ };
683
+ return shape.map((pg) => pg.map(despike).filter((r) => r.length >= 4 && Math.abs(area(r)) > 1)).filter((pg) => pg.length && Math.abs(area(pg[0])) > 1);
684
+ };