grailwind 0.3.0 → 0.5.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 +73 -1
- package/README.md +51 -10
- package/charges.js +56 -32
- package/data.js +130 -0
- package/divisions.js +95 -7
- package/furs.js +78 -0
- package/grailwind.css +83 -2
- package/ordinaries.js +249 -52
- package/package.json +20 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,76 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-11-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `in-chief` and `in-base`: one to three charges along the chief or base. On a
|
|
14
|
+
chief, they sit on it. The ESLint plugin reports an arrangement put on a
|
|
15
|
+
charge rather than its container.
|
|
16
|
+
- `-of-the-first`: the field's first tincture, as in a blazon, e.g.
|
|
17
|
+
`pale-of-the-first`. The ESLint plugin checks it.
|
|
18
|
+
- Striped fields: `barry-*` and `paly-*` (of six), `barry-of-eight-*`,
|
|
19
|
+
`barry-of-ten-*` and `paly-of-eight-*`. The first tincture is in chief or
|
|
20
|
+
on the dexter. They take furs and counterchanged, like the other divisions.
|
|
21
|
+
- `per-saltire-*`: a field divided on both diagonals, the first tincture in
|
|
22
|
+
chief and base.
|
|
23
|
+
- Bars, named by count: `bar-*`, `two-bars-*` and `three-bars-*`. They take
|
|
24
|
+
lines, furs, fimbriation and counterchanged, like the fess.
|
|
25
|
+
- `pile-*`: a wedge from the chief to a point. It takes lines, furs,
|
|
26
|
+
fimbriation and counterchanged, one or three charges `on` it, and two
|
|
27
|
+
`between`.
|
|
28
|
+
- `canton`: a ninth of the shield in dexter chief, over every ordinary and
|
|
29
|
+
charge, with its own field, divisions, ordinaries and charges, like the
|
|
30
|
+
inescutcheon.
|
|
31
|
+
- Six charges `between` a fess, bend, bend sinister, chevron or pale.
|
|
32
|
+
- `inescutcheon`: a small shield at the centre of the arms, over everything
|
|
33
|
+
else, with its own field, ordinaries and charges. A bordure on it is a third
|
|
34
|
+
as wide.
|
|
35
|
+
- The ESLint plugin checks a plain canton or inescutcheon against the field it
|
|
36
|
+
lies on. One with anything on it counts as arms of its own.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- The `in-chief` and `in-base` points are now `in-chief-point` and
|
|
41
|
+
`in-base-point`, like the honour, fess and nombril points.
|
|
42
|
+
- The canton and the chief surmount a bordure, as in heraldry. The canton is
|
|
43
|
+
still a third of the whole shield, and the chief's lower edge doesn't move.
|
|
44
|
+
- `@grailwind/eslint-plugin` 0.1.0-beta.1 needs `grailwind` 0.5.x, for the
|
|
45
|
+
renamed points.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Two charges `between` a fess, saltire or pall are placed one each side.
|
|
50
|
+
|
|
51
|
+
## [0.4.0] - 2026-11-03
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `grailwind/data` (experimental): the tinctures, metals and colours, furs,
|
|
56
|
+
ordinaries and their lines, charges, arrangements and points as plain data,
|
|
57
|
+
for tooling such as `@grailwind/eslint-plugin`. Its shape may change in any
|
|
58
|
+
minor release.
|
|
59
|
+
- `@grailwind/eslint-plugin` 0.1.0-beta.0, a separate package: the rule of
|
|
60
|
+
tincture and charge structure, for JSX and HTML. Install with
|
|
61
|
+
`@grailwind/eslint-plugin@beta`. It knows engrailed, invected and fur
|
|
62
|
+
ordinaries, and counts furs by their ground (configurable).
|
|
63
|
+
- `in-bend-sinister`: two or three charges in a line from sinister chief to
|
|
64
|
+
dexter base.
|
|
65
|
+
- Engrailed and invected ordinaries, e.g. `fess-engrailed-or` and
|
|
66
|
+
`chief-invected-azure`. Browsers without `clip-path: shape()` draw them with
|
|
67
|
+
straight edges.
|
|
68
|
+
- Furs: `ermine`, `ermines`, `erminois` and `pean`, on the field, on a charge,
|
|
69
|
+
or in a division (`per-pale-ermine-gules`).
|
|
70
|
+
- Fur ordinaries, e.g. `chief-ermine` and `cross-engrailed-erminois`. They
|
|
71
|
+
aren't fimbriated.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- Five charges now default to 2, 1, 2 (in saltire). They were invisible on a
|
|
76
|
+
plain field, and piled up in a corner on an ordinary without five points.
|
|
77
|
+
- An ordinary in a quarter no longer paints over the shield's own ordinary.
|
|
78
|
+
|
|
9
79
|
## [0.3.0] - 2026-11-01
|
|
10
80
|
|
|
11
81
|
### Added
|
|
@@ -43,7 +113,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
43
113
|
Initial release: tinctures, divisions, ordinaries, charges, strict mode and
|
|
44
114
|
custom SVG charges.
|
|
45
115
|
|
|
46
|
-
[Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.
|
|
116
|
+
[Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.5.0...HEAD
|
|
117
|
+
[0.5.0]: https://github.com/pafirmin/grailwind/compare/v0.4.0...v0.5.0
|
|
118
|
+
[0.4.0]: https://github.com/pafirmin/grailwind/compare/v0.3.0...v0.4.0
|
|
47
119
|
[0.3.0]: https://github.com/pafirmin/grailwind/compare/v0.2.0...v0.3.0
|
|
48
120
|
[0.2.0]: https://github.com/pafirmin/grailwind/compare/v0.1.0...v0.2.0
|
|
49
121
|
[0.1.0]: https://github.com/pafirmin/grailwind/releases/tag/v0.1.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
|
|
@@ -47,25 +52,61 @@ tinctures replace Tailwind's colours entirely.
|
|
|
47
52
|
## What's included
|
|
48
53
|
|
|
49
54
|
- **Tinctures:** a tincture on its own sets the background (`azure`), and
|
|
50
|
-
Tailwind's prefixes work as usual (`text-or`).
|
|
51
|
-
|
|
55
|
+
Tailwind's prefixes work as usual (`text-or`). There are also the furs `ermine`,
|
|
56
|
+
`ermines`, `erminois` and `pean`, which work in divisions too.
|
|
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.
|
|
52
59
|
- **Divided fields:** `per-pale-or-gules`, `quarterly-azure-argent`,
|
|
53
|
-
`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
|
|
54
61
|
layout.
|
|
55
|
-
- **Ordinaries:** `fess-or`, `chevron-gules`, `cross-argent`, `chief-azure
|
|
56
|
-
and more, with `fimbriated-*` for a narrow border,
|
|
57
|
-
and the like
|
|
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
|
+
`chief-ermine` and the like in a fur, or `chevron-counterchanged` and the like
|
|
66
|
+
on a divided field.
|
|
58
67
|
- **Charges:** `mullet`, `fleur-de-lis`, `bezant` and more, arranged by
|
|
59
|
-
number, `in-pale`, `in-fess`, `in-bend`, `on` or `between`
|
|
60
|
-
placed at a named point such as `in-dexter-chief`, sized
|
|
61
|
-
`charge-size-*`, and `counterchanged` on a divided field.
|
|
68
|
+
number, `in-pale`, `in-fess`, `in-bend`, `in-bend-sinister`, `on` or `between`
|
|
69
|
+
the ordinary, or placed at a named point such as `in-dexter-chief`, sized
|
|
70
|
+
with `charge-size-*`, and `counterchanged` on a divided field.
|
|
62
71
|
- **Your own charges:** any SVG, via `charge-[url(…)]`.
|
|
63
72
|
|
|
73
|
+
## Linting
|
|
74
|
+
|
|
75
|
+
For typos and clashing classes, use
|
|
76
|
+
[`eslint-plugin-better-tailwindcss`](https://github.com/schoero/eslint-plugin-better-tailwindcss).
|
|
77
|
+
It reads Grailwind's classes from your CSS entry file, so it flags `fess-ore` or
|
|
78
|
+
`per-pale-or-or`, and catches two classes fighting over the same thing, such as
|
|
79
|
+
`bend-sable bend-or`:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
// eslint.config.js
|
|
83
|
+
import betterTailwindcss from "eslint-plugin-better-tailwindcss";
|
|
84
|
+
|
|
85
|
+
export default [
|
|
86
|
+
{
|
|
87
|
+
plugins: { "better-tailwindcss": betterTailwindcss },
|
|
88
|
+
settings: { "better-tailwindcss": { entryPoint: "src/app.css" } },
|
|
89
|
+
rules: {
|
|
90
|
+
"better-tailwindcss/no-unknown-classes": "error",
|
|
91
|
+
"better-tailwindcss/no-conflicting-classes": "error",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For the heraldry itself, such as the rule of tincture and charges outside a `charges`
|
|
98
|
+
container, there is [`@grailwind/eslint-plugin`](packages/eslint-plugin) (beta).
|
|
99
|
+
|
|
100
|
+
The plugin reads names from `grailwind/data`. That export is **experimental**: it's there for
|
|
101
|
+
Grailwind's own tooling, and its shape may change in any minor release.
|
|
102
|
+
|
|
64
103
|
## Development
|
|
65
104
|
|
|
66
105
|
```sh
|
|
67
106
|
npm install
|
|
68
|
-
npm test # checks
|
|
107
|
+
npm test # checks the default and strict builds, and the ESLint plugin
|
|
108
|
+
npm run test:render # renders shields in Chromium and Firefox and checks their colours;
|
|
109
|
+
# run `npx playwright install chromium firefox` first
|
|
69
110
|
npm run format # formats with Prettier; CI runs format:check
|
|
70
111
|
npm run dev:watch # rebuilds the docs site on change and serves it at http://localhost:8000
|
|
71
112
|
```
|
package/charges.js
CHANGED
|
@@ -2,6 +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
6
|
|
|
6
7
|
const star = (cx, cy, r, points = 5) => {
|
|
7
8
|
const coords = [];
|
|
@@ -30,15 +31,6 @@ const SHAPES = {
|
|
|
30
31
|
<path d="M42 65C38 74 28 80 16 78C20 88 36 90 44 74Z"/>`,
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
// Roundels have their own names by tincture.
|
|
34
|
-
const ROUNDELS = {
|
|
35
|
-
bezant: "or",
|
|
36
|
-
plate: "argent",
|
|
37
|
-
torteau: "gules",
|
|
38
|
-
hurt: "azure",
|
|
39
|
-
pellet: "sable",
|
|
40
|
-
};
|
|
41
|
-
|
|
42
34
|
// Points as [left%, top%] of the shield, plus a charge size as % of its width.
|
|
43
35
|
// The default for each number of charges, e.g. three are placed 2 and 1.
|
|
44
36
|
// prettier-ignore
|
|
@@ -47,25 +39,29 @@ const DEFAULTS = {
|
|
|
47
39
|
2: { size: 30, points: [[50, 24], [50, 66]] },
|
|
48
40
|
3: { size: 28, points: [[28, 25], [72, 25], [50, 64]] },
|
|
49
41
|
4: { size: 24, points: [[30, 24], [70, 24], [32, 60], [68, 60]] },
|
|
42
|
+
5: { size: 22, points: [[27, 20], [73, 20], [50, 42], [32, 64], [68, 64]] },
|
|
50
43
|
6: { size: 20, points: [[20, 18], [50, 18], [80, 18], [35, 42], [65, 42], [50, 66]] },
|
|
51
44
|
};
|
|
52
45
|
|
|
53
|
-
// The points of the shield, for placing a single charge, e.g. `in-dexter-chief`.
|
|
54
|
-
const POINTS = {
|
|
55
|
-
"dexter-chief": [25, 17],
|
|
56
|
-
chief: [50, 15],
|
|
57
|
-
"sinister-chief": [75, 17],
|
|
58
|
-
"honour-point": [50, 30],
|
|
59
|
-
"fess-point": [50, 45],
|
|
60
|
-
"nombril-point": [50, 60],
|
|
61
|
-
"dexter-base": [30, 72],
|
|
62
|
-
base: [50, 84],
|
|
63
|
-
"sinister-base": [70, 72],
|
|
64
|
-
};
|
|
65
46
|
const POINT_SIZE = 22;
|
|
66
47
|
|
|
67
48
|
// With a chief on the shield, these points sit on it (see ordinaries.js).
|
|
68
|
-
const CHIEF_POINTS = new Set(["dexter-chief", "chief", "sinister-chief"]);
|
|
49
|
+
const CHIEF_POINTS = new Set(["dexter-chief", "chief-point", "sinister-chief"]);
|
|
50
|
+
|
|
51
|
+
// Arrangements along the chief or base, by the points each count of charges takes.
|
|
52
|
+
// prettier-ignore
|
|
53
|
+
const ALONG = {
|
|
54
|
+
"in-chief": {
|
|
55
|
+
1: ["chief-point"],
|
|
56
|
+
2: ["dexter-chief", "sinister-chief"],
|
|
57
|
+
3: ["dexter-chief", "chief-point", "sinister-chief"],
|
|
58
|
+
},
|
|
59
|
+
"in-base": {
|
|
60
|
+
1: ["base-point"],
|
|
61
|
+
2: ["dexter-base", "sinister-base"],
|
|
62
|
+
3: ["dexter-base", "base-point", "sinister-base"],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
69
65
|
|
|
70
66
|
// prettier-ignore
|
|
71
67
|
const ARRANGEMENTS = {
|
|
@@ -81,6 +77,10 @@ const ARRANGEMENTS = {
|
|
|
81
77
|
2: { size: 28, points: [[30, 26], [70, 64]] },
|
|
82
78
|
3: { size: 22, points: [[22, 16], [50, 45], [78, 74]] },
|
|
83
79
|
},
|
|
80
|
+
"in-bend-sinister": {
|
|
81
|
+
2: { size: 28, points: [[70, 26], [30, 64]] },
|
|
82
|
+
3: { size: 22, points: [[78, 16], [50, 45], [22, 74]] },
|
|
83
|
+
},
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
const svgUrl = (shape) =>
|
|
@@ -122,6 +122,18 @@ const counterchanged = {
|
|
|
122
122
|
},
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
+
// A charge at one of the shield's points. The chief's points sit on a chief when there is one.
|
|
126
|
+
const atPoint = (name) => {
|
|
127
|
+
const [left, top] = POINTS[name];
|
|
128
|
+
return CHIEF_POINTS.has(name)
|
|
129
|
+
? {
|
|
130
|
+
left: `${left}%`,
|
|
131
|
+
top: `var(--chief-points-y, ${top}%)`,
|
|
132
|
+
"--charge-width": `var(--charge-size, var(--chief-points-size, ${POINT_SIZE}%))`,
|
|
133
|
+
}
|
|
134
|
+
: place([left, top], POINT_SIZE);
|
|
135
|
+
};
|
|
136
|
+
|
|
125
137
|
const hasCount = (n) => `:has(> :nth-child(${n}):last-child)`;
|
|
126
138
|
|
|
127
139
|
export default plugin(({ addUtilities, matchUtilities }) => {
|
|
@@ -175,6 +187,16 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
175
187
|
utilities[`.${name}`] = arrangement;
|
|
176
188
|
}
|
|
177
189
|
|
|
190
|
+
for (const [name, counts] of Object.entries(ALONG)) {
|
|
191
|
+
const arrangement = {};
|
|
192
|
+
for (const [count, points] of Object.entries(counts)) {
|
|
193
|
+
points.forEach((point, i) => {
|
|
194
|
+
arrangement[`&${hasCount(count)} > :nth-child(${i + 1})`] = atPoint(point);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
utilities[`.${name}`] = arrangement;
|
|
198
|
+
}
|
|
199
|
+
|
|
178
200
|
// Positions come from the ordinary on the shield (see ordinaries.js).
|
|
179
201
|
const between = {};
|
|
180
202
|
for (let i = 1; i <= 4; i++) {
|
|
@@ -184,6 +206,16 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
184
206
|
"--charge-width": "var(--charge-size, var(--between-size))",
|
|
185
207
|
};
|
|
186
208
|
}
|
|
209
|
+
// The counts some ordinary overrides in BETWEEN's `counts` (see data.js).
|
|
210
|
+
for (const count of [2, 6]) {
|
|
211
|
+
for (let i = 1; i <= count; i++) {
|
|
212
|
+
between[`&${hasCount(count)} > :nth-child(${i})`] = {
|
|
213
|
+
left: `var(--between-${count}-${i}-x, var(--between-${i}-x))`,
|
|
214
|
+
top: `var(--between-${count}-${i}-y, var(--between-${i}-y))`,
|
|
215
|
+
"--charge-width": `var(--charge-size, var(--between-${count}-size, var(--between-size)))`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
187
219
|
utilities[".between"] = between;
|
|
188
220
|
|
|
189
221
|
// Falls back to the plain-field default when the ordinary has no points for this count.
|
|
@@ -206,16 +238,8 @@ export default plugin(({ addUtilities, matchUtilities }) => {
|
|
|
206
238
|
}
|
|
207
239
|
utilities[".on"] = on;
|
|
208
240
|
|
|
209
|
-
for (const
|
|
210
|
-
utilities[`.in-${name}`] = {
|
|
211
|
-
".charges > &": CHIEF_POINTS.has(name)
|
|
212
|
-
? {
|
|
213
|
-
left: `${left}%`,
|
|
214
|
-
top: `var(--chief-points-y, ${top}%)`,
|
|
215
|
-
"--charge-width": `var(--charge-size, var(--chief-points-size, ${POINT_SIZE}%))`,
|
|
216
|
-
}
|
|
217
|
-
: place([left, top], POINT_SIZE),
|
|
218
|
-
};
|
|
241
|
+
for (const name of Object.keys(POINTS)) {
|
|
242
|
+
utilities[`.in-${name}`] = { ".charges > &": atPoint(name) };
|
|
219
243
|
}
|
|
220
244
|
|
|
221
245
|
// On a charge, or on `charges` for all of them.
|
package/data.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Grailwind data shared by the CSS plugins and tooling such as the ESLint plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const METALS = ["or", "argent"];
|
|
6
|
+
export const COLOURS = ["gules", "azure", "vert", "purpure", "sable"];
|
|
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";
|
|
10
|
+
|
|
11
|
+
export const DIVISIONS = [
|
|
12
|
+
"per-pale",
|
|
13
|
+
"per-fess",
|
|
14
|
+
"per-bend",
|
|
15
|
+
"per-bend-sinister",
|
|
16
|
+
"per-saltire",
|
|
17
|
+
"quarterly",
|
|
18
|
+
"gyronny",
|
|
19
|
+
"barry",
|
|
20
|
+
"barry-of-eight",
|
|
21
|
+
"barry-of-ten",
|
|
22
|
+
"paly",
|
|
23
|
+
"paly-of-eight",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const ORDINARIES = [
|
|
27
|
+
"pale",
|
|
28
|
+
"fess",
|
|
29
|
+
"bar",
|
|
30
|
+
"two-bars",
|
|
31
|
+
"three-bars",
|
|
32
|
+
"bend",
|
|
33
|
+
"bend-sinister",
|
|
34
|
+
"chevron",
|
|
35
|
+
"cross",
|
|
36
|
+
"saltire",
|
|
37
|
+
"pall",
|
|
38
|
+
"chief",
|
|
39
|
+
"pile",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const CHARGES = [
|
|
43
|
+
"roundel",
|
|
44
|
+
"annulet",
|
|
45
|
+
"mullet",
|
|
46
|
+
"lozenge",
|
|
47
|
+
"billet",
|
|
48
|
+
"crescent",
|
|
49
|
+
"fleur-de-lis",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Roundels have their own names by tincture.
|
|
53
|
+
export const ROUNDELS = {
|
|
54
|
+
bezant: "or",
|
|
55
|
+
plate: "argent",
|
|
56
|
+
torteau: "gules",
|
|
57
|
+
hurt: "azure",
|
|
58
|
+
pellet: "sable",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Lines of partition for an ordinary's edge, e.g. `fess-engrailed-or`.
|
|
62
|
+
export const LINES = ["engrailed", "invected"];
|
|
63
|
+
|
|
64
|
+
// Each fur's [ground, spot] tinctures.
|
|
65
|
+
export const FURS = {
|
|
66
|
+
ermine: ["argent", "sable"],
|
|
67
|
+
ermines: ["sable", "argent"],
|
|
68
|
+
erminois: ["or", "sable"],
|
|
69
|
+
pean: ["sable", "or"],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// The points of the shield, as [left%, top%], for placing a single charge, e.g. `in-dexter-chief`.
|
|
73
|
+
export const POINTS = {
|
|
74
|
+
"dexter-chief": [25, 17],
|
|
75
|
+
"chief-point": [50, 15],
|
|
76
|
+
"sinister-chief": [75, 17],
|
|
77
|
+
"honour-point": [50, 30],
|
|
78
|
+
"fess-point": [50, 45],
|
|
79
|
+
"nombril-point": [50, 60],
|
|
80
|
+
"dexter-base": [30, 72],
|
|
81
|
+
"base-point": [50, 84],
|
|
82
|
+
"sinister-base": [70, 72],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Plain-field arrangements of a `charges` container, besides `between` and `on`.
|
|
86
|
+
export const ARRANGEMENTS = [
|
|
87
|
+
"in-pale",
|
|
88
|
+
"in-fess",
|
|
89
|
+
"in-bend",
|
|
90
|
+
"in-bend-sinister",
|
|
91
|
+
"in-chief",
|
|
92
|
+
"in-base",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Where charges go for "between", as [left%, top%], plus their size as % of width.
|
|
96
|
+
// `counts` overrides the points (and optionally the size) for one number of charges.
|
|
97
|
+
// prettier-ignore
|
|
98
|
+
export const BETWEEN = {
|
|
99
|
+
pale: { size: 18, points: [[18, 42], [82, 42]], counts: {
|
|
100
|
+
6: { size: 14, points: [[18, 18], [18, 45], [18, 70], [82, 18], [82, 45], [82, 70]] } } },
|
|
101
|
+
fess: { size: 20, points: [[30, 15], [70, 15], [50, 78]], counts: {
|
|
102
|
+
2: { points: [[50, 15], [50, 78]] },
|
|
103
|
+
6: { size: 14, points: [[20, 14], [50, 14], [80, 14], [32, 70], [68, 70], [50, 86]] } } },
|
|
104
|
+
bend: { size: 24, points: [[70, 26], [30, 68]], counts: {
|
|
105
|
+
6: { size: 14, points: [[48, 9], [76, 12], [84, 38], [9, 48], [18, 72], [38, 84]] } } },
|
|
106
|
+
"bend-sinister": { size: 24, points: [[30, 26], [70, 68]], counts: {
|
|
107
|
+
6: { size: 14, points: [[52, 9], [24, 12], [16, 38], [91, 48], [82, 72], [62, 84]] } } },
|
|
108
|
+
chevron: { size: 22, points: [[24, 22], [76, 22], [50, 70]], counts: {
|
|
109
|
+
6: { size: 12, points: [[18, 14], [50, 11], [82, 14], [38, 66], [62, 66], [50, 84]] } } },
|
|
110
|
+
cross: { size: 18, points: [[20, 17], [80, 17], [24, 72], [76, 72]] },
|
|
111
|
+
saltire: { size: 16, points: [[50, 13], [14, 50], [86, 50], [50, 84]], counts: {
|
|
112
|
+
2: { points: [[14, 50], [86, 50]] } } },
|
|
113
|
+
pall: { size: 18, points: [[50, 11], [18, 56], [82, 56]], counts: {
|
|
114
|
+
2: { points: [[18, 56], [82, 56]] } } },
|
|
115
|
+
pile: { size: 20, points: [[18, 55], [82, 55]] },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Where charges go for "on", along the ordinary's centre line, by number of charges.
|
|
119
|
+
// prettier-ignore
|
|
120
|
+
export const ON = {
|
|
121
|
+
pale: { size: 20, counts: { 1: [[50, 45]], 2: [[50, 28], [50, 62]], 3: [[50, 18], [50, 45], [50, 72]] } },
|
|
122
|
+
fess: { size: 22, counts: { 1: [[50, 45]], 2: [[30, 45], [70, 45]], 3: [[20, 45], [50, 45], [80, 45]] } },
|
|
123
|
+
bend: { size: 18, counts: { 1: [[50, 50]], 2: [[33, 33], [67, 67]], 3: [[24, 24], [50, 50], [76, 76]] } },
|
|
124
|
+
"bend-sinister": { size: 18, counts: { 1: [[50, 50]], 2: [[67, 33], [33, 67]], 3: [[76, 24], [50, 50], [24, 76]] } },
|
|
125
|
+
chevron: { size: 12, counts: { 1: [[50, 36]], 3: [[20, 65], [50, 36], [80, 65]] } },
|
|
126
|
+
cross: { size: 15, counts: { 1: [[50, 45]], 5: [[50, 15], [18, 45], [50, 45], [82, 45], [50, 80]] } },
|
|
127
|
+
saltire: { size: 14, counts: { 1: [[50, 50]], 5: [[22, 22], [78, 22], [50, 50], [22, 78], [78, 78]] } },
|
|
128
|
+
pall: { size: 14, counts: { 1: [[50, 50]], 3: [[22, 22], [78, 22], [50, 75]] } },
|
|
129
|
+
pile: { size: 14, counts: { 1: [[50, 25]], 3: [[50, 14], [50, 34], [50, 54]] } },
|
|
130
|
+
};
|
package/divisions.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Grailwind CSS: field divisions as decoration.
|
|
3
3
|
*/
|
|
4
4
|
import plugin from "tailwindcss/plugin";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { FURS, TINCTURES } from "./data.js";
|
|
6
|
+
import { furLayers } from "./furs.js";
|
|
7
7
|
|
|
8
8
|
// Horizontal lines of partition pass through the fess point, as the cross and fess do.
|
|
9
9
|
const FESS_POINT = "45%";
|
|
@@ -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,12 +36,28 @@ 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.
|
|
57
|
+
quarterly: (a, b, fur) => ({
|
|
31
58
|
"background-image": `linear-gradient(to right, ${a} 50%, ${b} 50%), linear-gradient(to right, ${b} 50%, ${a} 50%)`,
|
|
32
|
-
"background-size": `100% ${FESS_POINT}, 100% 100
|
|
33
|
-
"background-position": "top, top",
|
|
59
|
+
"background-size": `100% ${FESS_POINT}, 100% ${fur ? `calc(100% - ${FESS_POINT})` : "100%"}`,
|
|
60
|
+
"background-position": fur ? "top, bottom" : "top, top",
|
|
34
61
|
"background-repeat": "no-repeat",
|
|
35
62
|
}),
|
|
36
63
|
|
|
@@ -49,6 +76,45 @@ const DIVISIONS = {
|
|
|
49
76
|
"background-position": "left top, right top, right bottom, left bottom",
|
|
50
77
|
"background-repeat": "no-repeat",
|
|
51
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),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// A division's background rules as one { image, size, position, repeat } per layer.
|
|
89
|
+
const toLayers = (rules) => {
|
|
90
|
+
return rules["background-image"].split(/, (?=linear-gradient)/).map((gradient, i) => {
|
|
91
|
+
const size = (rules["background-size"] ?? "100% 100%").split(", ");
|
|
92
|
+
const position = (rules["background-position"] ?? "0 0").split(", ");
|
|
93
|
+
const repeat = (rules["background-repeat"] ?? "no-repeat").split(", ");
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
image: gradient,
|
|
97
|
+
size: size[i % size.length],
|
|
98
|
+
position: position[i % position.length],
|
|
99
|
+
repeat: repeat[i % repeat.length],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// A division with one fur: the fur's part is left transparent, over the fur's own layers.
|
|
105
|
+
const withFur = (division, first, second, theme) => {
|
|
106
|
+
const fur = FURS[first] ? first : second;
|
|
107
|
+
const paint = (t) => (t === fur ? "transparent" : `var(--color-${t})`);
|
|
108
|
+
const all = [
|
|
109
|
+
...toLayers(DIVISIONS[division](paint(first), paint(second), true)),
|
|
110
|
+
...furLayers(fur, theme),
|
|
111
|
+
];
|
|
112
|
+
return Object.fromEntries(
|
|
113
|
+
["image", "size", "position", "repeat"].map((p) => [
|
|
114
|
+
`background-${p}`,
|
|
115
|
+
all.map((layer) => layer[p]).join(", "),
|
|
116
|
+
]),
|
|
117
|
+
);
|
|
52
118
|
};
|
|
53
119
|
|
|
54
120
|
// The field with its tinctures swapped, for counterchanged ordinaries to paint.
|
|
@@ -60,23 +126,45 @@ const counterchange = (rules) =>
|
|
|
60
126
|
]),
|
|
61
127
|
);
|
|
62
128
|
|
|
63
|
-
export default plugin(({ addUtilities }) => {
|
|
129
|
+
export default plugin(({ addUtilities, theme }) => {
|
|
64
130
|
const utilities = {};
|
|
65
131
|
|
|
66
132
|
for (const [division, background] of Object.entries(DIVISIONS)) {
|
|
67
133
|
for (const first of TINCTURES) {
|
|
68
134
|
for (const second of TINCTURES) {
|
|
69
|
-
if (first === second)
|
|
135
|
+
if (first === second) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
70
138
|
|
|
71
139
|
const a = `var(--color-${first})`;
|
|
72
140
|
const b = `var(--color-${second})`;
|
|
73
141
|
utilities[`.${division}-${first}-${second}`] = {
|
|
74
142
|
...background(a, b),
|
|
75
143
|
...counterchange(background(b, a)),
|
|
144
|
+
"--field-first": a,
|
|
76
145
|
};
|
|
77
146
|
}
|
|
78
147
|
}
|
|
79
148
|
}
|
|
80
149
|
|
|
150
|
+
// One fur and one tincture; two furs can't each be kept to their own part.
|
|
151
|
+
for (const division of Object.keys(DIVISIONS)) {
|
|
152
|
+
for (const fur of Object.keys(FURS)) {
|
|
153
|
+
for (const tincture of TINCTURES) {
|
|
154
|
+
for (const [first, second] of [
|
|
155
|
+
[fur, tincture],
|
|
156
|
+
[tincture, fur],
|
|
157
|
+
]) {
|
|
158
|
+
// Only a tincture can be "of the first"; a fur first leaves it unset.
|
|
159
|
+
utilities[`.${division}-${first}-${second}`] = {
|
|
160
|
+
...withFur(division, first, second, theme),
|
|
161
|
+
...counterchange(withFur(division, second, first, theme)),
|
|
162
|
+
...(!FURS[first] && { "--field-first": `var(--color-${first})` }),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
81
169
|
addUtilities(utilities);
|
|
82
170
|
});
|
package/furs.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Grailwind CSS: furs, as a tiled pattern of spots over a ground tincture.
|
|
3
|
+
*/
|
|
4
|
+
import plugin from "tailwindcss/plugin";
|
|
5
|
+
import { FURS } from "./data.js";
|
|
6
|
+
|
|
7
|
+
const SPOT = [
|
|
8
|
+
`<circle cx='0' cy='-8.5' r='1.9'/><circle cx='-3.4' cy='-5.6' r='1.9'/><circle cx='3.4' cy='-5.6' r='1.9'/>`,
|
|
9
|
+
`<path d='M0,-4.5 C0.8,-1 2.6,4 5,9.5 L2.1,7.6 L0,11 L-2.1,7.6 L-5,9.5 C-2.6,4 -0.8,-1 0,-4.5 Z'/>`,
|
|
10
|
+
].join("");
|
|
11
|
+
|
|
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
|
+
// corner that join up with the neighbouring tiles.
|
|
14
|
+
const AT = [
|
|
15
|
+
[25, 30],
|
|
16
|
+
[0, 0],
|
|
17
|
+
[50, 0],
|
|
18
|
+
[0, 60],
|
|
19
|
+
[50, 60],
|
|
20
|
+
];
|
|
21
|
+
const tile = (fill) =>
|
|
22
|
+
`url("data:image/svg+xml,${encodeURIComponent(
|
|
23
|
+
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 60'><g fill='${fill}'>${AT.map(
|
|
24
|
+
([x, y]) => `<g transform='translate(${x} ${y})'>${SPOT}</g>`,
|
|
25
|
+
).join("")}</g></svg>`,
|
|
26
|
+
)}")`;
|
|
27
|
+
|
|
28
|
+
// The spots are baked into an SVG, which can't read CSS variables, so they take the theme's
|
|
29
|
+
// value at build time.
|
|
30
|
+
const spots = (fur, theme) => tile(theme(`colors.${FURS[fur][1]}`));
|
|
31
|
+
|
|
32
|
+
// Spots across the field, one per tile in each row.
|
|
33
|
+
const ACROSS = 6;
|
|
34
|
+
|
|
35
|
+
// A quarter of a tile up and left, so whole spots sit along the top and sides. A % position is a
|
|
36
|
+
// share of (box - tile), which is (ACROSS - 1) tiles.
|
|
37
|
+
const SHIFT = `${-100 / (4 * (ACROSS - 1))}%`;
|
|
38
|
+
const FIELD = { size: `calc(100% / ${ACROSS}) auto`, position: `${SHIFT} ${SHIFT}` };
|
|
39
|
+
|
|
40
|
+
// The layers under a fur in a divided field: the spots, then the ground.
|
|
41
|
+
export const furLayers = (fur, theme) => {
|
|
42
|
+
const ground = `var(--color-${FURS[fur][0]})`;
|
|
43
|
+
return [
|
|
44
|
+
{ image: spots(fur, theme), ...FIELD, repeat: "repeat" },
|
|
45
|
+
{
|
|
46
|
+
image: `linear-gradient(${ground}, ${ground})`,
|
|
47
|
+
size: "100% 100%",
|
|
48
|
+
position: "0 0",
|
|
49
|
+
repeat: "no-repeat",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// A fur on its own, as on the field or an ordinary.
|
|
55
|
+
export const furBackground = (fur, theme) => ({
|
|
56
|
+
"background-color": `var(--color-${FURS[fur][0]})`,
|
|
57
|
+
"background-image": spots(fur, theme),
|
|
58
|
+
"background-size": FIELD.size,
|
|
59
|
+
"background-position": FIELD.position,
|
|
60
|
+
"background-repeat": "repeat",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default plugin(({ addUtilities, theme }) => {
|
|
64
|
+
const utilities = {};
|
|
65
|
+
|
|
66
|
+
for (const fur of Object.keys(FURS)) {
|
|
67
|
+
utilities[`.${fur}`] = {
|
|
68
|
+
...furBackground(fur, theme),
|
|
69
|
+
// Two tiles across a charge, centred, so a whole spot sits in its middle.
|
|
70
|
+
".charges > &": {
|
|
71
|
+
"background-size": "50% auto",
|
|
72
|
+
"background-position": "center",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addUtilities(utilities);
|
|
78
|
+
});
|
package/grailwind.css
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
@plugin "./divisions.js";
|
|
13
13
|
@plugin "./ordinaries.js";
|
|
14
14
|
@plugin "./charges.js";
|
|
15
|
+
@plugin "./furs.js";
|
|
15
16
|
|
|
16
17
|
@property --fimbriated {
|
|
17
18
|
syntax: "*";
|
|
@@ -41,6 +42,19 @@
|
|
|
41
42
|
syntax: "*";
|
|
42
43
|
inherits: false;
|
|
43
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
|
+
}
|
|
44
58
|
|
|
45
59
|
/* ---------------------------------------------------------------------------
|
|
46
60
|
* Tinctures
|
|
@@ -60,27 +74,41 @@
|
|
|
60
74
|
|
|
61
75
|
/* ---------------------------------------------------------------------------
|
|
62
76
|
* Bare tinctures
|
|
77
|
+
*
|
|
78
|
+
* The furs (`ermine` and the like) are in furs.js, as they need the theme's
|
|
79
|
+
* colours at build time.
|
|
63
80
|
* ------------------------------------------------------------------------- */
|
|
64
81
|
@utility or {
|
|
65
82
|
background-color: var(--color-or);
|
|
83
|
+
--field-first: var(--color-or);
|
|
66
84
|
}
|
|
67
85
|
@utility argent {
|
|
68
86
|
background-color: var(--color-argent);
|
|
87
|
+
--field-first: var(--color-argent);
|
|
69
88
|
}
|
|
70
89
|
@utility gules {
|
|
71
90
|
background-color: var(--color-gules);
|
|
91
|
+
--field-first: var(--color-gules);
|
|
72
92
|
}
|
|
73
93
|
@utility azure {
|
|
74
94
|
background-color: var(--color-azure);
|
|
95
|
+
--field-first: var(--color-azure);
|
|
75
96
|
}
|
|
76
97
|
@utility vert {
|
|
77
98
|
background-color: var(--color-vert);
|
|
99
|
+
--field-first: var(--color-vert);
|
|
78
100
|
}
|
|
79
101
|
@utility purpure {
|
|
80
102
|
background-color: var(--color-purpure);
|
|
103
|
+
--field-first: var(--color-purpure);
|
|
81
104
|
}
|
|
82
105
|
@utility sable {
|
|
83
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);
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
/* ---------------------------------------------------------------------------
|
|
@@ -107,6 +135,12 @@
|
|
|
107
135
|
display: grid;
|
|
108
136
|
grid-template-columns: 1fr 1fr;
|
|
109
137
|
grid-template-rows: 1fr 1fr;
|
|
138
|
+
|
|
139
|
+
/* Each quarter is a coat of its own, so it starts its own count. */
|
|
140
|
+
& > * {
|
|
141
|
+
--the-first: var(--field-first);
|
|
142
|
+
--outer-inset: 0px;
|
|
143
|
+
}
|
|
110
144
|
}
|
|
111
145
|
|
|
112
146
|
/* ---------------------------------------------------------------------------
|
|
@@ -116,8 +150,51 @@
|
|
|
116
150
|
/* Bordure: a border in a given tincture, e.g. `bordure-or`. */
|
|
117
151
|
@utility bordure-* {
|
|
118
152
|
border-style: solid;
|
|
119
|
-
border-width: 0.75rem;
|
|
153
|
+
border-width: var(--bordure-width, 0.75rem);
|
|
120
154
|
border-color: --value(--color-*);
|
|
155
|
+
--bordure-inset: var(--bordure-width, 0.75rem);
|
|
156
|
+
}
|
|
157
|
+
@utility bordure-of-the-first {
|
|
158
|
+
border-style: solid;
|
|
159
|
+
border-width: var(--bordure-width, 0.75rem);
|
|
160
|
+
border-color: var(--the-first, transparent);
|
|
161
|
+
--bordure-inset: var(--bordure-width, 0.75rem);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Inescutcheon: a shield a third as wide, centred on the fess point (50%, 45%),
|
|
165
|
+
* over everything else on the shield. As both are 5:6, it's a third as tall too. */
|
|
166
|
+
@utility inescutcheon {
|
|
167
|
+
position: absolute;
|
|
168
|
+
z-index: 2;
|
|
169
|
+
width: 33%;
|
|
170
|
+
left: 33.5%;
|
|
171
|
+
top: 28.5%;
|
|
172
|
+
aspect-ratio: 5 / 6;
|
|
173
|
+
overflow: clip;
|
|
174
|
+
overflow-clip-margin: border-box;
|
|
175
|
+
border-radius: 0 0 50% 50% / 0 0 45% 45%;
|
|
176
|
+
box-shadow: 0 0 0 2px var(--color-sable);
|
|
177
|
+
contain: layout;
|
|
178
|
+
/* In proportion with the shield's own bordure. */
|
|
179
|
+
--bordure-width: 0.25rem;
|
|
180
|
+
--outer-inset: var(--bordure-inset);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Canton: a third of the shield each way in dexter chief, so a ninth of it, and
|
|
184
|
+
* 5:6 like the shield, so everything on it places as on a shield. Over every
|
|
185
|
+
* ordinary and charge, as a later addition to the arms, and over the bordure. */
|
|
186
|
+
@utility canton {
|
|
187
|
+
position: absolute;
|
|
188
|
+
z-index: 2;
|
|
189
|
+
width: calc((100% + 2 * var(--outer-inset, 0px)) / 3);
|
|
190
|
+
left: calc(-1 * var(--outer-inset, 0px));
|
|
191
|
+
top: calc(-1 * var(--outer-inset, 0px));
|
|
192
|
+
aspect-ratio: 5 / 6;
|
|
193
|
+
/* Like the shield, so a chief on it surmounts its own bordure. */
|
|
194
|
+
overflow: clip;
|
|
195
|
+
overflow-clip-margin: border-box;
|
|
196
|
+
contain: layout;
|
|
197
|
+
--bordure-width: 0.25rem;
|
|
121
198
|
}
|
|
122
199
|
|
|
123
200
|
/* ---------------------------------------------------------------------------
|
|
@@ -128,11 +205,15 @@
|
|
|
128
205
|
@utility escutcheon {
|
|
129
206
|
position: relative;
|
|
130
207
|
aspect-ratio: 5 / 6;
|
|
131
|
-
|
|
208
|
+
/* At the border's edge, so a canton or chief can surmount the bordure. */
|
|
209
|
+
overflow: clip;
|
|
210
|
+
overflow-clip-margin: border-box;
|
|
132
211
|
border-radius: 0 0 50% 50% / 0 0 45% 45%;
|
|
133
212
|
box-shadow: 0 0 0 2px var(--color-sable);
|
|
134
213
|
/* The containing block for counterchanged charges' fixed layers. */
|
|
135
214
|
contain: layout;
|
|
215
|
+
--the-first: var(--field-first);
|
|
216
|
+
--outer-inset: var(--bordure-inset);
|
|
136
217
|
}
|
|
137
218
|
|
|
138
219
|
/* ---------------------------------------------------------------------------
|
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
|
-
|
|
6
|
-
|
|
5
|
+
import { BETWEEN, FURS, OF_THE_FIRST, ON, TINCTURES } from "./data.js";
|
|
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,6 +32,7 @@ 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
|
};
|
|
29
37
|
|
|
30
38
|
const num = (v) => +v.toFixed(3);
|
|
@@ -34,6 +42,20 @@ const polygon = (vertices) => `polygon(${vertices.map(point).join(", ")})`;
|
|
|
34
42
|
|
|
35
43
|
const PSEUDO = { chief: "&::after" };
|
|
36
44
|
|
|
45
|
+
// The chief surmounts a bordure.
|
|
46
|
+
const INSET = "var(--bordure-inset)";
|
|
47
|
+
const OVER_BORDURE = {
|
|
48
|
+
"--bordure-inset": "inherit",
|
|
49
|
+
inset: `calc(-1 * ${INSET}) calc(-1 * ${INSET}) 0`,
|
|
50
|
+
padding: `${INSET} ${INSET} 0`,
|
|
51
|
+
"background-origin": "content-box",
|
|
52
|
+
};
|
|
53
|
+
// A vertex on the shield's top or side edges, moved out to the bordure's outer edge.
|
|
54
|
+
const outer = ([x, y]) => {
|
|
55
|
+
const ox = x === 0 ? `calc(-1 * ${INSET})` : x === 100 ? `calc(100% + ${INSET})` : pct(x);
|
|
56
|
+
return `${ox} ${y === 0 ? `calc(-1 * ${INSET})` : pct(y)}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
37
59
|
// A y in % of the field, as % of the whole shield when a chief takes the top of it.
|
|
38
60
|
const belowChief = (y) =>
|
|
39
61
|
y === 100 ? "100%" : `calc(${num(y)}% + var(--field-top, 0%) * ${num(1 - y / 100)})`;
|
|
@@ -104,10 +126,19 @@ const widenedPolygon = (vertices) => {
|
|
|
104
126
|
const S = 12;
|
|
105
127
|
const band = (kind, from, to, tile) => ({ kind, from, to, tile });
|
|
106
128
|
const stripe = (kind, from, to, tile) => ({ kind, from, to, tile, stripe: true });
|
|
129
|
+
// Half a tile, split on its diagonal, as gradient stops: 0–50% or 50–100%. The one diagonal band
|
|
130
|
+
// a tile that isn't square can take, as its diagonal isn't a line of constant x ± y.
|
|
131
|
+
const half = (kind, end, tile) => ({ kind, tile, stops: end === "start" ? [0, 50] : [50, 100] });
|
|
132
|
+
// A 2px strip down the centre, from the top to `bottom`, where two tiles meet mid-pixel and
|
|
133
|
+
// would let the fimbriation show between them. Unsoftened, as it lies on its own tincture.
|
|
134
|
+
const seam = (bottom) => ({ kind: "x", tile: [0, 0, 100, bottom], seam: true });
|
|
107
135
|
// prettier-ignore
|
|
108
136
|
const BANDS = {
|
|
109
137
|
pale: [band("x", 35, 65)],
|
|
110
138
|
fess: [band("y", 30, 60)],
|
|
139
|
+
bar: [band("y", 38, 52)],
|
|
140
|
+
"two-bars": [band("y", 27, 39), band("y", 51, 63)],
|
|
141
|
+
"three-bars": [band("y", 20, 30), band("y", 40, 50), band("y", 60, 70)],
|
|
111
142
|
bend: [band("u-v", -20, 20)],
|
|
112
143
|
"bend-sinister": [band("u+v", 80, 120)],
|
|
113
144
|
chevron: [
|
|
@@ -127,6 +158,8 @@ const BANDS = {
|
|
|
127
158
|
band("u+v", 85, 115),
|
|
128
159
|
band("x", 40, 60, [0, 55, 100, 100]),
|
|
129
160
|
],
|
|
161
|
+
// Each slanted side is its tile's diagonal.
|
|
162
|
+
pile: [half("u-v", "end", [20, 0, 50, 85]), half("u+v", "start", [50, 0, 80, 85]), seam(83)],
|
|
130
163
|
};
|
|
131
164
|
|
|
132
165
|
const DIRECTION = {
|
|
@@ -140,7 +173,9 @@ const DIRECTION = {
|
|
|
140
173
|
const stop = ({ kind, tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }, value) => {
|
|
141
174
|
const w = x1 - x0;
|
|
142
175
|
const h = y1 - y0;
|
|
143
|
-
if (kind.length > 1 && w !== h)
|
|
176
|
+
if (kind.length > 1 && w !== h) {
|
|
177
|
+
throw new Error("diagonal bands need a square tile");
|
|
178
|
+
}
|
|
144
179
|
switch (kind) {
|
|
145
180
|
case "x":
|
|
146
181
|
return ((value - x0) * 100) / w;
|
|
@@ -153,23 +188,24 @@ const stop = ({ kind, tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }, value) => {
|
|
|
153
188
|
}
|
|
154
189
|
};
|
|
155
190
|
|
|
156
|
-
// Soften each edge by a pixel, as hard gradient stops aren't antialiased.
|
|
191
|
+
// Soften each edge by a pixel, as hard gradient stops aren't antialiased. A stop at 0 or 100% is
|
|
192
|
+
// the tile's own edge, so it isn't softened.
|
|
157
193
|
const gradient = (layer, colour) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
194
|
+
if (layer.seam) {
|
|
195
|
+
return `linear-gradient(to right, transparent calc(50% - 1px), ${colour} 0 calc(50% + 1px), transparent 0)`;
|
|
196
|
+
}
|
|
197
|
+
const [a, b] = (layer.stops ?? [stop(layer, layer.from), stop(layer, layer.to)]).map(num);
|
|
198
|
+
const from =
|
|
199
|
+
a === 0 ? `${colour} 0` : `transparent calc(${a}% - 0.5px), ${colour} calc(${a}% + 0.5px)`;
|
|
200
|
+
const to = b === 100 ? "100%" : `calc(${b}% - 0.5px), transparent calc(${b}% + 0.5px)`;
|
|
201
|
+
return `linear-gradient(${DIRECTION[layer.kind]}, ${from} ${to})`;
|
|
164
202
|
};
|
|
165
203
|
|
|
166
204
|
const tilePosition = (start, size) => (size === 100 ? "0" : pct((start * 100) / (100 - size)));
|
|
167
205
|
|
|
168
|
-
const bands = (layers,
|
|
206
|
+
const bands = (layers, colour) => ({
|
|
169
207
|
"background-image": layers
|
|
170
|
-
.map((l) =>
|
|
171
|
-
gradient(l, l.stripe ? "var(--fimbriation, transparent)" : `var(--color-${tincture})`),
|
|
172
|
-
)
|
|
208
|
+
.map((l) => gradient(l, l.stripe ? "var(--fimbriation, transparent)" : colour))
|
|
173
209
|
.join(", "),
|
|
174
210
|
"background-size": layers
|
|
175
211
|
.map(({ tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }) => `${pct(x1 - x0)} ${pct(y1 - y0)}`)
|
|
@@ -183,38 +219,122 @@ const bands = (layers, tincture) => ({
|
|
|
183
219
|
"background-repeat": "no-repeat",
|
|
184
220
|
});
|
|
185
221
|
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
222
|
+
// Lines of partition. Scallops are round on the 5:6 shield; below a chief the
|
|
223
|
+
// box is shorter, so they are slightly squashed.
|
|
224
|
+
const SCALLOP = 14;
|
|
225
|
+
const SCALLOPS = { chevron: 10 };
|
|
226
|
+
const DEPTH = 0.35;
|
|
227
|
+
|
|
228
|
+
const at = ([x, y]) => point([(x * 100) / W, (y * 100) / H]);
|
|
229
|
+
|
|
230
|
+
// Arcs along an edge, in real units: { to, r, sweep }.
|
|
231
|
+
const arcs = (a, b, scallop, sweep) => {
|
|
232
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
233
|
+
const n = Math.max(1, Math.round(length / scallop));
|
|
234
|
+
const chord = length / n;
|
|
235
|
+
const depth = chord * DEPTH;
|
|
236
|
+
const r = (chord * chord) / 4 / (2 * depth) + depth / 2;
|
|
237
|
+
return Array.from({ length: n }, (_, i) => {
|
|
238
|
+
const t = (i + 1) / n;
|
|
239
|
+
return { to: [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t], r, sweep };
|
|
240
|
+
});
|
|
197
241
|
};
|
|
198
242
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
243
|
+
// Each line maps an edge to arcs; `inward` is the sweep that bulges into the ordinary.
|
|
244
|
+
const LINES = {
|
|
245
|
+
engrailed: (a, b, scallop, inward) => arcs(a, b, scallop, inward),
|
|
246
|
+
invected: (a, b, scallop, inward) => arcs(a, b, scallop, inward === "cw" ? "ccw" : "cw"),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const side = (a, b, p) => (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
250
|
+
|
|
251
|
+
// Centre of the minor arc from a to b.
|
|
252
|
+
const centre = (a, b, { r, sweep }) => {
|
|
253
|
+
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
254
|
+
const len = Math.hypot(dx, dy);
|
|
255
|
+
const k = Math.sqrt(Math.max(0, r * r - (len / 2) ** 2)) * (sweep === "cw" ? 1 : -1);
|
|
256
|
+
return [(a[0] + b[0]) / 2 - (k * dy) / len, (a[1] + b[1]) / 2 + (k * dx) / len];
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Where the arc a→v crosses the arc v→b, if it does. Two circles through v meet
|
|
260
|
+
// again at v's reflection across the line between their centres.
|
|
261
|
+
const crossing = (a, v, b, first, second) => {
|
|
262
|
+
const c1 = centre(a, v, first);
|
|
263
|
+
const c2 = centre(v, b, second);
|
|
264
|
+
const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
|
|
265
|
+
const t = ((v[0] - c1[0]) * dx + (v[1] - c1[1]) * dy) / (dx * dx + dy * dy);
|
|
266
|
+
const p = [2 * (c1[0] + t * dx) - v[0], 2 * (c1[1] + t * dy) - v[1]];
|
|
267
|
+
const onArc = (from, to, c) => Math.sign(side(from, to, p)) === -Math.sign(side(from, to, c));
|
|
268
|
+
return Math.hypot(p[0] - v[0], p[1] - v[1]) > 1e-6 && onArc(a, v, c1) && onArc(v, b, c2)
|
|
269
|
+
? p
|
|
270
|
+
: null;
|
|
210
271
|
};
|
|
211
272
|
|
|
212
|
-
|
|
273
|
+
// `extend` moves the sides on the shield's edge out, as `outer` does; the lined sides stay put.
|
|
274
|
+
const shape = (vertices, line, scallop, extend) => {
|
|
275
|
+
const inward = signedArea(vertices) > 0 ? "ccw" : "cw";
|
|
276
|
+
const pts = vertices.map(real);
|
|
277
|
+
const straight = vertices.map((v, i) => onShieldEdge(v, vertices[(i + 1) % vertices.length]));
|
|
278
|
+
const edges = pts.map((a, i) => {
|
|
279
|
+
const j = (i + 1) % pts.length;
|
|
280
|
+
return straight[i] ? [{ to: pts[j] }] : line(a, pts[j], scallop, inward);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Where arcs cross at a corner, e.g. a chevron engrailed's peak, end both at the
|
|
284
|
+
// crossing; otherwise the loop beyond it is filled.
|
|
285
|
+
let start = pts[0];
|
|
286
|
+
edges.forEach((edge, i) => {
|
|
287
|
+
const last = edge.at(-1);
|
|
288
|
+
const first = edges[(i + 1) % edges.length][0];
|
|
289
|
+
if (!last.r || !first.r) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const from = edge.length > 1 ? edge.at(-2).to : pts[i];
|
|
293
|
+
const p = crossing(from, last.to, first.to, last, first);
|
|
294
|
+
if (!p) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
last.to = p;
|
|
298
|
+
if (i === edges.length - 1) {
|
|
299
|
+
start = p;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const commands = edges.flatMap((edge, i) => {
|
|
304
|
+
const segments = edge.map(({ to, r, sweep }) =>
|
|
305
|
+
r
|
|
306
|
+
? `arc to ${at(to)} of ${pct((r / W) * 100)} ${pct((r / H) * 100)} ${sweep}`
|
|
307
|
+
: `line to ${at(to)}`,
|
|
308
|
+
);
|
|
309
|
+
if (!extend) {
|
|
310
|
+
return segments;
|
|
311
|
+
}
|
|
312
|
+
const outside = straight.at(i - 1);
|
|
313
|
+
if (straight[i]) {
|
|
314
|
+
const out = `line to ${extend(vertices[(i + 1) % vertices.length])}`;
|
|
315
|
+
return outside ? [out] : [`line to ${extend(vertices[i])}`, out];
|
|
316
|
+
}
|
|
317
|
+
return outside ? [`line to ${at(pts[i])}`, ...segments] : segments;
|
|
318
|
+
});
|
|
319
|
+
const from = extend ? extend(vertices[0]) : at(start);
|
|
320
|
+
return `shape(from ${from}, ${commands.join(", ")}, close)`;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const betweenVars = ({ size, points, counts = {} }) => {
|
|
213
324
|
const vars = { "--between-size": `${size}%` };
|
|
214
325
|
points.forEach(([left, top], i) => {
|
|
215
326
|
vars[`--between-${i + 1}-x`] = `${left}%`;
|
|
216
327
|
vars[`--between-${i + 1}-y`] = `${top}%`;
|
|
217
328
|
});
|
|
329
|
+
for (const [count, override] of Object.entries(counts)) {
|
|
330
|
+
if (override.size !== undefined) {
|
|
331
|
+
vars[`--between-${count}-size`] = `${override.size}%`;
|
|
332
|
+
}
|
|
333
|
+
override.points.forEach(([left, top], i) => {
|
|
334
|
+
vars[`--between-${count}-${i + 1}-x`] = `${left}%`;
|
|
335
|
+
vars[`--between-${count}-${i + 1}-y`] = `${top}%`;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
218
338
|
return vars;
|
|
219
339
|
};
|
|
220
340
|
|
|
@@ -229,18 +349,53 @@ const onVars = ({ size, counts }) => {
|
|
|
229
349
|
return vars;
|
|
230
350
|
};
|
|
231
351
|
|
|
232
|
-
|
|
352
|
+
// Every layout var an ordinary sets on its shield, for an inescutcheon or canton to reset.
|
|
353
|
+
const layoutVars = () => {
|
|
354
|
+
const allVars = [];
|
|
355
|
+
|
|
356
|
+
for (const division of Object.values(BETWEEN)) {
|
|
357
|
+
const vars = betweenVars(division);
|
|
358
|
+
allVars.push(...Object.keys(vars));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const division of Object.values(ON)) {
|
|
362
|
+
const vars = onVars(division);
|
|
363
|
+
allVars.push(...Object.keys(vars));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const chiefVars = ["--field-top", "--chief-points-y", "--chief-points-size"];
|
|
367
|
+
allVars.push(...chiefVars);
|
|
368
|
+
|
|
369
|
+
return [...new Set(allVars)];
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Each tincture's class suffix and colour, and "of the first" (see --the-first in grailwind.css).
|
|
373
|
+
const PAINTS = [
|
|
374
|
+
...TINCTURES.map((tincture) => [tincture, `var(--color-${tincture})`]),
|
|
375
|
+
[OF_THE_FIRST, "var(--the-first, transparent)"],
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
export default plugin(({ addBase, addUtilities, theme }) => {
|
|
379
|
+
// Unset on an inescutcheon or canton, so it can't inherit its shield's layout. In the base
|
|
380
|
+
// layer, so an ordinary on it still sets its own.
|
|
381
|
+
addBase({
|
|
382
|
+
".inescutcheon, .canton": Object.fromEntries(layoutVars().map((name) => [name, "initial"])),
|
|
383
|
+
});
|
|
384
|
+
|
|
233
385
|
const utilities = {};
|
|
234
386
|
|
|
235
|
-
for (const tincture of
|
|
387
|
+
for (const [tincture, colour] of PAINTS) {
|
|
236
388
|
utilities[`.fimbriated-${tincture}`] = {
|
|
237
389
|
"--fimbriated": "1",
|
|
238
|
-
"--fimbriation":
|
|
390
|
+
"--fimbriation": colour,
|
|
239
391
|
};
|
|
240
392
|
}
|
|
241
393
|
|
|
394
|
+
// Isolated, so an ordinary in a quarter paints under the shield's own ordinary.
|
|
395
|
+
// The chief's ::after stays unraised, so charges on it stay on top.
|
|
242
396
|
const ordinaryRules = (ordinary, pseudo) => ({
|
|
243
397
|
position: "relative",
|
|
398
|
+
isolation: "isolate",
|
|
244
399
|
...(BETWEEN[ordinary] && betweenVars(BETWEEN[ordinary])),
|
|
245
400
|
...(ON[ordinary] && onVars(ON[ordinary])),
|
|
246
401
|
// With a chief, everything else is drawn in the field below it.
|
|
@@ -250,14 +405,32 @@ export default plugin(({ addUtilities }) => {
|
|
|
250
405
|
"--chief-points-y": "calc(-15% / 0.7)",
|
|
251
406
|
"--chief-points-size": "18%",
|
|
252
407
|
}),
|
|
253
|
-
[PSEUDO[ordinary] ?? "&::before"]: {
|
|
408
|
+
[PSEUDO[ordinary] ?? "&::before"]: {
|
|
409
|
+
content: '""',
|
|
410
|
+
position: "absolute",
|
|
411
|
+
...(!PSEUDO[ordinary] && { "z-index": "1" }),
|
|
412
|
+
...pseudo,
|
|
413
|
+
},
|
|
254
414
|
});
|
|
255
415
|
|
|
256
416
|
for (const [ordinary, vertices] of Object.entries(ORDINARIES)) {
|
|
417
|
+
const chief = ordinary === "chief";
|
|
418
|
+
const placed = chief ? OVER_BORDURE : { inset: "var(--field-top, 0%) 0 0" };
|
|
419
|
+
const plain = chief
|
|
420
|
+
? `polygon(${vertices.map(outer).join(", ")}) content-box`
|
|
421
|
+
: polygon(vertices);
|
|
422
|
+
// Browsers without shape() keep the straight polygon.
|
|
423
|
+
const lined = (line) => [
|
|
424
|
+
plain,
|
|
425
|
+
chief
|
|
426
|
+
? `${shape(vertices, line, SCALLOP, outer)} content-box`
|
|
427
|
+
: shape(vertices, line, SCALLOPS[ordinary] ?? SCALLOP),
|
|
428
|
+
];
|
|
429
|
+
|
|
257
430
|
// Covers the whole shield, so the swapped field lines up with the real one,
|
|
258
431
|
// and the clip moves the ordinary below any chief instead.
|
|
259
432
|
utilities[`.${ordinary}-counterchanged`] = ordinaryRules(ordinary, {
|
|
260
|
-
inset: "0",
|
|
433
|
+
...(chief ? OVER_BORDURE : { inset: "0" }),
|
|
261
434
|
// Registered not to inherit, so a shield inside another can't pick up its swap.
|
|
262
435
|
...Object.fromEntries(
|
|
263
436
|
["color", "image", "size", "position", "repeat"].map((n) => [
|
|
@@ -270,28 +443,52 @@ export default plugin(({ addUtilities }) => {
|
|
|
270
443
|
"background-size": "var(--counterchange-size, auto)",
|
|
271
444
|
"background-position": "var(--counterchange-position, 0 0)",
|
|
272
445
|
"background-repeat": "var(--counterchange-repeat, repeat)",
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
446
|
+
// The swapped field's tiles would wrap into the bordure's strips, so the bordure shows there.
|
|
447
|
+
...(chief && { "background-clip": "content-box" }),
|
|
448
|
+
"clip-path": chief
|
|
449
|
+
? plain
|
|
450
|
+
: `polygon(${vertices.map(([x, y]) => `${pct(x)} ${belowChief(y)}`).join(", ")})`,
|
|
277
451
|
});
|
|
278
452
|
|
|
279
|
-
for (const tincture of
|
|
453
|
+
for (const [tincture, colour] of PAINTS) {
|
|
280
454
|
utilities[`.${ordinary}-${tincture}`] = ordinaryRules(ordinary, {
|
|
281
|
-
|
|
455
|
+
...placed,
|
|
282
456
|
...(BANDS[ordinary]
|
|
283
457
|
? {
|
|
284
458
|
"--fimbriated": "inherit",
|
|
285
459
|
"--fimbriation": "inherit",
|
|
286
|
-
"background-color": `var(--fimbriation,
|
|
287
|
-
...bands(BANDS[ordinary],
|
|
460
|
+
"background-color": `var(--fimbriation, ${colour})`,
|
|
461
|
+
...bands(BANDS[ordinary], colour),
|
|
288
462
|
"clip-path": widenedPolygon(vertices),
|
|
289
463
|
}
|
|
290
464
|
: {
|
|
291
|
-
"background-color":
|
|
292
|
-
"clip-path":
|
|
465
|
+
"background-color": colour,
|
|
466
|
+
"clip-path": plain,
|
|
293
467
|
}),
|
|
294
468
|
});
|
|
469
|
+
// Not fimbriated.
|
|
470
|
+
for (const [name, line] of Object.entries(LINES)) {
|
|
471
|
+
utilities[`.${ordinary}-${name}-${tincture}`] = ordinaryRules(ordinary, {
|
|
472
|
+
...placed,
|
|
473
|
+
"background-color": colour,
|
|
474
|
+
"clip-path": lined(line),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// A plain polygon, without the fimbriation vars, so fimbriated-* can't reach a fur ordinary.
|
|
480
|
+
for (const fur of Object.keys(FURS)) {
|
|
481
|
+
const pseudo = { ...placed, ...furBackground(fur, theme) };
|
|
482
|
+
utilities[`.${ordinary}-${fur}`] = ordinaryRules(ordinary, {
|
|
483
|
+
...pseudo,
|
|
484
|
+
"clip-path": plain,
|
|
485
|
+
});
|
|
486
|
+
for (const [name, line] of Object.entries(LINES)) {
|
|
487
|
+
utilities[`.${ordinary}-${name}-${fur}`] = ordinaryRules(ordinary, {
|
|
488
|
+
...pseudo,
|
|
489
|
+
"clip-path": lined(line),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
295
492
|
}
|
|
296
493
|
}
|
|
297
494
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tailwindcss -i docs/app.css -o docs/site.css",
|
|
6
6
|
"dev": "tailwindcss -i docs/app.css -o docs/site.css --watch",
|
|
7
|
-
"test": "node test/modes.mjs",
|
|
7
|
+
"test": "node test/modes.mjs && npm test -w packages/eslint-plugin",
|
|
8
|
+
"test:render": "node test/render.mjs",
|
|
8
9
|
"dev:watch": "tailwindcss -i docs/app.css -o docs/site.css --watch=always & python3 -m http.server ${PORT:-8000} -d docs",
|
|
10
|
+
"lint": "eslint .",
|
|
9
11
|
"format": "prettier --write .",
|
|
10
12
|
"format:check": "prettier --check ."
|
|
11
13
|
},
|
|
@@ -14,17 +16,28 @@
|
|
|
14
16
|
"tailwind",
|
|
15
17
|
"heraldry",
|
|
16
18
|
"blazon",
|
|
17
|
-
"css"
|
|
19
|
+
"css",
|
|
20
|
+
"coat-of-arms",
|
|
21
|
+
"coats-of-arms",
|
|
22
|
+
"tailwindcss-plugin",
|
|
23
|
+
"eslint"
|
|
18
24
|
],
|
|
19
25
|
"author": "Paul Firmin",
|
|
20
26
|
"license": "MIT",
|
|
21
27
|
"description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
|
|
22
28
|
"devDependencies": {
|
|
29
|
+
"@grailwind/eslint-plugin": "^0.1.0-beta.1",
|
|
30
|
+
"@html-eslint/parser": "^0.67.0",
|
|
23
31
|
"@tailwindcss/cli": "^4.3.3",
|
|
32
|
+
"eslint": "^10.14.0",
|
|
33
|
+
"playwright": "~1.64.1",
|
|
24
34
|
"prettier": "^3.9.10",
|
|
25
35
|
"tailwindcss": "^4.3.3"
|
|
26
36
|
},
|
|
27
37
|
"type": "module",
|
|
38
|
+
"workspaces": [
|
|
39
|
+
"packages/*"
|
|
40
|
+
],
|
|
28
41
|
"style": "grailwind.css",
|
|
29
42
|
"exports": {
|
|
30
43
|
".": {
|
|
@@ -34,13 +47,16 @@
|
|
|
34
47
|
"./strict": {
|
|
35
48
|
"style": "./strict.css",
|
|
36
49
|
"default": "./strict.css"
|
|
37
|
-
}
|
|
50
|
+
},
|
|
51
|
+
"./data": "./data.js"
|
|
38
52
|
},
|
|
39
53
|
"files": [
|
|
54
|
+
"data.js",
|
|
40
55
|
"grailwind.css",
|
|
41
56
|
"divisions.js",
|
|
42
57
|
"ordinaries.js",
|
|
43
58
|
"charges.js",
|
|
59
|
+
"furs.js",
|
|
44
60
|
"strict.css",
|
|
45
61
|
"palette-reset.css",
|
|
46
62
|
"CHANGELOG.md"
|