Merge branch 'master' into feature/split-label-view-data

This commit is contained in:
kruschen 2026-02-24 19:05:05 +01:00 committed by GitHub
commit ba0ce8e40b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 8023 additions and 5800 deletions

6
package-lock.json generated
View file

@ -1353,7 +1353,6 @@
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@ -1394,7 +1393,6 @@
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/browser": "4.0.18",
"@vitest/mocker": "4.0.18",
@ -1876,7 +1874,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@ -2163,7 +2160,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -2475,7 +2471,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -2551,7 +2546,6 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -574,3 +574,121 @@ function saveGeoJsonMarkers() {
const fileName = getFileName("Markers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJsonZones() {
const {zones, cells, vertices} = pack;
const json = {type: "FeatureCollection", features: []};
// Helper function to convert zone cells to polygon coordinates
// Handles multiple disconnected components and holes properly
function getZonePolygonCoordinates(zoneCells) {
const cellsInZone = new Set(zoneCells);
const ofSameType = (cellId) => cellsInZone.has(cellId);
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
const checkedCells = new Set();
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
// Find all boundary components by tracing each connected region
for (const cellId of zoneCells) {
if (checkedCells.has(cellId)) continue;
// Check if this cell is on the boundary (has a neighbor outside the zone)
const neighbors = cells.c[cellId];
const onBorder = neighbors.some(ofDifferentType);
if (!onBorder) continue;
// Check if this is an inner lake (hole) - skip if so
const feature = pack.features[cells.f[cellId]];
if (feature.type === "lake" && feature.shoreline) {
if (feature.shoreline.every(ofSameType)) continue;
}
// Find a starting vertex that's on the boundary
const cellVertices = cells.v[cellId];
let startingVertex = null;
for (const vertexId of cellVertices) {
const vertexCells = vertices.c[vertexId];
if (vertexCells.some(ofDifferentType)) {
startingVertex = vertexId;
break;
}
}
if (startingVertex === null) continue;
// Use connectVertices to trace the boundary (reusing existing logic)
const vertexChain = connectVertices({
vertices,
startingVertex,
ofSameType,
addToChecked: (cellId) => checkedCells.add(cellId),
closeRing: false, // We'll close it manually after converting to coordinates
});
if (vertexChain.length < 3) continue;
// Convert vertex chain to coordinates
const coordinates = [];
for (const vertexId of vertexChain) {
const [x, y] = vertices.p[vertexId];
coordinates.push(getCoordinates(x, y, 4));
}
// Close the ring (first coordinate = last coordinate)
if (coordinates.length > 0) {
coordinates.push(coordinates[0]);
}
// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
if (coordinates.length >= 4) {
rings.push(coordinates);
}
}
return rings;
}
// Filter and process zones
zones.forEach(zone => {
// Exclude hidden zones and zones with no cells
if (zone.hidden || !zone.cells || zone.cells.length === 0) return;
const rings = getZonePolygonCoordinates(zone.cells);
// Skip if no valid rings were generated
if (rings.length === 0) return;
const properties = {
id: zone.i,
name: zone.name,
type: zone.type,
color: zone.color,
cells: zone.cells
};
// If there's only one ring, use Polygon geometry
if (rings.length === 1) {
const feature = {
type: "Feature",
geometry: {type: "Polygon", coordinates: rings},
properties
};
json.features.push(feature);
} else {
// Multiple disconnected components: use MultiPolygon
// Each component is wrapped in its own array
const multiPolygonCoordinates = rings.map(ring => [ring]);
const feature = {
type: "Feature",
geometry: {type: "MultiPolygon", coordinates: multiPolygonCoordinates},
properties
};
json.features.push(feature);
}
});
const fileName = getFileName("Zones") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}

File diff suppressed because it is too large Load diff

View file

@ -246,6 +246,11 @@ window.Military = (function () {
const expected = 3 * populationRate; // expected regiment size
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
// remove all existing regiment notes before regenerating
for (let i = notes.length - 1; i >= 0; i--) {
if (notes[i].id.startsWith("regiment")) notes.splice(i, 1);
}
// get regiments for each state
valid.forEach(s => {
s.military = createRegiments(s.temp.platoons, s);
@ -380,7 +385,14 @@ window.Military = (function () {
: gauss(options.year - 100, 150, 1, options.year - 6);
const conflict = campaign ? ` during the ${campaign.name}` : "";
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
notes.push({id: `regiment${s.i}-${r.i}`, name: r.name, legend});
const id = `regiment${s.i}-${r.i}`;
const existing = notes.find(n => n.id === id);
if (existing) {
existing.name = r.name;
existing.legend = legend;
} else {
notes.push({id, name: r.name, legend});
}
};
return {

View file

@ -330,15 +330,11 @@ function editHeightmap(options) {
c.y = p[1];
}
// recalculate zones to grid
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-cells");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const g = cells.map(i => pack.cells.g[i]);
zone.attr("data-cells", g);
zone.selectAll("*").remove();
});
const zoneGridCellsMap = new Map();
for (const zone of pack.zones) {
if (!zone.cells || !zone.cells.length) continue;
zoneGridCellsMap.set(zone.i, zone.cells.map(i => pack.cells.g[i]));
}
Features.markupGrid();
if (erosionAllowed) addLakesInDeepDepressions();
@ -448,24 +444,18 @@ function editHeightmap(options) {
Lakes.defineNames();
}
// restore zones from grid
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const g = zone.attr("data-cells");
const gCells = g ? g.split(",").map(i => +i) : [];
const cells = pack.cells.i.filter(i => gCells.includes(pack.cells.g[i]));
const gridToPackMap = new Map();
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (!gridToPackMap.has(g)) gridToPackMap.set(g, []);
gridToPackMap.get(g).push(i);
}
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone
.selectAll("*")
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
});
for (const zone of pack.zones) {
const gridCells = zoneGridCellsMap.get(zone.i);
if (!gridCells || !gridCells.length) continue;
zone.cells = gridCells.flatMap(g => gridToPackMap.get(g) || []);
}
// recalculate ice
Ice.generate();

View file

@ -49,6 +49,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
<li>New routes generation algorithm</li>
<li>Routes overview tool</li>
<li>Configurable longitude</li>
<li>Export zones to GeoJSON</li>
</ul>
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>

View file

@ -6152,6 +6152,7 @@
<button onclick="saveGeoJsonRoutes()" data-tip="Download routes data in GeoJSON format">routes</button>
<button onclick="saveGeoJsonRivers()" data-tip="Download rivers data in GeoJSON format">rivers</button>
<button onclick="saveGeoJsonMarkers()" data-tip="Download markers data in GeoJSON format">markers</button>
<button onclick="saveGeoJsonZones()" data-tip="Download zones data in GeoJSON format">zones</button>
</div>
<p>
GeoJSON format is used in GIS tools such as QGIS. Check out
@ -8494,16 +8495,12 @@
<script defer src="config/heightmap-templates.js"></script>
<script defer src="config/precreated-heightmaps.js"></script>
<script defer src="modules/ice.js?v=1.111.0"></script>
<script defer src="modules/military-generator.js?v=1.107.0"></script>
<script defer src="modules/markers-generator.js?v=1.107.0"></script>
<script defer src="modules/coa-generator.js?v=1.99.00"></script>
<script defer src="modules/military-generator.js?v=1.112.3"></script>
<script defer src="modules/resample.js?v=1.112.1"></script>
<script defer src="libs/alea.min.js?v1.105.0"></script>
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
<script defer src="libs/lineclip.min.js?v1.105.0"></script>
<script defer src="libs/simplify.js?v1.105.6"></script>
<script defer src="modules/fonts.js?v=1.99.03"></script>
<script defer src="modules/ui/layers.js?v=1.111.0"></script>
<script defer src="modules/ui/measurers.js?v=1.99.00"></script>
<script defer src="modules/ui/style-presets.js?v=1.100.00"></script>
@ -8515,7 +8512,7 @@
<script defer src="modules/ui/editors.js?v=1.112.1"></script>
<script defer src="modules/ui/tools.js?v=1.111.0"></script>
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.105.2"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.112.2"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.108.1"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.112.0"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.105.11"></script>
@ -8552,12 +8549,11 @@
<script defer src="modules/ui/submap-tool.js?v=1.106.2"></script>
<script defer src="modules/ui/transform-tool.js?v=1.106.2"></script>
<script defer src="modules/ui/hotkeys.js?v=1.104.0"></script>
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
<script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="modules/io/save.js?v=1.113.0"></script>
<script defer src="modules/io/load.js?v=1.113.0"></script>
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
<script defer src="modules/io/export.js?v=1.108.13"></script>
<script defer src="modules/io/export.js?v=1.112.2"></script>
</body>
</html>

View file

@ -289,7 +289,7 @@ class BurgModule {
? "City"
: burg.type;
burg.coa = COA.generate(stateCOA, kinship, null, type);
burg.coa.shield = COA.getShield(burg.culture, burg.state);
burg.coa.shield = COA.getShield(burg.culture!, burg.state!);
}
private defineFeatures(burg: Burg) {

53
src/modules/emblem/box.ts Normal file
View file

@ -0,0 +1,53 @@
// shield-specific size multiplier
export const shieldBox = {
heater: "0 10 200 200",
spanish: "0 10 200 200",
french: "0 10 200 200",
horsehead: "0 10 200 200",
horsehead2: "0 10 200 200",
polish: "0 0 200 200",
hessen: "0 5 200 200",
swiss: "0 10 200 200",
boeotian: "0 0 200 200",
roman: "0 0 200 200",
kite: "0 0 200 200",
oldFrench: "0 10 200 200",
renaissance: "0 5 200 200",
baroque: "0 10 200 200",
targe: "0 0 200 200",
targe2: "0 0 200 200",
pavise: "0 0 200 200",
wedged: "0 10 200 200",
flag: "0 0 200 200",
pennon: "2.5 0 200 200",
guidon: "2.5 0 200 200",
banner: "0 10 200 200",
dovetail: "0 10 200 200",
gonfalon: "0 10 200 200",
pennant: "0 0 200 200",
round: "0 0 200 200",
oval: "0 0 200 200",
vesicaPiscis: "0 0 200 200",
square: "0 0 200 200",
diamond: "0 0 200 200",
no: "0 0 200 200",
fantasy1: "0 0 200 200",
fantasy2: "0 5 200 200",
fantasy3: "0 5 200 200",
fantasy4: "0 5 200 200",
fantasy5: "0 0 200 200",
noldor: "0 0 200 200",
gondor: "0 5 200 200",
easterling: "0 0 200 200",
erebor: "0 0 200 200",
ironHills: "0 5 200 200",
urukHai: "0 0 200 200",
moriaOrc: "0 0 200 200",
};

View file

@ -0,0 +1,883 @@
export interface ChargeDataEntry {
colors?: number;
sinister?: boolean;
reversed?: boolean;
positions?: Record<string, number>;
natural?: string;
}
export const chargeData: Record<string, ChargeDataEntry> = {
agnusDei: {
colors: 2,
sinister: true,
},
angel: {
colors: 2,
positions: { e: 1 },
},
anvil: {
sinister: true,
},
apple: {
colors: 2,
},
arbalest: {
colors: 3,
reversed: true,
},
archer: {
colors: 3,
sinister: true,
},
armEmbowedHoldingSabre: {
colors: 3,
sinister: true,
},
armEmbowedVambraced: {
sinister: true,
},
armEmbowedVambracedHoldingSword: {
colors: 3,
sinister: true,
},
armillarySphere: {
positions: { e: 1 },
},
arrow: {
colors: 3,
reversed: true,
},
arrowsSheaf: {
colors: 3,
reversed: true,
},
axe: {
colors: 2,
sinister: true,
},
badgerStatant: {
colors: 2,
sinister: true,
},
banner: {
colors: 2,
},
basilisk: {
colors: 3,
sinister: true,
},
bearPassant: {
colors: 3,
sinister: true,
},
bearRampant: {
colors: 3,
sinister: true,
},
bee: {
colors: 3,
reversed: true,
},
bell: {
colors: 2,
},
boarHeadErased: {
colors: 3,
sinister: true,
},
boarRampant: {
colors: 3,
sinister: true,
positions: { e: 12, beh: 1, kn: 1, jln: 2 },
},
boat: {
colors: 2,
},
bookClosed: {
colors: 3,
sinister: true,
},
bookClosed2: {
sinister: true,
},
bookOpen: {
colors: 3,
},
bow: {
sinister: true,
},
bowWithArrow: {
colors: 3,
reversed: true,
},
bowWithThreeArrows: {
colors: 3,
},
bucket: {
colors: 2,
},
bugleHorn: {
colors: 2,
},
bugleHorn2: {
colors: 2,
},
bullHeadCaboshed: {
colors: 2,
},
bullPassant: {
colors: 3,
sinister: true,
},
butterfly: {
colors: 3,
reversed: true,
},
camel: {
colors: 2,
sinister: true,
},
cancer: {
reversed: true,
},
cannon: {
colors: 2,
sinister: true,
},
caravel: {
colors: 3,
sinister: true,
},
castle: {
colors: 2,
},
castle2: {
colors: 3,
},
catPassantGuardant: {
colors: 2,
sinister: true,
},
cavalier: {
colors: 3,
sinister: true,
positions: { e: 1 },
},
centaur: {
colors: 3,
sinister: true,
},
chalice: {
colors: 2,
},
cinquefoil: {
reversed: true,
},
cock: {
colors: 3,
sinister: true,
},
comet: {
reversed: true,
},
cowStatant: {
colors: 3,
sinister: true,
},
cossack: {
colors: 3,
sinister: true,
},
crescent: {
reversed: true,
},
crocodile: {
colors: 2,
sinister: true,
},
crosier: {
sinister: true,
},
crossbow: {
colors: 3,
sinister: true,
},
crossGamma: {
sinister: true,
},
crossLatin: {
reversed: true,
},
crossTau: {
reversed: true,
},
crossTriquetra: {
reversed: true,
},
crown: {
colors: 2,
positions: {
e: 10,
abcdefgzi: 1,
beh: 3,
behdf: 2,
acegi: 1,
kn: 1,
pq: 2,
abc: 1,
jln: 4,
jleh: 1,
def: 2,
abcpqh: 3,
},
},
crown2: {
colors: 3,
positions: {
e: 10,
abcdefgzi: 1,
beh: 3,
behdf: 2,
acegi: 1,
kn: 1,
pq: 2,
abc: 1,
jln: 4,
jleh: 1,
def: 2,
abcpqh: 3,
},
},
deerHeadCaboshed: {
colors: 2,
},
dolphin: {
colors: 2,
sinister: true,
},
donkeyHeadCaboshed: {
colors: 2,
},
dove: {
colors: 2,
natural: "argent",
sinister: true,
},
doveDisplayed: {
colors: 2,
natural: "argent",
sinister: true,
},
dragonfly: {
colors: 2,
reversed: true,
},
dragonPassant: {
colors: 3,
sinister: true,
},
dragonRampant: {
colors: 3,
sinister: true,
},
drakkar: {
colors: 3,
sinister: true,
},
drawingCompass: {
sinister: true,
},
drum: {
colors: 3,
},
duck: {
colors: 3,
sinister: true,
},
eagle: {
colors: 3,
sinister: true,
positions: { e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1 },
},
eagleTwoHeads: {
colors: 3,
},
elephant: {
colors: 2,
sinister: true,
},
elephantHeadErased: {
colors: 2,
sinister: true,
},
falchion: {
colors: 2,
reversed: true,
},
falcon: {
colors: 3,
sinister: true,
},
fan: {
colors: 2,
reversed: true,
},
fasces: {
colors: 3,
sinister: true,
},
feather: {
sinister: true,
},
flamberge: {
colors: 2,
reversed: true,
},
flangedMace: {
reversed: true,
},
fly: {
colors: 3,
reversed: true,
},
foot: {
sinister: true,
},
fountain: {
natural: "azure",
},
frog: {
reversed: true,
},
garb: {
colors: 2,
natural: "or",
positions: {
e: 1,
def: 3,
abc: 2,
beh: 1,
kn: 1,
jln: 3,
jleh: 1,
abcpqh: 1,
joe: 1,
lme: 1,
},
},
gauntlet: {
sinister: true,
reversed: true,
},
goat: {
colors: 3,
sinister: true,
},
goutte: {
reversed: true,
},
grapeBunch: {
colors: 3,
sinister: true,
},
grapeBunch2: {
colors: 3,
sinister: true,
},
grenade: {
colors: 2,
},
greyhoundCourant: {
colors: 3,
sinister: true,
positions: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
},
greyhoundRampant: {
colors: 2,
sinister: true,
positions: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
},
greyhoundSejant: {
colors: 3,
sinister: true,
},
griffinPassant: {
colors: 3,
sinister: true,
positions: { e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1 },
},
griffinRampant: {
colors: 3,
sinister: true,
positions: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
},
hand: {
sinister: true,
reversed: true,
positions: { e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1 },
},
harp: {
colors: 2,
sinister: true,
},
hatchet: {
colors: 2,
sinister: true,
},
head: {
colors: 2,
sinister: true,
positions: { e: 1 },
},
headWreathed: {
colors: 3,
sinister: true,
positions: { e: 1 },
},
hedgehog: {
colors: 3,
sinister: true,
},
helmet: {
sinister: true,
},
helmetCorinthian: {
colors: 3,
sinister: true,
},
helmetGreat: {
sinister: true,
},
helmetZischagge: {
sinister: true,
},
heron: {
colors: 2,
sinister: true,
},
hindStatant: {
colors: 2,
sinister: true,
},
hook: {
sinister: true,
},
horseHeadCouped: {
sinister: true,
},
horsePassant: {
colors: 2,
sinister: true,
},
horseRampant: {
colors: 3,
sinister: true,
},
horseSalient: {
colors: 2,
sinister: true,
},
horseshoe: {
reversed: true,
},
hourglass: {
colors: 3,
},
ladybird: {
colors: 3,
reversed: true,
},
lamb: {
colors: 2,
sinister: true,
},
lambPassantReguardant: {
colors: 2,
sinister: true,
},
lanceWithBanner: {
colors: 3,
sinister: true,
},
laurelWreath: {
colors: 2,
},
lighthouse: {
colors: 3,
},
lionHeadCaboshed: {
colors: 2,
},
lionHeadErased: {
colors: 2,
sinister: true,
},
lionPassant: {
colors: 3,
sinister: true,
positions: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
},
lionPassantGuardant: {
colors: 3,
sinister: true,
},
lionRampant: {
colors: 3,
sinister: true,
positions: { e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1 },
},
lionSejant: {
colors: 3,
sinister: true,
},
lizard: {
reversed: true,
},
lochaberAxe: {
colors: 2,
sinister: true,
},
log: {
sinister: true,
},
lute: {
colors: 2,
sinister: true,
},
lymphad: {
colors: 3,
sinister: true,
positions: { e: 1 },
},
mace: {
colors: 2,
},
maces: {
colors: 2,
},
mallet: {
colors: 2,
},
mantle: {
colors: 3,
},
martenCourant: {
colors: 3,
sinister: true,
},
mascle: {
positions: {
e: 15,
abcdefgzi: 3,
beh: 3,
bdefh: 4,
acegi: 1,
kn: 3,
joe: 2,
abc: 3,
jlh: 8,
jleh: 1,
df: 3,
abcpqh: 4,
pqe: 3,
eknpq: 3,
},
},
mastiffStatant: {
colors: 3,
sinister: true,
},
mitre: {
colors: 3,
},
monk: {
sinister: true,
},
moonInCrescent: {
sinister: true,
},
mullet: {
reversed: true,
},
mullet7: {
reversed: true,
},
oak: {
colors: 3,
},
orb: {
colors: 3,
},
ouroboros: {
sinister: true,
},
owl: {
colors: 2,
sinister: true,
},
owlDisplayed: {
colors: 2,
},
palmTree: {
colors: 3,
},
parrot: {
colors: 2,
sinister: true,
},
peacock: {
colors: 3,
sinister: true,
},
peacockInPride: {
colors: 3,
sinister: true,
},
pear: {
colors: 2,
},
pegasus: {
colors: 3,
sinister: true,
},
pike: {
colors: 2,
sinister: true,
},
pineTree: {
colors: 2,
},
plaice: {
colors: 2,
sinister: true,
},
plough: {
colors: 2,
sinister: true,
},
ploughshare: {
sinister: true,
},
porcupine: {
colors: 2,
sinister: true,
},
portcullis: {
colors: 2,
},
rabbitSejant: {
colors: 2,
sinister: true,
},
rake: {
reversed: true,
},
rapier: {
colors: 2,
sinister: true,
reversed: true,
},
ramHeadErased: {
colors: 3,
sinister: true,
},
ramPassant: {
colors: 3,
sinister: true,
},
ratRampant: {
colors: 2,
sinister: true,
},
raven: {
colors: 2,
natural: "sable",
sinister: true,
positions: { e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1 },
},
rhinoceros: {
colors: 2,
sinister: true,
},
rose: {
colors: 3,
},
sabre: {
colors: 2,
sinister: true,
},
sabre2: {
colors: 2,
sinister: true,
reversed: true,
},
sabresCrossed: {
colors: 2,
reversed: true,
},
sagittarius: {
colors: 3,
sinister: true,
},
salmon: {
colors: 2,
sinister: true,
},
saw: {
colors: 2,
},
scale: {
colors: 2,
},
scaleImbalanced: {
colors: 2,
sinister: true,
},
scissors: {
reversed: true,
},
scorpion: {
reversed: true,
},
scrollClosed: {
colors: 2,
sinister: true,
},
scythe: {
colors: 2,
sinister: true,
reversed: true,
},
scythe2: {
sinister: true,
},
serpent: {
colors: 2,
sinister: true,
},
shield: {
colors: 2,
sinister: true,
},
sickle: {
colors: 2,
sinister: true,
reversed: true,
},
snail: {
colors: 2,
sinister: true,
},
snake: {
colors: 2,
sinister: true,
},
spear: {
colors: 2,
reversed: true,
},
spiral: {
sinister: true,
reversed: true,
},
squirrel: {
sinister: true,
},
stagLodgedRegardant: {
colors: 3,
sinister: true,
},
stagPassant: {
colors: 2,
sinister: true,
},
stirrup: {
colors: 2,
},
swallow: {
colors: 2,
sinister: true,
},
swan: {
colors: 3,
sinister: true,
},
swanErased: {
colors: 3,
sinister: true,
},
sword: {
colors: 2,
reversed: true,
},
talbotPassant: {
colors: 3,
sinister: true,
},
talbotSejant: {
colors: 3,
sinister: true,
},
tower: {
colors: 2,
},
tree: {
positions: { e: 1 },
},
trefoil: {
reversed: true,
},
trowel: {
colors: 2,
sinister: true,
reversed: true,
},
unicornRampant: {
colors: 3,
sinister: true,
},
wasp: {
colors: 3,
reversed: true,
},
wheatStalk: {
colors: 2,
},
windmill: {
colors: 3,
sinister: true,
},
wing: {
sinister: true,
},
wingSword: {
colors: 3,
sinister: true,
},
wolfHeadErased: {
colors: 2,
sinister: true,
},
wolfPassant: {
colors: 3,
sinister: true,
positions: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
},
wolfRampant: {
colors: 3,
sinister: true,
},
wolfStatant: {
colors: 3,
sinister: true,
},
wyvern: {
colors: 3,
sinister: true,
positions: { e: 10, jln: 1 },
},
wyvernWithWingsDisplayed: {
colors: 3,
sinister: true,
},
};

View file

@ -0,0 +1,477 @@
import { chargeData } from "./chargeData";
export const charges = {
types: {
conventional: 33, // 40 charges
crosses: 13, // 30 charges
beasts: 7, // 41 charges
beastHeads: 3, // 10 charges
birds: 3, // 16 charges
reptiles: 2, // 5 charges
bugs: 2, // 8 charges
fishes: 1, // 3 charges
molluscs: 1, // 2 charges
plants: 3, // 18 charges
fantastic: 5, // 14 charges
agriculture: 2, // 8 charges
arms: 5, // 32 charges
bodyparts: 2, // 12 charges
people: 2, // 4 charges
architecture: 3, // 11 charges
seafaring: 3, // 9 charges
tools: 3, // 15 charges
miscellaneous: 5, // 30 charges
inescutcheon: 3, // 43 charges
ornaments: 0, // 9 charges
uploaded: 0,
},
single: {
conventional: 10,
crosses: 8,
beasts: 7,
beastHeads: 3,
birds: 3,
reptiles: 2,
bugs: 2,
fishes: 1,
molluscs: 1,
plants: 3,
fantastic: 5,
agriculture: 2,
arms: 5,
bodyparts: 2,
people: 2,
architecture: 3,
seafaring: 3,
tools: 3,
miscellaneous: 5,
inescutcheon: 1,
},
semy: {
conventional: 4,
crosses: 1,
},
conventional: {
annulet: 4,
billet: 5,
carreau: 1,
comet: 1,
compassRose: 1,
crescent: 5,
delf: 0,
estoile: 1,
fleurDeLis: 6,
fountain: 1,
fusil: 4,
gear: 1,
goutte: 4,
heart: 4,
lozenge: 2,
lozengeFaceted: 3,
lozengePloye: 1,
mascle: 4,
moonInCrescent: 1,
mullet: 5,
mullet10: 1,
mullet4: 3,
mullet6: 4,
mullet6Faceted: 1,
mullet6Pierced: 1,
mullet7: 1,
mullet8: 1,
mulletFaceted: 1,
mulletPierced: 1,
pique: 2,
roundel: 4,
roundel2: 3,
rustre: 2,
spiral: 1,
sun: 3,
sunInSplendour: 1,
sunInSplendour2: 1,
trefle: 2,
triangle: 3,
trianglePierced: 1,
},
crosses: {
crossHummetty: 15,
crossVoided: 1,
crossPattee: 2,
crossPatteeAlisee: 1,
crossFormee: 1,
crossFormee2: 2,
crossPotent: 2,
crossJerusalem: 1,
crosslet: 1,
crossClechy: 3,
crossBottony: 1,
crossFleury: 3,
crossPatonce: 1,
crossPommy: 1,
crossGamma: 1,
crossArrowed: 1,
crossFitchy: 1,
crossCercelee: 1,
crossMoline: 2,
crossFourchy: 1,
crossAvellane: 1,
crossErminee: 1,
crossBiparted: 1,
crossMaltese: 3,
crossTemplar: 2,
crossCeltic: 1,
crossCeltic2: 1,
crossTriquetra: 1,
crossCarolingian: 1,
crossOccitan: 1,
crossSaltire: 3,
crossBurgundy: 1,
crossLatin: 3,
crossPatriarchal: 1,
crossOrthodox: 1,
crossCalvary: 1,
crossDouble: 1,
crossTau: 1,
crossSantiago: 1,
crossAnkh: 1,
},
beasts: {
agnusDei: 1,
badgerStatant: 1,
bearPassant: 1,
bearRampant: 3,
boarRampant: 1,
bullPassant: 1,
camel: 1,
catPassantGuardant: 1,
cowStatant: 1,
dolphin: 1,
elephant: 1,
goat: 1,
greyhoundCourant: 1,
greyhoundRampant: 1,
greyhoundSejant: 1,
hedgehog: 1,
hindStatant: 1,
horsePassant: 1,
horseRampant: 2,
horseSalient: 1,
lamb: 1,
lambPassantReguardant: 1,
lionPassant: 3,
lionPassantGuardant: 2,
lionRampant: 7,
lionSejant: 2,
martenCourant: 1,
mastiffStatant: 1,
porcupine: 1,
rabbitSejant: 1,
ramPassant: 1,
ratRampant: 1,
rhinoceros: 1,
squirrel: 1,
stagLodgedRegardant: 1,
stagPassant: 1,
talbotPassant: 1,
talbotSejant: 1,
wolfPassant: 1,
wolfRampant: 1,
wolfStatant: 1,
},
beastHeads: {
boarHeadErased: 1,
bullHeadCaboshed: 1,
deerHeadCaboshed: 1,
donkeyHeadCaboshed: 1,
elephantHeadErased: 1,
horseHeadCouped: 1,
lionHeadCaboshed: 2,
lionHeadErased: 2,
ramHeadErased: 1,
wolfHeadErased: 2,
},
birds: {
cock: 3,
dove: 2,
doveDisplayed: 1,
duck: 1,
eagle: 9,
falcon: 2,
heron: 1,
owl: 1,
owlDisplayed: 1,
parrot: 1,
peacock: 1,
peacockInPride: 1,
raven: 2,
swallow: 1,
swan: 2,
swanErased: 1,
},
reptiles: {
crocodile: 1,
frog: 1,
lizard: 1,
ouroboros: 1,
snake: 1,
},
bugs: {
bee: 1,
butterfly: 1,
cancer: 1,
dragonfly: 1,
fly: 1,
ladybird: 1,
scorpion: 1,
wasp: 1,
},
fishes: {
pike: 1,
plaice: 1,
salmon: 1,
},
molluscs: {
escallop: 4,
snail: 1,
},
plants: {
apple: 1,
cinquefoil: 1,
earOfWheat: 1,
grapeBunch: 1,
grapeBunch2: 1,
mapleLeaf: 1,
oak: 1,
palmTree: 1,
pear: 1,
pineCone: 1,
pineTree: 1,
quatrefoil: 1,
rose: 1,
sextifoil: 1,
thistle: 1,
tree: 1,
trefoil: 1,
wheatStalk: 1,
},
fantastic: {
angel: 3,
basilisk: 1,
centaur: 1,
dragonPassant: 3,
dragonRampant: 2,
eagleTwoHeads: 2,
griffinPassant: 1,
griffinRampant: 2,
pegasus: 1,
sagittarius: 1,
serpent: 1,
unicornRampant: 1,
wyvern: 1,
wyvernWithWingsDisplayed: 1,
},
agriculture: {
garb: 2,
millstone: 1,
plough: 1,
ploughshare: 1,
rake: 1,
scythe: 1,
scythe2: 1,
sickle: 1,
},
arms: {
arbalest: 1,
arbalest2: 1,
arrow: 1,
arrowsSheaf: 1,
axe: 3,
bow: 1,
bowWithArrow: 2,
bowWithThreeArrows: 1,
cannon: 1,
falchion: 1,
flamberge: 1,
flangedMace: 1,
gauntlet: 1,
grenade: 1,
hatchet: 3,
helmet: 2,
helmetCorinthian: 1,
helmetGreat: 2,
helmetZischagge: 1,
lanceHead: 1,
lanceWithBanner: 1,
lochaberAxe: 1,
mace: 1,
maces: 1,
mallet: 1,
rapier: 1,
sabre: 1,
sabre2: 1,
sabresCrossed: 1,
shield: 1,
spear: 1,
sword: 4,
},
bodyparts: {
armEmbowedHoldingSabre: 1,
armEmbowedVambraced: 1,
armEmbowedVambracedHoldingSword: 1,
bone: 1,
crossedBones: 2,
foot: 1,
hand: 4,
head: 1,
headWreathed: 1,
skeleton: 2,
skull: 2,
skull2: 1,
},
people: {
archer: 1,
cavalier: 3,
cossack: 1,
monk: 1,
},
architecture: {
bridge: 1,
bridge2: 1,
castle: 2,
castle2: 1,
column: 1,
lighthouse: 1,
palace: 1,
pillar: 1,
portcullis: 1,
tower: 2,
windmill: 1,
},
seafaring: {
anchor: 6,
armillarySphere: 1,
boat: 2,
boat2: 1,
caravel: 1,
drakkar: 1,
lymphad: 2,
raft: 1,
shipWheel: 1,
},
tools: {
anvil: 2,
drawingCompass: 2,
fan: 1,
hook: 1,
ladder: 1,
ladder2: 1,
pincers: 1,
saw: 1,
scale: 1,
scaleImbalanced: 1,
scalesHanging: 1,
scissors: 1,
scissors2: 1,
shears: 1,
trowel: 1,
},
miscellaneous: {
attire: 2,
banner: 2,
bell: 3,
bookClosed: 1,
bookClosed2: 1,
bookOpen: 1,
bucket: 1,
buckle: 1,
bugleHorn: 2,
bugleHorn2: 1,
chain: 2,
chalice: 2,
cowHorns: 3,
crosier: 1,
crown: 3,
crown2: 2,
drum: 1,
fasces: 1,
feather: 3,
harp: 2,
horseshoe: 3,
hourglass: 2,
key: 3,
laurelWreath: 2,
laurelWreath2: 1,
log: 1,
lute: 2,
lyre: 1,
mitre: 1,
orb: 1,
pot: 2,
ramsHorn: 1,
sceptre: 1,
scrollClosed: 1,
snowflake: 1,
stagsAttires: 1,
stirrup: 2,
wheel: 3,
wing: 2,
wingSword: 1,
},
inescutcheon: {
inescutcheonHeater: 1,
inescutcheonSpanish: 1,
inescutcheonFrench: 1,
inescutcheonHorsehead: 1,
inescutcheonHorsehead2: 1,
inescutcheonPolish: 1,
inescutcheonHessen: 1,
inescutcheonSwiss: 1,
inescutcheonBoeotian: 1,
inescutcheonRoman: 1,
inescutcheonKite: 1,
inescutcheonOldFrench: 1,
inescutcheonRenaissance: 1,
inescutcheonBaroque: 1,
inescutcheonTarge: 1,
inescutcheonTarge2: 1,
inescutcheonPavise: 1,
inescutcheonWedged: 1,
inescutcheonFlag: 1,
inescutcheonPennon: 1,
inescutcheonGuidon: 1,
inescutcheonBanner: 1,
inescutcheonDovetail: 1,
inescutcheonGonfalon: 1,
inescutcheonPennant: 1,
inescutcheonRound: 1,
inescutcheonOval: 1,
inescutcheonVesicaPiscis: 1,
inescutcheonSquare: 1,
inescutcheonDiamond: 1,
inescutcheonNo: 1,
inescutcheonFantasy1: 1,
inescutcheonFantasy2: 1,
inescutcheonFantasy3: 1,
inescutcheonFantasy4: 1,
inescutcheonFantasy5: 1,
inescutcheonNoldor: 1,
inescutcheonGondor: 1,
inescutcheonEasterling: 1,
inescutcheonErebor: 1,
inescutcheonIronHills: 1,
inescutcheonUrukHai: 1,
inescutcheonMoriaOrc: 1,
},
ornaments: {
mantle: 0,
ribbon1: 3,
ribbon2: 2,
ribbon3: 1,
ribbon4: 1,
ribbon5: 1,
ribbon6: 1,
ribbon7: 1,
ribbon8: 1,
},
data: chargeData,
};

View file

@ -0,0 +1,12 @@
export const colors = {
argent: "#fafafa",
or: "#ffe066",
gules: "#d7374a",
sable: "#333333",
azure: "#377cd7",
vert: "#26c061",
purpure: "#522d5b",
murrey: "#85185b",
sanguine: "#b63a3a",
tenné: "#cc7f19",
};

View file

@ -0,0 +1,46 @@
import { lineWeights } from "./lineWeights";
export const divisions = {
variants: {
perPale: 5,
perFess: 5,
perBend: 2,
perBendSinister: 1,
perChevron: 1,
perChevronReversed: 1,
perCross: 5,
perPile: 1,
perSaltire: 1,
gyronny: 1,
chevronny: 1,
},
perPale: lineWeights,
perFess: lineWeights,
perBend: lineWeights,
perBendSinister: lineWeights,
perChevron: lineWeights,
perChevronReversed: lineWeights,
perCross: {
straight: 20,
wavy: 5,
engrailed: 4,
invecked: 3,
rayonne: 1,
embattled: 1,
raguly: 1,
urdy: 1,
indented: 2,
dentilly: 1,
bevilled: 1,
angled: 1,
embattledGhibellin: 1,
embattledGrady: 1,
dovetailedIndented: 1,
dovetailed: 1,
potenty: 1,
potentyDexter: 1,
potentySinister: 1,
nebuly: 1,
},
perPile: lineWeights,
};

View file

@ -0,0 +1,591 @@
import { P, rw } from "../../utils";
import { charges } from "./charges";
import { divisions } from "./divisions";
import { lineWeights } from "./lineWeights";
import { ordinaries } from "./ordinaries";
import { positions } from "./positions";
import { shields } from "./shields";
import { createTinctures } from "./tinctures";
import { typeMapping } from "./typeMapping";
declare global {
var COA: EmblemGeneratorModule;
}
export interface EmblemCharge {
charge: string;
t: string;
p: string;
t2?: string;
t3?: string;
size?: number;
sinister?: number;
reversed?: number;
divided?: string;
}
export interface EmblemOrdinary {
ordinary: string;
t: string;
line?: string;
divided?: string;
above?: boolean;
}
export interface EmblemDivision {
division: string;
t: string;
line?: string;
}
export interface Emblem {
t1: string;
shield?: string;
division?: EmblemDivision;
ordinaries?: EmblemOrdinary[];
charges?: EmblemCharge[];
custom?: boolean;
}
class EmblemGeneratorModule {
generate(
parent: Emblem | null,
kinship: number | null,
dominion: number | null,
type?: string,
): Emblem {
if (!parent || parent.custom) {
parent = null;
kinship = 0;
dominion = 0;
}
let usedPattern: string | null = null;
const usedTinctures: string[] = [];
const t1 = P(kinship as number)
? parent!.t1
: this.getTincture("field", usedTinctures, null);
if (t1.includes("-")) usedPattern = t1;
const coa: Emblem = { t1 };
const addCharge = P(usedPattern ? 0.5 : 0.93); // 80% for charge
const linedOrdinary =
(addCharge && P(0.3)) || P(0.5)
? parent?.ordinaries && P(kinship as number)
? parent.ordinaries[0].ordinary
: rw(ordinaries.lined)
: null;
const ordinary =
(!addCharge && P(0.65)) || P(0.3)
? linedOrdinary
? linedOrdinary
: rw(ordinaries.straight)
: null; // 36% for ordinary
const rareDivided = [
"chief",
"terrace",
"chevron",
"quarter",
"flaunches",
].includes(ordinary!);
const divisioned = (() => {
if (rareDivided) return P(0.03);
if (addCharge && ordinary) return P(0.03);
if (addCharge) return P(0.3);
if (ordinary) return P(0.7);
return P(0.995);
})();
const division = (() => {
if (divisioned) {
if (parent?.division && P((kinship as number) - 0.1))
return parent.division.division;
return rw(divisions.variants);
}
return null;
})();
if (division) {
const t = this.getTincture(
"division",
usedTinctures,
P(0.98) ? coa.t1 : null,
);
coa.division = { division, t };
if (divisions[division as keyof typeof divisions])
coa.division.line =
usedPattern || (ordinary && P(0.7))
? "straight"
: rw(divisions[division as keyof typeof divisions]);
}
if (ordinary) {
coa.ordinaries = [
{ ordinary, t: this.getTincture("charge", usedTinctures, coa.t1) },
];
if (linedOrdinary)
coa.ordinaries[0].line =
usedPattern || (division && P(0.7)) ? "straight" : rw(lineWeights);
if (
division &&
!addCharge &&
!usedPattern &&
P(0.5) &&
ordinary !== "bordure" &&
ordinary !== "orle"
) {
if (P(0.8)) coa.ordinaries[0].divided = "counter";
// 40%
else if (P(0.6)) coa.ordinaries[0].divided = "field";
// 6%
else coa.ordinaries[0].divided = "division"; // 4%
}
}
if (addCharge) {
const charge = (() => {
if (parent?.charges && P((kinship as number) - 0.1))
return parent.charges[0].charge;
if (type && type !== "Generic" && P(0.3)) return rw(typeMapping[type]);
return this.selectCharge(
ordinary || divisioned ? charges.types : charges.single,
);
})();
const chargeDataEntry = charges.data[charge] || {};
let p: string;
let t: string;
const ordinaryData = ordinaries.data[ordinary!];
const tOrdinary = coa.ordinaries ? coa.ordinaries[0].t : null;
if (ordinaryData?.positionsOn && P(0.8)) {
// place charge over ordinary (use tincture of field type)
p = rw(ordinaryData.positionsOn);
t =
!usedPattern && P(0.3)
? coa.t1
: this.getTincture("charge", [], tOrdinary);
} else if (ordinaryData?.positionsOff && P(0.95)) {
// place charge out of ordinary (use tincture of ordinary type)
p = rw(ordinaryData.positionsOff);
t =
!usedPattern && P(0.3)
? tOrdinary!
: this.getTincture("charge", usedTinctures, coa.t1);
} else if (
positions.divisions[division as keyof typeof positions.divisions]
) {
// place charge in fields made by division
p = rw(
positions.divisions[division as keyof typeof positions.divisions],
);
t = this.getTincture(
"charge",
tOrdinary ? usedTinctures.concat(tOrdinary) : usedTinctures,
coa.t1,
);
} else if (chargeDataEntry.positions) {
// place charge-suitable position
p = rw(chargeDataEntry.positions);
t = this.getTincture("charge", usedTinctures, coa.t1);
} else {
// place in standard position (use new tincture)
p = usedPattern
? "e"
: charges.conventional[charge as keyof typeof charges.conventional]
? rw(positions.conventional)
: rw(positions.complex);
t = this.getTincture(
"charge",
usedTinctures.concat(tOrdinary!),
coa.t1,
);
}
if (
chargeDataEntry.natural &&
chargeDataEntry.natural !== t &&
chargeDataEntry.natural !== tOrdinary
)
t = chargeDataEntry.natural;
const item: EmblemCharge = { charge: charge, t, p };
const colors = chargeDataEntry.colors || 1;
if (colors > 1)
item.t2 = P(0.25)
? this.getTincture("charge", usedTinctures, coa.t1)
: t;
if (colors > 2 && item.t2)
item.t3 = P(0.5)
? this.getTincture("charge", usedTinctures, coa.t1)
: t;
coa.charges = [item];
if (p === "ABCDEFGHIJKL" && P(0.95)) {
// add central charge if charge is in bordure
coa.charges[0].charge = rw(charges.conventional);
const chargeNew = this.selectCharge(charges.single);
const tNew = this.getTincture("charge", usedTinctures, coa.t1);
coa.charges.push({ charge: chargeNew, t: tNew, p: "e" });
} else if (P(0.8) && charge === "inescutcheon") {
// add charge to inescutcheon
const chargeNew = this.selectCharge(charges.types);
const t2 = this.getTincture("charge", [], t);
coa.charges.push({ charge: chargeNew, t: t2, p, size: 0.5 });
} else if (division && !ordinary) {
const allowCounter =
!usedPattern &&
(!coa.division?.line || coa.division.line === "straight");
// dimidiation: second charge at division basic positions
if (
P(0.3) &&
["perPale", "perFess"].includes(division) &&
coa.division?.line === "straight"
) {
coa.charges[0].divided = "field";
if (P(0.95)) {
const p2 =
p === "e" || P(0.5)
? "e"
: rw(
positions.divisions[
division as keyof typeof positions.divisions
],
);
const chargeNew = this.selectCharge(charges.single);
const tNew = this.getTincture(
"charge",
usedTinctures,
coa.division!.t,
);
coa.charges.push({
charge: chargeNew,
t: tNew,
p: p2,
divided: "division",
});
}
} else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
// counterchanged, 40%
else if (
["perPale", "perFess", "perBend", "perBendSinister"].includes(
division,
) &&
P(0.8)
) {
// place 2 charges in division standard positions
const [p1, p2] =
division === "perPale"
? ["p", "q"]
: division === "perFess"
? ["k", "n"]
: division === "perBend"
? ["l", "m"]
: ["j", "o"]; // perBendSinister
coa.charges[0].p = p1;
const chargeNew = this.selectCharge(charges.single);
const tNew = this.getTincture(
"charge",
usedTinctures,
coa.division!.t,
);
coa.charges.push({ charge: chargeNew, t: tNew, p: p2 });
} else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
// place 4 charges in division standard positions
const [p1, p2, p3, p4] =
division === "perCross"
? ["j", "l", "m", "o"]
: ["b", "d", "f", "h"];
coa.charges[0].p = p1;
const c2 = this.selectCharge(charges.single);
const t2 = this.getTincture("charge", [], coa.division!.t);
const c3 = this.selectCharge(charges.single);
const t3 = this.getTincture("charge", [], coa.division!.t);
const c4 = this.selectCharge(charges.single);
const t4 = this.getTincture("charge", [], coa.t1);
coa.charges.push(
{ charge: c2, t: t2, p: p2 },
{ charge: c3, t: t3, p: p3 },
{ charge: c4, t: t4, p: p4 },
);
} else if (allowCounter && p.length > 1)
coa.charges[0].divided = "counter"; // counterchanged, 40%
}
for (const c of coa.charges) {
this.defineChargeAttributes(ordinary, division, c);
}
}
// dominions have canton with parent coa
if (P(dominion as number) && parent?.charges) {
const invert = this.isSameType(parent.t1, coa.t1);
const t = invert
? this.getTincture("division", usedTinctures, coa.t1)
: parent.t1;
const canton: EmblemOrdinary = { ordinary: "canton", t };
if (coa.charges) {
for (let i = coa.charges.length - 1; i >= 0; i--) {
const charge = coa.charges[i];
if (charge.size === 1.5) charge.size = 1.4;
charge.p = charge.p.replaceAll(/[ajy]/g, "");
if (!charge.p) coa.charges.splice(i, 1);
}
}
let charge = parent.charges[0].charge;
if (charge === "inescutcheon" && parent.charges[1])
charge = parent.charges[1].charge;
let t2 = invert ? parent.t1 : parent.charges[0].t;
if (this.isSameType(t, t2))
t2 = this.getTincture("charge", usedTinctures, t);
if (!coa.charges) coa.charges = [];
coa.charges.push({ charge, t: t2, p: "y", size: 0.5 });
if (coa.ordinaries) {
coa.ordinaries.push(canton);
} else {
coa.ordinaries = [canton];
}
}
return coa;
}
private selectCharge(set?: Record<string, number>): string {
const type = set ? rw(set) : rw(charges.types);
return type === "inescutcheon"
? "inescutcheon"
: rw(charges[type as keyof typeof charges] as Record<string, number>);
}
// Select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
private getTincture(
element: "field" | "division" | "charge",
fields: string[] = [],
RoT: string | null,
): string {
const base = RoT ? (RoT.includes("-") ? RoT.split("-")[1] : RoT) : null;
const tinctures = createTinctures();
let type = rw(tinctures[element]); // metals, colours, stains, patterns
if (RoT && type !== "patterns")
type = this.getType(base!) === "metals" ? "colours" : "metals"; // follow RoT
if (type === "metals" && fields.includes("or") && fields.includes("argent"))
type = "colours"; // exclude metals overuse
let tincture = rw(
tinctures[type as keyof typeof tinctures] as Record<string, number>,
);
while (tincture === base || fields.includes(tincture)) {
tincture = rw(
tinctures[type as keyof typeof tinctures] as Record<string, number>,
);
} // follow RoT
if (type !== "patterns" && element !== "charge") fields.push(tincture); // add field tincture
if (type === "patterns") {
tincture = this.definePattern(tincture, element, fields);
}
return tincture;
}
private defineChargeAttributes(
ordinary: string | null,
division: string | null,
c: EmblemCharge,
): void {
// define size
c.size = (c.size || 1) * this.getSize(c.p, ordinary, division);
// clean-up position
c.p = [...new Set(c.p)].join("");
// define orientation
if (P(0.02) && charges.data[c.charge]?.sinister) c.sinister = 1;
if (P(0.02) && charges.data[c.charge]?.reversed) c.reversed = 1;
}
private getType(t: string): string | undefined {
const tinc = t.includes("-") ? t.split("-")[1] : t;
const tinctures = createTinctures();
if (Object.keys(tinctures.metals).includes(tinc)) return "metals";
if (Object.keys(tinctures.colours).includes(tinc)) return "colours";
if (Object.keys(tinctures.stains).includes(tinc)) return "stains";
return undefined;
}
private isSameType(t1: string, t2: string): boolean {
return this.typeOf(t1) === this.typeOf(t2);
}
private typeOf(tinc: string): string {
const tinctures = createTinctures();
if (Object.keys(tinctures.metals).includes(tinc)) return "metals";
if (Object.keys(tinctures.colours).includes(tinc)) return "colours";
if (Object.keys(tinctures.stains).includes(tinc)) return "stains";
return "pattern";
}
private definePattern(
pattern: string,
element: "field" | "division" | "charge",
usedTinctures: string[],
): string {
let t1: string | null = null;
let t2: string | null = null;
let size = "";
// Size selection - must use sequential P() calls to match original behavior
if (P(0.1)) size = "-small";
// biome-ignore lint/suspicious/noDuplicateElseIf: <explanation>
else if (P(0.1)) size = "-smaller";
else if (P(0.01)) size = "-big";
else if (P(0.005)) size = "-smallest";
// apply standard tinctures
if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
t1 = "azure";
t2 = "argent";
} else if (P(0.8) && pattern === "ermine") {
t1 = "argent";
t2 = "sable";
} else if (pattern === "pappellony") {
if (P(0.2)) {
t1 = "gules";
t2 = "or";
// biome-ignore lint/suspicious/noDuplicateElseIf: <explanation>
} else if (P(0.2)) {
t1 = "argent";
t2 = "sable";
// biome-ignore lint/suspicious/noDuplicateElseIf: <explanation>
} else if (P(0.2)) {
t1 = "azure";
t2 = "argent";
}
} else if (pattern === "masoned") {
if (P(0.3)) {
t1 = "gules";
t2 = "argent";
// biome-ignore lint/suspicious/noDuplicateElseIf: <explanation>
} else if (P(0.3)) {
t1 = "argent";
t2 = "sable";
} else if (P(0.1)) {
t1 = "or";
t2 = "sable";
}
} else if (pattern === "fretty") {
if (t2 === "sable" || P(0.35)) {
t1 = "argent";
t2 = "gules";
} else if (P(0.25)) {
t1 = "sable";
t2 = "or";
} else if (P(0.15)) {
t1 = "gules";
t2 = "argent";
}
} else if (pattern === "semy")
pattern = `${pattern}_of_${this.selectCharge(charges.semy)}`;
if (!t1 || !t2) {
const tinctures = createTinctures();
const startWithMetal = P(0.7);
t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours);
t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals);
}
// division should not be the same tincture as base field
if (element === "division") {
if (usedTinctures.includes(t1)) t1 = this.replaceTincture(t1);
if (usedTinctures.includes(t2)) t2 = this.replaceTincture(t2);
}
usedTinctures.push(t1, t2);
return `${pattern}-${t1}-${t2}${size}`;
}
private replaceTincture(t: string): string {
const type = this.getType(t);
let n: string | null = null;
const tinctures = createTinctures();
while (!n || n === t) {
n = rw(
tinctures[type as keyof typeof tinctures] as Record<string, number>,
);
}
return n;
}
private getSize(
p: string,
o: string | null = null,
d: string | null = null,
): number {
if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
if (p === "e") return 1.5;
if (p === "jln" || p === "jlh") return 0.7;
if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p))
return 0.5;
if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross")
return 0.6;
if (p.length > 10) return 0.18; // >10 (bordure)
if (p.length > 7) return 0.3; // 8, 9, 10
if (p.length > 4) return 0.4; // 5, 6, 7
if (p.length > 2) return 0.5; // 3, 4
return 0.7; // 1, 2
}
getShield(culture: number, state?: number): string {
const emblemShape = document.getElementById(
"emblemShape",
) as HTMLSelectElement | null;
const shapeGroup =
emblemShape?.selectedOptions[0]?.parentElement?.getAttribute("label") ||
"Diversiform";
if (shapeGroup !== "Diversiform") return emblemShape!.value;
if (emblemShape?.value === "state" && state && pack.states[state].coa)
return pack.states[state].coa!.shield!;
if (pack.cultures[culture].shield) return pack.cultures[culture].shield!;
ERROR &&
console.error(
"Shield shape is not defined on culture level",
pack.cultures[culture],
);
return "heater";
}
toString(coa: Emblem): string {
return JSON.stringify(coa).replaceAll("#", "%23");
}
copy(coa: Emblem): Emblem {
return JSON.parse(JSON.stringify(coa));
}
get shields() {
return shields;
}
}
export default EmblemGeneratorModule;
window.COA = new EmblemGeneratorModule();

