This commit is contained in:
Azgaar 2021-02-22 14:15:40 +03:00
commit d40251a4c7
30 changed files with 899 additions and 631 deletions

View file

@ -208,8 +208,12 @@
if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === "lake") return "Lake";
if (cells.h[i] > 60) return "Highland";
if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return "River";
if ([1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
if (!cells.burg[i] || pack.burgs[cells.burg[i]].population < 6) {
if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
}
return "Generic";
}
@ -419,7 +423,7 @@
const hull = getHull(start, s.i, s.cells / 10);
const points = [...hull].map(v => pack.vertices.p[v]);
const delaunay = Delaunator.from(points);
const voronoi = Voronoi(delaunay, points, points.length);
const voronoi = new Voronoi(delaunay, points, points.length);
const chain = connectCenters(voronoi.vertices, s.pole[1]);
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length);
paths.push([s.i, relaxed]);

View file

@ -11,7 +11,14 @@
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: 1, vair: 2, vairInPale: 1, vairEnPointe: 2, ermine: 2, chequy: 5, lozengy: 2, fusily: 1, pally: 4, barry: 4, gemelles: 1, bendy: 3, bendySinister: 2, palyBendy: 1, pappellony: 2, masoned: 3, fretty: 2 }
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 }
}
const charges = {
@ -23,7 +30,7 @@
conventional: {
lozenge: 2, fusil: 4, mascle: 4, rustre: 2, lozengeFaceted: 3, lozengePloye: 1, roundel: 4, roundel2: 3, annulet: 4,
mullet: 5, mulletPierced: 1, mulletFaceted: 1, mullet4: 3, mullet6: 4, mullet6Pierced: 1, mullet6Faceted: 1, mullet7: 1, mullet8: 1, mullet10: 1,
estoile: 1, compassRose: 1, billet: 5, delf: 0, triangle: 3, trianglePierced: 1, goutte: 4, heart: 4, pique: 2, сarreau: 1, trefle: 2,
estoile: 1, compassRose: 1, billet: 5, delf: 0, triangle: 3, trianglePierced: 1, goutte: 4, heart: 4, pique: 2, carreau: 1, trefle: 2,
fleurDeLis: 6, sun: 3, sunInSplendour: 1, crescent: 5, fountain: 1
},
crosses: {
@ -189,6 +196,18 @@
}
};
const shields = {
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}
}
const generate = function(parent, kinship, dominion, type) {
if (parent === "custom") parent = null;
let usedPattern = null, usedTinctures = [];
@ -398,10 +417,10 @@
function definePattern(pattern, element, size = "") {
let t1 = null, t2 = null;
if (P(.15)) size = "-small";
else if (P(.05)) size = "-smaller";
else if (P(.035)) size = "-big";
else if (P(.001)) size = "-smallest";
if (P(.1)) size = "-small";
else if (P(.1)) size = "-smaller";
else if (P(.01)) size = "-big";
else if (P(.005)) size = "-smallest";
// apply standard tinctures
if (P(.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {t1 = "azure"; t2 = "argent";}
@ -464,16 +483,19 @@
}
const getShield = function(culture, state) {
const emblemShape = document.getElementById("emblemShape").value;
if (emblemShape === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
const emblemShape = document.getElementById("emblemShape");
const shapeGroup = emblemShape.selectedOptions[0].parentNode.label;
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;
console.error("Emblem shape is not defined on culture level", pack.cultures[culture]);
console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
return "heater";
}
const toString = coa => JSON.stringify(coa).replaceAll("#", "%23");
const copy = coa => JSON.parse(JSON.stringify(coa));
return {generate, toString, copy, getShield};
return {generate, toString, copy, getShield, shields};
})));

View file

