grailwind 0.8.1 → 0.9.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 +46 -1
- package/README.md +13 -8
- package/charges.js +36 -13
- package/data.js +95 -14
- package/divisions.js +48 -12
- package/furs.js +181 -23
- package/grailwind.css +178 -30
- package/ordinaries.js +452 -17
- package/package.json +2 -2
- package/paths.js +8 -0
package/ordinaries.js
CHANGED
|
@@ -5,6 +5,9 @@ import plugin from "tailwindcss/plugin";
|
|
|
5
5
|
import {
|
|
6
6
|
BETWEEN,
|
|
7
7
|
BETWEEN_LINED,
|
|
8
|
+
BORDURE_LINES,
|
|
9
|
+
CHARGES,
|
|
10
|
+
FRAMES,
|
|
8
11
|
FURS,
|
|
9
12
|
LINES as LINE_NAMES,
|
|
10
13
|
lineOrdinaries,
|
|
@@ -15,7 +18,7 @@ import {
|
|
|
15
18
|
TINCTURES,
|
|
16
19
|
VOIDABLE,
|
|
17
20
|
} from "./data.js";
|
|
18
|
-
import { furBackground } from "./furs.js";
|
|
21
|
+
import { furBackground, ringBackground } from "./furs.js";
|
|
19
22
|
import { PATHS } from "./paths.js";
|
|
20
23
|
|
|
21
24
|
// prettier-ignore
|
|
@@ -61,17 +64,25 @@ const polygon = (vertices) => `polygon(${vertices.map(point).join(", ")})`;
|
|
|
61
64
|
|
|
62
65
|
const PSEUDO = { chief: "&::after" };
|
|
63
66
|
|
|
67
|
+
// A split field's parts: its children that aren't over all of it. Written out in grailwind.css.
|
|
68
|
+
const PART = ":not(.charges, .inescutcheon, .canton)";
|
|
69
|
+
|
|
64
70
|
// The chief surmounts a bordure.
|
|
65
71
|
const INSET = "var(--bordure-inset)";
|
|
72
|
+
// Each side's step-out. A bordure on a per-pale part sets its palar side's to 0.
|
|
73
|
+
const DEXTER = `var(--bordure-inset-dexter, ${INSET})`;
|
|
74
|
+
const SINISTER = `var(--bordure-inset-sinister, ${INSET})`;
|
|
66
75
|
const OVER_BORDURE = {
|
|
67
76
|
"--bordure-inset": "inherit",
|
|
68
|
-
inset:
|
|
69
|
-
|
|
77
|
+
"--bordure-inset-dexter": "inherit",
|
|
78
|
+
"--bordure-inset-sinister": "inherit",
|
|
79
|
+
inset: `calc(-1 * ${INSET}) calc(-1 * ${SINISTER}) 0 calc(-1 * ${DEXTER})`,
|
|
80
|
+
padding: `${INSET} ${SINISTER} 0 ${DEXTER}`,
|
|
70
81
|
"background-origin": "content-box",
|
|
71
82
|
};
|
|
72
83
|
// A vertex on the shield's top or side edges, moved out to the bordure's outer edge.
|
|
73
84
|
const outer = ([x, y]) => {
|
|
74
|
-
const ox = x === 0 ? `calc(-1 * ${
|
|
85
|
+
const ox = x === 0 ? `calc(-1 * ${DEXTER})` : x === 100 ? `calc(100% + ${SINISTER})` : pct(x);
|
|
75
86
|
return `${ox} ${y === 0 ? `calc(-1 * ${INSET})` : pct(y)}`;
|
|
76
87
|
};
|
|
77
88
|
|
|
@@ -603,6 +614,303 @@ const pathShape = (segments) => {
|
|
|
603
614
|
return `shape(from ${at([startX, startY])}, ${commands.join(", ")}, close)`;
|
|
604
615
|
};
|
|
605
616
|
|
|
617
|
+
// Polygon points per curve or arc, where shape() isn't supported.
|
|
618
|
+
const SAMPLES = 16;
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* A fraction of the frame across or down, in a box inset by `inset` on every side.
|
|
622
|
+
* @param {number} fraction 0 at the frame's top or left edge, 1 at its bottom or right.
|
|
623
|
+
* @param {string|null} inset A CSS length, or null for the frame's own edge.
|
|
624
|
+
* @returns {string} A CSS length-percentage.
|
|
625
|
+
*/
|
|
626
|
+
const insetCoordinate = (fraction, inset) => {
|
|
627
|
+
if (!inset) {
|
|
628
|
+
return pct(fraction * 100);
|
|
629
|
+
}
|
|
630
|
+
if (num(fraction) === 0) {
|
|
631
|
+
return inset;
|
|
632
|
+
}
|
|
633
|
+
if (num(fraction) === 1) {
|
|
634
|
+
return `calc(100% - ${inset})`;
|
|
635
|
+
}
|
|
636
|
+
return `calc(${inset} + ${num(fraction)} * (100% - 2 * ${inset}))`;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* A drawing's point, in real units, in the frame inset by `inset`.
|
|
641
|
+
* @param {number[]} point [x, y] in real units (W × H).
|
|
642
|
+
* @param {string|null} inset A CSS length, or null for the frame's own edge.
|
|
643
|
+
* @returns {string} A CSS position.
|
|
644
|
+
*/
|
|
645
|
+
const insetPoint = ([x, y], inset) =>
|
|
646
|
+
`${insetCoordinate(x / W, inset)} ${insetCoordinate(y / H, inset)}`;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* An arc's radius, scaled with the inset box as its points are.
|
|
650
|
+
* @param {number} radius In real units.
|
|
651
|
+
* @param {number} across W for a horizontal radius, H for a vertical one.
|
|
652
|
+
* @param {string|null} inset A CSS length, or null for the frame's own edge.
|
|
653
|
+
* @returns {string} A CSS length-percentage.
|
|
654
|
+
*/
|
|
655
|
+
const insetRadius = (radius, across, inset) =>
|
|
656
|
+
inset ? `calc(${num(radius / across)} * (100% - 2 * ${inset}))` : pct((radius * 100) / across);
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* A drawn outline's shape() commands after its start, in the frame inset by `inset`.
|
|
660
|
+
* @param {Array[]} segments The drawing's segments, as in paths.js, ending in ["Z"].
|
|
661
|
+
* @param {string|null} inset A CSS length, or null for the frame's own edge.
|
|
662
|
+
* @returns {string[]} The commands, ending in "close".
|
|
663
|
+
*/
|
|
664
|
+
const insetCommands = (segments, inset) =>
|
|
665
|
+
segments.slice(1).map(([command, ...n]) => {
|
|
666
|
+
switch (command) {
|
|
667
|
+
case "L":
|
|
668
|
+
return `line to ${insetPoint([n[0], n[1]], inset)}`;
|
|
669
|
+
case "C":
|
|
670
|
+
return `curve to ${insetPoint([n[4], n[5]], inset)} with ${insetPoint([n[0], n[1]], inset)} / ${insetPoint([n[2], n[3]], inset)}`;
|
|
671
|
+
case "A": {
|
|
672
|
+
const [rx, ry, rotation, large, sweep, x, y] = n;
|
|
673
|
+
return [
|
|
674
|
+
`arc to ${insetPoint([x, y], inset)} of ${insetRadius(rx, W, inset)} ${insetRadius(ry, H, inset)}`,
|
|
675
|
+
sweep ? "cw" : "ccw",
|
|
676
|
+
large ? "large" : "",
|
|
677
|
+
rotation ? `rotate ${rotation}deg` : "",
|
|
678
|
+
]
|
|
679
|
+
.filter(Boolean)
|
|
680
|
+
.join(" ");
|
|
681
|
+
}
|
|
682
|
+
case "Z":
|
|
683
|
+
return "close";
|
|
684
|
+
default:
|
|
685
|
+
throw new Error(`a frame's outline can't use ${command}`);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Points along an SVG arc, after its start, from its endpoint form (SVG 1.1, F.6.5).
|
|
691
|
+
* @param {number[]} from The arc's start, in real units.
|
|
692
|
+
* @param {number[]} arc [rx, ry, rotation, large, sweep, x, y], as in paths.js.
|
|
693
|
+
* @returns {number[][]} SAMPLES points, the last at the arc's end.
|
|
694
|
+
*/
|
|
695
|
+
const arcPoints = (from, [rx, ry, rotation, large, sweep, x, y]) => {
|
|
696
|
+
const angle = (rotation * Math.PI) / 180;
|
|
697
|
+
const [cos, sin] = [Math.cos(angle), Math.sin(angle)];
|
|
698
|
+
const [dx, dy] = [(from[0] - x) / 2, (from[1] - y) / 2];
|
|
699
|
+
const [x1, y1] = [cos * dx + sin * dy, -sin * dx + cos * dy];
|
|
700
|
+
// Radii too small to reach are scaled up, as SVG does.
|
|
701
|
+
const scale = Math.max(1, Math.sqrt((x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry)));
|
|
702
|
+
const [radiusX, radiusY] = [rx * scale, ry * scale];
|
|
703
|
+
const numerator = Math.max(
|
|
704
|
+
0,
|
|
705
|
+
radiusX ** 2 * radiusY ** 2 - radiusX ** 2 * y1 ** 2 - radiusY ** 2 * x1 ** 2,
|
|
706
|
+
);
|
|
707
|
+
const sign = large === sweep ? -1 : 1;
|
|
708
|
+
const factor =
|
|
709
|
+
sign * Math.sqrt(numerator / (radiusX ** 2 * y1 ** 2 + radiusY ** 2 * x1 ** 2) || 0);
|
|
710
|
+
const [cx1, cy1] = [(factor * radiusX * y1) / radiusY, (-factor * radiusY * x1) / radiusX];
|
|
711
|
+
const centre = [
|
|
712
|
+
cos * cx1 - sin * cy1 + (from[0] + x) / 2,
|
|
713
|
+
sin * cx1 + cos * cy1 + (from[1] + y) / 2,
|
|
714
|
+
];
|
|
715
|
+
const start = Math.atan2((y1 - cy1) / radiusY, (x1 - cx1) / radiusX);
|
|
716
|
+
let sweepAngle = Math.atan2((-y1 - cy1) / radiusY, (-x1 - cx1) / radiusX) - start;
|
|
717
|
+
if (sweep && sweepAngle < 0) {
|
|
718
|
+
sweepAngle += 2 * Math.PI;
|
|
719
|
+
} else if (!sweep && sweepAngle > 0) {
|
|
720
|
+
sweepAngle -= 2 * Math.PI;
|
|
721
|
+
}
|
|
722
|
+
return Array.from({ length: SAMPLES }, (_, i) => {
|
|
723
|
+
const theta = start + (sweepAngle * (i + 1)) / SAMPLES;
|
|
724
|
+
const [ex, ey] = [radiusX * Math.cos(theta), radiusY * Math.sin(theta)];
|
|
725
|
+
return [centre[0] + cos * ex - sin * ey, centre[1] + sin * ex + cos * ey];
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* A drawn outline as polygon points, its curves and arcs sampled.
|
|
731
|
+
* @param {Array[]} segments The drawing's segments, as in paths.js.
|
|
732
|
+
* @param {number} [samples] Points per curve.
|
|
733
|
+
* @returns {number[][]} Points in real units, starting at the move.
|
|
734
|
+
*/
|
|
735
|
+
const outlinePoints = (segments, samples = SAMPLES) => {
|
|
736
|
+
const points = [[segments[0][1], segments[0][2]]];
|
|
737
|
+
for (const [command, ...n] of segments.slice(1)) {
|
|
738
|
+
const from = points.at(-1);
|
|
739
|
+
if (command === "L") {
|
|
740
|
+
points.push([n[0], n[1]]);
|
|
741
|
+
} else if (command === "C") {
|
|
742
|
+
for (let i = 1; i <= samples; i++) {
|
|
743
|
+
const t = i / samples;
|
|
744
|
+
const [a, b, c, d] = [(1 - t) ** 3, 3 * (1 - t) ** 2 * t, 3 * (1 - t) * t ** 2, t ** 3];
|
|
745
|
+
points.push([
|
|
746
|
+
a * from[0] + b * n[0] + c * n[2] + d * n[4],
|
|
747
|
+
a * from[1] + b * n[1] + c * n[3] + d * n[5],
|
|
748
|
+
]);
|
|
749
|
+
}
|
|
750
|
+
} else if (command === "A") {
|
|
751
|
+
points.push(...arcPoints(from, n));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return points;
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// A bordure's width, as a fraction of its frame's. grailwind.css has it as `calc(100cqw / 8)` on a
|
|
758
|
+
// frame and a third of that on a canton, which test/modes.mjs checks. A lined bordure is drawn for
|
|
759
|
+
// this width.
|
|
760
|
+
const BORDURE_WIDTH = 1 / 8;
|
|
761
|
+
// How deep a lined bordure's scallops are, as a fraction of its frame's width.
|
|
762
|
+
const SCALLOP_DEPTH = 0.037;
|
|
763
|
+
// The same as a fraction of the bordure's width: an engrailed field's border narrows by it.
|
|
764
|
+
const BORDURE_DEPTH = SCALLOP_DEPTH / BORDURE_WIDTH;
|
|
765
|
+
// A lined bordure's scallop length in real units, as SCALLOP is an ordinary's: a run between
|
|
766
|
+
// corners gets one scallop per this much of its length inside the bordure.
|
|
767
|
+
const BORDURE_SCALLOP = 12;
|
|
768
|
+
// An engrailed bordure's top corners are round lobes, their radius this many times the scallops'
|
|
769
|
+
// depth (at least 1), centred inside the corner so they reach the depth past each edge, as the
|
|
770
|
+
// bites do. Every other corner is a cusp.
|
|
771
|
+
const CORNER_LOBE = 1.8;
|
|
772
|
+
// Points per curve when a run is cut into scallops by arc length.
|
|
773
|
+
const RUN_SAMPLES = 256;
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* @param {number[]} vector A vector.
|
|
777
|
+
* @returns {number[]} The unit vector the same way.
|
|
778
|
+
*/
|
|
779
|
+
const unit = ([x, y]) => [x / Math.hypot(x, y), y / Math.hypot(x, y)];
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Points evenly spaced along a polyline, one scallop apart.
|
|
783
|
+
* @param {number[][]} polyline Points of a drawing, in real units.
|
|
784
|
+
* @param {number} start How far along it to start.
|
|
785
|
+
* @param {number} [end] How far along it to end, by default its end.
|
|
786
|
+
* @returns {number[][]} The chords' ends, from `start` to `end`.
|
|
787
|
+
*/
|
|
788
|
+
const chordEnds = (polyline, start, end) => {
|
|
789
|
+
const lengths = [0];
|
|
790
|
+
for (let i = 1; i < polyline.length; i++) {
|
|
791
|
+
const [from, to] = [polyline[i - 1], polyline[i]];
|
|
792
|
+
lengths.push(lengths[i - 1] + Math.hypot(to[0] - from[0], to[1] - from[1]));
|
|
793
|
+
}
|
|
794
|
+
const last = end ?? lengths.at(-1);
|
|
795
|
+
const count = Math.max(
|
|
796
|
+
1,
|
|
797
|
+
Math.round(((last - start) * (1 - 2 * BORDURE_WIDTH)) / BORDURE_SCALLOP),
|
|
798
|
+
);
|
|
799
|
+
return Array.from({ length: count + 1 }, (_, part) => {
|
|
800
|
+
const distance = start + ((last - start) * part) / count;
|
|
801
|
+
const i = Math.max(
|
|
802
|
+
1,
|
|
803
|
+
lengths.findIndex((length) => length >= distance),
|
|
804
|
+
);
|
|
805
|
+
const fraction = (distance - lengths[i - 1]) / (lengths[i] - lengths[i - 1]);
|
|
806
|
+
return polyline[i - 1].map((v, axis) => v + (polyline[i][axis] - v) * fraction);
|
|
807
|
+
});
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* A lined bordure's inner edge, clockwise, in real units. Its frame's outline, inside the bordure,
|
|
812
|
+
* is cut at the top corners and the base point into runs, and each run into equal chords by its
|
|
813
|
+
* length in the drawing. Each chord is a scallop, a cubic whose controls are its ends moved 4/3 of
|
|
814
|
+
* the depth along the chord's normal, so it peaks at the depth and leaves its cusps square to the
|
|
815
|
+
* chord. The left side is the right's mirror.
|
|
816
|
+
* @param {Array[]} segments A heater's drawing, as in paths.js: two lines, then two curves.
|
|
817
|
+
* @param {number} sweep 1 for engrailed, bulging toward the frame's edge; -1 for invected.
|
|
818
|
+
* @returns {{ from: number[], c1: number[], c2: number[], to: number[], kind: string }[]} Its
|
|
819
|
+
* cubics from the top edge's first cusp, each from where the last ends, and the last back to the
|
|
820
|
+
* first. `kind` is "scallop" or "lobe".
|
|
821
|
+
*/
|
|
822
|
+
const bordurePath = (segments, sweep) => {
|
|
823
|
+
const inset = BORDURE_WIDTH * W;
|
|
824
|
+
const depth = SCALLOP_DEPTH * W;
|
|
825
|
+
const scale = [(W - 2 * inset) / W, (H - 2 * inset) / H];
|
|
826
|
+
const inside = (point) => point.map((v, axis) => inset + v * scale[axis]);
|
|
827
|
+
const scallops = (ends) =>
|
|
828
|
+
ends.slice(1).map((end, i) => {
|
|
829
|
+
const start = ends[i];
|
|
830
|
+
const normal = unit([end[1] - start[1], start[0] - end[0]]);
|
|
831
|
+
const push = (point) => point.map((v, axis) => v + (normal[axis] * sweep * 4 * depth) / 3);
|
|
832
|
+
const [from, to] = [inside(start), inside(end)];
|
|
833
|
+
return { from, c1: push(from), c2: push(to), to, kind: "scallop" };
|
|
834
|
+
});
|
|
835
|
+
// The top-right lobe: its circle's centre is `centre` inside the corner each way, and its cusps
|
|
836
|
+
// are `reach` from the corner along each edge. It runs clockwise from the top cusp through the
|
|
837
|
+
// apex to the side cusp. Without a lobe, the corner is the cusp.
|
|
838
|
+
const radius = CORNER_LOBE * depth;
|
|
839
|
+
const centre = radius - depth;
|
|
840
|
+
const reach = sweep > 0 ? centre + Math.sqrt(radius ** 2 - centre ** 2) : 0;
|
|
841
|
+
const corner = inside([W, 0]);
|
|
842
|
+
const middle = [corner[0] - centre, corner[1] + centre];
|
|
843
|
+
const onCircle = (angle) => [
|
|
844
|
+
middle[0] + radius * Math.cos(angle),
|
|
845
|
+
middle[1] + radius * Math.sin(angle),
|
|
846
|
+
];
|
|
847
|
+
const [first, last] = [Math.atan2(-centre, centre - reach), Math.atan2(reach - centre, centre)];
|
|
848
|
+
const apex = (first + last) / 2;
|
|
849
|
+
const handle = (4 / 3) * Math.tan((last - first) / 8) * radius;
|
|
850
|
+
const toward = (angle, along) =>
|
|
851
|
+
onCircle(angle).map(
|
|
852
|
+
(v, axis) => v + [-Math.sin(angle), Math.cos(angle)][axis] * handle * along,
|
|
853
|
+
);
|
|
854
|
+
const lobe = [
|
|
855
|
+
{ from: onCircle(first), c1: toward(first, 1), c2: toward(apex, -1), to: onCircle(apex) },
|
|
856
|
+
{ from: onCircle(apex), c1: toward(apex, 1), c2: toward(last, -1), to: onCircle(last) },
|
|
857
|
+
].map((piece) => ({ ...piece, kind: "lobe" }));
|
|
858
|
+
const outline = outlinePoints(segments, RUN_SAMPLES);
|
|
859
|
+
const top = chordEnds(outline.slice(0, 2), reach / scale[0], W - reach / scale[0]);
|
|
860
|
+
const side = outline.slice(1, 3 + RUN_SAMPLES);
|
|
861
|
+
const right = [...(sweep > 0 ? lobe : []), ...scallops(chordEnds(side, reach / scale[1]))];
|
|
862
|
+
const flip = ([x, y]) => [W - x, y];
|
|
863
|
+
const left = right
|
|
864
|
+
.map(({ from, c1, c2, to, kind }) => ({
|
|
865
|
+
from: flip(to),
|
|
866
|
+
c1: flip(c2),
|
|
867
|
+
c2: flip(c1),
|
|
868
|
+
to: flip(from),
|
|
869
|
+
kind,
|
|
870
|
+
}))
|
|
871
|
+
.reverse();
|
|
872
|
+
return [...scallops(top), ...right, ...left];
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* A frame's clip-paths from its drawing: the outline, and rings just inside it. Each is a polygon
|
|
877
|
+
* fallback, then the shape().
|
|
878
|
+
* @param {Array[]} segments The drawing's segments, as in paths.js.
|
|
879
|
+
* @returns {{ outer: string[], ring: (inset: string) => string[], lined: (inset: string, sweep: number) => string[] }}
|
|
880
|
+
* The outline, and a ring `inset` wide: the outline minus the outline mapped into a box inset by
|
|
881
|
+
* `inset`. A lined ring has its inner edge scalloped as bordurePath() draws it, bulging outward
|
|
882
|
+
* for a `sweep` of 1 and inward for -1. Its shape() is drawn for an `inset` of BORDURE_WIDTH,
|
|
883
|
+
* and its polygon is the plain ring. The outline must be a heater's.
|
|
884
|
+
*/
|
|
885
|
+
const frameOutline = (segments) => {
|
|
886
|
+
const points = outlinePoints(segments);
|
|
887
|
+
const start = [segments[0][1], segments[0][2]];
|
|
888
|
+
const loop = (inset) => [...points, points[0]].map((p) => insetPoint(p, inset)).join(", ");
|
|
889
|
+
const ring = (inset) => [
|
|
890
|
+
`polygon(evenodd, ${loop(null)}, ${loop(inset)})`,
|
|
891
|
+
`shape(evenodd from ${insetPoint(start, null)}, ${insetCommands(segments, null).join(", ")}, move to ${insetPoint(start, inset)}, ${insetCommands(segments, inset).join(", ")})`,
|
|
892
|
+
];
|
|
893
|
+
return {
|
|
894
|
+
outer: [
|
|
895
|
+
`polygon(${points.map((p) => insetPoint(p, null)).join(", ")})`,
|
|
896
|
+
`shape(from ${insetPoint(start, null)}, ${insetCommands(segments, null).join(", ")})`,
|
|
897
|
+
],
|
|
898
|
+
ring,
|
|
899
|
+
lined: (inset, sweep) => {
|
|
900
|
+
// The inner subpath runs counter-clockwise, against the outer, so a nonzero fill leaves the
|
|
901
|
+
// field unpainted and still paints where invected scallops overlap.
|
|
902
|
+
const pieces = bordurePath(segments, sweep);
|
|
903
|
+
const inner = pieces
|
|
904
|
+
.toReversed()
|
|
905
|
+
.map(({ from, c1, c2 }) => `curve to ${at(from)} with ${at(c2)} / ${at(c1)}`);
|
|
906
|
+
return [
|
|
907
|
+
ring(inset)[0],
|
|
908
|
+
`shape(nonzero from ${insetPoint(start, null)}, ${insetCommands(segments, null).join(", ")}, move to ${at(pieces[0].from)}, ${inner.join(", ")}, close)`,
|
|
909
|
+
];
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
};
|
|
913
|
+
|
|
606
914
|
// The same shape upside down, for an ordinary's reversed form.
|
|
607
915
|
const flipPath = (segments) =>
|
|
608
916
|
segments.map(([command, ...n]) => {
|
|
@@ -621,9 +929,14 @@ const flipPath = (segments) =>
|
|
|
621
929
|
|
|
622
930
|
// Drawn shapes by `ordinary-line`, each reversible one also upside down as `ordinary-reversed-line`
|
|
623
931
|
// unless it was drawn too. Upper-only battlements would flip onto the lower edges, so those aren't.
|
|
624
|
-
|
|
932
|
+
// Shield outlines, such as the heater, have no line and aren't ordinaries.
|
|
933
|
+
const DRAWN = {};
|
|
625
934
|
for (const key of Object.keys(PATHS)) {
|
|
626
935
|
const name = LINE_NAMES.find((line) => key.endsWith(`-${line}`));
|
|
936
|
+
if (!name) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
DRAWN[key] = PATHS[key];
|
|
627
940
|
const ordinary = key.slice(0, -name.length - 1);
|
|
628
941
|
const reversedKey = `${ordinary}-reversed-${name}`;
|
|
629
942
|
const upperOnly = name === "embattled" && UPPER_ONLY.includes(ordinary);
|
|
@@ -659,8 +972,11 @@ const mergeLined = (ordinary, line) => {
|
|
|
659
972
|
|
|
660
973
|
const onVars = ({ size, counts }, scale = 1) => {
|
|
661
974
|
const vars = { "--on-size": `${+(size * scale).toFixed(2)}%` };
|
|
662
|
-
for (const [count,
|
|
663
|
-
|
|
975
|
+
for (const [count, override] of Object.entries(counts)) {
|
|
976
|
+
if (override.size !== undefined) {
|
|
977
|
+
vars[`--on-${count}-size`] = `${+(override.size * scale).toFixed(2)}%`;
|
|
978
|
+
}
|
|
979
|
+
override.points.forEach(([left, top], i) => {
|
|
664
980
|
vars[`--on-${count}-${i + 1}-x`] = `${left}%`;
|
|
665
981
|
vars[`--on-${count}-${i + 1}-y`] = `${top}%`;
|
|
666
982
|
});
|
|
@@ -700,18 +1016,133 @@ const PAINTS = [
|
|
|
700
1016
|
[OF_THE_FIRST, "var(--the-first, transparent)"],
|
|
701
1017
|
];
|
|
702
1018
|
|
|
703
|
-
|
|
1019
|
+
// Any frame, e.g. the heater, and a frame's field as `${FRAME} > *`.
|
|
1020
|
+
const FRAME = `:is(${FRAMES.map((frame) => `.${frame}`).join(", ")})`;
|
|
1021
|
+
|
|
1022
|
+
// The outline's width, just inside the frame's edge.
|
|
1023
|
+
const OUTLINE_WIDTH = "2px";
|
|
1024
|
+
|
|
1025
|
+
export {
|
|
1026
|
+
BORDURE_DEPTH,
|
|
1027
|
+
BORDURE_WIDTH,
|
|
1028
|
+
bordurePath,
|
|
1029
|
+
CORNER_LOBE,
|
|
1030
|
+
outlinePoints,
|
|
1031
|
+
SCALLOP_DEPTH,
|
|
1032
|
+
draftOutline,
|
|
1033
|
+
DRAWN,
|
|
1034
|
+
flipPath,
|
|
1035
|
+
frameOutline,
|
|
1036
|
+
generatedShape,
|
|
1037
|
+
ORDINARIES,
|
|
1038
|
+
pathShape,
|
|
1039
|
+
svgPath,
|
|
1040
|
+
};
|
|
704
1041
|
|
|
705
1042
|
export default plugin(({ addBase, addUtilities, theme }) => {
|
|
706
1043
|
// Unset on an inescutcheon, a canton or a part of a divided shield, so it can't inherit its
|
|
707
1044
|
// shield's layout. In the base layer, so an ordinary on it still sets its own.
|
|
708
1045
|
addBase({
|
|
709
|
-
|
|
1046
|
+
[`.inescutcheon, .inescutcheon > *, .canton, :is(.heater, .heater *):is(.quarterly, .per-pale, .per-fess) > ${PART}`]:
|
|
710
1047
|
Object.fromEntries(layoutVars().map((name) => [name, "initial"])),
|
|
711
1048
|
});
|
|
712
1049
|
|
|
713
1050
|
const utilities = {};
|
|
714
1051
|
|
|
1052
|
+
// The frame's shape. Inside it, from the bottom: the field's ordinaries (1), its charges, the
|
|
1053
|
+
// bordure's ring (2, set by bordure-*), the chief (3) and its charges (4), a canton or
|
|
1054
|
+
// inescutcheon (5), and the outline (6). Every frame is drawn as a heater.
|
|
1055
|
+
const heater = frameOutline(PATHS.heater);
|
|
1056
|
+
for (const frame of FRAMES) {
|
|
1057
|
+
utilities[`.${frame}`] = {
|
|
1058
|
+
"clip-path": heater.outer,
|
|
1059
|
+
"&::before": {
|
|
1060
|
+
content: '""',
|
|
1061
|
+
position: "absolute",
|
|
1062
|
+
inset: "0",
|
|
1063
|
+
"z-index": "6",
|
|
1064
|
+
"background-color": "var(--color-sable)",
|
|
1065
|
+
"clip-path": heater.ring(OUTLINE_WIDTH),
|
|
1066
|
+
},
|
|
1067
|
+
"&::after": {
|
|
1068
|
+
position: "absolute",
|
|
1069
|
+
inset: "0",
|
|
1070
|
+
"z-index": "2",
|
|
1071
|
+
"clip-path": heater.ring("var(--bordure-width)"),
|
|
1072
|
+
// A bordure on a per-pale part sets --ring-dexter or --ring-sinister: the ring on that half.
|
|
1073
|
+
"background-image":
|
|
1074
|
+
"linear-gradient(to right, var(--ring-dexter, transparent) 50%, var(--ring-sinister, transparent) 50%)",
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// On a frame's field the border is transparent and the ring paints the bordure. An engrailed
|
|
1080
|
+
// ring bites into the field, so the field's border is narrower by the scallops' depth and the
|
|
1081
|
+
// field grows into the bites. Anywhere else the border is the bordure, plain.
|
|
1082
|
+
//
|
|
1083
|
+
// A bordure on a per-pale part is the ring on that half (see the frame's ::after), and the part
|
|
1084
|
+
// has no border on the palar side, so the bordure stops at the line as the rule says. Limits:
|
|
1085
|
+
// a part's bordure is always plain, a fur's ground only, and of-the-first takes the shield's
|
|
1086
|
+
// first. A bordure on both the field and a part isn't handled. A chief on a bordured part goes
|
|
1087
|
+
// under the ring, as the part is a stacking context; it steps out only by the sides that have
|
|
1088
|
+
// a bordure (--bordure-inset-dexter and -sinister).
|
|
1089
|
+
const BORDER = "var(--bordure-width, 0.75rem)";
|
|
1090
|
+
const narrowed = `calc(var(--bordure-width) * ${num(1 - BORDURE_DEPTH)})`;
|
|
1091
|
+
// Each part, and the side it has no bordure on: its half of the ring, its palar border and the
|
|
1092
|
+
// step-out that's 0 there.
|
|
1093
|
+
const PARTS = {
|
|
1094
|
+
[`nth-child(1 of ${PART})`]: ["dexter", "border-right-width", "--bordure-inset-sinister"],
|
|
1095
|
+
[`nth-last-child(1 of ${PART})`]: ["sinister", "border-left-width", "--bordure-inset-dexter"],
|
|
1096
|
+
};
|
|
1097
|
+
const furPaints = Object.keys(FURS).map((fur) => ({
|
|
1098
|
+
name: fur,
|
|
1099
|
+
colour: `var(--color-${FURS[fur][0]})`,
|
|
1100
|
+
ring: ringBackground(fur, theme),
|
|
1101
|
+
}));
|
|
1102
|
+
const tinctures = PAINTS.map(([tincture, colour]) => ({
|
|
1103
|
+
name: tincture,
|
|
1104
|
+
colour,
|
|
1105
|
+
ring: {
|
|
1106
|
+
"background-color": tincture === OF_THE_FIRST ? "var(--frame-first, transparent)" : colour,
|
|
1107
|
+
},
|
|
1108
|
+
}));
|
|
1109
|
+
for (const line of ["", ...BORDURE_LINES]) {
|
|
1110
|
+
const sweep = line === "engrailed" ? 1 : -1;
|
|
1111
|
+
const clip = line && heater.lined("var(--bordure-width)", sweep);
|
|
1112
|
+
// Plain tinctures are in grailwind.css.
|
|
1113
|
+
for (const { name, colour, ring } of line ? [...tinctures, ...furPaints] : furPaints) {
|
|
1114
|
+
const ringColour = ring["background-color"];
|
|
1115
|
+
const part = {};
|
|
1116
|
+
for (const [child, [side, open, inset]] of Object.entries(PARTS)) {
|
|
1117
|
+
part[`${FRAME} > .per-pale > &:${child}`] = {
|
|
1118
|
+
"border-color": "transparent",
|
|
1119
|
+
[open]: "0",
|
|
1120
|
+
[inset]: "0px",
|
|
1121
|
+
};
|
|
1122
|
+
part[`${FRAME}:has(> .per-pale > &:${child})::after`] = {
|
|
1123
|
+
content: '""',
|
|
1124
|
+
[`--ring-${side}`]: ringColour,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
utilities[line ? `.bordure-${line}-${name}` : `.bordure-${name}`] = {
|
|
1128
|
+
"border-style": "solid",
|
|
1129
|
+
"border-width": BORDER,
|
|
1130
|
+
"border-color": colour,
|
|
1131
|
+
"--bordure-inset": BORDER,
|
|
1132
|
+
":is(.heater, .inescutcheon) > &": {
|
|
1133
|
+
"border-color": "transparent",
|
|
1134
|
+
...(sweep === 1 && { "border-width": narrowed, "--bordure-inset": narrowed }),
|
|
1135
|
+
},
|
|
1136
|
+
":is(.heater, .inescutcheon):has(> &)::after": {
|
|
1137
|
+
content: '""',
|
|
1138
|
+
...ring,
|
|
1139
|
+
...(clip && { "clip-path": clip }),
|
|
1140
|
+
},
|
|
1141
|
+
...part,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
715
1146
|
for (const [tincture, colour] of PAINTS) {
|
|
716
1147
|
utilities[`.fimbriated-${tincture}`] = {
|
|
717
1148
|
"--fimbriated": "1",
|
|
@@ -719,11 +1150,10 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
719
1150
|
};
|
|
720
1151
|
}
|
|
721
1152
|
|
|
722
|
-
// Isolated, so an ordinary in a quarter paints under the shield's own ordinary.
|
|
723
|
-
//
|
|
1153
|
+
// Isolated, so an ordinary in a quarter paints under the shield's own ordinary. A frame's field
|
|
1154
|
+
// isn't, so the frame's bordure ring can go between its ordinaries and its chief.
|
|
724
1155
|
const ordinaryRules = (ordinary, pseudo, line) => ({
|
|
725
1156
|
position: "relative",
|
|
726
|
-
isolation: "isolate",
|
|
727
1157
|
...(BETWEEN[ordinary] && betweenVars(mergeLined(ordinary, line))),
|
|
728
1158
|
...(ON[ordinary] && onVars(ON[ordinary], ON_SCALE[line] ?? 1)),
|
|
729
1159
|
// With a chief, everything else is drawn in the field below it.
|
|
@@ -736,9 +1166,11 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
736
1166
|
[PSEUDO[ordinary] ?? "&::before"]: {
|
|
737
1167
|
content: '""',
|
|
738
1168
|
position: "absolute",
|
|
739
|
-
|
|
1169
|
+
// The chief goes over a frame's bordure ring, and its charges over it.
|
|
1170
|
+
"z-index": PSEUDO[ordinary] ? "3" : "1",
|
|
740
1171
|
...pseudo,
|
|
741
1172
|
},
|
|
1173
|
+
[`&:not(${FRAME} > *)`]: { isolation: "isolate" },
|
|
742
1174
|
});
|
|
743
1175
|
|
|
744
1176
|
for (const [ordinary, vertices] of Object.entries(ORDINARIES)) {
|
|
@@ -831,10 +1263,13 @@ export default plugin(({ addBase, addUtilities, theme }) => {
|
|
|
831
1263
|
// A plain polygon, without the fimbriation vars, so fimbriated-* can't reach a fur ordinary.
|
|
832
1264
|
for (const fur of Object.keys(FURS)) {
|
|
833
1265
|
const pseudo = { ...placed, ...furBackground(fur, theme) };
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1266
|
+
// `cross-potent` is the charge, not a cross of the fur potent.
|
|
1267
|
+
if (!CHARGES.includes(`${ordinary}-${fur}`)) {
|
|
1268
|
+
utilities[`.${ordinary}-${fur}`] = ordinaryRules(ordinary, {
|
|
1269
|
+
...pseudo,
|
|
1270
|
+
"clip-path": plain,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
838
1273
|
for (const [name, line] of Object.entries(LINES)) {
|
|
839
1274
|
if (!lineOrdinaries(name).includes(ordinary)) {
|
|
840
1275
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@grailwind/eslint-plugin": "^0.1.0-beta.
|
|
31
|
+
"@grailwind/eslint-plugin": "^0.1.0-beta.5",
|
|
32
32
|
"@html-eslint/parser": "^0.67.0",
|
|
33
33
|
"@tailwindcss/cli": "^4.3.3",
|
|
34
34
|
"eslint": "^10.14.0",
|
package/paths.js
CHANGED
|
@@ -312,6 +312,14 @@ export const PATHS = {
|
|
|
312
312
|
["L",65.4,120],
|
|
313
313
|
["Z"],
|
|
314
314
|
],
|
|
315
|
+
"heater": [
|
|
316
|
+
["M",0,0],
|
|
317
|
+
["L",100,0],
|
|
318
|
+
["L",100,66],
|
|
319
|
+
["C",100,96,75,114,50,120],
|
|
320
|
+
["C",25,114,0,96,0,66],
|
|
321
|
+
["Z"],
|
|
322
|
+
],
|
|
315
323
|
"pall-embattled": [
|
|
316
324
|
["M",50,36.22],
|
|
317
325
|
["L",42.5,29.01],
|