@logomarks/core 0.1.11 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -2
  3. package/package.json +47 -4
  4. package/src/analyse/index.d.ts +1 -0
  5. package/src/analyse/index.js +3 -2
  6. package/src/analyse/optical.d.ts +20 -0
  7. package/src/analyse/optical.js +76 -0
  8. package/src/edit/cells.d.ts +13 -0
  9. package/src/edit/cells.js +26 -0
  10. package/src/edit/index.d.ts +3 -1
  11. package/src/edit/index.js +4 -2
  12. package/src/edit/pixelate.d.ts +2 -0
  13. package/src/edit/pixelate.js +15 -0
  14. package/src/font/glyphs.d.ts +31 -0
  15. package/src/font/glyphs.js +228 -0
  16. package/src/font/index.d.ts +10 -0
  17. package/src/font/index.js +10 -0
  18. package/src/font/measure.d.ts +16 -0
  19. package/src/font/measure.js +201 -0
  20. package/src/font/moves.d.ts +42 -0
  21. package/src/font/moves.js +263 -0
  22. package/src/forms/discs.d.ts +20 -0
  23. package/src/forms/discs.js +114 -0
  24. package/src/forms/emblems.d.ts +22 -0
  25. package/src/forms/emblems.js +109 -0
  26. package/src/forms/flow.d.ts +21 -0
  27. package/src/forms/flow.js +80 -0
  28. package/src/forms/folds.d.ts +18 -0
  29. package/src/forms/folds.js +147 -0
  30. package/src/forms/geo.d.ts +46 -0
  31. package/src/forms/geo.js +233 -0
  32. package/src/forms/hooks.d.ts +40 -0
  33. package/src/forms/hooks.js +204 -0
  34. package/src/forms/index.d.ts +22 -0
  35. package/src/forms/index.js +24 -0
  36. package/src/forms/jog.d.ts +33 -0
  37. package/src/forms/jog.js +162 -0
  38. package/src/forms/lines.d.ts +32 -0
  39. package/src/forms/lines.js +233 -0
  40. package/src/forms/ref.d.ts +41 -0
  41. package/src/forms/ref.js +246 -0
  42. package/src/forms/shatter.d.ts +40 -0
  43. package/src/forms/shatter.js +341 -0
  44. package/src/forms/slats.d.ts +35 -0
  45. package/src/forms/slats.js +78 -0
  46. package/src/forms/swash.d.ts +23 -0
  47. package/src/forms/swash.js +74 -0
  48. package/src/forms/tech.d.ts +40 -0
  49. package/src/forms/tech.js +293 -0
  50. package/src/forms/tracks.d.ts +36 -0
  51. package/src/forms/tracks.js +121 -0
  52. package/src/index.js +2 -1
  53. package/src/iso/index.d.ts +5 -0
  54. package/src/iso/index.js +13 -0
  55. package/src/iso/objects.d.ts +168 -0
  56. package/src/iso/objects.js +1042 -0
  57. package/src/iso/planes.d.ts +27 -0
  58. package/src/iso/planes.js +138 -0
  59. package/src/iso/voxels.d.ts +43 -0
  60. package/src/iso/voxels.js +255 -0
  61. package/src/letters/blackletter.d.ts +31 -0
  62. package/src/letters/blackletter.js +199 -0
  63. package/src/letters/dotstroke.d.ts +16 -0
  64. package/src/letters/dotstroke.js +77 -0
  65. package/src/letters/echo.d.ts +42 -0
  66. package/src/letters/echo.js +246 -0
  67. package/src/letters/frame.d.ts +6 -0
  68. package/src/letters/frame.js +14 -0
  69. package/src/letters/index.d.ts +16 -0
  70. package/src/letters/index.js +18 -0
  71. package/src/letters/inktrap.d.ts +66 -0
  72. package/src/letters/inktrap.js +244 -0
  73. package/src/letters/mono.d.ts +145 -0
  74. package/src/letters/mono.js +684 -0
  75. package/src/letters/pillar.d.ts +31 -0
  76. package/src/letters/pillar.js +257 -0
  77. package/src/letters/serif.d.ts +109 -0
  78. package/src/letters/serif.js +666 -0
  79. package/src/letters/stair.d.ts +8 -0
  80. package/src/letters/stair.js +43 -0
  81. package/src/letters/stem.d.ts +29 -0
  82. package/src/letters/stem.js +205 -0
  83. package/src/objects/animals.d.ts +105 -0
  84. package/src/objects/animals.js +469 -0
  85. package/src/objects/bio.d.ts +14 -0
  86. package/src/objects/bio.js +76 -0
  87. package/src/objects/devglyphs.d.ts +34 -0
  88. package/src/objects/devglyphs.js +226 -0
  89. package/src/objects/devices.d.ts +31 -0
  90. package/src/objects/devices.js +114 -0
  91. package/src/objects/faces.d.ts +94 -0
  92. package/src/objects/faces.js +344 -0
  93. package/src/objects/food.d.ts +12 -0
  94. package/src/objects/food.js +74 -0
  95. package/src/objects/index.d.ts +13 -0
  96. package/src/objects/index.js +18 -0
  97. package/src/objects/rosettes.d.ts +14 -0
  98. package/src/objects/rosettes.js +64 -0
  99. package/src/objects/sport.d.ts +8 -0
  100. package/src/objects/sport.js +37 -0
  101. package/src/objects/weapons.d.ts +31 -0
  102. package/src/objects/weapons.js +179 -0
  103. package/src/pixels/dither.d.ts +78 -0
  104. package/src/pixels/dither.js +399 -0
  105. package/src/pixels/emote.d.ts +6 -0
  106. package/src/pixels/emote.js +76 -0
  107. package/src/pixels/index.d.ts +7 -0
  108. package/src/pixels/index.js +8 -0
  109. package/src/sdf/index.d.ts +2 -0
  110. package/src/sdf/index.js +2 -0
  111. package/src/sdf/y2k.d.ts +12 -0
  112. package/src/sdf/y2k.js +80 -0
  113. package/src/util/vec.d.ts +14 -0
  114. package/src/util/vec.js +61 -0