View file

@ -0,0 +1,2 @@
import "./generator";
import "./renderer";

View file

@ -0,0 +1,37 @@
// Line weights for random selection
// Different from lines.ts which contains SVG path data for rendering
export const lineWeights = {
straight: 50,
wavy: 8,
engrailed: 4,
invecked: 3,
rayonne: 3,
embattled: 1,
raguly: 1,
urdy: 1,
dancetty: 1,
indented: 2,
dentilly: 1,
bevilled: 1,
angled: 1,
flechy: 1,
barby: 1,
enclavy: 1,
escartely: 1,
arched: 2,
archedReversed: 1,
nowy: 1,
nowyReversed: 1,
embattledGhibellin: 1,
embattledNotched: 1,
embattledGrady: 1,
dovetailedIndented: 1,
dovetailed: 1,
potenty: 1,
potentyDexter: 1,
potentySinister: 1,
nebuly: 2,
seaWaves: 1,
dragonTeeth: 1,
firTrees: 1,
};

View file

@ -0,0 +1,57 @@
export const lines = {
straight: "m 0,100 v15 h 200 v -15 z",
engrailed:
"m 0,95 a 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 v 20 H 0 Z",
invecked:
"M0,102.5 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 v12.5 H0 z",
embattled:
"M 0,105 H 2.5 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 2.5 v 10 H 0 Z",
wavy: "m 200,115 v -15 c -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 v 15 z",
raguly:
"m 200,95 h -3 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 H 97 l -5,10 H 82 L 87,95 H 77 l -5,10 H 62 L 67,95 H 57 l -5,10 H 42 L 47,95 H 37 l -5,10 H 22 L 27,95 H 17 l -5,10 H 2 L 7,95 H 0 v 20 h 200 z",
dancetty:
"m 0,105 10,-15 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 10,15 v 10 H 0 Z",
dentilly:
"M 180,105 170,95 v 10 L 160,95 v 10 L 150,95 v 10 L 140,95 v 10 L 130,95 v 10 L 120,95 v 10 L 110,95 v 10 L 100,95 v 10 L 90,95 v 10 L 80,95 v 10 L 70,95 v 10 L 60,95 v 10 L 50,95 v 10 L 40,95 v 10 L 30,95 v 10 L 20,95 v 10 L 10,95 v 10 L 0,95 v 20 H 200 V 105 L 190,95 v 10 L 180,95 Z",
angled: "m 0,95 h 100 v 10 h 100 v 10 H 0 Z",
urdy: "m 200,90 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 L 0,90 v 25 h 200",
indented:
"m 100,95 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 v 20 H 0 V 95 l 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 z",
bevilled: "m 0,92.5 h 110 l -20,15 H 200 V 115 H 0 Z",
nowy: "m 0,95 h 80 c 0,0 0.1,20.1 20,20 19.9,-0.1 20,-20 20,-20 h 80 v 20 H 0 Z",
nowyReversed:
"m 200,105 h -80 c 0,0 -0.1,-20.1 -20,-20 -19.9,0.1 -20,20 -20,20 H 0 v 10 h 200 z",
potenty:
"m 3,95 v 5 h 5 v 5 H 0 v 10 h 200 l 0.5,-10 H 193 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 H 100.5 93 v -5 h 5 V 95 H 83 v 5 h 5 v 5 H 73 v -5 h 5 V 95 H 63 v 5 h 5 v 5 H 53 v -5 h 5 V 95 H 43 v 5 h 5 v 5 H 33 v -5 h 5 V 95 H 23 v 5 h 5 v 5 H 13 v -5 h 5 v -5 z",
potentyDexter:
"m 200,105 h -2 v -10 0 0 h -10 v 5 h 5 v 5 H 183 V 95 h -10 v 5 h 5 v 5 H 168 V 95 h -10 v 5 h 5 v 5 H 153 V 95 h -10 v 5 h 5 v 5 H 138 V 95 h -10 v 5 h 5 v 5 H 123 V 95 h -10 v 5 h 5 v 5 h -10 v 0 0 -10 H 98 v 5 h 5 v 5 H 93 V 95 H 83 v 5 h 5 v 5 H 78 V 95 H 68 v 5 h 5 v 5 H 63 V 95 H 53 v 5 h 5 v 5 H 48 V 95 H 38 v 5 h 5 v 5 H 33 V 95 H 23 v 5 h 5 v 5 H 18 V 95 H 8 v 5 h 5 v 5 H 3 V 95 H 0 v 20 h 200 z",
potentySinister:
"m 2.5,95 v 10 H 0 v 10 h 202.5 v -15 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 z",
embattledGhibellin:
"M 200,200 V 100 l -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 v 15 h 200",
embattledNotched:
"m 200,105 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 H 90 V 95 l -5,5 -5,-5 v 10 H 75 V 95 l -5,5 -5,-5 v 10 H 60 V 95 l -5,5 -5,-5 v 10 H 45 V 95 l -5,5 -5,-5 v 10 H 30 V 95 l -5,5 -5,-5 v 10 H 15 V 95 l -5,5 -5,-5 v 10 H 0 v 10 h 200",
embattledGrady:
"m 0,95 v 20 H 200 V 95 h -2.5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 z",
dovetailed:
"m 200,95 h -7 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 H 93 l 4,10 H 83 L 87,95 H 73 l 4,10 H 63 L 67,95 H 53 l 4,10 H 43 L 47,95 H 33 l 4,10 H 23 L 27,95 H 13 l 4,10 H 3 L 7,95 H 0 v 20 h 200",
dovetailedIndented:
"m 200,100 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 v 15 h 200",
nebuly:
"m 13.1,89.8 c -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.2,4.5 -7.3,4.5 -0.5,0 -2.2,-0.2 -2.2,-0.2 V 115 h 200 v -10.1 c -3.7,-0.2 -6.7,-2.2 -6.7,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 -1.5,4.1 -4.2,4.4 -8.8,4.5 -4.7,-0.1 -8.7,-1.5 -8.9,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 z",
rayonne:
"M0 115l-.1-6 .2.8c1.3-1 2.3-2.5 2.9-4.4.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4A9 9 0 015.5 90c-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 2.1 3.1 3.1 4.6 1 1.6 2.4 3.1 2.7 4.8.3 1.7.3 3.3 0 5.2 1.3-1 2.6-2.7 3.2-4.6.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.75 2.79 2.72 4.08 4.45 5.82L200 115z",
seaWaves:
"m 28.83,94.9 c -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.33,-2.03 -2.19,-3.56 -4.45,-3.56 -4.24,0 -6.91,3.13 -8.5,5.13 V 115 h 200 v -14.89 c -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -6.6,3.09 -8.19,5.09 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 z",
dragonTeeth:
"M 9.4,85 C 6.5,88.1 4.1,92.9 3,98.8 1.9,104.6 2.3,110.4 3.8,115 2.4,113.5 0,106.6 0,109.3 v 5.7 h 200 v -5.7 c -1.1,-2.4 -2,-5.1 -2.6,-8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -1.4,-1.5 -2.8,-3.9 -3.8,-6.1 -1.1,-2.4 -2.3,-6.1 -2.6,-7.7 -0.2,-5.9 0.2,-11.7 1.7,-16.3 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 C 63,95.4 63.4,89.6 64.9,85 c -2.9,3.1 -5.3,7.9 -6.3,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.6,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 C 18.6,95.4 19,89.6 20.5,85 17.6,88.1 15.2,92.9 14.1,98.8 13,104.6 13.4,110.4 14.9,115 12,111.9 9.6,107.1 8.6,101.2 7.5,95.4 7.9,89.6 9.4,85 Z",
firTrees:
"m 3.9,90 -4,7 2,-0.5 L 0,100 v 15 h 200 v -15 l -1.9,-3.5 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 z",
flechy: "m 0,100 h 85 l 15,-15 15,15 h 85 v 15 H 0 Z",
barby: "m 0,100 h 85 l 15,15 15,-15 h 85 v 15 H 0 Z",
enclavy: "M 0,100 H 85 V 85 h 30 v 15 h 85 v 15 H 0 Z",
escartely: "m 0,100 h 85 v 15 h 30 v -15 h 85 v 15 H 0 Z",
arched: "m 100,95 c 40,-0.2 100,20 100,20 H 0 c 0,0 60,-19.8 100,-20 z",
archedReversed:
"m 0,85 c 0,0 60,20.2 100,20 40,-0.2 100,-20 100,-20 v 30 H 0 Z",
};

