grailwind 0.4.0 → 0.6.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 +70 -1
- package/README.md +19 -9
- package/charges.js +110 -12
- package/data.js +94 -10
- package/divisions.js +36 -0
- package/furs.js +1 -1
- package/grailwind.css +98 -2
- package/ordinaries.js +228 -50
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,73 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-11-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Crosses and the shakefork as charges: `cross-couped`, `cross-potent`,
|
|
14
|
+
`cross-pattee`, `maltese-cross`, `cross-crosslet`, `cross-moline`,
|
|
15
|
+
`cross-fleury` and `shakefork`.
|
|
16
|
+
- `semy`: a semé, one built-in charge strewn across the field, e.g. bezanty or
|
|
17
|
+
crusilly.
|
|
18
|
+
- Voided ordinaries, e.g. `cross-voided-argent`: the field shows through.
|
|
19
|
+
`cross-voided-counterchanged` and the like counterchange the outline.
|
|
20
|
+
- Reversed ordinaries: `pile-reversed-*`, `chevron-reversed-*` and
|
|
21
|
+
`pall-reversed-*`, upside down.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `@grailwind/eslint-plugin` 0.1.0-beta.2 needs `grailwind` 0.6.x, for voided
|
|
26
|
+
ordinaries.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Charges, a canton or an inescutcheon in a quarter (or a half of `per-pale` or
|
|
31
|
+
`per-fess`) are placed and counterchanged within it, under the shield's own
|
|
32
|
+
ordinary, rather than against the whole shield.
|
|
33
|
+
|
|
34
|
+
## [0.5.0] - 2026-11-04
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `in-chief` and `in-base`: one to three charges along the chief or base. On a
|
|
39
|
+
chief, they sit on it. The ESLint plugin reports an arrangement put on a
|
|
40
|
+
charge rather than its container.
|
|
41
|
+
- `-of-the-first`: the field's first tincture, as in a blazon, e.g.
|
|
42
|
+
`pale-of-the-first`. The ESLint plugin checks it.
|
|
43
|
+
- Striped fields: `barry-*` and `paly-*` (of six), `barry-of-eight-*`,
|
|
44
|
+
`barry-of-ten-*` and `paly-of-eight-*`. The first tincture is in chief or
|
|
45
|
+
on the dexter. They take furs and counterchanged, like the other divisions.
|
|
46
|
+
- `per-saltire-*`: a field divided on both diagonals, the first tincture in
|
|
47
|
+
chief and base.
|
|
48
|
+
- Bars, named by count: `bar-*`, `two-bars-*` and `three-bars-*`. They take
|
|
49
|
+
lines, furs, fimbriation and counterchanged, like the fess.
|
|
50
|
+
- `pile-*`: a wedge from the chief to a point. It takes lines, furs,
|
|
51
|
+
fimbriation and counterchanged, one or three charges `on` it, and two
|
|
52
|
+
`between`.
|
|
53
|
+
- `canton`: a ninth of the shield in dexter chief, over every ordinary and
|
|
54
|
+
charge, with its own field, divisions, ordinaries and charges, like the
|
|
55
|
+
inescutcheon.
|
|
56
|
+
- Six charges `between` a fess, bend, bend sinister, chevron or pale.
|
|
57
|
+
- `inescutcheon`: a small shield at the centre of the arms, over everything
|
|
58
|
+
else, with its own field, ordinaries and charges. A bordure on it is a third
|
|
59
|
+
as wide.
|
|
60
|
+
- The ESLint plugin checks a plain canton or inescutcheon against the field it
|
|
61
|
+
lies on. One with anything on it counts as arms of its own.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- The `in-chief` and `in-base` points are now `in-chief-point` and
|
|
66
|
+
`in-base-point`, like the honour, fess and nombril points.
|
|
67
|
+
- The canton and the chief surmount a bordure, as in heraldry. The canton is
|
|
68
|
+
still a third of the whole shield, and the chief's lower edge doesn't move.
|
|
69
|
+
- `@grailwind/eslint-plugin` 0.1.0-beta.1 needs `grailwind` 0.5.x, for the
|
|
70
|
+
renamed points.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Two charges `between` a fess, saltire or pall are placed one each side.
|
|
75
|
+
|
|
9
76
|
## [0.4.0] - 2026-11-03
|
|
10
77
|
|
|
11
78
|
### Added
|
|
@@ -71,7 +138,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
71
138
|
Initial release: tinctures, divisions, ordinaries, charges, strict mode and
|
|
72
139
|
custom SVG charges.
|
|
73
140
|
|
|
74
|
-
[Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.
|
|
141
|
+
[Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.6.0...HEAD
|
|
142
|
+
[0.6.0]: https://github.com/pafirmin/grailwind/compare/v0.5.0...v0.6.0
|
|
143
|
+
[0.5.0]: https://github.com/pafirmin/grailwind/compare/v0.4.0...v0.5.0
|
|
75
144
|
[0.4.0]: https://github.com/pafirmin/grailwind/compare/v0.3.0...v0.4.0
|
|
76
145
|
[0.3.0]: https://github.com/pafirmin/grailwind/compare/v0.2.0...v0.3.0
|
|
77
146
|
[0.2.0]: https://github.com/pafirmin/grailwind/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Grailwind CSS
|
|
2
2
|
|
|
3
|
+
[](https://github.com/pafirmin/grailwind/actions/workflows/test.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/grailwind)
|
|
5
|
+
[](https://www.npmjs.com/package/@grailwind/eslint-plugin)
|
|
6
|
+
[](https://github.com/pafirmin/grailwind/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|

|
|
4
9
|
|
|
5
10
|
Heraldic blazon for [Tailwind CSS](https://tailwindcss.com). Draw coats of
|
|
@@ -49,18 +54,23 @@ tinctures replace Tailwind's colours entirely.
|
|
|
49
54
|
- **Tinctures:** a tincture on its own sets the background (`azure`), and
|
|
50
55
|
Tailwind's prefixes work as usual (`text-or`). There are also the furs `ermine`,
|
|
51
56
|
`ermines`, `erminois` and `pean`, which work in divisions too.
|
|
52
|
-
- **The shield:** `escutcheon`, plus `bordure
|
|
57
|
+
- **The shield:** `escutcheon`, plus `bordure-*`, `inescutcheon` (a small shield at the centre)
|
|
58
|
+
and `canton` (a ninth of the shield in dexter chief), each with its own field and charges.
|
|
53
59
|
- **Divided fields:** `per-pale-or-gules`, `quarterly-azure-argent`,
|
|
54
|
-
`gyronny-or-sable` and more, or `per-pale`, `per-fess` and `quarterly` for
|
|
60
|
+
`gyronny-or-sable`, `barry-argent-azure` and more, or `per-pale`, `per-fess` and `quarterly` for
|
|
55
61
|
layout.
|
|
56
|
-
- **Ordinaries:** `fess-or`, `chevron-gules`, `cross-argent`, `chief-azure
|
|
57
|
-
and more, with `fimbriated-*` for a narrow border,
|
|
58
|
-
the like for an engrailed or invected edge,
|
|
59
|
-
|
|
60
|
-
-
|
|
62
|
+
- **Ordinaries:** `fess-or`, `chevron-gules`, `cross-argent`, `chief-azure`,
|
|
63
|
+
`two-bars-or` and more, with `fimbriated-*` for a narrow border,
|
|
64
|
+
`fess-engrailed-or` and the like for an engrailed or invected edge,
|
|
65
|
+
`cross-voided-argent` and the like to show the field through it,
|
|
66
|
+
`pile-reversed-or` (also the chevron and pall) to turn one upside down,
|
|
67
|
+
`chief-ermine` and the like in a fur, or `chevron-counterchanged` and the like
|
|
68
|
+
on a divided field.
|
|
69
|
+
- **Charges:** `mullet`, `fleur-de-lis`, `bezant`, crosses and more, arranged by
|
|
61
70
|
number, `in-pale`, `in-fess`, `in-bend`, `in-bend-sinister`, `on` or `between`
|
|
62
|
-
the ordinary, or placed at a named point such as `in-dexter-chief`,
|
|
63
|
-
with `charge-size-*`, and `counterchanged` on
|
|
71
|
+
the ordinary, or placed at a named point such as `in-dexter-chief`, strewn
|
|
72
|
+
`semy` across the field, sized with `charge-size-*`, and `counterchanged` on
|
|
73
|
+
a divided field.
|
|
64
74
|
- **Your own charges:** any SVG, via `charge-[url(…)]`.
|
|
65
75
|
|
|
66
76
|
## Linting
|
package/charges.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import plugin from "tailwindcss/plugin";
|
|
5
5
|
import { POINTS, ROUNDELS } from "./data.js";
|
|
6
|
+
import { AT } from "./furs.js";
|
|
6
7
|
|
|
7
8
|
const star = (cx, cy, r, points = 5) => {
|
|
8
9
|
const coords = [];
|
|
@@ -14,6 +15,20 @@ const star = (cx, cy, r, points = 5) => {
|
|
|
14
15
|
return `<polygon points="${coords.join(" ")}"/>`;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
// One arm drawn upward from the centre, repeated x times evenly about (50, 50).
|
|
19
|
+
const rotateX = (d, x) => {
|
|
20
|
+
const path = (d, deg) => `<path d="${d}" transform="rotate(${deg} 50 50)" />`;
|
|
21
|
+
|
|
22
|
+
const paths = [];
|
|
23
|
+
const rotation = 360 / x;
|
|
24
|
+
let curr = 0;
|
|
25
|
+
for (let i = 0; i < x; i++) {
|
|
26
|
+
paths.push(path(d, curr));
|
|
27
|
+
curr += rotation;
|
|
28
|
+
}
|
|
29
|
+
return paths.join("");
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
const SHAPES = {
|
|
18
33
|
roundel: `<circle cx="50" cy="50" r="46"/>`,
|
|
19
34
|
annulet: `<circle cx="50" cy="50" r="39" fill="none" stroke="#000" stroke-width="14"/>`,
|
|
@@ -29,6 +44,20 @@ const SHAPES = {
|
|
|
29
44
|
<path d="M45 65H55C55 74 53 82 50 90C47 82 45 74 45 65Z"/>
|
|
30
45
|
<path d="M58 65C62 74 72 80 84 78C80 88 64 90 56 74Z"/>
|
|
31
46
|
<path d="M42 65C38 74 28 80 16 78C20 88 36 90 44 74Z"/>`,
|
|
47
|
+
"cross-couped": rotateX("M39 50V2H61V50Z", 4),
|
|
48
|
+
"cross-potent": rotateX("M43 50V16H31V2H69V16H57V50Z", 4),
|
|
49
|
+
"cross-pattee": rotateX("M40 50V40Q36 20 22 2H78Q64 20 60 40V50Z", 4),
|
|
50
|
+
"maltese-cross": rotateX("M50 50L30 2L50 18L70 2Z", 4),
|
|
51
|
+
"cross-crosslet": rotateX("M43 50V25H32V13H43V2H57V13H68V25H57V50Z", 4),
|
|
52
|
+
"cross-moline": rotateX(
|
|
53
|
+
"M43 50V18C42 12 38 11 33 16C31 6 40 -1 50 7C60 -1 69 6 67 16C62 11 58 12 57 18V50Z",
|
|
54
|
+
4,
|
|
55
|
+
),
|
|
56
|
+
"cross-fleury": rotateX(
|
|
57
|
+
"M43 50V21C41 17 37 16 33 20C30 12 37 8 45 13C45 8 47 5 50 2C53 5 55 8 55 13C63 8 70 12 67 20C63 16 59 17 57 21V50Z",
|
|
58
|
+
4,
|
|
59
|
+
),
|
|
60
|
+
shakefork: `<polygon points="50,96 39,85 39,43 12.6,20.9 11.3,5.4 26.8,4 50,23.5 73.2,4 88.7,5.4 87.4,20.9 61,43 61,85"/>`,
|
|
32
61
|
};
|
|
33
62
|
|
|
34
63
|
// Points as [left%, top%] of the shield, plus a charge size as % of its width.
|
|
@@ -46,7 +75,22 @@ const DEFAULTS = {
|
|
|
46
75
|
const POINT_SIZE = 22;
|
|
47
76
|
|
|
48
77
|
// With a chief on the shield, these points sit on it (see ordinaries.js).
|
|
49
|
-
const CHIEF_POINTS = new Set(["dexter-chief", "chief", "sinister-chief"]);
|
|
78
|
+
const CHIEF_POINTS = new Set(["dexter-chief", "chief-point", "sinister-chief"]);
|
|
79
|
+
|
|
80
|
+
// Arrangements along the chief or base, by the points each count of charges takes.
|
|
81
|
+
// prettier-ignore
|
|
82
|
+
const ALONG = {
|
|
83
|
+
"in-chief": {
|
|
84
|
+
1: ["chief-point"],
|
|
85
|
+
2: ["dexter-chief", "sinister-chief"],
|
|
86
|
+
3: ["dexter-chief", "chief-point", "sinister-chief"],
|
|
87
|
+
},
|
|
88
|
+
"in-base": {
|
|
89
|
+
1: ["base-point"],
|
|
90
|
+
2: ["dexter-base", "sinister-base"],
|
|
91
|
+
3: ["dexter-base", "base-point", "sinister-base"],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
50
94
|
|
|
51
95
|
// prettier-ignore
|
|
52
96
|
const ARRANGEMENTS = {
|
|
@@ -73,6 +117,19 @@ const svgUrl = (shape) =>
|
|
|
73
117
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${shape}</svg>`,
|
|
74
118
|
)}")`;
|
|
75
119
|
|
|
120
|
+
// Semé: whole charges across the field, staggered on the furs' tile (see furs.js), cut at the
|
|
121
|
+
// edges. The shape is defined once and reused, so a shape with its own ids stays valid.
|
|
122
|
+
const SEMY_ACROSS = 4;
|
|
123
|
+
const SEMY_SCALE = 0.25;
|
|
124
|
+
const SEMY_SHIFT = `${-100 / (4 * (SEMY_ACROSS - 1))}%`;
|
|
125
|
+
const semyTile = (shape) => {
|
|
126
|
+
const at = (x, y) => `translate(${x - 50 * SEMY_SCALE} ${y - 50 * SEMY_SCALE})`;
|
|
127
|
+
const uses = AT.map(([x, y]) => `<use href="#s" transform="${at(x, y)} scale(${SEMY_SCALE})"/>`);
|
|
128
|
+
return `url("data:image/svg+xml,${encodeURIComponent(
|
|
129
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 60"><defs><g id="s">${shape}</g></defs>${uses.join("")}</svg>`,
|
|
130
|
+
)}")`;
|
|
131
|
+
};
|
|
132
|
+
|
|
76
133
|
const maskRules = (image) => ({
|
|
77
134
|
"mask-image": image,
|
|
78
135
|
"mask-size": "contain",
|
|
@@ -107,6 +164,18 @@ const counterchanged = {
|
|
|
107
164
|
},
|
|
108
165
|
};
|
|
109
166
|
|
|
167
|
+
// A charge at one of the shield's points. The chief's points sit on a chief when there is one.
|
|
168
|
+
const atPoint = (name) => {
|
|
169
|
+
const [left, top] = POINTS[name];
|
|
170
|
+
return CHIEF_POINTS.has(name)
|
|
171
|
+
? {
|
|
172
|
+
left: `${left}%`,
|
|
173
|
+
top: `var(--chief-points-y, ${top}%)`,
|
|
174
|
+
"--charge-width": `var(--charge-size, var(--chief-points-size, ${POINT_SIZE}%))`,
|
|
175
|
+
}
|
|
176
|
+
: place([left, top], POINT_SIZE);
|
|
177
|
+
};
|
|
178
|
+
|
|
110
179
|
const hasCount = (n) => `:has(> :nth-child(${n}):last-child)`;
|
|
111
180
|
|
|
112
181
|
export default plugin(({ addUtilities, matchUtilities }) => {
|
|
@@ -116,12 +185,13 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
116
185
|
const utilities = {};
|
|
117
186
|
|
|
118
187
|
for (const [name, shape] of Object.entries(SHAPES)) {
|
|
119
|
-
utilities[`.${name}`] = maskRules(svgUrl(shape));
|
|
188
|
+
utilities[`.${name}`] = { ...maskRules(svgUrl(shape)), "--semy-image": semyTile(shape) };
|
|
120
189
|
}
|
|
121
190
|
|
|
122
191
|
for (const [name, tincture] of Object.entries(ROUNDELS)) {
|
|
123
192
|
utilities[`.${name}`] = {
|
|
124
193
|
...maskRules(svgUrl(SHAPES.roundel)),
|
|
194
|
+
"--semy-image": semyTile(SHAPES.roundel),
|
|
125
195
|
"background-color": `var(--color-${tincture})`,
|
|
126
196
|
};
|
|
127
197
|
}
|
|
@@ -160,6 +230,16 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
160
230
|
utilities[`.${name}`] = arrangement;
|
|
161
231
|
}
|
|
162
232
|
|
|
233
|
+
for (const [name, counts] of Object.entries(ALONG)) {
|
|
234
|
+
const arrangement = {};
|
|
235
|
+
for (const [count, points] of Object.entries(counts)) {
|
|
236
|
+
points.forEach((point, i) => {
|
|
237
|
+
arrangement[`&${hasCount(count)} > :nth-child(${i + 1})`] = atPoint(point);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
utilities[`.${name}`] = arrangement;
|
|
241
|
+
}
|
|
242
|
+
|
|
163
243
|
// Positions come from the ordinary on the shield (see ordinaries.js).
|
|
164
244
|
const between = {};
|
|
165
245
|
for (let i = 1; i <= 4; i++) {
|
|
@@ -169,6 +249,16 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
169
249
|
"--charge-width": "var(--charge-size, var(--between-size))",
|
|
170
250
|
};
|
|
171
251
|
}
|
|
252
|
+
// The counts some ordinary overrides in BETWEEN's `counts` (see data.js).
|
|
253
|
+
for (const count of [2, 6]) {
|
|
254
|
+
for (let i = 1; i <= count; i++) {
|
|
255
|
+
between[`&${hasCount(count)} > :nth-child(${i})`] = {
|
|
256
|
+
left: `var(--between-${count}-${i}-x, var(--between-${i}-x))`,
|
|
257
|
+
top: `var(--between-${count}-${i}-y, var(--between-${i}-y))`,
|
|
258
|
+
"--charge-width": `var(--charge-size, var(--between-${count}-size, var(--between-size)))`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
172
262
|
utilities[".between"] = between;
|
|
173
263
|
|
|
174
264
|
// Falls back to the plain-field default when the ordinary has no points for this count.
|
|
@@ -191,18 +281,26 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
191
281
|
}
|
|
192
282
|
utilities[".on"] = on;
|
|
193
283
|
|
|
194
|
-
for (const
|
|
195
|
-
utilities[`.in-${name}`] = {
|
|
196
|
-
".charges > &": CHIEF_POINTS.has(name)
|
|
197
|
-
? {
|
|
198
|
-
left: `${left}%`,
|
|
199
|
-
top: `var(--chief-points-y, ${top}%)`,
|
|
200
|
-
"--charge-width": `var(--charge-size, var(--chief-points-size, ${POINT_SIZE}%))`,
|
|
201
|
-
}
|
|
202
|
-
: place([left, top], POINT_SIZE),
|
|
203
|
-
};
|
|
284
|
+
for (const name of Object.keys(POINTS)) {
|
|
285
|
+
utilities[`.in-${name}`] = { ".charges > &": atPoint(name) };
|
|
204
286
|
}
|
|
205
287
|
|
|
288
|
+
// The whole field, under the ordinaries and the chief. A custom charge has no tile, so it
|
|
289
|
+
// draws nothing: `none` would mean no mask, and flood the field.
|
|
290
|
+
utilities[".semy"] = {
|
|
291
|
+
"&.charges": { inset: "0", "z-index": "0" },
|
|
292
|
+
"&.charges > *": {
|
|
293
|
+
inset: "0",
|
|
294
|
+
width: "auto",
|
|
295
|
+
margin: "0",
|
|
296
|
+
"aspect-ratio": "auto",
|
|
297
|
+
"mask-image": "var(--semy-image, linear-gradient(transparent, transparent))",
|
|
298
|
+
"mask-size": `calc(100% / ${SEMY_ACROSS}) auto`,
|
|
299
|
+
"mask-repeat": "repeat",
|
|
300
|
+
"mask-position": `${SEMY_SHIFT} ${SEMY_SHIFT}`,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
206
304
|
// On a charge, or on `charges` for all of them.
|
|
207
305
|
utilities[".counterchanged"] = {
|
|
208
306
|
".charges > &": counterchanged,
|
package/data.js
CHANGED
|
@@ -5,19 +5,34 @@
|
|
|
5
5
|
export const METALS = ["or", "argent"];
|
|
6
6
|
export const COLOURS = ["gules", "azure", "vert", "purpure", "sable"];
|
|
7
7
|
export const TINCTURES = [...METALS, ...COLOURS];
|
|
8
|
+
// Paints in the field's first tincture, as "of the first" in a blazon.
|
|
9
|
+
export const OF_THE_FIRST = "of-the-first";
|
|
8
10
|
|
|
9
11
|
export const DIVISIONS = [
|
|
10
12
|
"per-pale",
|
|
11
13
|
"per-fess",
|
|
12
14
|
"per-bend",
|
|
13
15
|
"per-bend-sinister",
|
|
16
|
+
"per-saltire",
|
|
14
17
|
"quarterly",
|
|
15
18
|
"gyronny",
|
|
19
|
+
"barry",
|
|
20
|
+
"barry-of-eight",
|
|
21
|
+
"barry-of-ten",
|
|
22
|
+
"paly",
|
|
23
|
+
"paly-of-eight",
|
|
16
24
|
];
|
|
17
25
|
|
|
26
|
+
// Ordinaries that can be reversed, e.g. `pile-reversed-or`: the same shape upside down.
|
|
27
|
+
export const REVERSIBLE = ["chevron", "pall", "pile"];
|
|
28
|
+
const reversed = (ordinary) => `${ordinary}-reversed`;
|
|
29
|
+
|
|
18
30
|
export const ORDINARIES = [
|
|
19
31
|
"pale",
|
|
20
32
|
"fess",
|
|
33
|
+
"bar",
|
|
34
|
+
"two-bars",
|
|
35
|
+
"three-bars",
|
|
21
36
|
"bend",
|
|
22
37
|
"bend-sinister",
|
|
23
38
|
"chevron",
|
|
@@ -25,6 +40,8 @@ export const ORDINARIES = [
|
|
|
25
40
|
"saltire",
|
|
26
41
|
"pall",
|
|
27
42
|
"chief",
|
|
43
|
+
"pile",
|
|
44
|
+
...REVERSIBLE.map(reversed),
|
|
28
45
|
];
|
|
29
46
|
|
|
30
47
|
export const CHARGES = [
|
|
@@ -35,6 +52,14 @@ export const CHARGES = [
|
|
|
35
52
|
"billet",
|
|
36
53
|
"crescent",
|
|
37
54
|
"fleur-de-lis",
|
|
55
|
+
"cross-couped",
|
|
56
|
+
"cross-potent",
|
|
57
|
+
"cross-pattee",
|
|
58
|
+
"maltese-cross",
|
|
59
|
+
"cross-crosslet",
|
|
60
|
+
"cross-moline",
|
|
61
|
+
"cross-fleury",
|
|
62
|
+
"shakefork",
|
|
38
63
|
];
|
|
39
64
|
|
|
40
65
|
// Roundels have their own names by tincture.
|
|
@@ -49,6 +74,21 @@ export const ROUNDELS = {
|
|
|
49
74
|
// Lines of partition for an ordinary's edge, e.g. `fess-engrailed-or`.
|
|
50
75
|
export const LINES = ["engrailed", "invected"];
|
|
51
76
|
|
|
77
|
+
// Ordinaries that can be voided, e.g. `cross-voided-argent`.
|
|
78
|
+
export const VOIDABLE = [
|
|
79
|
+
"pale",
|
|
80
|
+
"fess",
|
|
81
|
+
"bar",
|
|
82
|
+
"bend",
|
|
83
|
+
"bend-sinister",
|
|
84
|
+
"chevron",
|
|
85
|
+
"cross",
|
|
86
|
+
"saltire",
|
|
87
|
+
"pall",
|
|
88
|
+
"pile",
|
|
89
|
+
...REVERSIBLE.map(reversed),
|
|
90
|
+
];
|
|
91
|
+
|
|
52
92
|
// Each fur's [ground, spot] tinctures.
|
|
53
93
|
export const FURS = {
|
|
54
94
|
ermine: ["argent", "sable"],
|
|
@@ -60,30 +100,48 @@ export const FURS = {
|
|
|
60
100
|
// The points of the shield, as [left%, top%], for placing a single charge, e.g. `in-dexter-chief`.
|
|
61
101
|
export const POINTS = {
|
|
62
102
|
"dexter-chief": [25, 17],
|
|
63
|
-
chief: [50, 15],
|
|
103
|
+
"chief-point": [50, 15],
|
|
64
104
|
"sinister-chief": [75, 17],
|
|
65
105
|
"honour-point": [50, 30],
|
|
66
106
|
"fess-point": [50, 45],
|
|
67
107
|
"nombril-point": [50, 60],
|
|
68
108
|
"dexter-base": [30, 72],
|
|
69
|
-
base: [50, 84],
|
|
109
|
+
"base-point": [50, 84],
|
|
70
110
|
"sinister-base": [70, 72],
|
|
71
111
|
};
|
|
72
112
|
|
|
73
113
|
// Plain-field arrangements of a `charges` container, besides `between` and `on`.
|
|
74
|
-
export const ARRANGEMENTS = [
|
|
114
|
+
export const ARRANGEMENTS = [
|
|
115
|
+
"in-pale",
|
|
116
|
+
"in-fess",
|
|
117
|
+
"in-bend",
|
|
118
|
+
"in-bend-sinister",
|
|
119
|
+
"in-chief",
|
|
120
|
+
"in-base",
|
|
121
|
+
"semy",
|
|
122
|
+
];
|
|
75
123
|
|
|
76
124
|
// Where charges go for "between", as [left%, top%], plus their size as % of width.
|
|
125
|
+
// `counts` overrides the points (and optionally the size) for one number of charges.
|
|
77
126
|
// prettier-ignore
|
|
78
127
|
export const BETWEEN = {
|
|
79
|
-
pale: { size: 18, points: [[18, 42], [82, 42]]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
pale: { size: 18, points: [[18, 42], [82, 42]], counts: {
|
|
129
|
+
6: { size: 14, points: [[18, 18], [18, 45], [18, 70], [82, 18], [82, 45], [82, 70]] } } },
|
|
130
|
+
fess: { size: 20, points: [[30, 15], [70, 15], [50, 78]], counts: {
|
|
131
|
+
2: { points: [[50, 15], [50, 78]] },
|
|
132
|
+
6: { size: 14, points: [[20, 14], [50, 14], [80, 14], [32, 70], [68, 70], [50, 86]] } } },
|
|
133
|
+
bend: { size: 24, points: [[70, 26], [30, 68]], counts: {
|
|
134
|
+
6: { size: 14, points: [[48, 9], [76, 12], [84, 38], [9, 48], [18, 72], [38, 84]] } } },
|
|
135
|
+
"bend-sinister": { size: 24, points: [[30, 26], [70, 68]], counts: {
|
|
136
|
+
6: { size: 14, points: [[52, 9], [24, 12], [16, 38], [91, 48], [82, 72], [62, 84]] } } },
|
|
137
|
+
chevron: { size: 22, points: [[24, 22], [76, 22], [50, 70]], counts: {
|
|
138
|
+
6: { size: 12, points: [[18, 14], [50, 11], [82, 14], [38, 66], [62, 66], [50, 84]] } } },
|
|
84
139
|
cross: { size: 18, points: [[20, 17], [80, 17], [24, 72], [76, 72]] },
|
|
85
|
-
saltire: { size: 16, points: [[50, 13], [14, 50], [86, 50], [50, 84]]
|
|
86
|
-
|
|
140
|
+
saltire: { size: 16, points: [[50, 13], [14, 50], [86, 50], [50, 84]], counts: {
|
|
141
|
+
2: { points: [[14, 50], [86, 50]] } } },
|
|
142
|
+
pall: { size: 18, points: [[50, 11], [18, 56], [82, 56]], counts: {
|
|
143
|
+
2: { points: [[18, 56], [82, 56]] } } },
|
|
144
|
+
pile: { size: 20, points: [[18, 55], [82, 55]] },
|
|
87
145
|
};
|
|
88
146
|
|
|
89
147
|
// Where charges go for "on", along the ordinary's centre line, by number of charges.
|
|
@@ -97,4 +155,30 @@ export const ON = {
|
|
|
97
155
|
cross: { size: 15, counts: { 1: [[50, 45]], 5: [[50, 15], [18, 45], [50, 45], [82, 45], [50, 80]] } },
|
|
98
156
|
saltire: { size: 14, counts: { 1: [[50, 50]], 5: [[22, 22], [78, 22], [50, 50], [22, 78], [78, 78]] } },
|
|
99
157
|
pall: { size: 14, counts: { 1: [[50, 50]], 3: [[22, 22], [78, 22], [50, 75]] } },
|
|
158
|
+
pile: { size: 14, counts: { 1: [[50, 25]], 3: [[50, 14], [50, 34], [50, 54]] } },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const mirror = (points) => points.map(([x, y]) => [x, 100 - y]);
|
|
162
|
+
const mapValues = (object, fn) =>
|
|
163
|
+
Object.fromEntries(Object.entries(object).map(([key, value]) => [key, fn(value)]));
|
|
164
|
+
|
|
165
|
+
for (const ordinary of REVERSIBLE) {
|
|
166
|
+
const { size, points, counts = {} } = BETWEEN[ordinary];
|
|
167
|
+
BETWEEN[reversed(ordinary)] = {
|
|
168
|
+
size,
|
|
169
|
+
points: mirror(points),
|
|
170
|
+
counts: mapValues(counts, (count) => ({ ...count, points: mirror(count.points) })),
|
|
171
|
+
};
|
|
172
|
+
ON[reversed(ordinary)] = {
|
|
173
|
+
size: ON[ordinary].size,
|
|
174
|
+
counts: mapValues(ON[ordinary].counts, mirror),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Mirrored, the lower charges would clip on the heater's curved flanks.
|
|
179
|
+
// prettier-ignore
|
|
180
|
+
BETWEEN["chevron-reversed"] = {
|
|
181
|
+
...BETWEEN["chevron-reversed"],
|
|
182
|
+
points: [[28, 74], [72, 74], [50, 30]],
|
|
183
|
+
counts: { 6: { size: 12, points: [[26, 78], [50, 89], [74, 78], [38, 34], [62, 34], [50, 16]] } },
|
|
100
184
|
};
|
package/divisions.js
CHANGED
|
@@ -11,6 +11,17 @@ const FESS_POINT = "45%";
|
|
|
11
11
|
// Each division maps (first, second) tincture colours to background rules.
|
|
12
12
|
const layer = (image) => ({ "background-image": image });
|
|
13
13
|
|
|
14
|
+
// Equal stripes, the first tincture at the top (barry) or on the dexter (paly): one pair, tiled.
|
|
15
|
+
// The repeat must be explicit, as toLayers defaults to no-repeat for the fur variants.
|
|
16
|
+
const stripes = (to, pieces) => (a, b) => {
|
|
17
|
+
const pair = `calc(200% / ${pieces})`;
|
|
18
|
+
return {
|
|
19
|
+
"background-image": `linear-gradient(to ${to}, ${a} 50%, ${b} 50%)`,
|
|
20
|
+
"background-size": to === "bottom" ? `100% ${pair}` : `${pair} 100%`,
|
|
21
|
+
"background-repeat": to === "bottom" ? "repeat-y" : "repeat-x",
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
14
25
|
const DIVISIONS = {
|
|
15
26
|
// Vertical split: first on the dexter (viewer's left).
|
|
16
27
|
"per-pale": (a, b) => layer(`linear-gradient(to right, ${a} 50%, ${b} 50%)`),
|
|
@@ -25,6 +36,21 @@ const DIVISIONS = {
|
|
|
25
36
|
// Line from sinister chief to dexter base: first in the upper half.
|
|
26
37
|
"per-bend-sinister": (a, b) => layer(`linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`),
|
|
27
38
|
|
|
39
|
+
// Both diagonals, crossing at the centre like the saltire: first in chief and base.
|
|
40
|
+
// Gyronny's four tiles, with the top-right and bottom-left swapped.
|
|
41
|
+
"per-saltire": (a, b) => ({
|
|
42
|
+
"background-color": b,
|
|
43
|
+
"background-image": [
|
|
44
|
+
`linear-gradient(to bottom left, ${a} 50%, ${b} 50%)`,
|
|
45
|
+
`linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`,
|
|
46
|
+
`linear-gradient(to bottom left, ${b} 50%, ${a} 50%)`,
|
|
47
|
+
`linear-gradient(to bottom right, ${b} 50%, ${a} 50%)`,
|
|
48
|
+
].join(", "),
|
|
49
|
+
"background-size": "50% 50%",
|
|
50
|
+
"background-position": "left top, right top, right bottom, left bottom",
|
|
51
|
+
"background-repeat": "no-repeat",
|
|
52
|
+
}),
|
|
53
|
+
|
|
28
54
|
// Quarters 1 and 4 (dexter chief, sinister base) are the first tincture.
|
|
29
55
|
// Linear gradients rather than conic, which render jagged edges in some browsers.
|
|
30
56
|
// With a fur, the bands can't overlap, or the fur's transparent half shows the band below.
|
|
@@ -50,6 +76,13 @@ const DIVISIONS = {
|
|
|
50
76
|
"background-position": "left top, right top, right bottom, left bottom",
|
|
51
77
|
"background-repeat": "no-repeat",
|
|
52
78
|
}),
|
|
79
|
+
|
|
80
|
+
// Across the whole height, not through the fess point, so the shield's point clips the last bar.
|
|
81
|
+
barry: stripes("bottom", 6),
|
|
82
|
+
"barry-of-eight": stripes("bottom", 8),
|
|
83
|
+
"barry-of-ten": stripes("bottom", 10),
|
|
84
|
+
paly: stripes("right", 6),
|
|
85
|
+
"paly-of-eight": stripes("right", 8),
|
|
53
86
|
};
|
|
54
87
|
|
|
55
88
|
// A division's background rules as one { image, size, position, repeat } per layer.
|
|
@@ -108,6 +141,7 @@ export default plugin(({ addUtilities, theme }) => {
|
|
|
108
141
|
utilities[`.${division}-${first}-${second}`] = {
|
|
109
142
|
...background(a, b),
|
|
110
143
|
...counterchange(background(b, a)),
|
|
144
|
+
"--field-first": a,
|
|
111
145
|
};
|
|
112
146
|
}
|
|
113
147
|
}
|
|
@@ -121,9 +155,11 @@ export default plugin(({ addUtilities, theme }) => {
|
|
|
121
155
|
[fur, tincture],
|
|
122
156
|
[tincture, fur],
|
|
123
157
|
]) {
|
|
158
|
+
// Only a tincture can be "of the first"; a fur first leaves it unset.
|
|
124
159
|
utilities[`.${division}-${first}-${second}`] = {
|
|
125
160
|
...withFur(division, first, second, theme),
|
|
126
161
|
...counterchange(withFur(division, second, first, theme)),
|
|
162
|
+
...(!FURS[first] && { "--field-first": `var(--color-${first})` }),
|
|
127
163
|
};
|
|
128
164
|
}
|
|
129
165
|
}
|
package/furs.js
CHANGED
|
@@ -11,7 +11,7 @@ const SPOT = [
|
|
|
11
11
|
|
|
12
12
|
// A 5:6 tile, the shield's own shape, so whole tiles fill it: a spot at its centre, and quarters of one at each
|
|
13
13
|
// corner that join up with the neighbouring tiles.
|
|
14
|
-
const AT = [
|
|
14
|
+
export const AT = [
|
|
15
15
|
[25, 30],
|
|
16
16
|
[0, 0],
|
|
17
17
|
[50, 0],
|
package/grailwind.css
CHANGED
|
@@ -42,6 +42,19 @@
|
|
|
42
42
|
syntax: "*";
|
|
43
43
|
inherits: false;
|
|
44
44
|
}
|
|
45
|
+
/* A field's first tincture. The escutcheon copies it to --the-first, which inherits,
|
|
46
|
+
* so a canton's own field can't change it. */
|
|
47
|
+
@property --field-first {
|
|
48
|
+
syntax: "*";
|
|
49
|
+
inherits: false;
|
|
50
|
+
}
|
|
51
|
+
/* A field's own bordure width, 0 without one. The shield copies it to --outer-inset, which
|
|
52
|
+
* inherits, so a canton can step out over the shield's bordure and not its own. */
|
|
53
|
+
@property --bordure-inset {
|
|
54
|
+
syntax: "<length>";
|
|
55
|
+
inherits: false;
|
|
56
|
+
initial-value: 0px;
|
|
57
|
+
}
|
|
45
58
|
|
|
46
59
|
/* ---------------------------------------------------------------------------
|
|
47
60
|
* Tinctures
|
|
@@ -67,24 +80,35 @@
|
|
|
67
80
|
* ------------------------------------------------------------------------- */
|
|
68
81
|
@utility or {
|
|
69
82
|
background-color: var(--color-or);
|
|
83
|
+
--field-first: var(--color-or);
|
|
70
84
|
}
|
|
71
85
|
@utility argent {
|
|
72
86
|
background-color: var(--color-argent);
|
|
87
|
+
--field-first: var(--color-argent);
|
|
73
88
|
}
|
|
74
89
|
@utility gules {
|
|
75
90
|
background-color: var(--color-gules);
|
|
91
|
+
--field-first: var(--color-gules);
|
|
76
92
|
}
|
|
77
93
|
@utility azure {
|
|
78
94
|
background-color: var(--color-azure);
|
|
95
|
+
--field-first: var(--color-azure);
|
|
79
96
|
}
|
|
80
97
|
@utility vert {
|
|
81
98
|
background-color: var(--color-vert);
|
|
99
|
+
--field-first: var(--color-vert);
|
|
82
100
|
}
|
|
83
101
|
@utility purpure {
|
|
84
102
|
background-color: var(--color-purpure);
|
|
103
|
+
--field-first: var(--color-purpure);
|
|
85
104
|
}
|
|
86
105
|
@utility sable {
|
|
87
106
|
background-color: var(--color-sable);
|
|
107
|
+
--field-first: var(--color-sable);
|
|
108
|
+
}
|
|
109
|
+
/* The field's first tincture, for charges and for a canton or inescutcheon field. */
|
|
110
|
+
@utility of-the-first {
|
|
111
|
+
background-color: var(--the-first, transparent);
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
/* ---------------------------------------------------------------------------
|
|
@@ -96,6 +120,13 @@
|
|
|
96
120
|
display: grid;
|
|
97
121
|
grid-auto-flow: column;
|
|
98
122
|
grid-auto-columns: 1fr;
|
|
123
|
+
|
|
124
|
+
/* On a shield, each part is a coat of its own: what's in it is placed, stacked and
|
|
125
|
+
* counterchanged within it. */
|
|
126
|
+
&:is(.escutcheon, .escutcheon *) > * {
|
|
127
|
+
position: relative;
|
|
128
|
+
contain: layout;
|
|
129
|
+
}
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
/* Per fess: split horizontally into chief / base. */
|
|
@@ -103,6 +134,12 @@
|
|
|
103
134
|
display: grid;
|
|
104
135
|
grid-auto-flow: row;
|
|
105
136
|
grid-auto-rows: 1fr;
|
|
137
|
+
|
|
138
|
+
/* See per-pale. */
|
|
139
|
+
&:is(.escutcheon, .escutcheon *) > * {
|
|
140
|
+
position: relative;
|
|
141
|
+
contain: layout;
|
|
142
|
+
}
|
|
106
143
|
}
|
|
107
144
|
|
|
108
145
|
/* Quarterly: four quarters, numbered 1–4 across then down.
|
|
@@ -111,6 +148,18 @@
|
|
|
111
148
|
display: grid;
|
|
112
149
|
grid-template-columns: 1fr 1fr;
|
|
113
150
|
grid-template-rows: 1fr 1fr;
|
|
151
|
+
|
|
152
|
+
/* Each quarter is a coat of its own, so it starts its own count. */
|
|
153
|
+
& > * {
|
|
154
|
+
--the-first: var(--field-first);
|
|
155
|
+
--outer-inset: 0px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* See per-pale. */
|
|
159
|
+
&:is(.escutcheon, .escutcheon *) > * {
|
|
160
|
+
position: relative;
|
|
161
|
+
contain: layout;
|
|
162
|
+
}
|
|
114
163
|
}
|
|
115
164
|
|
|
116
165
|
/* ---------------------------------------------------------------------------
|
|
@@ -120,8 +169,51 @@
|
|
|
120
169
|
/* Bordure: a border in a given tincture, e.g. `bordure-or`. */
|
|
121
170
|
@utility bordure-* {
|
|
122
171
|
border-style: solid;
|
|
123
|
-
border-width: 0.75rem;
|
|
172
|
+
border-width: var(--bordure-width, 0.75rem);
|
|
124
173
|
border-color: --value(--color-*);
|
|
174
|
+
--bordure-inset: var(--bordure-width, 0.75rem);
|
|
175
|
+
}
|
|
176
|
+
@utility bordure-of-the-first {
|
|
177
|
+
border-style: solid;
|
|
178
|
+
border-width: var(--bordure-width, 0.75rem);
|
|
179
|
+
border-color: var(--the-first, transparent);
|
|
180
|
+
--bordure-inset: var(--bordure-width, 0.75rem);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Inescutcheon: a shield a third as wide, centred on the fess point (50%, 45%),
|
|
184
|
+
* over everything else on the shield. As both are 5:6, it's a third as tall too. */
|
|
185
|
+
@utility inescutcheon {
|
|
186
|
+
position: absolute;
|
|
187
|
+
z-index: 2;
|
|
188
|
+
width: 33%;
|
|
189
|
+
left: 33.5%;
|
|
190
|
+
top: 28.5%;
|
|
191
|
+
aspect-ratio: 5 / 6;
|
|
192
|
+
overflow: clip;
|
|
193
|
+
overflow-clip-margin: border-box;
|
|
194
|
+
border-radius: 0 0 50% 50% / 0 0 45% 45%;
|
|
195
|
+
box-shadow: 0 0 0 2px var(--color-sable);
|
|
196
|
+
contain: layout;
|
|
197
|
+
/* In proportion with the shield's own bordure. */
|
|
198
|
+
--bordure-width: 0.25rem;
|
|
199
|
+
--outer-inset: var(--bordure-inset);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Canton: a third of the shield each way in dexter chief, so a ninth of it, and
|
|
203
|
+
* 5:6 like the shield, so everything on it places as on a shield. Over every
|
|
204
|
+
* ordinary and charge, as a later addition to the arms, and over the bordure. */
|
|
205
|
+
@utility canton {
|
|
206
|
+
position: absolute;
|
|
207
|
+
z-index: 2;
|
|
208
|
+
width: calc((100% + 2 * var(--outer-inset, 0px)) / 3);
|
|
209
|
+
left: calc(-1 * var(--outer-inset, 0px));
|
|
210
|
+
top: calc(-1 * var(--outer-inset, 0px));
|
|
211
|
+
aspect-ratio: 5 / 6;
|
|
212
|
+
/* Like the shield, so a chief on it surmounts its own bordure. */
|
|
213
|
+
overflow: clip;
|
|
214
|
+
overflow-clip-margin: border-box;
|
|
215
|
+
contain: layout;
|
|
216
|
+
--bordure-width: 0.25rem;
|
|
125
217
|
}
|
|
126
218
|
|
|
127
219
|
/* ---------------------------------------------------------------------------
|
|
@@ -132,11 +224,15 @@
|
|
|
132
224
|
@utility escutcheon {
|
|
133
225
|
position: relative;
|
|
134
226
|
aspect-ratio: 5 / 6;
|
|
135
|
-
|
|
227
|
+
/* At the border's edge, so a canton or chief can surmount the bordure. */
|
|
228
|
+
overflow: clip;
|
|
229
|
+
overflow-clip-margin: border-box;
|
|
136
230
|
border-radius: 0 0 50% 50% / 0 0 45% 45%;
|
|
137
231
|
box-shadow: 0 0 0 2px var(--color-sable);
|
|
138
232
|
/* The containing block for counterchanged charges' fixed layers. */
|
|
139
233
|
contain: layout;
|
|
234
|
+
--the-first: var(--field-first);
|
|
235
|
+
--outer-inset: var(--bordure-inset);
|
|
140
236
|
}
|
|
141
237
|
|
|
142
238
|
/* ---------------------------------------------------------------------------
|
package/ordinaries.js
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
* Grailwind CSS: ordinaries, e.g. `fess-or`.
|
|
3
3
|
*/
|
|
4
4
|
import plugin from "tailwindcss/plugin";
|
|
5
|
-
import { BETWEEN, FURS, ON, TINCTURES } from "./data.js";
|
|
5
|
+
import { BETWEEN, FURS, OF_THE_FIRST, ON, REVERSIBLE, TINCTURES, VOIDABLE } from "./data.js";
|
|
6
6
|
import { furBackground } from "./furs.js";
|
|
7
7
|
|
|
8
8
|
// prettier-ignore
|
|
9
9
|
const ORDINARIES = {
|
|
10
10
|
pale: [[35, 0], [65, 0], [65, 100], [35, 100]],
|
|
11
11
|
fess: [[0, 30], [100, 30], [100, 60], [0, 60]],
|
|
12
|
+
// Bars that don't touch are one polygon, joined along the shield's dexter edge.
|
|
13
|
+
bar: [[0, 38], [100, 38], [100, 52], [0, 52]],
|
|
14
|
+
"two-bars": [[0, 27], [100, 27], [100, 39], [0, 39], [0, 51], [100, 51], [100, 63], [0, 63]],
|
|
15
|
+
"three-bars": [
|
|
16
|
+
[0, 20], [100, 20], [100, 30], [0, 30], [0, 40], [100, 40],
|
|
17
|
+
[100, 50], [0, 50], [0, 60], [100, 60], [100, 70], [0, 70],
|
|
18
|
+
],
|
|
12
19
|
bend: [[0, 0], [20, 0], [100, 80], [100, 100], [80, 100], [0, 20]],
|
|
13
20
|
"bend-sinister": [[100, 0], [80, 0], [0, 80], [0, 100], [20, 100], [100, 20]],
|
|
14
21
|
chevron: [[50, 25], [100, 75], [100, 95], [50, 45], [0, 95], [0, 75]],
|
|
@@ -25,7 +32,12 @@ const ORDINARIES = {
|
|
|
25
32
|
[60, 55], [60, 100], [40, 100], [40, 55], [0, 15],
|
|
26
33
|
],
|
|
27
34
|
chief: [[0, 0], [100, 0], [100, 30], [0, 30]],
|
|
35
|
+
pile: [[20, 0], [80, 0], [50, 85]],
|
|
28
36
|
};
|
|
37
|
+
// signedArea() follows the flip's winding, so the order stays.
|
|
38
|
+
for (const ordinary of REVERSIBLE) {
|
|
39
|
+
ORDINARIES[`${ordinary}-reversed`] = ORDINARIES[ordinary].map(([x, y]) => [x, 100 - y]);
|
|
40
|
+
}
|
|
29
41
|
|
|
30
42
|
const num = (v) => +v.toFixed(3);
|
|
31
43
|
const pct = (v) => (num(v) === 0 ? "0" : `${num(v)}%`);
|
|
@@ -34,9 +46,24 @@ const polygon = (vertices) => `polygon(${vertices.map(point).join(", ")})`;
|
|
|
34
46
|
|
|
35
47
|
const PSEUDO = { chief: "&::after" };
|
|
36
48
|
|
|
49
|
+
// The chief surmounts a bordure.
|
|
50
|
+
const INSET = "var(--bordure-inset)";
|
|
51
|
+
const OVER_BORDURE = {
|
|
52
|
+
"--bordure-inset": "inherit",
|
|
53
|
+
inset: `calc(-1 * ${INSET}) calc(-1 * ${INSET}) 0`,
|
|
54
|
+
padding: `${INSET} ${INSET} 0`,
|
|
55
|
+
"background-origin": "content-box",
|
|
56
|
+
};
|
|
57
|
+
// A vertex on the shield's top or side edges, moved out to the bordure's outer edge.
|
|
58
|
+
const outer = ([x, y]) => {
|
|
59
|
+
const ox = x === 0 ? `calc(-1 * ${INSET})` : x === 100 ? `calc(100% + ${INSET})` : pct(x);
|
|
60
|
+
return `${ox} ${y === 0 ? `calc(-1 * ${INSET})` : pct(y)}`;
|
|
61
|
+
};
|
|
62
|
+
|
|
37
63
|
// A y in % of the field, as % of the whole shield when a chief takes the top of it.
|
|
38
64
|
const belowChief = (y) =>
|
|
39
65
|
y === 100 ? "100%" : `calc(${num(y)}% + var(--field-top, 0%) * ${num(1 - y / 100)})`;
|
|
66
|
+
const pointBelowChief = ([x, y]) => `${pct(x)} ${belowChief(y)}`;
|
|
40
67
|
|
|
41
68
|
// Fimbriation: the ordinary's clip is widened and filled with the fimbriation
|
|
42
69
|
// tincture, and the ordinary's own tincture is painted over it as gradient
|
|
@@ -64,15 +91,15 @@ const offsetLine = (a, b, outward, distance) => {
|
|
|
64
91
|
return { n, c: n[0] * ax + n[1] * ay + distance };
|
|
65
92
|
};
|
|
66
93
|
|
|
67
|
-
// How far each vertex moves, in %, when
|
|
68
|
-
// don't move, so their vertices slide along it.
|
|
69
|
-
const widen = (vertices) => {
|
|
94
|
+
// How far each vertex moves, in %, when the sides move out by `distance` (in by a negative one).
|
|
95
|
+
// Sides on the shield's edge don't move, so their vertices slide along it.
|
|
96
|
+
const widen = (vertices, distance = FIMBRIATION) => {
|
|
70
97
|
const outward = Math.sign(signedArea(vertices));
|
|
71
98
|
const count = vertices.length;
|
|
72
99
|
const line = (i) => {
|
|
73
100
|
const a = vertices[i];
|
|
74
101
|
const b = vertices[(i + 1) % count];
|
|
75
|
-
return offsetLine(a, b, outward, onShieldEdge(a, b) ? 0 :
|
|
102
|
+
return offsetLine(a, b, outward, onShieldEdge(a, b) ? 0 : distance);
|
|
76
103
|
};
|
|
77
104
|
return vertices.map((v, i) => {
|
|
78
105
|
const p = line((i - 1 + count) % count);
|
|
@@ -84,6 +111,23 @@ const widen = (vertices) => {
|
|
|
84
111
|
});
|
|
85
112
|
};
|
|
86
113
|
|
|
114
|
+
const rotate = (list, start) => [...list.slice(start), ...list.slice(0, start)];
|
|
115
|
+
|
|
116
|
+
// The ordinary with a hole the shape of it shrunk by `distance`, cut with evenodd.
|
|
117
|
+
const voidedPolygon = (vertices, distance, format = point) => {
|
|
118
|
+
const offsets = widen(vertices, -distance);
|
|
119
|
+
const inner = vertices.map(([x, y], i) => [x + offsets[i][0], y + offsets[i][1]]);
|
|
120
|
+
// Start and end at the shield's edge so we don't draw through it.
|
|
121
|
+
const edge = vertices.findIndex((v, i) => onShieldEdge(v, vertices[(i + 1) % vertices.length]));
|
|
122
|
+
if (edge === -1) {
|
|
123
|
+
throw new Error("a voided ordinary needs a side on the shield's edge");
|
|
124
|
+
}
|
|
125
|
+
const outerStr = [...rotate(vertices, edge), vertices[edge]].map(format).join(", ");
|
|
126
|
+
const innerStr = [...rotate(inner, edge), inner[edge]].map(format).join(", ");
|
|
127
|
+
|
|
128
|
+
return `polygon(evenodd, ${outerStr}, ${innerStr})`;
|
|
129
|
+
};
|
|
130
|
+
|
|
87
131
|
const shift = (v, d) =>
|
|
88
132
|
num(d) === 0
|
|
89
133
|
? pct(v)
|
|
@@ -104,10 +148,19 @@ const widenedPolygon = (vertices) => {
|
|
|
104
148
|
const S = 12;
|
|
105
149
|
const band = (kind, from, to, tile) => ({ kind, from, to, tile });
|
|
106
150
|
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.
|
|
153
|
+
const half = (kind, end, tile) => ({ kind, tile, stops: end === "start" ? [0, 50] : [50, 100] });
|
|
154
|
+
// A 2px strip down the centre, from the top to `bottom`, where two tiles meet mid-pixel and
|
|
155
|
+
// would let the fimbriation show between them. Unsoftened, as it lies on its own tincture.
|
|
156
|
+
const seam = (bottom) => ({ kind: "x", tile: [0, 0, 100, bottom], seam: true });
|
|
107
157
|
// prettier-ignore
|
|
108
158
|
const BANDS = {
|
|
109
159
|
pale: [band("x", 35, 65)],
|
|
110
160
|
fess: [band("y", 30, 60)],
|
|
161
|
+
bar: [band("y", 38, 52)],
|
|
162
|
+
"two-bars": [band("y", 27, 39), band("y", 51, 63)],
|
|
163
|
+
"three-bars": [band("y", 20, 30), band("y", 40, 50), band("y", 60, 70)],
|
|
111
164
|
bend: [band("u-v", -20, 20)],
|
|
112
165
|
"bend-sinister": [band("u+v", 80, 120)],
|
|
113
166
|
chevron: [
|
|
@@ -127,7 +180,31 @@ const BANDS = {
|
|
|
127
180
|
band("u+v", 85, 115),
|
|
128
181
|
band("x", 40, 60, [0, 55, 100, 100]),
|
|
129
182
|
],
|
|
183
|
+
// 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)],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// The bands upside down. A diagonal swaps kind, as x − y becomes x + (100 − y) − 100. A half's
|
|
188
|
+
// stops stay: its 50% line is the flipped tile's other diagonal, which is the flipped side.
|
|
189
|
+
const reverseBand = ({ kind, from, to, tile, ...rest }) => {
|
|
190
|
+
const layer = { ...rest, tile: tile && [tile[0], 100 - tile[3], tile[2], 100 - tile[1]] };
|
|
191
|
+
if (from === undefined) {
|
|
192
|
+
return { ...layer, kind: { "u-v": "u+v", "u+v": "u-v" }[kind] ?? kind };
|
|
193
|
+
}
|
|
194
|
+
switch (kind) {
|
|
195
|
+
case "x":
|
|
196
|
+
return { ...layer, kind, from, to };
|
|
197
|
+
case "y":
|
|
198
|
+
return { ...layer, kind, from: 100 - to, to: 100 - from };
|
|
199
|
+
case "u-v":
|
|
200
|
+
return { ...layer, kind: "u+v", from: from + 100, to: to + 100 };
|
|
201
|
+
case "u+v":
|
|
202
|
+
return { ...layer, kind: "u-v", from: from - 100, to: to - 100 };
|
|
203
|
+
}
|
|
130
204
|
};
|
|
205
|
+
for (const ordinary of REVERSIBLE) {
|
|
206
|
+
BANDS[`${ordinary}-reversed`] = BANDS[ordinary].map(reverseBand);
|
|
207
|
+
}
|
|
131
208
|
|
|
132
209
|
const DIRECTION = {
|
|
133
210
|
x: "to right",
|
|
@@ -155,23 +232,24 @@ const stop = ({ kind, tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }, value) => {
|
|
|
155
232
|
}
|
|
156
233
|
};
|
|
157
234
|
|
|
158
|
-
// Soften each edge by a pixel, as hard gradient stops aren't antialiased.
|
|
235
|
+
// Soften each edge by a pixel, as hard gradient stops aren't antialiased. A stop at 0 or 100% is
|
|
236
|
+
// the tile's own edge, so it isn't softened.
|
|
159
237
|
const gradient = (layer, colour) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
238
|
+
if (layer.seam) {
|
|
239
|
+
return `linear-gradient(to right, transparent calc(50% - 1px), ${colour} 0 calc(50% + 1px), transparent 0)`;
|
|
240
|
+
}
|
|
241
|
+
const [a, b] = (layer.stops ?? [stop(layer, layer.from), stop(layer, layer.to)]).map(num);
|
|
242
|
+
const from =
|
|
243
|
+
a === 0 ? `${colour} 0` : `transparent calc(${a}% - 0.5px), ${colour} calc(${a}% + 0.5px)`;
|
|
244
|
+
const to = b === 100 ? "100%" : `calc(${b}% - 0.5px), transparent calc(${b}% + 0.5px)`;
|
|
245
|
+
return `linear-gradient(${DIRECTION[layer.kind]}, ${from} ${to})`;
|
|
166
246
|
};
|
|
167
247
|
|
|
168
248
|
const tilePosition = (start, size) => (size === 100 ? "0" : pct((start * 100) / (100 - size)));
|
|
169
249
|
|
|
170
|
-
const bands = (layers,
|
|
250
|
+
const bands = (layers, colour) => ({
|
|
171
251
|
"background-image": layers
|
|
172
|
-
.map((l) =>
|
|
173
|
-
gradient(l, l.stripe ? "var(--fimbriation, transparent)" : `var(--color-${tincture})`),
|
|
174
|
-
)
|
|
252
|
+
.map((l) => gradient(l, l.stripe ? "var(--fimbriation, transparent)" : colour))
|
|
175
253
|
.join(", "),
|
|
176
254
|
"background-size": layers
|
|
177
255
|
.map(({ tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }) => `${pct(x1 - x0)} ${pct(y1 - y0)}`)
|
|
@@ -189,6 +267,19 @@ const bands = (layers, tincture) => ({
|
|
|
189
267
|
// box is shorter, so they are slightly squashed.
|
|
190
268
|
const SCALLOP = 14;
|
|
191
269
|
const SCALLOPS = { chevron: 10 };
|
|
270
|
+
|
|
271
|
+
// A quarter of the ordinary's width, in real units. The pile tapers, so its is set by eye.
|
|
272
|
+
// prettier-ignore
|
|
273
|
+
const VOIDS = {
|
|
274
|
+
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,
|
|
276
|
+
};
|
|
277
|
+
for (const ordinary of REVERSIBLE) {
|
|
278
|
+
if (SCALLOPS[ordinary]) {
|
|
279
|
+
SCALLOPS[`${ordinary}-reversed`] = SCALLOPS[ordinary];
|
|
280
|
+
}
|
|
281
|
+
VOIDS[`${ordinary}-reversed`] = VOIDS[ordinary];
|
|
282
|
+
}
|
|
192
283
|
const DEPTH = 0.35;
|
|
193
284
|
|
|
194
285
|
const at = ([x, y]) => point([(x * 100) / W, (y * 100) / H]);
|
|
@@ -236,14 +327,14 @@ const crossing = (a, v, b, first, second) => {
|
|
|
236
327
|
: null;
|
|
237
328
|
};
|
|
238
329
|
|
|
239
|
-
|
|
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) => {
|
|
240
332
|
const inward = signedArea(vertices) > 0 ? "ccw" : "cw";
|
|
241
333
|
const pts = vertices.map(real);
|
|
334
|
+
const straight = vertices.map((v, i) => onShieldEdge(v, vertices[(i + 1) % vertices.length]));
|
|
242
335
|
const edges = pts.map((a, i) => {
|
|
243
336
|
const j = (i + 1) % pts.length;
|
|
244
|
-
return
|
|
245
|
-
? [{ to: pts[j] }]
|
|
246
|
-
: line(a, pts[j], scallop, inward);
|
|
337
|
+
return straight[i] ? [{ to: pts[j] }] : line(a, pts[j], scallop, inward);
|
|
247
338
|
});
|
|
248
339
|
|
|
249
340
|
// Where arcs cross at a corner, e.g. a chevron engrailed's peak, end both at the
|
|
@@ -266,22 +357,41 @@ const shape = (vertices, line, scallop) => {
|
|
|
266
357
|
}
|
|
267
358
|
});
|
|
268
359
|
|
|
269
|
-
const commands = edges
|
|
270
|
-
.
|
|
271
|
-
.map(({ to, r, sweep }) =>
|
|
360
|
+
const commands = edges.flatMap((edge, i) => {
|
|
361
|
+
const segments = edge.map(({ to, r, sweep }) =>
|
|
272
362
|
r
|
|
273
363
|
? `arc to ${at(to)} of ${pct((r / W) * 100)} ${pct((r / H) * 100)} ${sweep}`
|
|
274
364
|
: `line to ${at(to)}`,
|
|
275
365
|
);
|
|
276
|
-
|
|
366
|
+
if (!extend) {
|
|
367
|
+
return segments;
|
|
368
|
+
}
|
|
369
|
+
const outside = straight.at(i - 1);
|
|
370
|
+
if (straight[i]) {
|
|
371
|
+
const out = `line to ${extend(vertices[(i + 1) % vertices.length])}`;
|
|
372
|
+
return outside ? [out] : [`line to ${extend(vertices[i])}`, out];
|
|
373
|
+
}
|
|
374
|
+
return outside ? [`line to ${at(pts[i])}`, ...segments] : segments;
|
|
375
|
+
});
|
|
376
|
+
const from = extend ? extend(vertices[0]) : at(start);
|
|
377
|
+
return `shape(from ${from}, ${commands.join(", ")}, close)`;
|
|
277
378
|
};
|
|
278
379
|
|
|
279
|
-
const betweenVars = ({ size, points }) => {
|
|
380
|
+
const betweenVars = ({ size, points, counts = {} }) => {
|
|
280
381
|
const vars = { "--between-size": `${size}%` };
|
|
281
382
|
points.forEach(([left, top], i) => {
|
|
282
383
|
vars[`--between-${i + 1}-x`] = `${left}%`;
|
|
283
384
|
vars[`--between-${i + 1}-y`] = `${top}%`;
|
|
284
385
|
});
|
|
386
|
+
for (const [count, override] of Object.entries(counts)) {
|
|
387
|
+
if (override.size !== undefined) {
|
|
388
|
+
vars[`--between-${count}-size`] = `${override.size}%`;
|
|
389
|
+
}
|
|
390
|
+
override.points.forEach(([left, top], i) => {
|
|
391
|
+
vars[`--between-${count}-${i + 1}-x`] = `${left}%`;
|
|
392
|
+
vars[`--between-${count}-${i + 1}-y`] = `${top}%`;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
285
395
|
return vars;
|
|
286
396
|
};
|
|
287
397
|
|
|
@@ -296,13 +406,46 @@ const onVars = ({ size, counts }) => {
|
|
|
296
406
|
return vars;
|
|
297
407
|
};
|
|
298
408
|
|
|
299
|
-
|
|
409
|
+
// Every layout var an ordinary sets on its shield, for an inescutcheon or canton to reset.
|
|
410
|
+
const layoutVars = () => {
|
|
411
|
+
const allVars = [];
|
|
412
|
+
|
|
413
|
+
for (const division of Object.values(BETWEEN)) {
|
|
414
|
+
const vars = betweenVars(division);
|
|
415
|
+
allVars.push(...Object.keys(vars));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const division of Object.values(ON)) {
|
|
419
|
+
const vars = onVars(division);
|
|
420
|
+
allVars.push(...Object.keys(vars));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const chiefVars = ["--field-top", "--chief-points-y", "--chief-points-size"];
|
|
424
|
+
allVars.push(...chiefVars);
|
|
425
|
+
|
|
426
|
+
return [...new Set(allVars)];
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Each tincture's class suffix and colour, and "of the first" (see --the-first in grailwind.css).
|
|
430
|
+
const PAINTS = [
|
|
431
|
+
...TINCTURES.map((tincture) => [tincture, `var(--color-${tincture})`]),
|
|
432
|
+
[OF_THE_FIRST, "var(--the-first, transparent)"],
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
export default plugin(({ addBase, addUtilities, theme }) => {
|
|
436
|
+
// Unset on an inescutcheon, a canton or a part of a divided shield, so it can't inherit its
|
|
437
|
+
// shield's layout. In the base layer, so an ordinary on it still sets its own.
|
|
438
|
+
addBase({
|
|
439
|
+
".inescutcheon, .canton, :is(.escutcheon, .escutcheon *):is(.quarterly, .per-pale, .per-fess) > *":
|
|
440
|
+
Object.fromEntries(layoutVars().map((name) => [name, "initial"])),
|
|
441
|
+
});
|
|
442
|
+
|
|
300
443
|
const utilities = {};
|
|
301
444
|
|
|
302
|
-
for (const tincture of
|
|
445
|
+
for (const [tincture, colour] of PAINTS) {
|
|
303
446
|
utilities[`.fimbriated-${tincture}`] = {
|
|
304
447
|
"--fimbriated": "1",
|
|
305
|
-
"--fimbriation":
|
|
448
|
+
"--fimbriation": colour,
|
|
306
449
|
};
|
|
307
450
|
}
|
|
308
451
|
|
|
@@ -329,10 +472,23 @@ export default plugin(({ addUtilities, theme }) => {
|
|
|
329
472
|
});
|
|
330
473
|
|
|
331
474
|
for (const [ordinary, vertices] of Object.entries(ORDINARIES)) {
|
|
475
|
+
const chief = ordinary === "chief";
|
|
476
|
+
const placed = chief ? OVER_BORDURE : { inset: "var(--field-top, 0%) 0 0" };
|
|
477
|
+
const plain = chief
|
|
478
|
+
? `polygon(${vertices.map(outer).join(", ")}) content-box`
|
|
479
|
+
: polygon(vertices);
|
|
480
|
+
// 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
|
+
];
|
|
487
|
+
|
|
332
488
|
// Covers the whole shield, so the swapped field lines up with the real one,
|
|
333
489
|
// and the clip moves the ordinary below any chief instead.
|
|
334
|
-
|
|
335
|
-
inset: "0",
|
|
490
|
+
const counterchanged = {
|
|
491
|
+
...(chief ? OVER_BORDURE : { inset: "0" }),
|
|
336
492
|
// Registered not to inherit, so a shield inside another can't pick up its swap.
|
|
337
493
|
...Object.fromEntries(
|
|
338
494
|
["color", "image", "size", "position", "repeat"].map((n) => [
|
|
@@ -345,55 +501,77 @@ export default plugin(({ addUtilities, theme }) => {
|
|
|
345
501
|
"background-size": "var(--counterchange-size, auto)",
|
|
346
502
|
"background-position": "var(--counterchange-position, 0 0)",
|
|
347
503
|
"background-repeat": "var(--counterchange-repeat, repeat)",
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
504
|
+
// The swapped field's tiles would wrap into the bordure's strips, so the bordure shows there.
|
|
505
|
+
...(chief && { "background-clip": "content-box" }),
|
|
506
|
+
};
|
|
507
|
+
utilities[`.${ordinary}-counterchanged`] = ordinaryRules(ordinary, {
|
|
508
|
+
...counterchanged,
|
|
509
|
+
"clip-path": chief ? plain : `polygon(${vertices.map(pointBelowChief).join(", ")})`,
|
|
352
510
|
});
|
|
353
511
|
|
|
354
|
-
for (const tincture of
|
|
512
|
+
for (const [tincture, colour] of PAINTS) {
|
|
355
513
|
utilities[`.${ordinary}-${tincture}`] = ordinaryRules(ordinary, {
|
|
356
|
-
|
|
514
|
+
...placed,
|
|
357
515
|
...(BANDS[ordinary]
|
|
358
516
|
? {
|
|
359
517
|
"--fimbriated": "inherit",
|
|
360
518
|
"--fimbriation": "inherit",
|
|
361
|
-
"background-color": `var(--fimbriation,
|
|
362
|
-
...bands(BANDS[ordinary],
|
|
519
|
+
"background-color": `var(--fimbriation, ${colour})`,
|
|
520
|
+
...bands(BANDS[ordinary], colour),
|
|
363
521
|
"clip-path": widenedPolygon(vertices),
|
|
364
522
|
}
|
|
365
523
|
: {
|
|
366
|
-
"background-color":
|
|
367
|
-
"clip-path":
|
|
524
|
+
"background-color": colour,
|
|
525
|
+
"clip-path": plain,
|
|
368
526
|
}),
|
|
369
527
|
});
|
|
370
|
-
// Not fimbriated.
|
|
528
|
+
// Not fimbriated.
|
|
371
529
|
for (const [name, line] of Object.entries(LINES)) {
|
|
372
530
|
utilities[`.${ordinary}-${name}-${tincture}`] = ordinaryRules(ordinary, {
|
|
373
|
-
|
|
374
|
-
"background-color":
|
|
375
|
-
"clip-path":
|
|
531
|
+
...placed,
|
|
532
|
+
"background-color": colour,
|
|
533
|
+
"clip-path": lined(line),
|
|
376
534
|
});
|
|
377
535
|
}
|
|
378
536
|
}
|
|
379
537
|
|
|
380
538
|
// A plain polygon, without the fimbriation vars, so fimbriated-* can't reach a fur ordinary.
|
|
381
539
|
for (const fur of Object.keys(FURS)) {
|
|
382
|
-
const pseudo = {
|
|
383
|
-
inset: ordinary === "chief" ? "0" : "var(--field-top, 0%) 0 0",
|
|
384
|
-
...furBackground(fur, theme),
|
|
385
|
-
};
|
|
540
|
+
const pseudo = { ...placed, ...furBackground(fur, theme) };
|
|
386
541
|
utilities[`.${ordinary}-${fur}`] = ordinaryRules(ordinary, {
|
|
387
542
|
...pseudo,
|
|
388
|
-
"clip-path":
|
|
543
|
+
"clip-path": plain,
|
|
389
544
|
});
|
|
390
545
|
for (const [name, line] of Object.entries(LINES)) {
|
|
391
546
|
utilities[`.${ordinary}-${name}-${fur}`] = ordinaryRules(ordinary, {
|
|
392
547
|
...pseudo,
|
|
393
|
-
"clip-path":
|
|
548
|
+
"clip-path": lined(line),
|
|
394
549
|
});
|
|
395
550
|
}
|
|
396
551
|
}
|
|
552
|
+
|
|
553
|
+
// Not fimbriated.
|
|
554
|
+
if (VOIDABLE.includes(ordinary)) {
|
|
555
|
+
const clip = voidedPolygon(vertices, VOIDS[ordinary]);
|
|
556
|
+
for (const [tincture, colour] of PAINTS) {
|
|
557
|
+
utilities[`.${ordinary}-voided-${tincture}`] = ordinaryRules(ordinary, {
|
|
558
|
+
...placed,
|
|
559
|
+
"background-color": colour,
|
|
560
|
+
"clip-path": clip,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
for (const fur of Object.keys(FURS)) {
|
|
564
|
+
utilities[`.${ordinary}-voided-${fur}`] = ordinaryRules(ordinary, {
|
|
565
|
+
...placed,
|
|
566
|
+
...furBackground(fur, theme),
|
|
567
|
+
"clip-path": clip,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
utilities[`.${ordinary}-voided-counterchanged`] = ordinaryRules(ordinary, {
|
|
571
|
+
...counterchanged,
|
|
572
|
+
"clip-path": voidedPolygon(vertices, VOIDS[ordinary], pointBelowChief),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
397
575
|
}
|
|
398
576
|
|
|
399
577
|
addUtilities(utilities);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
"tailwind",
|
|
17
17
|
"heraldry",
|
|
18
18
|
"blazon",
|
|
19
|
-
"css"
|
|
19
|
+
"css",
|
|
20
|
+
"coat-of-arms",
|
|
21
|
+
"coats-of-arms",
|
|
22
|
+
"tailwindcss-plugin",
|
|
23
|
+
"eslint"
|
|
20
24
|
],
|
|
21
25
|
"author": "Paul Firmin",
|
|
22
26
|
"license": "MIT",
|
|
23
27
|
"description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
|
|
24
28
|
"devDependencies": {
|
|
25
|
-
"@grailwind/eslint-plugin": "^0.1.0-beta.
|
|
29
|
+
"@grailwind/eslint-plugin": "^0.1.0-beta.2",
|
|
26
30
|
"@html-eslint/parser": "^0.67.0",
|
|
27
31
|
"@tailwindcss/cli": "^4.3.3",
|
|
28
32
|
"eslint": "^10.14.0",
|