@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.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -0
- package/package.json +47 -4
- package/src/font/glyphs.d.ts +31 -0
- package/src/font/glyphs.js +228 -0
- package/src/font/index.d.ts +10 -0
- package/src/font/index.js +10 -0
- package/src/font/measure.d.ts +16 -0
- package/src/font/measure.js +201 -0
- package/src/font/moves.d.ts +42 -0
- package/src/font/moves.js +263 -0
- package/src/forms/discs.d.ts +20 -0
- package/src/forms/discs.js +114 -0
- package/src/forms/emblems.d.ts +22 -0
- package/src/forms/emblems.js +109 -0
- package/src/forms/flow.d.ts +21 -0
- package/src/forms/flow.js +80 -0
- package/src/forms/folds.d.ts +18 -0
- package/src/forms/folds.js +147 -0
- package/src/forms/geo.d.ts +46 -0
- package/src/forms/geo.js +233 -0
- package/src/forms/hooks.d.ts +40 -0
- package/src/forms/hooks.js +204 -0
- package/src/forms/index.d.ts +22 -0
- package/src/forms/index.js +24 -0
- package/src/forms/jog.d.ts +33 -0
- package/src/forms/jog.js +162 -0
- package/src/forms/lines.d.ts +32 -0
- package/src/forms/lines.js +233 -0
- package/src/forms/ref.d.ts +41 -0
- package/src/forms/ref.js +246 -0
- package/src/forms/shatter.d.ts +40 -0
- package/src/forms/shatter.js +341 -0
- package/src/forms/slats.d.ts +35 -0
- package/src/forms/slats.js +78 -0
- package/src/forms/swash.d.ts +23 -0
- package/src/forms/swash.js +74 -0
- package/src/forms/tech.d.ts +40 -0
- package/src/forms/tech.js +293 -0
- package/src/forms/tracks.d.ts +36 -0
- package/src/forms/tracks.js +121 -0
- package/src/index.js +2 -1
- package/src/iso/index.d.ts +5 -0
- package/src/iso/index.js +13 -0
- package/src/iso/objects.d.ts +168 -0
- package/src/iso/objects.js +1042 -0
- package/src/iso/planes.d.ts +27 -0
- package/src/iso/planes.js +138 -0
- package/src/iso/voxels.d.ts +43 -0
- package/src/iso/voxels.js +255 -0
- package/src/letters/blackletter.d.ts +31 -0
- package/src/letters/blackletter.js +199 -0
- package/src/letters/dotstroke.d.ts +16 -0
- package/src/letters/dotstroke.js +77 -0
- package/src/letters/echo.d.ts +42 -0
- package/src/letters/echo.js +246 -0
- package/src/letters/frame.d.ts +6 -0
- package/src/letters/frame.js +14 -0
- package/src/letters/index.d.ts +16 -0
- package/src/letters/index.js +18 -0
- package/src/letters/inktrap.d.ts +66 -0
- package/src/letters/inktrap.js +244 -0
- package/src/letters/mono.d.ts +145 -0
- package/src/letters/mono.js +684 -0
- package/src/letters/pillar.d.ts +31 -0
- package/src/letters/pillar.js +257 -0
- package/src/letters/serif.d.ts +109 -0
- package/src/letters/serif.js +666 -0
- package/src/letters/stair.d.ts +8 -0
- package/src/letters/stair.js +43 -0
- package/src/letters/stem.d.ts +29 -0
- package/src/letters/stem.js +205 -0
- package/src/objects/animals.d.ts +105 -0
- package/src/objects/animals.js +469 -0
- package/src/objects/bio.d.ts +14 -0
- package/src/objects/bio.js +76 -0
- package/src/objects/devglyphs.d.ts +34 -0
- package/src/objects/devglyphs.js +226 -0
- package/src/objects/devices.d.ts +31 -0
- package/src/objects/devices.js +114 -0
- package/src/objects/faces.d.ts +94 -0
- package/src/objects/faces.js +344 -0
- package/src/objects/food.d.ts +12 -0
- package/src/objects/food.js +74 -0
- package/src/objects/index.d.ts +13 -0
- package/src/objects/index.js +18 -0
- package/src/objects/rosettes.d.ts +14 -0
- package/src/objects/rosettes.js +64 -0
- package/src/objects/sport.d.ts +8 -0
- package/src/objects/sport.js +37 -0
- package/src/objects/weapons.d.ts +31 -0
- package/src/objects/weapons.js +179 -0
- package/src/pixels/dither.d.ts +78 -0
- package/src/pixels/dither.js +399 -0
- package/src/pixels/emote.d.ts +6 -0
- package/src/pixels/emote.js +76 -0
- package/src/pixels/index.d.ts +7 -0
- package/src/pixels/index.js +8 -0
- package/src/sdf/index.d.ts +2 -0
- package/src/sdf/index.js +2 -0
- package/src/sdf/y2k.d.ts +12 -0
- package/src/sdf/y2k.js +80 -0
- package/src/util/vec.d.ts +14 -0
- package/src/util/vec.js +61 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
import type { Face, Placer, SourceGlyph, GlyphSource } from '../letters/serif.js';
|
|
3
|
+
|
|
4
|
+
/** A move on a blackletter letter: the letter and the gap to the new letter. */
|
|
5
|
+
export type LetterMove = (s: Shape, g: number) => Shape;
|
|
6
|
+
|
|
7
|
+
/** Split (A): the gap just under the crossbar. */
|
|
8
|
+
export function underCrossbar(m: Face, w: number, g: number, L: SourceGlyph): Polygon;
|
|
9
|
+
/** Split (T): the gap right under the top bar. */
|
|
10
|
+
export function underTopBar(at?: number): Placer<Polygon>;
|
|
11
|
+
/** Split (P): the gap across the stem under the bowl. */
|
|
12
|
+
export function underBowl(below?: number): Placer<Polygon>;
|
|
13
|
+
/** Dot (L): a ball in the L's corner, clear of the foot. */
|
|
14
|
+
export function cornerDot(o?: { along?: number; from?: number; clear?: number; r?: number }): Placer<number[]>;
|
|
15
|
+
/** Stop (H): a full stop a gap after the letter. */
|
|
16
|
+
export function fullStop(k?: number): Placer<number[]>;
|
|
17
|
+
/** Star (J): a five-point star before the stem. */
|
|
18
|
+
export function starBefore(o?: { k?: number; y?: number }): Placer<number[]>;
|
|
19
|
+
/** Split (Y): the gap just under the fork. */
|
|
20
|
+
export function underFork(m: Face, w: number, g: number, L: SourceGlyph): Polygon;
|
|
21
|
+
/** Split (R): the gap across the stem only, under the bowl's foot. */
|
|
22
|
+
export function underBowlR(m: Face, w: number, g: number, L: SourceGlyph): Polygon;
|
|
23
|
+
/** The serif inline, its channel opened where a stroke tapers. */
|
|
24
|
+
export function inlineOpen(src: GlyphSource, H?: number, open?: number): (g: number) => Shape;
|
|
25
|
+
/** The inline with a straight channel, for straight-stroked letters. */
|
|
26
|
+
export function inlineStraight(src: GlyphSource, H?: number, open?: number): (g: number) => Shape;
|
|
27
|
+
/** A parted lozenge on a font's middle arm. */
|
|
28
|
+
export function armLozenge(cap: number, y0: number, y1: number, k?: number): LetterMove;
|
|
29
|
+
/** A split stem down from the head or up from the foot. */
|
|
30
|
+
export function stemSplit(cap: number, end: 'head' | 'foot', depth: number): LetterMove;
|
|
31
|
+
/** A lozenge set across the H's bar. */
|
|
32
|
+
export function barLozenge(cap: number, o?: { k?: number; at?: number }): LetterMove;
|
|
33
|
+
/** armLozenge mirrored, for a stroke ending on the left. */
|
|
34
|
+
export function leftLozenge(cap: number, y0: number, y1: number, k?: number): LetterMove;
|
|
35
|
+
/** A short middle tongue taken off whole, a lozenge a gap clear of the stem. */
|
|
36
|
+
export function tongueLozenge(cap: number, y0: number, y1: number, z?: number): LetterMove;
|
|
37
|
+
/** A bar's right end run into a level parted lozenge. */
|
|
38
|
+
export function barEnd(cap: number, o?: { back?: number; k?: number }): LetterMove;
|
|
39
|
+
/** barEnd at both ends of the bar. */
|
|
40
|
+
export function barEnds(cap: number, o?: { back?: number; k?: number }): LetterMove;
|
|
41
|
+
/** A parted lozenge at the crossing or the fork. */
|
|
42
|
+
export function crossLozenge(cap: number, y0: number, y1: number, k?: number): LetterMove;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moves placed on a font's letter: each measures the font's own letter (its bar, stem, bowl or arm), never a house
|
|
3
|
+
* face's fractions, so every font edition places its source mark's move the same way. The serif ones are the `cut` and
|
|
4
|
+
* `at` arguments of /letters' split() and dotted(): `(m, w, g, L)`, with L the glyph source's { s, w, m }; the blackletter
|
|
5
|
+
* ones are `(s, g) => shape` on a letter at cap `cap`. From the Logomarks tools (fontparts.mjs: batch 242's placements,
|
|
6
|
+
* 243's blackletter ones, 292's and 298's inlines and cuts, 312's tongue and 334's bar ends).
|
|
7
|
+
*/
|
|
8
|
+
import { U, D, I, move, bbox, rect, circle, flipX, grow, poly } from '../geometry/index.js';
|
|
9
|
+
import { parted, lozenge, splitHead, splitFoot } from '../letters/blackletter.js';
|
|
10
|
+
import { runsH, runsV, armEnd } from './measure.js';
|
|
11
|
+
|
|
12
|
+
/* ---------- serif moves: split, dot and star ---------- */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split (A): the gap across the letter just UNDER its crossbar (a gap through a 29 hairline leaves slivers), the bar
|
|
16
|
+
* found down the counter's middle at 0.8 of the cap.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../letters/serif.js').Face} m The glyph's face.
|
|
19
|
+
* @param {number} w Its width.
|
|
20
|
+
* @param {number} g The gap.
|
|
21
|
+
* @param {import('../letters/serif.js').SourceGlyph} L The glyph.
|
|
22
|
+
* @returns {import('../geometry/types.js').Polygon} The strip to cut.
|
|
23
|
+
*/
|
|
24
|
+
export const underCrossbar = (m, w, g, L) => {
|
|
25
|
+
const [l, r] = runsH(L.s, m.H * 0.8), x = (l[1] + r[0]) / 2;
|
|
26
|
+
const bar = runsV(L.s, x).find(([a]) => a > m.H * 0.3); return rect(-10, bar[1], w + 20, g);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Split (T): the gap right under the top bar, measured `at` of the width in.
|
|
31
|
+
*
|
|
32
|
+
* @param {number} [at=0.3] Where across the bar is measured.
|
|
33
|
+
* @returns {import('../letters/serif.js').Placer<import('../geometry/types.js').Polygon>} The cut.
|
|
34
|
+
*/
|
|
35
|
+
export const underTopBar = (at = 0.3) => (m, w, g, L) => rect(-10, runsV(L.s, w * at)[0][1], w + 20, g);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Split (P): the gap across the stem `below` under the bowl (the bowl's foot found halfway out from the stem).
|
|
39
|
+
*
|
|
40
|
+
* @param {number} [below=6] How far under the bowl's foot.
|
|
41
|
+
* @returns {import('../letters/serif.js').Placer<import('../geometry/types.js').Polygon>} The cut.
|
|
42
|
+
*/
|
|
43
|
+
export const underBowl = (below = 6) => (m, w, g, L) => {
|
|
44
|
+
const x = runsH(L.s, m.H * 0.8)[0][1], y = runsV(L.s, (x + w) / 2).at(-1)[1];
|
|
45
|
+
return rect(-10, y + below, w + 20, g);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Dot (L): a ball of half the stem in the L's corner, `along` the way from the stem to the arm's end, risen from `from`
|
|
50
|
+
* of the cap until it sits `clear` gaps off the foot (a font's foot curves up into a beak).
|
|
51
|
+
*
|
|
52
|
+
* @param {{ along?: number, from?: number, clear?: number, r?: number }} [o] Where across (0.42), where it starts (0.62
|
|
53
|
+
* of the cap), its clearance in gaps (1.5) and its radius as a fraction of the stem (0.5).
|
|
54
|
+
* @returns {import('../letters/serif.js').Placer<number[]>} The placement: [cx, cy, r].
|
|
55
|
+
*/
|
|
56
|
+
export const cornerDot = ({ along = 0.42, from = 0.62, clear = 1.5, r: rk = 0.5 } = {}) => (m, w, g, L) => {
|
|
57
|
+
const x = runsH(L.s, m.H * 0.5)[0][1], cx = x + (w - x) * along, r = m.S * rk; let cy = m.H * from;
|
|
58
|
+
while (I(circle(cx, cy, r + g * clear, 64), L.s).length && cy > m.H * 0.4) cy -= 2;
|
|
59
|
+
return [cx, cy, r];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stop (H): a full stop (radius `k` of the stem) a gap after the letter, sitting on the baseline.
|
|
64
|
+
*
|
|
65
|
+
* @param {number} [k=0.52] The radius, as a fraction of the stem.
|
|
66
|
+
* @returns {import('../letters/serif.js').Placer<number[]>} The placement: [cx, cy, r].
|
|
67
|
+
*/
|
|
68
|
+
export const fullStop = (k = 0.52) => (m, w, g) => [w + g + m.S * k, m.H - m.S * k, m.S * k];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Star (J): a five-point star (radius `k` of the cap) a radius before the stem at 0.3 of the cap, its centre at `y` of
|
|
72
|
+
* the cap.
|
|
73
|
+
*
|
|
74
|
+
* @param {{ k?: number, y?: number }} [o] The radius (0.13) and the height (0.42), as fractions of the cap.
|
|
75
|
+
* @returns {import('../letters/serif.js').Placer<number[]>} The placement: [cx, cy, r].
|
|
76
|
+
*/
|
|
77
|
+
export const starBefore = ({ k = 0.13, y = 0.42 } = {}) => (m, w, g, L) => {
|
|
78
|
+
const r = m.H * k, x = runsH(L.s, m.H * 0.3)[0][0];
|
|
79
|
+
return [x - m.H * k - r * 0.95, m.H * y, r];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Split (Y): the gap across the letter just under the fork, where the two arms have run into one stem.
|
|
84
|
+
*
|
|
85
|
+
* @param {import('../letters/serif.js').Face} m The glyph's face.
|
|
86
|
+
* @param {number} w Its width.
|
|
87
|
+
* @param {number} g The gap.
|
|
88
|
+
* @param {import('../letters/serif.js').SourceGlyph} L The glyph.
|
|
89
|
+
* @returns {import('../geometry/types.js').Polygon} The strip to cut.
|
|
90
|
+
*/
|
|
91
|
+
export const underFork = (m, w, g, L) => { let y = m.H * 0.25; while (runsH(L.s, y).length > 1 && y < m.H * 0.8) y += 1; return rect(-10, y, w + 20, g); };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Split (R): the gap across the stem just under the bowl's foot, cutting only the stem (the leg runs on whole), so the
|
|
95
|
+
* bowl and the leg float as one piece beside the stem's foot.
|
|
96
|
+
*
|
|
97
|
+
* @param {import('../letters/serif.js').Face} m The glyph's face.
|
|
98
|
+
* @param {number} w Its width.
|
|
99
|
+
* @param {number} g The gap.
|
|
100
|
+
* @param {import('../letters/serif.js').SourceGlyph} L The glyph.
|
|
101
|
+
* @returns {import('../geometry/types.js').Polygon} The strip to cut.
|
|
102
|
+
*/
|
|
103
|
+
export const underBowlR = (m, w, g, L) => {
|
|
104
|
+
const x = runsH(L.s, m.H * 0.25)[0][1] + 4, y = runsV(L.s, x + 30).find(([a]) => a > m.H * 0.3)[1];
|
|
105
|
+
return rect(-10, y + 4, runsH(L.s, y + 4 + g / 2)[0][1] + 10, g);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/* ---------- inlines ---------- */
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The serif inline with its channel opened: the shrunk letter less any part under `open` wide, so where a stroke tapers
|
|
112
|
+
* (K's and R's legs, a bowl's side) the channel stops rather than running on as a slit under 10. Centred.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('../letters/serif.js').GlyphSource} src The glyph source.
|
|
115
|
+
* @param {number} [H=380] The cap height.
|
|
116
|
+
* @param {number} [open=6] The narrowest channel kept.
|
|
117
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
118
|
+
*/
|
|
119
|
+
export const inlineOpen = (src, H = 380, open = 6) => (g) => {
|
|
120
|
+
const { s, w, m } = src(H, { g }), inner = grow(grow(s, -(m.S - g) / 2), -open);
|
|
121
|
+
return move(D(s, grow(inner, open)), 256 - w / 2, 256 - H / 2);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The inline with a STRAIGHT channel, for letters of straight strokes (E, F, M, T): each piece of the shrunk letter
|
|
126
|
+
* clipped to the strip through its own runs at 0.3 and 0.7 of its height, so the channel keeps its width past a junction
|
|
127
|
+
* instead of bulging into the arm. The strip follows a slanted stroke as well as an upright. Centred.
|
|
128
|
+
*
|
|
129
|
+
* @param {import('../letters/serif.js').GlyphSource} src The glyph source.
|
|
130
|
+
* @param {number} [H=380] The cap height.
|
|
131
|
+
* @param {number} [open=6] The narrowest channel kept.
|
|
132
|
+
* @returns {(g: number) => import('../geometry/types.js').Shape} The build.
|
|
133
|
+
*/
|
|
134
|
+
export const inlineStraight = (src, H = 380, open = 6) => (g) => {
|
|
135
|
+
const { s, w, m } = src(H, { g }), inner = grow(grow(grow(s, -(m.S - g) / 2), -open), open);
|
|
136
|
+
const ch = U(...inner.map((pg) => {
|
|
137
|
+
const [, t, , b] = bbox([pg]), at = (f) => runsH([pg], t + (b - t) * f)[0], y1 = t + (b - t) * 0.3, y2 = t + (b - t) * 0.7;
|
|
138
|
+
const [a1, b1] = at(0.3), [a2, b2] = at(0.7), X = (x1, x2, y) => x1 + ((x2 - x1) * (y - y1)) / (y2 - y1);
|
|
139
|
+
return I([pg], poly([[X(a1, a2, -500), -500], [X(b1, b2, -500), -500], [X(b1, b2, 1500), 1500], [X(a1, a2, 1500), 1500]]));
|
|
140
|
+
}));
|
|
141
|
+
return move(D(s, ch), 256 - w / 2, 256 - H / 2);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/* ---------- blackletter moves ---------- */
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The blackletter's parted lozenge on a font's middle arm (E, F): its tip found between y0 and y1 of the cap, a lozenge
|
|
148
|
+
* `k` times the arm's depth (110 at least) parted off by the gap.
|
|
149
|
+
*
|
|
150
|
+
* @param {number} cap The cap height.
|
|
151
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
152
|
+
* @param {number} y1 Its bottom.
|
|
153
|
+
* @param {number} [k=1.7] The lozenge's size, in arm depths.
|
|
154
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
155
|
+
*/
|
|
156
|
+
export const armLozenge = (cap, y0, y1, k = 1.7) => (s, g) => {
|
|
157
|
+
const a = armEnd(s, cap * y0, cap * y1), z = Math.max(110, a.th * k);
|
|
158
|
+
return parted(s, lozenge(a.x + z * 0.13, a.yc, z), g);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The blackletter's split stem on a font's letter: a slit g wide down the left stem from above the cap to `depth` of the
|
|
163
|
+
* cap ('head', B D P) or up from below the base to it ('foot', R). The slit is centred on the median of the stem's
|
|
164
|
+
* middles along the slit's length, spike tips (runs under half the median width) left out: a flag head juts left at the
|
|
165
|
+
* top and the bowl joins on the right, so one middle measured there puts the slit on the joint and leaves a sliver.
|
|
166
|
+
*
|
|
167
|
+
* @param {number} cap The cap height.
|
|
168
|
+
* @param {'head' | 'foot'} end Which end the slit runs from.
|
|
169
|
+
* @param {number} depth How far, as a fraction of the cap.
|
|
170
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
171
|
+
*/
|
|
172
|
+
export const stemSplit = (cap, end, depth) => (s, g) => {
|
|
173
|
+
const [y0, y1] = end === 'head' ? [0, cap * depth] : [cap * depth, cap];
|
|
174
|
+
const rs = [...Array(19)].map((_, i) => runsH(s, y0 + ((y1 - y0) * (i + 1)) / 20)[0]).filter(Boolean), med = (a) => a.sort((p, q) => p - q)[a.length >> 1];
|
|
175
|
+
const wm = med(rs.map(([a, b]) => b - a)), x = med(rs.filter(([a, b]) => b - a >= wm / 2).map(([a, b]) => (a + b) / 2));
|
|
176
|
+
return D(s, (end === 'head' ? splitHead : splitFoot)(x, cap * depth, g));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The blackletter's H: a lozenge set across the bar, `k` times the counter's width from corner to corner, on the bar's
|
|
181
|
+
* middle (the counter and bar measured across and down the letter's middle).
|
|
182
|
+
*
|
|
183
|
+
* @param {number} cap The cap height.
|
|
184
|
+
* @param {{ k?: number, at?: number }} [o] The lozenge's size in counter widths (1.7), and its height when no bar is
|
|
185
|
+
* found (0.5 of the cap).
|
|
186
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
187
|
+
*/
|
|
188
|
+
export const barLozenge = (cap, { k = 1.7, at = 0.5 } = {}) => (s, g) => {
|
|
189
|
+
const [x0, , x1] = bbox(s), xm = (x0 + x1) / 2;
|
|
190
|
+
const across = runsH(s, cap * 0.25), cw = across[1][0] - across[0][1], bar = runsV(s, xm).find(([a, b]) => (a + b) / 2 > cap * 0.25 && (a + b) / 2 < cap * 0.75);
|
|
191
|
+
return U(s, lozenge(xm, bar ? (bar[0] + bar[1]) / 2 : cap * at, cw * k));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* armLozenge mirrored: a stroke ending on the LEFT (a J's foot) runs into a parted lozenge, its tip found between y0
|
|
196
|
+
* and y1.
|
|
197
|
+
*
|
|
198
|
+
* @param {number} cap The cap height.
|
|
199
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
200
|
+
* @param {number} y1 Its bottom.
|
|
201
|
+
* @param {number} [k] The lozenge's size, in arm depths (1.7).
|
|
202
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
203
|
+
*/
|
|
204
|
+
export const leftLozenge = (cap, y0, y1, k) => (s, g) => flipX(armLozenge(cap, y0, y1, k)(flipX(s), g));
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The middle arm as a lozenge a gap off the stem (E, F), for a face whose middle arm is a short tongue (armLozenge would
|
|
208
|
+
* leave a stub on the stem): the tongue goes whole (the ink right of the stem between y0 and y1 of the cap, the stem's
|
|
209
|
+
* right edge read at 0.3 and 0.7 of the cap) and a lozenge `z` across stands a gap clear of the stem, centred on the
|
|
210
|
+
* tongue.
|
|
211
|
+
*
|
|
212
|
+
* @param {number} cap The cap height.
|
|
213
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
214
|
+
* @param {number} y1 Its bottom.
|
|
215
|
+
* @param {number} [z=120] The lozenge, corner to corner.
|
|
216
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
217
|
+
*/
|
|
218
|
+
export const tongueLozenge = (cap, y0, y1, z = 120) => (s, g) => {
|
|
219
|
+
const sr = Math.max(runsH(s, cap * 0.3)[0][1], runsH(s, cap * 0.7)[0][1]);
|
|
220
|
+
let a = Infinity, b = -Infinity; for (let y = cap * y0; y <= cap * y1; y += 1) { const r = runsH(s, y)[0]; if (r[1] > sr + 2) { a = Math.min(a, y); b = Math.max(b, y); } }
|
|
221
|
+
const body = D(s, rect(sr, a - 1, 400, b - a + 2)), yc = (a + b) / 2;
|
|
222
|
+
return U(body, lozenge(sr + g + z / 2, yc, z));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A bar's right end run into a lozenge parted off by the gap and set LEVEL on the bar (the geometric T's lozenge
|
|
227
|
+
* terminals), for a face whose bar curls up at its tips (armLozenge would perch it above the curl): the bar's depth and
|
|
228
|
+
* middle are read `back` in from the tip (found in the top 0.2 of the cap), the lozenge is `k` times that depth (110 at
|
|
229
|
+
* least) and centred on the tip itself, so the moat takes the curl off whole.
|
|
230
|
+
*
|
|
231
|
+
* @param {number} cap The cap height.
|
|
232
|
+
* @param {{ back?: number, k?: number }} [o] How far in the bar is read (60) and the lozenge's size in bar depths (1.5).
|
|
233
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
234
|
+
*/
|
|
235
|
+
export const barEnd = (cap, { back = 60, k = 1.5 } = {}) => (s, g) => {
|
|
236
|
+
const a = armEnd(s, 0, cap * 0.2), r = runsV(s, a.x - back).find(([y0]) => y0 < cap * 0.3);
|
|
237
|
+
const z = Math.max(110, (r[1] - r[0]) * k); return parted(s, lozenge(a.x, (r[0] + r[1]) / 2, z), g);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* barEnd at both ends of the bar: the left end is the right end of the letter mirrored.
|
|
242
|
+
*
|
|
243
|
+
* @param {number} cap The cap height.
|
|
244
|
+
* @param {{ back?: number, k?: number }} [o] As barEnd().
|
|
245
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
246
|
+
*/
|
|
247
|
+
export const barEnds = (cap, o) => (s, g) => { const f = barEnd(cap, o); return flipX(f(flipX(f(s, g)), g)); };
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The blackletter's parted lozenge `k` times the stroke at the crossing (X) or the fork (Y): placed on the first row
|
|
251
|
+
* between y0 and y1 of the cap where the ink is one run, narrowest.
|
|
252
|
+
*
|
|
253
|
+
* @param {number} cap The cap height.
|
|
254
|
+
* @param {number} y0 The band's top, as a fraction of the cap.
|
|
255
|
+
* @param {number} y1 Its bottom.
|
|
256
|
+
* @param {number} [k=1.6] The lozenge's size, in stroke widths.
|
|
257
|
+
* @returns {(s: import('../geometry/types.js').Shape, g: number) => import('../geometry/types.js').Shape} The move.
|
|
258
|
+
*/
|
|
259
|
+
export const crossLozenge = (cap, y0, y1, k = 1.6) => (s, g) => {
|
|
260
|
+
let best = null;
|
|
261
|
+
for (let y = cap * y0; y <= cap * y1; y += 1) { const r = runsH(s, y); if (r.length === 1 && (!best || r[0][1] - r[0][0] < best.w)) best = { y, x: (r[0][0] + r[0][1]) / 2, w: r[0][1] - r[0][0] }; }
|
|
262
|
+
return parted(s, lozenge(best.x, best.y, best.w * k), g);
|
|
263
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The disc's radius (224). */
|
|
4
|
+
export const DISC_R: number;
|
|
5
|
+
/** The disc, moved by (dx, dy). */
|
|
6
|
+
export function disc(dx?: number, dy?: number, r?: number): Polygon;
|
|
7
|
+
/** A curve across the disc: f(u) → [u, v] in a frame turned by deg. */
|
|
8
|
+
export function across(f: (u: number) => readonly number[], deg?: number, span?: number, n?: number): Point[];
|
|
9
|
+
/** The half plane on one side (s = ±1) of a curve that runs right across the disc. */
|
|
10
|
+
export function discSide(L: readonly Point[], s: number): Polygon;
|
|
11
|
+
/** The disc parted along L by g, each half's rim moved by sa / sb. */
|
|
12
|
+
export function split(L: readonly Point[], g: number, sa?: readonly number[], sb?: readonly number[]): Shape;
|
|
13
|
+
/** A band b wide along L, parted from both halves by g, riding on a disc moved by sm. */
|
|
14
|
+
export function bandCut(L: readonly Point[], b: number | ((i: number, n: number) => number), g: number, sm?: readonly number[], sa?: readonly number[], sb?: readonly number[]): Shape;
|
|
15
|
+
/** A point at radius r and angle deg from the disc's centre. */
|
|
16
|
+
export function discAt(r: number, deg: number): Point;
|
|
17
|
+
/** A shape turned by deg about p. */
|
|
18
|
+
export function turn(s: Geom, deg: number, p: readonly number[]): Geom;
|
|
19
|
+
/** The two sides of a circular cut through the disc, parted by g, the inner one turned th° about P. */
|
|
20
|
+
export function arcSplit(P: readonly number[], Ra: number, g: number, th: number): Shape;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bold discs: one solid disc of radius DISC_R, centred, and the cuts that keep its gap exact when a piece moves. A
|
|
3
|
+
* moving piece is I(region, moved disc): the region's edge (the cut) stays put and only the rim moves, so the gap along
|
|
4
|
+
* the cut is still g. From the Logomarks tools (discparts.mjs, batch 201, "Bold · Discs", and its editions in batch 220).
|
|
5
|
+
*/
|
|
6
|
+
import { C, D, I, U, circle, poly, rot, sample, 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 disc's radius. */
|
|
14
|
+
export const DISC_R = 224;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The disc (720 points), moved by (dx, dy).
|
|
18
|
+
*
|
|
19
|
+
* @param {number} [dx=0] Moved right.
|
|
20
|
+
* @param {number} [dy=0] Moved down.
|
|
21
|
+
* @param {number} [r=DISC_R] Its radius.
|
|
22
|
+
* @returns {Polygon} The disc.
|
|
23
|
+
*/
|
|
24
|
+
export const disc = (dx = 0, dy = 0, r = DISC_R) => circle(C + dx, C + dy, r, 720);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A curve across the disc: `f(u)` gives [u, v] in a frame turned by `deg` about the centre, u running from −span·R to
|
|
28
|
+
* span·R.
|
|
29
|
+
*
|
|
30
|
+
* @param {(u: number) => readonly number[]} f The curve in its own frame.
|
|
31
|
+
* @param {number} [deg=0] The frame's turn.
|
|
32
|
+
* @param {number} [span=1.5] How far either side it runs, in radii.
|
|
33
|
+
* @param {number} [n=300] Points along it.
|
|
34
|
+
* @returns {Point[]} The curve on the canvas.
|
|
35
|
+
*/
|
|
36
|
+
export const across = (f, deg = 0, span = 1.5, n = 300) => {
|
|
37
|
+
const a = (deg * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a);
|
|
38
|
+
return sample(f, -span * DISC_R, span * DISC_R, n).map(([u, v]) => [C + u * c - v * s, C + u * s + v * c]);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The half plane on one side of an open curve that runs right across the disc: the curve closed 3000 out along the
|
|
43
|
+
* normal of its first-to-last chord.
|
|
44
|
+
*
|
|
45
|
+
* @param {readonly Point[]} L The curve.
|
|
46
|
+
* @param {number} s Which side: 1 or −1.
|
|
47
|
+
* @returns {Polygon} The half plane.
|
|
48
|
+
*/
|
|
49
|
+
export const discSide = (L, s) => {
|
|
50
|
+
const a = L[0], b = L.at(-1), d = [b[0] - a[0], b[1] - a[1]], l = Math.hypot(...d), n = [(-d[1] / l) * s * 3000, (d[0] / l) * s * 3000];
|
|
51
|
+
return poly([...L, [b[0] + n[0], b[1] + n[1]], [a[0] + n[0], a[1] + n[1]]]);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The disc parted along the curve L by the gap g, each half's rim moved by sa / sb (the cut stays put, so the gap stays g).
|
|
56
|
+
*
|
|
57
|
+
* @param {readonly Point[]} L The cut.
|
|
58
|
+
* @param {number} g The gap.
|
|
59
|
+
* @param {readonly number[]} [sa=[0, 0]] The left half's rim, moved.
|
|
60
|
+
* @param {readonly number[]} [sb=[0, 0]] The right half's rim, moved.
|
|
61
|
+
* @returns {Shape} The two halves.
|
|
62
|
+
*/
|
|
63
|
+
export const split = (L, g, sa = [0, 0], sb = [0, 0]) => {
|
|
64
|
+
const z = stroke(L, g, { chunk: 8 });
|
|
65
|
+
return U(D(I(disc(...sa), discSide(L, 1)), z), D(I(disc(...sb), discSide(L, -1)), z));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A solid band of width b along L, parted from both halves of the disc by g; the band's ends follow a disc moved by sm, so
|
|
70
|
+
* it rides out past the rim at one end.
|
|
71
|
+
*
|
|
72
|
+
* @param {readonly Point[]} L The band's centreline.
|
|
73
|
+
* @param {number | ((i: number, n: number) => number)} b Its width, or a width per point.
|
|
74
|
+
* @param {number} g The gap.
|
|
75
|
+
* @param {readonly number[]} [sm=[0, 0]] The band's disc, moved.
|
|
76
|
+
* @param {readonly number[]} [sa=[0, 0]] The left half's rim, moved.
|
|
77
|
+
* @param {readonly number[]} [sb=[0, 0]] The right half's rim, moved.
|
|
78
|
+
* @returns {Shape} The halves and the band.
|
|
79
|
+
*/
|
|
80
|
+
export const bandCut = (L, b, g, sm = [0, 0], sa = [0, 0], sb = [0, 0]) => {
|
|
81
|
+
const wz = typeof b === 'function' ? (i, n) => b(i, n) + 2 * g : b + 2 * g, z = stroke(L, wz, { chunk: 8 });
|
|
82
|
+
return U(D(I(disc(...sa), discSide(L, 1)), z), D(I(disc(...sb), discSide(L, -1)), z), I(disc(...sm), stroke(L, b, { chunk: 8 })));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A point on the disc's frame: radius r from the centre at angle deg (0 right, 90 down).
|
|
87
|
+
*
|
|
88
|
+
* @param {number} r The radius.
|
|
89
|
+
* @param {number} deg The angle.
|
|
90
|
+
* @returns {Point} The point.
|
|
91
|
+
*/
|
|
92
|
+
export const discAt = (r, deg) => [C + r * Math.cos((deg * Math.PI) / 180), C + r * Math.sin((deg * Math.PI) / 180)];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A shape turned by deg about the point p.
|
|
96
|
+
*
|
|
97
|
+
* @param {Geom} s The shape.
|
|
98
|
+
* @param {number} deg The turn, degrees.
|
|
99
|
+
* @param {readonly number[]} p The pivot.
|
|
100
|
+
* @returns {Geom} The shape, turned.
|
|
101
|
+
*/
|
|
102
|
+
export const turn = (s, deg, p) => rot(s, deg, p[0], p[1]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The two sides of a circular cut (centre P, radius Ra) through the disc, parted by g; the inner side turned th° about P,
|
|
106
|
+
* so it glides along the cut.
|
|
107
|
+
*
|
|
108
|
+
* @param {readonly number[]} P The cut's centre.
|
|
109
|
+
* @param {number} Ra The cut's radius.
|
|
110
|
+
* @param {number} g The gap.
|
|
111
|
+
* @param {number} th The inner side's turn, degrees.
|
|
112
|
+
* @returns {Shape} The two sides.
|
|
113
|
+
*/
|
|
114
|
+
export const arcSplit = (P, Ra, g, th) => U(I(turn(disc(), th, P), circle(...P, Ra - g / 2, 900)), D(disc(), circle(...P, Ra + g / 2, 900)));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Geom, Point, Polygon, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** The frame's left edge (32). */
|
|
4
|
+
export const EMBLEM_X0: number;
|
|
5
|
+
/** The frame's right edge (480). */
|
|
6
|
+
export const EMBLEM_X1: number;
|
|
7
|
+
/** The frame's bottom edge (480). */
|
|
8
|
+
export const EMBLEM_Y1: number;
|
|
9
|
+
/** The frame's width (448). */
|
|
10
|
+
export const EMBLEM_W: number;
|
|
11
|
+
/** The scene parted by the horizon: everything within g/2 of y = hz cut away. */
|
|
12
|
+
export function horizon(shape: Geom, hz: number, g: number): Shape;
|
|
13
|
+
/** A sine line across the frame at height y. */
|
|
14
|
+
export function sine(y: number, amp: number, cycles: number, ph?: number, x0?: number, x1?: number): Point[];
|
|
15
|
+
/** The sea: a block cut by n wave bands g wide. */
|
|
16
|
+
export function sea(y0: number, y1: number, g: number, o?: { n?: number; amp?: number; cycles?: number; ph?: number; block?: Geom }): Shape;
|
|
17
|
+
/** A ploughed field: a block cut by n bands fanning from a vanishing point. */
|
|
18
|
+
export function field(y0: number, y1: number, g: number, n?: number, vy?: number, spread?: number, block?: Geom): Shape;
|
|
19
|
+
/** A triangle, its base on y from xa to xb, its apex at (ax, ay). */
|
|
20
|
+
export function tri(xa: number, xb: number, y: number, ax: number, ay: number): Polygon;
|
|
21
|
+
/** A cloud: a flat base on y, three bumps. */
|
|
22
|
+
export function cloud(cx: number, y: number, w: number, h: number): Shape;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split emblems: a 448 frame (x and y from 32 to 480) whose scene is drawn whole and then parted by the horizon gap
|
|
3
|
+
* (`horizon()`), so the cut is always exactly g; the lower half is a ground block carved by gap-wide bands (`sea()`: sine
|
|
4
|
+
* bands; `field()`: bands fanning from a vanishing point). Each mark sets its own horizon. From the Logomarks tools
|
|
5
|
+
* (emblemparts.mjs, batch 216, "Bold · Emblems", and its editions in batch 241).
|
|
6
|
+
*/
|
|
7
|
+
import { U, D, I, poly, rect, circle, sample, mitreBand as band } from '../geometry/index.js';
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('../geometry/types.js').Point} Point */
|
|
10
|
+
/** @typedef {import('../geometry/types.js').Polygon} Polygon */
|
|
11
|
+
/** @typedef {import('../geometry/types.js').Shape} Shape */
|
|
12
|
+
/** @typedef {import('../geometry/types.js').Geom} Geom */
|
|
13
|
+
|
|
14
|
+
/** The frame's left edge. */
|
|
15
|
+
export const EMBLEM_X0 = 32;
|
|
16
|
+
|
|
17
|
+
/** The frame's right edge. */
|
|
18
|
+
export const EMBLEM_X1 = 480;
|
|
19
|
+
|
|
20
|
+
/** The frame's bottom edge. */
|
|
21
|
+
export const EMBLEM_Y1 = 480;
|
|
22
|
+
|
|
23
|
+
/** The frame's width. */
|
|
24
|
+
export const EMBLEM_W = EMBLEM_X1 - EMBLEM_X0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The scene parted by the horizon: everything within g/2 of y = hz cut away.
|
|
28
|
+
*
|
|
29
|
+
* @param {Geom} shape The scene.
|
|
30
|
+
* @param {number} hz The horizon's height.
|
|
31
|
+
* @param {number} g The gap.
|
|
32
|
+
* @returns {Shape} The scene, parted.
|
|
33
|
+
*/
|
|
34
|
+
export const horizon = (shape, hz, g) => D(shape, rect(-200, hz - g / 2, 900, g));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A sine line across the frame at height y: amp tall, `cycles` waves over the frame's width, starting at phase ph.
|
|
38
|
+
*
|
|
39
|
+
* @param {number} y Its middle height.
|
|
40
|
+
* @param {number} amp Its amplitude.
|
|
41
|
+
* @param {number} cycles Waves across the frame.
|
|
42
|
+
* @param {number} [ph=0] The phase, degrees.
|
|
43
|
+
* @param {number} [x0=EMBLEM_X0 - 40] Where it starts.
|
|
44
|
+
* @param {number} [x1=EMBLEM_X1 + 40] Where it ends.
|
|
45
|
+
* @returns {Point[]} The line (160 steps).
|
|
46
|
+
*/
|
|
47
|
+
export const sine = (y, amp, cycles, ph = 0, x0 = EMBLEM_X0 - 40, x1 = EMBLEM_X1 + 40) =>
|
|
48
|
+
sample((x) => [x, y + amp * Math.sin(((x - EMBLEM_X0) / EMBLEM_W) * cycles * 2 * Math.PI + (ph * Math.PI) / 180)], x0, x1, 160);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The sea: the block from y0 to y1 with n wave bands g wide cut across it, evenly spaced.
|
|
52
|
+
*
|
|
53
|
+
* @param {number} y0 The block's top.
|
|
54
|
+
* @param {number} y1 Its bottom.
|
|
55
|
+
* @param {number} g The gap (the bands' width).
|
|
56
|
+
* @param {{ n?: number, amp?: number, cycles?: number, ph?: number, block?: Geom }} [o] Bands (2), their amplitude (16),
|
|
57
|
+
* waves across (2), phase (0) and the block to carve (the frame's width from y0 to y1).
|
|
58
|
+
* @returns {Shape} The sea.
|
|
59
|
+
*/
|
|
60
|
+
export const sea = (y0, y1, g, { n = 2, amp = 16, cycles = 2, ph = 0, block = rect(EMBLEM_X0, y0, EMBLEM_W, y1 - y0) } = {}) => {
|
|
61
|
+
const step = (y1 - y0) / (n + 1);
|
|
62
|
+
return D(block, ...[...Array(n)].map((_, i) => band(sine(y0 + step * (i + 1), amp, cycles, ph), g)));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A ploughed field in perspective: the block cut by n bands g wide fanning down from a vanishing point (256, vy) above
|
|
67
|
+
* the horizon, their feet spread across the width (spread 1 lays the outer feet on the frame's corners).
|
|
68
|
+
*
|
|
69
|
+
* @param {number} y0 The block's top.
|
|
70
|
+
* @param {number} y1 Its bottom.
|
|
71
|
+
* @param {number} g The gap (the bands' width).
|
|
72
|
+
* @param {number} [n=3] The bands.
|
|
73
|
+
* @param {number} [vy=y0 - 150] The vanishing point's height.
|
|
74
|
+
* @param {number} [spread=1.15] How far the feet spread.
|
|
75
|
+
* @param {Geom} [block] The block to carve (the frame's width from y0 to y1).
|
|
76
|
+
* @returns {Shape} The field.
|
|
77
|
+
*/
|
|
78
|
+
export const field = (y0, y1, g, n = 3, vy = y0 - 150, spread = 1.15, block = rect(EMBLEM_X0, y0, EMBLEM_W, y1 - y0)) =>
|
|
79
|
+
D(block, ...[...Array(n)].map((_, i) => {
|
|
80
|
+
const t = (i - (n - 1) / 2) / ((n + 1) / 2), foot = [256 + t * (EMBLEM_W / 2) * spread * 1.6, y1 + 60];
|
|
81
|
+
return I(band([[256, vy], foot], g), rect(-200, y0, 900, 900));
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A triangle with its base on y from xa to xb and its apex at (ax, ay).
|
|
86
|
+
*
|
|
87
|
+
* @param {number} xa The base's left end.
|
|
88
|
+
* @param {number} xb The base's right end.
|
|
89
|
+
* @param {number} y The base's height.
|
|
90
|
+
* @param {number} ax The apex.
|
|
91
|
+
* @param {number} ay The apex.
|
|
92
|
+
* @returns {Polygon} The triangle.
|
|
93
|
+
*/
|
|
94
|
+
export const tri = (xa, xb, y, ax, ay) => poly([[xa, y], [ax, ay], [xb, y]]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A cloud: a flat base on y and three bumps, w wide and h tall about cx.
|
|
98
|
+
*
|
|
99
|
+
* @param {number} cx Its middle.
|
|
100
|
+
* @param {number} y Its base.
|
|
101
|
+
* @param {number} w Its width.
|
|
102
|
+
* @param {number} h Its height.
|
|
103
|
+
* @returns {Shape} The cloud.
|
|
104
|
+
*/
|
|
105
|
+
export const cloud = (cx, y, w, h) => {
|
|
106
|
+
const r1 = h * 0.42, r2 = h * 0.62, r3 = h * 0.36;
|
|
107
|
+
return I(U(circle(cx - w * 0.26, y - r1, r1, 240), circle(cx + w * 0.04, y - h + r2, r2, 240), circle(cx + w * 0.3, y - r3, r3, 240),
|
|
108
|
+
rect(cx - w * 0.26, y - r1, w * 0.56, r1)), rect(cx - w, y - 2 * h, 2 * w, 2 * h));
|
|
109
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Point, Shape } from '../geometry/types.js';
|
|
2
|
+
|
|
3
|
+
/** A flow grid's drawing: n × m discs of radius r, gap g, the path, the bridges' neck, blend, fillet and skipped cells. */
|
|
4
|
+
export interface FlowOptions {
|
|
5
|
+
n: number;
|
|
6
|
+
m?: number;
|
|
7
|
+
r: number;
|
|
8
|
+
g: number;
|
|
9
|
+
path: string;
|
|
10
|
+
neck?: number;
|
|
11
|
+
k?: number;
|
|
12
|
+
fillet?: number;
|
|
13
|
+
skip?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A flow path ("a0 b0 b1", a trailing "~" closing it) as cells [column, row]. */
|
|
17
|
+
export function cells(s: string): { path: [number, number][]; closed: boolean };
|
|
18
|
+
/** A flow grid's pitch and each cell's centre, for n × m discs of radius r and gap g centred on (cx, cy). */
|
|
19
|
+
export function grid(n: number, m: number, r: number, g: number, cx?: number, cy?: number): { p: number; at: (cell: readonly number[]) => Point };
|
|
20
|
+
/** A flow grid as shapes: the path, the free discs, both, and the narrowest gap between them. */
|
|
21
|
+
export function flowShape(o: FlowOptions): { path: Shape; free: Shape; shape: Shape; clearance: number };
|