View file

@ -0,0 +1,162 @@
export const ordinaries = {
lined: {
pale: 7,
fess: 5,
bend: 3,
bendSinister: 2,
chief: 5,
bar: 2,
gemelle: 1,
fessCotissed: 1,
fessDoubleCotissed: 1,
bendlet: 2,
bendletSinister: 1,
terrace: 3,
cross: 6,
crossParted: 1,
saltire: 2,
saltireParted: 1,
},
straight: {
bordure: 8,
orle: 4,
mount: 1,
point: 2,
flaunches: 1,
gore: 1,
gyron: 1,
quarter: 1,
canton: 2,
pall: 3,
pallReversed: 2,
chevron: 4,
chevronReversed: 3,
pile: 2,
pileInBend: 2,
pileInBendSinister: 1,
piles: 1,
pilesInPoint: 2,
label: 1,
},
data: {
bar: {
positionsOn: { defdefdef: 1 },
positionsOff: { abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1 },
},
bend: {
positionsOn: { ee: 2, jo: 1, joe: 1 },
positionsOff: { ccg: 2, ccc: 1 },
},
bendSinister: {
positionsOn: { ee: 1, lm: 1, lem: 4 },
positionsOff: { aai: 2, aaa: 1 },
},
bendlet: {
positionsOn: { joejoejoe: 1 },
positionsOff: { ccg: 2, ccc: 1 },
},
bendletSinister: {
positionsOn: { lemlemlem: 1 },
positionsOff: { aai: 2, aaa: 1 },
},
bordure: {
positionsOn: { ABCDEFGHIJKL: 1 },
positionsOff: { e: 4, jleh: 2, kenken: 1, peqpeq: 1 },
},
canton: {
positionsOn: { yyyy: 1 },
positionsOff: { e: 5, beh: 1, def: 1, bdefh: 1, kn: 1 },
},
chevron: {
positionsOn: { ach: 3, hhh: 1 },
},
chevronReversed: {
positionsOff: { bbb: 1 },
},
chief: {
positionsOn: { abc: 5, bbb: 1 },
positionsOff: { emo: 2, emoz: 1, ez: 2 },
},
cross: {
positionsOn: { eeee: 1, behdfbehdf: 3, behbehbeh: 2 },
positionsOff: { acgi: 1 },
},
crossParted: {
positionsOn: { e: 5, ee: 1 },
},
fess: {
positionsOn: { ee: 1, def: 3 },
positionsOff: { abc: 3, abcz: 1 },
},
fessCotissed: {
positionsOn: { ee: 1, def: 3 },
},
fessDoubleCotissed: {
positionsOn: { ee: 1, defdef: 3 },
},
flaunches: {
positionsOff: { e: 3, kn: 1, beh: 3 },
},
gemelle: {
positionsOff: { abc: 1 },
},
gyron: {
positionsOff: { bh: 1 },
},
label: {
positionsOff: { defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5 },
},
mount: {
positionsOff: { e: 5, def: 1, bdf: 3 },
},
orle: {
positionsOff: { e: 4, jleh: 1, kenken: 1, peqpeq: 1 },
},
pale: {
positionsOn: { ee: 12, beh: 10, kn: 3, bb: 1 },
positionsOff: { yyy: 1 },
},
pall: {
positionsOn: { ee: 1, jleh: 5, jlhh: 3 },
positionsOff: { BCKFEILGJbdmfo: 1 },
},
pallReversed: {
positionsOn: { ee: 1, bemo: 5 },
positionsOff: { aczac: 1 },
},
pile: {
positionsOn: { bbb: 1 },
positionsOff: { acdfgi: 1, acac: 1 },
},
pileInBend: {
positionsOn: { eeee: 1, eeoo: 1 },
positionsOff: { cg: 1 },
},
pileInBendSinister: {
positionsOn: { eeee: 1, eemm: 1 },
positionsOff: { ai: 1 },
},
point: {
positionsOff: { e: 2, def: 1, bdf: 3, acbdef: 1 },
},
quarter: {
positionsOn: { jjj: 1 },
positionsOff: { e: 1 },
},
saltire: {
positionsOn: { ee: 5, jlemo: 1 },
},
saltireParted: {
positionsOn: { e: 5, ee: 1 },
},
terrace: {
positionsOff: { e: 5, def: 1, bdf: 3 },
},
} as Record<
string,
{
positionsOn?: Record<string, number>;
positionsOff?: Record<string, number>;
}
>,
};

View file