@@ -0,0 +1,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;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Bold hooks: capsules whose round end runs on into a curl that wraps a neighbour's round end, the gap exact all round,
3
+ * and their Track build (the same layout drawn as two tracks). Angles are degrees, 0 right, 90 down. From the Logomarks
4
+ * tools (hookparts.mjs, batch 214, "Bold · Hooks", and its editions in batch 220).
5
+ */
6
+ import { I, U, circle, fillet, pill, poly, rad, soften, stroke, tracks } from '../geometry/index.js';
7
+
8
+ /** @typedef {import('../geometry/types.js').Point} Point */
9
+ /** @typedef {import('../geometry/types.js').Shape} Shape */
10
+ /** @typedef {import('../geometry/types.js').Geom} Geom */
11
+
12
+ /**
13
+ * A hook: the end of a piece it grows from, the neighbour's cap centre it wraps, and how far it sweeps.
14
+ *
15
+ * @typedef {object} Hook
16
+ * @property {0 | 1} [end] The end it grows from: 0 the first point, 1 (the default) the last.
17
+ * @property {readonly number[]} c The neighbour's cap centre.
18
+ * @property {number} [rn] The neighbour's cap radius (HOOK_W / 2).
19
+ * @property {number} [sw] The sweep, degrees; or give `e`.
20
+ * @property {number} [e] The angle the curl should end at (the neighbour's end direction), when `sw` isn't given.
21
+ * @property {number} [past] Degrees past `e`.
22
+ * @property {number} [t0] The curl's width at its root (the piece's width).
23
+ * @property {number} [t1] Its width at the tip (0.44 of the piece's width).
24
+ */
25
+
26
+ /** The capsule's width. */
27
+ export const HOOK_W = 150;
28
+
29
+ /**
30
+ * The point d from p at angle a.
31
+ *
32
+ * @param {readonly number[]} p The point.
33
+ * @param {number} a The angle, degrees.
34
+ * @param {number} d The distance.
35
+ * @returns {Point} The point reached.
36
+ */
37
+ export const hookAt = (p, a, d) => [p[0] + d * Math.cos(rad(a)), p[1] + d * Math.sin(rad(a))];
38
+
39
+ /**
40
+ * The angle from p to q, degrees.
41
+ *
42
+ * @param {readonly number[]} p From.
43
+ * @param {readonly number[]} q To.
44
+ * @returns {number} The angle.
45
+ */
46
+ const ang = (p, q) => (Math.atan2(q[1] - p[1], q[0] - p[0]) * 180) / Math.PI;
47
+
48
+ /**
49
+ * The distance from p to q.
50
+ *
51
+ * @param {readonly number[]} p From.
52
+ * @param {readonly number[]} q To.
53
+ * @returns {number} The distance.
54
+ */
55
+ const dist = (p, q) => Math.hypot(q[0] - p[0], q[1] - p[1]);
56
+
57
+ /**
58
+ * Where a piece's cap centre sits to hook the neighbour cap at c: at angle a from c, the two caps and the gap apart.
59
+ *
60
+ * @param {readonly number[]} c The neighbour's cap centre.
61
+ * @param {number} a The angle from it, degrees.
62
+ * @param {number} g The gap.
63
+ * @param {number} [r=HOOK_W / 2] This cap's radius.
64
+ * @param {number} [rn=HOOK_W / 2] The neighbour's.
65
+ * @returns {Point} The cap centre.
66
+ */
67
+ export const kiss = (c, a, g, r = HOOK_W / 2, rn = HOOK_W / 2) => hookAt(c, a, r + rn + g);
68
+
69
+ /**
70
+ * The curl: a band whose inner edge is radius ri about c, from angle a0 sweeping sw degrees, tapering t0 → t1, with a
71
+ * round tip.
72
+ *
73
+ * @param {readonly number[]} c The centre.
74
+ * @param {number} ri The inner radius.
75
+ * @param {number} a0 The start angle, degrees.
76
+ * @param {number} sw The sweep, degrees.
77
+ * @param {number} t0 The width at the start.
78
+ * @param {number} t1 The width at the tip.
79
+ * @param {number} [n=120] Steps along it.
80
+ * @returns {Shape} The curl.
81
+ */
82
+ export const curl = (c, ri, a0, sw, t0, t1, n = 120) => {
83
+ const inner = [], outer = [];
84
+ for (let i = 0; i <= n; i++) { const f = i / n, a = a0 + sw * f, t = t1 + (t0 - t1) * (1 - f) ** 2; inner.push(hookAt(c, a, ri)); outer.push(hookAt(c, a, ri + t)); }
85
+ return U(poly([...inner, ...outer.reverse()]), circle(...hookAt(c, a0 + sw, ri + t1 / 2), t1 / 2, 120));
86
+ };
87
+
88
+ /**
89
+ * A piece: a capsule w wide along `pts` (corners filleted by `bend`), with hooks. Each hook names the end it grows from,
90
+ * the neighbour's cap centre and the sweep; it logs when the end isn't a kiss from the neighbour. The root of each curl is
91
+ * filleted (`soft`) where it leaves the round end, and only there, so a same-piece gap under 2·soft (a bowl's tip near its
92
+ * stem) doesn't close up; `whole` fillets every inner corner of a piece with no such gap.
93
+ *
94
+ * @param {readonly Point[]} pts The capsule's centreline.
95
+ * @param {readonly Hook[]} [hooks=[]] Its hooks.
96
+ * @param {{ w?: number, bend?: number, g?: number, soft?: number, whole?: boolean }} [o] Width (HOOK_W), corner fillet
97
+ * (90), gap (22), root fillet (40), and whether to fillet every inner corner.
98
+ * @returns {Shape} The piece.
99
+ */
100
+ export const piece = (pts, hooks = [], { w = HOOK_W, bend = 90, g = 22, soft = 40, whole = false } = {}) => {
101
+ const body = pts.length === 2 ? pill(pts[0], pts[1], w) : stroke(bend ? fillet(pts, bend) : pts, w, { roundCaps: true, chunk: 8 });
102
+ const curls = hooks.map(({ end = 1, c, rn = HOOK_W / 2, sw, e, past = 0, t0 = w, t1 = 0.44 * w }) => {
103
+ const q = end ? pts.at(-1) : pts[0];
104
+ const d = dist(c, q), want = w / 2 + rn + g; if (Math.abs(d - want) > 0.5) console.log(` hook off by ${(d - want).toFixed(1)}`);
105
+ const a0 = ang(c, q); if (sw == null) { const dd = ((e - a0 + 540) % 360) - 180; sw = dd + Math.sign(dd) * past; }
106
+ return curl(c, rn + g, a0, sw, t0, t1);
107
+ });
108
+ const roots = hooks.map(({ end = 1 }) => circle(...(end ? pts.at(-1) : pts[0]), 0.9 * w, 160));
109
+ if (soft && whole) return soften(U(body, ...curls), soft, 0);
110
+ return soft && roots.length ? U(body, ...curls, I(soften(U(body, ...curls), soft, 0), ...(roots.length > 1 ? [U(...roots)] : roots))) : U(body, ...curls);
111
+ };
112
+
113
+ /**
114
+ * The Track band's width for the gap g: 74 at 22, so the Track build lands on the house Track rules once fitted. Pass it
115
+ * as `r` and `rn` to kiss() for a Track layout.
116
+ *
117
+ * @param {number} g The gap.
118
+ * @returns {number} The band width.
119
+ */
120
+ export const TB = (g) => (74 * g) / 22;
121
+
122
+ /**
123
+ * The Track build of a piece: its centreline run on round each hook's neighbour (at the kiss distance, so the curl's inner
124
+ * track clears the neighbour's tracks by g), drawn as two tracks 26 wide at pitch 48, both scaled with g.
125
+ *
126
+ * @param {readonly Point[]} pts The centreline.
127
+ * @param {readonly Hook[]} [hooks=[]] Its hooks (`end`, `c`, `sw` or `e` and `past`).
128
+ * @param {{ g?: number, bend?: number }} [o] The gap (22) and the corner fillet (60).
129
+ * @returns {Shape} The two tracks.
130
+ */
131
+ export const trackPiece = (pts, hooks = [], { g = 22, bend = 60 } = {}) => {
132
+ let path = pts.slice();
133
+ for (const { end = 1, c, sw: sw0, e, past = 0 } of hooks) {
134
+ if (!end) path = path.slice().reverse(); const q = path.at(-1), a0 = ang(c, q);
135
+ let sw = sw0; if (sw == null) { const dd = ((e - a0 + 540) % 360) - 180; sw = dd + Math.sign(dd) * past; }
136
+ path = [...path, ...arc(c, dist(c, q), a0, a0 + sw, Math.max(8, Math.ceil(Math.abs(sw) / 4))).slice(1)];
137
+ }
138
+ return tracks(bend ? fillet(path, bend) : path, { n: 2, pitch: (48 * g) / 22, w: (26 * g) / 22 });
139
+ };
140
+
141
+ /**
142
+ * Points along an arc about c, radius r, from a0 to a1 degrees.
143
+ *
144
+ * @param {readonly number[]} c The centre.
145
+ * @param {number} r The radius.
146
+ * @param {number} a0 The start angle.
147
+ * @param {number} a1 The end angle.
148
+ * @param {number} [n=60] Steps.
149
+ * @returns {Point[]} n + 1 points.
150
+ */
151
+ export const arc = (c, r, a0, a1, n = 60) => [...Array(n + 1)].map((_, i) => hookAt(c, a0 + ((a1 - a0) * i) / n, r));
152
+
153
+ /**
154
+ * A neighbour's end, for a hook: its cap centre and the way the end faces.
155
+ *
156
+ * @param {readonly Point[]} pts The neighbour's centreline.
157
+ * @param {0 | 1} [end=1] Which end.
158
+ * @returns {{ c: Point, e: number }} The cap centre and the angle the end faces.
159
+ */
160
+ export const endOf = (pts, end = 1) => (end ? { c: pts.at(-1), e: ang(pts.at(-2), pts.at(-1)) } : { c: pts[0], e: ang(pts[1], pts[0]) });
161
+
162
+ /**
163
+ * Every point of every ring of a shape.
164
+ *
165
+ * @param {Shape} s The shape.
166
+ * @returns {Point[]} The points.
167
+ */
168
+ export const pts0 = (s) => s.flatMap((pg) => pg.flatMap((r) => r));
169
+
170
+ /**
171
+ * The least distance between two shapes' outline points.
172
+ *
173
+ * @param {Shape} a One shape.
174
+ * @param {Shape} b The other.
175
+ * @returns {number} The distance.
176
+ */
177
+ const near = (a, b) => { let m = 1e9; const pa = pts0(a), pb = pts0(b); for (const p of pa) for (const q of pb) { const d = (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2; if (d < m) m = d; } return Math.sqrt(m); };
178
+
179
+ /** join()'s check: set `on` to have it log any two pieces closer than the gap. */
180
+ export const hookCheck = { on: false };
181
+
182
+ /**
183
+ * The pieces as one shape; with hookCheck.on, it logs any two that come closer than the gap.
184
+ *
185
+ * @param {number} g The gap.
186
+ * @param {...Shape} ps The pieces.
187
+ * @returns {Shape} Their union.
188
+ */
189
+ export const join = (g, ...ps) => {
190
+ if (hookCheck.on) for (let i = 0; i < ps.length; i++) for (let j = i + 1; j < ps.length; j++) { const d = near(ps[i], ps[j]); if (d < g - 0.6) console.log(` pieces ${i},${j} only ${d.toFixed(1)} apart (g ${g.toFixed(1)})`); }
191
+ return U(...ps);
192
+ };
193
+
194
+ /**
195
+ * For the Track build: a straight run must enter the curl on its tangent (a corner there kinks the outer track), so the
196
+ * piece's end sits a quarter turn round from where the bold build puts it: c + (TB(g) + g) along d ± 90°.
197
+ *
198
+ * @param {readonly number[]} c The neighbour's cap centre.
199
+ * @param {number} d The run's direction, degrees.
200
+ * @param {number} side Which side: 1 or −1.
201
+ * @param {number} g The gap.
202
+ * @returns {Point} The piece's end.
203
+ */
204
+ export const tq = (c, d, side, g) => hookAt(c, d + side * 90, TB(g) + g);
@@ -0,0 +1,22 @@
1
+ export * from './geo.js';
2
+ export * from './discs.js';
3
+ export * from './hooks.js';
4
+ export * from './jog.js';
5
+ export * from './tech.js';
6
+ export * from './emblems.js';
7
+ export * from './shatter.js';
8
+ export * from './slats.js';
9
+ export * from './flow.js';
10
+ export * from './folds.js';
11
+ export * from './swash.js';
12
+ export * from './ref.js';
13
+ export * from './tracks.js';
14
+ export * from './lines.js';
15
+ export type { Hook } from './hooks.js';
16
+ export type { JogArm } from './jog.js';
17
+ export type { Rail, Shard, ShardMove } from './shatter.js';
18
+ export type { Slat, SlatRowOptions, SlatEdges } from './slats.js';
19
+ export type { FlowOptions } from './flow.js';
20
+ export type { SwashOptions } from './swash.js';
21
+ export type { StripeEdge } from './ref.js';
22
+ export type { TrackKit } from './tracks.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @module @logomarks/core/forms
3
+ *
4
+ * The constructions the Bold, Line and Track families are drawn with, on the 512 canvas with the house gap: needle
5
+ * gaps, knobs and split discs (geo), one disc cut so a moved piece keeps its gap (discs), capsules whose ends curl round
6
+ * a neighbour (hooks), jogged bursts and chamfer checkers (jog), sparkles, ribbons and keyed chevrons (tech), the split
7
+ * emblem's horizon and grounds (emblems), a form breaking into shards (shatter), turning slats, flow grids, fold seams
8
+ * and straps, swashes, rotation, stripes, guilloche and speed letters (ref), the Track editions' kit (tracks), and soft
9
+ * blobs, Rail lines, chamfered paths and interlocking overlaps (lines).
10
+ */
11
+ export * from './geo.js';
12
+ export * from './discs.js';
13
+ export * from './hooks.js';
14
+ export * from './jog.js';
15
+ export * from './tech.js';
16
+ export * from './emblems.js';
17
+ export * from './shatter.js';
18
+ export * from './slats.js';
19
+ export * from './flow.js';
20
+ export * from './folds.js';
21
+ export * from './swash.js';
22
+ export * from './ref.js';
23
+ export * from './tracks.js';
24
+ export * from './lines.js';
@@ -0,0 +1,33 @@
1
+ import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
2
+
3
+ /** One jogged arm's options. */
4
+ export interface JogArm {
5
+ r0?: number;
6
+ rb?: number;
7
+ breaks?: readonly number[];
8
+ R: number;
9
+ w: number;
10
+ g: number;
11
+ shift: number;
12
+ scarf?: number;
13
+ innerShift?: number;
14
+ cx?: number;
15
+ cy?: number;
16
+ }
17
+
18
+ /** The point `along` the axis at deg from (cx, cy), stepped `side` square to it. */
19
+ export function axisAt(deg: number, along: number, side?: number, cx?: number, cy?: number): Point;
20
+ /** A bar on the axis at deg from a0 to a1, w wide, `side` off the axis, its ends slanted by cut0 / cut1. */
21
+ export function axisBar(deg: number, a0: number, a1: number, w: number, side?: number, o?: { cut0?: number; cut1?: number; cx?: number; cy?: number }): Polygon;
22
+ /** One jogged arm: runs broken by g and stepped sideways. */
23
+ export function jogArm(deg: number, o: JogArm): Shape;
24
+ /** n jogged arms round the centre, the first at deg0. */
25
+ export function jogBurst(n: number, deg0: number, o: JogArm, each?: (i: number) => Partial<JogArm>): Shape;
26
+ /** A polygon with its corners cut at 45° by c (one or one per corner). */
27
+ export function chamfer(pts: readonly Point[], c: number | readonly number[]): Polygon;
28
+ /** A box from its corners, chamfered by c. */
29
+ export function chamferBox(x0: number, y0: number, x1: number, y1: number, c?: number | readonly number[]): Polygon;
30
+ /** A checker's dark cells, corner meetings cut so two squares are parted by exactly g. */
31
+ export function checkerCells(x0: number, y0: number, p: number, cols: number, rows: number, g: number, o?: { parity?: 0 | 1; outer?: number; skip?: (i: number, j: number) => boolean }): Shape;
32
+ /** Throws unless a stands at least g (less 0.5) clear of b. */
33
+ export function assertClear(a: Geom, b: Geom, g: number, what?: string): void;