@ -756,72 +756,112 @@
}
const templates = {
// divisions
perFess: line => `<path d="${line}"/><rect x="0" y="115" width="200" height="85"/>`,
perPale: line => `<path d="${line}" transform="rotate(-90)" transform-origin="center"/><rect x="115" y="0" width="85" height="200"/>`,
perBend: line => `<path d="${line}" transform="rotate(45) scale(1.1)" transform-origin="center"/><rect x="0" y="115" width="200" height="85" transform="rotate(45) scale(1.1)" transform-origin="center"/>`,
perBendSinister: line => `<path d="${line}" transform="rotate(-45) scale(1.1)" transform-origin="center"/><rect x="0" y="115" width="200" height="85" transform="rotate(-45) scale(1.1)" transform-origin="center"/>`,
perChevron: line => `<path d="${line}" transform="translate(-70.7,70.7) rotate(-45) scale(-1,1)" transform-origin="center"/><polygon points="20,200 100,120 180,200"/><path d="${line}" transform="translate(70.7,70.7) rotate(45)" transform-origin="center"/>`,
perChevronReversed: line => `<path d="${line}" transform="translate(-70.7,-70.7) rotate(225) scale(1,1)" transform-origin="center"/><polygon points="21,0 100,79 179,0"/><path d="${line}" transform="translate(70.7,-70.7) rotate(-225) scale(-1,1)" transform-origin="center"/>`,
perCross: line => `<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,.5001)"/><path d="${line}" transform="translate(50,0) scale(-.5001,-.5001)" transform-origin="center"/>`,
perPile: line => `<path d="${line}" transform="translate(-35,15) rotate(66.8) scale(-1,1)" transform-origin="center"/><path d="${line}" transform="translate(35,15) rotate(-66.8)" transform-origin="center"/><polygon points="0,0 86,200 114,200 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"/>`,
// oprinaries
fess: line => `<path d="${line}" transform="translate(0,-25)"/><path d="${line}" transform="translate(0,25) rotate(180.00001)" transform-origin="center"/><rect x="0" y="88" width="200" height="24" stroke="none"/>`,
pale: line => `<path d="${line}" transform="rotate(-90) translate(0,-25)" transform-origin="center"/><path d="${line}" transform="rotate(90) translate(0,-25)" transform-origin="center"/><rect x="88" y="0" width="24" height="200" stroke="none"/>`,
bend: line => `<path d="${line}" transform="rotate(45) translate(0,-25) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(225) translate(0,-25) scale(1.1,1)" transform-origin="center"/><rect x="0" y="88" width="200" height="24" transform="rotate(45) scale(1.1,1)" transform-origin="center" stroke="none"/>`,
bendSinister: line => `<path d="${line}" transform="rotate(-45) translate(0,-25) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(-225) translate(0,-25) scale(1.1,1)" transform-origin="center"/><rect x="0" y="88" width="200" height="24" transform="rotate(-45) scale(1.1,1)" transform-origin="center" stroke="none"/>`,
chief: line => `<path d="${line}" transform="translate(0,-25) rotate(180.00001)" transform-origin="center"/><rect x="0" y="0" width="200" height="62" stroke="none"/>`,
bar: line => `<path d="${line}" transform="translate(0,-12.5)" transform-origin="center"/><path d="${line}" transform="translate(0,12.5) rotate(180.00001)" transform-origin="center"/><rect x="0" y="94" width="200" height="12" stroke="none"/>`,
gemelle: line => `<path d="${line}" transform="translate(0,-22.5)"/><path d="${line}" transform="translate(0,22.5) rotate(180.00001)" transform-origin="center"/>`,
fessCotissed: line => `<path d="${line}" transform="translate(0,-35) scale(1,.5)" transform-origin="center"/><path d="${line}" transform="translate(0,35) rotate(180.0001) scale(1,.5)" transform-origin="center"/><rect x="0" y="80" width="200" height="40"/>`,
fessDoubleCotissed: line => `<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,-40) scale(1,.5)" transform-origin="center"/><path d="${line}" transform="translate(0,40) rotate(180.0001) scale(1,.5)" transform-origin="center"/>`,
bendlet: line => `<path d="${line}" transform="rotate(45) translate(0,-16) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(225) translate(0,-16) scale(1.1,1)" transform-origin="center"/><rect x="0" y="94" width="200" height="12" transform="rotate(45) scale(1.1,1)" transform-origin="center" stroke="none"/>`,
bendletSinister: line => `<path d="${line}" transform="rotate(-45) translate(0,-16) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(-225) translate(0,-16) scale(1.1,1)" transform-origin="center"/><rect x="0" y="94" width="200" height="12" transform="rotate(-45) scale(1.1,1)" transform-origin="center" stroke="none"/>`,
terrace: line => `<path d="${line}" transform="translate(0,50)"/><rect x="0" y="164" width="200" height="36" stroke="none"/>`,
cross: line => `<path d="${line}" transform="translate(0,-14.5)" transform-origin="center"/><path d="${line}" transform="rotate(180) translate(0,-14.5)" transform-origin="center"/><path d="${line}" transform="rotate(-90) translate(0,-14.5)" transform-origin="center"/><path d="${line}" transform="rotate(-270) translate(0,-14.5)" transform-origin="center"/>`,
crossParted: line => `<path d="${line}" transform="translate(0,-20)" transform-origin="center"/><path d="${line}" transform="rotate(180) translate(0,-20)" transform-origin="center"/><path d="${line}" transform="rotate(-90) translate(0,-20)" transform-origin="center"/><path d="${line}" transform="rotate(-270) translate(0,-20)" transform-origin="center"/>`,
saltire: line => `<path d="${line}" transform="rotate(45) translate(0,-14.5) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(225) translate(0,-14.5) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(-45) translate(0,-14.5) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(-225) translate(0,-14.5) scale(1.1,1)" transform-origin="center"/>`,
saltireParted: line => `<path d="${line}" transform="rotate(45) translate(0,-20) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(225) translate(0,-20) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(-45) translate(0,-20) scale(1.1,1)" transform-origin="center"/><path d="${line}" transform="rotate(-225) translate(0,-20) scale(1.1,1)" transform-origin="center"/>`,
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 x="0" y="0" width="50%" height="50%"/>`,
canton: () => `<rect x="0" y="0" 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"/>`
// 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 => `<path d="${line}"/><rect x="0" y="115" width="200" height="85" shape-rendering="crispedges"/>`,
perPaleLined: line => `<path d="${line}" transform="rotate(-90 100 100)"/><rect x="115" y="0" width="85" height="200" shape-rendering="crispedges"/>`,
perBendLined: line => `<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 => `<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 => `<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 => `<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 => `<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 => `<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 => `<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 => `<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 => `<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 => `<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 => `<path d="${line}" transform="translate(0,-25) rotate(180.00001 100 100)"/><rect width="200" height="62" stroke="none"/>`,
barLined: line => `<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 => `<path d="${line}" transform="translate(0,-22.5)"/><path d="${line}" transform="translate(0,22.5) rotate(180.00001 100 100)"/>`,
fessCotissedLined: line => `<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 => `<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 => `<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 => `<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 => `<path d="${line}" transform="translate(0,50)"/><rect x="0" y="164" width="200" height="36" stroke="none"/>`,
crossLined: line => `<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 => `<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 => `<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 => `<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)"/>`
}
const patterns = {
semy: (p, c1, c2, size, chargeId) => `<pattern id="${p}" width="${size * .134}" height="${size * .1787}" viewBox="0 0 150 200" stroke="#000"><rect x="0" y="0" width="150" height="200" fill="${c1}" stroke="none"/><g fill="${c2}"><g transform="translate(-60,-50)"><use href="#${chargeId}"/></g><g transform="translate(10,50)"><use href="#${chargeId}"/></g></g></pattern>`,
vair: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .25}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect x="0" y="0" width="25" height="25" fill="${c2}" 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="${c1}"/><rect x="0" y="25" width="25" height="25" fill="${c1}" stroke-width="1" 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="${c2}"/></pattern>`,
vairInPale: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .125}" viewBox="0 0 25 25"><rect x="0" y="0" 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}" stroke="#000" stroke-width=".2"/></pattern>`,
vairEnPointe: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .25}" viewBox="0 0 25 50"><rect x="0" y="0" 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>`,
ermine: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .125}" viewBox="0 0 25 25" fill="${c2}"><rect x="0" y="0" width="25" height="25" fill="${c1}"/><path d="m19.1,14.8 c-0.7,2.9 -2.1,5 -3.5,6.5 0.6,-0.1 1.3,-0.6 2,-0.9 -0.4,0.8 -0.8,1.4 -1.2,2.1 0.2,-0.1 1,-0.8 2,-1.8 0.2,1.4 0.4,2.9 0.7,3.9 0.3,-0.9 0.5,-2.5 0.7,-3.9 0.6,0.6 1.2,1.3 2.1,1.8 l -1.2,-2.2 c 0.6,0.3 1.3,0.8 1.9,1 -1.5,-1.6 -2.8,-3.6 -3.5,-6.5z"/><path d="m16.1,14.9 c-0.1,-0.2 -1,0.4 -1.5,-0.8 1.2,1.1 2.5,-1.2 3.5,0.4 0.3,0.7 -1.1,1.8 -2,0.4z"/><path d="m21.9,14.9 c.1,-.2 1,0.4 1.5,-0.8 -1.2,1.1 -2.5,-1.2 -3.5,0.4 -0.3,0.7 1.1,1.8 2,0.4z"/><path d="m19.4,12.4 c-0.2,-0.1 0.7,-0.7 -0.6,-1.4 1.1,1.2 -2,1.7 -0.3,2.9 0.7,0.4 2.4,-0.5 0.9,-1.5z"/><path d="M5.8,4.6 C5.1,7.5 3.7,9.5 2.3,11 2.9,10.9 3.6,10.5 4.2,10.1 3.8,10.9 3.4,11.5 3,12.2 3.3,12.1 4,11.4 5.1,10.4 c 0.2,1.4 0.4,2.9 0.7,3.9 0.3,-0.9 0.5,-2.5 0.7,-3.9 0.6,0.6 1.2,1.3 2.1,1.8 L 7.3,10 c 0.6,0.3 1.3,0.8 1.9,1 C7.7,9.5 6.4,7.5 5.8,4.6Z"/><path d="M2.9,4.7 C2.8,4.6 1.9,5.1 1.3,4 2.6,5.1 3.8,2.8 4.9,4.3 5.2,5 3.8,6.1 2.9,4.7Z"/><path d="M8.6,4.7 C8.7,4.5 9.6,5.1 10.1,3.9 8.9,5.1 7.6,2.7 6.6,4.3 6.3,5 7.7,6.1 8.6,4.7Z"/><path d="M6.1,2.2 C 5.9,2.1 6.8,1.5 5.5,0.8 6.6,2.1 3.5,2.6 5.2,3.7 5.9,4.1 7.6,3.3 6.1,2.2Z"/></pattern>`,
chequy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .25}" height="${size * .25}" viewBox="0 0 50 50" fill="${c2}"><rect x="0" y="0" width="50" height="50"/><rect x="0" y="0" width="25" height="25" fill="${c1}"/><rect x="25" y="25" width="25" height="25" fill="${c1}"/></pattern>`,
lozengy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .125}" viewBox="0 0 50 50"><rect x="0" y="0" width="50" height="50" fill="${c1}"/><polygon points="25,0 50,25 25,50 0,25" fill="${c2}"/></pattern>`,
fusily: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .25}" viewBox="0 0 50 100"><rect x="0" y="0" width="50" height="100" fill="${c1}"/><polygon points="25,0 50,50 25,100 0,50" fill="${c2}"/></pattern>`,
pally: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .5}" height="${size * .125}" viewBox="0 0 100 25"><rect x="0" y="0" width="100" height="25" fill="${c1}"/><rect x="25" y="0" width="25" height="25" fill="${c2}"/><rect x="75" y="0" width="25" height="25" fill="${c2}"/></pattern>`,
barry: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .5}" viewBox="0 0 25 100"><rect x="0" y="0" 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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .5}" viewBox="0 0 25 100"><rect x="0" y="0" width="25" height="100" fill="${c2}"/><rect x="0" y="35" width="25" height="10" fill="${c1}"/><rect x="0" y="55" width="25" height="10" fill="${c1}"/></pattern>`,
bendy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .36}" height="${size * .36}" viewBox="0 0 50 50" patternTransform="rotate(45)"><rect x="0" y="0" width="50" height="50" fill="${c2}"/><line x1="0" y1="37.5" x2="50" y2="37.5" stroke="${c1}" stroke-width="25"/></pattern>`,
bendySinister: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .36}" height="${size * .36}" viewBox="0 0 50 50" patternTransform="rotate(-45)"><rect x="0" y="0" width="50" height="50" fill="${c2}"/><line x1="0" y1="37.5" x2="50" y2="37.5" stroke="${c1}" stroke-width="25"/></pattern>`,
palyBendy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .25}" viewBox="0 0 50 100" patternTransform="translate(22,44) rotate(-26.5)"><rect x="0" y="0" width="50" height="100" fill="${c1}"/><polygon points="25,0 50,50 25,100 0,50" fill="${c2}"/></pattern>`,
pappellony: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .125}" viewBox="0 0 100 100"><rect x="0" y="0" 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>`,
masoned: (p, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .125}" viewBox="0 0 100 100" fill="none"><rect x="0" y="0" width="100" height="100" fill="${c1}"/><rect x="0" y="0" 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, c1, c2, size) => `<pattern id="${p}" width="${size * .28}" height="${size * .28}" viewBox="0 0 200 200" patternTransform="translate(-19,21) rotate(45)" stroke="#000" stroke-width="2"><rect x="0" y="0" width="200" height="200" stroke="none" fill="${c1}"/><rect x="0" y="35" width="200" height="30" stroke="none" fill="${c2}"/><rect x="0" y="135" width="200" height="30" stroke="none" fill="${c2}"/><rect x="35" y="0" width="30" height="200" stroke="none" fill="${c2}"/><rect x="135" y="0" width="30" height="200" stroke="none" fill="${c2}"/><line x1="0" y1="35" x2="35" y2="35"/><line x1="0" y1="65" x2="35" y2="65"/><line x1="35" y1="165" x2="35" y2="200"/><line x1="65" y1="165" x2="65" y2="200"/><line x1="135" y1="0" x2="135" y2="35"/><line x1="165" y1="0" x2="165" y2="35"/><line x1="135" y1="65" x2="135" y2="200"/><line x1="165" y1="65" x2="165" y2="200"/><line x1="35" y1="0" x2="35" y2="135"/><line x1="65" y1="0" x2="65" y2="135"/><line x1="65" y1="35" x2="200" y2="35"/><line x1="65" y1="65" x2="200" y2="65"/><line x1="0" y1="135" x2="135" y2="135"/><line x1="0" y1="165" x2="135" y2="165"/><line x1="165" y1="135" x2="200" y2="135"/><line x1="165" y1="165" x2="200" y2="165"/></pattern>`
semy: (p, c1, c2, size, chargeId) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .25}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .5}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .5}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .5}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .6258}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .3572}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .125}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .2}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .25}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .167}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .167}" height="${size * .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, c1, c2, size) => `<pattern id="${p}" width="${size * .143}" height="${size * .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>`
}
const draw = async function(id, coa) {
@ -839,11 +879,7 @@
const divisionClip = division ? `<clipPath id="divisionClip_${id}">${getTemplate(division.division, division.line)}</clipPath>` : "";
const loadedCharges = await getCharges(coa, id, shieldPath);
const loadedPatterns = 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 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="${clr(coa.t1)}"/>`;
const divisionGroup = division ? templateDivision() : "";
const overlay = `<path d="${shieldPath}" fill="url(#backlight_${id})" stroke="#333"/>`;
@ -855,6 +891,7 @@
// insert coa svg to defs
document.getElementById("coas").insertAdjacentHTML("beforeend", svg);
return true;
function templateDivision() {
let svg = "";
@ -1000,17 +1037,18 @@
}
function getSizeMod(size) {
if (size === "small") return .5;
if (size === "smaller") return .25;
if (size === "smallest") return .125;
if (size === "big") return 2;
if (size === "small") return .8;
if (size === "smaller") return .5;
if (size === "smallest") return .25;
if (size === "big") return 1.6;
return 1;
}
function getTemplate(templateId, lineId) {
if (!lineId) return templates[templateId]();
const line = lines[lineId] || lines.straight;
return templates[templateId](line);
function getTemplate(id, line) {
const linedId = id+"Lined";
if (!line || line === "straight" || !templates[linedId]) return templates[id];
const linePath = lines[line];
return templates[linedId](linePath);
}
// get color or link to pattern
@ -1027,7 +1065,7 @@
}
// render coa if does not exist
const trigger = function(id, coa) {
const trigger = async function(id, coa) {
if (coa === "custom") {
console.warn("Cannot render custom emblem", coa);
return;
@ -1036,7 +1074,7 @@
console.warn(`Emblem ${id} is undefined`);
return;
}
if (!document.getElementById(id)) draw(id, coa);
if (!document.getElementById(id)) return draw(id, coa);
}
const add = function(type, i, coa, x, y) {
@ -1051,6 +1089,6 @@
if (layerIsOn("toggleEmblems")) trigger(id, coa);
}
return {trigger, add};
return {trigger, add, shieldPaths};
})));

View file

@ -55,26 +55,9 @@
c.origin = 0;
c.code = getCode(c.name);
cells.culture[cell] = i+1;
if (emblemShape === "random") c.shield = getRandomShiled();
else if (emblemShape !== "culture" && emblemShape !== "state") c.shield = emblemShape;
if (emblemShape === "random") c.shield = getRandomShield();
});
function getRandomShiled() {
const shields = {
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}
}
const type = rw(shields.types);
return rw(shields[type]);
}
function placeCenter(v) {
let c, spacing = (graphWidth + graphHeight) / 2 / count;
const sorted = [...populated].sort((a, b) => v(b) - v(a)), max = Math.floor(sorted.length / 2);
@ -161,7 +144,13 @@
const code = getCode(name);
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0, origin:0, code});
// define emblem shape
let shield = culture.shield;
const emblemShape = document.getElementById("emblemShape").value;
if (emblemShape === "random") shield = getRandomShield();
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0, origin:0, code, shield});
}
const getDefault = function(count) {
@ -428,6 +417,11 @@
return 0;
}
return {generate, add, expand, getDefault};
const getRandomShield = function() {
const type = rw(COA.shields.types);
return rw(COA.shields[type]);
}
return {generate, add, expand, getDefault, getRandomShield};
})));

View file

@ -1,296 +1,376 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Rivers = factory());
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Rivers = factory());
}(this, (function () {'use strict';
const generate = function(changeHeights = true) {
TIME && console.time('generateRivers');
Math.random = aleaPRNG(seed);
const cells = pack.cells, p = cells.p, features = pack.features;
const generate = function(changeHeights = true) {
TIME && console.time('generateRivers');
Math.random = aleaPRNG(seed);
const cells = pack.cells, p = cells.p, features = pack.features;
// build distance field in cells from water (cells.t)
void function markupLand() {
const q = t => cells.i.filter(i => cells.t[i] === t);
for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) {
queue.forEach(i => cells.c[i].forEach(c => {
if (!cells.t[c]) cells.t[c] = t+1;
}));
// build distance field in cells from water (cells.t)
void function markupLand() {
const q = t => cells.i.filter(i => cells.t[i] === t);
for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) {
queue.forEach(i => cells.c[i].forEach(c => {
if (!cells.t[c]) cells.t[c] = t+1;
}));
}
}()
// height with added t value to make map less depressed
const h = Array.from(cells.h)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
resolveDepressions(h);
features.forEach(f => {delete f.river; delete f.flux; delete f.inlets});
const riversData = []; // rivers data
cells.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array
let riverNext = 1; // first river id is 1, not 0
void function drainWater() {
const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
const outlets = new Uint32Array(features.length);
// enumerate lake outlet positions
features.filter(f => f.type === "lake" && (f.group === "freshwater" || f.group === "frozen")).forEach(l => {
let outlet = 0;
if (l.shoreline) {
outlet = l.shoreline[d3.scan(l.shoreline, (a,b) => h[a] - h[b])];
} else { // in case it got missed or deleted
WARN && console.warn('Re-scanning shoreline of a lake');
const shallows = cells.i.filter(j => cells.t[j] === -1 && cells.f[j] === l.i);
let shoreline = [];
shallows.map(w => cells.c[w]).forEach(cList => cList.forEach(s => shoreline.push(s)));
outlet = shoreline[d3.scan(shoreline, (a,b) => h[a] - h[b])];
}
}()
outlets[l.i] = outlet;
delete l.shoreline // cleanup temp data once used
});
// height with added t value to make map less depressed
const h = Array.from(cells.h)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
resolveDepressions(h);
features.forEach(f => {delete f.river; delete f.flux;});
const riversData = []; // rivers data
cells.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array
let riverNext = 1; // first river id is 1, not 0
void function drainWater() {
const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
land.forEach(function(i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
const x = p[i][0], y = p[i][1];
// near-border cell: pour out of the screen
if (cells.b[i]) {
if (cells.r[i]) {
const to = [];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) {to[0] = x; to[1] = 0;} else
if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
if (min === x) {to[0] = 0; to[1] = y;} else
if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
}
return;
}
//const min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
let min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
// allow only one river can flow through a lake
const cf = features[cells.f[i]]; // current cell feature
if (cf.river && cf.river !== cells.r[i]) {
cells.fl[i] = 0;
}
if (cells.fl[i] < 30) {
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
return; // flux is too small to operate as river
}
// Proclaim a new river
if (!cells.r[i]) {
cells.r[i] = riverNext;
riversData.push({river: riverNext, cell: i, x, y});
riverNext++;
}
if (cells.r[min]) { // downhill cell already has river assigned
if (cells.fl[min] < cells.fl[i]) {
cells.conf[min] = cells.fl[min]; // mark confluence
if (h[min] >= 20) riversData.find(r => r.river === cells.r[min]).parent = cells.r[i]; // min river is a tributary of current river
cells.r[min] = cells.r[i]; // re-assign river if downhill part has less flux
} else {
cells.conf[min] += cells.fl[i]; // mark confluence
if (h[min] >= 20) riversData.find(r => r.river === cells.r[i]).parent = cells.r[min]; // current river is a tributary of min river
}
} else cells.r[min] = cells.r[i]; // assign the river to the downhill cell
const nx = p[min][0], ny = p[min][1];
if (h[min] < 20) {
// pour water to the sea haven
riversData.push({river: cells.r[i], cell: cells.haven[i], x: nx, y: ny});
const flowDown = function(min, mFlux, iFlux, ri, i = 0){
if (cells.r[min]) { // downhill cell already has river assigned
if (mFlux < iFlux) {
cells.conf[min] = cells.fl[min]; // mark confluence
if (h[min] >= 20) riversData.find(r => r.river === cells.r[min]).parent = ri; // min river is a tributary of current river
cells.r[min] = ri; // re-assign river if downhill part has less flux
} else {
const mf = features[cells.f[min]]; // feature of min cell
if (mf.type === "lake") {
if (!mf.river || cells.fl[i] > mf.flux) {
mf.river = cells.r[i]; // pour water to temporaly elevated lake
mf.flux = cells.fl[i]; // entering flux
cells.conf[min] += iFlux; // mark confluence
if (h[min] >= 20) riversData.find(r => r.river === ri).parent = cells.r[min]; // current river is a tributary of min river
}
} else cells.r[min] = ri; // assign the river to the downhill cell
if (h[min] < 20) {
// pour water to the sea haven
const oh = i ? cells.haven[i] : min;
riversData.push({river: ri, cell: oh, x: p[min][0], y: p[min][1]});
const mf = features[cells.f[min]]; // feature of min cell
if (mf.type === "lake") {
if (!mf.river || iFlux > mf.flux) {
mf.river = ri; // pour water to temporaly elevated lake
mf.flux = iFlux; // entering flux
}
mf.totalFlux += iFlux;
if (mf.inlets) {
mf.inlets.push(ri);
} else {
mf.inlets = [ri];
}
}
} else {
cells.fl[min] += iFlux; // propagate flux
riversData.push({river: ri, cell: min, x: p[min][0], y: p[min][1]}); // add next River segment
}
}
land.forEach(function(i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
const x = p[i][0], y = p[i][1];
// lake outlets draw from lake
let n = -1, out2 = 0;
while (outlets.includes(i, n+1)) {
n = outlets.indexOf(i, n+1);
const l = features[n];
if ( ! l ) {continue;}
const j = cells.haven[i];
// allow chain lakes to retain identity
if(cells.r[j] !== l.river) {
let touch = false;
for (const c of cells.c[j]){
if (cells.r[c] === l.river) {
touch = true;
break;
}
}
cells.fl[min] += cells.fl[i]; // propagate flux
riversData.push({river: cells.r[i], cell: min, x: nx, y: ny}); // add next River segment
}
});
}()
void function defineRivers() {
pack.rivers = []; // rivers data
const riverPaths = []; // temporary data for all rivers
for (let r = 1; r <= riverNext; r++) {
const riverSegments = riversData.filter(d => d.river === r);
if (riverSegments.length > 2) {
const riverEnhanced = addMeandring(riverSegments);
const width = rn(.8 + Math.random() * .4, 1); // river width modifier
const increment = rn(.8 + Math.random() * .6, 1); // river bed widening modifier
const [path, length] = getPath(riverEnhanced, width, increment);
riverPaths.push([r, path, width, increment]);
const source = riverSegments[0], mouth = riverSegments[riverSegments.length-2];
const parent = source.parent || 0;
pack.rivers.push({i:r, parent, length, source:source.cell, mouth:mouth.cell});
} else {
// remove too short rivers
riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
if (touch) {
cells.r[j] = l.river;
riversData.push({river: l.river, cell: j, x: p[j][0], y: p[j][1]});
} else {
cells.r[j] = riverNext;
riversData.push({river: riverNext, cell: j, x: p[j][0], y: p[j][1]});
riverNext++;
}
}
cells.fl[j] = l.totalFlux; // signpost river size
flowDown(i, cells.fl[i], l.totalFlux, cells.r[j]);
// prevent dropping imediately back into the lake
out2 = cells.c[i].filter(c => (h[c] >= 20 || cells.f[c] !== cells.f[j])).sort((a,b) => h[a] - h[b])[0]; // downhill cell not in the source lake
// assign all to outlet basin
if (l.inlets) l.inlets.forEach(fork => riversData.find(r => r.river === fork).parent = cells.r[j]);
}
const html = riverPaths.map(r =>`<path id="river${r[0]}" d="${r[1]}" data-width="${r[2]}" data-increment="${r[3]}"/>`).join("");
rivers.html(html);
}()
// apply change heights as basic one
if (changeHeights) cells.h = Uint8Array.from(h);
TIME && console.timeEnd('generateRivers');
}
// depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function(h) {
const cells = pack.cells;
const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
land.sort((a,b) => h[b] - h[a]); // highest cells go first
let depressed = false;
for (let l = 0, depression = Infinity; depression && l < 100; l++) {
depression = 0;
for (const i of land) {
const minHeight = d3.min(cells.c[i].map(c => h[c]));
if (minHeight === 100) continue; // already max height
if (h[i] <= minHeight) {
h[i] = Math.min(minHeight + 1, 100);
depression++;
depressed = true;
// near-border cell: pour out of the screen
if (cells.b[i]) {
if (cells.r[i]) {
const to = [];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) {to[0] = x; to[1] = 0;} else
if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
if (min === x) {to[0] = 0; to[1] = y;} else
if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
}
return;
}
const min = out2 ? out2 : cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
if (cells.fl[i] < 30) {
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
return; // flux is too small to operate as river
}
// Proclaim a new river
if (!cells.r[i]) {
cells.r[i] = riverNext;
riversData.push({river: riverNext, cell: i, x, y});
riverNext++;
}
flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i);
});
}()
void function defineRivers() {
pack.rivers = []; // rivers data
const riverPaths = []; // temporary data for all rivers
for (let r = 1; r <= riverNext; r++) {
const riverSegments = riversData.filter(d => d.river === r);
if (riverSegments.length > 2) {
const source = riverSegments[0], mouth = riverSegments[riverSegments.length-2];
const riverEnhanced = addMeandring(riverSegments);
let width = rn(.8 + Math.random() * .4, 1); // river width modifier [.2, 10]
let increment = rn(.8 + Math.random() * .6, 1); // river bed widening modifier [.01, 3]
const [path, length] = getPath(riverEnhanced, width, increment, cells.h[source.cell] >= 20 ? .1 : .6);
riverPaths.push([r, path, width, increment]);
const parent = source.parent || 0;
pack.rivers.push({i:r, parent, length, source:source.cell, mouth:mouth.cell});
} else {
// remove too short rivers
riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
}
}
return depressed;
}
// drawRivers
rivers.selectAll("path").remove();
rivers.selectAll("path").data(riverPaths).enter()
.append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0])
.attr("data-width", d => d[2]).attr("data-increment", d => d[3]);
}()
// add more river points on 1/3 and 2/3 of length
const addMeandring = function(segments, rndFactor = 0.3) {
const riverEnhanced = []; // to store enhanced segments
let side = 1; // to control meandring direction
// apply change heights as basic one
if (changeHeights) cells.h = Uint8Array.from(h);
for (let s = 0; s < segments.length; s++) {
const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
riverEnhanced.push([sX, sY, c]);
TIME && console.timeEnd('generateRivers');
}
if (s+1 === segments.length) break; // do not enhance last segment
const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
const angle = Math.atan2(eY - sY, eX - sX);
const sin = Math.sin(angle), cos = Math.cos(angle);
const serpentine = 1 / (s + 1) + 0.3;
const meandr = serpentine + Math.random() * rndFactor;
if (P(.5)) side *= -1; // change meandring direction in 50%
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
if (P(.2)) side *= -1; // change 2nd extra point meandring direction in 20%
const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
riverEnhanced.push([p1x, p1y], [p2x, p2y]);
// if dist is medium or river is small add 1 extra middlepoint
} else if (dist2 > 16 || segments.length < 6) {
const p1x = (sX + eX) / 2 + side * -sin * meandr;
const p1y = (sY + eY) / 2 + side * cos * meandr;
riverEnhanced.push([p1x, p1y]);
// depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function(h) {
const cells = pack.cells;
const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
const lakes = pack.features.filter(f => f.type === "lake" && (f.group === "freshwater" || f.group === "frozen")); // to keep lakes flat
lakes.forEach(l => {
l.shoreline = [];
l.height = 21;
l.totalFlux = grid.cells.prec[cells.g[l.firstCell]];
});
for (let i of land.filter(i => cells.t[i] === 1)) { // select shoreline cells
cells.c[i].map(c => pack.features[cells.f[c]]).forEach(cf => {
if (lakes.includes(cf) && !cf.shoreline.includes(i)) {
cf.shoreline.push(i);
}
})
}
land.sort((a,b) => h[b] - h[a]); // highest cells go first
let depressed = false;
for (let l = 0, depression = Infinity; depression && l < 100; l++) {
depression = 0;
for (const l of lakes) {
const minHeight = d3.min(l.shoreline.map(s => h[s]));
if (minHeight === 100) continue; // already max height
if (l.height <= minHeight) {
l.height = Math.min(minHeight + 1, 100);
depression++;
depressed = true;
}
}
for (const i of land) {
const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] :
pack.features[cells.f[c]].height || h[c] // NB undefined is falsy (a || b is short for a ? a : b)
));
if (minHeight === 100) continue; // already max height
if (h[i] <= minHeight) {
h[i] = Math.min(minHeight + 1, 100);
depression++;
depressed = true;
}
}
return riverEnhanced;
}
const getPath = function(points, width = 1, increment = 1) {
let offset, extraOffset = .1; // starting river width (to make river source visible)
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i-1][0], v[1] - p[i-1][1]) : 0), 0); // summ of segments length
const widening = rn((1000 + (riverLength * 30)) * increment);
const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
const last = points.length - 1;
const factor = riverLength / points.length;
return depressed;
}
// first point
let x = points[0][0], y = points[0][1], c;
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
let sin = Math.sin(angle), cos = Math.cos(angle);
let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
riverPointsLeft.push([xLeft, yLeft]);
let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
riverPointsRight.unshift([xRight, yRight]);
// add more river points on 1/3 and 2/3 of length
const addMeandring = function(segments, rndFactor = 0.3) {
const riverEnhanced = []; // to store enhanced segments
let side = 1; // to control meandring direction
// middle points
for (let p = 1; p < last; p++) {
x = points[p][0], y = points[p][1], c = points[p][2] || 0;
const xPrev = points[p-1][0], yPrev = points[p - 1][1];
const xNext = points[p+1][0], yNext = points[p + 1][1];
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
sin = Math.sin(angle), cos = Math.cos(angle);
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
const confOffset = Math.atan(c * 5 / widening);
extraOffset += confOffset;
xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
riverPointsLeft.push([xLeft, yLeft]);
xRight = x + sin * offset, yRight = y + -cos * offset;
riverPointsRight.unshift([xRight, yRight]);
for (let s = 0; s < segments.length; s++) {
const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
riverEnhanced.push([sX, sY, c]);
if (s+1 === segments.length) break; // do not enhance last segment
const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
const angle = Math.atan2(eY - sY, eX - sX);
const sin = Math.sin(angle), cos = Math.cos(angle);
const serpentine = 1 / (s + 1) + 0.3;
const meandr = serpentine + Math.random() * rndFactor;
if (P(.5)) side *= -1; // change meandring direction in 50%
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
if (P(.2)) side *= -1; // change 2nd extra point meandring direction in 20%
const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
riverEnhanced.push([p1x, p1y], [p2x, p2y]);
// if dist is medium or river is small add 1 extra middlepoint
} else if (dist2 > 16 || segments.length < 6) {
const p1x = (sX + eX) / 2 + side * -sin * meandr;
const p1y = (sY + eY) / 2 + side * cos * meandr;
riverEnhanced.push([p1x, p1y]);
}
// end point
x = points[last][0], y = points[last][1], c = points[last][2];
if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
}
return riverEnhanced;
}
const getPath = function(points, width = 1, increment = 1, starting = .1) {
let offset, extraOffset = starting; // starting river width (to make river source visible)
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i-1][0], v[1] - p[i-1][1]) : 0), 0); // summ of segments length
const widening = rn((1000 + (riverLength * 30)) * increment);
const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
const last = points.length - 1;
const factor = riverLength / points.length;
// first point
let x = points[0][0], y = points[0][1], c;
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
let sin = Math.sin(angle), cos = Math.cos(angle);
let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
riverPointsLeft.push([xLeft, yLeft]);
let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
riverPointsRight.unshift([xRight, yRight]);
// middle points
for (let p = 1; p < last; p++) {
x = points[p][0], y = points[p][1], c = points[p][2] || 0;
const xPrev = points[p-1][0], yPrev = points[p - 1][1];
const xNext = points[p+1][0], yNext = points[p + 1][1];
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
sin = Math.sin(angle), cos = Math.cos(angle);
xLeft = x + -sin * offset, yLeft = y + cos * offset;
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
const confOffset = Math.atan(c * 5 / widening);
extraOffset += confOffset;
xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
riverPointsLeft.push([xLeft, yLeft]);
xRight = x + sin * offset, yRight = y + -cos * offset;
riverPointsRight.unshift([xRight, yRight]);
// generate polygon path and return
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const right = lineGen(riverPointsRight);
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
return [round(right + left, 2), rn(riverLength, 2)];
}
const specify = function() {
if (!pack.rivers.length) return;
Math.random = aleaPRNG(seed);
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
// end point
x = points[last][0], y = points[last][1], c = points[last][2];
if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
sin = Math.sin(angle), cos = Math.cos(angle);
xLeft = x + -sin * offset, yLeft = y + cos * offset;
riverPointsLeft.push([xLeft, yLeft]);
xRight = x + sin * offset, yRight = y + -cos * offset;
riverPointsRight.unshift([xRight, yRight]);
for (const r of pack.rivers) {
r.basin = getBasin(r.i, r.parent);
r.name = getName(r.mouth);
//debug.append("circle").attr("cx", pack.cells.p[r.mouth][0]).attr("cy", pack.cells.p[r.mouth][1]).attr("r", 2);
const small = r.length < smallLength;
r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
}
// generate polygon path and return
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const right = lineGen(riverPointsRight);
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
return [round(right + left, 2), rn(riverLength, 2)];
}
const specify = function() {
if (!pack.rivers.length) return;
Math.random = aleaPRNG(seed);
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
for (const r of pack.rivers) {
r.basin = getBasin(r.i, r.parent);
r.name = getName(r.mouth);
//debug.append("circle").attr("cx", pack.cells.p[r.mouth][0]).attr("cy", pack.cells.p[r.mouth][1]).attr("r", 2);
const small = r.length < smallLength;
r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
}
}
const getName = function(cell) {
return Names.getCulture(pack.cells.culture[cell]);
const getName = function(cell) {
return Names.getCulture(pack.cells.culture[cell]);
}
// remove river and all its tributaries
const remove = function(id) {
const cells = pack.cells;
const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i);
riversToRemove.forEach(r => rivers.select("#river"+r).remove());
cells.r.forEach((r, i) => {
if (!r || !riversToRemove.includes(r)) return;
cells.r[i] = 0;
cells.fl[i] = grid.cells.prec[cells.g[i]];
cells.conf[i] = 0;
});
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
}
const getBasin = function(r, p, e) {
while (p && r !== p && r !== e) {
const parent = pack.rivers.find(r => r.i === p);
if (!parent) return r;
r = parent.i;
p = parent.parent;
}
return r;
}
// remove river and all its tributaries
const remove = function(id) {
const cells = pack.cells;
const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i);
riversToRemove.forEach(r => rivers.select("#river"+r).remove());
cells.r.forEach((r, i) => {
if (!r || !riversToRemove.includes(r)) return;
cells.r[i] = 0;
cells.fl[i] = grid.cells.prec[cells.g[i]];
cells.conf[i] = 0;
});
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
}
return {generate, resolveDepressions, addMeandring, getPath, specify, getName, getBasin, remove};
const getBasin = function(r, p, e) {
while (p && r !== p && r !== e) {
const parent = pack.rivers.find(r => r.i === p);
if (!parent) return r;
r = parent.i;
p = parent.parent;
}
return r;
}
return {generate, resolveDepressions, addMeandring, getPath, specify, getName, getBasin, remove};
})));
})));

View file

@ -1060,27 +1060,32 @@ function parseLoadedData(data) {
}
if (version < 1.5) {
// v 1.5 added emblems
emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
emblems.append("g").attr("id", "burgEmblems");
emblems.append("g").attr("id", "provinceEmblems");
emblems.append("g").attr("id", "stateEmblems");
regenerateEmblems();
toggleEmblems();
// not need to store default styles from v 1.5
localStorage.removeItem("styleClean");
localStorage.removeItem("styleGloom");
localStorage.removeItem("styleAncient");
localStorage.removeItem("styleMonochrome");
// v 1.5 cultures has shield attribute
pack.cultures.forEach(culture => {
if (culture.removed) return;
culture.shield = Cultures.getRandomShield();
});
// v 1.5 added burg type value
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed) return;
burg.type = BurgsAndStates.getType(burg.cell, burg.port);
});
BurgsAndStates.getType(cell, false);
// v 1.5 added emblems
defs.append("g").attr("id", "defs-emblems");
emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none");
emblems.append("g").attr("id", "burgEmblems");
emblems.append("g").attr("id", "provinceEmblems");
emblems.append("g").attr("id", "stateEmblems");
regenerateEmblems();
toggleEmblems();
}
}()
@ -1155,7 +1160,7 @@ function parseLoadedData(data) {
invokeActiveZooming();
WARN && console.warn(`TOTAL: ${rn((performance.now()-uploadMap.timeStart)/1000,2)}s`);
INFO && showStatistics();
showStatistics();
INFO && console.groupEnd("Loaded Map " + seed);
tip("Map is successfully loaded", true, "success", 7000);
}

View file

@ -60,6 +60,9 @@ function editCultures() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
const emblemShapeGroup = document.getElementById("emblemShape").selectedOptions[0].parentNode.label;
const selectShape = emblemShapeGroup === "Diversiform"
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * (distanceScaleInput.value ** 2);
@ -73,7 +76,7 @@ function editCultures() {
if (!c.i) {
// Uncultured (neutral) line
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="">
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
@ -86,26 +89,28 @@ function editCultures() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ""}
</div>`;
continue;
}
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism}>
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism. Defines competitive size" class="icon-resize-full hide"></span>
<input data-tip="Culture expansionism. Defines competitive size. Click to change" class="statePower hide" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<input data-tip="Culture expansionism. Defines competitive size. Click to change, then click Recalculate to apply change" class="statePower hide" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<select data-tip="Culture type. Defines growth model. Click to change" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ""}
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
@ -128,6 +133,7 @@ function editCultures() {
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("change", cultureChangeBase));
body.querySelectorAll("div > select.cultureShape").forEach(el => el.addEventListener("change", cultureChangeShape));
body.querySelectorAll("div > div.culturePopulation").forEach(el => el.addEventListener("click", changePopulation));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
@ -150,6 +156,11 @@ function editCultures() {
return options;
}
function getShapeOptions(selected) {
const shapes = Object.keys(COA.shields.types).map(type => Object.keys(COA.shields[type])).flat();
return shapes.map(shape => `<option ${shape === selected ? "selected" : ""} value="${shape}">${capitalize(shape)}</option>`);
}
function cultureHighlightOn(event) {
const culture = +event.target.dataset.id;
const info = document.getElementById("cultureInfo");
@ -225,6 +236,40 @@ function editCultures() {
this.parentNode.dataset.base = pack.cultures[culture].base = v;
}
function cultureChangeShape() {
const culture = +this.parentNode.dataset.id;
const shape = this.value;
this.parentNode.dataset.emblems = pack.cultures[culture].shield = shape;
const rerenderCOA = (id, coa) => {
const coaEl = document.getElementById(id);
if (!coaEl) return; // not rendered
coaEl.remove();
COArenderer.trigger(id, coa);
}
pack.states.forEach(state => {
if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa === "custom") return;
if (shape === state.coa.shield) return;
state.coa.shield = shape;
rerenderCOA("stateCOA" + state.i, state.coa);
});
pack.provinces.forEach(province => {
if (pack.cells.culture[province.center] !== culture || !province.i || province.removed || !province.coa || province.coa === "custom") return;
if (shape === province.coa.shield) return;
province.coa.shield = shape;
rerenderCOA("provinceCOA" + province.i, province.coa);
});
pack.burgs.forEach(burg => {
if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
if (shape === burg.coa.shield) return;
burg.coa.shield = shape
rerenderCOA("burgCOA" + burg.i, burg.coa);
});
}
function changePopulation() {
const culture = +this.parentNode.dataset.id;
const c = pack.cultures[culture];
@ -293,7 +338,7 @@ function editCultures() {
b.name = Names.getCulture(culture);
labels.select("[data-id='" + b.i +"']").text(b.name);
});
tip(`Names for ${cBurgs.length} burgs are re-generated`);
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success");
}
function cultureRemove() {
@ -306,12 +351,12 @@ function editCultures() {
Remove: function() {
cults.select("#culture"+culture).remove();
debug.select("#cultureCenter"+culture).remove();
pack.burgs.filter(b => b.culture == culture).forEach(b => b.culture = 0);
pack.states.forEach((s, i) => {if(s.culture === culture) s.culture = 0;});
pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;});
pack.cultures[culture].removed = true;
const origin = pack.cultures[culture].origin;
pack.cultures.forEach(c => {if(c.origin === culture) c.origin = origin;});
refreshCulturesEditor();
@ -493,8 +538,8 @@ function editCultures() {
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
culturesHeader.querySelector("div[data-sortby='type']").style.left = "8.8em";
culturesHeader.querySelector("div[data-sortby='base']").style.left = "13.6em";
culturesFooter.style.display = "none";
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "20px";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
@ -589,8 +634,8 @@ function editCultures() {
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
culturesHeader.querySelector("div[data-sortby='type']").style.left = "18.6em";
culturesHeader.querySelector("div[data-sortby='base']").style.left = "35.8em";
culturesFooter.style.display = "block";
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "2px";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
@ -634,7 +679,7 @@ function editCultures() {
function downloadCulturesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase,Emblems Shape\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
@ -646,7 +691,8 @@ function editCultures() {
data += el.dataset.area + ",";
data += el.dataset.population + ",";
const base = +el.dataset.base;
data += nameBases[base].name + "\n";
data += nameBases[base].name + ",";
data += el.dataset.emblems + "\n";
});
const name = getFileName("Cultures") + ".csv";

View file

@ -13,7 +13,7 @@ function editEmblem(type, id, el) {
updateElementSelectors(type, id, el);
$("#emblemEditor").dialog({
title: "Edit Emblem", resizable: true, width: "18em", height: "auto",
title: "Edit Emblem", resizable: true, width: "18.2em", height: "auto",
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeEmblemEditor
});
@ -150,7 +150,8 @@ function editEmblem(type, id, el) {
function changeShape() {
el.coa.shield = this.value;
document.getElementById(id).remove();
const coaEl = document.getElementById(id);
if (coaEl) coaEl.remove();
COArenderer.trigger(id, el.coa);
}
@ -180,7 +181,7 @@ function editEmblem(type, id, el) {
function openInArmoria() {
const coa = el.coa && el.coa !== "custom" ? el.coa : {t1: "sable"};
const json = JSON.stringify(coa).replaceAll("#", "%23");
const url = `http://azgaar.github.io/Armoria/?coa=${json}`;
const url = `https://azgaar.github.io/Armoria/?coa=${json}&from=FMG`;
openURL(url);
}
@ -232,10 +233,10 @@ function editEmblem(type, id, el) {
buttons.classList.toggle("hidden");
}
function download(format) {
async function download(format) {
const coa = document.getElementById(id);
const size = +emblemsDownloadSize.value;
const url = getURL(coa, el.coa, size);
const url = await getURL(coa, size);
const link = document.createElement("a");
link.download = getFileName(`Emblem ${el.fullName || el.name}`) + "." + format;
@ -246,7 +247,6 @@ function editEmblem(type, id, el) {
function downloadSVG(url, link) {
link.href = url;
link.click();
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
}
function downloadRaster(format, url, link, size) {
@ -263,52 +263,49 @@ function editEmblem(type, id, el) {
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const URL = canvas.toDataURL("image/" + format, .92);
link.href = URL;
const dataURL = canvas.toDataURL("image/" + format, .92);
link.href = dataURL;
link.click();
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
window.setTimeout(() => window.URL.revokeObjectURL(dataURL), 6000);
}
}
function getURL(svg, coa, size) {
const serialized = getSVG(svg, coa, size);
const blob = new Blob([serialized], { type: 'image/svg+xml;charset=utf-8' });
async function getURL(svg, size) {
const serialized = getSVG(svg, size);
const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 6000);
return url;
}
function getSVG(svg, size) {
const clone = svg.cloneNode(true); // clone svg
const clone = svg.cloneNode(true);
clone.setAttribute("width", size);
clone.setAttribute("height", size);
return (new XMLSerializer()).serializeToString(clone);
}
function downloadGallery() {
async function downloadGallery() {
const name = getFileName("Emblems Gallery");
const validStates = pack.states.filter(s => s.i && !s.removed && s.coa);
const validProvinces = pack.provinces.filter(p => p.i && !p.removed && p.coa);
const validBurgs = pack.burgs.filter(b => b.i && !b.removed && b.coa);
triggerCOALoad(validStates, validProvinces, validBurgs);
const timeout = (validStates.length + validProvinces.length + validBurgs.length) * 8;
tip("Preparing to download...", true, "warn", timeout);
d3.timeout(runDownload, timeout);
await renderAllEmblems(validStates, validProvinces, validBurgs);
runDownload();
function runDownload() {
const back = `<a href="javascript:history.back()">Go Back</a>`;
const stateSection = `<div><h2>States</h2>` + validStates.map(state => {
const el = document.getElementById("stateCOA"+state.i);
const svg = getSVG(el, state.coa, 200);
return `<figure id="state_${state.i}"><a href="#provinces_${state.i}"><figcaption>${state.fullName}</figcaption>${svg}</a></figure>`;
return `<figure id="state_${state.i}"><a href="#provinces_${state.i}"><figcaption>${state.fullName}</figcaption>${getSVG(el, 200)}</a></figure>`;
}).join("") + `</div>`;
const provinceSections = validStates.map(state => {
const stateProvinces = validProvinces.filter(p => p.state === state.i);
const figures = stateProvinces.map(province => {
const el = document.getElementById("provinceCOA"+province.i);
const svg = getSVG(el, province.coa, 200);
return `<figure id="province_${province.i}"><a href="#burgs_${province.i}"><figcaption>${province.fullName}</figcaption>${svg}</a></figure>`;
return `<figure id="province_${province.i}"><a href="#burgs_${province.i}"><figcaption>${province.fullName}</figcaption>${getSVG(el, 200)}</a></figure>`;
}).join("");
return stateProvinces.length ? `<div id="provinces_${state.i}">${back}<h2>${state.fullName} provinces</h2>${figures}</div>` : "";
}).join("");
@ -319,8 +316,7 @@ function editEmblem(type, id, el) {
const provinceBurgs = stateBurgs.filter(b => pack.cells.province[b.cell] === province.i);
const provinceBurgFigures = provinceBurgs.map(burg => {
const el = document.getElementById("burgCOA"+burg.i);
const svg = getSVG(el, burg.coa, 200);
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${svg}</figure>`;
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${getSVG(el, 200)}</figure>`;
}).join("");
return provinceBurgs.length ? `<div id="burgs_${province.i}">${back}<h2>${province.fullName} burgs</h2>${provinceBurgFigures}</div>` : "";
}).join("");
@ -328,8 +324,7 @@ function editEmblem(type, id, el) {
const stateBurgOutOfProvinces = stateBurgs.filter(b => !pack.cells.province[b.cell]);
const stateBurgOutOfProvincesFigures = stateBurgOutOfProvinces.map(burg => {
const el = document.getElementById("burgCOA"+burg.i);
const svg = getSVG(el, burg.coa, 200);
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${svg}</figure>`;
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${getSVG(el, 200)}</figure>`;
}).join("");
if (stateBurgOutOfProvincesFigures) stateBurgSections += `<div><h2>${state.fullName} burgs under direct control</h2>${stateBurgOutOfProvincesFigures}</div>`;
return stateBurgSections;
@ -338,8 +333,7 @@ function editEmblem(type, id, el) {
const neutralBurgs = validBurgs.filter(b => !b.state);
const neutralsSection = neutralBurgs.length ? "<div><h2>Independent burgs</h2>" + neutralBurgs.map(burg => {
const el = document.getElementById("burgCOA"+burg.i);
const svg = getSVG(el, burg.coa, 200);
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${svg}</figure>`;
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${getSVG(el, 200)}</figure>`;
}).join("") + "</div>" : "";
const FMG = `<a href="https://azgaar.github.io/Fantasy-Map-Generator" target="_blank">Azgaar's Fantasy Map Generator</a>`;
@ -370,10 +364,15 @@ function editEmblem(type, id, el) {
}
}
function triggerCOALoad(states, provinces, burgs) {
states.forEach(state => COArenderer.trigger("stateCOA"+state.i, state.coa));
provinces.forEach(province => COArenderer.trigger("provinceCOA"+province.i, province.coa));
burgs.forEach(burg => COArenderer.trigger("burgCOA"+burg.i, burg.coa));
async function renderAllEmblems(states, provinces, burgs) {
tip("Preparing for download...", true, "warn");
const statePromises = states.map(state => COArenderer.trigger("stateCOA"+state.i, state.coa));
const provincePromises = provinces.map(province => COArenderer.trigger("provinceCOA"+province.i, province.coa));
const burgPromises = burgs.map(burg => COArenderer.trigger("burgCOA"+burg.i, burg.coa));
const promises = [...statePromises, ...provincePromises, ...burgPromises];
return Promise.allSettled(promises).then(res => clearMainTip());
}
function dragEmblem() {

View file

@ -19,6 +19,12 @@ document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip);
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error, warn, success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = "Tip is undefined", main, type, time) {
tooltip.innerHTML = tip;
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
@ -100,13 +106,13 @@ function showMapTooltip(point, e, i, g) {
parent.id === "provinceEmblems" ? [pack.provinces, "province"] :
[pack.states, "state"];
const i = +e.target.dataset.i;
highlightEmblemElement(type, g[i]);
if (event.shiftKey) highlightEmblemElement(type, g[i]);
d3.select(e.target).raise();
d3.select(parent).raise();
const name = g[i].fullName || g[i].name;
tip(`${name} ${type} emblem. Click to edit`);
tip(`${name} ${type} emblem. Click to edit. Hold Shift to show associated area or place`);
return;
}
if (group === "rivers") {
@ -297,13 +303,12 @@ function getPopulationTip(i) {
}
function highlightEmblemElement(type, el) {
if (emblems.selectAll("line, circle").size()) return;
const i = el.i, cells = pack.cells;
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
if (type === "burg") {
const {x, y} = el;
emblems.append("circle").attr("cx", x).attr("cy", y).attr("r", 0)
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0)
.attr("fill", "none").attr("stroke", "#d0240f").attr("stroke-width", 1).attr("opacity", 1)
.transition(animation).attr("r", 20).attr("opacity", .1).attr("stroke-width", 0).remove();
return;
@ -314,7 +319,7 @@ function highlightEmblemElement(type, el) {
const borderCells = cells.i.filter(id => obj[id] === i && cells.c[id].some(n => obj[n] !== i));
const data = Array.from(borderCells).filter((c, i) => !(i%2)).map(i => cells.p[i]).map(i => [i[0], i[1], Math.hypot(i[0]-x, i[1]-y)]);
emblems.selectAll("line").data(data).enter().append("line")
debug.selectAll("line").data(data).enter().append("line")
.attr("x1", x).attr("y1", y).attr("x2", d => d[0]).attr("y2", d => d[1])
.attr("stroke", "#d0240f").attr("stroke-width", .5).attr("opacity", .2)
.attr("stroke-dashoffset", d => d[2]).attr("stroke-dasharray", d => d[2])
@ -474,6 +479,7 @@ document.addEventListener("keyup", event => {
else if (shift && key === 78) editNamesbase(); // Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones(); // Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions(); // Shift + "R" to edit Religions
else if (shift && key === 89) openEmblemEditor(); // Shift + "Y" to edit Emblems
else if (shift && key === 81) editUnits(); // Shift + "Q" to edit Units
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview

View file

@ -176,7 +176,6 @@ function editHeightmap() {
reGraph();
drawCoastline();
elevateLakes();
Rivers.generate(change);
if (!change) {
@ -288,10 +287,7 @@ function editHeightmap() {
reGraph();
drawCoastline();
if (changeHeights.checked) {
elevateLakes();
Rivers.generate(changeHeights.checked);
}
if (changeHeights.checked) Rivers.generate(changeHeights.checked);
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
@ -314,7 +310,6 @@ function editHeightmap() {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.features[pack.cells.f[i]].group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
const land = pack.cells.h[i] >= 20;
// check biome

View file

@ -289,15 +289,45 @@ function changeCultureSet() {
if (+culturesOutput.value > +max) culturesInput.value = culturesOutput.value = max;
}
function changeEmblemShape(value) {
function changeEmblemShape(emblemShape) {
const image = document.getElementById("emblemShapeImage");
const shapeEl = document.getElementById(value);
if (shapeEl) {
const shape = shapeEl.querySelector("path").getAttribute("d");
image.setAttribute("d", shape);
} else {
image.removeAttribute("d");
const shapePath = window.COArenderer && COArenderer.shieldPaths[emblemShape];
shapePath ? image.setAttribute("d", shapePath) : image.removeAttribute("d");
const specificShape = ["culture", "state", "random"].includes(emblemShape) ? null : emblemShape;
if (emblemShape === "random") pack.cultures.filter(c => !c.removed).forEach(c => c.shield = Cultures.getRandomShield());
const rerenderCOA = (id, coa) => {
const coaEl = document.getElementById(id);
if (!coaEl) return; // not rendered
coaEl.remove();
COArenderer.trigger(id, coa);
}
pack.states.forEach(state => {
if (!state.i || state.removed || !state.coa || state.coa === "custom") return;
const newShield = specificShape || COA.getShield(state.culture, null);
if (newShield === state.coa.shield) return;
state.coa.shield = newShield;
rerenderCOA("stateCOA" + state.i, state.coa);
});
pack.provinces.forEach(province => {
if (!province.i || province.removed || !province.coa || province.coa === "custom") return;
const culture = pack.cells.culture[province.center];
const newShield = specificShape || COA.getShield(culture, province.state);
if (newShield === province.coa.shield) return;
province.coa.shield = newShield;
rerenderCOA("provinceCOA" + province.i, province.coa);
});
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
const newShield = specificShape || COA.getShield(burg.culture, burg.state);
if (newShield === burg.coa.shield) return;
burg.coa.shield = newShield
rerenderCOA("burgCOA" + burg.i, burg.coa);
});
}
function changeStatesNumber(value) {
@ -419,7 +449,6 @@ function randomizeOptions() {
// World settings
generateEra();
changeEmblemShape(emblemShape.value); // change emblem shape image
}
// select heightmap template pseudo-randomly

View file

@ -14,6 +14,7 @@ toolsContent.addEventListener("click", function(event) {
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
if (button === "editEmblemButton") openEmblemEditor(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
@ -72,13 +73,28 @@ function processFeatureRegeneration(event, button) {
if (button === "regenerateZones") regenerateZones(event);
}
function regenerateRivers() {
elevateLakes();
Rivers.generate();
for (const i of pack.cells.i) {
const f = pack.features[pack.cells.f[i]]; // feature
if (f.group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
async function openEmblemEditor() {
let type, id, el;
if (pack.states[1]?.coa) {
type = "state";
id = "stateCOA1";
el = pack.states[1];
} else if (pack.burgs[1]?.coa) {
type = "burg";
id = "burgCOA1";
el = pack.burgs[1];
} else {
tip("No emblems to edit, please generate states and burgs first", false, "error");
return;
}
await COArenderer.trigger(id, el.coa);
editEmblem(type, id, el);
}
function regenerateRivers() {
Rivers.generate();
Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers();
}

View file

@ -45,7 +45,6 @@ function editWorld() {
updateGlobePosition();
calculateTemperatures();
generatePrecipitation();
elevateLakes();
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
Rivers.specify();

View file

@ -1,82 +1,135 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Voronoi = factory());
}(this, (function () { 'use strict';
class Voronoi {
/**
* Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
* The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
* @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
* @param {[number, number][]} points A list of coordinates.
* @param {number} pointsN The number of points.
*/
constructor(delaunay, points, pointsN) {
this.delaunay = delaunay;
this.points = points;
this.pointsN = pointsN;
this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
var Voronoi = function Voronoi(delaunay, points, pointsN) {
const cells = {v: [], c: [], b: []}; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
const vertices = {p: [], v: [], c: []}; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
// Half-edges are the indices into the delaunator outputs:
// delaunay.triangles[e] gives the point ID where the half-edge starts
// delaunay.triangles[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
for (let e = 0; e < this.delaunay.triangles.length; e++) {
for (let e=0; e < delaunay.triangles.length; e++) {
const p = delaunay.triangles[nextHalfedge(e)];
if (p < pointsN && !cells.c[p]) {
const edges = edgesAroundPoint(e);
cells.v[p] = edges.map(e => triangleOfEdge(e)); // cell: adjacent vertex
cells.c[p] = edges.map(e => delaunay.triangles[e]).filter(c => c < pointsN); // cell: adjacent valid cells
cells.b[p] = edges.length > cells.c[p].length ? 1 : 0; // cell: is border
const p = this.delaunay.triangles[this.nextHalfedge(e)];
if (p < this.pointsN && !this.cells.c[p]) {
const edges = this.edgesAroundPoint(e);
this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
}
const t = triangleOfEdge(e);
if (!vertices.p[t]) {
vertices.p[t] = triangleCenter(t); // vertex: coordinates
vertices.v[t] = trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
vertices.c[t] = pointsOfTriangle(t); // vertex: adjacent cells
const t = this.triangleOfEdge(e);
if (!this.vertices.p[t]) {
this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
}
}
function pointsOfTriangle(t) {
return edgesOfTriangle(t).map(e => delaunay.triangles[e]);
}
function trianglesAdjacentToTriangle(t) {
let triangles = [];
for (let e of edgesOfTriangle(t)) {
let opposite = delaunay.halfedges[e];
triangles.push(triangleOfEdge(opposite));
}
return triangles;
}
function edgesAroundPoint(start) {
let result = [], incoming = start;
do {
result.push(incoming);
const outgoing = nextHalfedge(incoming);
incoming = delaunay.halfedges[outgoing];
} while (incoming !== -1 && incoming !== start && result.length < 20);
return result;
}
function triangleCenter(t) {
let vertices = pointsOfTriangle(t).map(p => points[p]);
return circumcenter(vertices[0], vertices[1], vertices[2]);
}
return {cells, vertices}
}
function edgesOfTriangle(t) {return [3*t, 3*t+1, 3*t+2];}
/**
* Gets the IDs of the points comprising the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-points| the Delaunator docs.}
* @param {number} t The index of the triangle
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
*/
pointsOfTriangle(t) {
return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
}
function triangleOfEdge(e) {return Math.floor(e/3);}
/**
* Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
* @param {number} t The index of the triangle
* @returns {number[]} The indices of the triangles that share half-edges with this triangle.
*/
trianglesAdjacentToTriangle(t) {
let triangles = [];
for (let edge of this.edgesOfTriangle(t)) {
let opposite = this.delaunay.halfedges[edge];
triangles.push(this.triangleOfEdge(opposite));
}
return triangles;
}
function nextHalfedge(e) {return (e % 3 === 2) ? e-2 : e+1;}
/**
* Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
* @param {number} start The index of an incoming half-edge that leads to the desired point
* @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
*/
edgesAroundPoint(start) {
const result = [];
let incoming = start;
do {
result.push(incoming);
const outgoing = this.nextHalfedge(incoming);
incoming = this.delaunay.halfedges[outgoing];
} while (incoming !== -1 && incoming !== start && result.length < 20);
return result;
}
function prevHalfedge(e) {return (e % 3 === 0) ? e+2 : e-1;}
/**
* Returns the center of the triangle located at the given index.
* @param {number} t The index of the triangle
* @returns {[number, number]}
*/
triangleCenter(t) {
let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
}
function circumcenter(a, b, c) {
let ad = a[0]*a[0] + a[1]*a[1],
bd = b[0]*b[0] + b[1]*b[1],
cd = c[0]*c[0] + c[1]*c[1];
let D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
/**
* Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
* @param {number} t The index of the triangle
* @returns {[number, number, number]} The edges of the triangle.
*/
edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
/**
* Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
* @param {number} e The index of the edge
* @returns {number} The index of the triangle
*/
triangleOfEdge(e) { return Math.floor(e / 3); }
/**
* Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the next half edge
*/
nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
/**
* Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the previous half edge
*/
prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
/**
* Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
* @param {[number, number]} a The coordinates of the first point of the triangle
* @param {[number, number]} b The coordinates of the second point of the triangle
* @param {[number, number]} c The coordinates of the third point of the triangle
* @return {[number, number]} The coordinates of the circumcenter of the triangle.
*/
circumcenter(a, b, c) {
const [ax, ay] = a;
const [bx, by] = b;
const [cx, cy] = c;
const ad = ax * ax + ay * ay;
const bd = bx * bx + by * by;
const cd = cx * cx + cy * cy;
const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
return [
Math.floor(1/D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1]))),
Math.floor(1/D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0])))
Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
];
}
return Voronoi;
})));
}