@ -0,0 +1,70 @@
export const shieldPaths = {
heater: "m25,25 h150 v50 a150,150,0,0,1,-75,125 a150,150,0,0,1,-75,-125 z",
spanish: "m25,25 h150 v100 a75,75,0,0,1,-150,0 z",
french:
"m 25,25 h 150 v 139.15 c 0,41.745 -66,18.15 -75,36.3 -9,-18.15 -75,5.445 -75,-36.3 v 0 z",
horsehead:
"m 20,40 c 0,60 40,80 40,100 0,10 -4,15 -0.35,30 C 65,185.7 81,200 100,200 c 19.1,0 35.3,-14.6 40.5,-30.4 C 144.2,155 140,150 140,140 140,120 180,100 180,40 142.72,40 150,15 100,15 55,15 55,40 20,40 Z",
horsehead2:
"M60 20c-5 20-10 35-35 55 25 35 35 65 30 100 20 0 35 10 45 26 10-16 30-26 45-26-5-35 5-65 30-100a87 87 0 01-35-55c-25 3-55 3-80 0z",
polish:
"m 90.3,6.3 c -12.7,0 -20.7,10.9 -40.5,14 0,11.8 -4.9,23.5 -11.4,31.1 0,0 12.7,6 12.7,19.3 C 51.1,90.8 30,90.8 30,90.8 c 0,0 -3.6,7.4 -3.6,22.4 0,34.3 23.1,60.2 40.7,68.2 17.6,8 27.7,11.4 32.9,18.6 5.2,-7.3 15.3,-10.7 32.8,-18.6 17.6,-8 40.7,-33.9 40.7,-68.2 0,-15 -3.6,-22.4 -3.6,-22.4 0,0 -21.1,0 -21.1,-20.1 0,-13.3 12.7,-19.3 12.7,-19.3 C 155.1,43.7 150.2,32.1 150.2,20.3 130.4,17.2 122.5,6.3 109.7,6.3 102.5,6.3 100,10 100,10 c 0,0 -2.5,-3.7 -9.7,-3.7 z",
hessen:
"M170 20c4 5 8 13 15 20 0 0-10 0-10 15 0 100-15 140-75 145-65-5-75-45-75-145 0-15-10-15-10-15l15-20c0 15 10-5 70-5s70 20 70 5z",
swiss:
"m 25,20 c -0.1,0 25.2,8.5 37.6,8.5 C 75.1,28.5 99.1,20 100,20 c 0.6,0 24.9,8.5 37.3,8.5 C 149.8,28.5 174.4,20 175,20 l -0.3,22.6 C 173.2,160.3 100,200 100,200 100,200 26.5,160.9 25.2,42.6 Z",
boeotian:
"M150 115c-5 0-10-5-10-15s5-15 10-15c10 0 7 10 15 10 10 0 0-30 0-30-10-25-30-55-65-55S45 40 35 65c0 0-10 30 0 30 8 0 5-10 15-10 5 0 10 5 10 15s-5 15-10 15c-10 0-7-10-15-10-10 0 0 30 0 30 10 25 30 55 65 55s55-30 65-55c0 0 10-30 0-30-8 0-5 10-15 10z",
roman: "m 160,170 c -40,20 -80,20 -120,0 V 30 C 80,10 120,10 160,30 Z",
kite: "m 53.3,46.4 c 0,4.1 1,12.3 1,12.3 7.1,55.7 45.7,141.3 45.7,141.3 0,0 38.6,-85.6 45.7,-141.2 0,0 1,-8.1 1,-12.3 C 146.7,20.9 125.8,0.1 100,0.1 74.2,0.1 53.3,20.9 53.3,46.4 Z",
oldFrench: "m25,25 h150 v75 a100,100,0,0,1,-75,100 a100,100,0,0,1,-75,-100 z",
renaissance:
"M 25,33.9 C 33.4,50.3 36.2,72.9 36.2,81.7 36.2,109.9 25,122.6 25,141 c 0,29.4 24.9,44.1 40.2,47.7 15.3,3.7 29.3,0 34.8,11.3 5.5,-11.3 19.6,-7.6 34.8,-11.3 C 150.1,185 175,170.3 175,141 c 0,-18.4 -11.2,-31.1 -11.2,-59.3 0,-8.8 2.8,-31.3 11.2,-47.7 L 155.7,14.4 C 138.2,21.8 119.3,25.7 100,25.7 c -19.3,0 -38.2,-3.9 -55.7,-11.3 z",
baroque:
"m 100,25 c 18,0 50,2 75,14 v 37 l -2.7,3.2 c -4.9,5.4 -6.6,9.6 -6.7,16.2 0,6.5 2,11.6 6.9,17.2 l 2.8,3.1 v 10.2 c 0,17.7 -2.2,27.7 -7.8,35.9 -5,7.3 -11.7,11.3 -32.3,19.4 -12.6,5 -20.2,8.8 -28.6,14.5 C 103.3,198 100,200 100,200 c 0,0 -2.8,-2.3 -6.4,-4.7 C 85.6,189.8 78,186 65,180.9 32.4,168.1 26.9,160.9 25.8,129.3 L 25,116 l 3.3,-3.3 c 4.8,-5.2 7,-10.7 7,-17.3 0,-6.8 -1.8,-11.1 -6.5,-16.1 L 25,76 V 39 C 50,27 82,25 100,25 Z",
targe:
"m 20,35 c 15,0 115,-60 155,-10 -5,10 -15,15 -10,50 5,45 10,70 -10,90 C 125,195 75,195 50,175 25,150 30,130 35,85 50,95 65,85 65,70 65,50 50,45 40,50 30,55 27,65 30,70 23,73 20,70 14,70 11,60 20,45 20,35 Z",
targe2:
"m 84,32.2 c 6.2,-1 19.5,-31.4 94.1,-20.2 -30.57,33.64 -21.66,67.37 -11.2,95 20.2,69.5 -41.17549,84.7 -66.88,84.7 C 74.32,191.7071 8.38,168.95 32,105.9 36.88,92.88 31,89 31,82.6 35.15,82.262199 56.79,86.17 56.5,69.8 56.20,52.74 42.2,47.9 25.9,55.2 25.9,51.4 39.8,6.7 84,32.2 Z",
pavise:
"M95 7L39.9 37.3a10 10 0 00-5.1 9.5L46 180c.4 5.2 3.7 10 9 10h90c5.3 0 9.6-4.8 10-10l10.6-133.2a10 10 0 00-5-9.5L105 7c-4.2-2.3-6.2-2.3-10 0z",
wedged:
"m 51.2,19 h 96.4 c 3.1,12.7 10.7,20.9 26.5,20.8 C 175.7,94.5 165.3,144.3 100,200 43.5,154.2 22.8,102.8 25.1,39.7 37,38.9 47.1,34.7 51.2,19 Z",
round:
"m 185,100 a 85,85 0 0 1 -85,85 85,85 0 0 1 -85,-85 85,85 0 0 1 85,-85 85,85 0 0 1 85,85",
oval: "m 32.3,99.5 a 67.7,93.7 0 1 1 0,1.3 z",
vesicaPiscis:
"M 100,0 C 63.9,20.4 41,58.5 41,100 c 0,41.5 22.9,79.6 59,100 36.1,-20.4 59,-58.5 59,-100 C 159,58.5 136.1,20.4 100,0 Z",
square: "M 25,25 H 175 V 175 H 25 Z",
diamond: "M 25,100 100,200 175,100 100,0 Z",
no: "m0,0 h200 v200 h-200 z",
flag: "M 10,40 h180 v120 h-180 Z",
pennon: "M 10,40 l190,60 -190,60 Z",
guidon: "M 10,40 h190 l-65,60 65,60 h-190 Z",
banner: "m 25,25 v 170 l 25,-40 25,40 25,-40 25,40 25,-40 25,40 V 25 Z",
dovetail: "m 25,25 v 175 l 75,-40 75,40 V 25 Z",
gonfalon: "m 25,25 v 125 l 75,50 75,-50 V 25 Z",
pennant: "M 25,15 100,200 175,15 Z",
fantasy1:
"M 100,5 C 85,30 40,35 15,40 c 40,35 20,90 40,115 15,25 40,30 45,45 5,-15 30,-20 45,-45 20,-25 0,-80 40,-115 C 160,35 115,30 100,5 Z",
fantasy2:
"m 152,21 c 0,0 -27,14 -52,-4 C 75,35 48,21 48,21 50,45 30,55 30,75 60,75 60,115 32,120 c 3,40 53,50 68,80 15,-30 65,-40 68,-80 -28,-5 -28,-45 2,-45 C 170,55 150,45 152,21 Z",
fantasy3:
"M 167,67 C 165,0 35,0 33,67 c 32,-7 27,53 -3,43 -5,45 60,65 70,90 10,-25 75,-47.51058 70,-90 -30,10 -35,-50 -3,-43 z",
fantasy4:
"M100 9C55 48 27 27 13 39c23 50 3 119 49 150 14 9 28 11 38 11s27-4 38-11c55-39 24-108 49-150-14-12-45 7-87-30z",
fantasy5:
"M 100,0 C 75,25 30,25 30,25 c 0,69 20,145 70,175 50,-30 71,-106 70,-175 0,0 -45,0 -70,-25 z",
noldor:
"m 55,75 h 2 c 3,-25 38,-10 3,20 15,50 30,75 40,105 10,-30 25,-55 40,-105 -35,-30 0,-45 3,-20 h 2 C 150,30 110,20 100,0 90,20 50,30 55,75 Z",
gondor:
"m 100,200 c 15,-15 38,-35 45,-60 h 5 V 30 h -5 C 133,10 67,10 55,30 h -5 v 110 h 5 c 7,25 30,45 45,60 z",
easterling: "M 160,185 C 120,170 80,170 40,185 V 15 c 40,15 80,15 120,0 z",
erebor:
"M25 135 V60 l22-13 16-37 h75 l15 37 22 13 v75l-22 18-16 37 H63l-16-37z",
ironHills: "m 30,25 60,-10 10,10 10,-10 60,10 -5,125 -65,50 -65,-50 z",
urukHai:
"M 30,60 C 40,60 60,50 60,20 l -5,-3 45,-17 75,40 -5,5 -35,155 -5,-35 H 70 v 35 z",
moriaOrc:
"M45 35c5 3 7 10 13 9h19c4-2 7-4 9-9 6 1 9 9 16 11 7-2 14 0 21 0 6-3 6-10 10-15 2-5 1-10-2-15-2-4-5-14-4-16 3 6 7 11 12 14 7 3 3 12 7 16 3 6 4 12 9 18 2 4 6 8 5 14 0 6-1 12 3 18-3 6-2 13-1 20 1 6-2 12-1 18 0 6-3 13 0 18 8 4 0 8-5 7-4 3-9 3-13 9-5 5-5 13-8 19 0 6 0 15-7 16-1 6-7 6-10 12-1-6 0-6-2-9l2-19c2-4 5-12-3-12-4-5-11-5-15 1l-13-18c-3-4-2 9-3 12 2 2-4-6-7-5-8-2-8 7-11 11-2 4-5 10-8 9 3-10 3-16 1-23-1-4 2-9-4-11 0-6 1-13-2-19-4-2-9-6-13-7V91c4-7-5-13 0-19-3-7 2-11 2-18-1-6 1-12 3-17v-1z",
};

View file

@ -0,0 +1,126 @@
export const patterns = {
semy: (p: string, c1: string, c2: string, size: number, chargeId: string) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="#000"><rect width="200" height="200" fill="${c1}" stroke="none"/><g fill="${c2}"><use transform="translate(-100 -50)" href="#${chargeId}"/><use transform="translate(100 -50)" href="#${chargeId}"/><use transform="translate(0 50)" href="#${chargeId}"/></g></pattern>`,
vair: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect width="25" height="25" fill="${c1}" stroke="none"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}"/><rect x="0" y="25" width="25" height="25" fill="${c2}" stroke="none"/><path d="m25,25 l-6.25,6.25 v12.5 l-6.25,6.25 l-6.25,-6.25 v-12.5 l-6.25,-6.25 z" fill="${c1}"/><path d="M0 50 h25" fill="none"/></pattern>`,
counterVair: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect width="25" height="50" fill="${c2}" stroke="none"/><path d="m 12.5,0 6.25,6.25 v 12.5 L 25,25 18.75,31.25 v 12.5 L 12.5,50 6.25,43.75 V 31.25 L 0,25 6.25,18.75 V 6.25 Z" fill="${c1}"/></pattern>`,
vairInPale: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 25 25"><rect width="25" height="25" fill="${c1}"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}" stroke="#000" stroke-width=".2"/></pattern>`,
vairEnPointe: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 25 50"><rect width="25" height="25" fill="${c2}"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c1}"/><rect x="0" y="25" width="25" height="25" fill="${c1}" stroke-width="1" stroke="${c1}"/><path d="m12.5,25 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}"/></pattern>`,
vairAncien: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><path fill="${c2}" stroke="none" d="m 0,90 c 10,0 25,-5 25,-40 0,-25 10,-40 25,-40 15,0 25,15 25,40 0,35 15,40 25,40 v 10 H 0 Z"/><path fill="none" stroke="#000" d="M 0,90 c 10,0 25,-5 25,-40 0,-35 15,-40 25,-40 10,0 25,5 25,40 0,35 15,40 25,40 M0,100 h100"/></pattern>`,
potent: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="#000"><rect width="200" height="100" fill="${c1}" stroke="none"/><rect y="100" width="200" height="100" fill="${c2}" stroke="none"/><path d="m25 50h50v-50h50v50h50v50h-150z" fill="${c2}"/><path d="m25 100v50h50v50h50v-50h50v-50z" fill="${c1}"/><path d="m0 0h200 M0 100h200" fill="none"/></pattern>`,
counterPotent: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="none"><rect width="200" height="200" fill="${c1}"/><path d="m25 50h50v-50h50v50h50v100h-50v50h-50v-50h-50v-50z" fill="${c2}"/><path d="m0 0h200 M0 100h200 M0 200h200"/></pattern>`,
potentInPale: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.0625
}" viewBox="0 0 200 100" stroke-width="1"><rect width="200" height="100" fill="${c1}" stroke="none"/><path d="m25 50h50v-50h50v50h50v50h-150z" fill="${c2}" stroke="#000"/><path d="m0 0h200 M0 100h200" fill="none" stroke="#000"/></pattern>`,
potentEnPointe: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="none"><rect width="200" height="200" fill="${c1}"/><path d="m0 0h25v50h50v50h50v-50h50v-50h25v100h-25v50h-50v50h-50v-50h-50v-50h-25v-100" fill="${c2}"/></pattern>`,
ermine: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" fill="${c2}"><rect width="200" height="200" fill="${c1}"/><g stroke="none" fill="${c2}"><g transform="translate(-100 -50)"><path d="m100 81.1c-4.25 17.6-12.7 29.8-21.2 38.9 3.65-0.607 7.9-3.04 11.5-5.47-2.42 4.86-4.86 8.51-7.3 12.7 1.82-0.607 6.07-4.86 12.7-10.9 1.21 8.51 2.42 17.6 4.25 23.6 1.82-5.47 3.04-15.2 4.25-23.6 3.65 3.65 7.3 7.9 12.7 10.9l-7.9-13.3c3.65 1.82 7.9 4.86 11.5 6.07-9.11-9.11-17-21.2-20.6-38.9z"/><path d="m82.4 81.7c-0.607-0.607-6.07 2.42-9.72-4.25 7.9 6.68 15.2-7.3 21.8 1.82 1.82 4.25-6.68 10.9-12.1 2.42z"/><path d="m117 81.7c0.607-1.21 6.07 2.42 9.11-4.86-7.3 7.3-15.2-7.3-21.2 2.42-1.82 4.25 6.68 10.9 12.1 2.42z"/><path d="m101 66.5c-1.02-0.607 3.58-4.25-3.07-8.51 5.63 7.9-10.2 10.9-1.54 17.6 3.58 2.42 12.2-2.42 4.6-9.11z"/></g><g transform="translate(100 -50)"><path d="m100 81.1c-4.25 17.6-12.7 29.8-21.2 38.9 3.65-0.607 7.9-3.04 11.5-5.47-2.42 4.86-4.86 8.51-7.3 12.7 1.82-0.607 6.07-4.86 12.7-10.9 1.21 8.51 2.42 17.6 4.25 23.6 1.82-5.47 3.04-15.2 4.25-23.6 3.65 3.65 7.3 7.9 12.7 10.9l-7.9-13.3c3.65 1.82 7.9 4.86 11.5 6.07-9.11-9.11-17-21.2-20.6-38.9z"/><path d="m82.4 81.7c-0.607-0.607-6.07 2.42-9.72-4.25 7.9 6.68 15.2-7.3 21.8 1.82 1.82 4.25-6.68 10.9-12.1 2.42z"/><path d="m117 81.7c0.607-1.21 6.07 2.42 9.11-4.86-7.3 7.3-15.2-7.3-21.2 2.42-1.82 4.25 6.68 10.9 12.1 2.42z"/><path d="m101 66.5c-1.02-0.607 3.58-4.25-3.07-8.51 5.63 7.9-10.2 10.9-1.54 17.6 3.58 2.42 12.2-2.42 4.6-9.11z"/></g><g transform="translate(0 50)"><path d="m100 81.1c-4.25 17.6-12.7 29.8-21.2 38.9 3.65-0.607 7.9-3.04 11.5-5.47-2.42 4.86-4.86 8.51-7.3 12.7 1.82-0.607 6.07-4.86 12.7-10.9 1.21 8.51 2.42 17.6 4.25 23.6 1.82-5.47 3.04-15.2 4.25-23.6 3.65 3.65 7.3 7.9 12.7 10.9l-7.9-13.3c3.65 1.82 7.9 4.86 11.5 6.07-9.11-9.11-17-21.2-20.6-38.9z"/><path d="m82.4 81.7c-0.607-0.607-6.07 2.42-9.72-4.25 7.9 6.68 15.2-7.3 21.8 1.82 1.82 4.25-6.68 10.9-12.1 2.42z"/><path d="m117 81.7c0.607-1.21 6.07 2.42 9.11-4.86-7.3 7.3-15.2-7.3-21.2 2.42-1.82 4.25 6.68 10.9 12.1 2.42z"/><path d="m101 66.5c-1.02-0.607 3.58-4.25-3.07-8.51 5.63 7.9-10.2 10.9-1.54 17.6 3.58 2.42 12.2-2.42 4.6-9.11z"/></g></g></pattern>`,
chequy: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.25}" height="${
size * 0.25
}" viewBox="0 0 50 50" fill="${c2}"><rect width="50" height="50"/><rect width="25" height="25" fill="${c1}"/><rect x="25" y="25" width="25" height="25" fill="${c1}"/></pattern>`,
lozengy: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 50 50"><rect width="50" height="50" fill="${c1}"/><polygon points="25,0 50,25 25,50 0,25" fill="${c2}"/></pattern>`,
fusily: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 50 100"><rect width="50" height="100" fill="${c2}"/><polygon points="25,0 50,50 25,100 0,50" fill="${c1}"/></pattern>`,
pally: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.5}" height="${
size * 0.125
}" viewBox="0 0 100 25"><rect width="100" height="25" fill="${c2}"/><rect x="25" y="0" width="25" height="25" fill="${c1}"/><rect x="75" y="0" width="25" height="25" fill="${c1}"/></pattern>`,
barry: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.5
}" viewBox="0 0 25 100"><rect width="25" height="100" fill="${c2}"/><rect x="0" y="25" width="25" height="25" fill="${c1}"/><rect x="0" y="75" width="25" height="25" fill="${c1}"/></pattern>`,
gemelles: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 50 50"><rect width="50" height="50" fill="${c1}"/><rect y="5" width="50" height="10" fill="${c2}"/><rect y="40" width="50" height="10" fill="${c2}"/></pattern>`,
bendy: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.5}" height="${
size * 0.5
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><polygon points="0,25 75,100 25,100 0,75" fill="${c2}"/><polygon points="25,0 75,0 100,25 100,75" fill="${c2}"/></pattern>`,
bendySinister: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.5}" height="${
size * 0.5
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c2}"/><polygon points="0,25 25,0 75,0 0,75" fill="${c1}"/><polygon points="25,100 100,25 100,75 75,100" fill="${c1}"/></pattern>`,
palyBendy: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.6258}" height="${
size * 0.3576
}" viewBox="0 0 175 100"><rect y="0" x="0" width="175" height="100" fill="${c2}"/><g fill="${c1}"><path d="m0 20 35 30v50l-35-30z"/><path d="m35 0 35 30v50l-35-30z"/><path d="m70 0h23l12 10v50l-35-30z"/><path d="m70 80 23 20h-23z"/><path d="m105 60 35 30v10h-35z"/><path d="m105 0h35v40l-35-30z"/><path d="m 140,40 35,30 v 30 h -23 l -12,-10z"/><path d="M 175,0 V 20 L 152,0 Z"/></g></pattern>`,
barryBendy: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.3572}" height="${
size * 0.6251
}" viewBox="0 0 100 175"><rect width="100" height="175" fill="${c2}"/><g fill="${c1}"><path d="m20 0 30 35h50l-30-35z"/><path d="m0 35 30 35h50l-30-35z"/><path d="m0 70v23l10 12h50l-30-35z"/><path d="m80 70 20 23v-23z"/><path d="m60 105 30 35h10v-35z"/><path d="m0 105v35h40l-30-35z"/><path d="m 40,140 30,35 h 30 v -23 l -10,-12 z"/><path d="m0 175h20l-20-23z"/></g></pattern>`,
pappellony: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><circle cx="0" cy="51" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/><circle cx="100" cy="51" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/><circle cx="50" cy="1" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/></pattern>`,
pappellony2: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100" stroke="#000" stroke-width="2"><rect width="100" height="100" fill="${c1}" stroke="none"/><circle cy="50" r="49" fill="${c2}"/><circle cx="100" cy="50" r="49" fill="${c2}"/><circle cx="50" cy="0" r="49" fill="${c1}"/></pattern>`,
scaly: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100" stroke="#000"><rect width="100" height="100" fill="${c1}" stroke="none"/><path d="M 0,84 C -40,84 -50,49 -50,49 -50,79 -27,99 0,99 27,99 50,79 50,49 50,49 40,84 0,84 Z" fill="${c2}"/><path d="M 100,84 C 60,84 50,49 50,49 c 0,30 23,50 50,50 27,0 50,-20 50,-50 0,0 -10,35 -50,35 z" fill="${c2}"/><path d="M 50,35 C 10,35 0,0 0,0 0,30 23,50 50,50 77,50 100,30 100,0 100,0 90,35 50,35 Z" fill="${c2}"/></pattern>`,
plumetty: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 50 100" stroke-width=".8"><rect width="50" height="100" fill="${c2}" stroke="none"/><path fill="${c1}" stroke="none" d="M 25,100 C 44,88 49.5,74 50,50 33.5,40 25,25 25,4e-7 25,25 16.5,40 0,50 0.5,74 6,88 25,100 Z"/><path fill="none" stroke="${c2}" d="m17 40c5.363 2.692 10.7 2.641 16 0m-19 7c7.448 4.105 14.78 3.894 22 0m-27 7c6-2 10.75 3.003 16 3 5.412-0.0031 10-5 16-3m-35 9c4-7 12 3 19 2 7 1 15-9 19-2m-35 6c6-2 11 3 16 3s10-5 16-3m-30 7c8 0 8 3 14 3s7-3 14-3m-25 8c7.385 4.048 14.72 3.951 22 0m-19 8c5.455 2.766 10.78 2.566 16 0m-8 6v-78"/><g fill="none" stroke="${c1}"><path d="m42 90c2.678 1.344 5.337 2.004 8 2m-11 5c3.686 2.032 7.344 3.006 10.97 3m0.0261-1.2e-4v-30"/><path d="m0 92c2.689 0.0045 5.328-0.6687 8-2m-8 10c3.709-0.0033 7.348-1.031 11-3m-11 3v-30"/><path d="m0 7c5.412-0.0031 10-5 16-3m-16 11c7 1 15-9 19-2m-19 9c5 0 10-5 16-3m-16 10c6 0 7-3 14-3m-14.02 11c3.685-0.002185 7.357-1.014 11.02-3m-11 10c2.694-0.01117 5.358-0.7036 7.996-2m-8 6v-48"/><path d="m34 4c6-2 10.75 3.003 16 3m-19 6c4-7 12 3 19 2m-16 4c6-2 11 3 16 3m-14 4c8 0 8 3 14 3m-11 5c3.641 1.996 7.383 2.985 11 3m-8 5c2.762 1.401 5.303 2.154 8.002 2.112m-0.00154 3.888v-48"/></g></pattern>`,
masoned: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100" fill="none"><rect width="100" height="100" fill="${c1}"/><rect width="100" height="50" stroke="${c2}" stroke-width="4"/><line x1="50" y1="50" x2="50" y2="100" stroke="${c2}" stroke-width="5"/></pattern>`,
fretty: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.2}" height="${
size * 0.2
}" viewBox="0 0 140 140" stroke="#000" stroke-width="2"><rect width="140" height="140" fill="${c1}" stroke="none"/><path d="m-15 5 150 150 20-20-150-150z" fill="${c2}"/><path d="m10 150 140-140-20-20-140 140z" fill="${c2}" stroke="none"/><path d="m0 120 20 20 120-120-20-20z" fill="none"/></pattern>`,
grillage: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.25}" height="${
size * 0.25
}" viewBox="0 0 200 200" stroke="#000" stroke-width="2"><rect width="200" height="200" fill="${c1}" stroke="none"/><path d="m205 65v-30h-210v30z" fill="${c2}"/><path d="m65-5h-30v210h30z" fill="${c2}"/><path d="m205 165v-30h-210v30z" fill="${c2}"/><path d="m165,65h-30v140h30z" fill="${c2}"/><path d="m 165,-5h-30v40h30z" fill="${c2}"/></pattern>`,
chainy: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.167}" height="${
size * 0.167
}" viewBox="0 0 200 200" stroke="#000" stroke-width="2"><rect x="-6.691e-6" width="200" height="200" fill="${c1}" stroke="none"/><path d="m155-5-20-20-160 160 20 20z" fill="${c2}"/><path d="m45 205 160-160 20 20-160 160z" fill="${c2}"/><path d="m45-5 20-20 160 160-20 20-160-160" fill="${c2}"/><path d="m-5 45-20 20 160 160 20-20-160-160" fill="${c2}"/></pattern>`,
maily: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.167}" height="${
size * 0.167
}" viewBox="0 0 200 200" stroke="#000" stroke-width="1.2"><path fill="${c1}" stroke="none" d="M0 0h200v200H0z"/><g fill="${c2}"><path d="m80-2c-5.27e-4 2.403-0.1094 6.806-0.3262 9.199 5.014-1.109 10.1-1.768 15.19-2.059 0.09325-1.712 0.1401-5.426 0.1406-7.141z"/><path d="m100 5a95 95 0 0 0-95 95 95 95 0 0 0 95 95 95 95 0 0 0 95-95 95 95 0 0 0-95-95zm0 15a80 80 0 0 1 80 80 80 80 0 0 1-80 80 80 80 0 0 1-80-80 80 80 0 0 1 80-80z"/><path d="m92.8 20.33c-5.562 0.4859-11.04 1.603-16.34 3.217-7.793 25.31-27.61 45.12-52.91 52.91-5.321 1.638-10.8 2.716-16.34 3.217-2.394 0.2168-6.796 0.3256-9.199 0.3262v15c1.714-4.79e-4 5.429-0.04737 7.141-0.1406 5.109-0.2761 10.19-0.9646 15.19-2.059 36.24-7.937 64.54-36.24 72.47-72.47z"/><path d="m202 80c-2.403-5.31e-4 -6.806-0.1094-9.199-0.3262 1.109 5.014 1.768 10.1 2.059 15.19 1.712 0.09326 5.426 0.1401 7.141 0.1406z"/><path d="m179.7 92.8c-0.4859-5.562-1.603-11.04-3.217-16.34-25.31-7.793-45.12-27.61-52.91-52.91-1.638-5.321-2.716-10.8-3.217-16.34-0.2168-2.394-0.3256-6.796-0.3262-9.199h-15c4.8e-4 1.714 0.0474 5.429 0.1406 7.141 0.2761 5.109 0.9646 10.19 2.059 15.19 7.937 36.24 36.24 64.54 72.47 72.47z"/><path d="m120 202c5.3e-4 -2.403 0.1094-6.806 0.3262-9.199-5.014 1.109-10.1 1.768-15.19 2.059-0.0933 1.712-0.1402 5.426-0.1406 7.141z"/><path d="m107.2 179.7c5.562-0.4859 11.04-1.603 16.34-3.217 7.793-25.31 27.61-45.12 52.91-52.91 5.321-1.638 10.8-2.716 16.34-3.217 2.394-0.2168 6.796-0.3256 9.199-0.3262v-15c-1.714 4.7e-4 -5.429 0.0474-7.141 0.1406-5.109 0.2761-10.19 0.9646-15.19 2.059-36.24 7.937-64.54 36.24-72.47 72.47z"/><path d="m -2,120 c 2.403,5.4e-4 6.806,0.1094 9.199,0.3262 -1.109,-5.014 -1.768,-10.1 -2.059,-15.19 -1.712,-0.0933 -5.426,-0.1402 -7.141,-0.1406 z"/><path d="m 20.33,107.2 c 0.4859,5.562 1.603,11.04 3.217,16.34 25.31,7.793 45.12,27.61 52.91,52.91 1.638,5.321 2.716,10.8 3.217,16.34 0.2168,2.394 0.3256,6.796 0.3262,9.199 L 95,202 c -4.8e-4,-1.714 -0.0472,-5.44 -0.1404,-7.152 -0.2761,-5.109 -0.9646,-10.19 -2.059,-15.19 -7.937,-36.24 -36.24,-64.54 -72.47,-72.47 z"/></g></pattern>`,
honeycombed: (p: string, c1: string, c2: string, size: number) =>
`<pattern id="${p}" width="${size * 0.143}" height="${
size * 0.24514
}" viewBox="0 0 70 120"><rect width="70" height="120" fill="${c1}"/><path d="M 70,0 V 20 L 35,40 m 35,80 V 100 L 35,80 M 0,120 V 100 L 35,80 V 40 L 0,20 V 0" stroke="${c2}" fill="none" stroke-width="3"/></pattern>`,
};

