@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.
- package/CHANGELOG.md +33 -0
- package/README.md +9 -2
- package/package.json +47 -4
- package/src/analyse/index.d.ts +1 -0
- package/src/analyse/index.js +3 -2
- package/src/analyse/optical.d.ts +20 -0
- package/src/analyse/optical.js +76 -0
- package/src/edit/cells.d.ts +13 -0
- package/src/edit/cells.js +26 -0
- package/src/edit/index.d.ts +3 -1
- package/src/edit/index.js +4 -2
- package/src/edit/pixelate.d.ts +2 -0
- package/src/edit/pixelate.js +15 -0
- package/src/font/glyphs.d.ts +31 -0
- package/src/font/glyphs.js +228 -0
- package/src/font/index.d.ts +10 -0
- package/src/font/index.js +10 -0
- package/src/font/measure.d.ts +16 -0
- package/src/font/measure.js +201 -0
- package/src/font/moves.d.ts +42 -0
- package/src/font/moves.js +263 -0
- package/src/forms/discs.d.ts +20 -0
- package/src/forms/discs.js +114 -0
- package/src/forms/emblems.d.ts +22 -0
- package/src/forms/emblems.js +109 -0
- package/src/forms/flow.d.ts +21 -0
- package/src/forms/flow.js +80 -0
- package/src/forms/folds.d.ts +18 -0
- package/src/forms/folds.js +147 -0
- package/src/forms/geo.d.ts +46 -0
- package/src/forms/geo.js +233 -0
- package/src/forms/hooks.d.ts +40 -0
- package/src/forms/hooks.js +204 -0
- package/src/forms/index.d.ts +22 -0
- package/src/forms/index.js +24 -0
- package/src/forms/jog.d.ts +33 -0
- package/src/forms/jog.js +162 -0
- package/src/forms/lines.d.ts +32 -0
- package/src/forms/lines.js +233 -0
- package/src/forms/ref.d.ts +41 -0
- package/src/forms/ref.js +246 -0
- package/src/forms/shatter.d.ts +40 -0
- package/src/forms/shatter.js +341 -0
- package/src/forms/slats.d.ts +35 -0
- package/src/forms/slats.js +78 -0
- package/src/forms/swash.d.ts +23 -0
- package/src/forms/swash.js +74 -0
- package/src/forms/tech.d.ts +40 -0
- package/src/forms/tech.js +293 -0
- package/src/forms/tracks.d.ts +36 -0
- package/src/forms/tracks.js +121 -0
- package/src/index.js +2 -1
- package/src/iso/index.d.ts +5 -0
- package/src/iso/index.js +13 -0
- package/src/iso/objects.d.ts +168 -0
- package/src/iso/objects.js +1042 -0
- package/src/iso/planes.d.ts +27 -0
- package/src/iso/planes.js +138 -0
- package/src/iso/voxels.d.ts +43 -0
- package/src/iso/voxels.js +255 -0
- package/src/letters/blackletter.d.ts +31 -0
- package/src/letters/blackletter.js +199 -0
- package/src/letters/dotstroke.d.ts +16 -0
- package/src/letters/dotstroke.js +77 -0
- package/src/letters/echo.d.ts +42 -0
- package/src/letters/echo.js +246 -0
- package/src/letters/frame.d.ts +6 -0
- package/src/letters/frame.js +14 -0
- package/src/letters/index.d.ts +16 -0
- package/src/letters/index.js +18 -0
- package/src/letters/inktrap.d.ts +66 -0
- package/src/letters/inktrap.js +244 -0
- package/src/letters/mono.d.ts +145 -0
- package/src/letters/mono.js +684 -0
- package/src/letters/pillar.d.ts +31 -0
- package/src/letters/pillar.js +257 -0
- package/src/letters/serif.d.ts +109 -0
- package/src/letters/serif.js +666 -0
- package/src/letters/stair.d.ts +8 -0
- package/src/letters/stair.js +43 -0
- package/src/letters/stem.d.ts +29 -0
- package/src/letters/stem.js +205 -0
- package/src/objects/animals.d.ts +105 -0
- package/src/objects/animals.js +469 -0
- package/src/objects/bio.d.ts +14 -0
- package/src/objects/bio.js +76 -0
- package/src/objects/devglyphs.d.ts +34 -0
- package/src/objects/devglyphs.js +226 -0
- package/src/objects/devices.d.ts +31 -0
- package/src/objects/devices.js +114 -0
- package/src/objects/faces.d.ts +94 -0
- package/src/objects/faces.js +344 -0
- package/src/objects/food.d.ts +12 -0
- package/src/objects/food.js +74 -0
- package/src/objects/index.d.ts +13 -0
- package/src/objects/index.js +18 -0
- package/src/objects/rosettes.d.ts +14 -0
- package/src/objects/rosettes.js +64 -0
- package/src/objects/sport.d.ts +8 -0
- package/src/objects/sport.js +37 -0
- package/src/objects/weapons.d.ts +31 -0
- package/src/objects/weapons.js +179 -0
- package/src/pixels/dither.d.ts +78 -0
- package/src/pixels/dither.js +399 -0
- package/src/pixels/emote.d.ts +6 -0
- package/src/pixels/emote.js +76 -0
- package/src/pixels/index.d.ts +7 -0
- package/src/pixels/index.js +8 -0
- package/src/sdf/index.d.ts +2 -0
- package/src/sdf/index.js +2 -0
- package/src/sdf/y2k.d.ts +12 -0
- package/src/sdf/y2k.js +80 -0
- package/src/util/vec.d.ts +14 -0
- package/src/util/vec.js +61 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A high-contrast (Didone) serif capital alphabet built from geometry, so every serif letter, monogram and cartouche
|
|
3
|
+
* shares one construction, and the one-move builds (rule, split, dot, plinth, frame, inline…) that take any glyph source,
|
|
4
|
+
* a font's letter included. From the Logomarks tools (serifparts.mjs, batch 230; P and G from batch 240, the moves shared
|
|
5
|
+
* since batch 242).
|
|
6
|
+
*
|
|
7
|
+
* A face is the metrics of one cap height H: thick stems S, hairlines h (keep h at 30 or more once fitted, or the
|
|
8
|
+
* hairlines break up at 32 px), hairline serifs reaching ov past a stem, brackets of radius br where a serif meets its
|
|
9
|
+
* stroke, and beak serifs (the short vertical serifs on an E's or T's arms) beak long. Each glyph is drawn with its cap
|
|
10
|
+
* line at y 0 and its baseline at y H, its ink starting at x 0, and is { s: shape, w: ink width }:
|
|
11
|
+
* - stems: vertical stems and slanted strokes (`slant()`), with flat hairline serifs (`foot()`) and their brackets;
|
|
12
|
+
* - bowls: D, B and R bowls (`dBowl()`): a straight run off the stem then half an ellipse, thick at the side, hairline
|
|
13
|
+
* at top and bottom (vertical stress);
|
|
14
|
+
* - curves: C, J and S from stressed rings (`ringArc()`): an ellipse less an inner one, sides thick and top and bottom
|
|
15
|
+
* hairline, cut along rays, ending in ball terminals (`ball()`);
|
|
16
|
+
* - interlace: two shapes laid over each other, at each crossing one in front and the other cut back by the gap.
|
|
17
|
+
*/
|
|
18
|
+
import { U, D, I, rect, poly, ellipse, circle, move, mapPts, grown, grow, soften, bbox, GAP } from '../geometry/index.js';
|
|
19
|
+
import { despeckShape } from '../parts/index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The metrics of a serif face with cap height H.
|
|
23
|
+
*
|
|
24
|
+
* @param {number} [H=360] The cap height.
|
|
25
|
+
* @param {{ S?: number, h?: number, ov?: number, br?: number, beak?: number, bw?: number }} [o] Any metric to set
|
|
26
|
+
* instead of its default: stem S (0.22 H), hairline h (32), serif reach ov (0.085 H), bracket br (0.05 H), beak length
|
|
27
|
+
* (0.19 H), beak width bw (1.25 h).
|
|
28
|
+
* @returns {import('./serif.js').Face} The face.
|
|
29
|
+
*/
|
|
30
|
+
export const face = (H = 360, o = {}) => ({ H, S: o.S ?? Math.round(H * 0.22), h: o.h ?? 32, ov: o.ov ?? Math.round(H * 0.085),
|
|
31
|
+
br: o.br ?? Math.round(H * 0.05), beak: o.beak ?? Math.round(H * 0.19), bw: o.bw ?? Math.round((o.h ?? 32) * 1.25) });
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A rectangle from its corners.
|
|
35
|
+
*
|
|
36
|
+
* @param {number} x0 Left.
|
|
37
|
+
* @param {number} y0 Top.
|
|
38
|
+
* @param {number} x1 Right.
|
|
39
|
+
* @param {number} y1 Bottom.
|
|
40
|
+
* @returns {import('../geometry/types.js').Polygon} The rectangle.
|
|
41
|
+
*/
|
|
42
|
+
const R = (x0, y0, x1, y1) => rect(x0, y0, x1 - x0, y1 - y0);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A vector made unit length.
|
|
46
|
+
*
|
|
47
|
+
* @param {readonly number[]} v The vector.
|
|
48
|
+
* @returns {number[]} Its direction.
|
|
49
|
+
*/
|
|
50
|
+
const unit = ([x, y]) => { const l = Math.hypot(x, y) || 1; return [x / l, y / l]; };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A bracket: the concave fillet of radius r in the corner K between two edges leaving K along the unit directions d1
|
|
54
|
+
* and d2. An acute corner gets a smaller bracket, so it never runs far up a slanted side.
|
|
55
|
+
*
|
|
56
|
+
* @param {readonly number[]} K The corner.
|
|
57
|
+
* @param {readonly number[]} d1 One edge's direction.
|
|
58
|
+
* @param {readonly number[]} d2 The other's.
|
|
59
|
+
* @param {number} r The radius (0: no bracket).
|
|
60
|
+
* @returns {import('../geometry/types.js').Shape} The fillet's ink.
|
|
61
|
+
*/
|
|
62
|
+
export const bracket = (K, d1, d2, r) => {
|
|
63
|
+
if (!r) return [];
|
|
64
|
+
const cos = d1[0] * d2[0] + d1[1] * d2[1], th = Math.acos(Math.max(-1, Math.min(1, cos)));
|
|
65
|
+
r *= Math.min(1, Math.tan(th / 2)); // an acute corner gets a smaller bracket, so it never runs far up a slanted side
|
|
66
|
+
const t = r / Math.tan(th / 2), c = r / Math.sin(th / 2);
|
|
67
|
+
const bis = unit([d1[0] + d2[0], d1[1] + d2[1]]), T1 = [K[0] + d1[0] * t, K[1] + d1[1] * t], T2 = [K[0] + d2[0] * t, K[1] + d2[1] * t];
|
|
68
|
+
const Cc = [K[0] + bis[0] * c, K[1] + bis[1] * c];
|
|
69
|
+
return D(poly([K, T1, Cc, T2]), circle(...Cc, r, 64));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A hairline serif slab on the foot of a stroke whose end spans [a, b] at the cap line ('top') or the baseline ('base').
|
|
74
|
+
* It reaches ov out on the left (l) and right (r) sides, bracketed against the stroke's sides, which leave the foot along
|
|
75
|
+
* the unit directions da (left side) and db (right side): straight up or down for a stem.
|
|
76
|
+
*
|
|
77
|
+
* @param {import('./serif.js').Face} m The face.
|
|
78
|
+
* @param {number} a The stroke end's left x.
|
|
79
|
+
* @param {number} b Its right x.
|
|
80
|
+
* @param {'top' | 'base'} at Which end.
|
|
81
|
+
* @param {{ l?: number, r?: number, da?: readonly number[], db?: readonly number[], brk?: number }} [o] The reach on each
|
|
82
|
+
* side (1 full, 0 none), the sides' directions, and the bracket's scale (1).
|
|
83
|
+
* @returns {import('../geometry/types.js').Shape} The serif.
|
|
84
|
+
*/
|
|
85
|
+
export const foot = (m, a, b, at, { l = 1, r = 1, da, db, brk = 1 } = {}) => {
|
|
86
|
+
const top = at === 'top', y = top ? m.h : m.H - m.h, up = top ? 1 : -1;
|
|
87
|
+
da = da || [0, up]; db = db || [0, up];
|
|
88
|
+
const slab = top ? R(a - l * m.ov, 0, b + r * m.ov, m.h) : R(a - l * m.ov, m.H - m.h, b + r * m.ov, m.H);
|
|
89
|
+
// a slanted side meets the slab's face a little along from the foot's corner
|
|
90
|
+
const atFace = (x, d) => [x + d[0] * (m.h / Math.abs(d[1])), y], rr = brk * Math.min(m.br, m.ov * 0.8);
|
|
91
|
+
return U(slab, l ? bracket(atFace(a, da), [-1, 0], da, rr) : [], r ? bracket(atFace(b, db), [1, 0], db, rr) : []);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A straight stroke from y0 to y1 with flat ends: its left side runs from (xt, y0) to (xb, y1) and it is t thick across
|
|
96
|
+
* the stroke.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} xt The left side's x at y0.
|
|
99
|
+
* @param {number} xb The left side's x at y1.
|
|
100
|
+
* @param {number} t The thickness, across the stroke.
|
|
101
|
+
* @param {number} y0 The top.
|
|
102
|
+
* @param {number} y1 The bottom.
|
|
103
|
+
* @returns {import('./serif.js').Slant} The stroke, its sides at any height, its width along a flat end and the unit
|
|
104
|
+
* directions along it.
|
|
105
|
+
*/
|
|
106
|
+
export const slant = (xt, xb, t, y0, y1) => {
|
|
107
|
+
const dx = xb - xt, dy = y1 - y0, L = Math.hypot(dx, dy), w = (t * L) / dy;
|
|
108
|
+
const at = (y) => { const x = xt + (dx * (y - y0)) / dy; return [x, x + w]; };
|
|
109
|
+
const up = unit([-dx, -dy]);
|
|
110
|
+
return { s: poly([[xt, y0], [xt + w, y0], [xb + w, y1], [xb, y1]]), at, w, up, down: [-up[0], -up[1]] };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The right half of an ellipse.
|
|
115
|
+
*
|
|
116
|
+
* @param {number} cx The centre.
|
|
117
|
+
* @param {number} cy The centre.
|
|
118
|
+
* @param {number} rx The radius across.
|
|
119
|
+
* @param {number} ry The radius down.
|
|
120
|
+
* @returns {import('../geometry/types.js').Shape} The half.
|
|
121
|
+
*/
|
|
122
|
+
const halfE = (cx, cy, rx, ry) => I(ellipse(cx, cy, rx, ry, 0, 360), R(cx, cy - ry - 2, cx + rx + 2, cy + ry + 2));
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A D-bowl on a stem whose left edge is x0, from y0 to y1, reaching right to xr: a straight hairline run of `flat` off
|
|
126
|
+
* the stem, then half an ellipse, its side t thick and its top and bottom hairlines. Includes the stem's height between
|
|
127
|
+
* y0 and y1.
|
|
128
|
+
*
|
|
129
|
+
* @param {import('./serif.js').Face} m The face.
|
|
130
|
+
* @param {number} x0 The stem's left edge.
|
|
131
|
+
* @param {number} y0 The bowl's top.
|
|
132
|
+
* @param {number} y1 Its bottom.
|
|
133
|
+
* @param {number} xr Its right edge.
|
|
134
|
+
* @param {number} t Its side's thickness.
|
|
135
|
+
* @param {number} flat The straight run off the stem.
|
|
136
|
+
* @returns {import('../geometry/types.js').Shape} The bowl.
|
|
137
|
+
*/
|
|
138
|
+
export const dBowl = (m, x0, y0, y1, xr, t, flat) => {
|
|
139
|
+
const cy = (y0 + y1) / 2, ry = (y1 - y0) / 2, cx = x0 + m.S + flat, rx = xr - cx;
|
|
140
|
+
const outer = U(R(x0, y0, cx, y1), halfE(cx, cy, rx, ry)), inner = U(R(x0 + m.S, y0 + m.h, cx + 0.5, y1 - m.h), halfE(cx, cy, rx - t, ry - m.h));
|
|
141
|
+
return D(outer, inner);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Where the ray from an ellipse's centre at angle a (degrees, y down: −90 is the top) meets it.
|
|
146
|
+
*
|
|
147
|
+
* @param {number} cx The centre.
|
|
148
|
+
* @param {number} cy The centre.
|
|
149
|
+
* @param {number} rx The radius across.
|
|
150
|
+
* @param {number} ry The radius down.
|
|
151
|
+
* @param {number} a The angle.
|
|
152
|
+
* @returns {number[]} The point.
|
|
153
|
+
*/
|
|
154
|
+
const onE = (cx, cy, rx, ry, a) => {
|
|
155
|
+
const t = (a * Math.PI) / 180, c = Math.cos(t), n = Math.sin(t), r = 1 / Math.sqrt((c * c) / (rx * rx) + (n * n) / (ry * ry));
|
|
156
|
+
return [cx + r * c, cy + r * n];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A stressed ring: an ellipse rx × ry less an inner one t narrower each side and h shorter top and bottom, so its sides
|
|
161
|
+
* are t thick and its top and bottom hairlines (vertical stress). Kept from angle a0 to a1 (degrees, y down, a0 < a1,
|
|
162
|
+
* clockwise on screen), cut along rays from the centre.
|
|
163
|
+
*
|
|
164
|
+
* @param {number} cx The centre.
|
|
165
|
+
* @param {number} cy The centre.
|
|
166
|
+
* @param {number} rx The outer radius across.
|
|
167
|
+
* @param {number} ry The outer radius down.
|
|
168
|
+
* @param {number} t The sides' thickness.
|
|
169
|
+
* @param {number} h The hairline at top and bottom.
|
|
170
|
+
* @param {number} [a0=-180] Where it starts.
|
|
171
|
+
* @param {number} [a1=180] Where it ends.
|
|
172
|
+
* @returns {import('./serif.js').RingArc} The ring, and `end(a)`: the outer and inner points of the cut at a.
|
|
173
|
+
*/
|
|
174
|
+
export const ringArc = (cx, cy, rx, ry, t, h, a0 = -180, a1 = 180) => {
|
|
175
|
+
const n = Math.max(8, Math.ceil((a1 - a0) / 4)), far = 2 * (rx + ry);
|
|
176
|
+
const ring = D(ellipse(cx, cy, rx, ry, 0, 360), ellipse(cx, cy, rx - t, ry - h, 0, 360));
|
|
177
|
+
const wedge = a1 - a0 >= 360 ? null : poly([[cx, cy], ...[...Array(n + 1)].map((_, i) => { const q = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180; return [cx + far * Math.cos(q), cy + far * Math.sin(q)]; })]);
|
|
178
|
+
return { s: wedge ? I(ring, wedge) : ring, end: (a) => ({ o: onE(cx, cy, rx, ry, a), i: onE(cx, cy, rx - t, ry - h, a) }) };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A ball terminal of radius rb hung inside a curve's outer edge: tangent to it at the point o, toward the curve's
|
|
183
|
+
* centre c.
|
|
184
|
+
*
|
|
185
|
+
* @param {readonly number[]} o The point on the outer edge.
|
|
186
|
+
* @param {readonly number[]} c The curve's centre.
|
|
187
|
+
* @param {number} rb The ball's radius.
|
|
188
|
+
* @returns {import('../geometry/types.js').Polygon} The ball.
|
|
189
|
+
*/
|
|
190
|
+
export const ball = (o, c, rb) => { const u = unit([c[0] - o[0], c[1] - o[1]]); return circle(o[0] + u[0] * rb, o[1] + u[1] * rb, rb, 120); };
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Moves a glyph so its ink starts at x 0.
|
|
194
|
+
*
|
|
195
|
+
* @param {import('../geometry/types.js').Shape} s The ink.
|
|
196
|
+
* @returns {import('./serif.js').Glyph} The glyph and its width.
|
|
197
|
+
*/
|
|
198
|
+
const ink = (s) => { const [x0, , x1] = bbox(s); return { s: move(s, -x0, 0), w: x1 - x0 }; };
|
|
199
|
+
|
|
200
|
+
/* ---------- the capitals ---------- */
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A vertical stem [x, x + w] from the cap line to the baseline, with serifs top and base on the sides asked for.
|
|
204
|
+
*
|
|
205
|
+
* @param {import('./serif.js').Face} m The face.
|
|
206
|
+
* @param {number} x The stem's left edge.
|
|
207
|
+
* @param {number} w Its width.
|
|
208
|
+
* @param {number[] | 0} [top] The top serif's [left, right] reach, or 0 for none.
|
|
209
|
+
* @param {number[] | 0} [base] The base serif's.
|
|
210
|
+
* @returns {import('../geometry/types.js').Shape} The stem.
|
|
211
|
+
*/
|
|
212
|
+
const stemV = (m, x, w, top = [1, 1], base = [1, 1]) => U(R(x, 0, x + w, m.H), top ? foot(m, x, x + w, 'top', { l: top[0], r: top[1] }) : [], base ? foot(m, x, x + w, 'base', { l: base[0], r: base[1] }) : []);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A beak serif: a short vertical spur bw wide whose outer edge is x, hanging from the cap line ('top') or rising from
|
|
216
|
+
* the baseline ('base'), bracketed on its inner side against the arm.
|
|
217
|
+
*
|
|
218
|
+
* @param {import('./serif.js').Face} m The face.
|
|
219
|
+
* @param {number} x The spur's outer edge.
|
|
220
|
+
* @param {'top' | 'base'} at Which line.
|
|
221
|
+
* @param {number} [len] Its length (the face's beak).
|
|
222
|
+
* @returns {import('../geometry/types.js').Shape} The beak.
|
|
223
|
+
*/
|
|
224
|
+
const beak = (m, x, at, len = m.beak) => {
|
|
225
|
+
const top = at === 'top';
|
|
226
|
+
const s = top ? R(x - m.bw, 0, x, len) : R(x - m.bw, m.H - len, x, m.H);
|
|
227
|
+
const K = top ? [x - m.bw, m.h] : [x - m.bw, m.H - m.h];
|
|
228
|
+
return U(s, bracket(K, [-1, 0], [0, top ? 1 : -1], m.br * 0.8));
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The capitals, each `(m) => { s, w }` in the face m: A B C D E G H J K L M N P R S T.
|
|
233
|
+
*/
|
|
234
|
+
export const SERIF_GLYPHS = {
|
|
235
|
+
A(m) {
|
|
236
|
+
const W = m.H * 0.94, xL = m.ov, xR = W - m.ov, e = m.H * 0.05, ax = W * 0.5, dy = m.H + e;
|
|
237
|
+
const lx = (y) => ax + ((xL - ax) * (y + e)) / dy, rx = (y) => ax + ((xR - ax) * (y + e)) / dy;
|
|
238
|
+
const wl = (m.h * Math.hypot(ax - xL, dy)) / dy, wr = (m.S * Math.hypot(xR - ax, dy)) / dy;
|
|
239
|
+
// the counter: the outline's left side moved in by the hairline, its right side by the stem; they meet at the inner apex
|
|
240
|
+
const yi = ((wl + wr) * dy) / (xR - xL) - e, Y = m.H + 60;
|
|
241
|
+
const outer = poly([[ax, -e], [xR, m.H], [xL, m.H]]), inner = poly([[lx(yi) + wl, yi], [rx(Y) - wr, Y], [lx(Y) + wl, Y]]);
|
|
242
|
+
const yb = m.H * 0.62, up = (x0, x1) => unit([x0 - x1, -dy]);
|
|
243
|
+
const s = U(I(U(D(outer, inner), I(outer, R(-10, yb, W + 10, yb + m.h))), R(-10, 0, W + 10, m.H)),
|
|
244
|
+
foot(m, xL, xL + wl, 'base', { da: up(ax, xL), db: up(ax, xL) }), foot(m, xR - wr, xR, 'base', { da: up(ax, xR), db: up(ax, xR) }));
|
|
245
|
+
return ink(s);
|
|
246
|
+
},
|
|
247
|
+
B(m) {
|
|
248
|
+
const x0 = m.ov, yu = m.H * 0.47 + m.h / 2;
|
|
249
|
+
const s = U(dBowl(m, x0, 0, yu, x0 + m.H * 0.54, m.S * 0.9, m.H * 0.05), dBowl(m, x0, yu - m.h, m.H, x0 + m.H * 0.62, m.S * 0.98, m.H * 0.06),
|
|
250
|
+
foot(m, x0, x0 + m.S, 'top', { r: 0 }), foot(m, x0, x0 + m.S, 'base', { r: 0 }));
|
|
251
|
+
return ink(s);
|
|
252
|
+
},
|
|
253
|
+
C(m) {
|
|
254
|
+
const W = m.H * 0.86, cx = W / 2, cy = m.H / 2, rx = W / 2, ry = m.H / 2, a0 = -38, a1 = 58;
|
|
255
|
+
const r = ringArc(cx, cy, rx, ry, m.S * 1.04, m.h, a1, 360 + a0), top = r.end(360 + a0), bot = r.end(a1);
|
|
256
|
+
// a ball hangs from the top terminal; a beak rises from the bottom one
|
|
257
|
+
const x0 = Math.min(bot.o[0], bot.i[0]) - 1, xe = Math.max(bot.o[0], bot.i[0], x0 + m.bw) + 1, bk = R(x0, m.H - m.h - m.beak * 0.62, xe, bot.o[1]);
|
|
258
|
+
return ink(U(r.s, ball(top.o, [cx, cy], m.S * 0.5), I(bk, ellipse(cx, cy, rx, ry, 0, 360)), bk));
|
|
259
|
+
},
|
|
260
|
+
D(m) {
|
|
261
|
+
const x0 = m.ov;
|
|
262
|
+
return ink(U(dBowl(m, x0, 0, m.H, x0 + m.H * 0.8, m.S * 1.04, m.H * 0.1), foot(m, x0, x0 + m.S, 'top', { r: 0 }), foot(m, x0, x0 + m.S, 'base', { r: 0 })));
|
|
263
|
+
},
|
|
264
|
+
E(m) {
|
|
265
|
+
const x0 = m.ov, W = x0 + m.H * 0.6, ym = m.H * 0.47 - m.h / 2, xm = W - m.H * 0.08, xb = W + m.H * 0.03;
|
|
266
|
+
return ink(U(stemV(m, x0, m.S, [1, 0], [1, 0]), R(x0, 0, W, m.h), R(x0, ym, xm, ym + m.h), R(x0, m.H - m.h, xb, m.H),
|
|
267
|
+
beak(m, W, 'top'), beak(m, xb, 'base', m.beak * 1.1), R(xm - m.bw, ym - m.beak * 0.32, xm, ym + m.h + m.beak * 0.32),
|
|
268
|
+
bracket([xm - m.bw, ym], [-1, 0], [0, -1], m.br * 0.6), bracket([xm - m.bw, ym + m.h], [-1, 0], [0, 1], m.br * 0.6)));
|
|
269
|
+
},
|
|
270
|
+
H(m) {
|
|
271
|
+
const a = m.ov, b = a + m.S + m.H * 0.5, yc = m.H * 0.47 - m.h / 2;
|
|
272
|
+
return ink(U(stemV(m, a, m.S), stemV(m, b, m.S), R(a + m.S - 1, yc, b + 1, yc + m.h)));
|
|
273
|
+
},
|
|
274
|
+
J(m) {
|
|
275
|
+
const jx = m.H * 0.42, rx = m.H * 0.26, ry = m.H * 0.22, cx = jx + m.S - rx, cy = m.H - ry;
|
|
276
|
+
const r = ringArc(cx, cy, rx, ry, m.S, m.h, 0, 160), e = r.end(160);
|
|
277
|
+
return ink(U(R(jx, 0, jx + m.S, cy + 1), foot(m, jx, jx + m.S, 'top'), r.s, ball(e.o, [cx, cy - ry * 0.4], m.S * 0.5)));
|
|
278
|
+
},
|
|
279
|
+
K(m) {
|
|
280
|
+
const W = m.H * 0.98, x0 = m.ov, sx = x0 + m.S;
|
|
281
|
+
const arm = slant(W - m.ov * 1.4 - m.h * 1.3, sx - m.h * 1.2, m.h, 0, m.H * 0.7), armS = I(arm.s, R(x0, -1, W + 50, m.H));
|
|
282
|
+
// the leg springs from the arm's centreline at y0 and runs to the baseline, its top cut along the arm
|
|
283
|
+
const y0 = m.H * 0.38, P = [(arm.at(y0)[0] + arm.at(y0)[1]) / 2, y0], B = [W - m.ov - m.S * 1.15, m.H], u = unit([B[0] - P[0], B[1] - P[1]]);
|
|
284
|
+
const ad = arm.up, cr = Math.abs(ad[0] * u[1] - ad[1] * u[0]), Q = [P[0] + (ad[0] * m.S) / cr, P[1] + (ad[1] * m.S) / cr], wb = m.S / u[1];
|
|
285
|
+
const leg = poly([P, Q, [B[0] + wb, m.H], B]), lu = unit([P[0] - B[0], P[1] - B[1]]);
|
|
286
|
+
return ink(U(stemV(m, x0, m.S), armS, leg, foot(m, arm.at(0)[0], arm.at(0)[1], 'top', { da: arm.down, db: arm.down, brk: 0 }),
|
|
287
|
+
foot(m, B[0], B[0] + wb, 'base', { da: lu, db: lu })));
|
|
288
|
+
},
|
|
289
|
+
L(m) {
|
|
290
|
+
const x0 = m.ov, W = x0 + m.H * 0.58;
|
|
291
|
+
return ink(U(stemV(m, x0, m.S, [1, 1], [1, 0]), R(x0, m.H - m.h, W, m.H), beak(m, W, 'base', m.beak * 1.1)));
|
|
292
|
+
},
|
|
293
|
+
M(m) {
|
|
294
|
+
const x0 = m.ov, W = m.H * 1.12, rs = W - m.ov - m.S, wd = m.S * 1.05, vx = W * 0.5 + m.S * 0.45;
|
|
295
|
+
const dl = slant(x0, vx - (wd * Math.hypot(vx - x0, m.H)) / m.H, wd, 0, m.H), dr = slant(rs - m.h * 0.4, vx - m.h * 1.1, m.h, 0, m.H);
|
|
296
|
+
return ink(U(R(x0, 0, x0 + m.h, m.H), foot(m, x0, x0 + m.h, 'top', { r: 0 }), foot(m, x0, x0 + m.h, 'base'),
|
|
297
|
+
I(U(dl.s, dr.s), R(-10, 0, W + 10, m.H)), stemV(m, rs, m.S, [0, 1], [1, 1])));
|
|
298
|
+
},
|
|
299
|
+
N(m) {
|
|
300
|
+
const x0 = m.ov, W = m.H * 0.94, rx = W - m.ov - m.h, wd = m.S * 1.04;
|
|
301
|
+
const d = slant(x0, rx + m.h - (wd * Math.hypot(rx - x0, m.H)) / m.H, wd, 0, m.H);
|
|
302
|
+
return ink(U(R(x0, 0, x0 + m.h, m.H), foot(m, x0, x0 + m.h, 'top', { r: 0 }), foot(m, x0, x0 + m.h, 'base'),
|
|
303
|
+
I(d.s, R(-10, 0, W + 10, m.H)), R(rx, 0, rx + m.h, m.H), foot(m, rx, rx + m.h, 'top')));
|
|
304
|
+
},
|
|
305
|
+
R(m) {
|
|
306
|
+
const x0 = m.ov, yr = m.H * 0.53, W = x0 + m.H * 0.84, flat = m.H * 0.05, xr = x0 + m.H * 0.6, cx = x0 + m.S + flat;
|
|
307
|
+
const bowl = dBowl(m, x0, 0, yr, xr, m.S * 0.94, flat), wedge = U(R(-10, yr - m.h, W + 10, m.H), bowl);
|
|
308
|
+
// the leg runs on up into the bowl's side, so it springs from the curve rather than standing under it
|
|
309
|
+
const leg = slant(cx + m.H * 0.02, W - m.ov - m.S * 1.1, m.S, yr - m.h, m.H), legUp = slant(...[yr * 0.45].map((y) => leg.at(y)[0]), W - m.ov - m.S * 1.1, m.S, yr * 0.45, m.H);
|
|
310
|
+
return ink(U(bowl, R(x0, 0, x0 + m.S, m.H), foot(m, x0, x0 + m.S, 'top', { r: 0 }), foot(m, x0, x0 + m.S, 'base'),
|
|
311
|
+
I(legUp.s, wedge), foot(m, leg.at(m.H)[0], leg.at(m.H)[1], 'base', { da: leg.up, db: leg.up })));
|
|
312
|
+
},
|
|
313
|
+
S(m) {
|
|
314
|
+
const W = m.H * 0.66, r1y = m.H * 0.255, r2y = m.H / 2 - r1y + m.h * 0.5, r1x = W * 0.46, r2x = W * 0.5, c1 = [W * 0.5, r1y], c2 = [W * 0.5, m.H - r2y];
|
|
315
|
+
const t = m.S * 0.98, up = ringArc(...c1, r1x, r1y, t, m.h, -197, -26), lo = ringArc(...c2, r2x, r2y, t, m.h, -18, 154);
|
|
316
|
+
// the spine joins the upper bowl's lower-left cut to the lower bowl's upper-right cut
|
|
317
|
+
const a = up.end(-197), b = lo.end(-18), spine = poly([a.o, b.i, b.o, a.i]);
|
|
318
|
+
const tt = up.end(-26), bb = lo.end(154);
|
|
319
|
+
return ink(U(soften(U(up.s, lo.s, spine), m.br, m.h * 0.35), ball(tt.o, c1, m.S * 0.44), ball(bb.o, c2, m.S * 0.46)));
|
|
320
|
+
},
|
|
321
|
+
// P is R's bowl on a stem; G is C with a spur
|
|
322
|
+
P(m) {
|
|
323
|
+
const x0 = m.ov, yr = m.H * 0.56, xr = x0 + m.H * 0.64, flat = m.H * 0.05;
|
|
324
|
+
return ink(U(dBowl(m, x0, 0, yr, xr, m.S * 0.96, flat), R(x0, 0, x0 + m.S, m.H), foot(m, x0, x0 + m.S, 'top', { r: 0 }), foot(m, x0, x0 + m.S, 'base')));
|
|
325
|
+
},
|
|
326
|
+
G(m) {
|
|
327
|
+
const W = m.H * 0.9, cx = W / 2, cy = m.H / 2, rx = W / 2, ry = m.H / 2, a0 = -38, a1 = 12, sw = m.S * 0.96, ys = m.H * 0.56;
|
|
328
|
+
const r = ringArc(cx, cy, rx, ry, m.S * 1.04, m.h, a1, 360 + a0), top = r.end(360 + a0);
|
|
329
|
+
// the spur: a thick upright under the right side, from the arc's end down into the bowl's foot, a hairline serif on its top
|
|
330
|
+
const xs = W - sw, spur = I(R(xs, ys, W, m.H), ellipse(cx, cy, rx, ry, 0, 360));
|
|
331
|
+
return ink(U(r.s, ball(top.o, [cx, cy], m.S * 0.5), spur, R(xs - m.ov * 0.7, ys, W + m.ov * 0.4, ys + m.h)));
|
|
332
|
+
},
|
|
333
|
+
T(m) {
|
|
334
|
+
const W = m.H * 0.8, sx = (W - m.S) / 2, bk = beak(m, W, 'top');
|
|
335
|
+
return ink(U(R(0, 0, W, m.h), bk, mapPts(bk, ([x, y]) => [W - x, y]), R(sx, 0, sx + m.S, m.H),
|
|
336
|
+
foot(m, sx, sx + m.S, 'base'), bracket([sx, m.h], [-1, 0], [0, 1], m.br), bracket([sx + m.S, m.h], [1, 0], [0, 1], m.br)));
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The serif capital k in face m, its ink's left edge at x and its cap line at y.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} k The letter (a SERIF_GLYPHS key).
|
|
344
|
+
* @param {import('./serif.js').Face} m The face.
|
|
345
|
+
* @param {number} [x=0] Where its ink starts.
|
|
346
|
+
* @param {number} [y=0] Its cap line.
|
|
347
|
+
* @returns {import('./serif.js').Glyph} The letter and its width.
|
|
348
|
+
*/
|
|
349
|
+
export const serifLetter = (k, m, x = 0, y = 0) => { const g = SERIF_GLYPHS[k](m); return { s: move(g.s, x, y), w: g.w }; };
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The serif capital k in face m, centred on (cx, cy).
|
|
353
|
+
*
|
|
354
|
+
* @param {string} k The letter (a SERIF_GLYPHS key).
|
|
355
|
+
* @param {import('./serif.js').Face} m The face.
|
|
356
|
+
* @param {number} cx The centre.
|
|
357
|
+
* @param {number} cy The centre.
|
|
358
|
+
* @returns {import('../geometry/types.js').Shape} The letter.
|
|
359
|
+
*/
|
|
360
|
+
export const serifLetterAt = (k, m, cx, cy) => { const g = SERIF_GLYPHS[k](m); return move(g.s, cx - g.w / 2, cy - m.H / 2); };
|
|
361
|
+
|
|
362
|
+
/* ---------- interlace ---------- */
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* A ring's area.
|
|
366
|
+
*
|
|
367
|
+
* @param {readonly (readonly number[])[]} r The ring.
|
|
368
|
+
* @returns {number} The area.
|
|
369
|
+
*/
|
|
370
|
+
const area = (r) => Math.abs(r.reduce((a, p, i) => { const q = r[(i + 1) % r.length]; return a + p[0] * q[1] - q[0] * p[1]; }, 0)) / 2;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The mean of a polygon's outer ring's points.
|
|
374
|
+
*
|
|
375
|
+
* @param {import('../geometry/types.js').Polygon} pg The polygon.
|
|
376
|
+
* @returns {number[]} The point.
|
|
377
|
+
*/
|
|
378
|
+
const centre = (pg) => { const r = pg[0], n = r.length; return r.reduce((a, p) => [a[0] + p[0] / n, a[1] + p[1] / n], [0, 0]); };
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The places two shapes cross (their overlaps, larger than `min`), sorted by `key` (x then y by default).
|
|
382
|
+
*
|
|
383
|
+
* @param {import('../geometry/types.js').Geom} a One shape.
|
|
384
|
+
* @param {import('../geometry/types.js').Geom} b The other.
|
|
385
|
+
* @param {number} [min=40] The smallest overlap that counts.
|
|
386
|
+
* @param {(c: number[]) => number} [key] The sort key of a crossing's centre.
|
|
387
|
+
* @returns {{ pg: import('../geometry/types.js').Polygon, c: number[] }[]} Each crossing and its centre.
|
|
388
|
+
*/
|
|
389
|
+
export const crossings = (a, b, min = 40, key = ([x, y]) => x * 1000 + y) =>
|
|
390
|
+
I(a, b).filter((pg) => area(pg[0]) > min).map((pg) => ({ pg, c: centre(pg) })).sort((p, q) => key(p.c) - key(q.c));
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Two shapes laid over each other with an over-under: at each crossing one is in front and the other is cut back by the
|
|
394
|
+
* gap g round the front's strokes near that crossing (within `reach` of it). `order` picks the front: a string read
|
|
395
|
+
* crossing by crossing in `key` order ('ab': a in front at the first, b at the second, the last letter repeating), or
|
|
396
|
+
* `{ under: [[x, y]] }`: a in front everywhere except the crossings nearest those points, where b is.
|
|
397
|
+
*
|
|
398
|
+
* @param {import('../geometry/types.js').Geom} a One shape.
|
|
399
|
+
* @param {import('../geometry/types.js').Geom} b The other.
|
|
400
|
+
* @param {string | { under?: number[][] }} order Which is in front at each crossing.
|
|
401
|
+
* @param {number} [g] The gap (22).
|
|
402
|
+
* @param {{ reach?: number, key?: (c: number[]) => number, min?: number }} [o] How far from a crossing the cut runs (80),
|
|
403
|
+
* the crossings' order and the smallest crossing (40).
|
|
404
|
+
* @returns {{ s: import('../geometry/types.js').Shape, n: number, xs: { pg: import('../geometry/types.js').Polygon, c: number[] }[] }}
|
|
405
|
+
* The pair, how many crossings, and the crossings.
|
|
406
|
+
*/
|
|
407
|
+
export const interlace = (a, b, order, g = GAP, { reach = 80, key, min = 40 } = {}) => {
|
|
408
|
+
const xs = crossings(a, b, min, key); let A = a, B = b;
|
|
409
|
+
const tucked = new Set((order.under || []).map(([x, y]) => xs.reduce((bi, q, i) => (Math.hypot(q.c[0] - x, q.c[1] - y) < Math.hypot(xs[bi].c[0] - x, xs[bi].c[1] - y) ? i : bi), 0)));
|
|
410
|
+
const front = (i) => (typeof order === 'string' ? (order[i] || order.at(-1)) === 'a' : !tucked.has(i));
|
|
411
|
+
let ga, gb; // each letter grown by the gap once; a crossing cuts the other back by it within reach of the crossing
|
|
412
|
+
xs.forEach(({ pg }, i) => {
|
|
413
|
+
const near = grown([pg], reach);
|
|
414
|
+
if (front(i)) B = D(B, I(ga ||= grow(a, g), near)); else A = D(A, I(gb ||= grow(b, g), near));
|
|
415
|
+
});
|
|
416
|
+
return { s: U(A, B), n: xs.length, xs };
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* A tucked serif pair: the first letter a leads at cap H, the second b at H·kb sits fx of the lead's width to the right
|
|
421
|
+
* and fy·H lower, behind it, cut back by the gap where they cross (`order` as interlace()). The hairline is set so it fits
|
|
422
|
+
* to 31 at 448.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} a The lead (a SERIF_GLYPHS key).
|
|
425
|
+
* @param {string} b The second letter.
|
|
426
|
+
* @param {{ H?: number, kb?: number, fx?: number, fy?: number, order?: string | { under?: number[][] } }} [o] The lead's
|
|
427
|
+
* cap (360), the second's scale (0.8), its offset across (0.65) and down (0.2), and the over-under ('a').
|
|
428
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build, for exact().
|
|
429
|
+
*/
|
|
430
|
+
export const pairOf = (a, b, { H = 360, kb = 0.8, fx = 0.65, fy = 0.2, order = 'a' } = {}) => (g) => {
|
|
431
|
+
const draw = (h) => {
|
|
432
|
+
const ma = face(H, { h }), mb = face(H * kb, { h }), A = serifLetter(a, ma), B = serifLetter(b, mb, A.w * fx, H * fy);
|
|
433
|
+
return despeckShape(interlace(A.s, B.s, order, g).s, 500, 30);
|
|
434
|
+
};
|
|
435
|
+
const [x0, y0, x1, y1] = bbox(draw(32)); return draw(Math.ceil((31 * Math.max(x1 - x0, y1 - y0)) / 448));
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/* ---------- frames ---------- */
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* A stressed oval frame centred on (cx, cy), rx × ry: its sides t thick, its top and bottom hairlines h.
|
|
442
|
+
*
|
|
443
|
+
* @param {number} cx The centre.
|
|
444
|
+
* @param {number} cy The centre.
|
|
445
|
+
* @param {number} rx The radius across.
|
|
446
|
+
* @param {number} ry The radius down.
|
|
447
|
+
* @param {number} t The sides' thickness.
|
|
448
|
+
* @param {number} h The hairline.
|
|
449
|
+
* @returns {import('../geometry/types.js').Shape} The frame.
|
|
450
|
+
*/
|
|
451
|
+
export const ovalFrame = (cx, cy, rx, ry, t, h) => ringArc(cx, cy, rx, ry, t, h).s;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* A rectangle's four corners.
|
|
455
|
+
*
|
|
456
|
+
* @param {number} cx The centre.
|
|
457
|
+
* @param {number} cy The centre.
|
|
458
|
+
* @param {number} w The width.
|
|
459
|
+
* @param {number} h The height.
|
|
460
|
+
* @returns {number[][]} The corners.
|
|
461
|
+
*/
|
|
462
|
+
const corners = (cx, cy, w, h) => [[cx - w / 2, cy - h / 2], [cx + w / 2, cy - h / 2], [cx - w / 2, cy + h / 2], [cx + w / 2, cy + h / 2]];
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* A cartouche: a rectangle w × h centred on (cx, cy) with its corners scooped out by quarter circles of radius r.
|
|
466
|
+
*
|
|
467
|
+
* @param {number} cx The centre.
|
|
468
|
+
* @param {number} cy The centre.
|
|
469
|
+
* @param {number} w The width.
|
|
470
|
+
* @param {number} h The height.
|
|
471
|
+
* @param {number} r The scoops' radius.
|
|
472
|
+
* @returns {import('../geometry/types.js').Shape} The cartouche.
|
|
473
|
+
*/
|
|
474
|
+
export const cartouche = (cx, cy, w, h, r) => D(rect(cx - w / 2, cy - h / 2, w, h), ...corners(cx, cy, w, h).map(([x, y]) => circle(x, y, r, 160)));
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* A cartouche's outline, t thick: its inner edge scooped round the same corner centres at r + t.
|
|
478
|
+
*
|
|
479
|
+
* @param {number} cx The centre.
|
|
480
|
+
* @param {number} cy The centre.
|
|
481
|
+
* @param {number} w The width.
|
|
482
|
+
* @param {number} h The height.
|
|
483
|
+
* @param {number} r The scoops' radius.
|
|
484
|
+
* @param {number} t The outline's thickness.
|
|
485
|
+
* @returns {import('../geometry/types.js').Shape} The outline.
|
|
486
|
+
*/
|
|
487
|
+
export const cartoucheRule = (cx, cy, w, h, r, t) => D(cartouche(cx, cy, w, h, r),
|
|
488
|
+
D(rect(cx - w / 2 + t, cy - h / 2 + t, w - 2 * t, h - 2 * t), ...corners(cx, cy, w, h).map(([x, y]) => circle(x, y, r + t, 160))));
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* A hairline rule from x0 to x1 at height y, h thick, its ends tapered to a point over `taper`.
|
|
492
|
+
*
|
|
493
|
+
* @param {number} x0 Its left end.
|
|
494
|
+
* @param {number} x1 Its right end.
|
|
495
|
+
* @param {number} y Its top.
|
|
496
|
+
* @param {number} h Its thickness.
|
|
497
|
+
* @param {number} [taper=0] How long each tapered end is (0: square ends).
|
|
498
|
+
* @returns {import('../geometry/types.js').Polygon} The rule.
|
|
499
|
+
*/
|
|
500
|
+
export const rule = (x0, x1, y, h, taper = 0) => taper ? poly([[x0, y + h / 2], [x0 + taper, y], [x1 - taper, y], [x1, y + h / 2], [x1 - taper, y + h], [x0 + taper, y + h]]) : rect(x0, y, x1 - x0, h);
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* An octagonal plaque: a rectangle w × h centred on (cx, cy) with its corners chamfered c.
|
|
504
|
+
*
|
|
505
|
+
* @param {number} cx The centre.
|
|
506
|
+
* @param {number} cy The centre.
|
|
507
|
+
* @param {number} w The width.
|
|
508
|
+
* @param {number} h The height.
|
|
509
|
+
* @param {number} c The chamfer.
|
|
510
|
+
* @returns {import('../geometry/types.js').Polygon} The plaque.
|
|
511
|
+
*/
|
|
512
|
+
export const octagon = (cx, cy, w, h, c) => {
|
|
513
|
+
const x0 = cx - w / 2, y0 = cy - h / 2, x1 = cx + w / 2, y1 = cy + h / 2;
|
|
514
|
+
return poly([[x0 + c, y0], [x1 - c, y0], [x1, y0 + c], [x1, y1 - c], [x1 - c, y1], [x0 + c, y1], [x0, y1 - c], [x0, y0 + c]]);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* An octagonal plaque's outline, t thick (its inner edge parallel, so the chamfers stay even).
|
|
519
|
+
*
|
|
520
|
+
* @param {number} cx The centre.
|
|
521
|
+
* @param {number} cy The centre.
|
|
522
|
+
* @param {number} w The width.
|
|
523
|
+
* @param {number} h The height.
|
|
524
|
+
* @param {number} c The chamfer.
|
|
525
|
+
* @param {number} t The outline's thickness.
|
|
526
|
+
* @returns {import('../geometry/types.js').Shape} The outline.
|
|
527
|
+
*/
|
|
528
|
+
export const octagonRule = (cx, cy, w, h, c, t) => { const k = t * (Math.SQRT2 - 1); return D(octagon(cx, cy, w, h, c), octagon(cx, cy, w - 2 * t, h - 2 * t, c - k)); };
|
|
529
|
+
|
|
530
|
+
/* ---------- the moves ---------- */
|
|
531
|
+
// One-move builds. Each takes a glyph source `src(H, o)` → { s, w, m }: the capital at cap H on the glyph frame above (cap
|
|
532
|
+
// line y 0, baseline y H, ink from x 0) and its face m (H, S, h). `drawn(k)` is this face's capital k; /font's
|
|
533
|
+
// `fontSource()` is a font's. Each move returns `(g) => shape` on the 512 canvas for exact().
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* This face's capital k as a glyph source.
|
|
537
|
+
*
|
|
538
|
+
* @param {string} k The letter (a SERIF_GLYPHS key).
|
|
539
|
+
* @returns {import('./serif.js').GlyphSource} The source: `o` is face()'s options (stem S…), plus the gap g, which it
|
|
540
|
+
* ignores.
|
|
541
|
+
*/
|
|
542
|
+
export const drawn = (k) => (H, o = {}) => { const m = face(H, o); return { ...serifLetter(k, m), m }; };
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* A five-point star of radius R centred on (cx, cy), its inner radius R·k.
|
|
546
|
+
*
|
|
547
|
+
* @param {number} cx The centre.
|
|
548
|
+
* @param {number} cy The centre.
|
|
549
|
+
* @param {number} R The outer radius.
|
|
550
|
+
* @param {number} [k=0.42] The inner radius, as a fraction of R.
|
|
551
|
+
* @returns {import('../geometry/types.js').Polygon} The star.
|
|
552
|
+
*/
|
|
553
|
+
export const star5 = (cx, cy, R, k = 0.42) => poly([...Array(10)].map((_, i) => { const a = ((-90 + i * 36) * Math.PI) / 180, r = i % 2 ? R * k : R; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }));
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The capital alone, centred (a font edition of a mark whose move is the letter itself).
|
|
557
|
+
*
|
|
558
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
559
|
+
* @param {number} [H=380] The cap height.
|
|
560
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
561
|
+
*/
|
|
562
|
+
export const plain = (src, H = 380) => (g) => centred(src(H, { g }), H, 256, 256);
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* A capital standing a gap above a tapered hairline rule `over` wider than it each side (Rule).
|
|
566
|
+
*
|
|
567
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
568
|
+
* @param {number} H The cap height.
|
|
569
|
+
* @param {number} [over=60] How far the rule runs past the letter each side.
|
|
570
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
571
|
+
*/
|
|
572
|
+
export const ruled = (src, H, over = 60) => (g) => {
|
|
573
|
+
const { s, w, m } = src(H, { g }), x0 = 256 - w / 2, y0 = 256 - (H + g + m.h) / 2;
|
|
574
|
+
return U(move(s, x0, y0), rule(x0 - over, x0 + w + over, y0 + H + g * 1.4, m.h, over * 0.9));
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* A capital cut once across by the gap (Split).
|
|
579
|
+
*
|
|
580
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
581
|
+
* @param {number} H The cap height.
|
|
582
|
+
* @param {(m: import('./serif.js').Face, w: number, g: number, L: import('./serif.js').SourceGlyph) => import('../geometry/types.js').Geom} cut
|
|
583
|
+
* The strip removed, in the glyph's own coordinates.
|
|
584
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
585
|
+
*/
|
|
586
|
+
export const split = (src, H, cut) => (g) => { const L = src(H, { g }); return move(D(L.s, cut(L.m, L.w, g, L)), 256 - L.w / 2, 256 - H / 2); };
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* A capital with a ball (a full stop) or a five-point star placed by `at` (Dot, Star).
|
|
590
|
+
*
|
|
591
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
592
|
+
* @param {number} H The cap height.
|
|
593
|
+
* @param {(m: import('./serif.js').Face, w: number, g: number, L: import('./serif.js').SourceGlyph) => number[]} at
|
|
594
|
+
* Where it goes: [cx, cy, r] in the glyph's coordinates.
|
|
595
|
+
* @param {'dot' | 'star'} [kind='dot'] A ball or a star.
|
|
596
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
597
|
+
*/
|
|
598
|
+
export const dotted = (src, H, at, kind = 'dot') => (g) => {
|
|
599
|
+
const L = src(H, { g }), [cx, cy, r] = at(L.m, L.w, g, L);
|
|
600
|
+
const mark = kind === 'star' ? star5(cx, cy, r) : circle(cx, cy, r, 160), s = U(L.s, mark), [x0, , x1] = bbox(s); return move(s, 256 - (x0 + x1) / 2, 256 - H / 2);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* A capital whose base serifs run together into one hairline plinth, reaching `over` past the letter (Plinth).
|
|
605
|
+
*
|
|
606
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
607
|
+
* @param {number} H The cap height.
|
|
608
|
+
* @param {number} [over=40] How far the plinth runs past the letter each side.
|
|
609
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
610
|
+
*/
|
|
611
|
+
export const plinth = (src, H, over = 40) => (g) => { const { s, w, m } = src(H, { g }); return move(U(s, rect(-over, m.H - m.h, w + 2 * over, m.h)), 256 - w / 2, 256 - H / 2); };
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The glyph centred on (cx, cy); a source whose ink sits m.dy above its cap box (a grown font glyph) is moved down by it.
|
|
615
|
+
*
|
|
616
|
+
* @param {import('./serif.js').SourceGlyph} L The glyph.
|
|
617
|
+
* @param {number} H The cap height.
|
|
618
|
+
* @param {number} cx The centre.
|
|
619
|
+
* @param {number} cy The centre.
|
|
620
|
+
* @returns {import('../geometry/types.js').Shape} The glyph, placed.
|
|
621
|
+
*/
|
|
622
|
+
const centred = ({ s, w, m }, H, cx, cy) => move(s, cx - w / 2, cy - H / 2 + (m.dy || 0));
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* A capital (stems 0.25 of the cap) in a frame, centred a gap or more inside it (Frame).
|
|
626
|
+
*
|
|
627
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
628
|
+
* @param {number} H The cap height.
|
|
629
|
+
* @param {(g: number) => import('../geometry/types.js').Geom} frame The frame.
|
|
630
|
+
* @param {{ dx?: number, dy?: number }} [o] The letter's offset from the centre.
|
|
631
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
632
|
+
*/
|
|
633
|
+
export const framed = (src, H, frame, o = {}) => (g) => U(frame(g), centred(src(H, { S: Math.round(H * 0.25), g }), H, 256 + (o.dx || 0), 256 + (o.dy || 0)));
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* A double frame: the outer band, a gap, a hairline inner rule, and the capital (stems 0.27) inside (Cartouche).
|
|
637
|
+
*
|
|
638
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
639
|
+
* @param {number} H The cap height.
|
|
640
|
+
* @param {(g: number) => import('../geometry/types.js').Geom} outer The outer band.
|
|
641
|
+
* @param {(g: number) => import('../geometry/types.js').Geom} inner The inner rule.
|
|
642
|
+
* @param {{ dx?: number, dy?: number }} [o] The letter's offset from the centre.
|
|
643
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
644
|
+
*/
|
|
645
|
+
export const crest = (src, H, outer, inner, o = {}) => (g) => U(outer(g), inner(g), centred(src(H, { S: Math.round(H * 0.27), g }), H, 256 + (o.dx || 0), 256 + (o.dy || 0)));
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* A solid plaque with a hairline inner rule and the capital (stems 0.25) knocked out of it.
|
|
649
|
+
*
|
|
650
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
651
|
+
* @param {number} H The cap height.
|
|
652
|
+
* @param {() => import('../geometry/types.js').Geom} shape The plaque.
|
|
653
|
+
* @param {(g: number) => import('../geometry/types.js').Geom} ruleFn The inner rule.
|
|
654
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
655
|
+
*/
|
|
656
|
+
export const plaque = (src, H, shape, ruleFn) => (g) => D(shape(), ruleFn(g), centred(src(H, { S: Math.round(H * 0.25), g }), H, 256, 256));
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The fat-face inline: the capital (stems 0.26 of the cap, the framed weight) less itself shrunk by (S − g) ÷ 2, so every
|
|
660
|
+
* thick stroke keeps a g-wide channel down its middle that stops short of its ends and the hairlines stay whole (Inline).
|
|
661
|
+
*
|
|
662
|
+
* @param {import('./serif.js').GlyphSource} src The glyph source.
|
|
663
|
+
* @param {number} [H=380] The cap height.
|
|
664
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
665
|
+
*/
|
|
666
|
+
export const inline = (src, H = 380) => (g) => { const { s, w, m } = src(H, { S: Math.round(H * 0.26), g }); return move(D(s, grow(s, -(m.S - g) / 2)), 256 - w / 2, 256 - H / 2); };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The grid: the cell P, the neck's width NECK, the corner radius EDGE and the neck roots' fillet FIL. */
|
|
4
|
+
export const STAIR: { P: number; NECK: number; EDGE: number; FIL: number };
|
|
5
|
+
/** A bar NECK wide along each corner-only contact's diagonal. */
|
|
6
|
+
export function necks(rows: readonly string[], o?: { P?: number; NECK?: number }): Polygon[];
|
|
7
|
+
/** A stair glyph's build: the cells as one solid, corners rounded, corner contacts necked. */
|
|
8
|
+
export function stair(rows: readonly string[], o?: { P?: number; NECK?: number; EDGE?: number; FIL?: number }): () => Shape;
|