@logomarks/core 0.1.11 → 0.2.1

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 (114) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -2
  3. package/package.json +47 -4
  4. package/src/analyse/index.d.ts +1 -0
  5. package/src/analyse/index.js +3 -2
  6. package/src/analyse/optical.d.ts +20 -0
  7. package/src/analyse/optical.js +76 -0
  8. package/src/edit/cells.d.ts +13 -0
  9. package/src/edit/cells.js +26 -0
  10. package/src/edit/index.d.ts +3 -1
  11. package/src/edit/index.js +4 -2
  12. package/src/edit/pixelate.d.ts +2 -0
  13. package/src/edit/pixelate.js +15 -0
  14. package/src/font/glyphs.d.ts +31 -0
  15. package/src/font/glyphs.js +228 -0
  16. package/src/font/index.d.ts +10 -0
  17. package/src/font/index.js +10 -0
  18. package/src/font/measure.d.ts +16 -0
  19. package/src/font/measure.js +201 -0
  20. package/src/font/moves.d.ts +42 -0
  21. package/src/font/moves.js +263 -0
  22. package/src/forms/discs.d.ts +20 -0
  23. package/src/forms/discs.js +114 -0
  24. package/src/forms/emblems.d.ts +22 -0
  25. package/src/forms/emblems.js +109 -0
  26. package/src/forms/flow.d.ts +21 -0
  27. package/src/forms/flow.js +80 -0
  28. package/src/forms/folds.d.ts +18 -0
  29. package/src/forms/folds.js +147 -0
  30. package/src/forms/geo.d.ts +46 -0
  31. package/src/forms/geo.js +233 -0
  32. package/src/forms/hooks.d.ts +40 -0
  33. package/src/forms/hooks.js +204 -0
  34. package/src/forms/index.d.ts +22 -0
  35. package/src/forms/index.js +24 -0
  36. package/src/forms/jog.d.ts +33 -0
  37. package/src/forms/jog.js +162 -0
  38. package/src/forms/lines.d.ts +32 -0
  39. package/src/forms/lines.js +233 -0
  40. package/src/forms/ref.d.ts +41 -0
  41. package/src/forms/ref.js +246 -0
  42. package/src/forms/shatter.d.ts +40 -0
  43. package/src/forms/shatter.js +341 -0
  44. package/src/forms/slats.d.ts +35 -0
  45. package/src/forms/slats.js +78 -0
  46. package/src/forms/swash.d.ts +23 -0
  47. package/src/forms/swash.js +74 -0
  48. package/src/forms/tech.d.ts +40 -0
  49. package/src/forms/tech.js +293 -0
  50. package/src/forms/tracks.d.ts +36 -0
  51. package/src/forms/tracks.js +121 -0
  52. package/src/index.js +2 -1
  53. package/src/iso/index.d.ts +5 -0
  54. package/src/iso/index.js +13 -0
  55. package/src/iso/objects.d.ts +168 -0
  56. package/src/iso/objects.js +1042 -0
  57. package/src/iso/planes.d.ts +27 -0
  58. package/src/iso/planes.js +138 -0
  59. package/src/iso/voxels.d.ts +43 -0
  60. package/src/iso/voxels.js +255 -0
  61. package/src/letters/blackletter.d.ts +31 -0
  62. package/src/letters/blackletter.js +199 -0
  63. package/src/letters/dotstroke.d.ts +16 -0
  64. package/src/letters/dotstroke.js +77 -0
  65. package/src/letters/echo.d.ts +42 -0
  66. package/src/letters/echo.js +246 -0
  67. package/src/letters/frame.d.ts +6 -0
  68. package/src/letters/frame.js +14 -0
  69. package/src/letters/index.d.ts +16 -0
  70. package/src/letters/index.js +18 -0
  71. package/src/letters/inktrap.d.ts +66 -0
  72. package/src/letters/inktrap.js +244 -0
  73. package/src/letters/mono.d.ts +145 -0
  74. package/src/letters/mono.js +684 -0
  75. package/src/letters/pillar.d.ts +31 -0
  76. package/src/letters/pillar.js +257 -0
  77. package/src/letters/serif.d.ts +109 -0
  78. package/src/letters/serif.js +666 -0
  79. package/src/letters/stair.d.ts +8 -0
  80. package/src/letters/stair.js +43 -0
  81. package/src/letters/stem.d.ts +29 -0
  82. package/src/letters/stem.js +205 -0
  83. package/src/objects/animals.d.ts +105 -0
  84. package/src/objects/animals.js +469 -0
  85. package/src/objects/bio.d.ts +14 -0
  86. package/src/objects/bio.js +76 -0
  87. package/src/objects/devglyphs.d.ts +34 -0
  88. package/src/objects/devglyphs.js +226 -0
  89. package/src/objects/devices.d.ts +31 -0
  90. package/src/objects/devices.js +114 -0
  91. package/src/objects/faces.d.ts +94 -0
  92. package/src/objects/faces.js +344 -0
  93. package/src/objects/food.d.ts +12 -0
  94. package/src/objects/food.js +74 -0
  95. package/src/objects/index.d.ts +13 -0
  96. package/src/objects/index.js +18 -0
  97. package/src/objects/rosettes.d.ts +14 -0
  98. package/src/objects/rosettes.js +64 -0
  99. package/src/objects/sport.d.ts +8 -0
  100. package/src/objects/sport.js +37 -0
  101. package/src/objects/weapons.d.ts +31 -0
  102. package/src/objects/weapons.js +179 -0
  103. package/src/pixels/dither.d.ts +78 -0
  104. package/src/pixels/dither.js +399 -0
  105. package/src/pixels/emote.d.ts +6 -0
  106. package/src/pixels/emote.js +76 -0
  107. package/src/pixels/index.d.ts +7 -0
  108. package/src/pixels/index.js +8 -0
  109. package/src/sdf/index.d.ts +2 -0
  110. package/src/sdf/index.js +2 -0
  111. package/src/sdf/y2k.d.ts +12 -0
  112. package/src/sdf/y2k.js +80 -0
  113. package/src/util/vec.d.ts +14 -0
  114. package/src/util/vec.js +61 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Modern blackletter, drawn the way the hand draws it: one broad nib held at 30°. A stroke is the nib swept along a
3
+ * polyline (`nib()`), so the face's weights come from one rule: uprights and falling (↘) diagonals are heavy, bars a little
4
+ * lighter, rising (↗) strokes the hairline, and every free end is cut on the nib. Bowls are pointed (straight runs joined
5
+ * at 45°, never curves). On top of that each letter takes one or two nib features: a flag head and lozenge foot
6
+ * (`minim()`), a split stem (`splitHead()`, `splitFoot()`), or a lozenge, parted off by the gap (`parted()`) or set in as a
7
+ * bar or a terminal. Letters are drawn on a cap height of 440 (y down, origin at the cap's left); `blackLetter()` centres
8
+ * one. From the Logomarks tools (gothicparts.mjs, batch 231; batch 240 added F, J, L and P).
9
+ */
10
+ import { U, D, poly, rect, move, bbox } from '../geometry/index.js';
11
+
12
+ /* ---------- the nib ---------- */
13
+
14
+ /**
15
+ * The face's nib: its width W, its thickness TH (the hairline), its angle DEG (rising, in degrees from the horizontal)
16
+ * and the cap height CAP every letter is drawn to. An upright comes out W·cos 30° + TH·sin 30° ≈ 104 wide, a bar ≈ 76 deep.
17
+ */
18
+ export const NIB = { W: 104, TH: 28, DEG: 30, CAP: 440 };
19
+
20
+ /**
21
+ * The nib as a rectangle about the origin: its long axis rising at `deg`, `w` long and `th` thick.
22
+ *
23
+ * @param {{ w?: number, th?: number, deg?: number }} [o] The nib (NIB's by default).
24
+ * @returns {import('../geometry/types.js').Point[]} Its four corners.
25
+ */
26
+ const nibRect = ({ w = NIB.W, th = NIB.TH, deg = NIB.DEG } = {}) => {
27
+ const a = (deg * Math.PI) / 180, n = [(Math.cos(a) * w) / 2, (-Math.sin(a) * w) / 2], m = [(Math.sin(a) * th) / 2, (Math.cos(a) * th) / 2];
28
+ return [[n[0] + m[0], n[1] + m[1]], [n[0] - m[0], n[1] - m[1]], [-n[0] - m[0], -n[1] - m[1]], [-n[0] + m[0], -n[1] + m[1]]];
29
+ };
30
+
31
+ /**
32
+ * Points moved by (x, y).
33
+ *
34
+ * @param {readonly (readonly number[])[]} q The points.
35
+ * @param {readonly number[]} p The offset.
36
+ * @returns {import('../geometry/types.js').Point[]} The moved points.
37
+ */
38
+ const at = (q, [x, y]) => q.map(([a, b]) => [a + x, b + y]);
39
+
40
+ /**
41
+ * The nib swept along a polyline: the Minkowski sum of each segment with the nib, as the nib at both ends plus the
42
+ * parallelogram each nib edge sweeps (that union is the whole sum for a convex nib), all segments united.
43
+ *
44
+ * @param {readonly (readonly number[])[]} pts The polyline.
45
+ * @param {{ w?: number, th?: number, deg?: number }} [o] The nib (NIB's by default).
46
+ * @returns {import('../geometry/types.js').Shape} The stroke.
47
+ */
48
+ export const nib = (pts, o = {}) => {
49
+ const q = nibRect(o), parts = [];
50
+ pts.forEach((p, i) => {
51
+ parts.push(poly(at(q, p)));
52
+ if (i === 0) return;
53
+ const a = pts[i - 1];
54
+ q.forEach((e, k) => { const f = q[(k + 1) % 4]; parts.push(poly([[a[0] + e[0], a[1] + e[1]], [a[0] + f[0], a[1] + f[1]], [p[0] + f[0], p[1] + f[1]], [p[0] + e[0], p[1] + e[1]]])); });
55
+ });
56
+ return U(...parts);
57
+ };
58
+
59
+ /**
60
+ * Several nib strokes united.
61
+ *
62
+ * @param {readonly (readonly (readonly number[])[])[]} list The polylines.
63
+ * @param {{ w?: number, th?: number, deg?: number }} [o] The nib (NIB's by default).
64
+ * @returns {import('../geometry/types.js').Shape} The strokes.
65
+ */
66
+ export const nibs = (list, o = {}) => U(...list.map((pts) => nib(pts, o)));
67
+
68
+ /**
69
+ * A lozenge: a square standing on its corner, `s` from corner to corner, centred on (x, y).
70
+ *
71
+ * @param {number} x Its centre.
72
+ * @param {number} y Its centre.
73
+ * @param {number} s Corner to corner.
74
+ * @returns {import('../geometry/types.js').Polygon} The lozenge.
75
+ */
76
+ export const lozenge = (x, y, s) => poly([[x, y - s / 2], [x + s / 2, y], [x, y + s / 2], [x - s / 2, y]]);
77
+
78
+ /**
79
+ * A cut g wide along the line through a and b, run far past both.
80
+ *
81
+ * @param {readonly number[]} a A point on the line.
82
+ * @param {readonly number[]} b Another.
83
+ * @param {number} g The cut's width.
84
+ * @returns {import('../geometry/types.js').Polygon} The cut.
85
+ */
86
+ export const cutLine = (a, b, g) => {
87
+ const d = [b[0] - a[0], b[1] - a[1]], L = Math.hypot(...d), u = [d[0] / L, d[1] / L], v = [(-u[1] * g) / 2, (u[0] * g) / 2], F = 3000;
88
+ const p = [a[0] - u[0] * F, a[1] - u[1] * F], r = [a[0] + u[0] * F, a[1] + u[1] * F];
89
+ return poly([[p[0] + v[0], p[1] + v[1]], [r[0] + v[0], r[1] + v[1]], [r[0] - v[0], r[1] - v[1]], [p[0] - v[0], p[1] - v[1]]]);
90
+ };
91
+
92
+ /**
93
+ * A split head: a slit g wide down an upright at x from above the cap to depth y, so the stem forks into two prongs.
94
+ *
95
+ * @param {number} x The upright's middle.
96
+ * @param {number} y How far down the slit runs.
97
+ * @param {number} g The slit's width.
98
+ * @returns {import('../geometry/types.js').Polygon} The slit, to cut.
99
+ */
100
+ export const splitHead = (x, y, g) => rect(x - g / 2, -400, g, 400 + y);
101
+
102
+ /**
103
+ * A split foot: the slit up from below the base to height y.
104
+ *
105
+ * @param {number} x The upright's middle.
106
+ * @param {number} y How far up the slit runs.
107
+ * @param {number} g The slit's width.
108
+ * @returns {import('../geometry/types.js').Polygon} The slit, to cut.
109
+ */
110
+ export const splitFoot = (x, y, g) => rect(x - g / 2, y, g, 1200);
111
+
112
+ /* ---------- letters ---------- */
113
+
114
+ /**
115
+ * A minim: an upright from y0 to y1, with the nib's flag head (a short ↘ entry from the upper left) and lozenge foot (a
116
+ * short ↘ exit to the lower right).
117
+ *
118
+ * @param {number} x The upright's x.
119
+ * @param {number} y0 Its top.
120
+ * @param {number} y1 Its foot.
121
+ * @param {{ head?: boolean, foot?: boolean, d?: number }} [o] Whether it has its head and foot (both by default), and how
122
+ * far each runs (40).
123
+ * @returns {import('../geometry/types.js').Point[]} The polyline, for nib().
124
+ */
125
+ export const minim = (x, y0, y1, { head = true, foot = true, d = 40 } = {}) =>
126
+ [...(head ? [[x - d, y0 - d]] : []), [x, y0], [x, y1], ...(foot ? [[x + d, y1 + d]] : [])];
127
+
128
+ /**
129
+ * A lozenge parted from the ink: the ink less the lozenge grown by g, plus the lozenge. Run the stroke it ends into the
130
+ * lozenge, so the moat cuts it right across (a stroke that stops short leaves a hooked tip where its nib-cut end meets the
131
+ * moat).
132
+ *
133
+ * @param {import('../geometry/types.js').Geom} ink The letter.
134
+ * @param {import('../geometry/types.js').Polygon} z The lozenge (from lozenge()).
135
+ * @param {number} g The gap.
136
+ * @returns {import('../geometry/types.js').Shape} The letter with its lozenge parted off.
137
+ */
138
+ export const parted = (ink, z, g) => U(D(ink, grownLozenge(z, g)), z);
139
+
140
+ /**
141
+ * A lozenge grown by g all round (its moat).
142
+ *
143
+ * @param {import('../geometry/types.js').Polygon} z The lozenge.
144
+ * @param {number} g The gap.
145
+ * @returns {import('../geometry/types.js').Polygon} The grown lozenge.
146
+ */
147
+ const grownLozenge = (z, g) => { const [[x, y0], , [, y1]] = z[0], s = y1 - y0 + 2 * g * Math.SQRT2; return lozenge(x, (y0 + y1) / 2, s); };
148
+
149
+ /**
150
+ * The capitals, each `(g) => shape` on the 440 cap (lozenges float a gap clear of the strokes): A B C D E F G H J K L M N
151
+ * P R S T.
152
+ */
153
+ export const BLACKLETTERS = {
154
+ // A: a pointed arch: a thick upright rising to a hairline ↗ shoulder, the peak, the thick ↘ shoulder and the right upright
155
+ // ending in a lozenge foot; a bar across.
156
+ A: () => nibs([[[50, 440], [50, 160], [170, 30], [290, 150], [290, 400], [330, 440]], [[50, 320], [290, 250]]]),
157
+ // B: an upright with a flag head and two pointed bowls (each falls ↘ heavy and returns ↙ as the hairline), the upright split
158
+ // from its head to the lower bowl.
159
+ B: (g) => D(nibs([minim(52, 40, 410, { foot: false }), [[52, 40], [180, 40], [250, 110], [250, 140], [190, 210], [52, 210]], [[52, 210], [200, 210], [270, 280], [270, 330], [190, 410], [52, 410]]]), splitHead(52, 330, g)),
160
+ // C: a broken round (a thick back, the hairline ↗ shoulder, the thick ↘ heel), its top ending in a lozenge parted off.
161
+ C: (g) => parted(nibs([[[272, 46], [250, 40], [140, 40], [52, 128], [52, 330], [132, 410], [270, 410]]]), lozenge(272, 50, 120), g),
162
+ // D: an upright with a flag head, split at the top, and one pointed bowl.
163
+ D: (g) => D(nibs([minim(52, 40, 410, { foot: false }), [[52, 40], [170, 40], [270, 140], [270, 320], [180, 410], [52, 410]]]), splitHead(52, 170, g)),
164
+ // E: the broken round, its middle arm a lozenge parted off the back.
165
+ E: (g) => parted(nibs([[[270, 40], [140, 40], [52, 128], [52, 330], [132, 410], [270, 410]]]), lozenge(170, 225, 150), g),
166
+ // G: the broken round with a jaw rising to a spur, its top ending in a lozenge parted off.
167
+ G: (g) => parted(nibs([[[272, 46], [250, 40], [140, 40], [52, 128], [52, 330], [132, 410], [270, 410], [270, 250], [190, 250]]]), lozenge(272, 50, 120), g),
168
+ // H: two minims, their bar a lozenge set across them.
169
+ H: () => U(nibs([minim(52, 40, 400), minim(268, 40, 400)]), lozenge(160, 226, 200)),
170
+ // J: a minim falling to a foot that turns back left and runs into a lozenge parted off.
171
+ J: (g) => parted(nibs([minim(190, 40, 330, { foot: false }), [[190, 330], [120, 410], [30, 410], [14, 404]]]), lozenge(14, 400, 120), g),
172
+ // L: an upright split at its head, its foot bar running into a lozenge parted off.
173
+ L: (g) => parted(D(nibs([minim(52, 40, 410, { foot: false }), [[52, 410], [262, 410], [284, 404]]]), splitHead(52, 170, g)), lozenge(284, 400, 120), g),
174
+ // P: B's top bowl on an upright with a flag head and a lozenge foot, split at its head (R splits its foot).
175
+ P: (g) => D(nibs([minim(52, 40, 400), [[52, 40], [180, 40], [250, 110], [250, 160], [180, 236], [52, 236]]]), splitHead(52, 150, g)),
176
+ // F: an upright with a flag head and a lozenge foot, a top bar, its middle arm a lozenge parted off.
177
+ F: (g) => parted(nibs([minim(52, 40, 400), [[52, 40], [260, 40]], [[52, 225], [150, 225]]]), lozenge(170, 225, 150), g),
178
+ // K: an upright, the hairline arm rising ↗ from its middle, the thick leg falling ↘.
179
+ K: () => nibs([minim(52, 40, 400), [[80, 250], [270, 30]], [[140, 200], [260, 380], [300, 420]]]),
180
+ // M: three uprights under two pointed arches, each foot a lozenge.
181
+ M: () => nibs([[[44, 440], [44, 100], [114, 30], [184, 100], [184, 400], [224, 440]], [[184, 100], [254, 30], [324, 100], [324, 400], [364, 440]]]),
182
+ // N: two uprights joined by the thick ↘ diagonal.
183
+ N: () => nibs([minim(52, 40, 410, { foot: false }), [[52, 40], [268, 400]], minim(268, 40, 400, { head: false })]),
184
+ // R: B's top bowl over a leg falling ↘ to a lozenge foot, the upright split at its foot.
185
+ R: (g) => D(nibs([minim(52, 40, 440, { foot: false }), [[52, 40], [180, 40], [250, 110], [250, 150], [190, 226], [52, 226]], [[150, 226], [250, 380], [290, 420]]]), splitFoot(52, 300, g)),
186
+ // S: the angular S (a top bar, the heavy spine falling ↘ between two short uprights, a foot bar), each end a lozenge parted off.
187
+ S: (g) => parted(parted(nibs([[[272, 46], [250, 40], [130, 40], [50, 120], [50, 170], [260, 290], [260, 330], [180, 410], [40, 410], [18, 404]]]), lozenge(272, 50, 120), g), lozenge(18, 400, 120), g),
188
+ // T: a bar ending in two lozenges over a minim.
189
+ T: () => U(nibs([[[60, 50], [260, 50]], minim(160, 50, 400, { head: false })]), lozenge(20, 50, 130), lozenge(300, 50, 130)),
190
+ };
191
+
192
+ /**
193
+ * A blackletter capital drawn with the gap g, centred on (256, 256).
194
+ *
195
+ * @param {string} k The letter (a BLACKLETTERS key).
196
+ * @param {number} g The gap.
197
+ * @returns {import('../geometry/types.js').Shape} The letter.
198
+ */
199
+ export const blackLetter = (k, g) => { const s = BLACKLETTERS[k](g), [x0, y0, x1, y1] = bbox(s); return move(s, 256 - (x0 + x1) / 2, 256 - (y0 + y1) / 2); };
@@ -0,0 +1,16 @@
1
+ import type { Geom, Polygon, Shape } from '../geometry/types.js';
2
+
3
+ /** The stem width: 100. */
4
+ export const DOT_S: number;
5
+ /** The disc's radius: 56. */
6
+ export const DOT_R: number;
7
+ /** The band between two lines (cap line and baseline by default), wider than the canvas. */
8
+ export function dotBand(y0?: number, y1?: number): Polygon;
9
+ /** A straight stroke a → b, w wide square to it, run ext past both ends. */
10
+ export function stroke(a: readonly number[], b: readonly number[], w?: number, ext?: number): Polygon;
11
+ /** A diagonal from (xt, y0) to (xb, y1), w wide square to it, its ends flat on the lines. */
12
+ export function diag(xt: number, xb: number, y0?: number, y1?: number, w?: number): Shape;
13
+ /** The disc. */
14
+ export function dot(cx: number, cy: number, r?: number): Polygon;
15
+ /** The disc at (cx, cy) and the rest of the letter cut free of it by the gap g. */
16
+ export function withDot(rest: Shape | Geom[], cx: number, cy: number, g: number, r?: number): Shape;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Dot Stroke letters: a bold, square-ended letter, numeral or simple shape with ONE of its strokes replaced by ONE disc
3
+ * where that stroke would end or stand. Stems are DOT_S wide, diagonals the same width square to the stroke and clipped
4
+ * flat to the cap line and baseline (the 448 frame's T and B), the disc DOT_R in radius (1.12 of a stem across: about the
5
+ * stroke width or a touch larger), and the rest of the letter cut free of the disc by the gap (`withDot()`).
6
+ * From the Logomarks tools (dotstrokeparts.mjs, batches 331 and 333).
7
+ */
8
+ import { U, D, I, rect, circle, poly } from '../geometry/index.js';
9
+ import { T, B } from './frame.js';
10
+
11
+ /** The stem width: 100. */
12
+ export const DOT_S = 100;
13
+
14
+ /** The disc's radius: 56 (its diameter 1.12 of a stem). */
15
+ export const DOT_R = 56;
16
+
17
+ /**
18
+ * The band between two lines (the cap line and the baseline by default): every diagonal is clipped to it, so its ends sit
19
+ * flat on the lines.
20
+ *
21
+ * @param {number} [y0] The top line (T).
22
+ * @param {number} [y1] The bottom line (B).
23
+ * @returns {import('../geometry/types.js').Polygon} A rectangle wider than the canvas.
24
+ */
25
+ export const dotBand = (y0 = T, y1 = B) => rect(-400, y0, 1312, y1 - y0);
26
+
27
+ /**
28
+ * A straight stroke from a to b, w wide measured square to it, run `ext` past both ends (clip it with `dotBand()` or a
29
+ * union).
30
+ *
31
+ * @param {readonly number[]} a The start of the centre line.
32
+ * @param {readonly number[]} b The end of the centre line.
33
+ * @param {number} [w] The width, square to the stroke (DOT_S).
34
+ * @param {number} [ext=0] How far it runs on past both ends.
35
+ * @returns {import('../geometry/types.js').Polygon} The stroke.
36
+ */
37
+ export const stroke = (a, b, w = DOT_S, ext = 0) => {
38
+ const dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L,
39
+ nx = -uy * w / 2, ny = ux * w / 2, A = [a[0] - ux * ext, a[1] - uy * ext], Bp = [b[0] + ux * ext, b[1] + uy * ext];
40
+ return poly([[A[0] + nx, A[1] + ny], [Bp[0] + nx, Bp[1] + ny], [Bp[0] - nx, Bp[1] - ny], [A[0] - nx, A[1] - ny]]);
41
+ };
42
+
43
+ /**
44
+ * A diagonal between two lines: its centre line from (xt, y0) to (xb, y1), w wide square to it, its ends flat on the lines.
45
+ *
46
+ * @param {number} xt The centre line's x on the top line.
47
+ * @param {number} xb The centre line's x on the bottom line.
48
+ * @param {number} [y0] The top line (T).
49
+ * @param {number} [y1] The bottom line (B).
50
+ * @param {number} [w] The width, square to the stroke (DOT_S).
51
+ * @returns {import('../geometry/types.js').Shape} The diagonal.
52
+ */
53
+ export const diag = (xt, xb, y0 = T, y1 = B, w = DOT_S) => I(stroke([xt, y0], [xb, y1], w, 200), dotBand(y0, y1));
54
+
55
+ /**
56
+ * The disc.
57
+ *
58
+ * @param {number} cx Its centre's x.
59
+ * @param {number} cy Its centre's y.
60
+ * @param {number} [r] Its radius (DOT_R).
61
+ * @returns {import('../geometry/types.js').Polygon} The disc.
62
+ */
63
+ export const dot = (cx, cy, r = DOT_R) => circle(cx, cy, r, 120);
64
+
65
+ /**
66
+ * The family's one move: the disc at (cx, cy), and the rest of the letter cut free of it by the gap g where it comes
67
+ * nearer.
68
+ *
69
+ * @param {import('../geometry/types.js').Shape | import('../geometry/types.js').Geom[]} rest The rest of the letter (a
70
+ * shape, or a list of shapes and polygons, united).
71
+ * @param {number} cx The disc's centre's x.
72
+ * @param {number} cy The disc's centre's y.
73
+ * @param {number} g The gap.
74
+ * @param {number} [r] The disc's radius (DOT_R).
75
+ * @returns {import('../geometry/types.js').Shape} The mark.
76
+ */
77
+ export const withDot = (rest, cx, cy, g, r = DOT_R) => U(dot(cx, cy, r), D(U(...[rest].flat()), circle(cx, cy, r + g, 120)));
@@ -0,0 +1,42 @@
1
+ import type { Geom, Shape } from '../geometry/types.js';
2
+
3
+ /** An Echo letter: its cap height, width, shape and keep-out. */
4
+ export interface EchoLetter {
5
+ H: number;
6
+ W: number;
7
+ shape: Shape;
8
+ /** Every stroke widened by the gap: where an echo may not go. */
9
+ keepOut: (gap: number) => Shape;
10
+ }
11
+ /** A stroke part: its solid and its keep-out. */
12
+ export interface EchoPart {
13
+ s: Geom;
14
+ k: (gap: number) => Geom;
15
+ }
16
+ /** A stroke's centreline: its two ends. */
17
+ export type EchoLine = number[][];
18
+
19
+ /** How far a stroke runs on before it is clipped: 4000. */
20
+ export const BIG: number;
21
+ /** A letter: its parts united and clipped to its box. */
22
+ export function echoLetter(H: number, W: number, parts: readonly EchoPart[], x0?: number): EchoLetter;
23
+ /** The echo of stroke [a, b]: the same stroke set a gap off it toward `to`, cut back clear of every stroke. */
24
+ export function echo(L: EchoLetter, ab: readonly (readonly number[])[], to: readonly number[], gap: number, o?: { w?: number; y0?: number; y1?: number; x0?: number; x1?: number }): Shape;
25
+ /** A mark drawn at the cap height that makes its longest side span. */
26
+ export function fitH(draw: (H: number) => Shape, span?: number): Shape;
27
+ /** A letter and its echo at the size that makes the stroke 104, as a build for exact(). */
28
+ export function mark<K extends { L: EchoLetter }>(make: (H: number) => K, ech: (k: K, gap: number) => Shape, air?: boolean): (g: number) => Shape;
29
+ /** K: stem, arm and leg. */
30
+ export function K_(H: number): { arm: EchoLine; leg: EchoLine; L: EchoLetter };
31
+ /** R: stem, round bowl and leg. */
32
+ export function R_(H: number): { leg: EchoLine; L: EchoLetter };
33
+ /** Y: two arms into a stem. */
34
+ export function Y_(H: number): { r: EchoLine; j: number[]; L: EchoLetter };
35
+ /** Seven: the bar and the diagonal. */
36
+ export function Seven_(H: number): { d: EchoLine; W: number; L: EchoLetter };
37
+ /** Q: a ring and a tail. */
38
+ export function Q_(H: number): { d: EchoLine; L: EchoLetter };
39
+ /** Lowercase k. */
40
+ export function k_(H: number): { leg: EchoLine; arm: EchoLine; L: EchoLetter };
41
+ /** Lowercase y. */
42
+ export function y_(H: number): { long: EchoLine; xh: number; L: EchoLetter };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Echo letters: heavy geometric letters (stroke 104, flat terminals) built from stroke parts with keep-outs, and the echo
3
+ * of one stroke set off a gap beside it. Each part is { s, k }: its solid, and its keep-out (the solid widened by a gap),
4
+ * so an echo stops parallel to every stroke it meets. `mark()` builds a letter and its echo at the size that makes the
5
+ * stroke 104 once fitted.
6
+ * From the Logomarks tools (echoparts.mjs, batches 202 and 220).
7
+ */
8
+ import { D, I, U, bar, circle, grow, stroke, sample, bbox } from '../geometry/index.js';
9
+ import { despeckShape } from '../parts/index.js';
10
+ import { box } from '../util/vec.js';
11
+
12
+ /* ================================================================ geometry ================================================================ */
13
+
14
+ /** The stroke. */
15
+ const T = 104;
16
+
17
+ /** How far a stroke runs on before it is clipped: 4000, past any canvas. */
18
+ export const BIG = 4000;
19
+
20
+ /**
21
+ * The unit vector from a to b.
22
+ *
23
+ * @param {readonly number[]} a From.
24
+ * @param {readonly number[]} b To.
25
+ * @returns {number[]} The direction.
26
+ */
27
+ const unit = (a, b) => { const L = Math.hypot(b[0] - a[0], b[1] - a[1]); return [(b[0] - a[0]) / L, (b[1] - a[1]) / L]; };
28
+
29
+ /**
30
+ * A straight stroke w wide along a→b, run on by e0 before a and e1 past b (run a stroke past the cap or base, then clip it).
31
+ *
32
+ * @param {readonly number[]} a From.
33
+ * @param {readonly number[]} b To.
34
+ * @param {number} [w] The width (104).
35
+ * @param {number} [e0=0] The run before a.
36
+ * @param {number} [e1=0] The run past b.
37
+ * @returns {import('../geometry/types.js').Polygon} The stroke.
38
+ */
39
+ const seg = (a, b, w = T, e0 = 0, e1 = 0) => { const u = unit(a, b); return bar([a[0] - u[0] * e0, a[1] - u[1] * e0], [b[0] + u[0] * e1, b[1] + u[1] * e1], w); };
40
+
41
+ /**
42
+ * A ring's area.
43
+ *
44
+ * @param {readonly (readonly number[])[]} r The ring.
45
+ * @returns {number} Its area.
46
+ */
47
+ const area = (r) => Math.abs(r.reduce((s, p, i) => { const q = r[(i + 1) % r.length]; return s + p[0] * q[1] - q[0] * p[1]; }, 0)) / 2;
48
+
49
+ /**
50
+ * The largest piece of a shape (an echo cut by another stroke keeps its main run, not the scrap beyond).
51
+ *
52
+ * @param {import('../geometry/types.js').Shape} s The shape.
53
+ * @returns {import('../geometry/types.js').Shape} Its largest polygon.
54
+ */
55
+ const largest = (s) => (s.length ? [s.reduce((a, b) => (area(b[0]) > area(a[0]) ? b : a))] : s);
56
+
57
+ /**
58
+ * A diagonal's centreline from two anchors: k = +1 puts the stroke's left edge on the anchor, −1 its right edge, 0 its
59
+ * centre (the horizontal half-width is T / 2 over the slope's sine). So a stroke can run edge to corner, as a geometric Z
60
+ * or X does.
61
+ *
62
+ * @param {readonly number[]} p The top anchor.
63
+ * @param {readonly number[]} q The bottom anchor.
64
+ * @param {number} [kp=0] Which edge sits on p.
65
+ * @param {number} [kq=0] Which edge sits on q.
66
+ * @param {number} [w] The stroke (104).
67
+ * @returns {readonly number[][]} The centreline's two ends.
68
+ */
69
+ const diag = (p, q, kp = 0, kq = 0, w = T) => {
70
+ let a = p, b = q;
71
+ for (let i = 0; i < 4; i++) { const Th = (w * Math.hypot(b[0] - a[0], b[1] - a[1])) / Math.abs(b[1] - a[1]); a = [p[0] + (kp * Th) / 2, p[1]]; b = [q[0] + (kq * Th) / 2, q[1]]; }
72
+ return [a, b];
73
+ };
74
+
75
+ /**
76
+ * A stroke part: its solid and its keep-out (the stroke widened by a gap, so an echo stops parallel to it). k0, k1 set
77
+ * the keep-out's run past either end (0 where the stroke ends flush in another, as a 7's bar does in its diagonal).
78
+ *
79
+ * @param {readonly number[]} a From.
80
+ * @param {readonly number[]} b To.
81
+ * @param {number} [e0=0] The run before a.
82
+ * @param {number} [e1=0] The run past b.
83
+ * @param {number} [w] The stroke (104).
84
+ * @param {{ k0?: number | null, k1?: number | null }} [o] The keep-out's runs.
85
+ * @returns {{ s: import('../geometry/types.js').Polygon, k: (gap: number) => import('../geometry/types.js').Polygon, a: readonly number[], b: readonly number[] }} The part.
86
+ */
87
+ const st = (a, b, e0 = 0, e1 = 0, w = T, { k0 = null, k1 = null } = {}) =>
88
+ ({ s: seg(a, b, w, e0, e1), k: (gap) => seg(a, b, w + 2 * gap, k0 ?? e0 + gap, k1 ?? e1 + gap), a, b });
89
+
90
+ /**
91
+ * A solid part: any shape, its keep-out the shape grown by the gap.
92
+ *
93
+ * @param {import('../geometry/types.js').Geom} shape The shape.
94
+ * @returns {{ s: import('../geometry/types.js').Geom, k: (gap: number) => import('../geometry/types.js').Shape }} The part.
95
+ */
96
+ const solid = (shape) => ({ s: shape, k: (gap) => grow(shape, gap) });
97
+
98
+ /**
99
+ * A part kept inside a box (an arm cut flat on the x-height, a diagonal stopped inside the bar it lands on).
100
+ *
101
+ * @param {{ s: import('../geometry/types.js').Geom, k: (gap: number) => import('../geometry/types.js').Geom }} q The part.
102
+ * @param {readonly number[]} box The box [x0, y0, x1, y1].
103
+ * @returns {{ s: import('../geometry/types.js').Shape, k: (gap: number) => import('../geometry/types.js').Shape }} The part, clipped.
104
+ */
105
+ const clip = (q, [x0, y0, x1, y1]) => ({ ...q, s: I(q.s, box(x0, y0, x1, y1)), k: (gap) => I(q.k(gap), box(x0 - gap, y0 - gap, x1 + gap, y1 + gap)) });
106
+
107
+ /**
108
+ * A letter: its parts united and clipped to its box (flat terminals on the cap, base and sides).
109
+ *
110
+ * @param {number} H The cap height.
111
+ * @param {number} W The width.
112
+ * @param {readonly { s: import('../geometry/types.js').Geom, k: (gap: number) => import('../geometry/types.js').Geom }[]} parts The parts.
113
+ * @param {number} [x0=0] The box's left edge.
114
+ * @returns {import('./echo.js').EchoLetter} The letter: H, W, its shape and its keep-out.
115
+ */
116
+ export const echoLetter = (H, W, parts, x0 = 0) => ({ H, W, shape: I(U(...parts.map((p) => p.s)), box(x0, 0, W, H)), keepOut: (gap) => U(...parts.map((p) => p.k(gap))) });
117
+
118
+ /**
119
+ * The echo of stroke a→b: the same stroke (w wide) set off to the side of point `to`, `gap` clear of it, run long,
120
+ * clipped to the box x0…x1, y0…y1 (default the cap and base lines), cut back `gap` clear of every stroke, and its main
121
+ * piece kept.
122
+ *
123
+ * @param {import('./echo.js').EchoLetter} L The letter.
124
+ * @param {readonly (readonly number[])[]} ab The stroke's centreline [a, b].
125
+ * @param {readonly number[]} to A point on the side the echo goes.
126
+ * @param {number} gap The gap.
127
+ * @param {{ w?: number, y0?: number, y1?: number, x0?: number, x1?: number }} [o] The echo's width and box.
128
+ * @returns {import('../geometry/types.js').Shape} The echo.
129
+ */
130
+ export const echo = (L, [a, b], to, gap, { w = T, y0 = 0, y1 = L.H, x0 = -BIG, x1 = BIG } = {}) => {
131
+ const u = unit(a, b);
132
+ let n = [-u[1], u[0]];
133
+ if ((to[0] - a[0]) * n[0] + (to[1] - a[1]) * n[1] < 0) n = [-n[0], -n[1]];
134
+ const d = T / 2 + gap + w / 2, off = (p) => [p[0] + n[0] * d, p[1] + n[1] * d];
135
+ return largest(despeckShape(D(I(seg(off(a), off(b), w, BIG, BIG), box(x0, y0, x1, y1)), L.keepOut(gap)), 400, 30));
136
+ };
137
+
138
+ /**
139
+ * Builds a mark at the cap height that makes its longest side `span` (the stroke and gaps stay fixed), by up to eight
140
+ * rounds of rescaling.
141
+ *
142
+ * @param {(H: number) => import('../geometry/types.js').Shape} draw The mark at cap height H.
143
+ * @param {number} [span=448] The longest side wanted.
144
+ * @returns {import('../geometry/types.js').Shape} The mark.
145
+ */
146
+ export const fitH = (draw, span = 448) => {
147
+ let H = 420, s;
148
+ for (let i = 0; i < 8; i++) { s = draw(H); const [x0, y0, x1, y1] = bbox(s), m = Math.max(x1 - x0, y1 - y0); if (Math.abs(m - span) < 0.2) break; H *= span / m; }
149
+ return s;
150
+ };
151
+
152
+ /**
153
+ * A mark: the letter from `make(H)` plus its echo from `ech(k, gap)`, at the size that makes the stroke 104.
154
+ *
155
+ * @template {{ L: import('./echo.js').EchoLetter }} K
156
+ * @param {(H: number) => K} make A letter builder (K_, R_, Y_…).
157
+ * @param {(k: K, gap: number) => import('../geometry/types.js').Shape} ech Its echo, from the letter's parts.
158
+ * @param {boolean} [air=false] A wider gap (40 for 22).
159
+ * @returns {(g: number) => import('../geometry/types.js').Shape} The build, for exact().
160
+ */
161
+ export const mark = (make, ech, air = false) => (g) => fitH((H) => { const k = make(H); return U(k.L.shape, ech(k, air ? (g * 40) / 22 : g)); });
162
+
163
+ /* ================================================================ letters ================================================================ */
164
+
165
+ /**
166
+ * K: stem, arm to the top right corner, leg from the arm to the bottom right corner. Echo of the arm or the leg, outside.
167
+ *
168
+ * @param {number} H The cap height.
169
+ * @returns {{ arm: readonly number[][], leg: readonly number[][], L: import('./echo.js').EchoLetter }} The letter and its strokes.
170
+ */
171
+ export const K_ = (H) => {
172
+ const W = 0.74 * H, arm = diag([T / 2, 0.64 * H], [W, 0], 0, -1), t = 0.42, p = [arm[0][0] + (arm[1][0] - arm[0][0]) * t, arm[0][1] + (arm[1][1] - arm[0][1]) * t];
173
+ const leg = diag(p, [W, H], 0, -1);
174
+ return { arm, leg, L: echoLetter(H, W, [st([T / 2, -T], [T / 2, H + T]), st(...arm, 0, 2 * T), st(...leg, 0, 2 * T)]) };
175
+ };
176
+
177
+ /**
178
+ * R: stem, a round bowl, the leg from the bowl to the bottom right corner; the echo is the leg's, on the right under the
179
+ * bowl.
180
+ *
181
+ * @param {number} H The cap height.
182
+ * @returns {{ leg: readonly number[][], L: import('./echo.js').EchoLetter }} The letter and its leg.
183
+ */
184
+ export const R_ = (H) => {
185
+ const W = 0.74 * H, yb = 0.58 * H, r = (yb - T / 2) / 2, bx = W - T / 2 - r;
186
+ const bowl = stroke([[T / 2, T / 2], [bx, T / 2], ...sample((t) => [bx + r * Math.cos(t), T / 2 + r - r * Math.sin(t)], Math.PI / 2, -Math.PI / 2, 60).slice(1), [T / 2, yb]], T, { chunk: 80 });
187
+ const leg = diag([bx - 0.1 * H, yb], [W, H], 0, -1);
188
+ return { leg, L: echoLetter(H, W, [st([T / 2, -T], [T / 2, H + T]), solid(bowl), st(...leg, 0, 2 * T)]) };
189
+ };
190
+
191
+ /**
192
+ * Y: two arms into a stem. The echo of the right arm stands outside it and runs down until the stem stops it.
193
+ *
194
+ * @param {number} H The cap height.
195
+ * @returns {{ r: readonly number[][], j: number[], L: import('./echo.js').EchoLetter }} The letter, its right arm and the join.
196
+ */
197
+ export const Y_ = (H) => {
198
+ const W = 0.86 * H, j = [W / 2, 0.54 * H], r = diag([W, 0], j, -1, 0), l = diag([0, 0], j, 1, 0);
199
+ return { r, j, L: echoLetter(H, W, [st(...l, 2 * T, 0), st(...r, 2 * T, 0), st([W / 2, j[1] - T / 2], [W / 2, H], 0, T)]) };
200
+ };
201
+
202
+ /**
203
+ * Seven: the bar and the diagonal (its right edge from the bar's corner); the echo outside on the right.
204
+ *
205
+ * @param {number} H The cap height.
206
+ * @returns {{ d: readonly number[][], W: number, L: import('./echo.js').EchoLetter }} The numeral, its diagonal and width.
207
+ */
208
+ export const Seven_ = (H) => {
209
+ const W = 0.72 * H, d = diag([W, T], [0.16 * W, H], -1, 1);
210
+ return { d, W, L: echoLetter(H, W, [st([0, T / 2], [W, T / 2], 0, 0, T, { k1: 0 }), st(...d, 2 * T, 2 * T)]) };
211
+ };
212
+
213
+ /**
214
+ * Q: a ring and a tail; the tail's echo runs beside it, outside the ring.
215
+ *
216
+ * @param {number} H The cap height.
217
+ * @returns {{ d: readonly number[][], L: import('./echo.js').EchoLetter }} The letter and its tail.
218
+ */
219
+ export const Q_ = (H) => {
220
+ const W = 1.02 * H, r = (H - T) / 2, c = [H / 2, H / 2], d = diag([c[0] + r * 0.72, c[1] + r * 0.72], [W, H], 0, -1);
221
+ return { d, L: echoLetter(H, W, [solid(D(circle(...c, r + T / 2, 240), circle(...c, r - T / 2, 240))), st(...d, 0, 2 * T)]) };
222
+ };
223
+
224
+ /**
225
+ * Lowercase k: the tall stem, the arm up to the x-height, the leg to the base; the echo is the leg's.
226
+ *
227
+ * @param {number} H The ascender height.
228
+ * @returns {{ leg: readonly number[][], arm: readonly number[][], L: import('./echo.js').EchoLetter }} The letter and its strokes.
229
+ */
230
+ export const k_ = (H) => {
231
+ const W = 0.66 * H, xh = 0.36 * H, arm = diag([T / 2, 0.74 * H], [W, xh], 0, -1), t = 0.4, p = [arm[0][0] + (arm[1][0] - arm[0][0]) * t, arm[0][1] + (arm[1][1] - arm[0][1]) * t];
232
+ const leg = diag(p, [W, H], 0, -1);
233
+ return { leg, arm, L: echoLetter(H, W, [st([T / 2, -T], [T / 2, H + T]), clip(st(...arm, 0, 2 * T), [-BIG, xh, BIG, BIG]), st(...leg, 0, 2 * T)]) };
234
+ };
235
+
236
+ /**
237
+ * Lowercase y: the short arm meets the long stroke, which runs on to the descender; the echo is the long stroke's,
238
+ * outside.
239
+ *
240
+ * @param {number} H The height, x-height top to descender.
241
+ * @returns {{ long: readonly number[][], xh: number, L: import('./echo.js').EchoLetter }} The letter, its long stroke and x-height.
242
+ */
243
+ export const y_ = (H) => {
244
+ const W = 0.74 * H, xh = 0.3 * H, j = [0.5 * W, 0.72 * H], long = diag([W, xh], [0.06 * W, H], -1, 1), sh = diag([0, xh], j, 1, 0);
245
+ return { long, xh, L: echoLetter(H, W, [clip(st(...long, 2 * T, 2 * T), [-BIG, xh, BIG, BIG]), clip(st(sh[0], sh[1], 2 * T, 0), [-BIG, xh, BIG, BIG])]) };
246
+ };
@@ -0,0 +1,6 @@
1
+ /** The cap line: y 32. */
2
+ export const T: number;
3
+ /** The baseline: y 480. */
4
+ export const B: number;
5
+ /** The cap height: 448. */
6
+ export const H: number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The 448 cap frame the Dot Stroke and Ink Trap letters are drawn on: the cap line, the baseline and the cap height,
3
+ * centred on the 512 canvas. From the Logomarks tools (dotstrokeparts.mjs and inktrapparts.mjs, which each had a copy).
4
+ */
5
+ import { C } from '../geometry/index.js';
6
+
7
+ /** The cap line: y 32. */
8
+ export const T = C - 224;
9
+
10
+ /** The baseline: y 480. */
11
+ export const B = C + 224;
12
+
13
+ /** The cap height: 448. */
14
+ export const H = 448;
@@ -0,0 +1,16 @@
1
+ export * from './frame.js';
2
+ export * from './serif.js';
3
+ export * from './blackletter.js';
4
+ export * from './pillar.js';
5
+ export * from './stem.js';
6
+ export * from './mono.js';
7
+ export * from './echo.js';
8
+ export * from './dotstroke.js';
9
+ export * from './inktrap.js';
10
+ export * from './stair.js';
11
+ export type { Face, Glyph, SourceGlyph, GlyphSource, Slant, RingArc, Crossing, Order, Build, Placer } from './serif.js';
12
+ export type { Nib } from './blackletter.js';
13
+ export type { PillarFace, PillarPart, PillarOptions } from './pillar.js';
14
+ export type { StemFace, StemPart, StemPlacement } from './stem.js';
15
+ export type { EchoLetter, EchoPart, EchoLine } from './echo.js';
16
+ export type { TrapJoin, TrapOptions } from './inktrap.js';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @module @logomarks/core/letters
3
+ *
4
+ * Letters built from geometry, never typeset: a high-contrast Didone capital alphabet with its frames and one-move
5
+ * builds (serif), modern blackletter from one 30° broad nib (blackletter), condensed Didone pairs on one shared stem
6
+ * (pillar, stem), the monogram letters (Fused, Track, Badge and Fan), and the Echo, Dot Stroke, Ink Trap and Stair faces.
7
+ * The Dot Stroke and Ink Trap letters share the 448 cap frame T, B, H. Moves that place on a font's letter are /font's.
8
+ */
9
+ export * from './frame.js';
10
+ export * from './serif.js';
11
+ export * from './blackletter.js';
12
+ export * from './pillar.js';
13
+ export * from './stem.js';
14
+ export * from './mono.js';
15
+ export * from './echo.js';
16
+ export * from './dotstroke.js';
17
+ export * from './inktrap.js';
18
+ export * from './stair.js';