View file

@ -0,0 +1,51 @@
export const positions = {
conventional: {
e: 20,
abcdefgzi: 3,
beh: 3,
behdf: 2,
acegi: 1,
kn: 3,
bhdf: 1,
jeo: 1,
abc: 3,
jln: 6,
jlh: 3,
kmo: 2,
jleh: 1,
def: 3,
abcpqh: 4,
ABCDEFGHIJKL: 1,
},
complex: {
e: 40,
beh: 1,
kn: 1,
jeo: 1,
abc: 2,
jln: 7,
jlh: 2,
def: 1,
abcpqh: 1,
},
divisions: {
perPale: { e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 1 },
perFess: {
e: 12,
kn: 4,
jkl: 2,
gizgiz: 1,
jlh: 3,
kmo: 1,
ABCDEFGHIJKL: 1,
},
perBend: { e: 5, lm: 5, bcfdgh: 1 },
perBendSinister: { e: 1, jo: 1 },
perCross: { e: 4, jlmo: 1, j: 1, jo: 2, jl: 1 },
perChevron: { e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1 },
perChevronReversed: { e: 1, mok: 2, dfh: 2, dfbh: 1, bdefh: 1 },
perSaltire: { bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1 },
perPile: { ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1 },
},
inescutcheon: { e: 4, jln: 1 },
};

View file

@ -0,0 +1,363 @@
import { shieldBox } from "./box";
import { colors } from "./colors";
import { lines } from "./lines";
import { shieldPaths } from "./paths";
import { patterns } from "./patterns";
import { shieldPositions } from "./shieldPositions";
import { shieldSize } from "./size";
import { templates } from "./templates";
declare global {
var COArenderer: EmblemRenderModule;
}
interface Division {
division: string;
line?: string;
t: string;
}
interface Ordinary {
ordinary: string;
line?: string;
t: string;
divided?: "field" | "division" | "counter";
above?: boolean;
}
interface Charge {
stroke: string;
charge: string;
t: string;
size?: number;
sinister?: boolean;
reversed?: boolean;
line?: string;
divided?: "field" | "division" | "counter";
p: number[]; // position on shield from 1 to 9
}
interface Emblem {
shield: string;
t1: string;
division?: Division;
ordinaries?: Ordinary[];
charges?: Charge[];
custom?: boolean; // if true, coa will not be rendered
}
class EmblemRenderModule {
get shieldPaths() {
return shieldPaths;
}
private getTemplate(id: string, line?: string) {
const linedId = `${id}Lined` as keyof typeof templates;
if (!line || line === "straight" || !templates[linedId])
return templates[id as keyof typeof templates]; // return regular template if no line or line is straight or lined template does not exist
const linePath = lines[line as keyof typeof lines];
return (templates[linedId] as (line: string) => string)(linePath);
}
// get charge is string starts with "semy"
private semy(input: string | undefined) {
if (!input) return false;
const isSemy = /^semy/.test(input);
if (!isSemy) return false;
const match = input.match(/semy_of_(.*?)-/);
return match ? match[1] : false;
}
private async fetchCharge(charge: string, id: string) {
const fetched = fetch(`./charges/${charge}.svg`)
.then((res) => {
if (res.ok) return res.text();
else throw new Error("Cannot fetch charge");
})
.then((text) => {
const html = document.createElement("html");
html.innerHTML = text;
const g: SVGAElement = html.querySelector("g") as SVGAElement;
g.setAttribute("id", `${charge}_${id}`);
return g.outerHTML;
})
.catch((err) => {
ERROR && console.error(err);
});
return fetched;
}
private async getCharges(coa: Emblem, id: string, shieldPath: string) {
const charges = coa.charges
? coa.charges.map((charge) => charge.charge)
: []; // add charges
if (this.semy(coa.t1)) charges.push(this.semy(coa.t1) as string); // add field semy charge
if (this.semy(coa.division?.t))
charges.push(this.semy(coa.division?.t) as string); // add division semy charge
const uniqueCharges = [...new Set(charges)];
const fetchedCharges = await Promise.all(
uniqueCharges.map(async (charge) => {
if (charge === "inescutcheon")
return `<g id="inescutcheon_${id}"><path transform="translate(66 66) scale(.34)" d="${shieldPath}"/></g>`;
const fetched = await this.fetchCharge(charge, id);
return fetched;
}),
);
return fetchedCharges.join("");
}
// get color or link to pattern
private clr(tincture: string) {
return tincture in colors
? colors[tincture as keyof typeof colors]
: `url(#${tincture})`;
}
private getSizeMod(size: string) {
if (size === "small") return 0.8;
if (size === "smaller") return 0.5;
if (size === "smallest") return 0.25;
if (size === "big") return 1.6;
return 1;
}
private getPatterns(coa: Emblem, id: string) {
const isPattern = (string: string) => string.includes("-");
const patternsToAdd = [];
if (coa.t1.includes("-")) patternsToAdd.push(coa.t1); // add field pattern
if (coa.division && isPattern(coa.division.t))
patternsToAdd.push(coa.division.t); // add division pattern
if (coa.ordinaries)
coa.ordinaries
.filter((ordinary) => isPattern(ordinary.t))
.forEach((ordinary) => {
patternsToAdd.push(ordinary.t); // add ordinaries pattern
});
if (coa.charges)
coa.charges
.filter((charge) => isPattern(charge.t))
.forEach((charge) => {
patternsToAdd.push(charge.t); // add charges pattern
});
if (!patternsToAdd.length) return "";
return [...new Set(patternsToAdd)]
.map((patternString) => {
const [pattern, t1, t2, size] = patternString.split("-");
const charge = this.semy(patternString);
if (charge)
return patterns.semy(
patternString,
this.clr(t1),
this.clr(t2),
this.getSizeMod(size),
`${charge}_${id}`,
);
return patterns[pattern as keyof typeof patterns](
patternString,
this.clr(t1),
this.clr(t2),
this.getSizeMod(size),
charge as string,
);
})
.join("");
}
private async draw(id: string, coa: Emblem) {
const { shield = "heater", division, ordinaries = [], charges = [] } = coa;
const ordinariesRegular = ordinaries.filter((o) => !o.above);
const ordinariesAboveCharges = ordinaries.filter((o) => o.above);
const shieldPath =
shield in shieldPaths
? shieldPaths[shield as keyof typeof shieldPaths]
: shieldPaths.heater;
const tDiv = division
? division.t.includes("-")
? division.t.split("-")[1]
: division.t
: null;
const positions =
shield in shieldPositions
? shieldPositions[shield as keyof typeof shieldPositions]
: shieldPositions.heater;
const sizeModifier =
shield in shieldSize ? shieldSize[shield as keyof typeof shieldSize] : 1;
const viewBox =
shield in shieldBox
? shieldBox[shield as keyof typeof shieldBox]
: "0 0 200 200";
const shieldClip = `<clipPath id="${shield}_${id}"><path d="${shieldPath}"/></clipPath>`;
const divisionClip = division
? `<clipPath id="divisionClip_${id}">${this.getTemplate(division.division, division.line)}</clipPath>`
: "";
const loadedCharges = await this.getCharges(coa, id, shieldPath);
const loadedPatterns = this.getPatterns(coa, id);
const blacklight = `<radialGradient id="backlight_${id}" cx="100%" cy="100%" r="150%"><stop stop-color="#fff" stop-opacity=".3" offset="0"/><stop stop-color="#fff" stop-opacity=".15" offset=".25"/><stop stop-color="#000" stop-opacity="0" offset="1"/></radialGradient>`;
const field = `<rect x="0" y="0" width="200" height="200" fill="${this.clr(coa.t1)}"/>`;
const style = `<style>
g.secondary,path.secondary {fill: var(--secondary);}
g.tertiary,path.tertiary {fill: var(--tertiary);}
</style>`;
const templateCharge = (
charge: Charge,
tincture: string,
secondaryTincture?: string,
tertiaryTincture?: string,
) => {
const primary = this.clr(tincture);
const secondary = this.clr(secondaryTincture || tincture);
const tertiary = this.clr(tertiaryTincture || tincture);
const stroke = charge.stroke || "#000";
const chargePositions = [...new Set(charge.p)].filter(
(position) => positions[position as unknown as keyof typeof positions],
); // filter out invalid positions
let svg = `<g fill="${primary}" style="--secondary: ${secondary}; --tertiary: ${tertiary}" stroke="${stroke}">`;
for (const p of chargePositions) {
const transform = getElTransform(charge, p);
svg += `<use href="#${charge.charge}_${id}" transform="${transform}"></use>`;
}
return `${svg}</g>`;
function getElTransform(c: Charge, p: string | number) {
const s = (c.size || 1) * sizeModifier;
const sx = c.sinister ? -s : s;
const sy = c.reversed ? -s : s;
let [x, y] = positions[p as keyof typeof positions];
x = x - 100 * (sx - 1);
y = y - 100 * (sy - 1);
const scale = c.sinister || c.reversed ? `${sx} ${sy}` : s;
return `translate(${x} ${y}) scale(${scale})`;
}
};
const templateOrdinary = (ordinary: Ordinary, tincture: string) => {
const fill = this.clr(tincture);
let svg = `<g fill="${fill}" stroke="none">`;
if (ordinary.ordinary === "bordure")
svg += `<path d="${shieldPath}" fill="none" stroke="${fill}" stroke-width="16.7%"/>`;
else if (ordinary.ordinary === "orle")
svg += `<path d="${shieldPath}" fill="none" stroke="${fill}" stroke-width="5%" transform="scale(.85)" transform-origin="center"/>`;
else svg += this.getTemplate(ordinary.ordinary, ordinary.line);
return `${svg}</g>`;
};
const templateDivision = () => {
let svg = "";
// In field part
for (const ordinary of ordinariesRegular) {
if (ordinary.divided === "field")
svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter")
svg += templateOrdinary(ordinary, tDiv!);
}
for (const charge of charges) {
if (charge.divided === "field") svg += templateCharge(charge, charge.t);
else if (charge.divided === "counter")
svg += templateCharge(charge, tDiv!);
}
for (const ordinary of ordinariesAboveCharges) {
if (ordinary.divided === "field")
svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter")
svg += templateOrdinary(ordinary, tDiv!);
}
// In division part
svg += `<g clip-path="url(#divisionClip_${id})"><rect x="0" y="0" width="200" height="200" fill="${this.clr(
division!.t,
)}"/>`;
for (const ordinary of ordinariesRegular) {
if (ordinary.divided === "division")
svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter")
svg += templateOrdinary(ordinary, coa.t1);
}
for (const charge of charges) {
if (charge.divided === "division")
svg += templateCharge(charge, charge.t);
else if (charge.divided === "counter")
svg += templateCharge(charge, coa.t1);
}
for (const ordinary of ordinariesAboveCharges) {
if (ordinary.divided === "division")
svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter")
svg += templateOrdinary(ordinary, coa.t1);
}
svg += `</g>`;
return svg;
};
const templateAboveAll = () => {
let svg = "";
ordinariesRegular
.filter((o) => !o.divided)
.forEach((ordinary) => {
svg += templateOrdinary(ordinary, ordinary.t);
});
charges
.filter((o) => !o.divided || !division)
.forEach((charge) => {
svg += templateCharge(charge, charge.t);
});
ordinariesAboveCharges
.filter((o) => !o.divided)
.forEach((ordinary) => {
svg += templateOrdinary(ordinary, ordinary.t);
});
return svg;
};
const divisionGroup = division ? templateDivision() : "";
const overlay = `<path d="${shieldPath}" fill="url(#backlight_${id})" stroke="#333"/>`;
const svg = `<svg id="${id}" width="200" height="200" viewBox="${viewBox}">
<defs>${shieldClip}${divisionClip}${loadedCharges}${loadedPatterns}${blacklight}${style}</defs>
<g clip-path="url(#${shield}_${id})">${field}${divisionGroup}${templateAboveAll()}</g>
${overlay}</svg>`;
// insert coa svg to defs
document.getElementById("coas")!.insertAdjacentHTML("beforeend", svg);
return true;
}
// render coa if does not exist
async trigger(id: string, coa: Emblem) {
if (!coa) return console.warn(`Emblem ${id} is undefined`);
if (coa.custom) return console.warn("Cannot render custom emblem", coa);
if (!document.getElementById(id)) return this.draw(id, coa);
}
async add(type: string, i: number, coa: Emblem, x: number, y: number) {
const id = `${type}COA${i}`;
const g: HTMLElement = document.getElementById(
`${type}Emblems`,
) as HTMLElement;
if (emblems.selectAll("use").size()) {
const size = parseFloat(g.getAttribute("font-size") || "50");
const use = `<use data-i="${i}" x="${x - size / 2}" y="${y - size / 2}" width="1em" height="1em" href="#${id}"/>`;
g.insertAdjacentHTML("beforeend", use);
}
if (layerIsOn("toggleEmblems")) this.trigger(id, coa);
}
}
window.COArenderer = new EmblemRenderModule();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
export const shields: {
types: Record<string, number>;
[key: string]: Record<string, number>;
} = {
types: {
basic: 10,
regional: 2,
historical: 1,
specific: 1,
banner: 1,
simple: 2,
fantasy: 1,
middleEarth: 0,
},
basic: { heater: 12, spanish: 6, french: 1 },
regional: { horsehead: 1, horsehead2: 1, polish: 1, hessen: 1, swiss: 1 },
historical: {
boeotian: 1,
roman: 2,
kite: 1,
oldFrench: 5,
renaissance: 2,
baroque: 2,
},
specific: { targe: 1, targe2: 0, pavise: 5, wedged: 10 },
banner: {
flag: 1,
pennon: 0,
guidon: 0,
banner: 0,
dovetail: 1,
gonfalon: 5,
pennant: 0,
},
simple: { round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0 },
fantasy: { fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3 },
middleEarth: {
noldor: 1,
gondor: 1,
easterling: 1,
erebor: 1,
ironHills: 1,
urukHai: 1,
moriaOrc: 1,
},
};

