mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-22 23:27:23 +01:00
Merge branch 'master' into feature/split-label-view-data
This commit is contained in:
commit
ba0ce8e40b
47 changed files with 8023 additions and 5800 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -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
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
53
src/modules/emblem/box.ts
Normal 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",
|
||||
};
|
||||
883
src/modules/emblem/chargeData.ts
Normal file
883
src/modules/emblem/chargeData.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
477
src/modules/emblem/charges.ts
Normal file
477
src/modules/emblem/charges.ts
Normal 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,
|
||||
};
|
||||
12
src/modules/emblem/colors.ts
Normal file
12
src/modules/emblem/colors.ts
Normal 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",
|
||||
};
|
||||
46
src/modules/emblem/divisions.ts
Normal file
46
src/modules/emblem/divisions.ts
Normal 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,
|
||||
};
|
||||
591
src/modules/emblem/generator.ts
Normal file
591
src/modules/emblem/generator.ts
Normal 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();
|
||||
2
src/modules/emblem/index.ts
Normal file
2
src/modules/emblem/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import "./generator";
|
||||
import "./renderer";
|
||||
37
src/modules/emblem/lineWeights.ts
Normal file
37
src/modules/emblem/lineWeights.ts
Normal 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,
|
||||
};
|
||||
57
src/modules/emblem/lines.ts
Normal file
57
src/modules/emblem/lines.ts
Normal 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",
|
||||
};
|
||||
162
src/modules/emblem/ordinaries.ts
Normal file
162
src/modules/emblem/ordinaries.ts
Normal 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>;
|
||||
}
|
||||
>,
|
||||
};
|
||||
70
src/modules/emblem/paths.ts
Normal file
70
src/modules/emblem/paths.ts
Normal 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",
|
||||
};
|
||||
126
src/modules/emblem/patterns.ts
Normal file
126
src/modules/emblem/patterns.ts
Normal 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>`,
|
||||
};
|
||||
51
src/modules/emblem/positions.ts
Normal file
51
src/modules/emblem/positions.ts
Normal 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 },
|
||||
};
|
||||
363
src/modules/emblem/renderer.ts
Normal file
363
src/modules/emblem/renderer.ts
Normal 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();
|
||||
1347
src/modules/emblem/shieldPositions.ts
Normal file
1347
src/modules/emblem/shieldPositions.ts
Normal file
File diff suppressed because it is too large
Load diff
46
src/modules/emblem/shields.ts
Normal file
46
src/modules/emblem/shields.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
32
src/modules/emblem/size.ts
Normal file
32
src/modules/emblem/size.ts
Normal 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,
|
||||
};
|
||||
98
src/modules/emblem/templates.ts
Normal file
98
src/modules/emblem/templates.ts
Normal 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)"/>`,
|
||||
};
|
||||
43
src/modules/emblem/tinctures.ts
Normal file
43
src/modules/emblem/tinctures.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
187
src/modules/emblem/typeMapping.ts
Normal file
187
src/modules/emblem/typeMapping.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
@ -14,3 +14,7 @@ import "./zones-generator";
|
|||
import "./religions-generator";
|
||||
import "./labels";
|
||||
import "./provinces-generator";
|
||||
import "./emblem";
|
||||
import "./ice";
|
||||
import "./markers-generator";
|
||||
import "./fonts";
|
||||
|
|
|
|||
1802
src/modules/markers-generator.ts
Normal file
1802
src/modules/markers-generator.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
379
src/utils/probabilityUtils.test.ts
Normal file
379
src/utils/probabilityUtils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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!;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
221
tests/e2e/load-map.spec.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
349
tests/e2e/zones-export.spec.ts
Normal file
349
tests/e2e/zones-export.spec.ts
Normal 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
174
tests/fixtures/demo.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue