@logomarks/core 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -0
  3. package/package.json +47 -4
  4. package/src/font/glyphs.d.ts +31 -0
  5. package/src/font/glyphs.js +228 -0
  6. package/src/font/index.d.ts +10 -0
  7. package/src/font/index.js +10 -0
  8. package/src/font/measure.d.ts +16 -0
  9. package/src/font/measure.js +201 -0
  10. package/src/font/moves.d.ts +42 -0
  11. package/src/font/moves.js +263 -0
  12. package/src/forms/discs.d.ts +20 -0
  13. package/src/forms/discs.js +114 -0
  14. package/src/forms/emblems.d.ts +22 -0
  15. package/src/forms/emblems.js +109 -0
  16. package/src/forms/flow.d.ts +21 -0
  17. package/src/forms/flow.js +80 -0
  18. package/src/forms/folds.d.ts +18 -0
  19. package/src/forms/folds.js +147 -0
  20. package/src/forms/geo.d.ts +46 -0
  21. package/src/forms/geo.js +233 -0
  22. package/src/forms/hooks.d.ts +40 -0
  23. package/src/forms/hooks.js +204 -0
  24. package/src/forms/index.d.ts +22 -0
  25. package/src/forms/index.js +24 -0
  26. package/src/forms/jog.d.ts +33 -0
  27. package/src/forms/jog.js +162 -0
  28. package/src/forms/lines.d.ts +32 -0
  29. package/src/forms/lines.js +233 -0
  30. package/src/forms/ref.d.ts +41 -0
  31. package/src/forms/ref.js +246 -0
  32. package/src/forms/shatter.d.ts +40 -0
  33. package/src/forms/shatter.js +341 -0
  34. package/src/forms/slats.d.ts +35 -0
  35. package/src/forms/slats.js +78 -0
  36. package/src/forms/swash.d.ts +23 -0
  37. package/src/forms/swash.js +74 -0
  38. package/src/forms/tech.d.ts +40 -0
  39. package/src/forms/tech.js +293 -0
  40. package/src/forms/tracks.d.ts +36 -0
  41. package/src/forms/tracks.js +121 -0
  42. package/src/index.js +2 -1
  43. package/src/iso/index.d.ts +5 -0
  44. package/src/iso/index.js +13 -0
  45. package/src/iso/objects.d.ts +168 -0
  46. package/src/iso/objects.js +1042 -0
  47. package/src/iso/planes.d.ts +27 -0
  48. package/src/iso/planes.js +138 -0
  49. package/src/iso/voxels.d.ts +43 -0
  50. package/src/iso/voxels.js +255 -0
  51. package/src/letters/blackletter.d.ts +31 -0
  52. package/src/letters/blackletter.js +199 -0
  53. package/src/letters/dotstroke.d.ts +16 -0
  54. package/src/letters/dotstroke.js +77 -0
  55. package/src/letters/echo.d.ts +42 -0
  56. package/src/letters/echo.js +246 -0
  57. package/src/letters/frame.d.ts +6 -0
  58. package/src/letters/frame.js +14 -0
  59. package/src/letters/index.d.ts +16 -0
  60. package/src/letters/index.js +18 -0
  61. package/src/letters/inktrap.d.ts +66 -0
  62. package/src/letters/inktrap.js +244 -0
  63. package/src/letters/mono.d.ts +145 -0
  64. package/src/letters/mono.js +684 -0
  65. package/src/letters/pillar.d.ts +31 -0
  66. package/src/letters/pillar.js +257 -0
  67. package/src/letters/serif.d.ts +109 -0
  68. package/src/letters/serif.js +666 -0
  69. package/src/letters/stair.d.ts +8 -0
  70. package/src/letters/stair.js +43 -0
  71. package/src/letters/stem.d.ts +29 -0
  72. package/src/letters/stem.js +205 -0
  73. package/src/objects/animals.d.ts +105 -0
  74. package/src/objects/animals.js +469 -0
  75. package/src/objects/bio.d.ts +14 -0
  76. package/src/objects/bio.js +76 -0
  77. package/src/objects/devglyphs.d.ts +34 -0
  78. package/src/objects/devglyphs.js +226 -0
  79. package/src/objects/devices.d.ts +31 -0
  80. package/src/objects/devices.js +114 -0
  81. package/src/objects/faces.d.ts +94 -0
  82. package/src/objects/faces.js +344 -0
  83. package/src/objects/food.d.ts +12 -0
  84. package/src/objects/food.js +74 -0
  85. package/src/objects/index.d.ts +13 -0
  86. package/src/objects/index.js +18 -0
  87. package/src/objects/rosettes.d.ts +14 -0
  88. package/src/objects/rosettes.js +64 -0
  89. package/src/objects/sport.d.ts +8 -0
  90. package/src/objects/sport.js +37 -0
  91. package/src/objects/weapons.d.ts +31 -0
  92. package/src/objects/weapons.js +179 -0
  93. package/src/pixels/dither.d.ts +78 -0
  94. package/src/pixels/dither.js +399 -0
  95. package/src/pixels/emote.d.ts +6 -0
  96. package/src/pixels/emote.js +76 -0
  97. package/src/pixels/index.d.ts +7 -0
  98. package/src/pixels/index.js +8 -0
  99. package/src/sdf/index.d.ts +2 -0
  100. package/src/sdf/index.js +2 -0
  101. package/src/sdf/y2k.d.ts +12 -0
  102. package/src/sdf/y2k.js +80 -0
  103. package/src/util/vec.d.ts +14 -0
  104. package/src/util/vec.js +61 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Flow grids: discs on a small square grid where one path of neighbouring discs is joined by capsule bridges into a