View file

@ -0,0 +1,32 @@
export const shieldSize = {
horsehead: 0.9,
horsehead2: 0.9,
polish: 0.85,
swiss: 0.95,
boeotian: 0.75,
roman: 0.95,
kite: 0.65,
targe2: 0.9,
pavise: 0.9,
wedged: 0.95,
flag: 0.7,
pennon: 0.5,
guidon: 0.65,
banner: 0.8,
dovetail: 0.8,
pennant: 0.6,
oval: 0.95,
vesicaPiscis: 0.8,
diamond: 0.8,
no: 1.2,
fantasy1: 0.8,
fantasy2: 0.7,
fantasy3: 0.7,
fantasy5: 0.9,
noldor: 0.5,
gondor: 0.75,
easterling: 0.8,
erebor: 0.9,
urukHai: 0.8,
moriaOrc: 0.7,
};

View file

@ -0,0 +1,98 @@
export const templates: Record<string, string | ((line: string) => string)> = {
// straight divisions
perFess: `<rect x="0" y="100" width="200" height="100"/>`,
perPale: `<rect x="100" y="0" width="100" height="200"/>`,
perBend: `<polygon points="0,0 200,200 0,200"/>`,
perBendSinister: `<polygon points="200,0 0,200 200,200"/>`,
perChevron: `<polygon points="0,200 100,100 200,200"/>`,
perChevronReversed: `<polygon points="0,0 100,100 200,0"/>`,
perCross: `<rect x="100" y="0" width="100" height="100"/><rect x="0" y="100" width="100" height="100"/>`,
perPile: `<polygon points="0,0 15,0 100,200 185,0 200,0 200,200 0,200"/>`,
perSaltire: `<polygon points="0,0 0,200 200,0 200,200"/>`,
gyronny: `<polygon points="0,0 200,200 200,100 0,100"/><polygon points="200,0 0,200 100,200 100,0"/>`,
chevronny: `<path d="M0,80 100,-15 200,80 200,120 100,25 0,120z M0,160 100,65 200,160 200,200 100,105 0,200z M0,240 100,145 200,240 0,240z"/>`,
// lined divisions
perFessLined: (line: string) =>
`<path d="${line}"/><rect x="0" y="115" width="200" height="85" shape-rendering="crispedges"/>`,
perPaleLined: (line: string) =>
`<path d="${line}" transform="rotate(-90 100 100)"/><rect x="115" y="0" width="85" height="200" shape-rendering="crispedges"/>`,
perBendLined: (line: string) =>
`<path d="${line}" transform="translate(-10 -10) rotate(45 110 110) scale(1.1)"/><rect x="0" y="115" width="200" height="85" transform="translate(-10 -10) rotate(45 110 110) scale(1.1)" shape-rendering="crispedges"/>`,
perBendSinisterLined: (line: string) =>
`<path d="${line}" transform="translate(-10 -10) rotate(-45 110 110) scale(1.1)"/><rect x="0" y="115" width="200" height="85" transform="translate(-10 -10) rotate(-45 110 110) scale(1.1)" shape-rendering="crispedges"/>`,
perChevronLined: (line: string) =>
`<rect x="15" y="115" width="200" height="200" transform="translate(70 70) rotate(45 100 100)"/><path d="${line}" transform="translate(129 71) rotate(-45 -100 100) scale(-1 1)"/><path d="${line}" transform="translate(71 71) rotate(45 100 100)"/>`,
perChevronReversedLined: (line: string) =>
`<rect x="15" y="115" width="200" height="200" transform="translate(-70 -70) rotate(225.001 100 100)"/><path d="${line}" transform="translate(-70.7 -70.7) rotate(225 100 100) scale(1 1)"/><path d="${line}" transform="translate(270.7 -70.7) rotate(-225 -100 100) scale(-1 1)"/>`,
perCrossLined: (line: string) =>
`<rect x="100" y="0" width="100" height="92.5"/><rect x="0" y="107.5" width="100" height="92.5"/><path d="${line}" transform="translate(0 50) scale(.5001)"/><path d="${line}" transform="translate(200 150) scale(-.5)"/>`,
perPileLined: (line: string) =>
`<path d="${line}" transform="translate(161.66 10) rotate(66.66 -100 100) scale(-1 1)"/><path d="${line}" transform="translate(38.33 10) rotate(-66.66 100 100)"/><polygon points="-2.15,0 84.15,200 115.85,200 202.15,0 200,200 0,200"/>`,
// straight ordinaries
fess: `<rect x="0" y="75" width="200" height="50"/>`,
pale: `<rect x="75" y="0" width="50" height="200"/>`,
bend: `<polygon points="35,0 200,165 200,200 165,200 0,35 0,0"/>`,
bendSinister: `<polygon points="0,165 165,0 200,0 200,35 35,200 0,200"/>`,
chief: `<rect width="200" height="75"/>`,
bar: `<rect x="0" y="87.5" width="200" height="25"/>`,
gemelle: `<rect x="0" y="76" width="200" height="16"/><rect x="0" y="108" width="200" height="16"/>`,
fessCotissed: `<rect x="0" y="67" width="200" height="8"/><rect x="0" y="83" width="200" height="34"/><rect x="0" y="125" width="200" height="8"/>`,
fessDoubleCotissed: `<rect x="0" y="60" width="200" height="7.5"/><rect x="0" y="72.5" width="200" height="7.5"/><rect x="0" y="85" width="200" height="30"/><rect x="0" y="120" width="200" height="7.5"/><rect x="0" y="132.5" width="200" height="7.5"/>`,
bendlet: `<polygon points="22,0 200,178 200,200 178,200 0,22 0,0"/>`,
bendletSinister: `<polygon points="0,178 178,0 200,0 200,22 22,200 0,200"/>`,
terrace: `<rect x="0" y="145" width="200" height="55"/>`,
cross: `<polygon points="85,0 85,85 0,85 0,115 85,115 85,200 115,200 115,115 200,115 200,85 115,85 115,0"/>`,
crossParted: `<path d="M 80 0 L 80 80 L 0 80 L 0 95 L 80 95 L 80 105 L 0 105 L 0 120 L 80 120 L 80 200 L 95 200 L 95 120 L 105 120 L 105 200 L 120 200 L 120 120 L 200 120 L 200 105 L 120 105 L 120 95 L 200 95 L 200 80 L 120 80 L 120 0 L 105 0 L 105 80 L 95 80 L 95 0 L 80 0 z M 95 95 L 105 95 L 105 105 L 95 105 L 95 95 z"/>`,
saltire: `<path d="M 0,21 79,100 0,179 0,200 21,200 100,121 179,200 200,200 200,179 121,100 200,21 200,0 179,0 100,79 21,0 0,0 Z"/>`,
saltireParted: `<path d="M 7 0 L 89 82 L 82 89 L 0 7 L 0 28 L 72 100 L 0 172 L 0 193 L 82 111 L 89 118 L 7 200 L 28 200 L 100 128 L 172 200 L 193 200 L 111 118 L 118 111 L 200 193 L 200 172 L 128 100 L 200 28 L 200 7 L 118 89 L 111 82 L 193 0 L 172 0 L 100 72 L 28 0 L 7 0 z M 100 93 L 107 100 L 100 107 L 93 100 L 100 93 z"/>`,
mount: `<path d="m0,250 a100,100,0,0,1,200,0"/>`,
point: `<path d="M0,200 Q80,180 100,135 Q120,180 200,200"/>`,
flaunches: `<path d="M0,0 q120,100 0,200 M200,0 q-120,100 0,200"/>`,
gore: `<path d="M20,0 Q30,75 100,100 Q80,150 100,200 L0,200 L0,0 Z"/>`,
pall: `<polygon points="0,0 30,0 100,70 170,0 200,0 200,30 122,109 122,200 78,200 78,109 0,30"/>`,
pallReversed: `<polygon points="0,200 0,170 78,91 78,0 122,0 122,91 200,170 200,200 170,200 100,130 30,200"/>`,
chevron: `<polygon points="0,125 100,60 200,125 200,165 100,100 0,165"/>`,
chevronReversed: `<polygon points="0,75 100,140 200,75 200,35 100,100 0,35"/>`,
gyron: `<polygon points="0,0 100,100 0,100"/>`,
quarter: `<rect width="50%" height="50%"/>`,
canton: `<rect width="37.5%" height="37.5%"/>`,
pile: `<polygon points="70,0 100,175 130,0"/>`,
pileInBend: `<polygon points="200,200 200,144 25,25 145,200"/>`,
pileInBendSinister: `<polygon points="0,200 0,144 175,25 55,200"/>`,
piles: `<polygon points="46,0 75,175 103,0"/><polygon points="95,0 125,175 154,0"/>`,
pilesInPoint: `<path d="M15,0 100,200 60,0Z M80,0 100,200 120,0Z M140,0 100,200 185,0Z"/>`,
label: `<path d="m 46,54.8 6.6,-15.6 95.1,0 5.9,15.5 -16.8,0.1 4.5,-11.8 L 104,43 l 4.3,11.9 -16.8,0 4.3,-11.8 -37.2,0 4.5,11.8 -16.9,0 z"/>`,
// lined ordinaries
fessLined: (line: string) =>
`<path d="${line}" transform="translate(0 -25)"/><path d="${line}" transform="translate(0 25) rotate(180 100 100)"/><rect x="0" y="88" width="200" height="24" stroke="none"/>`,
paleLined: (line: string) =>
`<path d="${line}" transform="rotate(-90 100 100) translate(0 -25)"/><path d="${line}" transform="rotate(90 100 100) translate(0 -25)"/><rect x="88" y="0" width="24" height="200" stroke="none"/>`,
bendLined: (line: string) =>
`<path d="${line}" transform="translate(8 -18) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-28 18) rotate(225 110 100) scale(1.1 1)"/><rect x="0" y="88" width="200" height="24" transform="translate(-10 0) rotate(45 110 100) scale(1.1 1)" stroke="none"/>`,
bendSinisterLined: (line: string) =>
`<path d="${line}" transform="translate(-28 -18) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(8 18) rotate(-225 110 100) scale(1.1 1)"/><rect x="0" y="88" width="200" height="24" transform="translate(-10 0) rotate(-45 110 100) scale(1.1 1)" stroke="none"/>`,
chiefLined: (line: string) =>
`<path d="${line}" transform="translate(0,-25) rotate(180.00001 100 100)"/><rect width="200" height="62" stroke="none"/>`,
barLined: (line: string) =>
`<path d="${line}" transform="translate(0,-12.5)"/><path d="${line}" transform="translate(0,12.5) rotate(180.00001 100 100)"/><rect x="0" y="94" width="200" height="12" stroke="none"/>`,
gemelleLined: (line: string) =>
`<path d="${line}" transform="translate(0,-22.5)"/><path d="${line}" transform="translate(0,22.5) rotate(180.00001 100 100)"/>`,
fessCotissedLined: (line: string) =>
`<path d="${line}" transform="translate(0 15) scale(1 .5)"/><path d="${line}" transform="translate(0 85) rotate(180 100 50) scale(1 .5)"/><rect x="0" y="80" width="200" height="40"/>`,
fessDoubleCotissedLined: (line: string) =>
`<rect x="0" y="85" width="200" height="30"/><rect x="0" y="72.5" width="200" height="7.5"/><rect x="0" y="120" width="200" height="7.5"/><path d="${line}" transform="translate(0 10) scale(1 .5)"/><path d="${line}" transform="translate(0 90) rotate(180 100 50) scale(1 .5)"/>`,
bendletLined: (line: string) =>
`<path d="${line}" transform="translate(2 -12) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-22 12) rotate(225 110 100) scale(1.1 1)"/><rect x="0" y="94" width="200" height="12" transform="translate(-10 0) rotate(45 110 100) scale(1.1 1)" stroke="none"/>`,
bendletSinisterLined: (line: string) =>
`<path d="${line}" transform="translate(-22 -12) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(2 12) rotate(-225 110 100) scale(1.1 1)"/><rect x="0" y="94" width="200" height="12" transform="translate(-10 0) rotate(-45 110 100) scale(1.1 1)" stroke="none"/>`,
terraceLined: (line: string) =>
`<path d="${line}" transform="translate(0,50)"/><rect x="0" y="164" width="200" height="36" stroke="none"/>`,
crossLined: (line: string) =>
`<path d="${line}" transform="translate(0,-14.5)"/><path d="${line}" transform="rotate(180 100 100) translate(0,-14.5)"/><path d="${line}" transform="rotate(-90 100 100) translate(0,-14.5)"/><path d="${line}" transform="rotate(-270 100 100) translate(0,-14.5)"/>`,
crossPartedLined: (line: string) =>
`<path d="${line}" transform="translate(0,-20)"/><path d="${line}" transform="rotate(180 100 100) translate(0,-20)"/><path d="${line}" transform="rotate(-90 100 100) translate(0,-20)"/><path d="${line}" transform="rotate(-270 100 100) translate(0,-20)"/>`,
saltireLined: (line: string) =>
`<path d="${line}" transform="translate(0 -10) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-20 10) rotate(225 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-20 -10) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(0 10) rotate(-225 110 100) scale(1.1 1)"/>`,
saltirePartedLined: (line: string) =>
`<path d="${line}" transform="translate(3 -13) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-23 13) rotate(225 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-23 -13) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(3 13) rotate(-225 110 100) scale(1.1 1)"/>`,
};

View file

@ -0,0 +1,43 @@
import { P } from "../../utils";
export const createTinctures = () => ({
field: { metals: 3, colours: 4, stains: +P(0.03), patterns: 1 },
division: { metals: 5, colours: 8, stains: +P(0.03), patterns: 1 },
charge: { metals: 2, colours: 3, stains: +P(0.05), patterns: 0 },
metals: { argent: 3, or: 2 },
colours: { gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2 },
stains: { murrey: 1, sanguine: 1, tenné: 1 },
patterns: {
semy: 8,
ermine: 6,
vair: 4,
counterVair: 1,
vairInPale: 1,
vairEnPointe: 2,
vairAncien: 2,
potent: 2,
counterPotent: 1,
potentInPale: 1,
potentEnPointe: 1,
chequy: 8,
lozengy: 5,
fusily: 2,
pally: 8,
barry: 10,
gemelles: 1,
bendy: 8,
bendySinister: 4,
palyBendy: 2,
barryBendy: 1,
pappellony: 2,
pappellony2: 3,
scaly: 1,
plumetty: 1,
masoned: 6,
fretty: 3,
grillage: 1,
chainy: 1,
maily: 2,
honeycombed: 1,
},
});

View file

@ -0,0 +1,187 @@
// Charges specific to culture or burg type (FMG-only config, not coming from Armoria)
export const typeMapping: Record<string, Record<string, number>> = {
Naval: {
anchor: 3,
drakkar: 1,
lymphad: 2,
caravel: 1,
shipWheel: 1,
armillarySphere: 1,
escallop: 1,
dolphin: 1,
plaice: 1,
},
Highland: {
tower: 1,
raven: 1,
wolfHeadErased: 1,
wolfPassant: 1,
goat: 1,
axe: 1,
},
River: {
garb: 1,
rake: 1,
raft: 1,
boat: 2,
drakkar: 2,
hook: 2,
pike: 2,
bullHeadCaboshed: 1,
apple: 1,
pear: 1,
plough: 1,
earOfWheat: 1,
salmon: 1,
cancer: 1,
bridge: 1,
bridge2: 2,
sickle: 1,
scythe: 1,
grapeBunch: 1,
wheatStalk: 1,
windmill: 1,
crocodile: 1,
},
Lake: {
hook: 3,
cancer: 2,
escallop: 1,
pike: 2,
heron: 1,
boat: 1,
boat2: 2,
salmon: 1,
sickle: 1,
windmill: 1,
swanErased: 1,
swan: 1,
frog: 1,
wasp: 1,
},
Nomadic: {
pot: 1,
buckle: 1,
wheel: 2,
sabre: 2,
sabresCrossed: 1,
bow: 2,
arrow: 1,
horseRampant: 1,
horseSalient: 1,
crescent: 1,
camel: 3,
scorpion: 1,
falcon: 1,
},
Hunting: {
bugleHorn: 2,
bugleHorn2: 1,
stagsAttires: 2,
attire: 2,
hatchet: 1,
bowWithArrow: 2,
arrowsSheaf: 1,
lanceHead: 1,
saw: 1,
deerHeadCaboshed: 1,
wolfStatant: 1,
oak: 1,
pineCone: 1,
pineTree: 1,
owl: 1,
falcon: 1,
peacock: 1,
boarHeadErased: 2,
horseHeadCouped: 1,
rabbitSejant: 1,
wolfRampant: 1,
wolfPassant: 1,
greyhoundCourant: 1,
greyhoundRampant: 1,
greyhoundSejant: 1,
mastiffStatant: 1,
talbotPassant: 1,
talbotSejant: 1,
stagPassant: 21,
},
// Selection based on type
City: {
key: 4,
bell: 3,
lute: 1,
tower: 1,
pillar: 1,
castle: 1,
castle2: 1,
portcullis: 1,
mallet: 1,
cannon: 1,
anvil: 1,
buckle: 1,
horseshoe: 1,
stirrup: 1,
lanceWithBanner: 1,
bookClosed: 1,
scissors: 1,
scissors2: 1,
shears: 1,
pincers: 1,
bridge: 2,
archer: 1,
shield: 1,
arbalest: 1,
arbalest2: 1,
bowWithThreeArrows: 1,
spear: 1,
lochaberAxe: 1,
armEmbowedHoldingSabre: 1,
grenade: 1,
maces: 1,
grapeBunch: 1,
cock: 1,
ramHeadErased: 1,
ratRampant: 1,
hourglass: 1,
scale: 1,
scrollClosed: 1,
},
Capital: {
crown: 2,
crown2: 2,
laurelWreath: 1,
orb: 1,
lute: 1,
lyre: 1,
banner: 1,
castle: 1,
castle2: 1,
palace: 1,
column: 1,
lionRampant: 1,
stagLodgedRegardant: 1,
drawingCompass: 1,
rapier: 1,
scaleImbalanced: 1,
scalesHanging: 1,
},
Сathedra: {
crossHummetty: 3,
mitre: 3,
chalice: 1,
orb: 1,
crosier: 2,
lamb: 1,
monk: 2,
angel: 3,
crossLatin: 2,
crossPatriarchal: 1,
crossOrthodox: 1,
crossCalvary: 1,
agnusDei: 3,
bookOpen: 1,
sceptre: 1,
bone: 1,
skull: 1,
},
};

View file

