@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,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Letters from font files (Node): a font's capital as a kit shape on the serif and blackletter faces' frame, grown so its
|
|
3
|
+
* hairline holds, the measures to place a move on it, and the moves. Fonts are passed in as paths or bytes; opentype.js,
|
|
4
|
+
* clipper-lib and @resvg/resvg-js are optional peers, loaded on first use.
|
|
5
|
+
*/
|
|
6
|
+
export type { FontSource, Font } from './glyphs.js';
|
|
7
|
+
export type { LetterMove } from './moves.js';
|
|
8
|
+
export { loadFont, capUnits, nonzeroShape, glyphShape, mitreGrow, weights, hairline, structuralHairline, fontSource, onBase } from './glyphs.js';
|
|
9
|
+
export { inside, runsH, runsV, strokeDepths, armEnd, thinnestStroke } from './measure.js';
|
|
10
|
+
export { underCrossbar, underTopBar, underBowl, cornerDot, fullStop, starBefore, underFork, underBowlR, inlineOpen, inlineStraight, armLozenge, stemSplit, barLozenge, leftLozenge, tongueLozenge, barEnd, barEnds, crossLozenge } from './moves.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/font
|
|
3
|
+
*
|
|
4
|
+
* Letters from font files (Node): a font's capital as a kit shape on the serif and blackletter faces' frame, grown so its
|
|
5
|
+
* hairline holds, the measures to place a move on it, and the moves. Fonts are passed in as paths or bytes; opentype.js,
|
|
6
|
+
* clipper-lib and @resvg/resvg-js are optional peers, loaded on first use.
|
|
7
|
+
*/
|
|
8
|
+
export { loadFont, capUnits, nonzeroShape, glyphShape, mitreGrow, weights, hairline, structuralHairline, fontSource, onBase } from './glyphs.js';
|
|
9
|
+
export { inside, runsH, runsV, strokeDepths, armEnd, thinnestStroke } from './measure.js';
|
|
10
|
+
export { underCrossbar, underTopBar, underBowl, cornerDot, fullStop, starBefore, underFork, underBowlR, inlineOpen, inlineStraight, armLozenge, stemSplit, barLozenge, leftLozenge, tongueLozenge, barEnd, barEnds, crossLozenge } from './moves.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The winding number of a ring about a point. */
|
|
4
|
+
export function wind(ring: readonly (readonly number[])[], p: readonly number[]): number;
|
|
5
|
+
/** Whether (x, y) is in the shape's ink, by winding. */
|
|
6
|
+
export function inside(s: Shape, x: number, y: number): boolean;
|
|
7
|
+
/** The ink runs [a, b] along the horizontal line y. */
|
|
8
|
+
export function runsH(s: Shape, y: number, x0?: number, x1?: number): number[][];
|
|
9
|
+
/** The ink runs [a, b] along the vertical line x. */
|
|
10
|
+
export function runsV(s: Shape, x: number, y0?: number, y1?: number): number[][];
|
|
11
|
+
/** The ink's depth across its strokes from points along the outline, smallest first. */
|
|
12
|
+
export function strokeDepths(s: Shape, step?: number): number[];
|
|
13
|
+
/** Where a stroke ends on the right between y0 and y1, and its depth there. */
|
|
14
|
+
export function armEnd(s: Shape, y0: number, y1: number, o?: { step?: number; back?: number }): { x: number; y: number; th: number; yc: number };
|
|
15
|
+
/** The thinnest structural stroke, by erosion (needs @resvg/resvg-js). */
|
|
16
|
+
export function thinnestStroke(s: Shape, k?: number): number;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures for placing a move on a letter (a font's or any shape): the ink's runs along a line, the depth across its
|
|
3
|
+
* strokes, where an arm ends, and the thinnest stroke holding it together. From the Logomarks tools (fontparts.mjs,
|
|
4
|
+
* batches 242 and 243).
|
|
5
|
+
*/
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { bbox, toPath } from '../geometry/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The winding number of a ring about a point.
|
|
11
|
+
*
|
|
12
|
+
* @param {readonly (readonly number[])[]} ring The ring.
|
|
13
|
+
* @param {readonly number[]} p The point.
|
|
14
|
+
* @returns {number} The winding number.
|
|
15
|
+
*/
|
|
16
|
+
export const wind = (ring, [px, py]) => {
|
|
17
|
+
let w = 0;
|
|
18
|
+
for (let i = 0, n = ring.length; i < n; i++) {
|
|
19
|
+
const [x1, y1] = ring[i], [x2, y2] = ring[(i + 1) % n], c = (x2 - x1) * (py - y1) - (px - x1) * (y2 - y1);
|
|
20
|
+
if (y1 <= py ? y2 > py && c > 0 : y2 <= py && c < 0) w += y1 <= py ? 1 : -1;
|
|
21
|
+
}
|
|
22
|
+
return w;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether (x, y) is in the shape's ink: inside an outer ring and outside its holes, by winding.
|
|
27
|
+
*
|
|
28
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
29
|
+
* @param {number} x The point.
|
|
30
|
+
* @param {number} y The point.
|
|
31
|
+
* @returns {boolean} True when it is.
|
|
32
|
+
*/
|
|
33
|
+
export const inside = (s, x, y) => s.some((pg) => wind(pg[0], [x, y]) !== 0 && !pg.slice(1).some((h) => wind(h, [x, y]) !== 0));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The runs [a, b] where `on(t)` holds, for t from a0 to a1 in steps of 0.25.
|
|
37
|
+
*
|
|
38
|
+
* @param {(t: number) => boolean} on The test.
|
|
39
|
+
* @param {number} a0 Where to start.
|
|
40
|
+
* @param {number} a1 Where to stop.
|
|
41
|
+
* @returns {number[][]} The runs.
|
|
42
|
+
*/
|
|
43
|
+
const runs = (on, a0, a1) => {
|
|
44
|
+
const r = []; let a = null;
|
|
45
|
+
for (let t = a0; t <= a1; t += 0.25) { const v = on(t); if (v && a === null) a = t; if (!v && a !== null) { r.push([a, t]); a = null; } }
|
|
46
|
+
if (a !== null) r.push([a, a1]);
|
|
47
|
+
return r;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The ink runs [a, b] along the horizontal line y (x from x0 to x1, in steps of 0.25).
|
|
52
|
+
*
|
|
53
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
54
|
+
* @param {number} y The line.
|
|
55
|
+
* @param {number} [x0=-50] Where to start.
|
|
56
|
+
* @param {number} [x1=1000] Where to stop.
|
|
57
|
+
* @returns {number[][]} The runs, left to right.
|
|
58
|
+
*/
|
|
59
|
+
export const runsH = (s, y, x0 = -50, x1 = 1000) => runs((x) => inside(s, x, y), x0, x1);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The ink runs [a, b] along the vertical line x (y from y0 to y1, in steps of 0.25).
|
|
63
|
+
*
|
|
64
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
65
|
+
* @param {number} x The line.
|
|
66
|
+
* @param {number} [y0=-50] Where to start.
|
|
67
|
+
* @param {number} [y1=1000] Where to stop.
|
|
68
|
+
* @returns {number[][]} The runs, top to bottom.
|
|
69
|
+
*/
|
|
70
|
+
export const runsV = (s, x, y0 = -50, y1 = 1000) => runs((y) => inside(s, x, y), y0, y1);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The ink's depth across its strokes, sorted: from points every `step` along the outline, the distance along the inward
|
|
74
|
+
* normal to the far side (the ray against every edge). Its low end is the thinnest stroke (tips and tapered swash ends
|
|
75
|
+
* included, as a reader at 32 px sees them).
|
|
76
|
+
*
|
|
77
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
78
|
+
* @param {number} [step=3] The spacing along the outline.
|
|
79
|
+
* @returns {number[]} The depths, smallest first.
|
|
80
|
+
*/
|
|
81
|
+
export const strokeDepths = (s, step = 3) => {
|
|
82
|
+
const E = s.flatMap((pg) => pg.flatMap((r) => r.map((p, i) => [p, r[(i + 1) % r.length]]))), out = [];
|
|
83
|
+
for (const [p, q] of E) {
|
|
84
|
+
const L = Math.hypot(q[0] - p[0], q[1] - p[1]); if (L < 0.5) continue; const u = [(q[0] - p[0]) / L, (q[1] - p[1]) / L];
|
|
85
|
+
for (let t = step / 2; t < L; t += step) {
|
|
86
|
+
const x = p[0] + u[0] * t, y = p[1] + u[1] * t; let n = [u[1], -u[0]];
|
|
87
|
+
if (!inside(s, x + n[0] * 0.5, y + n[1] * 0.5)) n = [-n[0], -n[1]];
|
|
88
|
+
let best = Infinity;
|
|
89
|
+
for (const [a, b] of E) {
|
|
90
|
+
const ex = b[0] - a[0], ey = b[1] - a[1], den = n[0] * ey - n[1] * ex; if (Math.abs(den) < 1e-12) continue;
|
|
91
|
+
const wx = a[0] - x, wy = a[1] - y, d = (wx * ey - wy * ex) / den, v = (wx * n[1] - wy * n[0]) / den;
|
|
92
|
+
if (d > 0.3 && v >= 0 && v <= 1 && d < best) best = d;
|
|
93
|
+
}
|
|
94
|
+
if (best < Infinity) out.push(best);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out.sort((a, b) => a - b);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Where a stroke ends on the right inside the band of heights y0…y1 (a middle arm's tip): the rightmost ink point found
|
|
102
|
+
* on rows `step` apart, and the stroke's depth a little back from it (the run down through x − back).
|
|
103
|
+
*
|
|
104
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
105
|
+
* @param {number} y0 The band's top.
|
|
106
|
+
* @param {number} y1 Its bottom.
|
|
107
|
+
* @param {{ step?: number, back?: number }} [o] The rows' spacing (2) and how far back the depth is read (24).
|
|
108
|
+
* @returns {{ x: number, y: number, th: number, yc: number }} The tip, the stroke's depth there and its middle.
|
|
109
|
+
*/
|
|
110
|
+
export const armEnd = (s, y0, y1, { step = 2, back = 24 } = {}) => {
|
|
111
|
+
let best = { x: -Infinity, y: y0 };
|
|
112
|
+
for (let y = y0; y <= y1; y += step) { const r = runsH(s, y); if (r.length && r.at(-1)[1] > best.x) best = { x: r.at(-1)[1], y }; }
|
|
113
|
+
const run = runsV(s, best.x - back).find(([a, b]) => a <= best.y + 2 && b >= best.y - 2) || [best.y - 30, best.y + 30];
|
|
114
|
+
return { ...best, th: run[1] - run[0], yc: (run[0] + run[1]) / 2 };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/* ---------- the structural stroke ---------- */
|
|
118
|
+
// strokeDepths() counts tips and spurs: right for a swash face, but a spiked face has so many sharp tips that its low end
|
|
119
|
+
// reads far thinner than its thinnest real stroke. thinnestStroke() measures by erosion instead: the glyph is rasterised k
|
|
120
|
+
// px a unit, eroded by r (a pixel survives while it is r or more from paper), and r raised until the topology changes (a
|
|
121
|
+
// piece splits off, or a counter opens into another). The stroke that breaks first is the thinnest stroke holding the
|
|
122
|
+
// letter together; tips, serif ends and spurs shrink without changing the topology, so they don't count. The raster is
|
|
123
|
+
// @resvg/resvg-js's (an optional peer, loaded on first use): core's own scanline fill differs from it on edge pixels.
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The shape as a 0/1 mask, k px a unit, with a 6 px margin of paper.
|
|
127
|
+
*
|
|
128
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
129
|
+
* @param {number} k Pixels a unit.
|
|
130
|
+
* @returns {{ M: Uint8Array, W: number, H: number }} The mask and its size.
|
|
131
|
+
*/
|
|
132
|
+
const mask = (s, k) => {
|
|
133
|
+
const { Resvg } = createRequire(import.meta.url)('@resvg/resvg-js'), [x0, y0, x1, y1] = bbox(s), m = 6;
|
|
134
|
+
const W = Math.ceil((x1 - x0) * k) + 2 * m, H = Math.ceil((y1 - y0) * k) + 2 * m;
|
|
135
|
+
const px = new Resvg(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}"><path shape-rendering="crispEdges" fill-rule="evenodd" fill="#000" transform="translate(${m} ${m}) scale(${k}) translate(${-x0} ${-y0})" d="${toPath(s)}"/></svg>`).render().pixels;
|
|
136
|
+
const M = new Uint8Array(W * H); for (let i = 0; i < W * H; i++) M[i] = px[i * 4 + 3] > 127 ? 1 : 0;
|
|
137
|
+
return { M, W, H };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The squared distance from each ink pixel to the nearest paper pixel (Felzenszwalb and Huttenlocher's two-pass
|
|
142
|
+
* transform).
|
|
143
|
+
*
|
|
144
|
+
* @param {{ M: Uint8Array, W: number, H: number }} mk The mask.
|
|
145
|
+
* @returns {Float64Array} The squared distances, row by row.
|
|
146
|
+
*/
|
|
147
|
+
const edt = ({ M, W, H }) => {
|
|
148
|
+
const INF = 1e12, Dt = new Float64Array(W * H), n = Math.max(W, H), f = new Float64Array(n), d = new Float64Array(n), v = new Int32Array(n), z = new Float64Array(n + 1);
|
|
149
|
+
const pass = (len) => {
|
|
150
|
+
let k = 0; v[0] = 0; z[0] = -INF; z[1] = INF;
|
|
151
|
+
for (let q = 1; q < len; q++) {
|
|
152
|
+
let s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
|
|
153
|
+
while (s <= z[k]) { k--; s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); }
|
|
154
|
+
k++; v[k] = q; z[k] = s; z[k + 1] = INF;
|
|
155
|
+
}
|
|
156
|
+
k = 0; for (let q = 0; q < len; q++) { while (z[k + 1] < q) k++; d[q] = (q - v[k]) * (q - v[k]) + f[v[k]]; }
|
|
157
|
+
};
|
|
158
|
+
for (let x = 0; x < W; x++) { for (let y = 0; y < H; y++) f[y] = M[y * W + x] ? INF : 0; pass(H); for (let y = 0; y < H; y++) Dt[y * W + x] = d[y]; }
|
|
159
|
+
for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) f[x] = Dt[y * W + x]; pass(W); for (let x = 0; x < W; x++) Dt[y * W + x] = d[x]; }
|
|
160
|
+
return Dt;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* How many connected pieces there are where on(i) holds (8-connected for ink, 4 for paper), ignoring any under minPx.
|
|
165
|
+
*
|
|
166
|
+
* @param {number} W The width.
|
|
167
|
+
* @param {number} H The height.
|
|
168
|
+
* @param {(i: number) => boolean} on Whether pixel i counts.
|
|
169
|
+
* @param {boolean} eight 8-connected (else 4).
|
|
170
|
+
* @param {number} [minPx=4] The smallest piece that counts.
|
|
171
|
+
* @returns {number} The count.
|
|
172
|
+
*/
|
|
173
|
+
const pieces = (W, H, on, eight, minPx = 4) => {
|
|
174
|
+
const seen = new Uint8Array(W * H), st = new Int32Array(W * H); let n = 0;
|
|
175
|
+
const nb = eight ? [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]] : [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
|
176
|
+
for (let i = 0; i < W * H; i++) {
|
|
177
|
+
if (seen[i] || !on(i)) continue; let sp = 0, size = 0; st[sp++] = i; seen[i] = 1;
|
|
178
|
+
while (sp) {
|
|
179
|
+
const j = st[--sp], x = j % W, y = (j - x) / W; size++;
|
|
180
|
+
for (const [dx, dy] of nb) { const X = x + dx, Y = y + dy; if (X < 0 || Y < 0 || X >= W || Y >= H) continue; const q = Y * W + X; if (!seen[q] && on(q)) { seen[q] = 1; st[sp++] = q; } }
|
|
181
|
+
}
|
|
182
|
+
if (size >= minPx) n++;
|
|
183
|
+
}
|
|
184
|
+
return n;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The thinnest structural stroke of a shape, in its units (±1/k): the erosion radius at which a piece splits off or a
|
|
189
|
+
* counter opens. Needs @resvg/resvg-js (an optional peer).
|
|
190
|
+
*
|
|
191
|
+
* @param {import('../geometry/types.js').Shape} s The shape.
|
|
192
|
+
* @param {number} [k=2] Pixels a unit.
|
|
193
|
+
* @returns {number} The stroke's width (Infinity when nothing breaks before r 80).
|
|
194
|
+
*/
|
|
195
|
+
export const thinnestStroke = (s, k = 2) => {
|
|
196
|
+
const mk = mask(s, k), Dt = edt(mk), { W, H } = mk;
|
|
197
|
+
const topo = (r) => { const r2 = r * r; return [pieces(W, H, (i) => Dt[i] >= r2, true), pieces(W, H, (i) => Dt[i] < r2, false)]; };
|
|
198
|
+
let [ink, paper] = topo(1);
|
|
199
|
+
for (let r = 1.5; r < 80; r += 0.5) { const [a, b] = topo(r); if (a > ink || b < paper) return (2 * (r - 0.75)) / k; ink = Math.min(ink, a); paper = b; }
|
|
200
|
+
return Infinity;
|
|
201
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
import type { Face, Placer, SourceGlyph, GlyphSource } from '../letters/serif.js';
|
|
3
|
+
|
|
4
|
+
/** A move on a blackletter letter: the letter and the gap to the new letter. */
|
|
5
|
+
export type LetterMove = (s: Shape, g: number) => Shape;
|
|
6
|
+
|
|
7
|
+
/** Split (A): the gap just under the crossbar. */
|
|
8
|
+
export function underCrossbar(m: Face, w: number, g: number, L: SourceGlyph): Polygon;
|
|
9
|
+
/** Split (T): the gap right under the top bar. */
|
|
10
|
+
export function underTopBar(at?: number): Placer<Polygon>;
|
|
11
|
+
/** Split (P): the gap across the stem under the bowl. */
|
|
12
|
+
export function underBowl(below?: number): Placer<Polygon>;
|
|
13
|
+
/** Dot (L): a ball in the L's corner, clear of the foot. */
|
|
14
|
+
export function cornerDot(o?: { along?: number; from?: number; clear?: number; r?: number }): Placer<number[]>;
|
|
15
|
+
/** Stop (H): a full stop a gap after the letter. */
|
|
16
|
+
export function fullStop(k?: number): Placer<number[]>;
|
|
17
|
+
/** Star (J): a five-point star before the stem. */
|
|
18
|
+
export function starBefore(o?: { k?: number; y?: number }): Placer<number[]>;
|
|
19
|
+
/** Split (Y): the gap just under the fork. */
|
|
20
|
+
export function underFork(m: Face, w: number, g: number, L: SourceGlyph): Polygon;
|
|
21
|
+
/** Split (R): the gap across the stem only, under the bowl's foot. */
|
|
22
|
+
export function underBowlR(m: Face, w: number, g: number, L: SourceGlyph): Polygon;
|
|
23
|
+
/** The serif inline, its channel opened where a stroke tapers. */
|
|
24
|
+
export function inlineOpen(src: GlyphSource, H?: number, open?: number): (g: number) => Shape;
|
|
25
|
+
/** The inline with a straight channel, for straight-stroked letters. */
|
|
26
|
+
export function inlineStraight(src: GlyphSource, H?: number, open?: number): (g: number) => Shape;
|
|
27
|
+
/** A parted lozenge on a font's middle arm. */
|
|
28
|
+
export function armLozenge(cap: number, y0: number, y1: number, k?: number): LetterMove;
|
|
29
|
+
/** A split stem down from the head or up from the foot. */
|
|
30
|
+
export function stemSplit(cap: number, end: 'head' | 'foot', depth: number): LetterMove;
|
|
31
|
+
/** A lozenge set across the H's bar. */
|
|
32
|
+
export function barLozenge(cap: number, o?: { k?: number; at?: number }): LetterMove;
|
|
33
|
+
/** armLozenge mirrored, for a stroke ending on the left. */
|
|
34
|
+
export function leftLozenge(cap: number, y0: number, y1: number, k?: number): LetterMove;
|
|
35
|
+
/** A short middle tongue taken off whole, a lozenge a gap clear of the stem. */
|
|
36
|
+
export function tongueLozenge(cap: number, y0: number, y1: number, z?: number): LetterMove;
|
|
37
|
+
/** A bar's right end run into a level parted lozenge. */
|
|
38
|
+
export function barEnd(cap: number, o?: { back?: number; k?: number }): LetterMove;
|
|
39
|
+
/** barEnd at both ends of the bar. */
|
|
40
|
+
export function barEnds(cap: number, o?: { back?: number; k?: number }): LetterMove;
|
|
41
|
+
/** A parted lozenge at the crossing or the fork. */
|
|
42
|
+
export function crossLozenge(cap: number, y0: number, y1: number, k?: number): LetterMove;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moves placed on a font's letter: each measures the font's own letter (its bar, stem, bowl or arm), never a house
|
|
3
|
+
* face's fractions, so every font edition places its source mark's move the same way. The serif ones are the `cut` and
|
|
4
|
+
* `at` arguments of /letters' split() and dotted(): `(m, w, g, L)`, with L the glyph source's { s, w, m }; the blackletter
|
|
5
|
+
* ones are `(s, g) => shape` on a letter at cap `cap`. From the Logomarks tools (fontparts.mjs: batch 242's placements,
|
|
6
|
+
* 243's blackletter ones, 292's and 298's inlines and cuts, 312's tongue and 334's bar ends).
|
|
7
|
+
*/
|
|
8
|
+
import { U, D, I, move, bbox, rect, circle, flipX, grow, poly } from '../geometry/index.js';
|
|
9
|
+
import { parted, lozenge, splitHead, splitFoot } from '../letters/blackletter.js';
|
|
10
|
+
import { runsH, runsV, armEnd } from './measure.js';
|
|
11
|
+
|
|
12
|
+
/* ---------- serif moves: split, dot and star ---------- */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split (A): the gap across the letter just UNDER its crossbar (a gap through a 29 hairline leaves slivers), the bar
|
|
16
|
+
* found down the counter's middle at 0.8 of the cap.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../letters/serif.js').Face} m The glyph's face.
|
|
19
|
+
* @param {number} w Its width.
|
|
20
|
+
* @param {number} g The gap.
|
|
21
|
+
* @param {import('../letters/serif.js').SourceGlyph} L The glyph.
|
|
22
|
+
* @returns {import('../geometry/types.js').Polygon} The strip to cut.
|
|
23
|
+
*/
|
|
24
|
+
export const underCrossbar = (m, w, g, L) => {
|
|
25
|
+
const [l, r] = runsH(L.s, m.H * 0.8), x = (l[1] + r[0]) / 2;
|
|
26
|
+
const bar = runsV(L.s, x).find(([a]) => a > m.H * 0.3); return rect(-10, bar[1], w + 20, g);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Split (T): the gap right under the top bar, measured `at` of the width in.
|
|
31
|
+
*
|
|
32
|
+
* @param {number} [at=0.3] Where across the bar is measured.
|
|
33
|
+
* @returns {import('../letters/serif.js').Placer<import('../geometry/types.js').Polygon>} The cut.
|
|
34
|
+
*/
|
|
35
|
+
export const underTopBar = (at = 0.3) => (m, w, g, L) => rect(-10, runsV(L.s, w * at)[0][1], w + 20, g);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Split (P): the gap across the stem `below` under the bowl (the bowl's foot found halfway out from the stem).
|
|
39
|
+
*
|
|
40
|
+
* @param {number} [below=6] How far under the bowl's foot.
|
|
41
|
+
* @returns {import('../letters/serif.js').Placer<import('../geometry/types.js').Polygon>} The cut.
|
|
42
|
+
*/
|
|
43
|
+
export const underBowl = (below = 6) => (m, w, g, L) => {
|
|
44
|
+
const x = runsH(L.s, m.H * 0.8)[0][1], y = runsV(L.s, (x + w) / 2).at(-1)[1];
|
|
45
|
+
return rect(-10, y + below, w + 20, g);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Dot (L): a ball of half the stem in the L's corner, `along` the way from the stem to the arm's end, risen from `from`
|
|
50
|
+
* of the cap until it sits `clear` gaps off the foot (a font's foot curves up into a beak).
|
|
51
|
+
*
|
|
52
|
+
* @param {{ along?: number, from?: number, clear?: number, r?: number }} [o] Where across (0.42), where it starts (0.62
|
|
53
|
+
* of the cap), its clearance in gaps (1.5) and its radius as a fraction of the stem (0.5).
|
|
54
|
+
* @returns {import('../letters/serif.js').Placer<number[]>} The placement: [cx, cy, r].
|
|
55
|
+
*/
|
|
56
|
+
export const cornerDot = ({ along = 0.42, from = 0.62, clear = 1.5, r: rk = 0.5 } = {}) => (m, w, g, L) => {
|
|
57
|
+
const x = runsH(L.s, m.H * 0.5)[0][1], cx = x + (w - x) * along, r = m.S * rk; let cy = m.H * from;
|
|
58
|
+
while (I(circle(cx, cy, r + g * clear, 64), L.s).length && cy > m.H * 0.4) cy -= 2;
|
|
59
|
+
return [cx, cy, r];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stop (H): a full stop (radius `k` of the stem) a gap after the letter, sitting on the baseline.
|
|
64
|
+
*
|
|
65
|
+
* @param {number} [k=0.52] The radius, as a fraction of the stem.
|
|
66
|
+
* @returns {import('../letters/serif.js').Placer<number[]>} The placement: [cx, cy, r].
|
|
67
|
+
*/
|
|
68
|
+
export const fullStop = (k = 0.52) => (m, w, g) => [w + g + m.S * k, m.H - m.S * k, m.S * k];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Star (J): a five-point star (radius `k` of the cap) a radius before the stem at 0.3 of the cap, its centre at `y` of
|
|
72
|
+
* the cap.
|
|
73
|
+
*
|
|
74
|
+
* @param {{ k?: number, y?: number }} [o] The radius (0.13) and the height (0.42), as fractions of the cap.
|
|
75
|
+
* @returns {import('../letters/serif.js').Placer<number[]>} The placement: [cx, cy, r].
|
|
76
|
+
*/
|
|
77
|
+
export const starBefore = ({ k = 0.13, y = 0.42 } = {}) => (m, w, g, L) => {
|
|
78
|
+
const r = m.H * k, x = runsH(L.s, m.H * 0.3)[0][0];
|
|
79
|
+
return [x - m.H * k - r * 0.95, m.H * y, r];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Split (Y): the gap across the letter just under the fork, where the two arms have run into one stem.
|
|
84
|
+
*
|
|
85
|
+
* @param {import('../letters/serif.js').Face} m The glyph's face.
|
|
86
|
+
* @param {number} w Its width.
|
|
87
|
+
* @param {number} g The gap.
|
|
88
|
+
* @param {import('../letters/serif.js').SourceGlyph} L The glyph.
|
|
89
|
+
* @returns {import('../geometry/types.js').Polygon} The strip to cut.
|
|
90
|
+
*/
|
|
91
|
+
export const underFork = (m, w, g, L) => { let y = m.H * 0.25; while (runsH(L.s, y).length > 1 && y < m.H * 0.8) y += 1; return rect(-10, y, w + 20, g); };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Split (R): the gap across the stem just under the bowl's foot, cutting only the stem (the leg runs on whole), so the
|
|
95
|
+
* bowl and the leg float as one piece beside the stem's foot.
|
|
96
|
+
*
|
|
97
|
+
* @param {import('../letters/serif.js').Face} m The glyph's face.
|
|
98
|
+
* @param {number} w Its width.
|
|
99
|
+
* @param {number} g The gap.
|
|
100
|
+
* @param {import('../letters/serif.js').SourceGlyph} L The glyph.
|
|
101
|
+
* @returns {import('../geometry/types.js').Polygon} The strip to cut.
|
|
102
|
+
*/
|
|
103
|
+
export const underBowlR = (m, w, g, L) => {
|
|
104
|
+
const x = runsH(L.s, m.H * 0.25)[0][1] + 4, y = runsV(L.s, x + 30).find(([a]) => a > m.H * 0.3)[1];
|
|
105
|
+
return rect(-10, y + 4, runsH(L.s, y + 4 + g / 2)[0][1] + 10, g);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/* ---------- inlines ---------- */
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The serif inline with its channel opened: the shrunk letter less any part under `open` wide, so where a stroke tapers
|
|
112
|
+
* (K's and R's legs, a bowl's side) the channel stops rather than running on as a slit under 10. Centred.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('../letters/serif.js').GlyphSource} src The glyph source.
|
|
115
|
+
* @param {number} [H=380] The cap height.
|
|
116
|
+
* @param {number} [open=6] The narrowest channel kept.
|
|
117
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
118
|
+
*/
|
|
119
|
+
export const inlineOpen = (src, H = 380, open = 6) => (g) => {
|
|
120
|
+
const { s, w, m } = src(H, { g }), inner = grow(grow(s, -(m.S - g) / 2), -open);
|
|
121
|
+
return move(D(s, grow(inner, open)), 256 - w / 2, 256 - H / 2);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The inline with a STRAIGHT channel, for letters of straight strokes (E, F, M, T): each piece of the shrunk letter
|
|
126
|
+
* clipped to the strip through its own runs at 0.3 and 0.7 of its height, so the channel keeps its width past a junction
|
|
127
|
+
* instead of bulging into the arm. The strip follows a slanted stroke as well as an upright. Centred.
|
|
128
|
+
*
|
|
129
|
+
* @param {import('../letters/serif.js').GlyphSource} src The glyph source.
|
|
130
|
+
* @param {number} [H=380] The cap height.
|
|
131
|
+
* @param {number} [open=6] The narrowest channel kept.
|
|
132
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
133
|
+
*/
|
|
134
|
+
export const inlineStraight = (src, H = 380, open = 6) => (g) => {
|
|
135
|
+
const { s, w, m } = src(H, { g }), inner = grow(grow(grow(s, -(m.S - g) / 2), -open), open);
|
|
136
|
+
const ch = U(...inner.map((pg) => {
|
|
137
|
+
const [, t, , b] = bbox([pg]), at = (f) => runsH([pg], t + (b - t) * f)[0], y1 = t + (b - t) * 0.3, y2 = t + (b - t) * 0.7;
|
|
138
|
+
const [a1, b1] = at(0.3), [a2, b2] = at(0.7), X = (x1, x2, y) => x1 + ((x2 - x1) * (y - y1)) / (y2 - y1);
|
|
139
|
+
return I([pg], poly([[X(a1, a2, -500), -500], [X(b1, b2, -500), -500], [X(b1, b2, 1500), 1500], [X(a1, a2, 1500), 1500]]));
|
|
140
|
+
}));
|
|
141
|
+
return move(D(s, ch), 256 - w / 2, 256 - H / 2);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/* ---------- blackletter moves ---------- */
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The blackletter's parted lozenge on a font's middle arm (E, F): its tip found between y0 and y1 of the cap, a lozenge
|
|
148
|
+
* `k` times the arm's depth (110 at least) parted off by the gap.
|
|
149
|
+
*
|
|
150
|
+
* @param {number} cap The cap height.
|
|
151
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
152
|
+
* @param {number} y1 Its bottom.
|
|
153
|
+
* @param {number} [k=1.7] The lozenge's size, in arm depths.
|
|
154
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
155
|
+
*/
|
|
156
|
+
export const armLozenge = (cap, y0, y1, k = 1.7) => (s, g) => {
|
|
157
|
+
const a = armEnd(s, cap * y0, cap * y1), z = Math.max(110, a.th * k);
|
|
158
|
+
return parted(s, lozenge(a.x + z * 0.13, a.yc, z), g);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The blackletter's split stem on a font's letter: a slit g wide down the left stem from above the cap to `depth` of the
|
|
163
|
+
* cap ('head', B D P) or up from below the base to it ('foot', R). The slit is centred on the median of the stem's
|
|
164
|
+
* middles along the slit's length, spike tips (runs under half the median width) left out: a flag head juts left at the
|
|
165
|
+
* top and the bowl joins on the right, so one middle measured there puts the slit on the joint and leaves a sliver.
|
|
166
|
+
*
|
|
167
|
+
* @param {number} cap The cap height.
|
|
168
|
+
* @param {'head' | 'foot'} end Which end the slit runs from.
|
|
169
|
+
* @param {number} depth How far, as a fraction of the cap.
|
|
170
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
171
|
+
*/
|
|
172
|
+
export const stemSplit = (cap, end, depth) => (s, g) => {
|
|
173
|
+
const [y0, y1] = end === 'head' ? [0, cap * depth] : [cap * depth, cap];
|
|
174
|
+
const rs = [...Array(19)].map((_, i) => runsH(s, y0 + ((y1 - y0) * (i + 1)) / 20)[0]).filter(Boolean), med = (a) => a.sort((p, q) => p - q)[a.length >> 1];
|
|
175
|
+
const wm = med(rs.map(([a, b]) => b - a)), x = med(rs.filter(([a, b]) => b - a >= wm / 2).map(([a, b]) => (a + b) / 2));
|
|
176
|
+
return D(s, (end === 'head' ? splitHead : splitFoot)(x, cap * depth, g));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The blackletter's H: a lozenge set across the bar, `k` times the counter's width from corner to corner, on the bar's
|
|
181
|
+
* middle (the counter and bar measured across and down the letter's middle).
|
|
182
|
+
*
|
|
183
|
+
* @param {number} cap The cap height.
|
|
184
|
+
* @param {{ k?: number, at?: number }} [o] The lozenge's size in counter widths (1.7), and its height when no bar is
|
|
185
|
+
* found (0.5 of the cap).
|
|
186
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
187
|
+
*/
|
|
188
|
+
export const barLozenge = (cap, { k = 1.7, at = 0.5 } = {}) => (s, g) => {
|
|
189
|
+
const [x0, , x1] = bbox(s), xm = (x0 + x1) / 2;
|
|
190
|
+
const across = runsH(s, cap * 0.25), cw = across[1][0] - across[0][1], bar = runsV(s, xm).find(([a, b]) => (a + b) / 2 > cap * 0.25 && (a + b) / 2 < cap * 0.75);
|
|
191
|
+
return U(s, lozenge(xm, bar ? (bar[0] + bar[1]) / 2 : cap * at, cw * k));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* armLozenge mirrored: a stroke ending on the LEFT (a J's foot) runs into a parted lozenge, its tip found between y0
|
|
196
|
+
* and y1.
|
|
197
|
+
*
|
|
198
|
+
* @param {number} cap The cap height.
|
|
199
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
200
|
+
* @param {number} y1 Its bottom.
|
|
201
|
+
* @param {number} [k] The lozenge's size, in arm depths (1.7).
|
|
202
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
203
|
+
*/
|
|
204
|
+
export const leftLozenge = (cap, y0, y1, k) => (s, g) => flipX(armLozenge(cap, y0, y1, k)(flipX(s), g));
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The middle arm as a lozenge a gap off the stem (E, F), for a face whose middle arm is a short tongue (armLozenge would
|
|
208
|
+
* leave a stub on the stem): the tongue goes whole (the ink right of the stem between y0 and y1 of the cap, the stem's
|
|
209
|
+
* right edge read at 0.3 and 0.7 of the cap) and a lozenge `z` across stands a gap clear of the stem, centred on the
|
|
210
|
+
* tongue.
|
|
211
|
+
*
|
|
212
|
+
* @param {number} cap The cap height.
|
|
213
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
214
|
+
* @param {number} y1 Its bottom.
|
|
215
|
+
* @param {number} [z=120] The lozenge, corner to corner.
|
|
216
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
217
|
+
*/
|
|
218
|
+
export const tongueLozenge = (cap, y0, y1, z = 120) => (s, g) => {
|
|
219
|
+
const sr = Math.max(runsH(s, cap * 0.3)[0][1], runsH(s, cap * 0.7)[0][1]);
|
|
220
|
+
let a = Infinity, b = -Infinity; for (let y = cap * y0; y <= cap * y1; y += 1) { const r = runsH(s, y)[0]; if (r[1] > sr + 2) { a = Math.min(a, y); b = Math.max(b, y); } }
|
|
221
|
+
const body = D(s, rect(sr, a - 1, 400, b - a + 2)), yc = (a + b) / 2;
|
|
222
|
+
return U(body, lozenge(sr + g + z / 2, yc, z));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A bar's right end run into a lozenge parted off by the gap and set LEVEL on the bar (the geometric T's lozenge
|
|
227
|
+
* terminals), for a face whose bar curls up at its tips (armLozenge would perch it above the curl): the bar's depth and
|
|
228
|
+
* middle are read `back` in from the tip (found in the top 0.2 of the cap), the lozenge is `k` times that depth (110 at
|
|
229
|
+
* least) and centred on the tip itself, so the moat takes the curl off whole.
|
|
230
|
+
*
|
|
231
|
+
* @param {number} cap The cap height.
|
|
232
|
+
* @param {{ back?: number, k?: number }} [o] How far in the bar is read (60) and the lozenge's size in bar depths (1.5).
|
|
233
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
234
|
+
*/
|
|
235
|
+
export const barEnd = (cap, { back = 60, k = 1.5 } = {}) => (s, g) => {
|
|
236
|
+
const a = armEnd(s, 0, cap * 0.2), r = runsV(s, a.x - back).find(([y0]) => y0 < cap * 0.3);
|
|
237
|
+
const z = Math.max(110, (r[1] - r[0]) * k); return parted(s, lozenge(a.x, (r[0] + r[1]) / 2, z), g);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* barEnd at both ends of the bar: the left end is the right end of the letter mirrored.
|
|
242
|
+
*
|
|
243
|
+
* @param {number} cap The cap height.
|
|
244
|
+
* @param {{ back?: number, k?: number }} [o] As barEnd().
|
|
245
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
246
|
+
*/
|
|
247
|
+
export const barEnds = (cap, o) => (s, g) => { const f = barEnd(cap, o); return flipX(f(flipX(f(s, g)), g)); };
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The blackletter's parted lozenge `k` times the stroke at the crossing (X) or the fork (Y): placed on the first row
|
|
251
|
+
* between y0 and y1 of the cap where the ink is one run, narrowest.
|
|
252
|
+
*
|
|
253
|
+
* @param {number} cap The cap height.
|
|
254
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
255
|
+
* @param {number} y1 Its bottom.
|
|
256
|
+
* @param {number} [k=1.6] The lozenge's size, in stroke widths.
|
|
257
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
258
|
+
*/
|
|
259
|
+
export const crossLozenge = (cap, y0, y1, k = 1.6) => (s, g) => {
|
|
260
|
+
let best = null;
|
|
261
|
+
for (let y = cap * y0; y <= cap * y1; y += 1) { const r = runsH(s, y); if (r.length === 1 && (!best || r[0][1] - r[0][0] < best.w)) best = { y, x: (r[0][0] + r[0][1]) / 2, w: r[0][1] - r[0][0] }; }
|
|
262
|
+
return parted(s, lozenge(best.x, best.y, best.w * k), g);
|
|
263
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The disc's radius (224). */
|
|
4
|
+
export const DISC_R: number;
|
|
5
|
+
/** The disc, moved by (dx, dy). */
|
|
6
|
+
export function disc(dx?: number, dy?: number, r?: number): Polygon;
|
|
7
|
+
/** A curve across the disc: f(u) → [u, v] in a frame turned by deg. */
|
|
8
|
+
export function across(f: (u: number) => readonly number[], deg?: number, span?: number, n?: number): Point[];
|
|
9
|
+
/** The half plane on one side (s = ±1) of a curve that runs right across the disc. */
|
|
10
|
+
export function discSide(L: readonly Point[], s: number): Polygon;
|
|
11
|
+
/** The disc parted along L by g, each half's rim moved by sa / sb. */
|
|
12
|
+
export function split(L: readonly Point[], g: number, sa?: readonly number[], sb?: readonly number[]): Shape;
|
|
13
|
+
/** A band b wide along L, parted from both halves by g, riding on a disc moved by sm. */
|
|
14
|
+
export function bandCut(L: readonly Point[], b: number | ((i: number, n: number) => number), g: number, sm?: readonly number[], sa?: readonly number[], sb?: readonly number[]): Shape;
|
|
15
|
+
/** A point at radius r and angle deg from the disc's centre. */
|
|
16
|
+
export function discAt(r: number, deg: number): Point;
|
|
17
|
+
/** A shape turned by deg about p. */
|
|
18
|
+
export function turn(s: Geom, deg: number, p: readonly number[]): Geom;
|
|
19
|
+
/** The two sides of a circular cut through the disc, parted by g, the inner one turned th° about P. */
|
|
20
|
+
export function arcSplit(P: readonly number[], Ra: number, g: number, th: number): Shape;
|