@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,246 @@
1
+ /**
2
+ * Four constructions from the reference sheets, as small helpers a generator composes (constructions only; no mark's
3
+ * geometry lives here):
4
+ *
5
+ * - rotation ("Bold · Rotational"): turns() sets one unit round a centre n times; cyclic() puts each copy in front of the
6
+ * one before it (a cyclic overlap on any unit), every crossing parted by the gap;
7
+ * - stripes ("Bold · Stripes"): stripeBands() draws stripes as bands between two edge functions of x, so a stripe can
8
+ * ripple or taper; stripesAlong() bends them with mitred corners, widths held square to each run; evenBands() spaces
9
+ * n stripes evenly across a span and thinning() with a fixed gap, their widths easing from one end to the other;
10
+ * - guilloche ("Line · Guilloche"): waveRing() is the closed curve the bold guilloche strands run along (the kit's
11
+ * weave() parts them over and under);
12
+ * - speed italics ("Letters · Speed"): italic() slants a glyph built upright, wedge() is the one sharp notch, and the
13
+ * slab capitals (SPEED_GLYPHS, on SPEED_BOX) take the speed slit (slit()) through speed().
14
+ *
15
+ * From the Logomarks tools (refparts.mjs, batch 237; the speed capitals shared from batch 240).
16
+ */
17
+ import { C, poly, rect, rot, mapPts, U, D, grown, closedCurve, mitreBand as band, mitreOffset as mitreOff } from '../geometry/index.js';
18
+
19
+ /* ---------- rotation ---------- */
20
+
21
+ /**
22
+ * n copies of a shape turned evenly round (cx, cy), the first turned deg0: a list, not yet united.
23
+ *
24
+ * @param {import('../geometry/types.js').Geom} g The unit.
25
+ * @param {number} n How many.
26
+ * @param {number} [deg0=0] The first copy's turn (degrees).
27
+ * @param {number} [cx=C] The centre.
28
+ * @param {number} [cy=C] The centre.
29
+ * @returns {import('../geometry/types.js').Geom[]} The copies.
30
+ */
31
+ export const turns = (g, n, deg0 = 0, cx = C, cy = C) => [...Array(n)].map((_, k) => rot(g, deg0 + (360 * k) / n, cx, cy));
32
+
33
+ /**
34
+ * A cyclic overlap: every unit cut free by the next one round, so each lies in front of one neighbour and behind the
35
+ * other (no real stack does that, which is the move); every crossing is parted by g.
36
+ *
37
+ * @param {readonly import('../geometry/types.js').Geom[]} units The units, in order round.
38
+ * @param {number} g The gap.
39
+ * @returns {import('../geometry/types.js').Shape} The overlap.
40
+ */
41
+ export const cyclic = (units, g) => U(...units.map((u, k) => D(u, grown(units[(k + 1) % units.length], g))));
42
+
43
+ /**
44
+ * A chevron: two arms w wide meeting at a sharp apex at the origin, opening `spread` degrees, pointing along +x, each
45
+ * arm L long (measured along its outer edge). Move and turn it into place.
46
+ *
47
+ * @param {number} L The arms' length.
48
+ * @param {number} w Their width.
49
+ * @param {number} spread The opening (degrees).
50
+ * @returns {import('../geometry/types.js').Polygon} The chevron.
51
+ */
52
+ export function chevron(L, w, spread) {
53
+ const h = (spread / 2) * (Math.PI / 180), s = w / Math.sin(h);
54
+ const arm = (k) => [-L * Math.cos(h), k * L * Math.sin(h)];
55
+ const a = arm(1), b = arm(-1), tipIn = [-s, 0];
56
+ const inner = (p) => [p[0] - s, p[1]];
57
+ return poly([[0, 0], b, inner(b), tipIn, inner(a), a]);
58
+ }
59
+
60
+ /* ---------- stripes ---------- */
61
+
62
+ /**
63
+ * Stripes as bands: each entry [top, bottom], each a number or a function of x (y down), sampled every dx across the
64
+ * canvas and beyond, so a region clips them.
65
+ *
66
+ * @param {readonly (readonly (number | ((x: number) => number))[])[]} list The stripes' [top, bottom] edges.
67
+ * @param {{ x0?: number, x1?: number, dx?: number }} [o] Where the sampling starts (−80) and ends (592), and its step (4).
68
+ * @returns {import('../geometry/types.js').Shape} The stripes.
69
+ */
70
+ export function stripeBands(list, { x0 = -80, x1 = 592, dx = 4 } = {}) {
71
+ const at = (f, x) => (typeof f === 'function' ? f(x) : f), xs = [];
72
+ for (let x = x0; x <= x1 + 1e-9; x += dx) xs.push(x);
73
+ return U(...list.map(([t, b]) => poly([...xs.map((x) => [x, at(t, x)]), ...xs.slice().reverse().map((x) => [x, at(b, x)])])));
74
+ }
75
+
76
+ /**
77
+ * n even stripes across [a, b] with a gap gs between them.
78
+ *
79
+ * @param {number} n How many.
80
+ * @param {number} a Where they start.
81
+ * @param {number} b Where they end.
82
+ * @param {number} gs The gap between two.
83
+ * @returns {[number, number][]} Each stripe's [top, bottom].
84
+ */
85
+ export const evenBands = (n, a, b, gs) => { const w = (b - a - (n - 1) * gs) / n; return [...Array(n)].map((_, k) => [a + k * (w + gs), a + k * (w + gs) + w]); };
86
+
87
+ /**
88
+ * n stripes across [a, b] with a fixed gap gs, their widths in the ratio r0 : r1 from the first to the last (linear).
89
+ *
90
+ * @param {number} n How many.
91
+ * @param {number} a Where they start.
92
+ * @param {number} b Where they end.
93
+ * @param {number} gs The gap between two.
94
+ * @param {number} r0 The first stripe's share.
95
+ * @param {number} r1 The last stripe's share.
96
+ * @returns {[number, number][]} Each stripe's [top, bottom].
97
+ */
98
+ export function thinning(n, a, b, gs, r0, r1) {
99
+ const ws = [...Array(n)].map((_, k) => r0 + ((r1 - r0) * k) / (n - 1)), k = (b - a - (n - 1) * gs) / ws.reduce((s, v) => s + v, 0);
100
+ let y = a; return ws.map((v) => { const out = [y, y + v * k]; y += v * k + gs; return out; });
101
+ }
102
+
103
+ /**
104
+ * n stripes w wide along a polyline, `pitch` apart measured square to every run (corners mitred), so a stripe that
105
+ * bends keeps its width and every gap through the bend.
106
+ *
107
+ * @param {readonly import('../geometry/types.js').Point[]} pts The polyline.
108
+ * @param {number} n How many stripes.
109
+ * @param {number} w Their width.
110
+ * @param {number} pitch Centre to centre.
111
+ * @returns {import('../geometry/types.js').Shape} The stripes.
112
+ */
113
+ export const stripesAlong = (pts, n, w, pitch) => U(...[...Array(n)].map((_, k) => band(mitreOff(pts, (k - (n - 1) / 2) * pitch), w)));
114
+
115
+ /**
116
+ * A stripe list shifted by f(x), so every stripe follows one wave or one pulse.
117
+ *
118
+ * @param {readonly (readonly number[])[]} list The stripes' [top, bottom].
119
+ * @param {(x: number) => number} f The shift.
120
+ * @returns {((x: number) => number)[][]} The stripes as edge functions, for stripeBands().
121
+ */
122
+ export const shifted = (list, f) => list.map(([t, b]) => [(x) => t + f(x), (x) => b + f(x)]);
123
+
124
+ /**
125
+ * Stripes in a turned frame: drawn level, then turned deg about the centre.
126
+ *
127
+ * @param {readonly (readonly (number | ((x: number) => number))[])[]} list The stripes' [top, bottom] edges.
128
+ * @param {number} deg The turn (degrees).
129
+ * @param {{ x0?: number, x1?: number, dx?: number }} [o] stripeBands()'s options.
130
+ * @returns {import('../geometry/types.js').Shape} The stripes.
131
+ */
132
+ export const turned = (list, deg, o) => rot(stripeBands(list, o), deg);
133
+
134
+ /* ---------- guilloche curves ---------- */
135
+
136
+ /**
137
+ * A ring whose radius waves k times round: r = R + A·cos(k·t + ph), sampled at n points.
138
+ *
139
+ * @param {number} R The mean radius.
140
+ * @param {number} A The wave's amplitude.
141
+ * @param {number} k Waves round.
142
+ * @param {number} [ph=0] The phase.
143
+ * @param {number} [n=480] Points.
144
+ * @param {number} [cx=C] The centre.
145
+ * @param {number} [cy=C] The centre.
146
+ * @returns {import('../geometry/types.js').Point[]} The ring.
147
+ */
148
+ export const waveRing = (R, A, k, ph = 0, n = 480, cx = C, cy = C) => closedCurve((t) => { const r = R + A * Math.cos(k * t + ph); return [cx + r * Math.cos(t), cy + r * Math.sin(t)]; }, n);
149
+
150
+ /* ---------- speed italics ---------- */
151
+
152
+ /**
153
+ * A glyph drawn upright, slanted deg degrees (tops lean right) about the line y = cy.
154
+ *
155
+ * @param {import('../geometry/types.js').Geom} g The glyph.
156
+ * @param {number} [deg=17] The slant.
157
+ * @param {number} [cy=C] The line it slants about.
158
+ * @returns {import('../geometry/types.js').Geom} The slanted glyph.
159
+ */
160
+ export const italic = (g, deg = 17, cy = C) => { const t = Math.tan((deg * Math.PI) / 180); return mapPts(g, ([x, y]) => [x + (cy - y) * t, y]); };
161
+
162
+ /**
163
+ * A sharp notch: the triangle from the edge points a and b to the tip t, set `out` past the edge so it cuts clean.
164
+ *
165
+ * @param {readonly number[]} a One edge point.
166
+ * @param {readonly number[]} b The other.
167
+ * @param {import('../geometry/types.js').Point} t The tip.
168
+ * @param {number} [out=6] How far past the edge it starts.
169
+ * @returns {import('../geometry/types.js').Polygon} The notch.
170
+ */
171
+ export const wedge = (a, b, t, out = 6) => {
172
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], v = [m[0] - t[0], m[1] - t[1]], L = Math.hypot(...v);
173
+ const o = [(v[0] / L) * out, (v[1] / L) * out]; return poly([[a[0] + o[0], a[1] + o[1]], [b[0] + o[0], b[1] + o[1]], t]);
174
+ };
175
+
176
+ /* ---------- speed slab capitals ---------- */
177
+ // Heavy slab capitals built upright on a 368 × 400 box (M 432 wide; stems 112, bars 92, sharp corners, flat terminals; S
178
+ // has its two outer turns chamfered so it never reads as a 5), slanted 17°, each with ONE speed slit: a sharp wedge cut
179
+ // in from the trailing (left) edge of one stroke, rising slightly forward.
180
+
181
+ /** The speed capitals' box: cap line T0, baseline B0, left edge L0, right edge R0, stem SW and bar BW. */
182
+ export const SPEED_BOX = { T0: 56, B0: 456, L0: 72, R0: 440, SW: 112, BW: 92 };
183
+ const { T0, B0, L0, R0, SW, BW } = SPEED_BOX;
184
+
185
+ /**
186
+ * A bar from y0 to y1, across the speed box unless told.
187
+ *
188
+ * @param {number} y0 Its top.
189
+ * @param {number} y1 Its bottom.
190
+ * @param {number} [x0] Its left (the box's).
191
+ * @param {number} [x1] Its right (the box's).
192
+ * @returns {import('../geometry/types.js').Polygon} The bar.
193
+ */
194
+ export const hz = (y0, y1, x0 = L0, x1 = R0) => rect(x0, y0, x1 - x0, y1 - y0);
195
+
196
+ /**
197
+ * An upright from x0 to x1, the speed box's full height unless told.
198
+ *
199
+ * @param {number} x0 Its left.
200
+ * @param {number} x1 Its right.
201
+ * @param {number} [y0] Its top (the cap line).
202
+ * @param {number} [y1] Its bottom (the baseline).
203
+ * @returns {import('../geometry/types.js').Polygon} The upright.
204
+ */
205
+ export const vt = (x0, x1, y0 = T0, y1 = B0) => rect(x0, y0, x1 - x0, y1 - y0);
206
+
207
+ /**
208
+ * The speed slit: a wedge `bw` wide where it enters at a, its sharp tip `len` further along `dir`.
209
+ *
210
+ * @param {readonly number[]} a Where it enters.
211
+ * @param {number} len How far the tip runs.
212
+ * @param {number} [dir=-8] Its direction (degrees, y down).
213
+ * @param {number} [bw=46] Its width at the entry.
214
+ * @returns {import('../geometry/types.js').Polygon} The slit.
215
+ */
216
+ export const slit = (a, len, dir = -8, bw = 46) => {
217
+ const r = (dir * Math.PI) / 180, n = [(-Math.sin(r) * bw) / 2, (Math.cos(r) * bw) / 2];
218
+ return wedge([a[0] + n[0], a[1] + n[1]], [a[0] - n[0], a[1] - n[1]], [a[0] + Math.cos(r) * len, a[1] + Math.sin(r) * len]);
219
+ };
220
+
221
+ /**
222
+ * A speed letter: the glyph less its slit, slanted 17°.
223
+ *
224
+ * @param {import('../geometry/types.js').Geom} glyph The upright glyph.
225
+ * @param {import('../geometry/types.js').Geom} cut Its slit.
226
+ * @returns {import('../geometry/types.js').Geom} The letter.
227
+ */
228
+ export const speed = (glyph, cut) => italic(D(glyph, cut), 17);
229
+
230
+ /** The speed slab capitals (N M Z K X S R A), each a function returning the upright glyph on SPEED_BOX. */
231
+ export const SPEED_GLYPHS = {
232
+ N: () => U(vt(L0, L0 + SW), vt(R0 - SW, R0), poly([[L0, T0], [L0 + SW + 18, T0], [R0, B0 - 150], [R0, B0], [R0 - SW - 18, B0], [L0, T0 + 150]])),
233
+ M: () => poly([[40, T0], [150, T0], [C, 262], [362, T0], [472, T0], [472, B0], [368, B0], [368, 232], [C + 30, B0], [C - 30, B0], [144, 232], [144, B0], [40, B0]]),
234
+ Z: () => U(hz(T0, T0 + BW), hz(B0 - BW, B0), poly([[R0 - 140, T0 + BW], [R0, T0 + BW], [L0 + 140, B0 - BW], [L0, B0 - BW]])),
235
+ K: () => {
236
+ const a = [L0 + SW - 10, 372], b = [R0, T0], p = (t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
237
+ return U(vt(L0, L0 + SW), poly([[L0 + SW - 10, 236], [R0 - 128, T0], [R0, T0], a]), poly([p(0.3), p(0.62), [R0, B0], [R0 - 126, B0]]));
238
+ },
239
+ X: () => U(poly([[L0, T0], [L0 + 124, T0], [R0, B0], [R0 - 124, B0]]), poly([[R0 - 124, T0], [R0, T0], [L0 + 124, B0], [L0, B0]])),
240
+ S: () => D(U(hz(T0, T0 + BW), vt(L0, L0 + SW, T0, 302), hz(210, 302), vt(R0 - SW, R0, 210, B0), hz(B0 - BW, B0)), poly([[L0 - 1, T0 - 1], [L0 + 76, T0 - 1], [L0 - 1, T0 + 76]]), poly([[R0 + 1, B0 + 1], [R0 - 76, B0 + 1], [R0 + 1, B0 - 76]])),
241
+ R: () => U(vt(L0, L0 + SW), hz(T0, T0 + BW), vt(R0 - SW, R0, T0, 314), hz(222, 314), poly([[R0 - SW - 70, 314], [R0 - SW + 44, 314], [R0, B0], [R0 - 120, B0]])),
242
+ A: () => {
243
+ const out = poly([[166, T0], [346, T0], [R0, B0], [L0, B0]]), inner = poly([[226, T0 + BW], [286, T0 + BW], [312, 290], [200, 290]]), foot = poly([[178, 372], [334, 372], [358, B0 + 4], [154, B0 + 4]]);
244
+ return D(out, inner, foot);
245
+ },
246
+ };
@@ -0,0 +1,40 @@
1
+ import type { Geom, Point, Ring, Shape } from '../geometry/types.js';
2
+
3
+ /** A rail: s in 0…1 to a point. */
4
+ export type Rail = (s: number) => Point;
5
+ /** A shard of a strip: its ring, its place along the strip and the rail its long side lies on. */
6
+ export interface Shard {
7
+ r: Ring;
8
+ s: number;
9
+ base: 'inner' | 'outer';
10
+ c?: Point;
11
+ }
12
+ /** A shard's shrink, turn and move. */
13
+ export interface ShardMove {
14
+ k?: number;
15
+ deg?: number;
16
+ d?: readonly number[];
17
+ }
18
+
19
+ /** A rail along a polyline, by arc length. */
20
+ export function rail(pts: readonly Point[]): Rail;
21
+ /** The run of a closed outline from the vertex nearest p to the vertex nearest q. */
22
+ export function outlineRun(ring: readonly Point[], p: readonly number[], q: readonly number[]): Point[];
23
+ /** The two rails of a band w wide along a centreline, mitred: [left, right]. */
24
+ export function bandRails(pts: readonly Point[], w: number): [Rail, Rail];
25
+ /** The triangular shards of a band between two rails, smaller down the strip. */
26
+ export function shardStrip(inner: Rail, outer: Rail, o?: { n?: number; ratio?: number; jit?: number; rng?: () => number; arcN?: number }): Shard[];
27
+ /** A shard inset by half a gap, its tips cleared. */
28
+ export function inset(r: readonly Point[], g: number): Ring | undefined;
29
+ /** The form less the band, cut back half a gap along the crack. */
30
+ export function cutBack(shape: Geom, inner: Rail, outer: Rail, i0: readonly number[], g: number, k?: number): Shape;
31
+ /** The shards moved off the body, each pushed on until it is g clear. */
32
+ export function drift(body: Geom, shards: readonly Shard[], o: { move: (p: Shard & { c: Point }) => ShardMove; g: number; step?: number; min: number }): Geom[];
33
+ /** A move spreading the shards out from a point. */
34
+ export function spread(from: readonly number[], grow: readonly number[], shrink?: number, turn?: number): (p: Shard & { c: Point }) => ShardMove;
35
+ /** A form breaking on one side into drifting shards. */
36
+ export function breakOff(shape: Geom, inner: Rail, outer: Rail, o: { n?: number; ratio?: number; jit?: number; all?: boolean; move: (p: Shard & { c: Point }) => ShardMove; g: number; min?: number }): Shape;
37
+ /** The house recipe: breakOff() spread from the centre, as a build of the gap. */
38
+ export function houseBreak(shape: Geom, inner: Rail, outer: Rail, o?: { from?: readonly number[]; grow?: readonly number[]; shrink?: number; turn?: number; n?: number; ratio?: number; jit?: number; all?: boolean; min?: number }): (g: number) => Shape;
39
+ /** The point k of the way from p to q. */
40
+ export function lerpAt(p: readonly number[], q: readonly number[], k: number): Point;
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Shatter: a solid form whose one side breaks into a few angular shards that drift off, smaller as they go, every part a
3
+ * house gap from the next. The shards are irregular triangles from a seeded strip (so seed Math.random per mark), not a
4
+ * grid of squares. A mark is made by `breakOff()`:
5
+ *
6
+ * 1. `shardStrip(inner, outer, o)`: the zone that breaks off is a band between two rails, the inner one (the crack, or a
7
+ * ring's inside) and the outer one (the form's own outline), each a function of s in 0…1 (`rail()`, `bandRails()`).
8
+ * Nodes go down both rails in turn, seeded and stepping down in size, and every three in a row make a triangle: a strip
9
+ * of shards that tiles the band, the outer side of each still on the form's outline, so the eye completes the form.
10
+ * 2. `cutBack()`: the body is the form less the band, cut back half a gap along the crack by a mitred band (its edge stays
11
+ * sharp); every shard is inset half a gap (`inset()`), so the strip starts as a mosaic with every part a gap apart.
12
+ * 3. `drift(body, shards, o)`: each shard is turned, shrunk about its centre and moved by its place along the strip
13
+ * (`spread()`: out from a point inside the form), nearest the body first, and pushed on along its move until it is a
14
+ * gap clear of the body and of every shard before it. A mark keeps no shard under 28 across once fitted.
15
+ *
16
+ * From the Logomarks tools (shatterparts.mjs, batch 231, "Bold · Shatter", and batch 241).
17
+ */
18
+ import { U, D, I, poly, mapPts, bar, soften, mitreBand as mitred, mitreOffset as mitreOff } from '../geometry/index.js';
19
+
20
+ /** @typedef {import('../geometry/types.js').Point} Point */
21
+ /** @typedef {import('../geometry/types.js').Ring} Ring */
22
+ /** @typedef {import('../geometry/types.js').Polygon} Polygon */
23
+ /** @typedef {import('../geometry/types.js').Shape} Shape */
24
+ /** @typedef {import('../geometry/types.js').Geom} Geom */
25
+ /** @typedef {(s: number) => Point} Rail */
26
+
27
+ /**
28
+ * A shard of a strip.
29
+ *
30
+ * @typedef {object} Shard
31
+ * @property {Ring} r Its ring.
32
+ * @property {number} s Its place along the strip, 0…1.
33
+ * @property {'inner' | 'outer'} base The rail its long side lies on.
34
+ * @property {Point} [c] Its centre (drift() adds it).
35
+ */
36
+
37
+ /**
38
+ * A shard's move in drift().
39
+ *
40
+ * @typedef {object} ShardMove
41
+ * @property {number} [k] Its shrink about its centre (1: none).
42
+ * @property {number} [deg] Its turn, degrees.
43
+ * @property {readonly number[]} [d] Its move [dx, dy].
44
+ */
45
+
46
+ /* ---------- measures ---------- */
47
+
48
+ /**
49
+ * The pieces of a shape as single polygons ([outer, ...holes]).
50
+ *
51
+ * @param {Geom} s The shape.
52
+ * @returns {Shape} The polygons.
53
+ */
54
+ const polys = (s) => (s.length && typeof s[0][0][0] === 'number' ? [s] : s);
55
+
56
+ /**
57
+ * The area of a ring.
58
+ *
59
+ * @param {readonly Point[]} r The ring.
60
+ * @returns {number} Its area.
61
+ */
62
+ const area = (r) => Math.abs(r.reduce((s, p, i) => { const q = r[(i + 1) % r.length]; return s + p[0] * q[1] - q[0] * p[1]; }, 0) / 2);
63
+
64
+ /**
65
+ * The centre of mass of a ring.
66
+ *
67
+ * @param {readonly Point[]} r The ring.
68
+ * @returns {Point} Its centroid.
69
+ */
70
+ const centroid = (r) => {
71
+ let a = 0, x = 0, y = 0;
72
+ r.forEach((p, i) => { const q = r[(i + 1) % r.length], c = p[0] * q[1] - q[0] * p[1]; a += c; x += (p[0] + q[0]) * c; y += (p[1] + q[1]) * c; });
73
+ return [x / (3 * a), y / (3 * a)];
74
+ };
75
+
76
+ /**
77
+ * A ring's width across its narrowest: over the edges every vertex lies behind (its hull edges), the least of each edge's
78
+ * greatest reach. A triangle's shortest altitude.
79
+ *
80
+ * @param {readonly Point[]} r The ring.
81
+ * @returns {number} The width.
82
+ */
83
+ const minWidth = (r) => {
84
+ let best = Infinity;
85
+ r.forEach((p, i) => {
86
+ const q = r[(i + 1) % r.length], L = Math.hypot(q[0] - p[0], q[1] - p[1]); if (L < 1e-6) return;
87
+ let lo = 0, hi = 0; for (const v of r) { const d = ((q[0] - p[0]) * (v[1] - p[1]) - (q[1] - p[1]) * (v[0] - p[0])) / L; lo = Math.min(lo, d); hi = Math.max(hi, d); }
88
+ if (lo > -0.5 || hi < 0.5) best = Math.min(best, Math.max(-lo, hi));
89
+ });
90
+ return best;
91
+ };
92
+
93
+ /**
94
+ * The distance from the point p to the segment ab.
95
+ *
96
+ * @param {readonly number[]} p The point.
97
+ * @param {readonly number[]} a The segment's start.
98
+ * @param {readonly number[]} b Its end.
99
+ * @returns {number} The distance.
100
+ */
101
+ const segDist = (p, a, b) => {
102
+ const dx = b[0] - a[0], dy = b[1] - a[1], L2 = dx * dx + dy * dy, t = L2 ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L2)) : 0;
103
+ return Math.hypot(p[0] - a[0] - t * dx, p[1] - a[1] - t * dy);
104
+ };
105
+
106
+ /**
107
+ * The gap between two shapes: 0 when they touch or overlap, else the least vertex-to-edge distance either way.
108
+ *
109
+ * @param {Geom} a One shape.
110
+ * @param {Geom} b The other.
111
+ * @returns {number} The gap.
112
+ */
113
+ const gapBetween = (a, b) => {
114
+ if (I(a, b).length) return 0; let best = Infinity;
115
+ const rings = (s) => polys(s).flat();
116
+ for (const ra of rings(a)) for (const rb of rings(b)) for (const [P, Q] of [[ra, rb], [rb, ra]]) for (const p of P) for (let i = 0; i < Q.length; i++) best = Math.min(best, segDist(p, Q[i], Q[(i + 1) % Q.length]));
117
+ return best;
118
+ };
119
+
120
+ /* ---------- rails ---------- */
121
+
122
+ /**
123
+ * The point t of the way from a to b.
124
+ *
125
+ * @param {readonly number[]} a From.
126
+ * @param {readonly number[]} b To.
127
+ * @param {number} t How far.
128
+ * @returns {Point} The point.
129
+ */
130
+ const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
131
+
132
+ /**
133
+ * A rail along a polyline, by arc length: s in 0…1 to a point.
134
+ *
135
+ * @param {readonly Point[]} pts The polyline.
136
+ * @returns {Rail} The rail.
137
+ */
138
+ export const rail = (pts) => {
139
+ const L = pts.slice(1).map((p, i) => Math.hypot(p[0] - pts[i][0], p[1] - pts[i][1])), tot = L.reduce((a, b) => a + b, 0);
140
+ return (s) => { let d = Math.max(0, Math.min(1, s)) * tot; for (let i = 0; i < L.length; i++) { if (d <= L[i] || i === L.length - 1) return lerp(pts[i], pts[i + 1], Math.min(1, d / L[i])); d -= L[i]; } };
141
+ };
142
+
143
+ /**
144
+ * The run of a closed outline from the vertex nearest p, forward, to the vertex nearest q.
145
+ *
146
+ * @param {readonly Point[]} ring The outline.
147
+ * @param {readonly number[]} p Where the run starts.
148
+ * @param {readonly number[]} q Where it ends.
149
+ * @returns {Point[]} The run.
150
+ */
151
+ export const outlineRun = (ring, p, q) => {
152
+ const near = (t) => ring.reduce((b, v, i) => (Math.hypot(v[0] - t[0], v[1] - t[1]) < Math.hypot(ring[b][0] - t[0], ring[b][1] - t[1]) ? i : b), 0);
153
+ const i = near(p), j = near(q), out = []; for (let k = i; ; k = (k + 1) % ring.length) { out.push(ring[k]); if (k === j) break; } return out;
154
+ };
155
+
156
+ /**
157
+ * The two rails of a band w wide along a centreline, offset by mitres.
158
+ *
159
+ * @param {readonly Point[]} pts The centreline.
160
+ * @param {number} w The band's width.
161
+ * @returns {[Rail, Rail]} The left rail (y down), then the right.
162
+ */
163
+ export const bandRails = (pts, w) => [rail(mitreOff(pts, w / 2)), rail(mitreOff(pts, -w / 2))];
164
+
165
+ /**
166
+ * The place s on a rail nearest a point (to 1/400).
167
+ *
168
+ * @param {Rail} f The rail.
169
+ * @param {readonly number[]} p The point.
170
+ * @returns {number} The place.
171
+ */
172
+ const closest = (f, p) => { let best = 0, d = Infinity; for (let q = 0; q <= 400; q++) { const v = f(q / 400), e = Math.hypot(v[0] - p[0], v[1] - p[1]); if (e < d) { d = e; best = q / 400; } } return best; };
173
+
174
+ /* ---------- the strip ---------- */
175
+
176
+ /**
177
+ * The shards of a band between two rails. n + 2 nodes alternate down the rails (outer first, at s = 0), each step `ratio`
178
+ * times the last, so the shards get smaller down the strip, and each inner node is jittered by up to `jit` of its step;
179
+ * the last two sit at s = 1 on both rails. Every three nodes in a row make a triangle, its rail sides following the rails'
180
+ * curves (`arcN` points), so the strip tiles the band; a triangle with no area (where the rails meet) is dropped.
181
+ *
182
+ * @param {Rail} inner The inner rail.
183
+ * @param {Rail} outer The outer rail.
184
+ * @param {{ n?: number, ratio?: number, jit?: number, rng?: () => number, arcN?: number }} [o] Shards (6), the step
185
+ * ratio (0.85), the jitter (0.3), the random source (Math.random) and the points along each rail side (80).
186
+ * @returns {Shard[]} The shards.
187
+ */
188
+ export const shardStrip = (inner, outer, { n = 6, ratio = 0.85, jit = 0.3, rng = Math.random, arcN = 80 } = {}) => {
189
+ const steps = [...Array(n)].map((_, i) => ratio ** i), tot = steps.reduce((a, b) => a + b, 0), p = [0];
190
+ steps.forEach((d, i) => p.push(i === n - 1 ? 1 : p.at(-1) + d / tot)); p.push(1);
191
+ for (let j = 1; j < n; j++) p[j] += (rng() - 0.5) * 2 * jit * (steps[j - 1] / tot) * 0.5;
192
+ const ns = p.map((s, j) => ({ rail: j % 2 ? inner : outer, s }));
193
+ const run = (rail, s0, s1) => [...Array(arcN + 1)].map((_, q) => rail(s0 + ((s1 - s0) * q) / arcN));
194
+ return [...Array(n)].map((_, i) => { const [a, b, c] = ns.slice(i, i + 3); return { r: [...run(a.rail, a.s, c.s), b.rail(b.s)], s: (a.s + b.s + c.s) / 3, base: i % 2 ? 'inner' : 'outer' }; })
195
+ .filter((q) => area(q.r) > 1);
196
+ };
197
+
198
+ /**
199
+ * A shard shrunk in by half a gap all round, each straight side's band run on 3g past its corners so an acute tip leaves
200
+ * no needle (a rail's short arc steps are not run on: on a hollow arc that would groove the shard), then opened by 3
201
+ * (soften) to clear any hairline left where an arc meets a side.
202
+ *
203
+ * @param {readonly Point[]} r The shard's ring.
204
+ * @param {number} g The gap.
205
+ * @returns {Ring | undefined} The inset ring (its largest piece), or nothing if it vanishes.
206
+ */
207
+ export const inset = (r, g) => polys(D(poly(r), ...r.flatMap((p, i) => {
208
+ const q = r[(i + 1) % r.length], L = Math.hypot(q[0] - p[0], q[1] - p[1]);
209
+ if (L < 1e-6) return []; const e = L > 30 ? (3 * g) / L : 0, u = [(q[0] - p[0]) * e, (q[1] - p[1]) * e];
210
+ return [bar([p[0] - u[0], p[1] - u[1]], [q[0] + u[0], q[1] + u[1]], g)];
211
+ })))
212
+ .map((pg) => polys(soften([pg[0]], 0, 3))[0]?.[0]).filter(Boolean).sort((a, b) => area(b) - area(a))[0];
213
+
214
+ /**
215
+ * The body: the form less the band between the rails, cut back by half a gap along the crack. The crack runs from the
216
+ * outer rail's start to the first inner node `i0`, down the inner rail, and back out to the outer rail's end (where the
217
+ * rails part there), run on 40 past both ends so it cuts cleanly through the outline; it is a mitred band g wide, so its
218
+ * edge stays sharp. Slivers of form under 400 in area are dropped.
219
+ *
220
+ * @param {Geom} shape The form.
221
+ * @param {Rail} inner The inner rail.
222
+ * @param {Rail} outer The outer rail.
223
+ * @param {readonly number[]} i0 The first inner node.
224
+ * @param {number} g The gap.
225
+ * @param {number} [k=60] Points along each rail run.
226
+ * @returns {Shape} The body.
227
+ */
228
+ export const cutBack = (shape, inner, outer, i0, g, k = 60) => {
229
+ const at = (f, s0, s1) => [...Array(k + 1)].map((_, q) => f(s0 + ((s1 - s0) * q) / k));
230
+ const o1 = outer(1), in1 = inner(1); let crack = [outer(0), ...at(inner, closest(inner, i0), 1)];
231
+ if (Math.hypot(o1[0] - in1[0], o1[1] - in1[1]) > 1) crack.push(o1);
232
+ crack = crack.filter((p, i) => !i || Math.hypot(p[0] - crack[i - 1][0], p[1] - crack[i - 1][1]) > 0.5);
233
+ const on = (a, b) => { const L = Math.hypot(b[0] - a[0], b[1] - a[1]); return [b[0] + ((b[0] - a[0]) / L) * 40, b[1] + ((b[1] - a[1]) / L) * 40]; };
234
+ const zone = poly([...crack, ...at(outer, 1, 0)]);
235
+ // the zone's outer side is a chord of a curved outline, so drop the hairline slivers of form it leaves outside the crack
236
+ return polys(D(shape, zone, mitred([on(crack[1], crack[0]), ...crack, on(crack.at(-2), crack.at(-1))], g))).filter((pg) => area(pg[0]) > 400);
237
+ };
238
+
239
+ /* ---------- the drift ---------- */
240
+
241
+ /**
242
+ * A shape shrunk by k about c, turned by deg and moved by (dx, dy).
243
+ *
244
+ * @param {Geom} s The shape.
245
+ * @param {readonly number[]} c The pivot.
246
+ * @param {number} k The shrink.
247
+ * @param {number} deg The turn, degrees.
248
+ * @param {readonly number[]} d The move.
249
+ * @returns {Geom} The shape, moved.
250
+ */
251
+ const xf = (s, c, k, deg, [dx, dy]) => {
252
+ const a = (deg * Math.PI) / 180, co = Math.cos(a), si = Math.sin(a);
253
+ return mapPts(s, ([x, y]) => { const u = (x - c[0]) * k, v = (y - c[1]) * k; return [c[0] + u * co - v * si + dx, c[1] + u * si + v * co + dy]; });
254
+ };
255
+
256
+ /**
257
+ * Lets the shards drift off the body, nearest first. `move(p)` gives each shard (its ring p.r, its place p.s, its centre
258
+ * p.c) its shrink, turn and move. A shard is pushed on along its move in steps until it sits at least g clear of the body
259
+ * and of every shard already placed; a shard under `min` across is dropped, so the strip thins out at its end.
260
+ *
261
+ * @param {Geom} body The body.
262
+ * @param {readonly Shard[]} shards The inset shards.
263
+ * @param {{ move: (p: Shard & { c: Point }) => ShardMove, g: number, step?: number, min: number }} o The move, the gap,
264
+ * the push step (4) and the least shard width.
265
+ * @returns {Geom[]} The placed shards.
266
+ */
267
+ export const drift = (body, shards, { move, g, step = 4, min }) => {
268
+ const ranked = shards.map((p) => ({ ...p, c: centroid(p.r) })).sort((a, b) => a.s - b.s), placed = [];
269
+ ranked.forEach((p) => {
270
+ const { k = 1, deg = 0, d = [0, 0] } = move(p), L = Math.hypot(...d) || 1, u = [d[0] / L, d[1] / L];
271
+ let s, extra = 0; for (let t = 0; t < 80; t++) {
272
+ s = xf(poly(p.r), p.c, k, deg, [d[0] + u[0] * extra, d[1] + u[1] * extra]);
273
+ if (gapBetween(s, body) >= g && placed.every((q) => gapBetween(s, q) >= g)) break; extra += step;
274
+ }
275
+ if (minWidth(s[0]) >= min) placed.push(s);
276
+ });
277
+ return placed;
278
+ };
279
+
280
+ /**
281
+ * A drift that spreads the shards away from a point inside the form (an exploded view of the broken edge): each moves out
282
+ * by `grow` [start, growth along the strip] times its distance from `from`, shrunk by `shrink` × its place, and turned up
283
+ * to ± `turn`° (seeded through Math.random, more further on).
284
+ *
285
+ * @param {readonly number[]} from The point they spread from.
286
+ * @param {readonly number[]} grow [start, growth along the strip].
287
+ * @param {number} [shrink=0.35] The shrink at the strip's end.
288
+ * @param {number} [turn=8] The most turn, degrees.
289
+ * @returns {(p: Shard & { c: Point }) => ShardMove} The move, for drift().
290
+ */
291
+ export const spread = (from, grow, shrink = 0.35, turn = 8) => (p) => {
292
+ const t = p.s, e = grow[0] + grow[1] * t;
293
+ return { k: 1 - shrink * t, deg: (Math.random() - 0.5) * 2 * turn * (0.3 + t), d: [(p.c[0] - from[0]) * e, (p.c[1] - from[1]) * e] };
294
+ };
295
+
296
+ /* ---------- the mark ---------- */
297
+
298
+ /**
299
+ * A form breaking on one side: the band between `inner` and `outer` splits into a strip of n shards (only those based on
300
+ * the outer rail are kept, unless `all`), the body is cut back, the shards are inset and drift by `move`. `min` scales
301
+ * with the gap, so no shard is under 28 across once the mark is fitted. Seeded through Math.random.
302
+ *
303
+ * @param {Geom} shape The form.
304
+ * @param {Rail} inner The inner rail.
305
+ * @param {Rail} outer The outer rail.
306
+ * @param {{ n?: number, ratio?: number, jit?: number, all?: boolean, move: (p: Shard & { c: Point }) => ShardMove, g: number, min?: number }} o
307
+ * Shards (6), the step ratio (0.8), the jitter (0.4), whether to keep every shard (true), the move, the gap and the least
308
+ * shard width (28/22 of g).
309
+ * @returns {Shape} The broken form.
310
+ */
311
+ export const breakOff = (shape, inner, outer, { n = 6, ratio = 0.8, jit = 0.4, all = true, move, g, min = (28 / 22) * g }) => {
312
+ const sh = shardStrip(inner, outer, { n, ratio, jit, rng: Math.random }), body = cutBack(shape, inner, outer, sh[0].r.at(-1), g);
313
+ const kept = (all ? sh : sh.filter((p) => p.base === 'outer')).map((p) => ({ ...p, r: inset(p.r, g) })).filter((p) => p.r && area(p.r) > 1);
314
+ return U(body, ...drift(body, kept, { move, g, min }));
315
+ };
316
+
317
+ /**
318
+ * The house recipe: breakOff() with 6 shards, each step 0.8 of the last, all kept, spread out from `from` (the canvas
319
+ * centre) by 0.05 + 0.5 × their place along the strip, shrunk up to 35%, turned up to ±8°; any of it overridden by `o`.
320
+ *
321
+ * @param {Geom} shape The form.
322
+ * @param {Rail} inner The inner rail.
323
+ * @param {Rail} outer The outer rail.
324
+ * @param {{ from?: readonly number[], grow?: readonly number[], shrink?: number, turn?: number, n?: number, ratio?: number, jit?: number, all?: boolean, min?: number }} [o]
325
+ * Overrides.
326
+ * @returns {(g: number) => Shape} The build of the gap.
327
+ */
328
+ export const houseBreak = (shape, inner, outer, o = {}) => (g) => {
329
+ const { from = [256, 256], grow = [0.05, 0.5], shrink = 0.35, turn = 8, ...rest } = o;
330
+ return breakOff(shape, inner, outer, { n: 6, ratio: 0.8, jit: 0.4, all: true, move: spread(from, grow, shrink, turn), ...rest, g });
331
+ };
332
+
333
+ /**
334
+ * The point k of the way from p to q.
335
+ *
336
+ * @param {readonly number[]} p From.
337
+ * @param {readonly number[]} q To.
338
+ * @param {number} k How far.
339
+ * @returns {Point} The point.
340
+ */
341
+ export const lerpAt = (p, q, k) => [p[0] + (q[0] - p[0]) * k, p[1] + (q[1] - p[1]) * k];
@@ -0,0 +1,35 @@
1
+ import type { Shape } from '../geometry/types.js';
2
+
3
+ /** One slab of a slat row: its turn (the sine, −1 to 1) and, optionally, its own width, height and centre shift. */
4
+ export interface Slat {
5
+ tau: number;
6
+ W?: number;
7
+ H?: number;
8
+ dy?: number;
9
+ }
10
+ /** A slat row's drawing: full width and height, perspective, gap, narrowest slab, centre and alignment. */
11
+ export interface SlatRowOptions {
12
+ W: number;
13
+ H: number;
14
+ k: number;
15
+ g: number;
16
+ wMin: number;
17
+ cx?: number;
18
+ cy?: number;
19
+ align?: 'centre' | 'base' | 'top';
20
+ }
21
+ /** A slab as its two edges, left then right: [x, top, bottom] each. */
22
+ export type SlatEdges = [number, number, number][];
23
+
24
+ /** The projected width of a slab of full width W turned by τ, never under wMin. */
25
+ export function turnWidth(W: number, tau: number, wMin: number): number;
26
+ /** A row of turning slabs side by side, every gap exactly g, centred on cx. */
27
+ export function slatRow(slabs: readonly Slat[], o: SlatRowOptions): SlatEdges[];
28
+ /** A slat row's slabs as one shape. */
29
+ export function quads(row: readonly (readonly (readonly number[])[])[]): Shape;
30
+ /** A slat row stood on its side as one shape ((x, y) becomes (y, x)). */
31
+ export function quadsT(row: readonly (readonly (readonly number[])[])[]): Shape;
32
+ /** n turns easing from a to b (sine ease). */
33
+ export function ease(n: number, a: number, b: number): number[];
34
+ /** n turns spaced evenly from a to b. */
35
+ export function lerpN(n: number, a: number, b: number): number[];