@ -1,272 +1,351 @@
"use strict";
import { byId } from "../utils";
const fonts = [
{family: "Arial"},
{family: "Brush Script MT"},
{family: "Century Gothic"},
{family: "Comic Sans MS"},
{family: "Copperplate"},
{family: "Courier New"},
{family: "Garamond"},
{family: "Georgia"},
{family: "Herculanum"},
{family: "Impact"},
{family: "Papyrus"},
{family: "Party LET"},
{family: "Times New Roman"},
{family: "Verdana"},
declare global {
var declareFont: (font: FontDefinition) => void;
var getUsedFonts: (svg: SVGSVGElement) => FontDefinition[];
var loadFontsAsDataURI: (
fonts: FontDefinition[],
) => Promise<FontDefinition[]>;
var addGoogleFont: (family: string) => Promise<void>;
var addLocalFont: (family: string) => void;
var addWebFont: (family: string, src: string) => void;
var fonts: FontDefinition[];
}
type FontDefinition = {
family: string;
src?: string;
unicodeRange?: string;
variant?: string;
};
window.fonts = [
{ family: "Arial" },
{ family: "Brush Script MT" },
{ family: "Century Gothic" },
{ family: "Comic Sans MS" },
{ family: "Copperplate" },
{ family: "Courier New" },
{ family: "Garamond" },
{ family: "Georgia" },
{ family: "Herculanum" },
{ family: "Impact" },
{ family: "Papyrus" },
{ family: "Party LET" },
{ family: "Times New Roman" },
{ family: "Verdana" },
{
family: "Almendra SC",
src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Amarante",
src: "url(https://fonts.gstatic.com/s/amarante/v22/xMQXuF1KTa6EvGx9bp-wAXs.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Amatic SC",
src: "url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Arima Madurai",
src: "url(https://fonts.gstatic.com/s/arimamadurai/v14/t5tmIRoeKYORG0WNMgnC3seB3T7Prw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Architects Daughter",
src: "url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Bitter",
src: "url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Caesar Dressing",
src: "url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Cinzel",
src: "url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Dancing Script",
src: "url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Eagle Lake",
src: "url(https://fonts.gstatic.com/s/eaglelake/v24/ptRMTiqbbuNJDOiKj9wG1On4KCFtpe4.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Faster One",
src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Forum",
src: "url(https://fonts.gstatic.com/s/forum/v16/6aey4Ky-Vb8Ew8IROpI.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Fredericka the Great",
src: "url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Gloria Hallelujah",
src: "url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Great Vibes",
src: "url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Henny Penny",
src: "url(https://fonts.gstatic.com/s/hennypenny/v17/wXKvE3UZookzsxz_kjGSfPQtvXI.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "IM Fell English",
src: "url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Kelly Slab",
src: "url(https://fonts.gstatic.com/s/kellyslab/v15/-W_7XJX0Rz3cxUnJC5t6fkQLfg.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Kranky",
src: "url(https://fonts.gstatic.com/s/kranky/v24/hESw6XVgJzlPsFn8oR2F.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Lobster Two",
src: "url(https://fonts.gstatic.com/s/lobstertwo/v18/BngMUXZGTXPUvIoyV6yN5-fN5qU.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Lugrasimo",
src: "url(https://fonts.gstatic.com/s/lugrasimo/v4/qkBXXvoF_s_eT9c7Y7au455KsgbLMA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Kaushan Script",
src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Macondo",
src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "MedievalSharp",
src: "url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Metal Mania",
src: "url(https://fonts.gstatic.com/s/metalmania/v22/RWmMoKWb4e8kqMfBUdPFJdXFiaQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Metamorphous",
src: "url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Montez",
src: "url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Nova Script",
src: "url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Orbitron",
src: "url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Oregano",
src: "url(https://fonts.gstatic.com/s/oregano/v13/If2IXTPxciS3H4S2oZDVPg.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Pirata One",
src: "url(https://fonts.gstatic.com/s/pirataone/v22/I_urMpiDvgLdLh0fAtofhi-Org.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Sail",
src: "url(https://fonts.gstatic.com/s/sail/v16/DPEjYwiBxwYJJBPJAQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Satisfy",
src: "url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Shadows Into Light",
src: "url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
{
family: "Tapestry",
src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Uncial Antiqua",
src: "url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Underdog",
src: "url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "UnifrakturMaguntia",
src: "url(https://fonts.gstatic.com/s/unifrakturmaguntia/v16/WWXPlieVYwiGNomYU-ciRLRvEmK7oaVemGZM.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
},
{
family: "Yellowtail",
src: "url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
}
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
},
];
declareDefaultFonts(); // execute once on load
function declareFont(font) {
const {family, src, ...rest} = font;
window.declareFont = (font: FontDefinition) => {
const { family, src, ...rest } = font;
addFontOption(family);
if (!src) return;
const fontFace = new FontFace(family, src, {...rest, display: "block"});
const fontFace = new FontFace(family, src, { ...rest, display: "block" });
document.fonts.add(fontFace);
}
};
declareDefaultFonts(); // execute once on load
function declareDefaultFonts() {
fonts.forEach(font => declareFont(font));
fonts.forEach((font) => {
declareFont(font);
});
}
function getUsedFonts(svg) {
function addFontOption(family: string) {
const options = document.getElementById("styleSelectFont")!;
const option = document.createElement("option");
option.value = family;
option.innerText = family;
option.style.fontFamily = family;
options.append(option);
}
async function fetchGoogleFont(family: string) {
const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
try {
const resp = await fetch(url);
const text = await resp.text();
const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
const fonts = fontFaceRules!.map((fontFace) => {
const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)?.[1];
const src = `url(${srcURL})`;
const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
const variant = fontFace.match(/font-style: (.*?);/)?.[1];
const font: FontDefinition = { family, src };
if (unicodeRange) font.unicodeRange = unicodeRange;
if (variant && variant !== "normal") font.variant = variant;
return font;
});
return fonts;
} catch (err) {
ERROR && console.error(err);
return null;
}
}
function readBlobAsDataURL(blob: Blob) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
window.loadFontsAsDataURI = async (fonts: FontDefinition[]) => {
const promises = fonts.map(async (font) => {
const url = font.src?.match(/url\(['"]?(.+?)['"]?\)/)?.[1];
if (!url) return font;
const resp = await fetch(url);
const blob = await resp.blob();
const dataURL = await readBlobAsDataURL(blob);
return { ...font, src: `url('${dataURL}')` };
});
return await Promise.all(promises);
};
window.getUsedFonts = (svg: SVGSVGElement) => {
const usedFontFamilies = new Set();
const labelGroups = svg.querySelectorAll("#labels g");
@ -282,112 +361,66 @@ function getUsedFonts(svg) {
const legendFont = legend?.getAttribute("font-family");
if (legendFont) usedFontFamilies.add(legendFont);
const usedFonts = fonts.filter(font => usedFontFamilies.has(font.family));
const usedFonts = fonts.filter((font) => usedFontFamilies.has(font.family));
return usedFonts;
}
};
function addFontOption(family) {
const options = document.getElementById("styleSelectFont");
const option = document.createElement("option");
option.value = family;
option.innerText = family;
option.style.fontFamily = family;
options.add(option);
}
async function fetchGoogleFont(family) {
const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
try {
const resp = await fetch(url);
const text = await resp.text();
const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
const fonts = fontFaceRules.map(fontFace => {
const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)[1];
const src = `url(${srcURL})`;
const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
const variant = fontFace.match(/font-style: (.*?);/)?.[1];
const font = {family, src};
if (unicodeRange) font.unicodeRange = unicodeRange;
if (variant && variant !== "normal") font.variant = variant;
return font;
});
return fonts;
} catch (err) {
ERROR && console.error(err);
return null;
}
}
function readBlobAsDataURL(blob) {
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
async function loadFontsAsDataURI(fonts) {
const promises = fonts.map(async font => {
const url = font.src.match(/url\(['"]?(.+?)['"]?\)/)[1];
const resp = await fetch(url);
const blob = await resp.blob();
const dataURL = await readBlobAsDataURL(blob);
return {...font, src: `url('${dataURL}')`};
});
return await Promise.all(promises);
}
async function addGoogleFont(family) {
window.addGoogleFont = async (family: string) => {
const fontRanges = await fetchGoogleFont(family);
if (!fontRanges) return tip("Cannot fetch Google font for this value", true, "error", 4000);
if (!fontRanges)
return tip("Cannot fetch Google font for this value", true, "error", 4000);
tip(`Google font ${family} is loading...`, true, "warn", 4000);
const promises = fontRanges.map(range => {
const {src, unicodeRange, variant} = range;
const fontFace = new FontFace(family, src, {unicodeRange, variant, display: "block"});
const promises = fontRanges.map((range) => {
const { src, unicodeRange } = range;
const fontFace = new FontFace(family, src!, {
unicodeRange,
display: "block",
});
return fontFace.load();
});
Promise.all(promises)
.then(fontFaces => {
fontFaces.forEach(fontFace => document.fonts.add(fontFace));
.then((fontFaces) => {
fontFaces.forEach((fontFace) => {
document.fonts.add(fontFace);
});
fonts.push(...fontRanges);
tip(`Google font ${family} is added to the list`, true, "success", 4000);
addFontOption(family);
document.getElementById("styleSelectFont").value = family;
const select = byId<HTMLSelectElement>("styleSelectFont");
if (select) select.value = family;
changeFont();
})
.catch(err => {
.catch((err) => {
tip(`Failed to load Google font ${family}`, true, "error", 4000);
ERROR && console.error(err);
});
}
};
function addLocalFont(family) {
fonts.push({family});
window.addLocalFont = (family: string) => {
fonts.push({ family });
const fontFace = new FontFace(family, `local(${family})`, {display: "block"});
const fontFace = new FontFace(family, `local(${family})`, {
display: "block",
});
document.fonts.add(fontFace);
tip(`Local font ${family} is added to the fonts list`, true, "success", 4000);
addFontOption(family);
document.getElementById("styleSelectFont").value = family;
const select = byId<HTMLSelectElement>("styleSelectFont");
if (select) select.value = family;
changeFont();
}
};
function addWebFont(family, url) {
window.addWebFont = (family: string, url: string) => {
const src = `url('${url}')`;
fonts.push({family, src});
fonts.push({ family, src });
const fontFace = new FontFace(family, src, {display: "block"});
const fontFace = new FontFace(family, src, { display: "block" });
document.fonts.add(fontFace);
tip(`Font ${family} is added to the list`, true, "success", 4000);
addFontOption(family);
document.getElementById("styleSelectFont").value = family;
const select = byId<HTMLSelectElement>("styleSelectFont");
if (select) select.value = family;
changeFont();
}
};

View file

@ -1,44 +1,65 @@
"use strict";
import Alea from "alea";
import { min } from "d3";
import {
clipPoly,
getGridPolygon,
getIsolines,
lerp,
minmax,
normalize,
P,
ra,
rand,
rn,
} from "../utils";
import type { Point } from "./voronoi";
// Ice layer data model - separates ice data from SVG rendering
window.Ice = (function () {
declare global {
var Ice: IceModule;
}
class IceModule {
// Find next available id for new ice element idealy filling gaps
function getNextId() {
private getNextId() {
if (pack.ice.length === 0) return 0;
// find gaps in existing ids
const existingIds = pack.ice.map(e => e.i).sort((a, b) => a - b);
const existingIds = pack.ice.map((e) => e.i).sort((a, b) => a - b);
for (let id = 0; id < existingIds[existingIds.length - 1]; id++) {
if (!existingIds.includes(id)) return id;
}
return existingIds[existingIds.length - 1] + 1;
}
// Clear all ice
private clear() {
pack.ice = [];
}
// Generate glaciers and icebergs based on temperature and height
function generate() {
clear();
public generate() {
this.clear();
const { cells, features } = grid;
const { temp, h } = cells;
Math.random = aleaPRNG(seed);
Math.random = Alea(seed);
const ICEBERG_MAX_TEMP = 0;
const GLACIER_MAX_TEMP = -8;
const minMaxTemp = d3.min(temp);
const minMaxTemp = min<number>(temp)!;
// Generate glaciers on cold land
{
const type = "iceShield";
const getType = cellId =>
const getType = (cellId: number) =>
h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null;
const isolines = getIsolines(grid, getType, { polygons: true });
if (isolines[type]?.polygons) {
isolines[type].polygons.forEach(points => {
const clipped = clipPoly(points);
isolines[type].polygons.forEach((points: Point[]) => {
const clipped = clipPoly(points, graphWidth, graphHeight);
pack.ice.push({
i: getNextId(),
i: this.getNextId(),
points: clipped,
type: "glacier"
type: "glacier",
});
});
}
@ -58,62 +79,53 @@ window.Ice = (function () {
const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1);
const [cx, cy] = grid.points[cellId];
const points = getGridPolygon(cellId).map(([x, y]) => [
const points = getGridPolygon(cellId, grid).map(([x, y]: Point) => [
rn(lerp(cx, x, size), 2),
rn(lerp(cy, y, size), 2)
rn(lerp(cy, y, size), 2),
]);
pack.ice.push({
i: getNextId(),
i: this.getNextId(),
points,
type: "iceberg",
cellId,
size
size,
});
}
}
function addIceberg(cellId, size) {
addIceberg(cellId: number, size: number) {
const [cx, cy] = grid.points[cellId];
const points = getGridPolygon(cellId).map(([x, y]) => [
const points = getGridPolygon(cellId, grid).map(([x, y]: Point) => [
rn(lerp(cx, x, size), 2),
rn(lerp(cy, y, size), 2)
rn(lerp(cy, y, size), 2),
]);
const id = getNextId();
const id = this.getNextId();
pack.ice.push({
i: id,
points,
type: "iceberg",
cellId,
size
size,
});
redrawIceberg(id);
}
function removeIce(id) {
const index = pack.ice.findIndex(element => element.i === id);
removeIce(id: number) {
const index = pack.ice.findIndex((element) => element.i === id);
if (index !== -1) {
const type = pack.ice.find(element => element.i === id).type;
const type = pack.ice.find((element) => element.i === id).type;
pack.ice.splice(index, 1);
if (type === "glacier") {
redrawGlacier(id);
} else {
redrawIceberg(id);
}
}
}
function updateIceberg(id, points, size) {
const iceberg = pack.ice.find(element => element.i === id);
if (iceberg) {
iceberg.points = points;
iceberg.size = size;
}
}
function randomizeIcebergShape(id) {
const iceberg = pack.ice.find(element => element.i === id);
randomizeIcebergShape(id: number) {
const iceberg = pack.ice.find((element) => element.i === id);
if (!iceberg) return;
const cellId = iceberg.cellId;
@ -123,17 +135,20 @@ window.Ice = (function () {
// Get a different random cell for the polygon template
const i = ra(grid.cells.i);
const cn = grid.points[i];
const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]);
const points = poly.map(p => [
const poly = getGridPolygon(i, grid).map((p: Point) => [
p[0] - cn[0],
p[1] - cn[1],
]);
const points = poly.map((p: Point) => [
rn(cx + p[0] * size, 2),
rn(cy + p[1] * size, 2)
rn(cy + p[1] * size, 2),
]);
iceberg.points = points;
}
function changeIcebergSize(id, newSize) {
const iceberg = pack.ice.find(element => element.i === id);
changeIcebergSize(id: number, newSize: number) {
const iceberg = pack.ice.find((element) => element.i === id);
if (!iceberg) return;
const cellId = iceberg.cellId;
@ -143,28 +158,18 @@ window.Ice = (function () {
const flat = iceberg.points.flat();
const pairs = [];
while (flat.length) pairs.push(flat.splice(0, 2));
const poly = pairs.map(p => [(p[0] - cx) / oldSize, (p[1] - cy) / oldSize]);
const points = poly.map(p => [
const poly = pairs.map((p) => [
(p[0] - cx) / oldSize,
(p[1] - cy) / oldSize,
]);
const points = poly.map((p) => [
rn(cx + p[0] * newSize, 2),
rn(cy + p[1] * newSize, 2)
rn(cy + p[1] * newSize, 2),
]);
iceberg.points = points;
iceberg.size = newSize;
}
}
// Clear all ice
function clear() {
pack.ice = [];
}
return {
generate,
addIceberg,
removeIce,
updateIceberg,
randomizeIcebergShape,
changeIcebergSize,
clear
};
})();
window.Ice = new IceModule();

View file

@ -14,3 +14,7 @@ import "./zones-generator";
import "./religions-generator";
import "./labels";
import "./provinces-generator";
import "./emblem";
import "./ice";
import "./markers-generator";
import "./fonts";

File diff suppressed because it is too large Load diff

View file

@ -330,7 +330,7 @@ class ProvinceModule {
: P(0.3);
const kinship = dominion ? 0 : 0.4;
const type = Burgs.getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
const coa = COA.generate(s.coa, kinship, dominion ? 1 : 0, type);
coa.shield = COA.getShield(c, s.i);
provinces.push({

View file

@ -71,7 +71,7 @@ class StatesModule {
const name = Names.getState(basename, burg.culture!);
const type = pack.cultures[burg.culture!].type;
const coa = COA.generate(null, null, null, type);
coa.shield = COA.getShield(burg.culture, null);
coa.shield = COA.getShield(burg.culture!);
states.push({
i: burg.i,
name,

View file

@ -30,6 +30,7 @@ declare global {
var mapName: HTMLInputElement;
var religionsNumber: HTMLInputElement;
var distanceUnitInput: HTMLInputElement;
var heightUnit: HTMLSelectElement;
var rivers: Selection<SVGElement, unknown, null, undefined>;
var oceanLayers: Selection<SVGGElement, unknown, null, undefined>;
@ -46,6 +47,7 @@ declare global {
var defs: Selection<SVGDefsElement, unknown, null, undefined>;
var coastline: Selection<SVGGElement, unknown, null, undefined>;
var lakes: Selection<SVGGElement, unknown, null, undefined>;
var provs: Selection<SVGGElement, unknown, null, undefined>;
var getColorScheme: (scheme: string | null) => (t: number) => string;
var getColor: (height: number, scheme: (t: number) => string) => string;
var svgWidth: number;
@ -62,7 +64,6 @@ declare global {
icons: string[][];
cost: number[];
};
var COA: any;
var notes: any[];
var style: {
burgLabels: { [key: string]: { [key: string]: string } };
@ -74,16 +75,18 @@ declare global {
var layerIsOn: (layerId: string) => boolean;
var drawRoute: (route: any) => void;
var invokeActiveZooming: () => void;
var COArenderer: { trigger: (id: string, coa: any) => void };
var FlatQueue: any;
var tip: (
message: string,
autoHide?: boolean,
type?: "info" | "warning" | "error",
type?: "info" | "warn" | "error" | "success",
timeout?: number,
) => void;
var locked: (settingId: string) => boolean;
var unlock: (settingId: string) => void;
var $: (selector: any) => any;
var scale: number;
var changeFont: () => void;
var getFriendlyHeight: (coords: [number, number]) => string;
}

View file

@ -0,0 +1,379 @@
import { describe, expect, it } from "vitest";
import {
biased,
each,
gauss,
generateSeed,
getNumberInRange,
P,
Pint,
ra,
rand,
rw,
} from "./probabilityUtils";
describe("rand", () => {
describe("when called with no arguments", () => {
it("should return a float between 0 and 1", () => {
for (let i = 0; i < 100; i++) {
const result = rand();
expect(result).toBeGreaterThanOrEqual(0);
expect(result).toBeLessThan(1);
}
});
});
describe("when called with one argument (max)", () => {
it("should return an integer between 0 and max (inclusive)", () => {
for (let i = 0; i < 100; i++) {
const result = rand(10);
expect(result).toBeGreaterThanOrEqual(0);
expect(result).toBeLessThanOrEqual(10);
expect(Number.isInteger(result)).toBe(true);
}
});
it("should return 0 when max is 0", () => {
expect(rand(0)).toBe(0);
});
});
describe("when called with two arguments (min, max)", () => {
it("should return an integer between min and max (inclusive)", () => {
for (let i = 0; i < 100; i++) {
const result = rand(5, 15);
expect(result).toBeGreaterThanOrEqual(5);
expect(result).toBeLessThanOrEqual(15);
expect(Number.isInteger(result)).toBe(true);
}
});
it("should handle negative ranges", () => {
for (let i = 0; i < 100; i++) {
const result = rand(-10, -5);
expect(result).toBeGreaterThanOrEqual(-10);
expect(result).toBeLessThanOrEqual(-5);
expect(Number.isInteger(result)).toBe(true);
}
});
it("should return the same value when min equals max", () => {
expect(rand(7, 7)).toBe(7);
});
});
});
describe("P", () => {
it("should always return true when probability is 1", () => {
for (let i = 0; i < 100; i++) {
expect(P(1)).toBe(true);
}
});
it("should always return true when probability is greater than 1", () => {
expect(P(1.5)).toBe(true);
expect(P(100)).toBe(true);
});
it("should always return false when probability is 0", () => {
for (let i = 0; i < 100; i++) {
expect(P(0)).toBe(false);
}
});
it("should always return false when probability is negative", () => {
expect(P(-0.5)).toBe(false);
expect(P(-1)).toBe(false);
});
it("should return boolean for probabilities between 0 and 1", () => {
for (let i = 0; i < 100; i++) {
const result = P(0.5);
expect(typeof result).toBe("boolean");
}
});
it("should approximately match the given probability over many trials", () => {
const trials = 10000;
let trueCount = 0;
const probability = 0.3;
for (let i = 0; i < trials; i++) {
if (P(probability)) trueCount++;
}
const observedProbability = trueCount / trials;
// Allow 5% tolerance
expect(observedProbability).toBeGreaterThan(probability - 0.05);
expect(observedProbability).toBeLessThan(probability + 0.05);
});
});
describe("each", () => {
it("should return true every n times starting from 0", () => {
const every3 = each(3);
expect(every3(0)).toBe(true);
expect(every3(1)).toBe(false);
expect(every3(2)).toBe(false);
expect(every3(3)).toBe(true);
expect(every3(4)).toBe(false);
expect(every3(5)).toBe(false);
expect(every3(6)).toBe(true);
});
it("should work with n=1 (always true)", () => {
const every1 = each(1);
expect(every1(0)).toBe(true);
expect(every1(1)).toBe(true);
expect(every1(2)).toBe(true);
});
it("should work with larger intervals", () => {
const every10 = each(10);
expect(every10(0)).toBe(true);
expect(every10(5)).toBe(false);
expect(every10(10)).toBe(true);
expect(every10(20)).toBe(true);
});
});
describe("gauss", () => {
it("should return a number", () => {
const result = gauss();
expect(typeof result).toBe("number");
});
it("should respect min and max bounds", () => {
for (let i = 0; i < 100; i++) {
const result = gauss(50, 20, 10, 90, 0);
expect(result).toBeGreaterThanOrEqual(10);
expect(result).toBeLessThanOrEqual(90);
}
});
it("should use default values when no arguments provided", () => {
for (let i = 0; i < 100; i++) {
const result = gauss();
expect(result).toBeGreaterThanOrEqual(0);
expect(result).toBeLessThanOrEqual(300);
}
});
it("should round to specified decimal places", () => {
const result = gauss(100, 30, 0, 300, 2);
const decimalPlaces = (result.toString().split(".")[1] || "").length;
expect(decimalPlaces).toBeLessThanOrEqual(2);
});
});
describe("Pint", () => {
it("should return the integer part for whole numbers", () => {
expect(Pint(5)).toBe(5);
expect(Pint(0)).toBe(0);
expect(Pint(10)).toBe(10);
});
it("should return at least the integer part for floats", () => {
// The function returns floor + (0 or 1 based on probability)
for (let i = 0; i < 100; i++) {
const result = Pint(5.5);
expect(result).toBeGreaterThanOrEqual(5);
expect(result).toBeLessThanOrEqual(6);
expect(Number.isInteger(result)).toBe(true);
}
});
it("should always return floor for very small decimals", () => {
// With very small decimal, almost always returns floor
let sumResults = 0;
for (let i = 0; i < 1000; i++) {
sumResults += Pint(5.001);
}
// Most should be 5, very few 6
expect(sumResults / 1000).toBeCloseTo(5, 0);
});
it("should return floor+1 more often for larger decimals", () => {
// With 0.9 decimal, should return floor+1 about 90% of the time
let count6 = 0;
for (let i = 0; i < 1000; i++) {
if (Pint(5.9) === 6) count6++;
}
expect(count6 / 1000).toBeGreaterThan(0.8);
});
});
describe("ra", () => {
it("should return an element from the array", () => {
const array = [1, 2, 3, 4, 5];
for (let i = 0; i < 100; i++) {
const result = ra(array);
expect(array).toContain(result);
}
});
it("should return the only element for single-element array", () => {
expect(ra([42])).toBe(42);
});
it("should work with arrays of different types", () => {
const stringArray = ["a", "b", "c"];
const result = ra(stringArray);
expect(stringArray).toContain(result);
const objectArray = [{ id: 1 }, { id: 2 }];
const objResult = ra(objectArray);
expect(objectArray).toContain(objResult);
});
it("should return undefined for empty array", () => {
expect(ra([])).toBeUndefined();
});
});
describe("rw", () => {
it("should return a key from the object", () => {
const obj = { a: 1, b: 2, c: 3 };
for (let i = 0; i < 100; i++) {
const result = rw(obj);
expect(["a", "b", "c"]).toContain(result);
}
});
it("should respect weights (higher weight = more likely)", () => {
const obj = { rare: 1, common: 99 };
let commonCount = 0;
const trials = 1000;
for (let i = 0; i < trials; i++) {
if (rw(obj) === "common") commonCount++;
}
// 'common' should appear much more frequently
expect(commonCount / trials).toBeGreaterThan(0.9);
});
it("should work with single key", () => {
expect(rw({ only: 5 })).toBe("only");
});
it("should handle keys with weight 0 (never selected)", () => {
const obj = { never: 0, always: 10 };
for (let i = 0; i < 100; i++) {
expect(rw(obj)).toBe("always");
}
});
});
describe("biased", () => {
it("should return a number between min and max", () => {
for (let i = 0; i < 100; i++) {
const result = biased(0, 100, 2);
expect(result).toBeGreaterThanOrEqual(0);
expect(result).toBeLessThanOrEqual(100);
expect(Number.isInteger(result)).toBe(true);
}
});
it("should be biased towards min with higher exponent", () => {
const trials = 1000;
let sumLowBias = 0;
let sumHighBias = 0;
for (let i = 0; i < trials; i++) {
sumLowBias += biased(0, 100, 1); // No bias (uniform)
sumHighBias += biased(0, 100, 3); // Strong bias towards min
}
const avgLowBias = sumLowBias / trials;
const avgHighBias = sumHighBias / trials;
// Higher exponent should result in lower average
expect(avgHighBias).toBeLessThan(avgLowBias);
});
it("should return min or max at boundaries", () => {
expect(biased(5, 5, 2)).toBe(5);
});
it("should work with negative ranges", () => {
for (let i = 0; i < 100; i++) {
const result = biased(-50, -10, 2);
expect(result).toBeGreaterThanOrEqual(-50);
expect(result).toBeLessThanOrEqual(-10);
}
});
});
describe("getNumberInRange", () => {
it("should parse simple integers", () => {
expect(getNumberInRange("5")).toBe(5);
expect(getNumberInRange("0")).toBe(0);
expect(getNumberInRange("100")).toBe(100);
});
it("should parse range strings and return value within range", () => {
for (let i = 0; i < 100; i++) {
const result = getNumberInRange("3-7");
expect(result).toBeGreaterThanOrEqual(3);
expect(result).toBeLessThanOrEqual(7);
expect(Number.isInteger(result)).toBe(true);
}
});
it("should handle negative start in range", () => {
for (let i = 0; i < 100; i++) {
const result = getNumberInRange("-5-10");
expect(result).toBeGreaterThanOrEqual(-5);
expect(result).toBeLessThanOrEqual(10);
}
});
it("should return 0 for non-string input", () => {
expect(getNumberInRange(5 as unknown as string)).toBe(0);
expect(getNumberInRange(null as unknown as string)).toBe(0);
});
it("should handle float strings with probability-based rounding", () => {
// "2.5" should return 2 or 3 based on probability
const results = new Set<number>();
for (let i = 0; i < 100; i++) {
results.add(getNumberInRange("2.5"));
}
// Should see both 2 and 3
expect(results.has(2) || results.has(3)).toBe(true);
});
it("should return 0 for invalid format without range separator", () => {
expect(getNumberInRange("abc")).toBe(0);
});
});
describe("generateSeed", () => {
it("should return a string", () => {
const result = generateSeed();
expect(typeof result).toBe("string");
});
it("should return a numeric string", () => {
const result = generateSeed();
expect(Number.isNaN(Number(result))).toBe(false);
});
it("should generate seeds less than 1 billion", () => {
for (let i = 0; i < 100; i++) {
const result = generateSeed();
expect(Number(result)).toBeLessThan(1e9);
expect(Number(result)).toBeGreaterThanOrEqual(0);
}
});
it("should generate different seeds on multiple calls (with high probability)", () => {
const seeds = new Set<string>();
for (let i = 0; i < 100; i++) {
seeds.add(generateSeed());
}
// Should have many unique seeds (allow for some rare collisions)
expect(seeds.size).toBeGreaterThan(90);
});
});

View file

@ -2,18 +2,18 @@ import { randomNormal } from "d3";
import { minmax, rn } from "./numberUtils";
/**
* Creates a random number between min and max (inclusive).
* Creates a random number between min and max (inclusive). If only one argument is provided, it will be considered as max and min will be 0. If no arguments are provided, it returns a random float between 0 and 1.
* @param {number} min - minimum value
* @param {number} max - maximum value
* @return {number} random integer between min and max
*/
export const rand = (min: number, max?: number): number => {
export const rand = (min?: number, max?: number): number => {
if (min === undefined && max === undefined) return Math.random();
if (max === undefined) {
max = min;
min = 0;
}
return Math.floor(Math.random() * (max - min + 1)) + min;
return Math.floor(Math.random() * (max! - min! + 1)) + min!;
};
/**

View file

@ -1,4 +1,5 @@
export const byId = document.getElementById.bind(document);
export const byId = <T extends HTMLElement>(id: string): T | undefined =>
document.getElementById(id) as T;
declare global {
interface Window {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

221
tests/e2e/load-map.spec.ts Normal file
View file

@ -0,0 +1,221 @@
import { test, expect } from "@playwright/test";
import path from "path";
test.describe("Map loading", () => {
test.beforeEach(async ({ context, page }) => {
await context.clearCookies();
await page.goto("/");
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
// Wait for the hidden file input to be available
await page.waitForSelector("#mapToLoad", { state: "attached" });
});
test("should load a saved map file", async ({ page }) => {
// Track errors during map loading
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.push(`console.error: ${msg.text()}`);
}
});
// Get the file input element and upload the map file
const fileInput = page.locator("#mapToLoad");
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
await fileInput.setInputFiles(mapFilePath);
// Wait for map to be fully loaded
// mapId is set at the very end of map loading in showStatistics()
await page.waitForFunction(() => (window as any).mapId !== undefined, {
timeout: 120000,
});
// Additional wait for rendering to settle
await page.waitForTimeout(500);
// Verify map data is loaded
const mapData = await page.evaluate(() => {
const pack = (window as any).pack;
return {
hasStates: pack.states && pack.states.length > 1,
hasBurgs: pack.burgs && pack.burgs.length > 1,
hasCells: pack.cells && pack.cells.i && pack.cells.i.length > 0,
hasRivers: pack.rivers && pack.rivers.length > 0,
mapId: (window as any).mapId,
};
});
expect(mapData.hasStates).toBe(true);
expect(mapData.hasBurgs).toBe(true);
expect(mapData.hasCells).toBe(true);
expect(mapData.hasRivers).toBe(true);
expect(mapData.mapId).toBeDefined();
// Ensure no JavaScript errors occurred during loading
// Filter out expected errors (external resources like Google Analytics, fonts)
const criticalErrors = errors.filter(
(e) =>
!e.includes("fonts.googleapis.com") &&
!e.includes("google-analytics") &&
!e.includes("googletagmanager") &&
!e.includes("Failed to load resource")
);
expect(criticalErrors).toEqual([]);
});
test("loaded map should have correct SVG structure", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.push(`console.error: ${msg.text()}`);
}
});
const fileInput = page.locator("#mapToLoad");
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
await fileInput.setInputFiles(mapFilePath);
await page.waitForFunction(() => (window as any).mapId !== undefined, {
timeout: 120000,
});
await page.waitForTimeout(500);
// Check essential SVG layers exist
const layers = await page.evaluate(() => {
return {
ocean: !!document.getElementById("ocean"),
lakes: !!document.getElementById("lakes"),
coastline: !!document.getElementById("coastline"),
rivers: !!document.getElementById("rivers"),
borders: !!document.getElementById("borders"),
burgs: !!document.getElementById("burgIcons"),
labels: !!document.getElementById("labels"),
};
});
expect(layers.ocean).toBe(true);
expect(layers.lakes).toBe(true);
expect(layers.coastline).toBe(true);
expect(layers.rivers).toBe(true);
expect(layers.borders).toBe(true);
expect(layers.burgs).toBe(true);
expect(layers.labels).toBe(true);
const criticalErrors = errors.filter(
(e) =>
!e.includes("fonts.googleapis.com") &&
!e.includes("google-analytics") &&
!e.includes("googletagmanager") &&
!e.includes("Failed to load resource")
);
expect(criticalErrors).toEqual([]);
});
test("loaded map should preserve state data", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.push(`console.error: ${msg.text()}`);
}
});
const fileInput = page.locator("#mapToLoad");
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
await fileInput.setInputFiles(mapFilePath);
await page.waitForFunction(() => (window as any).mapId !== undefined, {
timeout: 120000,
});
await page.waitForTimeout(500);
// Verify states have proper structure
const statesData = await page.evaluate(() => {
const pack = (window as any).pack;
const states = pack.states.filter((s: any) => s.i !== 0); // exclude neutral
return {
count: states.length,
allHaveNames: states.every((s: any) => s.name && s.name.length > 0),
allHaveCells: states.every((s: any) => s.cells > 0),
allHaveArea: states.every((s: any) => s.area > 0),
};
});
expect(statesData.count).toBeGreaterThan(0);
expect(statesData.allHaveNames).toBe(true);
expect(statesData.allHaveCells).toBe(true);
expect(statesData.allHaveArea).toBe(true);
const criticalErrors = errors.filter(
(e) =>
!e.includes("fonts.googleapis.com") &&
!e.includes("google-analytics") &&
!e.includes("googletagmanager") &&
!e.includes("Failed to load resource")
);
expect(criticalErrors).toEqual([]);
});
test("loaded map should preserve burg data", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.push(`console.error: ${msg.text()}`);
}
});
const fileInput = page.locator("#mapToLoad");
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
await fileInput.setInputFiles(mapFilePath);
await page.waitForFunction(() => (window as any).mapId !== undefined, {
timeout: 120000,
});
await page.waitForTimeout(500);
// Verify burgs have proper structure
const burgsData = await page.evaluate(() => {
const pack = (window as any).pack;
// Filter out placeholder (i=0) and removed burgs (removed=true or no name)
const activeBurgs = pack.burgs.filter(
(b: any) => b.i !== 0 && !b.removed && b.name
);
return {
count: activeBurgs.length,
allHaveNames: activeBurgs.every(
(b: any) => b.name && b.name.length > 0
),
allHaveCoords: activeBurgs.every(
(b: any) => typeof b.x === "number" && typeof b.y === "number"
),
allHaveCells: activeBurgs.every(
(b: any) => typeof b.cell === "number"
),
};
});
expect(burgsData.count).toBeGreaterThan(0);
expect(burgsData.allHaveNames).toBe(true);
expect(burgsData.allHaveCoords).toBe(true);
expect(burgsData.allHaveCells).toBe(true);
const criticalErrors = errors.filter(
(e) =>
!e.includes("fonts.googleapis.com") &&
!e.includes("google-analytics") &&
!e.includes("googletagmanager") &&
!e.includes("Failed to load resource")
);
expect(criticalErrors).toEqual([]);
});
});

View file

@ -0,0 +1,349 @@
import { test, expect } from "@playwright/test";
test.describe("Zone Export", () => {
test.beforeEach(async ({ context, page }) => {
await context.clearCookies();
await page.goto("/");
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
// Navigate with seed parameter and wait for full load
await page.goto("/?seed=test-zones-export&width=1280&height=720");
// Wait for map generation to complete
await page.waitForFunction(
() => (window as any).mapId !== undefined,
{ timeout: 60000 }
);
// Additional wait for any rendering/animations to settle
await page.waitForTimeout(500);
});
// Helper function to create a test zone programmatically
// Uses BFS to select a contiguous set of land cells for stable, representative testing
async function createTestZone(page: any): Promise<number> {
return await page.evaluate(() => {
const { cells, zones } = (window as any).pack;
// Find a starting land cell (height >= 20)
const totalCells = cells.i.length;
let startCell = -1;
for (let i = 1; i < totalCells; i++) {
if (cells.h[i] >= 20) {
startCell = i;
break;
}
}
if (startCell === -1) {
throw new Error("No land cells found to create a test zone");
}
// Use BFS to select a contiguous set of 10-20 land cells
const zoneCells: number[] = [];
const visited = new Set<number>();
const queue: number[] = [];
visited.add(startCell);
queue.push(startCell);
while (queue.length > 0 && zoneCells.length < 20) {
const current = queue.shift() as number;
// Only include land cells in the zone
if (cells.h[current] >= 20) {
zoneCells.push(current);
}
// Explore neighbors
const neighbors: number[] = cells.c[current] || [];
for (const neighbor of neighbors) {
if (neighbor && !visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
if (zoneCells.length < 10) {
throw new Error(`Not enough contiguous land cells found: ${zoneCells.length}`);
}
// Generate unique zone ID
const zoneId = zones.length;
// Create zone object
const zone = {
i: zoneId,
name: "Test Export Zone",
type: "Test",
color: "#FF0000",
cells: zoneCells,
};
// Add zone to pack.zones array
zones.push(zone);
return zoneId;
});
}
// Helper function to export zones to GeoJSON without file download
// This calls the production code from public/modules/io/export.js
async function exportZonesToGeoJson(page: any): Promise<any> {
return await page.evaluate(() => {
// Mock downloadFile to capture the JSON instead of downloading
const originalDownloadFile = (window as any).downloadFile;
let capturedJson: any = null;
(window as any).downloadFile = (data: string) => {
capturedJson = JSON.parse(data);
};
// Call the production code
(window as any).saveGeoJsonZones();
// Restore original downloadFile
(window as any).downloadFile = originalDownloadFile;
return capturedJson;
});
}
test("should export zone with valid GeoJSON root structure", async ({ page }) => {
// Create a test zone
const zoneId = await createTestZone(page);
expect(zoneId).toBeGreaterThanOrEqual(0);
// Export zones to GeoJSON
const geoJson = await exportZonesToGeoJson(page);
// Validate root GeoJSON structure (Task 5.1)
expect(geoJson).toBeDefined();
expect(geoJson).toHaveProperty("type");
expect(geoJson.type).toBe("FeatureCollection");
expect(geoJson).toHaveProperty("features");
expect(Array.isArray(geoJson.features)).toBe(true);
expect(geoJson.features.length).toBeGreaterThan(0);
// Verify the test zone is in the export
const testZoneFeature = geoJson.features.find((f: any) => f.properties.id === zoneId);
expect(testZoneFeature).toBeDefined();
expect(testZoneFeature.properties.name).toBe("Test Export Zone");
// Validate Feature structure (Task 5.2)
expect(testZoneFeature).toHaveProperty("type");
expect(testZoneFeature.type).toBe("Feature");
expect(testZoneFeature).toHaveProperty("geometry");
expect(testZoneFeature.geometry).toBeDefined();
expect(typeof testZoneFeature.geometry).toBe("object");
expect(testZoneFeature.geometry).toHaveProperty("type");
// Note: Geometry type can be "Polygon" (single component) or "MultiPolygon" (multiple disconnected components)
// For this test with contiguous BFS-selected cells, we expect "Polygon"
expect(testZoneFeature.geometry.type).toBe("Polygon");
expect(testZoneFeature.geometry).toHaveProperty("coordinates");
expect(Array.isArray(testZoneFeature.geometry.coordinates)).toBe(true);
expect(testZoneFeature).toHaveProperty("properties");
expect(testZoneFeature.properties).toBeDefined();
expect(typeof testZoneFeature.properties).toBe("object");
// Task 6.1: Validate zone property mapping
// Get the test zone from pack.zones in browser context
const testZone = await page.evaluate((id: number) => {
const { zones } = (window as any).pack;
return zones.find((z: any) => z.i === id);
}, zoneId);
expect(testZone).toBeDefined();
// Assert feature.properties match zone properties
expect(testZoneFeature.properties.id).toBe(testZone.i);
expect(testZoneFeature.properties.name).toBe(testZone.name);
expect(testZoneFeature.properties.type).toBe(testZone.type);
expect(testZoneFeature.properties.color).toBe(testZone.color);
expect(testZoneFeature.properties.cells).toEqual(testZone.cells);
// Task 7.1: Validate coordinate array structure
const { coordinates } = testZoneFeature.geometry;
// Assert geometry.coordinates is an array
expect(Array.isArray(coordinates)).toBe(true);
// Assert coordinates array is not empty
expect(coordinates.length).toBeGreaterThan(0);
// Validate each LinearRing in the coordinates array
// Note: Zones can have multiple rings (holes) or be MultiPolygon (disconnected components)
for (const linearRing of coordinates) {
// Assert LinearRing is an array
expect(Array.isArray(linearRing)).toBe(true);
// Task 7.2: Validate LinearRing validity
// Assert LinearRing has at least 4 positions
expect(linearRing.length).toBeGreaterThanOrEqual(4);
// Assert first position equals last position (closed ring)
const firstPosition = linearRing[0];
const lastPosition = linearRing[linearRing.length - 1];
expect(firstPosition[0]).toBe(lastPosition[0]);
expect(firstPosition[1]).toBe(lastPosition[1]);
// Assert each position in LinearRing is an array of 2 numbers
for (const position of linearRing) {
expect(Array.isArray(position)).toBe(true);
expect(position.length).toBe(2);
expect(typeof position[0]).toBe("number");
expect(typeof position[1]).toBe("number");
// Assert all positions are valid [longitude, latitude] pairs
// Longitude should be between -180 and 180
expect(position[0]).toBeGreaterThanOrEqual(-180);
expect(position[0]).toBeLessThanOrEqual(180);
// Latitude should be between -90 and 90
expect(position[1]).toBeGreaterThanOrEqual(-90);
expect(position[1]).toBeLessThanOrEqual(90);
}
}
});
test("should exclude hidden zones from GeoJSON export", async ({ page }) => {
// Create a regular test zone
const regularZoneId = await createTestZone(page);
expect(regularZoneId).toBeGreaterThanOrEqual(0);
// Create a hidden zone
const hiddenZoneId = await page.evaluate(() => {
const { cells, zones } = (window as any).pack;
// Find a starting land cell that's not already in a zone
const totalCells = cells.i.length;
let startCell = -1;
for (let i = 1; i < totalCells; i++) {
const isLand = cells.h[i] >= 20;
const notInZone = !zones.some((z: any) => z.cells && z.cells.includes(i));
if (isLand && notInZone) {
startCell = i;
break;
}
}
if (startCell === -1) {
throw new Error("No available land cells found for hidden zone");
}
// Use BFS to select a contiguous set of 10-20 land cells
const zoneCells: number[] = [];
const visited = new Set<number>();
const queue: number[] = [];
visited.add(startCell);
queue.push(startCell);
while (queue.length > 0 && zoneCells.length < 20) {
const current = queue.shift() as number;
// Only include land cells not already in a zone
const isLand = cells.h[current] >= 20;
const notInZone = !zones.some((z: any) => z.cells && z.cells.includes(current));
if (isLand && notInZone) {
zoneCells.push(current);
}
// Explore neighbors
const neighbors: number[] = cells.c[current] || [];
for (const neighbor of neighbors) {
if (neighbor && !visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
if (zoneCells.length < 10) {
throw new Error(`Not enough contiguous land cells found: ${zoneCells.length}`);
}
// Generate unique zone ID
const zoneId = zones.length;
// Create hidden zone object
const zone = {
i: zoneId,
name: "Hidden Test Zone",
type: "Test",
color: "#00FF00",
cells: zoneCells,
hidden: true, // Mark as hidden
};
// Add zone to pack.zones array
zones.push(zone);
return zoneId;
});
expect(hiddenZoneId).toBeGreaterThanOrEqual(0);
// Export zones to GeoJSON
const geoJson = await exportZonesToGeoJson(page);
// Validate that the regular zone is in the export
const regularZoneFeature = geoJson.features.find((f: any) => f.properties.id === regularZoneId);
expect(regularZoneFeature).toBeDefined();
expect(regularZoneFeature.properties.name).toBe("Test Export Zone");
// Validate that the hidden zone is NOT in the export
const hiddenZoneFeature = geoJson.features.find((f: any) => f.properties.id === hiddenZoneId);
expect(hiddenZoneFeature).toBeUndefined();
});
test("should exclude zones with empty cells array from GeoJSON export", async ({ page }) => {
// Create a regular test zone
const regularZoneId = await createTestZone(page);
expect(regularZoneId).toBeGreaterThanOrEqual(0);
// Create a zone with empty cells array
const emptyZoneId = await page.evaluate(() => {
const { zones } = (window as any).pack;
// Generate unique zone ID
const zoneId = zones.length;
// Create zone object with empty cells array
const zone = {
i: zoneId,
name: "Empty Test Zone",
type: "Test",
color: "#0000FF",
cells: [], // Empty cells array
};
// Add zone to pack.zones array
zones.push(zone);
return zoneId;
});
expect(emptyZoneId).toBeGreaterThanOrEqual(0);
// Export zones to GeoJSON
const geoJson = await exportZonesToGeoJson(page);
// Validate that the regular zone is in the export
const regularZoneFeature = geoJson.features.find((f: any) => f.properties.id === regularZoneId);
expect(regularZoneFeature).toBeDefined();
expect(regularZoneFeature.properties.name).toBe("Test Export Zone");
// Validate that the empty zone is NOT in the export
const emptyZoneFeature = geoJson.features.find((f: any) => f.properties.id === emptyZoneId);
expect(emptyZoneFeature).toBeUndefined();
});
});

174
tests/fixtures/demo.map vendored Normal file

File diff suppressed because one or more lines are too long