grailwind 0.6.0 → 0.8.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 +48 -1
- package/README.md +3 -3
- package/data.js +62 -9
- package/divisions.js +150 -26
- package/grailwind.css +21 -8
- package/ordinaries.js +341 -51
- package/package.json +12 -4
- package/paths.js +848 -0
package/ordinaries.js
CHANGED
|
@@ -2,8 +2,21 @@
|
|
|
2
2
|
* Grailwind CSS: ordinaries, e.g. `fess-or`.
|
|
3
3
|
*/
|
|
4
4
|
import plugin from "tailwindcss/plugin";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
BETWEEN,
|
|
7
|
+
BETWEEN_LINED,
|
|
8
|
+
FURS,
|
|
9
|
+
LINES as LINE_NAMES,
|
|
10
|
+
lineOrdinaries,
|
|
11
|
+
OF_THE_FIRST,
|
|
12
|
+
ON,
|
|
13
|
+
ON_SCALE,
|
|
14
|
+
REVERSIBLE,
|
|
15
|
+
TINCTURES,
|
|
16
|
+
VOIDABLE,
|
|
17
|
+
} from "./data.js";
|
|
6
18
|
import { furBackground } from "./furs.js";
|
|
19
|
+
import { PATHS } from "./paths.js";
|
|
7
20
|
|
|
8
21
|
// prettier-ignore
|
|
9
22
|
const ORDINARIES = {
|
|
@@ -18,21 +31,23 @@ const ORDINARIES = {
|
|
|
18
31
|
],
|
|
19
32
|
bend: [[0, 0], [20, 0], [100, 80], [100, 100], [80, 100], [0, 20]],
|
|
20
33
|
"bend-sinister": [[100, 0], [80, 0], [0, 80], [0, 100], [20, 100], [100, 20]],
|
|
21
|
-
chevron: [[50,
|
|
34
|
+
chevron: [[50, 22], [100, 72], [100, 98], [50, 48], [0, 98], [0, 72]],
|
|
22
35
|
cross: [
|
|
23
|
-
[
|
|
24
|
-
[
|
|
36
|
+
[38, 0], [62, 0], [62, 35], [100, 35], [100, 55], [62, 55],
|
|
37
|
+
[62, 100], [38, 100], [38, 55], [0, 55], [0, 35], [38, 35],
|
|
25
38
|
],
|
|
39
|
+
// Its centre lines run from the top corners through the fess point, reaching the flanks at 90%.
|
|
26
40
|
saltire: [
|
|
27
|
-
[0, 0], [
|
|
28
|
-
[100, 100], [
|
|
41
|
+
[0, 0], [16.35, 0], [50, 30.28], [83.65, 0], [100, 0], [100, 14.72], [66.35, 45], [100, 75.28],
|
|
42
|
+
[100, 100], [94.76, 100], [50, 59.72], [5.24, 100], [0, 100], [0, 75.28], [33.65, 45], [0, 14.72],
|
|
29
43
|
],
|
|
44
|
+
// Its arms and foot are 24 across, the arms' centre lines running from the top corners to (50, 40).
|
|
30
45
|
pall: [
|
|
31
|
-
[0, 0], [
|
|
32
|
-
[
|
|
46
|
+
[0, 0], [17.33, 0], [50, 26.14], [82.67, 0], [100, 0], [100, 13.86],
|
|
47
|
+
[62, 44.26], [62, 100], [38, 100], [38, 44.26], [0, 13.86],
|
|
33
48
|
],
|
|
34
49
|
chief: [[0, 0], [100, 0], [100, 30], [0, 30]],
|
|
35
|
-
pile: [[
|
|
50
|
+
pile: [[17, 0], [83, 0], [50, 85]],
|
|
36
51
|
};
|
|
37
52
|
// signedArea() follows the flip's winding, so the order stays.
|
|
38
53
|
for (const ordinary of REVERSIBLE) {
|
|
@@ -143,14 +158,32 @@ const widenedPolygon = (vertices) => {
|
|
|
143
158
|
// The ordinary's tincture, top layer first. `x`/`y` bands run between two x or y
|
|
144
159
|
// values in %; `u-v`/`u+v` are diagonal, between values of x − y or x + y.
|
|
145
160
|
// A `tile` [x0, y0, x1, y1] confines a layer to part of the box (square, if
|
|
146
|
-
// diagonal
|
|
147
|
-
// another's.
|
|
161
|
+
// diagonal, unless `half` or `diagonal` gives its stops). Stripes repaint the
|
|
162
|
+
// fimbriation where one arm's band runs on into another's.
|
|
148
163
|
const S = 12;
|
|
149
164
|
const band = (kind, from, to, tile) => ({ kind, from, to, tile });
|
|
150
165
|
const stripe = (kind, from, to, tile) => ({ kind, from, to, tile, stripe: true });
|
|
151
|
-
// Half a tile, split on its diagonal, as gradient stops: 0–50% or 50–100%.
|
|
152
|
-
//
|
|
166
|
+
// Half a tile, split on its diagonal, as gradient stops: 0–50% or 50–100%. For a tile that isn't
|
|
167
|
+
// square, whose diagonal isn't a line of constant x ± y.
|
|
153
168
|
const half = (kind, end, tile) => ({ kind, tile, stops: end === "start" ? [0, 50] : [50, 100] });
|
|
169
|
+
// A band `halfWidth` real units either side of a tile's diagonal, for an arm that isn't a line of
|
|
170
|
+
// constant x ± y: `u-v` runs from the tile's top left, `u+v` from its top right.
|
|
171
|
+
const diagonal = (kind, halfWidth, tile) => {
|
|
172
|
+
const [w, h] = real([tile[2] - tile[0], tile[3] - tile[1]]);
|
|
173
|
+
const length = (2 * w * h) / Math.hypot(w, h);
|
|
174
|
+
const offset = (halfWidth / length) * 100;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
kind,
|
|
178
|
+
tile,
|
|
179
|
+
stops: [50 - offset, 50 + offset],
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
// The saltire's arms are the diagonals of a tile as tall as the shield, sloping through (50, 45).
|
|
183
|
+
const SALTIRE_TILE = (100 * 50) / 45;
|
|
184
|
+
// The pall's arms are the diagonals of a tile reaching down to the foot's corners, through (50, 40).
|
|
185
|
+
const PALL_TILE_H = 44.26;
|
|
186
|
+
const PALL_TILE_W = (PALL_TILE_H * 50) / 40;
|
|
154
187
|
// A 2px strip down the centre, from the top to `bottom`, where two tiles meet mid-pixel and
|
|
155
188
|
// would let the fimbriation show between them. Unsoftened, as it lies on its own tincture.
|
|
156
189
|
const seam = (bottom) => ({ kind: "x", tile: [0, 0, 100, bottom], seam: true });
|
|
@@ -164,24 +197,23 @@ const BANDS = {
|
|
|
164
197
|
bend: [band("u-v", -20, 20)],
|
|
165
198
|
"bend-sinister": [band("u+v", 80, 120)],
|
|
166
199
|
chevron: [
|
|
167
|
-
stripe("u+v",
|
|
168
|
-
stripe("u-v",
|
|
169
|
-
band("u+v",
|
|
170
|
-
band("u-v",
|
|
200
|
+
stripe("u+v", 72 - S, 72),
|
|
201
|
+
stripe("u-v", 28, 28 + S),
|
|
202
|
+
band("u+v", 72, 98),
|
|
203
|
+
band("u-v", 2, 28),
|
|
204
|
+
],
|
|
205
|
+
cross: [band("x", 38, 62), band("y", 35, 55)],
|
|
206
|
+
saltire: [
|
|
207
|
+
diagonal("u-v", 12, [0, 0, SALTIRE_TILE, 100]),
|
|
208
|
+
diagonal("u+v", 12, [100 - SALTIRE_TILE, 0, 100, 100]),
|
|
171
209
|
],
|
|
172
|
-
cross: [band("x", 40, 60), band("y", 35, 55)],
|
|
173
|
-
saltire: [band("u-v", -15, 15), band("u+v", 85, 115)],
|
|
174
210
|
pall: [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
stripe("u-v", -15 - S, -15, [0, 30, 40, 70]),
|
|
179
|
-
band("u-v", -15, 15),
|
|
180
|
-
band("u+v", 85, 115),
|
|
181
|
-
band("x", 40, 60, [0, 55, 100, 100]),
|
|
211
|
+
diagonal("u-v", 12, [0, 0, PALL_TILE_W, PALL_TILE_H]),
|
|
212
|
+
diagonal("u+v", 12, [100 - PALL_TILE_W, 0, 100, PALL_TILE_H]),
|
|
213
|
+
band("x", 38, 62, [0, 40, 100, 100]),
|
|
182
214
|
],
|
|
183
215
|
// Each slanted side is its tile's diagonal.
|
|
184
|
-
pile: [half("u-v", "end", [
|
|
216
|
+
pile: [half("u-v", "end", [17, 0, 50, 85]), half("u+v", "start", [50, 0, 83, 85]), seam(83)],
|
|
185
217
|
};
|
|
186
218
|
|
|
187
219
|
// The bands upside down. A diagonal swaps kind, as x − y becomes x + (100 − y) − 100. A half's
|
|
@@ -267,17 +299,27 @@ const bands = (layers, colour) => ({
|
|
|
267
299
|
// box is shorter, so they are slightly squashed.
|
|
268
300
|
const SCALLOP = 14;
|
|
269
301
|
const SCALLOPS = { chevron: 10 };
|
|
302
|
+
// Wavy half-waves, longer where the default reads as beads: the bend, cross and cornered ordinaries.
|
|
303
|
+
// prettier-ignore
|
|
304
|
+
const WAVES = {
|
|
305
|
+
bend: 21, "bend-sinister": 21, cross: 20,
|
|
306
|
+
chevron: 28, saltire: 28, pall: 28,
|
|
307
|
+
};
|
|
308
|
+
const LENGTHS = { wavy: WAVES };
|
|
270
309
|
|
|
271
310
|
// A quarter of the ordinary's width, in real units. The pile tapers, so its is set by eye.
|
|
272
311
|
// prettier-ignore
|
|
273
312
|
const VOIDS = {
|
|
274
313
|
pale: 7.5, fess: 9, bar: 4.2, bend: 7.7, "bend-sinister": 7.7,
|
|
275
|
-
chevron:
|
|
314
|
+
chevron: 5, cross: 6, saltire: 6, pall: 6, pile: 8.8,
|
|
276
315
|
};
|
|
277
316
|
for (const ordinary of REVERSIBLE) {
|
|
278
317
|
if (SCALLOPS[ordinary]) {
|
|
279
318
|
SCALLOPS[`${ordinary}-reversed`] = SCALLOPS[ordinary];
|
|
280
319
|
}
|
|
320
|
+
if (WAVES[ordinary]) {
|
|
321
|
+
WAVES[`${ordinary}-reversed`] = WAVES[ordinary];
|
|
322
|
+
}
|
|
281
323
|
VOIDS[`${ordinary}-reversed`] = VOIDS[ordinary];
|
|
282
324
|
}
|
|
283
325
|
const DEPTH = 0.35;
|
|
@@ -297,10 +339,135 @@ const arcs = (a, b, scallop, sweep) => {
|
|
|
297
339
|
});
|
|
298
340
|
};
|
|
299
341
|
|
|
300
|
-
|
|
342
|
+
const WAVE_DEPTH = 0.2;
|
|
343
|
+
|
|
344
|
+
// Which way half-wave `index` bulges, for an edge walked along the shared axis (`forward`) or
|
|
345
|
+
// against it. Along the axis, `cw` bulges to its left: up for a horizontal edge.
|
|
346
|
+
const waveSweep = (index, forward) => {
|
|
347
|
+
return forward === (index % 2 === 0) ? "cw" : "ccw";
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// The edge a→b cut at every multiple of `period` along a shared axis, so the parallel edges of a
|
|
351
|
+
// band are cut at the same places whichever way the polygon walks them. Each piece runs from
|
|
352
|
+
// `start` to `end` (fractions of the edge) and lies in period `index`.
|
|
353
|
+
const stations = (a, b, period) => {
|
|
354
|
+
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
355
|
+
const length = Math.hypot(dx, dy);
|
|
356
|
+
// Right, or down for a vertical edge, whichever way the polygon walks it.
|
|
357
|
+
const forward = Math.abs(dx) > 1e-9 ? dx > 0 : dy > 0;
|
|
358
|
+
const axis = (forward ? [dx, dy] : [-dx, -dy]).map((d) => d / length);
|
|
359
|
+
const station = (p) => p[0] * axis[0] + p[1] * axis[1];
|
|
360
|
+
const [from, to] = [station(a), station(b)];
|
|
361
|
+
|
|
362
|
+
const ts = [0, 1];
|
|
363
|
+
const [low, high] = [Math.min(from, to), Math.max(from, to)];
|
|
364
|
+
for (let cut = Math.ceil(low / period); cut * period < high; cut++) {
|
|
365
|
+
const t = (cut * period - from) / (to - from);
|
|
366
|
+
if (t > 1e-6 && t < 1 - 1e-6) {
|
|
367
|
+
ts.push(t);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
ts.sort((x, y) => x - y);
|
|
371
|
+
|
|
372
|
+
const pieces = ts.slice(1).map((end, i) => {
|
|
373
|
+
const middle = (ts[i] + end) / 2;
|
|
374
|
+
return { start: ts[i], end, index: Math.floor((from + (to - from) * middle) / period) };
|
|
375
|
+
});
|
|
376
|
+
return { forward, axis, length, pieces };
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// One arc per half-wave, phased by position along a shared axis rather than from the edge's
|
|
380
|
+
// start, so the two edges of a band crest together.
|
|
381
|
+
const wavy = (a, b, halfWave) => {
|
|
382
|
+
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
383
|
+
const { forward, length, pieces } = stations(a, b, halfWave);
|
|
384
|
+
return pieces.map(({ start, end, index }) => {
|
|
385
|
+
const chord = length * (end - start);
|
|
386
|
+
// As high as a default wave whatever the length, so a longer wave is gentler, not deeper.
|
|
387
|
+
const depth = chord * WAVE_DEPTH * (SCALLOP / halfWave);
|
|
388
|
+
return {
|
|
389
|
+
to: [a[0] + dx * end, a[1] + dy * end],
|
|
390
|
+
r: (chord * chord) / (8 * depth) + depth / 2,
|
|
391
|
+
sweep: waveSweep(index, forward),
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// The unit normal of a→b pointing into the ordinary. An arc swept `inward` bulges that way.
|
|
397
|
+
const intoOrdinary = (a, b, inward) => {
|
|
398
|
+
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
399
|
+
const length = Math.hypot(dx, dy);
|
|
400
|
+
return inward === "cw" ? [dy / length, -dx / length] : [-dy / length, dx / length];
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const INDENT_DEPTH = 0.5;
|
|
404
|
+
// Seven whole teeth across the shield, so both ends land on the same kind of turn.
|
|
405
|
+
const TOOTH = W / 7;
|
|
406
|
+
|
|
407
|
+
// A zigzag centred on the edge, its turns at the shared stations, so a band's two edges run in
|
|
408
|
+
// phase, point opposite notch (FD Figs. 91, 106). At 0.5 deep the teeth are right-angled.
|
|
409
|
+
const indented = (a, b, scallop) => {
|
|
410
|
+
const half = scallop / 2;
|
|
411
|
+
const { axis, pieces } = stations(a, b, half);
|
|
412
|
+
const normal = [-axis[1], axis[0]];
|
|
413
|
+
const height = (scallop * INDENT_DEPTH) / 2;
|
|
414
|
+
// Even turns sit on one side and odd ones on the other; between them the zigzag is straight.
|
|
415
|
+
const turnSide = (turn) => (turn % 2 === 0 ? height : -height);
|
|
416
|
+
const onZigzag = (t) => {
|
|
417
|
+
const p = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
418
|
+
const position = (p[0] * axis[0] + p[1] * axis[1]) / half;
|
|
419
|
+
const before = Math.floor(position);
|
|
420
|
+
const blend = position - before;
|
|
421
|
+
const side = turnSide(before) * (1 - blend) + turnSide(before + 1) * blend;
|
|
422
|
+
return { to: [p[0] + normal[0] * side, p[1] + normal[1] * side] };
|
|
423
|
+
};
|
|
424
|
+
// Start and end on the zigzag itself, not on the plain line, so it meets the shield's edge at
|
|
425
|
+
// its true angle.
|
|
426
|
+
return [onZigzag(0), ...pieces.map(({ end }) => onZigzag(end)), { to: b }];
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// A merlon or crenel's width, and the battlements' height, in real units. An odd number across
|
|
430
|
+
// the shield's width, so a fess or chief has a merlon at both ends.
|
|
431
|
+
const BATTLEMENT = W / 15;
|
|
432
|
+
const BATTLEMENT_HEIGHT = 6;
|
|
433
|
+
// Embattled on the upper edge only (FD p. 93); the rest on every edge.
|
|
434
|
+
// prettier-ignore
|
|
435
|
+
const UPPER_ONLY = [
|
|
436
|
+
"fess", "bar", "two-bars", "three-bars", "bend", "bend-sinister", "chevron", "chevron-reversed",
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
// Battlements centred on the edge, in phase across a band like `wavy`, but merlon opposite
|
|
440
|
+
// merlon: a piece's side is taken from the ordinary, not from the shield.
|
|
441
|
+
const embattled = (a, b, merlon, inward, ordinary) => {
|
|
442
|
+
const into = intoOrdinary(a, b, inward);
|
|
443
|
+
// Upper-only ordinaries keep the edges whose outside doesn't face up plain.
|
|
444
|
+
if (UPPER_ONLY.includes(ordinary) && into[1] < 1e-8) {
|
|
445
|
+
return [{ to: b }];
|
|
446
|
+
}
|
|
447
|
+
const { pieces } = stations(a, b, merlon);
|
|
448
|
+
const along = (t, [ox, oy]) => [a[0] + (b[0] - a[0]) * t + ox, a[1] + (b[1] - a[1]) * t + oy];
|
|
449
|
+
// Even pieces are merlons, out of the ordinary; odd ones crenels, into it.
|
|
450
|
+
const offset = (index) => {
|
|
451
|
+
const shift = (index % 2 === 0 ? -1 : 1) * (BATTLEMENT_HEIGHT / 2);
|
|
452
|
+
return [into[0] * shift, into[1] * shift];
|
|
453
|
+
};
|
|
454
|
+
return [
|
|
455
|
+
...pieces.flatMap(({ start, end, index }) => {
|
|
456
|
+
const off = offset(index);
|
|
457
|
+
return [{ to: along(start, off) }, { to: along(end, off) }];
|
|
458
|
+
}),
|
|
459
|
+
{ to: b },
|
|
460
|
+
];
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Each line maps an edge to segments, arcs or straight; `inward` is the sweep that bulges into
|
|
464
|
+
// the ordinary.
|
|
301
465
|
const LINES = {
|
|
302
466
|
engrailed: (a, b, scallop, inward) => arcs(a, b, scallop, inward),
|
|
303
467
|
invected: (a, b, scallop, inward) => arcs(a, b, scallop, inward === "cw" ? "ccw" : "cw"),
|
|
468
|
+
wavy: (a, b, scallop) => wavy(a, b, scallop),
|
|
469
|
+
indented: (a, b) => indented(a, b, TOOTH),
|
|
470
|
+
embattled: (a, b, scallop, inward, ordinary) => embattled(a, b, BATTLEMENT, inward, ordinary),
|
|
304
471
|
};
|
|
305
472
|
|
|
306
473
|
const side = (a, b, p) => (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
@@ -327,8 +494,8 @@ const crossing = (a, v, b, first, second) => {
|
|
|
327
494
|
: null;
|
|
328
495
|
};
|
|
329
496
|
|
|
330
|
-
// `
|
|
331
|
-
const
|
|
497
|
+
// The lined edges of `vertices` as arcs and lines in real units, and where the shape starts.
|
|
498
|
+
const outline = (vertices, line, scallop) => {
|
|
332
499
|
const inward = signedArea(vertices) > 0 ? "ccw" : "cw";
|
|
333
500
|
const pts = vertices.map(real);
|
|
334
501
|
const straight = vertices.map((v, i) => onShieldEdge(v, vertices[(i + 1) % vertices.length]));
|
|
@@ -356,7 +523,12 @@ const shape = (vertices, line, scallop, extend) => {
|
|
|
356
523
|
start = p;
|
|
357
524
|
}
|
|
358
525
|
});
|
|
526
|
+
return { pts, straight, edges, start };
|
|
527
|
+
};
|
|
359
528
|
|
|
529
|
+
// `extend` moves the sides on the shield's edge out, as `outer` does; the lined sides stay put.
|
|
530
|
+
const shape = (vertices, line, scallop, extend) => {
|
|
531
|
+
const { pts, straight, edges, start } = outline(vertices, line, scallop);
|
|
360
532
|
const commands = edges.flatMap((edge, i) => {
|
|
361
533
|
const segments = edge.map(({ to, r, sweep }) =>
|
|
362
534
|
r
|
|
@@ -377,6 +549,89 @@ const shape = (vertices, line, scallop, extend) => {
|
|
|
377
549
|
return `shape(from ${from}, ${commands.join(", ")}, close)`;
|
|
378
550
|
};
|
|
379
551
|
|
|
552
|
+
// An outline as an SVG path, in real units. y points down in both, so cw is sweep-flag 1.
|
|
553
|
+
const svgPath = ({ edges, start }) => {
|
|
554
|
+
const parts = [`M ${num(start[0])} ${num(start[1])}`];
|
|
555
|
+
for (const { to, r, sweep } of edges.flat()) {
|
|
556
|
+
const [x, y] = [num(to[0]), num(to[1])];
|
|
557
|
+
parts.push(r ? `A ${num(r)} ${num(r)} 0 0 ${sweep === "cw" ? 1 : 0} ${x} ${y}` : `L ${x} ${y}`);
|
|
558
|
+
}
|
|
559
|
+
return `${parts.join(" ")} Z`;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// The generator's lined ordinary, e.g. `generated("pile", "engrailed")`: the arguments for
|
|
563
|
+
// `outline()` and `shape()`.
|
|
564
|
+
const generated = (ordinary, name) => [
|
|
565
|
+
ORDINARIES[ordinary],
|
|
566
|
+
(a, b, scallop, inward) => LINES[name](a, b, scallop, inward, ordinary),
|
|
567
|
+
(LENGTHS[name] ?? SCALLOPS)[ordinary] ?? SCALLOP,
|
|
568
|
+
];
|
|
569
|
+
const draftOutline = (ordinary, name) => outline(...generated(ordinary, name));
|
|
570
|
+
const generatedShape = (ordinary, name) => shape(...generated(ordinary, name));
|
|
571
|
+
|
|
572
|
+
// Drawn shapes are segments in real units: ["M", x, y], ["L", x, y], ["C", x1, y1, x2, y2, x, y],
|
|
573
|
+
// ["A", rx, ry, rotation, large, sweep, x, y] and ["Z"].
|
|
574
|
+
const pathShape = (segments) => {
|
|
575
|
+
const [, startX, startY] = segments[0];
|
|
576
|
+
const commands = segments.slice(1).map(([command, ...n]) => {
|
|
577
|
+
switch (command) {
|
|
578
|
+
case "L":
|
|
579
|
+
return `line to ${at([n[0], n[1]])}`;
|
|
580
|
+
case "C":
|
|
581
|
+
return `curve to ${at([n[4], n[5]])} with ${at([n[0], n[1]])} / ${at([n[2], n[3]])}`;
|
|
582
|
+
case "A": {
|
|
583
|
+
const [rx, ry, rotation, large, sweep, x, y] = n;
|
|
584
|
+
return [
|
|
585
|
+
`arc to ${at([x, y])} of ${pct((rx * 100) / W)} ${pct((ry * 100) / H)}`,
|
|
586
|
+
sweep ? "cw" : "ccw",
|
|
587
|
+
large ? "large" : "",
|
|
588
|
+
rotation ? `rotate ${rotation}deg` : "",
|
|
589
|
+
]
|
|
590
|
+
.filter(Boolean)
|
|
591
|
+
.join(" ");
|
|
592
|
+
}
|
|
593
|
+
case "Z":
|
|
594
|
+
return "close";
|
|
595
|
+
default:
|
|
596
|
+
throw new Error(`unknown path command ${command}`);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
const last = commands.pop();
|
|
600
|
+
if (last !== "close") {
|
|
601
|
+
throw new Error("a drawn path must end with Z");
|
|
602
|
+
}
|
|
603
|
+
return `shape(from ${at([startX, startY])}, ${commands.join(", ")}, close)`;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// The same shape upside down, for an ordinary's reversed form.
|
|
607
|
+
const flipPath = (segments) =>
|
|
608
|
+
segments.map(([command, ...n]) => {
|
|
609
|
+
switch (command) {
|
|
610
|
+
case "M":
|
|
611
|
+
case "L":
|
|
612
|
+
return [command, n[0], H - n[1]];
|
|
613
|
+
case "C":
|
|
614
|
+
return [command, n[0], H - n[1], n[2], H - n[3], n[4], H - n[5]];
|
|
615
|
+
case "A":
|
|
616
|
+
return [command, n[0], n[1], n[2] ? -n[2] : 0, n[3], n[4] ? 0 : 1, n[5], H - n[6]];
|
|
617
|
+
default:
|
|
618
|
+
return [command];
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Drawn shapes by `ordinary-line`, each reversible one also upside down as `ordinary-reversed-line`
|
|
623
|
+
// unless it was drawn too. Upper-only battlements would flip onto the lower edges, so those aren't.
|
|
624
|
+
const DRAWN = { ...PATHS };
|
|
625
|
+
for (const key of Object.keys(PATHS)) {
|
|
626
|
+
const name = LINE_NAMES.find((line) => key.endsWith(`-${line}`));
|
|
627
|
+
const ordinary = key.slice(0, -name.length - 1);
|
|
628
|
+
const reversedKey = `${ordinary}-reversed-${name}`;
|
|
629
|
+
const upperOnly = name === "embattled" && UPPER_ONLY.includes(ordinary);
|
|
630
|
+
if (REVERSIBLE.includes(ordinary) && !upperOnly && !(reversedKey in PATHS)) {
|
|
631
|
+
DRAWN[reversedKey] = flipPath(PATHS[key]);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
380
635
|
const betweenVars = ({ size, points, counts = {} }) => {
|
|
381
636
|
const vars = { "--between-size": `${size}%` };
|
|
382
637
|
points.forEach(([left, top], i) => {
|
|
@@ -395,8 +650,15 @@ const betweenVars = ({ size, points, counts = {} }) => {
|
|
|
395
650
|
return vars;
|
|
396
651
|
};
|
|
397
652
|
|
|
398
|
-
|
|
399
|
-
|
|
653
|
+
// The plain ordinary's `between` layout, with a line's overrides on top.
|
|
654
|
+
const mergeLined = (ordinary, line) => {
|
|
655
|
+
const plain = BETWEEN[ordinary];
|
|
656
|
+
const lined = BETWEEN_LINED[ordinary]?.[line];
|
|
657
|
+
return lined ? { ...plain, ...lined, counts: { ...plain.counts, ...lined.counts } } : plain;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const onVars = ({ size, counts }, scale = 1) => {
|
|
661
|
+
const vars = { "--on-size": `${+(size * scale).toFixed(2)}%` };
|
|
400
662
|
for (const [count, points] of Object.entries(counts)) {
|
|
401
663
|
points.forEach(([left, top], i) => {
|
|
402
664
|
vars[`--on-${count}-${i + 1}-x`] = `${left}%`;
|
|
@@ -415,6 +677,12 @@ const layoutVars = () => {
|
|
|
415
677
|
allVars.push(...Object.keys(vars));
|
|
416
678
|
}
|
|
417
679
|
|
|
680
|
+
for (const [ordinary, lines] of Object.entries(BETWEEN_LINED)) {
|
|
681
|
+
for (const line of Object.keys(lines)) {
|
|
682
|
+
allVars.push(...Object.keys(betweenVars(mergeLined(ordinary, line))));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
418
686
|
for (const division of Object.values(ON)) {
|
|
419
687
|
const vars = onVars(division);
|
|
420
688
|
allVars.push(...Object.keys(vars));
|
|
@@ -432,6 +700,8 @@ const PAINTS = [
|
|
|
432
700
|
[OF_THE_FIRST, "var(--the-first, transparent)"],
|
|
433
701
|
];
|
|
434
702
|
|
|
703
|
+
export { draftOutline, DRAWN, flipPath, generatedShape, ORDINARIES, pathShape, svgPath };
|
|
704
|
+
|
|
435
705
|
export default plugin(({ addBase, addUtilities, theme }) => {
|
|
436
706
|
// Unset on an inescutcheon, a canton or a part of a divided shield, so it can't inherit its
|
|
437
707
|
// shield's layout. In the base layer, so an ordinary on it still sets its own.
|
|
@@ -451,11 +721,11 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
451
721
|
|
|
452
722
|
// Isolated, so an ordinary in a quarter paints under the shield's own ordinary.
|
|
453
723
|
// The chief's ::after stays unraised, so charges on it stay on top.
|
|
454
|
-
const ordinaryRules = (ordinary, pseudo) => ({
|
|
724
|
+
const ordinaryRules = (ordinary, pseudo, line) => ({
|
|
455
725
|
position: "relative",
|
|
456
726
|
isolation: "isolate",
|
|
457
|
-
...(BETWEEN[ordinary] && betweenVars(
|
|
458
|
-
...(ON[ordinary] && onVars(ON[ordinary])),
|
|
727
|
+
...(BETWEEN[ordinary] && betweenVars(mergeLined(ordinary, line))),
|
|
728
|
+
...(ON[ordinary] && onVars(ON[ordinary], ON_SCALE[line] ?? 1)),
|
|
459
729
|
// With a chief, everything else is drawn in the field below it.
|
|
460
730
|
...(ordinary === "chief" && {
|
|
461
731
|
"--field-top": "30%",
|
|
@@ -478,12 +748,18 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
478
748
|
? `polygon(${vertices.map(outer).join(", ")}) content-box`
|
|
479
749
|
: polygon(vertices);
|
|
480
750
|
// Browsers without shape() keep the straight polygon.
|
|
481
|
-
const lined = (line) =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
751
|
+
const lined = (name, line) => {
|
|
752
|
+
const edge = (a, b, scallop, inward) => line(a, b, scallop, inward, ordinary);
|
|
753
|
+
const drawn = DRAWN[`${ordinary}-${name}`];
|
|
754
|
+
return [
|
|
755
|
+
plain,
|
|
756
|
+
chief
|
|
757
|
+
? `${shape(vertices, edge, SCALLOP, outer)} content-box`
|
|
758
|
+
: drawn
|
|
759
|
+
? pathShape(drawn)
|
|
760
|
+
: shape(vertices, edge, (LENGTHS[name] ?? SCALLOPS)[ordinary] ?? SCALLOP),
|
|
761
|
+
];
|
|
762
|
+
};
|
|
487
763
|
|
|
488
764
|
// Covers the whole shield, so the swapped field lines up with the real one,
|
|
489
765
|
// and the clip moves the ordinary below any chief instead.
|
|
@@ -527,11 +803,18 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
527
803
|
});
|
|
528
804
|
// Not fimbriated.
|
|
529
805
|
for (const [name, line] of Object.entries(LINES)) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
806
|
+
if (!lineOrdinaries(name).includes(ordinary)) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
utilities[`.${ordinary}-${name}-${tincture}`] = ordinaryRules(
|
|
810
|
+
ordinary,
|
|
811
|
+
{
|
|
812
|
+
...placed,
|
|
813
|
+
"background-color": colour,
|
|
814
|
+
"clip-path": lined(name, line),
|
|
815
|
+
},
|
|
816
|
+
name,
|
|
817
|
+
);
|
|
535
818
|
}
|
|
536
819
|
}
|
|
537
820
|
|
|
@@ -543,10 +826,17 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
543
826
|
"clip-path": plain,
|
|
544
827
|
});
|
|
545
828
|
for (const [name, line] of Object.entries(LINES)) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
829
|
+
if (!lineOrdinaries(name).includes(ordinary)) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
utilities[`.${ordinary}-${name}-${fur}`] = ordinaryRules(
|
|
833
|
+
ordinary,
|
|
834
|
+
{
|
|
835
|
+
...pseudo,
|
|
836
|
+
"clip-path": lined(name, line),
|
|
837
|
+
},
|
|
838
|
+
name,
|
|
839
|
+
);
|
|
550
840
|
}
|
|
551
841
|
}
|
|
552
842
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tailwindcss -i docs/app.css -o docs/site.css",
|
|
6
6
|
"dev": "tailwindcss -i docs/app.css -o docs/site.css --watch",
|
|
7
|
-
"test": "node test/modes.mjs && npm test -w packages/eslint-plugin",
|
|
7
|
+
"test": "node test/modes.mjs && node test/drawings.mjs && node test/docs.mjs && npm run drawings -- --check && npm test -w packages/eslint-plugin",
|
|
8
|
+
"drawings": "node drawings/build.mjs",
|
|
8
9
|
"test:render": "node test/render.mjs",
|
|
9
10
|
"dev:watch": "tailwindcss -i docs/app.css -o docs/site.css --watch=always & python3 -m http.server ${PORT:-8000} -d docs",
|
|
10
11
|
"lint": "eslint .",
|
|
11
12
|
"format": "prettier --write .",
|
|
12
|
-
"format:check": "prettier --check ."
|
|
13
|
+
"format:check": "prettier --check .",
|
|
14
|
+
"prepare": "husky"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
15
17
|
"tailwindcss",
|
|
@@ -26,10 +28,12 @@
|
|
|
26
28
|
"license": "MIT",
|
|
27
29
|
"description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@grailwind/eslint-plugin": "^0.1.0-beta.
|
|
31
|
+
"@grailwind/eslint-plugin": "^0.1.0-beta.4",
|
|
30
32
|
"@html-eslint/parser": "^0.67.0",
|
|
31
33
|
"@tailwindcss/cli": "^4.3.3",
|
|
32
34
|
"eslint": "^10.14.0",
|
|
35
|
+
"husky": "^9.1.7",
|
|
36
|
+
"lint-staged": "^17.6.0",
|
|
33
37
|
"playwright": "~1.64.1",
|
|
34
38
|
"prettier": "^3.9.10",
|
|
35
39
|
"tailwindcss": "^4.3.3"
|
|
@@ -57,6 +61,7 @@
|
|
|
57
61
|
"ordinaries.js",
|
|
58
62
|
"charges.js",
|
|
59
63
|
"furs.js",
|
|
64
|
+
"paths.js",
|
|
60
65
|
"strict.css",
|
|
61
66
|
"palette-reset.css",
|
|
62
67
|
"CHANGELOG.md"
|
|
@@ -71,5 +76,8 @@
|
|
|
71
76
|
},
|
|
72
77
|
"bugs": {
|
|
73
78
|
"url": "https://github.com/pafirmin/grailwind/issues"
|
|
79
|
+
},
|
|
80
|
+
"lint-staged": {
|
|
81
|
+
"*": "prettier --write --ignore-unknown"
|
|
74
82
|
}
|
|
75
83
|
}
|