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/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 { BETWEEN, FURS, OF_THE_FIRST, ON, REVERSIBLE, TINCTURES, VOIDABLE } from "./data.js";
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, 25], [100, 75], [100, 95], [50, 45], [0, 95], [0, 75]],
34
+ chevron: [[50, 22], [100, 72], [100, 98], [50, 48], [0, 98], [0, 72]],
22
35
  cross: [
23
- [40, 0], [60, 0], [60, 35], [100, 35], [100, 55], [60, 55],
24
- [60, 100], [40, 100], [40, 55], [0, 55], [0, 35], [40, 35],
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], [15, 0], [50, 35], [85, 0], [100, 0], [100, 15], [65, 50], [100, 85],
28
- [100, 100], [85, 100], [50, 65], [15, 100], [0, 100], [0, 85], [35, 50], [0, 15],
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], [15, 0], [50, 35], [85, 0], [100, 0], [100, 15],
32
- [60, 55], [60, 100], [40, 100], [40, 55], [0, 15],
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: [[20, 0], [80, 0], [50, 85]],
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). Stripes repaint the fimbriation where one arm's band runs on into
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%. The one diagonal band
152
- // a tile that isn't square can take, as its diagonal isn't a line of constant x ± y.
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", 75 - S, 75),
168
- stripe("u-v", 25, 25 + S),
169
- band("u+v", 75, 95),
170
- band("u-v", 5, 25),
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
- stripe("x", 60, 60 + S, [0, 55, 100, 100]),
176
- stripe("x", 40 - S, 40, [0, 55, 100, 100]),
177
- stripe("u+v", 115, 115 + S, [60, 30, 100, 70]),
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", [20, 0, 50, 85]), half("u+v", "start", [50, 0, 80, 85]), seam(83)],
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: 3.8, cross: 5, saltire: 5.8, pall: 5, pile: 8,
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
- // Each line maps an edge to arcs; `inward` is the sweep that bulges into the ordinary.
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
- // `extend` moves the sides on the shield's edge out, as `outer` does; the lined sides stay put.
331
- const shape = (vertices, line, scallop, extend) => {
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
- const onVars = ({ size, counts }) => {
399
- const vars = { "--on-size": `${size}%` };
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(BETWEEN[ordinary])),
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
- plain,
483
- chief
484
- ? `${shape(vertices, line, SCALLOP, outer)} content-box`
485
- : shape(vertices, line, SCALLOPS[ordinary] ?? SCALLOP),
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
- utilities[`.${ordinary}-${name}-${tincture}`] = ordinaryRules(ordinary, {
531
- ...placed,
532
- "background-color": colour,
533
- "clip-path": lined(line),
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
- utilities[`.${ordinary}-${name}-${fur}`] = ordinaryRules(ordinary, {
547
- ...pseudo,
548
- "clip-path": lined(line),
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.6.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.2",
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
  }