3
+ * single smooth stroke (a letter, a wave, a loop), every join filleted, while the other discs stay single dots.
4
+ *
5
+ * The grid's pitch is 2r + g, so a free disc sits exactly the gap g from any path disc beside it. A path is a string of
6
+ * cells, "a0 b0 b1 …" (column letter a–d, row digit 0–3, y down), each cell an orthogonal neighbour of the one before; a
7
+ * trailing "~" closes it into a loop. A tube (`neck` 1) is drawn with the kit: a pill per bridge, each turn's inner
8
+ * corner filleted by closing just that turn's two pills, so a fillet never reaches across to a cell it isn't joined to.
9
+ * A bead string (`neck` under 1) is a distance field: each bridge melted into its two discs by a smooth minimum, the
10
+ * bridges then joined sharply, so two path discs side by side but unbridged keep their gap. The smooth minimum bulges
11
+ * where two of its fields run level, so it suits waisted bridges only.
12
+ * From the Logomarks tools (flowparts.mjs, batch 236, "Modular · Flow").
13
+ */
14
+ import { circle, pill, U, soften } from '../geometry/index.js';
15
+ import { sdCircle, sdCapsule, smin, fmin, shapeOf } from '../sdf/index.js';
16
+
17
+ /**
18
+ * A flow path parsed into cells: "a0 b0 b1" → [[0, 0], [1, 0], [1, 1]]; a trailing "~" marks a closed loop.
19
+ *
20
+ * @param {string} s The path.
21
+ * @returns {{ path: [number, number][], closed: boolean }} Its cells (column, row) and whether it closes.
22
+ */
23
+ export const cells = (s) => {
24
+ const t = s.trim().split(/\s+/), closed = t[t.length - 1] === '~';
25
+ return { path: (closed ? t.slice(0, -1) : t).map((c) => [c.charCodeAt(0) - 97, +c.slice(1)]), closed };
26
+ };
27
+
28
+ /**
29
+ * A flow grid's geometry: its pitch and each cell's centre, for an n × m grid of discs of radius r and gap g centred on
30
+ * (cx, cy).
31
+ *
32
+ * @param {number} n Columns.
33
+ * @param {number} m Rows.
34
+ * @param {number} r The discs' radius.
35
+ * @param {number} g The gap.
36
+ * @param {number} [cx=256] The grid's centre.
37
+ * @param {number} [cy=256] The grid's centre.
38
+ * @returns {{ p: number, at: (cell: readonly number[]) => [number, number] }} The pitch and a cell's centre.
39
+ */
40
+ export const grid = (n, m, r, g, cx = 256, cy = 256) => {
41
+ const p = 2 * r + g, x0 = cx - ((n - 1) * p) / 2, y0 = cy - ((m - 1) * p) / 2;
42
+ return { p, at: ([i, j]) => [x0 + i * p, y0 + j * p] };
43
+ };
44
+
45
+ /**
46
+ * A flow grid as shapes: the path, the free discs, both together, and the narrowest gap between them (so a fillet that
47
+ * bulges into the gap is caught).
48
+ *
49
+ * @param {{ n: number, m?: number, r: number, g: number, path: string, neck?: number, k?: number, fillet?: number, skip?: string }} o
50
+ * The grid (n × m, m = n), the discs' radius r and the gap g; the path (cells()); `neck` the bridges' width as a share
51
+ * of the disc (1, a tube; under 1 a bead string melted with blend k, 0.6 r); `fillet` the tube's inner-corner radius
52
+ * (0.6 r); `skip` cells left empty (no free disc either).
53
+ * @returns {{ path: import('../geometry/types.js').Shape, free: import('../geometry/types.js').Shape, shape: import('../geometry/types.js').Shape, clearance: number }}
54
+ * The path, the free discs, both, and the narrowest gap from the path to a free disc.
55
+ * @throws {Error} When a step of the path isn't to a neighbouring cell.
56
+ */
57
+ export function flowShape({ n, m = n, r, g, path, neck = 1, k = r * 0.6, fillet = r * 0.6, skip = '' }) {
58
+ const G = grid(n, m, r, g), { path: P, closed } = cells(path), key = ([i, j]) => `${i},${j}`, on = new Set(P.map(key));
59
+ const off = new Set(skip.trim() ? cells(skip).path.map(key) : []);
60
+ P.forEach((c, i) => { if (i && Math.abs(c[0] - P[i - 1][0]) + Math.abs(c[1] - P[i - 1][1]) !== 1) throw new Error(`flow: ${key(P[i - 1])} → ${key(c)} is not a neighbour step`); });
61
+ const B = P.slice(1).map((c, i) => [P[i], c]); if (closed) B.push([P[P.length - 1], P[0]]);
62
+ let pathS;
63
+ if (neck >= 1) {
64
+ const pills = B.map(([a, b]) => pill(G.at(a), G.at(b), 2 * r));
65
+ // a turn: two bridges meeting at a cell at a right angle; close just those two so the fillet stays in that corner
66
+ const turns = B.map((b, i) => [b, B[(i + 1) % B.length], i]).filter(([a, b, i]) => (closed || i < B.length - 1) && a[1] === b[0]
67
+ && (a[1][0] - a[0][0]) * (b[1][0] - b[0][0]) + (a[1][1] - a[0][1]) * (b[1][1] - b[0][1]) === 0);
68
+ pathS = U(...pills, ...P.map((c) => circle(...G.at(c), r)), ...turns.map(([, , i]) => soften(U(pills[i], pills[(i + 1) % pills.length]), fillet, 0)));
69
+ } else pathS = shapeOf(fmin(...B.map(([a, b]) => smin(k, sdCircle(...G.at(a), r), sdCircle(...G.at(b), r), sdCapsule(G.at(a), G.at(b), 2 * r * neck)))));
70
+ const freeC = []; for (let j = 0; j < m; j++) for (let i = 0; i < n; i++) if (!on.has(key([i, j])) && !off.has(key([i, j]))) freeC.push(G.at([i, j]));
71
+ const free = freeC.length ? U(...freeC.map((c) => circle(...c, r))) : [];
72
+ // the narrowest gap: from each free disc's centre to the path's outline, less the radius
73
+ const seg = ([ax, ay], [bx, by], [x, y]) => {
74
+ const dx = bx - ax, dy = by - ay, t = Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy || 1e-9)));
75
+ return Math.hypot(x - ax - t * dx, y - ay - t * dy);
76
+ };
77
+ let clearance = Infinity;
78
+ for (const c of freeC) for (const pg of pathS) for (const ring of pg) for (let i = 1; i < ring.length; i++) clearance = Math.min(clearance, seg(ring[i - 1], ring[i], c) - r);
79
+ return { path: pathS, free, shape: freeC.length ? U(pathS, free) : pathS, clearance };
80
+ }
@@ -0,0 +1,18 @@
1
+ import type { Point, Polygon, Shape, Geom } from '../geometry/types.js';
2
+
3
+ /** A seam g wide along a polyline, round ends (two points: a pill). */
4
+ export function seamLine(g: number, pts: readonly Point[]): Geom;
5
+ /** A shape split into facets by seams g wide. */
6
+ export function crease(shape: Geom, g: number, ...seams: Point[][]): Shape;
7
+ /** A smooth seam through control points (Catmull-Rom, n points a span). */
8
+ export function curve(pts: readonly Point[], n?: number): Point[];
9
+ /** A quadratic Bézier from a to b pulled towards c, n + 1 points. */
10
+ export function foldQuad(a: readonly number[], c: readonly number[], b: readonly number[], n?: number): Point[];
11
+ /** A strap: a pill w wide from a to b. */
12
+ export function strap(a: Point, b: Point, w: number): Polygon;
13
+ /** A flat strip w wide from a to b, its ends cut on the lines y = y0 and y = y1 if given. */
14
+ export function strip(a: readonly number[], b: readonly number[], w: number, o?: { y0?: number; y1?: number }): Polygon;
15
+ /** A hand-cut edge: every ring wobbled along its normal by a smooth seeded wave of amplitude amp. */
16
+ export function wobble(shape: Geom, amp: number, rnd?: () => number, o?: { step?: number; waves?: number }): Shape;
17
+ /** Scissor-cut edges: every ring as straight snips about `step` long, ends nudged by up to `jit`. */
18
+ export function snip(shape: Geom, step?: number, jit?: number, rnd?: () => number, corner?: number): Shape;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Fold seams, straps and hand-cut edges, the parts the Crease, Strap and Snip marks share. A seam is half the house gap
3
+ * (11 once fitted, against the 22 gap): a fold or an overlap drawn as a hairline that shows at large sizes and fades at
4
+ * 32 px, where the mark must read as its silhouette alone. Every build takes the seam width g (exact() with a target of
5
+ * 11 keeps it 11 once fitted), so a part that is truly separate stands 2g clear. Laying parts one over another, each
6
+ * cut free by the seam, is the parts' chain().
7
+ * From the Logomarks tools (foldparts.mjs, batch 170).
8
+ */
9
+ import { U, D, poly, pill, stroke, dedupe } from '../geometry/index.js';
10
+ import { spline } from '../parts/index.js';
11
+
12
+ /**
13
+ * A seam along a polyline: g wide with round ends, so seams that meet at a node leave the facets' corners sharp.
14
+ *
15
+ * @param {number} g The seam's width.
16
+ * @param {readonly import('../geometry/types.js').Point[]} pts The seam's line (two points: a straight seam).
17
+ * @returns {import('../geometry/types.js').Geom} The seam.
18
+ */
19
+ export const seamLine = (g, pts) => (pts.length === 2 ? pill(pts[0], pts[1], g) : stroke(dedupe(pts), g, { roundCaps: true, chunk: 8 }));
20
+
21
+ /**
22
+ * A shape split into facets by seams. Run a seam that ends on the outline a little past it, and end inner seams exactly
23
+ * on a node where they meet another.
24
+ *
25
+ * @param {import('../geometry/types.js').Geom} shape The shape.
26
+ * @param {number} g The seam's width.
27
+ * @param {...import('../geometry/types.js').Point[]} seams Each seam's points (two for a straight seam, more for a curve).
28
+ * @returns {import('../geometry/types.js').Shape} The facets.
29
+ */
30
+ export const crease = (shape, g, ...seams) => D(shape, ...seams.map((s) => seamLine(g, s)));
31
+
32
+ /**
33
+ * A smooth seam through control points (Catmull-Rom, the ends held).
34
+ *
35
+ * @param {readonly import('../geometry/types.js').Point[]} pts The control points.
36
+ * @param {number} [n=24] Points a span.
37
+ * @returns {import('../geometry/types.js').Point[]} The seam's line.
38
+ */
39
+ export const curve = (pts, n = 24) => spline(pts, n);
40
+
41
+ /**
42
+ * A quadratic Bézier from a to b pulled towards c, as n + 1 points.
43
+ *
44
+ * @param {readonly number[]} a The start.
45
+ * @param {readonly number[]} c The control point.
46
+ * @param {readonly number[]} b The end.
47
+ * @param {number} [n=48] Steps.
48
+ * @returns {import('../geometry/types.js').Point[]} The curve.
49
+ */
50
+ export const foldQuad = (a, c, b, n = 48) => [...Array(n + 1)].map((_, i) => {
51
+ const t = i / n, u = 1 - t;
52
+ return [u * u * a[0] + 2 * u * t * c[0] + t * t * b[0], u * u * a[1] + 2 * u * t * c[1] + t * t * b[1]];
53
+ });
54
+
55
+ /**
56
+ * A strap: a pill stroke w wide from a to b (strap letters lay one strap over another with the parts' chain()).
57
+ *
58
+ * @param {import('../geometry/types.js').Point} a One end.
59
+ * @param {import('../geometry/types.js').Point} b The other.
60
+ * @param {number} w The width.
61
+ * @returns {import('../geometry/types.js').Polygon} The strap.
62
+ */
63
+ export const strap = (a, b, w) => pill(a, b, w);
64
+
65
+ /**
66
+ * A flat strip w wide from a to b, each end cut on a horizontal line (y0 at a, y1 at b) if given, else square.
67
+ *
68
+ * @param {readonly number[]} a One end.
69
+ * @param {readonly number[]} b The other.
70
+ * @param {number} w The width.
71
+ * @param {{ y0?: number, y1?: number }} [o] The heights the ends are cut on.
72
+ * @returns {import('../geometry/types.js').Polygon} The strip.
73
+ */
74
+ export const strip = (a, b, w, { y0, y1 } = {}) => {
75
+ const dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy), n = [-dy / L * w / 2, dx / L * w / 2];
76
+ const ends = (p, y, sgn) => {
77
+ if (y == null) return [[p[0] + n[0], p[1] + n[1]], [p[0] - n[0], p[1] - n[1]]];
78
+ // slide each edge along the strip's direction until it meets y
79
+ return [n, [-n[0], -n[1]]].map((o) => { const q = [p[0] + o[0], p[1] + o[1]], t = (y - q[1]) / dy; return [q[0] + dx * t, y]; });
80
+ };
81
+ const [a1, a2] = ends(a, y0), [b1, b2] = ends(b, y1); return poly([a1, b1, b2, a2]);
82
+ };
83
+
84
+ /**
85
+ * A hand-cut edge: every ring of a shape resampled every `step` and pushed along its normal by a smooth, seeded wobble
86
+ * of amplitude `amp` (three sine waves round the ring with random phases from `rnd`, about `waves` bumps per 1000 units
87
+ * of outline), so the cut reads as scissors on paper, not as noise. The same seed gives the same edge.
88
+ *
89
+ * @param {import('../geometry/types.js').Geom} shape The shape.
90
+ * @param {number} amp The wobble's amplitude.
91
+ * @param {() => number} [rnd=Math.random] The random source (seeded, for a repeatable edge).
92
+ * @param {{ step?: number, waves?: number }} [o] The resampling step (3) and the bumps per 1000 units (9).
93
+ * @returns {import('../geometry/types.js').Shape} The shape with its edges wobbled.
94
+ */
95
+ export const wobble = (shape, amp, rnd = Math.random, { step = 3, waves = 9 } = {}) => {
96
+ const polys = shape.length && typeof shape[0][0][0] === 'number' ? [shape] : shape;
97
+ const ph = [rnd(), rnd(), rnd()].map((v) => v * 2 * Math.PI);
98
+ const ringW = (ring) => {
99
+ const r = dedupe(ring.slice(0, -1).concat(ring.at(-1)[0] === ring[0][0] && ring.at(-1)[1] === ring[0][1] ? [] : [ring.at(-1)]));
100
+ const dense = []; r.forEach((p, i) => {
101
+ const q = r[(i + 1) % r.length], m = Math.max(1, Math.ceil(Math.hypot(q[0] - p[0], q[1] - p[1]) / step));
102
+ for (let j = 0; j < m; j++) dense.push([p[0] + ((q[0] - p[0]) * j) / m, p[1] + ((q[1] - p[1]) * j) / m]);
103
+ });
104
+ const n = dense.length, S = [0]; for (let i = 1; i < n; i++) S.push(S[i - 1] + Math.hypot(dense[i][0] - dense[i - 1][0], dense[i][1] - dense[i - 1][1]));
105
+ const L = S[n - 1] + Math.hypot(dense[0][0] - dense[n - 1][0], dense[0][1] - dense[n - 1][1]), k = Math.max(3, Math.round((waves * L) / 1000));
106
+ return dense.map((p, i) => {
107
+ const a = dense[(i - 1 + n) % n], b = dense[(i + 1) % n], dx = b[0] - a[0], dy = b[1] - a[1], l = Math.hypot(dx, dy) || 1, t = (2 * Math.PI * S[i]) / L;
108
+ const w = amp * (0.6 * Math.sin(k * t + ph[0]) + 0.3 * Math.sin((2 * k + 1) * t + ph[1]) + 0.2 * Math.sin((3 * k + 2) * t + ph[2]));
109
+ return [p[0] - (dy / l) * w, p[1] + (dx / l) * w];
110
+ });
111
+ };
112
+ return U(...polys.map((pg) => D(poly(ringW(pg[0])), ...pg.slice(1).map((h) => poly(ringW(h))))));
113
+ };
114
+
115
+ /**
116
+ * Scissor-cut edges: every ring of a shape redrawn as straight snips about `step` long (corners sharper than `corner`°
117
+ * kept where they are), each snip's end nudged along the normal by up to `jit` from the seeded `rnd`, so curves turn
118
+ * into short straight cuts and straight edges kink slightly, as paper cut by hand.
119
+ *
120
+ * @param {import('../geometry/types.js').Geom} shape The shape.
121
+ * @param {number} [step=34] The snips' length.
122
+ * @param {number} [jit=3] The most a snip's end moves.
123
+ * @param {() => number} [rnd=Math.random] The random source (seeded, for a repeatable edge).
124
+ * @param {number} [corner=140] Corners sharper than this (degrees) stay put.
125
+ * @returns {import('../geometry/types.js').Shape} The shape, snipped.
126
+ */
127
+ export const snip = (shape, step = 34, jit = 3, rnd = Math.random, corner = 140) => {
128
+ const polys = shape.length && typeof shape[0][0][0] === 'number' ? [shape] : shape;
129
+ const ringS = (ring) => {
130
+ const r = dedupe(ring.at(-1)[0] === ring[0][0] && ring.at(-1)[1] === ring[0][1] ? ring.slice(0, -1) : ring), n = r.length;
131
+ const ang = (i) => {
132
+ const a = r[(i - 1 + n) % n], p = r[i], b = r[(i + 1) % n], u = [a[0] - p[0], a[1] - p[1]], v = [b[0] - p[0], b[1] - p[1]];
133
+ return (Math.acos(Math.max(-1, Math.min(1, (u[0] * v[0] + u[1] * v[1]) / (Math.hypot(...u) * Math.hypot(...v) || 1)))) * 180) / Math.PI;
134
+ };
135
+ const out = []; let acc = 0;
136
+ for (let i = 0; i < n; i++) {
137
+ const p = r[i], q = r[(i + 1) % n], sharp = ang(i) < corner;
138
+ if (sharp || acc >= step || i === 0) {
139
+ const a = r[(i - 1 + n) % n], dx = q[0] - a[0], dy = q[1] - a[1], l = Math.hypot(dx, dy) || 1, j = sharp ? 0 : (rnd() * 2 - 1) * jit;
140
+ out.push([p[0] - (dy / l) * j, p[1] + (dx / l) * j]); acc = 0;
141
+ }
142
+ acc += Math.hypot(q[0] - p[0], q[1] - p[1]);
143
+ }
144
+ return out;
145
+ };
146
+ return U(...polys.map((pg) => D(poly(ringS(pg[0])), ...pg.slice(1).map((h) => poly(ringS(h))))));
147
+ };
@@ -0,0 +1,46 @@
1
+ import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
2
+
3
+ /** The base square's side (420). */
4
+ export const GEO_SQ: number;
5
+ /** The base square's left edge. */
6
+ export const GEO_X0: number;
7
+ /** The base square's right edge. */
8
+ export const GEO_X1: number;
9
+ /** The base square, side s, centred. */
10
+ export function sq(s?: number): Polygon;
11
+ /** The unit vector from a to b, and the distance. */
12
+ export function dirOf(a: readonly number[], b: readonly number[]): [Point, number];
13
+ /** A needle gap from a (width w0) to b (width w1), run on past b by ext at the same taper. */
14
+ export function needle(a: readonly number[], b: readonly number[], w0: number, w1: number, ext?: number): Polygon;
15
+ /** A straight band of width w along a → b, square ends. */
16
+ export function band(a: readonly number[], b: readonly number[], w: number): Polygon;
17
+ /** The half plane on one side (s = ±1) of an open polyline that runs right across a shape. */
18
+ export function side(L: readonly Point[], s: number): Polygon;
19
+ /** A front strand laid over a closed band near p: the band cut free round it by g within R of p. */
20
+ export function crossOver(all: Geom, front: Geom, p: readonly number[], R: number, g: number): Shape;
21
+ /** A disc with at least 180 points. */
22
+ export function geoDisc(cx: number, cy: number, r: number): Polygon;
23
+ /** A knob: a disc with one slot at deg, split right across (full) or a pointer from the centre. */
24
+ export function knob(cx: number, cy: number, r: number, deg: number, w: number, full?: boolean): Shape;
25
+ /** Knobs on a grid of tokens ('|', '-', '\\', '/', 'pN', '.'), discs of radius r on a pitch of 2r + g. */
26
+ export function knobs(rows: readonly (readonly string[])[], g: number, w: number, r: number): Shape;
27
+ /** The disc radius that makes n discs and n − 1 gaps span 448. */
28
+ export function rFor(n: number, g: number): number;
29
+ /** A roof tile: a square with a half disc on its far side, the cap pointing at deg. */
30
+ export function roofTile(cx: number, cy: number, a: number, deg?: number): Shape;
31
+ /** Tiles laid back to front, each over the ones before and cut free by g. */
32
+ export function layered(list: readonly Geom[], g: number): Geom;
33
+ /** A triangle's three corners, circumradius R about (cx, cy). */
34
+ export function triPts(cx: number, cy: number, R: number, deg?: number): Point[];
35
+ /** The half of a disc on the side sideDeg points to. */
36
+ export function halfDisc(cx: number, cy: number, r: number, sideDeg: number): Shape;
37
+ /** The split disc's radius (224). */
38
+ export const R7: number;
39
+ /** The disc parted along L by g, each half's rim moved by sa / sb. */
40
+ export function splitDisc(L: readonly Point[], g: number, sa: readonly number[], sb: readonly number[]): Shape;
41
+ /** A roof tile's side in scales() (150). */
42
+ export const SA: number;
43
+ /** Roof tiles on a turned lattice, laid back to front; tiles in `up` face back up. */
44
+ export function scales(cells: readonly Point[], g: number, o?: { a?: number; pi?: number; pj?: number; deg?: number; up?: readonly Point[] }): Shape;
45
+ /** The nine points of a 3 × 3 lattice. */
46
+ export const grid3: Point[];
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Bold geometric parts: needle gaps that taper to nothing, knobs and dials, roof-tile scales laid back to front, half
3
+ * discs and a disc split along a line with each half's rim moved. Every part is on the 512 canvas, centred on C. From the
4
+ * Logomarks tools (geoparts.mjs, batch 212, "Bold · Geometric", and its editions in batch 220).
5
+ */
6
+ import { C, D, I, U, circle, grown, over, polar, poly, rect, rot, stroke } from '../geometry/index.js';
7
+
8
+ /** @typedef {import('../geometry/types.js').Point} Point */
9
+ /** @typedef {import('../geometry/types.js').Polygon} Polygon */
10
+ /** @typedef {import('../geometry/types.js').Shape} Shape */
11
+ /** @typedef {import('../geometry/types.js').Geom} Geom */
12
+
13
+ /** The base square's side. */
14
+ export const GEO_SQ = 420;
15
+
16
+ /** The base square's left edge. */
17
+ export const GEO_X0 = C - GEO_SQ / 2;
18
+
19
+ /** The base square's right edge. */
20
+ export const GEO_X1 = C + GEO_SQ / 2;
21
+
22
+ /**
23
+ * The base square, side s, centred on the canvas.
24
+ *
25
+ * @param {number} [s=GEO_SQ] Its side.
26
+ * @returns {Polygon} The square.
27
+ */
28
+ export const sq = (s = GEO_SQ) => rect(C - s / 2, C - s / 2, s, s);
29
+
30
+ /**
31
+ * The unit vector from a to b, and the distance between them.
32
+ *
33
+ * @param {readonly number[]} a From.
34
+ * @param {readonly number[]} b To.
35
+ * @returns {[Point, number]} The direction and the length.
36
+ */
37
+ export const dirOf = (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], L]; };
38
+
39
+ /**
40
+ * A needle gap from a (width w0) to b (width w1), run on past b by `ext` at the same taper, and back past a by 2, so a
41
+ * zero-width tip still parts the corner cleanly.
42
+ *
43
+ * @param {readonly number[]} a The start.
44
+ * @param {readonly number[]} b Where the taper is measured to.
45
+ * @param {number} w0 The width at a.
46
+ * @param {number} w1 The width at b.
47
+ * @param {number} [ext=300] How far it runs on past b.
48
+ * @returns {Polygon} The gap, to cut.
49
+ */
50
+ export const needle = (a, b, w0, w1, ext = 300) => {
51
+ const [[ux, uy], L] = dirOf(a, b), n = [-uy, ux], w2 = w0 + ((w1 - w0) * (L + ext)) / L;
52
+ const s = [a[0] - ux * 2, a[1] - uy * 2], e = [a[0] + ux * (L + ext), a[1] + uy * (L + ext)];
53
+ return poly([[s[0] + (n[0] * w0) / 2, s[1] + (n[1] * w0) / 2], [e[0] + (n[0] * w2) / 2, e[1] + (n[1] * w2) / 2], [e[0] - (n[0] * w2) / 2, e[1] - (n[1] * w2) / 2], [s[0] - (n[0] * w0) / 2, s[1] - (n[1] * w0) / 2]]);
54
+ };
55
+
56
+ /**
57
+ * A straight band of width w along a → b, its ends square (make it long enough to clip).
58
+ *
59
+ * @param {readonly number[]} a The start.
60
+ * @param {readonly number[]} b The end.
61
+ * @param {number} w The width.
62
+ * @returns {Polygon} The band.
63
+ */
64
+ export const band = (a, b, w) => {
65
+ const [[ux, uy]] = dirOf(a, b), n = [(-uy * w) / 2, (ux * w) / 2];
66
+ return poly([[a[0] + n[0], a[1] + n[1]], [b[0] + n[0], b[1] + n[1]], [b[0] - n[0], b[1] - n[1]], [a[0] - n[0], a[1] - n[1]]]);
67
+ };
68
+
69
+ /**
70
+ * The half plane on one side of an open polyline that runs right across a shape: the polyline closed by two points 3000
71
+ * out along the normal of its first-to-last chord.
72
+ *
73
+ * @param {readonly Point[]} L The polyline.
74
+ * @param {number} s Which side: 1 or −1.
75
+ * @returns {Polygon} The half plane.
76
+ */
77
+ export const side = (L, s) => {
78
+ const a = L[0], b = L.at(-1), [[dx, dy]] = dirOf(a, b), n = [-dy * s * 3000, dx * s * 3000];
79
+ return poly([...L, [b[0] + n[0], b[1] + n[1]], [a[0] + n[0], a[1] + n[1]]]);
80
+ };
81
+
82
+ /**
83
+ * A front strand laid over a closed band where they cross near p: the band is cut free round `front` by g, only inside
84
+ * the radius R of p.
85
+ *
86
+ * @param {Geom} all The band (and anything else under the strand).
87
+ * @param {Geom} front The strand in front.
88
+ * @param {readonly number[]} p The crossing.
89
+ * @param {number} R How far round the crossing the cut reaches.
90
+ * @param {number} g The gap.
91
+ * @returns {Shape} The crossing drawn.
92
+ */
93
+ export const crossOver = (all, front, p, R, g) => U(D(all, I(grown(front, g), circle(...p, R, 120))), front);
94
+
95
+ /**
96
+ * A disc, with at least 180 points (two a unit of diameter).
97
+ *
98
+ * @param {number} cx The centre.
99
+ * @param {number} cy The centre.
100
+ * @param {number} r The radius.
101
+ * @returns {Polygon} The disc.
102
+ */
103
+ export const geoDisc = (cx, cy, r) => circle(cx, cy, r, Math.max(180, Math.round(r * 2)));
104
+
105
+ /**
106
+ * A knob: a disc of radius r with one slot of width w at `deg` (screen degrees, 0 right, 90 down). `full` splits it along
107
+ * the whole diameter; otherwise the slot runs from the centre out through the rim (a pointer).
108
+ *
109
+ * @param {number} cx The centre.
110
+ * @param {number} cy The centre.
111
+ * @param {number} r The radius.
112
+ * @param {number} deg The slot's angle.
113
+ * @param {number} w The slot's width.
114
+ * @param {boolean} [full=true] Split right across, or a pointer.
115
+ * @returns {Shape} The knob.
116
+ */
117
+ export const knob = (cx, cy, r, deg, w, full = true) => {
118
+ const p = polar(r + 30, deg, cx, cy), q = full ? polar(r + 30, deg + 180, cx, cy) : [cx, cy];
119
+ const slot = full ? band(q, p, w) : U(band(q, p, w), circle(cx, cy, w / 2, 48));
120
+ return D(geoDisc(cx, cy, r), slot);
121
+ };
122
+
123
+ /**
124
+ * Knobs on a grid, centred on the canvas: rows of tokens, each a slot angle (`|` 90, `-` 0, `\` 45, `/` −45, `pN` a
125
+ * pointer at N degrees, `.` no disc), discs of radius r on a pitch of 2r + g.
126
+ *
127
+ * @param {readonly (readonly string[])[]} rows The tokens, row by row.
128
+ * @param {number} g The gap between discs.
129
+ * @param {number} w The slot width.
130
+ * @param {number} r The disc radius.
131
+ * @returns {Shape} The knobs.
132
+ */
133
+ export const knobs = (rows, g, w, r) => {
134
+ const n = Math.max(...rows.map((x) => x.length)), m = rows.length, p = 2 * r + g, out = [];
135
+ const ang = { '|': 90, '-': 0, '\\': 45, '/': -45 };
136
+ rows.forEach((row, j) => row.forEach((t, i) => {
137
+ if (t === '.') return; const cx = C + (i - (n - 1) / 2) * p, cy = C + (j - (m - 1) / 2) * p;
138
+ out.push(t[0] === 'p' ? knob(cx, cy, r, +t.slice(1), w, false) : knob(cx, cy, r, ang[t], w, true));
139
+ }));
140
+ return U(...out);
141
+ };
142
+
143
+ /**
144
+ * The disc radius that makes n discs and n − 1 gaps span 448.
145
+ *
146
+ * @param {number} n Discs across.
147
+ * @param {number} g The gap.
148
+ * @returns {number} The radius.
149
+ */
150
+ export const rFor = (n, g) => (448 - (n - 1) * g) / n / 2;
151
+
152
+ /**
153
+ * A roof tile: a square of side a with a half disc on its far side, the cap pointing at `deg`, centred on the square.
154
+ *
155
+ * @param {number} cx The square's centre.
156
+ * @param {number} cy The square's centre.
157
+ * @param {number} a Its side.
158
+ * @param {number} [deg=90] Where the cap points (90: down).
159
+ * @returns {Shape} The tile.
160
+ */
161
+ export const roofTile = (cx, cy, a, deg = 90) => rot(U(rect(cx - a / 2, cy - a / 2, a, a), geoDisc(cx, cy + a / 2, a / 2)), deg - 90, cx, cy);
162
+
163
+ /**
164
+ * Tiles laid back to front, each over the ones before it and cut free of them by g.
165
+ *
166
+ * @param {readonly Geom[]} list The tiles, back first.
167
+ * @param {number} g The gap.
168
+ * @returns {Geom} The stack.
169
+ */
170
+ export const layered = (list, g) => list.reduce((acc, t) => (acc.length ? over(acc, t, g) : t), []);
171
+
172
+ /**
173
+ * A triangle's corner points (apex up by default), circumradius R, centred on (cx, cy).
174
+ *
175
+ * @param {number} cx The centre.
176
+ * @param {number} cy The centre.
177
+ * @param {number} R The circumradius.
178
+ * @param {number} [deg=-90] The first corner's angle.
179
+ * @returns {Point[]} The three corners.
180
+ */
181
+ export const triPts = (cx, cy, R, deg = -90) => [0, 1, 2].map((k) => polar(R, deg + k * 120, cx, cy));
182
+
183
+ /**
184
+ * Half a disc: the half on the side `sideDeg` points to.
185
+ *
186
+ * @param {number} cx The centre.
187
+ * @param {number} cy The centre.
188
+ * @param {number} r The radius.
189
+ * @param {number} sideDeg The direction of the half kept (90: the lower half).
190
+ * @returns {Shape} The half disc.
191
+ */
192
+ export const halfDisc = (cx, cy, r, sideDeg) => I(geoDisc(cx, cy, r), rot(rect(cx - r - 2, cy, 2 * r + 4, r + 2), sideDeg - 90, cx, cy));
193
+
194
+ /** The split disc's radius. */
195
+ export const R7 = 224;
196
+
197
+ /**
198
+ * The disc (radius R7, centred) parted along the polyline L by g, each half's rim moved by sa / sb: the cut stays put, so
199
+ * the gap stays g.
200
+ *
201
+ * @param {readonly Point[]} L The cut.
202
+ * @param {number} g The gap.
203
+ * @param {readonly number[]} sa How far the rim on the left side moves.
204
+ * @param {readonly number[]} sb How far the rim on the right side moves.
205
+ * @returns {Shape} The two halves.
206
+ */
207
+ export const splitDisc = (L, g, sa, sb) => {
208
+ const z = stroke(L, g, { chunk: 4 }), d = (o) => geoDisc(C + o[0], C + o[1], R7);
209
+ return U(D(I(d(sa), side(L, 1)), z), D(I(d(sb), side(L, -1)), z));
210
+ };
211
+
212
+ /** A roof tile's side in scales(). */
213
+ export const SA = 150;
214
+
215
+ /**
216
+ * Roof tiles at lattice points (i, j), pitch pi·a across and pj·a down (under 1, so each cap lies over the tile below and
217
+ * the tile beside), caps pointing down, the lattice turned by deg and laid back to front, the far rows first. A tile
218
+ * listed in `up` faces back up and is laid last, its cap over the tile above it.
219
+ *
220
+ * @param {readonly Point[]} cells The lattice points.
221
+ * @param {number} g The gap.
222
+ * @param {{ a?: number, pi?: number, pj?: number, deg?: number, up?: readonly Point[] }} [o] The tile side (150), the
223
+ * pitches (0.75), the turn (45) and the tiles that face up.
224
+ * @returns {Shape} The scales.
225
+ */
226
+ export const scales = (cells, g, { a = 150, pi = 0.75, pj = 0.75, deg = 45, up = [] } = {}) => {
227
+ const isUp = ([i, j]) => up.some(([ui, uj]) => ui === i && uj === j);
228
+ const list = cells.slice().sort((p, q) => isUp(p) - isUp(q) || q[1] - p[1] || q[0] - p[0]);
229
+ return rot(layered(list.map((c) => roofTile(C + c[0] * a * pi, C + c[1] * a * pj, a, isUp(c) ? -90 : 90)), g), deg);
230
+ };
231
+
232
+ /** The nine points of a 3 × 3 lattice, row by row. */
233
+ export const grid3 = [0, 1, 2].flatMap((j) => [0, 1, 2].map((i) => [i, j]));
@@ -0,0 +1,40 @@
1
+ import type { Point, Shape } from '../geometry/types.js';
2
+
3
+ /** A hook: the end it grows from, the neighbour's cap centre it wraps, and its sweep (or end angle e and past). */
4
+ export interface Hook {
5
+ end?: 0 | 1;
6
+ c: readonly number[];
7
+ rn?: number;
8
+ sw?: number;
9
+ e?: number;
10
+ past?: number;
11
+ t0?: number;
12
+ t1?: number;
13
+ }
14
+
15
+ /** The capsule's width (150). */
16
+ export const HOOK_W: number;
17
+ /** The point d from p at angle a (degrees). */
18
+ export function hookAt(p: readonly number[], a: number, d: number): Point;
19
+ /** Where a cap centre sits to hook the neighbour cap at c, at angle a. */
20
+ export function kiss(c: readonly number[], a: number, g: number, r?: number, rn?: number): Point;
21
+ /** A curl: a band of inner radius ri about c, from a0 sweeping sw, tapering t0 → t1, round tip. */
22
+ export function curl(c: readonly number[], ri: number, a0: number, sw: number, t0: number, t1: number, n?: number): Shape;
23
+ /** A capsule along pts with hooks, its curl roots filleted. */
24
+ export function piece(pts: readonly Point[], hooks?: readonly Hook[], o?: { w?: number; bend?: number; g?: number; soft?: number; whole?: boolean }): Shape;
25
+ /** The Track band's width for the gap g (74 at 22). */
26
+ export function TB(g: number): number;
27
+ /** A piece's Track build: its centreline run round each hook, as two tracks. */
28
+ export function trackPiece(pts: readonly Point[], hooks?: readonly Hook[], o?: { g?: number; bend?: number }): Shape;
29
+ /** Points along an arc about c, radius r, from a0 to a1 degrees. */
30
+ export function arc(c: readonly number[], r: number, a0: number, a1: number, n?: number): Point[];
31
+ /** A neighbour's end: its cap centre and the angle it faces. */
32
+ export function endOf(pts: readonly Point[], end?: 0 | 1): { c: Point; e: number };
33
+ /** Every point of every ring of a shape. */
34
+ export function pts0(s: Shape): Point[];
35
+ /** join()'s check switch: with `on`, it logs pieces closer than the gap. */
36
+ export const hookCheck: { on: boolean };
37
+ /** The pieces as one shape, checked when hookCheck.on. */
38
+ export function join(g: number, ...ps: Shape[]): Shape;
39
+ /** A Track piece's end: c + (TB(g) + g) along d ± 90°. */
40
+ export function tq(c: readonly number[], d: number, side: number, g: number): Point;