grailwind 0.8.0 → 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 +57 -1
- package/README.md +13 -8
- package/charges.js +50 -15
- package/data.js +95 -14
- package/divisions.js +139 -45
- package/furs.js +181 -23
- package/grailwind.css +214 -30
- package/ordinaries.js +466 -21
- package/package.json +2 -2
- package/paths.js +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,60 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] - 2026-11-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Engrailed and invected bordures, e.g. `bordure-engrailed-or`. On a frame the field grows into the
|
|
14
|
+
bites of an engrailed one; off a frame they are plain.
|
|
15
|
+
- Bordures in a fur, e.g. `bordure-ermine` and `bordure-engrailed-vair`. Off a shield's field they
|
|
16
|
+
show the fur's ground.
|
|
17
|
+
- A bordure on a per-pale part follows the shield's outline and stops at the line of
|
|
18
|
+
impalement. A chief on that part goes under the bordure.
|
|
19
|
+
- Vair and potent, e.g. `vair`, `counter-vair`, `potent` and `vairy-or-gules`.
|
|
20
|
+
- `fur-<n>` sets how many tiles of a fur go across, e.g. `fur-3` for beffroi.
|
|
21
|
+
- More charge counts: 7, 9 and 10 on a plain field; 4 on a cross or saltire, 2 and 5 on a chevron
|
|
22
|
+
and 4 and 5 on a pale, fess or bend with `on`; 3 round a bend and 4, 9 and 10 round a chevron
|
|
23
|
+
with `between`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The bordure is an eighth of the shield's width, so the field inside it is smaller.
|
|
28
|
+
- **Breaking:** the shield is a frame round its field. `escutcheon` is now
|
|
29
|
+
`heater`, with the blazon on its one child:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<!-- before -->
|
|
33
|
+
<div class="escutcheon azure fess-or"></div>
|
|
34
|
+
<!-- after -->
|
|
35
|
+
<div class="heater"><div class="azure fess-or"></div></div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The heater is pointed, after Fox-Davies, and its outline is drawn just
|
|
39
|
+
inside its edge. An inescutcheon is a frame too, round its own field, and
|
|
40
|
+
pointed. A bordure on a shield's field follows its outline and scales with
|
|
41
|
+
it; on a part of a divided field or a canton, it's still a border.
|
|
42
|
+
|
|
43
|
+
- `@grailwind/eslint-plugin` 0.1.0-beta.5 needs `grailwind` 0.9.x. The vair
|
|
44
|
+
family isn't checked by the rule of tincture unless the `furs` option names
|
|
45
|
+
it. The new `field-in-shield` rule flags a blazon on the frame, and
|
|
46
|
+
`escutcheon`.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- A canton, inescutcheon or `charges` over all of a divided field with parts no longer takes a
|
|
51
|
+
part's place.
|
|
52
|
+
|
|
53
|
+
## [0.8.1] - 2026-11-13
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- Counterchanged charges `on` a counterchanged ordinary showed the ordinary's
|
|
58
|
+
swapped field and vanished. They now show the field's own tinctures.
|
|
59
|
+
- `chevronny-*` in a part of a split shield (a `per-pale` half, a `per-fess`
|
|
60
|
+
chief) broke at the centre line. It now follows the part, as `per-chevron`
|
|
61
|
+
does, and the arms meet.
|
|
62
|
+
|
|
9
63
|
## [0.8.0] - 2026-11-12
|
|
10
64
|
|
|
11
65
|
### Added
|
|
@@ -183,7 +237,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
183
237
|
Initial release: tinctures, divisions, ordinaries, charges, strict mode and
|
|
184
238
|
custom SVG charges.
|
|
185
239
|
|
|
186
|
-
[Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.
|
|
240
|
+
[Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.9.0...HEAD
|
|
241
|
+
[0.9.0]: https://github.com/pafirmin/grailwind/compare/v0.8.1...v0.9.0
|
|
242
|
+
[0.8.1]: https://github.com/pafirmin/grailwind/compare/v0.8.0...v0.8.1
|
|
187
243
|
[0.8.0]: https://github.com/pafirmin/grailwind/compare/v0.7.0...v0.8.0
|
|
188
244
|
[0.7.0]: https://github.com/pafirmin/grailwind/compare/v0.6.0...v0.7.0
|
|
189
245
|
[0.6.0]: https://github.com/pafirmin/grailwind/compare/v0.5.0...v0.6.0
|
package/README.md
CHANGED
|
@@ -11,11 +11,13 @@ Heraldic blazon for [Tailwind CSS](https://tailwindcss.com). Draw coats of
|
|
|
11
11
|
arms, or style anything else, in the language heralds have used for 800 years:
|
|
12
12
|
|
|
13
13
|
```html
|
|
14
|
-
<div class="
|
|
15
|
-
<div class="
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
<div class="heater">
|
|
15
|
+
<div class="azure chevron-or">
|
|
16
|
+
<div class="charges between">
|
|
17
|
+
<i class="mullet argent"></i>
|
|
18
|
+
<i class="mullet argent"></i>
|
|
19
|
+
<i class="mullet argent"></i>
|
|
20
|
+
</div>
|
|
19
21
|
</div>
|
|
20
22
|
</div>
|
|
21
23
|
```
|
|
@@ -53,9 +55,12 @@ tinctures replace Tailwind's colours entirely.
|
|
|
53
55
|
|
|
54
56
|
- **Tinctures:** a tincture on its own sets the background (`azure`), and
|
|
55
57
|
Tailwind's prefixes work as usual (`text-or`). There are also the furs `ermine`,
|
|
56
|
-
`ermines`, `erminois` and `
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
`ermines`, `erminois`, `pean`, `vair` and `potent` (with `vairy-or-gules` and the like), which
|
|
59
|
+
work in divisions too, and `fur-<n>` sets their density.
|
|
60
|
+
- **The shield:** `heater`, a pointed shield round its one child, the field, plus `bordure-*` (a
|
|
61
|
+
eighth of the shield wide, also `bordure-engrailed-*` and `bordure-invected-*`), `inescutcheon` (a
|
|
62
|
+
small shield at the centre) and `canton` (a ninth of the shield in dexter chief), each with its
|
|
63
|
+
own field and charges.
|
|
59
64
|
- **Divided fields:** `per-pale-or-gules`, `quarterly-azure-argent`,
|
|
60
65
|
`gyronny-or-sable`, `barry-argent-azure`, `bendy-argent-gules`, `chequy-or-azure` and more, or
|
|
61
66
|
`per-pale`, `per-fess` and `quarterly` for layout.
|
package/charges.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Grailwind CSS: charges and their arrangement.
|
|
3
3
|
*/
|
|
4
4
|
import plugin from "tailwindcss/plugin";
|
|
5
|
-
import { POINTS, ROUNDELS } from "./data.js";
|
|
5
|
+
import { BETWEEN, BETWEEN_LINED, ON, POINTS, ROUNDELS } from "./data.js";
|
|
6
6
|
import { AT } from "./furs.js";
|
|
7
7
|
|
|
8
8
|
const star = (cx, cy, r, points = 5) => {
|
|
@@ -67,9 +67,12 @@ const DEFAULTS = {
|
|
|
67
67
|
1: { size: 50, points: [[50, 45]] },
|
|
68
68
|
2: { size: 30, points: [[50, 24], [50, 66]] },
|
|
69
69
|
3: { size: 28, points: [[28, 25], [72, 25], [50, 64]] },
|
|
70
|
-
4: { size: 24, points: [[30, 24], [70, 24], [
|
|
71
|
-
5: { size: 22, points: [[27, 20], [73, 20], [50, 42], [
|
|
70
|
+
4: { size: 24, points: [[30, 24], [70, 24], [30, 60], [70, 60]] },
|
|
71
|
+
5: { size: 22, points: [[27, 20], [73, 20], [50, 42], [27, 64], [73, 64]] },
|
|
72
72
|
6: { size: 20, points: [[20, 18], [50, 18], [80, 18], [35, 42], [65, 42], [50, 66]] },
|
|
73
|
+
7: { size: 20, points: [[24, 18], [50, 18], [76, 18], [24, 42], [50, 42], [76, 42], [50, 66]] },
|
|
74
|
+
9: { size: 17, points: [[22, 16], [50, 16], [78, 16], [24, 38], [50, 38], [76, 38], [32, 60], [50, 60], [68, 60]] },
|
|
75
|
+
10: { size: 15, points: [[17, 15], [39, 15], [61, 15], [83, 15], [28, 35], [50, 35], [72, 35], [39, 55], [61, 55], [50, 75]] },
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
const POINT_SIZE = 22;
|
|
@@ -149,17 +152,29 @@ const inheritCounterchange = Object.fromEntries(
|
|
|
149
152
|
COUNTERCHANGE.map((n) => [`--counterchange-${n}`, "inherit"]),
|
|
150
153
|
);
|
|
151
154
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
155
|
+
// What a charge on a counterchanged ordinary is counterchanged against: the field's own
|
|
156
|
+
// tinctures, handed down by the ordinary to `on` containers only.
|
|
157
|
+
const inheritOnCounterchange = Object.fromEntries(
|
|
158
|
+
COUNTERCHANGE.map((n) => [`--on-counterchange-${n}`, "inherit"]),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// A layer the size of the field, as the heater (or a part of a divided shield, or a canton) is
|
|
162
|
+
// the containing block for fixed positioning, and the field lies inside its bordure by
|
|
163
|
+
// --layer-inset. The charge's mask cuts it to shape.
|
|
154
164
|
const counterchanged = {
|
|
155
165
|
...inheritCounterchange,
|
|
166
|
+
...inheritOnCounterchange,
|
|
156
167
|
"&::before": {
|
|
157
168
|
content: '""',
|
|
158
169
|
position: "fixed",
|
|
159
|
-
inset: "
|
|
170
|
+
inset: "var(--layer-inset, 0px)",
|
|
160
171
|
...inheritCounterchange,
|
|
172
|
+
...inheritOnCounterchange,
|
|
161
173
|
...Object.fromEntries(
|
|
162
|
-
COUNTERCHANGE.map((n) => [
|
|
174
|
+
COUNTERCHANGE.map((n) => [
|
|
175
|
+
`background-${n}`,
|
|
176
|
+
`var(--on-counterchange-${n}, var(--counterchange-${n}))`,
|
|
177
|
+
]),
|
|
163
178
|
),
|
|
164
179
|
},
|
|
165
180
|
};
|
|
@@ -176,6 +191,15 @@ const atPoint = (name) => {
|
|
|
176
191
|
: place([left, top], POINT_SIZE);
|
|
177
192
|
};
|
|
178
193
|
|
|
194
|
+
/**
|
|
195
|
+
* @param {object[]} layouts Entries of `ON`, `BETWEEN` or `BETWEEN_LINED`, with optional `counts`.
|
|
196
|
+
* @returns {number[]} Every number of charges some layout places, ascending.
|
|
197
|
+
*/
|
|
198
|
+
const countKeys = (layouts) =>
|
|
199
|
+
[...new Set(layouts.flatMap((layout) => Object.keys(layout.counts ?? {}).map(Number)))].sort(
|
|
200
|
+
(a, b) => a - b,
|
|
201
|
+
);
|
|
202
|
+
|
|
179
203
|
const hasCount = (n) => `:has(> :nth-child(${n}):last-child)`;
|
|
180
204
|
|
|
181
205
|
export default plugin(({ addUtilities, matchUtilities }) => {
|
|
@@ -200,7 +224,7 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
200
224
|
const charges = {
|
|
201
225
|
position: "absolute",
|
|
202
226
|
inset: "var(--field-top, 0%) 0 0",
|
|
203
|
-
// Above the
|
|
227
|
+
// Above the field's ordinaries. Charges on the chief go higher, with it (see below).
|
|
204
228
|
"z-index": "1",
|
|
205
229
|
...inheritCounterchange,
|
|
206
230
|
"& > *": {
|
|
@@ -230,8 +254,11 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
230
254
|
utilities[`.${name}`] = arrangement;
|
|
231
255
|
}
|
|
232
256
|
|
|
257
|
+
// Charges on the chief go over it (z 3), which goes over a frame's bordure ring (z 2).
|
|
258
|
+
const onChief = { "z-index": "4" };
|
|
259
|
+
|
|
233
260
|
for (const [name, counts] of Object.entries(ALONG)) {
|
|
234
|
-
const arrangement = {};
|
|
261
|
+
const arrangement = name === "in-chief" ? { "&.charges": onChief } : {};
|
|
235
262
|
for (const [count, points] of Object.entries(counts)) {
|
|
236
263
|
points.forEach((point, i) => {
|
|
237
264
|
arrangement[`&${hasCount(count)} > :nth-child(${i + 1})`] = atPoint(point);
|
|
@@ -249,8 +276,12 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
249
276
|
"--charge-width": "var(--charge-size, var(--between-size))",
|
|
250
277
|
};
|
|
251
278
|
}
|
|
252
|
-
// The counts some ordinary overrides in
|
|
253
|
-
|
|
279
|
+
// The counts some ordinary overrides in `counts` (see data.js).
|
|
280
|
+
const betweenCounts = countKeys([
|
|
281
|
+
...Object.values(BETWEEN),
|
|
282
|
+
...Object.values(BETWEEN_LINED).flatMap((lines) => Object.values(lines)),
|
|
283
|
+
]);
|
|
284
|
+
for (const count of betweenCounts) {
|
|
254
285
|
for (let i = 1; i <= count; i++) {
|
|
255
286
|
between[`&${hasCount(count)} > :nth-child(${i})`] = {
|
|
256
287
|
left: `var(--between-${count}-${i}-x, var(--between-${i}-x))`,
|
|
@@ -264,7 +295,7 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
264
295
|
// Falls back to the plain-field default when the ordinary has no points for this count.
|
|
265
296
|
// An undefined var() would otherwise unset `left`/`top` instead of deferring to the :where() default.
|
|
266
297
|
const on = {};
|
|
267
|
-
for (const count of
|
|
298
|
+
for (const count of countKeys(Object.values(ON))) {
|
|
268
299
|
const fallback = DEFAULTS[count];
|
|
269
300
|
for (let i = 1; i <= count; i++) {
|
|
270
301
|
const [left, top] = fallback?.points[i - 1] ?? [];
|
|
@@ -275,14 +306,18 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
275
306
|
on[`&${hasCount(count)} > :nth-child(${i})`] = {
|
|
276
307
|
left: at("x", left),
|
|
277
308
|
top: at("y", top),
|
|
278
|
-
"--charge-width": `var(--charge-size, var(--on-size, ${fallback?.size ?? 16}%))`,
|
|
309
|
+
"--charge-width": `var(--charge-size, var(--on-${count}-size, var(--on-size, ${fallback?.size ?? 16}%)))`,
|
|
279
310
|
};
|
|
280
311
|
}
|
|
281
312
|
}
|
|
282
|
-
|
|
313
|
+
// Nested, so Tailwind keeps these apart from the per-count rules above.
|
|
314
|
+
utilities[".on"] = { "&": inheritOnCounterchange, ...on };
|
|
283
315
|
|
|
284
316
|
for (const name of Object.keys(POINTS)) {
|
|
285
|
-
utilities[`.in-${name}`] = {
|
|
317
|
+
utilities[`.in-${name}`] = {
|
|
318
|
+
".charges > &": atPoint(name),
|
|
319
|
+
...(CHIEF_POINTS.has(name) && { ".charges:has(> &)": onChief }),
|
|
320
|
+
};
|
|
286
321
|
}
|
|
287
322
|
|
|
288
323
|
// The whole field, under the ordinaries and the chief. A custom charge has no tile, so it
|
package/data.js
CHANGED
|
@@ -31,6 +31,12 @@ export const DIVISIONS = [
|
|
|
31
31
|
"chequy",
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
// The classes that frame a field: a shape round one child, which carries the blazon.
|
|
35
|
+
export const FRAMES = ["heater", "inescutcheon"];
|
|
36
|
+
|
|
37
|
+
// The lines a bordure can have besides plain, e.g. `bordure-engrailed-or`.
|
|
38
|
+
export const BORDURE_LINES = ["engrailed", "invected"];
|
|
39
|
+
|
|
34
40
|
// Ordinaries that can be reversed, e.g. `pile-reversed-or`: the same shape upside down.
|
|
35
41
|
export const REVERSIBLE = ["chevron", "pall", "pile"];
|
|
36
42
|
const reversed = (ordinary) => `${ordinary}-reversed`;
|
|
@@ -100,12 +106,47 @@ export const VOIDABLE = [
|
|
|
100
106
|
...REVERSIBLE.map(reversed),
|
|
101
107
|
];
|
|
102
108
|
|
|
103
|
-
//
|
|
109
|
+
// The vair family: plain vair and potent are argent and azure, and the -y forms take a metal and a
|
|
110
|
+
// colour, metal first as blazon writes them.
|
|
111
|
+
const VAIRS = ["vair", "counter-vair", "potent", "counter-potent"];
|
|
112
|
+
const VAIRY = {
|
|
113
|
+
vair: "vairy",
|
|
114
|
+
"counter-vair": "counter-vairy",
|
|
115
|
+
potent: "potenty",
|
|
116
|
+
"counter-potent": "counter-potenty",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Each fur's pattern: ermine's spots, or the vair family's panes.
|
|
120
|
+
export const FUR_PATTERNS = {
|
|
121
|
+
ermine: "ermine",
|
|
122
|
+
ermines: "ermine",
|
|
123
|
+
erminois: "ermine",
|
|
124
|
+
pean: "ermine",
|
|
125
|
+
...Object.fromEntries(VAIRS.map((pattern) => [pattern, pattern])),
|
|
126
|
+
...Object.fromEntries(
|
|
127
|
+
VAIRS.flatMap((pattern) =>
|
|
128
|
+
METALS.flatMap((metal) =>
|
|
129
|
+
COLOURS.map((colour) => [`${VAIRY[pattern]}-${metal}-${colour}`, pattern]),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Each fur's [ground, drawn] tinctures: ermine's spots, or the vair family's panes that point up in
|
|
136
|
+
// the top row, which is the first tincture blazoned.
|
|
104
137
|
export const FURS = {
|
|
105
138
|
ermine: ["argent", "sable"],
|
|
106
139
|
ermines: ["sable", "argent"],
|
|
107
140
|
erminois: ["or", "sable"],
|
|
108
141
|
pean: ["sable", "or"],
|
|
142
|
+
...Object.fromEntries(VAIRS.map((pattern) => [pattern, ["azure", "argent"]])),
|
|
143
|
+
...Object.fromEntries(
|
|
144
|
+
VAIRS.flatMap((pattern) =>
|
|
145
|
+
METALS.flatMap((metal) =>
|
|
146
|
+
COLOURS.map((colour) => [`${VAIRY[pattern]}-${metal}-${colour}`, [colour, metal]]),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
109
150
|
};
|
|
110
151
|
|
|
111
152
|
// The points of the shield, as [left%, top%], for placing a single charge, e.g. `in-dexter-chief`.
|
|
@@ -142,11 +183,16 @@ export const BETWEEN = {
|
|
|
142
183
|
2: { points: [[50, 15], [50, 78]] },
|
|
143
184
|
6: { size: 14, points: [[20, 14], [50, 14], [80, 14], [32, 70], [68, 70], [50, 86]] } } },
|
|
144
185
|
bend: { size: 24, points: [[70, 26], [30, 68]], counts: {
|
|
186
|
+
3: { size: 18, points: [[60, 13], [86, 36], [30, 68]] },
|
|
145
187
|
6: { size: 14, points: [[48, 9], [76, 12], [84, 38], [9, 48], [18, 72], [38, 84]] } } },
|
|
146
188
|
"bend-sinister": { size: 24, points: [[30, 26], [70, 68]], counts: {
|
|
189
|
+
3: { size: 18, points: [[40, 13], [14, 36], [70, 68]] },
|
|
147
190
|
6: { size: 14, points: [[52, 9], [24, 12], [16, 38], [91, 48], [82, 72], [62, 84]] } } },
|
|
148
191
|
chevron: { size: 22, points: [[24, 22], [76, 22], [50, 70]], counts: {
|
|
149
|
-
|
|
192
|
+
4: { size: 15, points: [[20, 16], [50, 9], [80, 16], [50, 74]] },
|
|
193
|
+
6: { size: 12, points: [[18, 14], [50, 11], [82, 14], [40, 77], [60, 77], [50, 89]] },
|
|
194
|
+
9: { size: 11, points: [[14, 12], [30, 12], [22, 26], [70, 12], [86, 12], [78, 26], [43, 73], [57, 73], [50, 86]] },
|
|
195
|
+
10: { size: 11, points: [[14, 12], [30, 12], [22, 26], [70, 12], [86, 12], [78, 26], [43, 71], [57, 71], [43, 85], [57, 85]] } } },
|
|
150
196
|
cross: { size: 18, points: [[20, 17], [80, 17], [24, 72], [76, 72]] },
|
|
151
197
|
saltire: { size: 16, points: [[50, 13], [14, 45], [86, 45], [50, 84]], counts: {
|
|
152
198
|
2: { points: [[14, 45], [86, 45]] } } },
|
|
@@ -158,18 +204,53 @@ export const BETWEEN = {
|
|
|
158
204
|
// `on` charges shrink on lines that cut deep into the ordinary, by line.
|
|
159
205
|
export const ON_SCALE = { engrailed: 0.8, indented: 0.9 };
|
|
160
206
|
|
|
161
|
-
// Where charges go for "on", along the ordinary's centre line, by number of charges.
|
|
207
|
+
// Where charges go for "on", along the ordinary's centre line, by number of charges. A count
|
|
208
|
+
// can set its own size, else the ordinary's applies.
|
|
162
209
|
// prettier-ignore
|
|
163
210
|
export const ON = {
|
|
164
|
-
pale: { size: 20, counts: {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
211
|
+
pale: { size: 20, counts: {
|
|
212
|
+
1: { points: [[50, 45]] },
|
|
213
|
+
2: { points: [[50, 28], [50, 62]] },
|
|
214
|
+
3: { points: [[50, 18], [50, 45], [50, 72]] },
|
|
215
|
+
4: { size: 18, points: [[50, 13], [50, 35], [50, 57], [50, 79]] },
|
|
216
|
+
5: { size: 15, points: [[50, 12], [50, 29], [50, 46], [50, 63], [50, 80]] } } },
|
|
217
|
+
fess: { size: 22, counts: {
|
|
218
|
+
1: { points: [[50, 45]] },
|
|
219
|
+
2: { points: [[30, 45], [70, 45]] },
|
|
220
|
+
3: { points: [[20, 45], [50, 45], [80, 45]] },
|
|
221
|
+
4: { size: 18, points: [[17, 45], [39, 45], [61, 45], [83, 45]] },
|
|
222
|
+
5: { size: 15, points: [[14, 45], [32, 45], [50, 45], [68, 45], [86, 45]] } } },
|
|
223
|
+
bend: { size: 18, counts: {
|
|
224
|
+
1: { points: [[50, 50]] },
|
|
225
|
+
2: { points: [[33, 33], [67, 67]] },
|
|
226
|
+
3: { points: [[24, 24], [50, 50], [76, 76]] },
|
|
227
|
+
4: { size: 16, points: [[22, 22], [40.7, 40.7], [59.3, 59.3], [78, 78]] },
|
|
228
|
+
5: { size: 12, points: [[17, 17], [32, 32], [47, 47], [62, 62], [77, 77]] } } },
|
|
229
|
+
"bend-sinister": { size: 18, counts: {
|
|
230
|
+
1: { points: [[50, 50]] },
|
|
231
|
+
2: { points: [[67, 33], [33, 67]] },
|
|
232
|
+
3: { points: [[76, 24], [50, 50], [24, 76]] },
|
|
233
|
+
4: { size: 16, points: [[78, 22], [59.3, 40.7], [40.7, 59.3], [22, 78]] },
|
|
234
|
+
5: { size: 12, points: [[83, 17], [68, 32], [53, 47], [38, 62], [23, 77]] } } },
|
|
235
|
+
chevron: { size: 15, counts: {
|
|
236
|
+
1: { points: [[50, 36]] },
|
|
237
|
+
2: { points: [[28, 57], [72, 57]] },
|
|
238
|
+
3: { points: [[20, 65], [50, 36], [80, 65]] },
|
|
239
|
+
5: { size: 13, points: [[50, 36], [35, 51], [65, 51], [20, 66], [80, 66]] } } },
|
|
240
|
+
cross: { size: 15, counts: {
|
|
241
|
+
1: { points: [[50, 45]] },
|
|
242
|
+
4: { points: [[50, 15], [18, 45], [82, 45], [50, 80]] },
|
|
243
|
+
5: { points: [[50, 15], [18, 45], [50, 45], [82, 45], [50, 80]] } } },
|
|
244
|
+
saltire: { size: 16, counts: {
|
|
245
|
+
1: { points: [[50, 45]] },
|
|
246
|
+
4: { points: [[22, 19.8], [78, 19.8], [22, 70.2], [78, 70.2]] },
|
|
247
|
+
5: { points: [[22, 19.8], [78, 19.8], [50, 45], [22, 70.2], [78, 70.2]] } } },
|
|
248
|
+
pall: { size: 16, counts: {
|
|
249
|
+
1: { points: [[50, 40]] },
|
|
250
|
+
3: { points: [[26, 20.8], [74, 20.8], [50, 75]] } } },
|
|
251
|
+
pile: { size: 14, counts: {
|
|
252
|
+
1: { points: [[50, 25]] },
|
|
253
|
+
3: { points: [[50, 14], [50, 34], [50, 54]] } } },
|
|
173
254
|
};
|
|
174
255
|
|
|
175
256
|
const mirror = (points) => points.map(([x, y]) => [x, 100 - y]);
|
|
@@ -185,7 +266,7 @@ for (const ordinary of REVERSIBLE) {
|
|
|
185
266
|
};
|
|
186
267
|
ON[reversed(ordinary)] = {
|
|
187
268
|
size: ON[ordinary].size,
|
|
188
|
-
counts: mapValues(ON[ordinary].counts, mirror),
|
|
269
|
+
counts: mapValues(ON[ordinary].counts, (count) => ({ ...count, points: mirror(count.points) })),
|
|
189
270
|
};
|
|
190
271
|
}
|
|
191
272
|
|
|
@@ -208,7 +289,7 @@ export const BETWEEN_LINED = {
|
|
|
208
289
|
indented: { points: [[70, 26], [24, 72]] },
|
|
209
290
|
},
|
|
210
291
|
chevron: {
|
|
211
|
-
embattled: { counts: { 6: { size: 10, points: [[16, 13], [50, 8], [84, 13], [40,
|
|
292
|
+
embattled: { counts: { 6: { size: 10, points: [[16, 13], [50, 8], [84, 13], [40, 77], [60, 77], [50, 89]] } } },
|
|
212
293
|
},
|
|
213
294
|
"chevron-reversed": {
|
|
214
295
|
embattled: { points: [[26, 76], [74, 76], [50, 24]] },
|
package/divisions.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import plugin from "tailwindcss/plugin";
|
|
5
5
|
import { FURS, TINCTURES } from "./data.js";
|
|
6
6
|
import { furLayers } from "./furs.js";
|
|
7
|
+
import { PATHS } from "./paths.js";
|
|
7
8
|
|
|
8
9
|
// The fess point, in % down the shield: horizontal lines of partition pass through it, as the
|
|
9
10
|
// cross and fess do.
|
|
@@ -26,16 +27,40 @@ const stripes = (to, pieces) => (a, b) => {
|
|
|
26
27
|
};
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Points along a drawn outline's curves and lines, densely, in fractions of its box.
|
|
32
|
+
* @param {Array[]} segments The drawing's segments, as in paths.js, in real units (100 × 120).
|
|
33
|
+
* @returns {number[][]} [x, y] points, each 0 to 1.
|
|
34
|
+
*/
|
|
35
|
+
const outlineFractions = (segments) => {
|
|
36
|
+
const points = [];
|
|
37
|
+
let from = [0, 0];
|
|
38
|
+
for (const [command, ...n] of segments) {
|
|
39
|
+
if (command === "M" || command === "L") {
|
|
40
|
+
from = [n[0], n[1]];
|
|
41
|
+
points.push(from);
|
|
42
|
+
} else if (command === "C") {
|
|
43
|
+
for (let i = 1; i <= 1000; i++) {
|
|
44
|
+
const t = i / 1000;
|
|
45
|
+
const [a, b, c, d] = [(1 - t) ** 3, 3 * (1 - t) ** 2 * t, 3 * (1 - t) * t ** 2, t ** 3];
|
|
46
|
+
points.push([
|
|
47
|
+
a * from[0] + b * n[0] + c * n[2] + d * n[4],
|
|
48
|
+
a * from[1] + b * n[1] + c * n[3] + d * n[5],
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
from = [n[4], n[5]];
|
|
52
|
+
} else if (command !== "Z") {
|
|
53
|
+
throw new Error(`bendy can't read ${command} in the heater's outline`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return points.map(([x, y]) => [x / 100, y / 120]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// How much of the corner-to-corner extent the shield covers, as the heater's base cuts the far
|
|
60
|
+
// corner away. On the 5:6 shield, a point's distance along the extent is (x + y) / 2 in fractions
|
|
61
|
+
// of the width and height, so it runs from the chief corner the bands start in to where a band's
|
|
62
|
+
// line is tangent to the base (drawings/heater.svg).
|
|
63
|
+
const BENDY_VISIBLE = Math.max(...outlineFractions(PATHS.heater).map(([x, y]) => (x + y) / 2));
|
|
39
64
|
|
|
40
65
|
// Diagonal bands parallel to per bend, `pieces` equal ones across the visible part of the shield,
|
|
41
66
|
// the last running to the edge. Explicit stops, not a repeating gradient, so a canton's square
|
|
@@ -50,31 +75,36 @@ const bendy = (to, pieces) => (a, b) => {
|
|
|
50
75
|
return layer(`linear-gradient(to ${to}, ${stops.join(", ")})`);
|
|
51
76
|
};
|
|
52
77
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// A
|
|
58
|
-
// Plain, the
|
|
59
|
-
//
|
|
78
|
+
// Four tiles, each half the box, so the lines follow the box's own proportions: a part of the
|
|
79
|
+
// shield is the whole one squeezed to fit, as per chevron's are. A tile's lines run parallel to its
|
|
80
|
+
// diagonal, dexter tiles `to bottom right` and sinister ones `to bottom left`.
|
|
81
|
+
// The period is 300/n of a tile's gradient line, which keeps a stripe's width on the whole shield.
|
|
82
|
+
// A lower tile starts half a gradient line further on, so its lines continue the upper tile's.
|
|
83
|
+
// Plain, the dexter and lower tiles overlap the others by 1px, hiding the seams. With a fur the
|
|
84
|
+
// transparent stripes would show the tile beneath, so they butt.
|
|
60
85
|
const chevronny = (pieces) => (a, b, fur) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
86
|
+
const period = 300 / pieces;
|
|
87
|
+
const lowerStart = (period - (50 % period)) % period;
|
|
88
|
+
const stripe = (to, start) =>
|
|
89
|
+
`repeating-linear-gradient(to bottom ${to}, ${a} ${start}% ${start + period / 2}%, ${b} 0 ${start + period}%)`;
|
|
90
|
+
const overlap = fur ? "50%" : "calc(50% + 1px)";
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"background-image": [
|
|
94
|
+
stripe("left", 0),
|
|
95
|
+
stripe("left", lowerStart),
|
|
96
|
+
stripe("right", 0),
|
|
97
|
+
stripe("right", lowerStart),
|
|
98
|
+
].join(", "),
|
|
99
|
+
"background-size": [
|
|
100
|
+
`50% 50%`,
|
|
101
|
+
`50% ${overlap}`,
|
|
102
|
+
`${overlap} 50%`,
|
|
103
|
+
`${overlap} ${overlap}`,
|
|
104
|
+
].join(", "),
|
|
105
|
+
"background-position": "right top, right bottom, left top, left bottom",
|
|
106
|
+
"background-repeat": "no-repeat",
|
|
107
|
+
};
|
|
78
108
|
};
|
|
79
109
|
|
|
80
110
|
const DIVISIONS = {
|
|
@@ -225,21 +255,81 @@ const toLayers = (rules) => {
|
|
|
225
255
|
});
|
|
226
256
|
};
|
|
227
257
|
|
|
228
|
-
|
|
229
|
-
|
|
258
|
+
/**
|
|
259
|
+
* A division with one fur: the fur's part is left transparent, over the fur's own layers.
|
|
260
|
+
* @param {string} division The division's name.
|
|
261
|
+
* @param {string} first The first tincture or fur.
|
|
262
|
+
* @param {string} second The second tincture or fur; one of the two is a fur.
|
|
263
|
+
* @param {Function} theme Tailwind's theme function, for the spots' colour.
|
|
264
|
+
* @returns {{image: string, size: string, position: string, repeat: string}[]} The layers, top first.
|
|
265
|
+
*/
|
|
266
|
+
const furredLayers = (division, first, second, theme) => {
|
|
230
267
|
const fur = FURS[first] ? first : second;
|
|
231
268
|
const paint = (t) => (t === fur ? "transparent" : `var(--color-${t})`);
|
|
232
|
-
|
|
269
|
+
return [
|
|
233
270
|
...toLayers(DIVISIONS[division](paint(first), paint(second), true)),
|
|
234
271
|
...furLayers(fur, theme),
|
|
235
272
|
];
|
|
236
|
-
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Layers joined into one comma-separated value per property.
|
|
277
|
+
* @param {{image: string, size: string, position: string, repeat: string}[]} layers
|
|
278
|
+
* @param {string} prefix The property prefix, such as "background" or "--field".
|
|
279
|
+
* @returns {Object<string, string>} `<prefix>-image`, `-size`, `-position` and `-repeat`.
|
|
280
|
+
*/
|
|
281
|
+
const joinLayers = (layers, prefix) =>
|
|
282
|
+
Object.fromEntries(
|
|
237
283
|
["image", "size", "position", "repeat"].map((p) => [
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
`${prefix}-${p}`,
|
|
285
|
+
layers.map((layer) => layer[p]).join(", "),
|
|
240
286
|
]),
|
|
241
287
|
);
|
|
242
|
-
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A division with one fur, as background rules.
|
|
291
|
+
* @param {string} division The division's name.
|
|
292
|
+
* @param {string} first The first tincture or fur.
|
|
293
|
+
* @param {string} second The second tincture or fur; one of the two is a fur.
|
|
294
|
+
* @param {Function} theme Tailwind's theme function, for the spots' colour.
|
|
295
|
+
* @returns {Object<string, string>} The four background-* properties.
|
|
296
|
+
*/
|
|
297
|
+
const withFur = (division, first, second, theme) =>
|
|
298
|
+
joinLayers(furredLayers(division, first, second, theme), "background");
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The field's own layers as --field-* variables, for charges on a counterchanged ordinary.
|
|
302
|
+
* A background colour becomes a last opaque layer, as the variables carry no colour.
|
|
303
|
+
* @param {{image: string, size: string, position: string, repeat: string}[]} layers
|
|
304
|
+
* @param {string} [color] The division's background-color, if it has one.
|
|
305
|
+
* @returns {Object<string, string>} The four --field-* variables.
|
|
306
|
+
*/
|
|
307
|
+
const field = (layers, color) =>
|
|
308
|
+
joinLayers(
|
|
309
|
+
color
|
|
310
|
+
? [
|
|
311
|
+
...layers,
|
|
312
|
+
{
|
|
313
|
+
image: `linear-gradient(${color}, ${color})`,
|
|
314
|
+
size: "100% 100%",
|
|
315
|
+
position: "0 0",
|
|
316
|
+
repeat: "no-repeat",
|
|
317
|
+
},
|
|
318
|
+
]
|
|
319
|
+
: layers,
|
|
320
|
+
"--field",
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A field's first tincture, and the shield's, for a bordure ring of the first (see grailwind.css).
|
|
325
|
+
* Only the heater sets it, as an inescutcheon's "of the first" is the shield's.
|
|
326
|
+
* @param {string} colour The first tincture's CSS colour.
|
|
327
|
+
* @returns {Record<string, unknown>} The declarations, with the heater's as a nested rule.
|
|
328
|
+
*/
|
|
329
|
+
const firstTincture = (colour) => ({
|
|
330
|
+
"--field-first": colour,
|
|
331
|
+
".heater:has(> &)": { "--frame-first": colour },
|
|
332
|
+
});
|
|
243
333
|
|
|
244
334
|
// The field with its tinctures swapped, for counterchanged ordinaries to paint.
|
|
245
335
|
const counterchange = (rules) =>
|
|
@@ -262,10 +352,12 @@ export default plugin(({ addUtilities, theme }) => {
|
|
|
262
352
|
|
|
263
353
|
const a = `var(--color-${first})`;
|
|
264
354
|
const b = `var(--color-${second})`;
|
|
355
|
+
const unswapped = background(a, b);
|
|
265
356
|
utilities[`.${division}-${first}-${second}`] = {
|
|
266
|
-
...
|
|
357
|
+
...unswapped,
|
|
358
|
+
...field(toLayers(unswapped), unswapped["background-color"]),
|
|
267
359
|
...counterchange(background(b, a)),
|
|
268
|
-
|
|
360
|
+
...firstTincture(a),
|
|
269
361
|
};
|
|
270
362
|
}
|
|
271
363
|
}
|
|
@@ -279,11 +371,13 @@ export default plugin(({ addUtilities, theme }) => {
|
|
|
279
371
|
[fur, tincture],
|
|
280
372
|
[tincture, fur],
|
|
281
373
|
]) {
|
|
374
|
+
const layers = furredLayers(division, first, second, theme);
|
|
282
375
|
// Only a tincture can be "of the first"; a fur first leaves it unset.
|
|
283
376
|
utilities[`.${division}-${first}-${second}`] = {
|
|
284
|
-
...
|
|
377
|
+
...joinLayers(layers, "background"),
|
|
378
|
+
...field(layers),
|
|
285
379
|
...counterchange(withFur(division, second, first, theme)),
|
|
286
|
-
...(!FURS[first] &&
|
|
380
|
+
...(!FURS[first] && firstTincture(`var(--color-${first})`)),
|
|
287
381
|
};
|
|
288
382
|
}
|
|
289
383
|
}
|