diff --git a/charges/anchor.svg b/charges/anchor.svg new file mode 100644 index 00000000..c05b085f --- /dev/null +++ b/charges/anchor.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/charges/annulet.svg b/charges/annulet.svg new file mode 100644 index 00000000..2677e045 --- /dev/null +++ b/charges/annulet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/armillarySphere.svg b/charges/armillarySphere.svg new file mode 100644 index 00000000..60a1cc65 --- /dev/null +++ b/charges/armillarySphere.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/charges/arrow.svg b/charges/arrow.svg new file mode 100644 index 00000000..c9e8f852 --- /dev/null +++ b/charges/arrow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/charges/arrowsSheaf.svg b/charges/arrowsSheaf.svg new file mode 100644 index 00000000..41880647 --- /dev/null +++ b/charges/arrowsSheaf.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/bearPassant.svg b/charges/bearPassant.svg new file mode 100644 index 00000000..9fe44e31 --- /dev/null +++ b/charges/bearPassant.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/bearRampant.svg b/charges/bearRampant.svg new file mode 100644 index 00000000..f320d194 --- /dev/null +++ b/charges/bearRampant.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/bell.svg b/charges/bell.svg new file mode 100644 index 00000000..73124adb --- /dev/null +++ b/charges/bell.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/charges/billet.svg b/charges/billet.svg new file mode 100644 index 00000000..6398df51 --- /dev/null +++ b/charges/billet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/boarRampant.svg b/charges/boarRampant.svg new file mode 100644 index 00000000..fced611a --- /dev/null +++ b/charges/boarRampant.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/boat.svg b/charges/boat.svg new file mode 100644 index 00000000..02070e6e --- /dev/null +++ b/charges/boat.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/charges/bow.svg b/charges/bow.svg new file mode 100644 index 00000000..c0033517 --- /dev/null +++ b/charges/bow.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/charges/bowWithArrow.svg b/charges/bowWithArrow.svg new file mode 100644 index 00000000..ad95d94d --- /dev/null +++ b/charges/bowWithArrow.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/charges/buckle.svg b/charges/buckle.svg new file mode 100644 index 00000000..e558bfaa --- /dev/null +++ b/charges/buckle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/charges/bugleHorn.svg b/charges/bugleHorn.svg new file mode 100644 index 00000000..b006a69f --- /dev/null +++ b/charges/bugleHorn.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/charges/bullHeadCaboshed.svg b/charges/bullHeadCaboshed.svg new file mode 100644 index 00000000..88ae177e --- /dev/null +++ b/charges/bullHeadCaboshed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/charges/bullPassant.svg b/charges/bullPassant.svg new file mode 100644 index 00000000..c73a9795 --- /dev/null +++ b/charges/bullPassant.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/charges/cancer.svg b/charges/cancer.svg new file mode 100644 index 00000000..49a2c83b --- /dev/null +++ b/charges/cancer.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/charges/carreau.svg b/charges/carreau.svg new file mode 100644 index 00000000..24f687c0 --- /dev/null +++ b/charges/carreau.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/castle.svg b/charges/castle.svg new file mode 100644 index 00000000..0a2168e6 --- /dev/null +++ b/charges/castle.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/cavalier.svg b/charges/cavalier.svg new file mode 100644 index 00000000..9e7207a7 --- /dev/null +++ b/charges/cavalier.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/charges/chalice.svg b/charges/chalice.svg new file mode 100644 index 00000000..daf10466 --- /dev/null +++ b/charges/chalice.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/charges/cinquefoil.svg b/charges/cinquefoil.svg new file mode 100644 index 00000000..1dfea574 --- /dev/null +++ b/charges/cinquefoil.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/charges/cock.svg b/charges/cock.svg new file mode 100644 index 00000000..cfd58106 --- /dev/null +++ b/charges/cock.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/compassRose.svg b/charges/compassRose.svg new file mode 100644 index 00000000..8a6a8139 --- /dev/null +++ b/charges/compassRose.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/charges/cowHorns.svg b/charges/cowHorns.svg new file mode 100644 index 00000000..6564982a --- /dev/null +++ b/charges/cowHorns.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/charges/crescent.svg b/charges/crescent.svg new file mode 100644 index 00000000..f6f1f921 --- /dev/null +++ b/charges/crescent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/charges/crosier.svg b/charges/crosier.svg new file mode 100644 index 00000000..1149b3ef --- /dev/null +++ b/charges/crosier.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/charges/crossArrowed.svg b/charges/crossArrowed.svg new file mode 100644 index 00000000..a3844430 --- /dev/null +++ b/charges/crossArrowed.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/charges/crossAvellane.svg b/charges/crossAvellane.svg new file mode 100644 index 00000000..387a7072 --- /dev/null +++ b/charges/crossAvellane.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/charges/crossBottony.svg b/charges/crossBottony.svg new file mode 100644 index 00000000..345d7d82 --- /dev/null +++ b/charges/crossBottony.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossCeltic.svg b/charges/crossCeltic.svg new file mode 100644 index 00000000..e47c2834 --- /dev/null +++ b/charges/crossCeltic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossCercelee.svg b/charges/crossCercelee.svg new file mode 100644 index 00000000..bc5771da --- /dev/null +++ b/charges/crossCercelee.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossClechy.svg b/charges/crossClechy.svg new file mode 100644 index 00000000..51a143e4 --- /dev/null +++ b/charges/crossClechy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossErminee.svg b/charges/crossErminee.svg new file mode 100644 index 00000000..c9190575 --- /dev/null +++ b/charges/crossErminee.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossFitchy.svg b/charges/crossFitchy.svg new file mode 100644 index 00000000..87e9265c --- /dev/null +++ b/charges/crossFitchy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossFleury.svg b/charges/crossFleury.svg new file mode 100644 index 00000000..86d6b9ed --- /dev/null +++ b/charges/crossFleury.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossFourchy.svg b/charges/crossFourchy.svg new file mode 100644 index 00000000..6a19f8e5 --- /dev/null +++ b/charges/crossFourchy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossGamma.svg b/charges/crossGamma.svg new file mode 100644 index 00000000..7610c995 --- /dev/null +++ b/charges/crossGamma.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossHummetty.svg b/charges/crossHummetty.svg new file mode 100644 index 00000000..5a7388fc --- /dev/null +++ b/charges/crossHummetty.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossMaltese.svg b/charges/crossMaltese.svg new file mode 100644 index 00000000..f704ccb4 --- /dev/null +++ b/charges/crossMaltese.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossMoline.svg b/charges/crossMoline.svg new file mode 100644 index 00000000..4df553ef --- /dev/null +++ b/charges/crossMoline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossOccitan.svg b/charges/crossOccitan.svg new file mode 100644 index 00000000..60f102d7 --- /dev/null +++ b/charges/crossOccitan.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossPatonce.svg b/charges/crossPatonce.svg new file mode 100644 index 00000000..3fda2f9c --- /dev/null +++ b/charges/crossPatonce.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossPattee.svg b/charges/crossPattee.svg new file mode 100644 index 00000000..d4c3da87 --- /dev/null +++ b/charges/crossPattee.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossPommy.svg b/charges/crossPommy.svg new file mode 100644 index 00000000..dbf85e7b --- /dev/null +++ b/charges/crossPommy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossPotent.svg b/charges/crossPotent.svg new file mode 100644 index 00000000..c8aa500f --- /dev/null +++ b/charges/crossPotent.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossSaltire.svg b/charges/crossSaltire.svg new file mode 100644 index 00000000..dd5d8f4b --- /dev/null +++ b/charges/crossSaltire.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossTau.svg b/charges/crossTau.svg new file mode 100644 index 00000000..4927e0a7 --- /dev/null +++ b/charges/crossTau.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crossVoided.svg b/charges/crossVoided.svg new file mode 100644 index 00000000..71287ee4 --- /dev/null +++ b/charges/crossVoided.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crosslet.svg b/charges/crosslet.svg new file mode 100644 index 00000000..e359ce28 --- /dev/null +++ b/charges/crosslet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/crown.svg b/charges/crown.svg new file mode 100644 index 00000000..3a7629c8 --- /dev/null +++ b/charges/crown.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/charges/deerHeadCaboshed.svg b/charges/deerHeadCaboshed.svg new file mode 100644 index 00000000..a4f702f5 --- /dev/null +++ b/charges/deerHeadCaboshed.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/delf.svg b/charges/delf.svg new file mode 100644 index 00000000..fc4c57c9 --- /dev/null +++ b/charges/delf.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/dolphin.svg b/charges/dolphin.svg new file mode 100644 index 00000000..afb48d23 --- /dev/null +++ b/charges/dolphin.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/dragonPassant.svg b/charges/dragonPassant.svg new file mode 100644 index 00000000..36986715 --- /dev/null +++ b/charges/dragonPassant.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/dragonRampant.svg b/charges/dragonRampant.svg new file mode 100644 index 00000000..d2b2ee68 --- /dev/null +++ b/charges/dragonRampant.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/eagle.svg b/charges/eagle.svg new file mode 100644 index 00000000..963a289c --- /dev/null +++ b/charges/eagle.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/eagleTwoHeards.svg b/charges/eagleTwoHeards.svg new file mode 100644 index 00000000..079f539c --- /dev/null +++ b/charges/eagleTwoHeards.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/elephant.svg b/charges/elephant.svg new file mode 100644 index 00000000..d5af22ed --- /dev/null +++ b/charges/elephant.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/charges/escallop.svg b/charges/escallop.svg new file mode 100644 index 00000000..6d912bfd --- /dev/null +++ b/charges/escallop.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/charges/estoile.svg b/charges/estoile.svg new file mode 100644 index 00000000..af35f897 --- /dev/null +++ b/charges/estoile.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/charges/fleurDeLis.svg b/charges/fleurDeLis.svg new file mode 100644 index 00000000..2902d3ac --- /dev/null +++ b/charges/fleurDeLis.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/charges/fountain.svg b/charges/fountain.svg new file mode 100644 index 00000000..2ef97e3b --- /dev/null +++ b/charges/fountain.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/charges/fusil.svg b/charges/fusil.svg new file mode 100644 index 00000000..8da86e7c --- /dev/null +++ b/charges/fusil.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/garb.svg b/charges/garb.svg new file mode 100644 index 00000000..07fb901e --- /dev/null +++ b/charges/garb.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/goat.svg b/charges/goat.svg new file mode 100644 index 00000000..c323cb0f --- /dev/null +++ b/charges/goat.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/charges/goutte.svg b/charges/goutte.svg new file mode 100644 index 00000000..a836f7b4 --- /dev/null +++ b/charges/goutte.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/greyhoundCourant.svg b/charges/greyhoundCourant.svg new file mode 100644 index 00000000..3dc41b55 --- /dev/null +++ b/charges/greyhoundCourant.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/griffinPassant.svg b/charges/griffinPassant.svg new file mode 100644 index 00000000..025c2654 --- /dev/null +++ b/charges/griffinPassant.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/griffinRampant.svg b/charges/griffinRampant.svg new file mode 100644 index 00000000..40d8c9f0 --- /dev/null +++ b/charges/griffinRampant.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/hand.svg b/charges/hand.svg new file mode 100644 index 00000000..f23ca361 --- /dev/null +++ b/charges/hand.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/charges/harp.svg b/charges/harp.svg new file mode 100644 index 00000000..7832707b --- /dev/null +++ b/charges/harp.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/hatchet.svg b/charges/hatchet.svg new file mode 100644 index 00000000..a543a5f7 --- /dev/null +++ b/charges/hatchet.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/charges/head.svg b/charges/head.svg new file mode 100644 index 00000000..5c49b6f8 --- /dev/null +++ b/charges/head.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/charges/headWreathed.svg b/charges/headWreathed.svg new file mode 100644 index 00000000..d3dab379 --- /dev/null +++ b/charges/headWreathed.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/heart.svg b/charges/heart.svg new file mode 100644 index 00000000..01ce307b --- /dev/null +++ b/charges/heart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/heron.svg b/charges/heron.svg new file mode 100644 index 00000000..8a4ecca3 --- /dev/null +++ b/charges/heron.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/horseRampant.svg b/charges/horseRampant.svg new file mode 100644 index 00000000..55b46119 --- /dev/null +++ b/charges/horseRampant.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/horseSalient.svg b/charges/horseSalient.svg new file mode 100644 index 00000000..2b487240 --- /dev/null +++ b/charges/horseSalient.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/horseshoe.svg b/charges/horseshoe.svg new file mode 100644 index 00000000..542acae1 --- /dev/null +++ b/charges/horseshoe.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/charges/key.svg b/charges/key.svg new file mode 100644 index 00000000..90f135e0 --- /dev/null +++ b/charges/key.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/charges/lamb.svg b/charges/lamb.svg new file mode 100644 index 00000000..8854a232 --- /dev/null +++ b/charges/lamb.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/charges/lionPassant.svg b/charges/lionPassant.svg new file mode 100644 index 00000000..6ff03aea --- /dev/null +++ b/charges/lionPassant.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/lionRampant.svg b/charges/lionRampant.svg new file mode 100644 index 00000000..552d2647 --- /dev/null +++ b/charges/lionRampant.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/lochaberAxe.svg b/charges/lochaberAxe.svg new file mode 100644 index 00000000..4f724811 --- /dev/null +++ b/charges/lochaberAxe.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/charges/log.svg b/charges/log.svg new file mode 100644 index 00000000..9f8a01bc --- /dev/null +++ b/charges/log.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/charges/lozenge.svg b/charges/lozenge.svg new file mode 100644 index 00000000..cfa6f506 --- /dev/null +++ b/charges/lozenge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/lozengeFaceted.svg b/charges/lozengeFaceted.svg new file mode 100644 index 00000000..23462185 --- /dev/null +++ b/charges/lozengeFaceted.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/charges/lozengePloye.svg b/charges/lozengePloye.svg new file mode 100644 index 00000000..9ff50ea2 --- /dev/null +++ b/charges/lozengePloye.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/lute.svg b/charges/lute.svg new file mode 100644 index 00000000..3e6c70d5 --- /dev/null +++ b/charges/lute.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/charges/lymphad.svg b/charges/lymphad.svg new file mode 100644 index 00000000..86186299 --- /dev/null +++ b/charges/lymphad.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/mallet.svg b/charges/mallet.svg new file mode 100644 index 00000000..b6d9fb24 --- /dev/null +++ b/charges/mallet.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/charges/mascle.svg b/charges/mascle.svg new file mode 100644 index 00000000..c274a6ad --- /dev/null +++ b/charges/mascle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet.svg b/charges/mullet.svg new file mode 100644 index 00000000..8aa647fd --- /dev/null +++ b/charges/mullet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet10.svg b/charges/mullet10.svg new file mode 100644 index 00000000..b34684f5 --- /dev/null +++ b/charges/mullet10.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet4.svg b/charges/mullet4.svg new file mode 100644 index 00000000..ea581ebf --- /dev/null +++ b/charges/mullet4.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet6.svg b/charges/mullet6.svg new file mode 100644 index 00000000..8821b639 --- /dev/null +++ b/charges/mullet6.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet6Faceted.svg b/charges/mullet6Faceted.svg new file mode 100644 index 00000000..bf5f4a08 --- /dev/null +++ b/charges/mullet6Faceted.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/charges/mullet6Pierced.svg b/charges/mullet6Pierced.svg new file mode 100644 index 00000000..a58b68e4 --- /dev/null +++ b/charges/mullet6Pierced.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet7.svg b/charges/mullet7.svg new file mode 100644 index 00000000..000038ab --- /dev/null +++ b/charges/mullet7.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mullet8.svg b/charges/mullet8.svg new file mode 100644 index 00000000..7e1a40af --- /dev/null +++ b/charges/mullet8.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/mulletFaceted.svg b/charges/mulletFaceted.svg new file mode 100644 index 00000000..83f7a993 --- /dev/null +++ b/charges/mulletFaceted.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/charges/mulletPierced.svg b/charges/mulletPierced.svg new file mode 100644 index 00000000..88f706b9 --- /dev/null +++ b/charges/mulletPierced.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/orb.svg b/charges/orb.svg new file mode 100644 index 00000000..4bf6e9c3 --- /dev/null +++ b/charges/orb.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/parrot.svg b/charges/parrot.svg new file mode 100644 index 00000000..1e65ce33 --- /dev/null +++ b/charges/parrot.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/pegasus.svg b/charges/pegasus.svg new file mode 100644 index 00000000..543b1a1e --- /dev/null +++ b/charges/pegasus.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/pike.svg b/charges/pike.svg new file mode 100644 index 00000000..3bd9ed53 --- /dev/null +++ b/charges/pike.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/charges/pique.svg b/charges/pique.svg new file mode 100644 index 00000000..c1e2e4d6 --- /dev/null +++ b/charges/pique.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/pot.svg b/charges/pot.svg new file mode 100644 index 00000000..96e39d04 --- /dev/null +++ b/charges/pot.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/charges/rake.svg b/charges/rake.svg new file mode 100644 index 00000000..03220f06 --- /dev/null +++ b/charges/rake.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/raven.svg b/charges/raven.svg new file mode 100644 index 00000000..3d1ae0a5 --- /dev/null +++ b/charges/raven.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/rose.svg b/charges/rose.svg new file mode 100644 index 00000000..b80c3d51 --- /dev/null +++ b/charges/rose.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/charges/roundel.svg b/charges/roundel.svg new file mode 100644 index 00000000..c02cc392 --- /dev/null +++ b/charges/roundel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/roundel2.svg b/charges/roundel2.svg new file mode 100644 index 00000000..c3d8c036 --- /dev/null +++ b/charges/roundel2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/charges/rustre.svg b/charges/rustre.svg new file mode 100644 index 00000000..dbfe8848 --- /dev/null +++ b/charges/rustre.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/sabre.svg b/charges/sabre.svg new file mode 100644 index 00000000..306ff201 --- /dev/null +++ b/charges/sabre.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/charges/sabresCrossed.svg b/charges/sabresCrossed.svg new file mode 100644 index 00000000..ad87ee2e --- /dev/null +++ b/charges/sabresCrossed.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/charges/serpent.svg b/charges/serpent.svg new file mode 100644 index 00000000..fb2089d0 --- /dev/null +++ b/charges/serpent.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/stagsAttires.svg b/charges/stagsAttires.svg new file mode 100644 index 00000000..0fd887c4 --- /dev/null +++ b/charges/stagsAttires.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/charges/sun.svg b/charges/sun.svg new file mode 100644 index 00000000..247515eb --- /dev/null +++ b/charges/sun.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/sunInSplendour.svg b/charges/sunInSplendour.svg new file mode 100644 index 00000000..a11ab0dc --- /dev/null +++ b/charges/sunInSplendour.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/charges/swan.svg b/charges/swan.svg new file mode 100644 index 00000000..491b23fd --- /dev/null +++ b/charges/swan.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/swanErased.svg b/charges/swanErased.svg new file mode 100644 index 00000000..40e1e588 --- /dev/null +++ b/charges/swanErased.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/charges/sword.svg b/charges/sword.svg new file mode 100644 index 00000000..65f0ac35 --- /dev/null +++ b/charges/sword.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/charges/template.svg b/charges/template.svg new file mode 100644 index 00000000..a7510939 --- /dev/null +++ b/charges/template.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/tower.svg b/charges/tower.svg new file mode 100644 index 00000000..40df5b9e --- /dev/null +++ b/charges/tower.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/charges/tree.svg b/charges/tree.svg new file mode 100644 index 00000000..41a4b032 --- /dev/null +++ b/charges/tree.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/charges/trefle.svg b/charges/trefle.svg new file mode 100644 index 00000000..9916c5c2 --- /dev/null +++ b/charges/trefle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/triangle.svg b/charges/triangle.svg new file mode 100644 index 00000000..7109e498 --- /dev/null +++ b/charges/triangle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/trianglePierced.svg b/charges/trianglePierced.svg new file mode 100644 index 00000000..9017aead --- /dev/null +++ b/charges/trianglePierced.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/charges/unicornRampant.svg b/charges/unicornRampant.svg new file mode 100644 index 00000000..18040236 --- /dev/null +++ b/charges/unicornRampant.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wheel.svg b/charges/wheel.svg new file mode 100644 index 00000000..aadbde83 --- /dev/null +++ b/charges/wheel.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wing.svg b/charges/wing.svg new file mode 100644 index 00000000..04c108f2 --- /dev/null +++ b/charges/wing.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wingSword.svg b/charges/wingSword.svg new file mode 100644 index 00000000..4fe4b70e --- /dev/null +++ b/charges/wingSword.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wolfHeadErased.svg b/charges/wolfHeadErased.svg new file mode 100644 index 00000000..4f821ba8 --- /dev/null +++ b/charges/wolfHeadErased.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wolfPassant.svg b/charges/wolfPassant.svg new file mode 100644 index 00000000..35e68ef2 --- /dev/null +++ b/charges/wolfPassant.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wolfRampant.svg b/charges/wolfRampant.svg new file mode 100644 index 00000000..3c9344b0 --- /dev/null +++ b/charges/wolfRampant.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wolfStatant.svg b/charges/wolfStatant.svg new file mode 100644 index 00000000..ff624fb7 --- /dev/null +++ b/charges/wolfStatant.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wyvern.svg b/charges/wyvern.svg new file mode 100644 index 00000000..2767237f --- /dev/null +++ b/charges/wyvern.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/charges/wyvernWithWingsDisplayed.svg b/charges/wyvernWithWingsDisplayed.svg new file mode 100644 index 00000000..ae71c5fe --- /dev/null +++ b/charges/wyvernWithWingsDisplayed.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons.css b/icons.css index eab23067..008faebc 100644 --- a/icons.css +++ b/icons.css @@ -252,17 +252,8 @@ .icon-if:before {font-style: italic; font-weight: bold;content:'if';} .icon-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */ .icon-half:before {font-weight: bold;content:'½';} -.icon-curve:before {content: 'C';} -.icon-area:before {content: 'O';} -.icon-curve:before, -.icon-area:before { - font-size: 1.5em; - padding: 0; - writing-mode: tb-rl; - margin-left: 1px; - width: .6em; - font-family: monospace; -} +.icon-voice:before {content:'🔊';} + .icon-die:before {content:'🎲';} .icon-button-die:before {content:'🎲'; padding-right: .4em;} .icon-button-power:before {content:'💪'; padding-right: .6em;} diff --git a/index.css b/index.css index 2fac268a..d104ffb3 100644 --- a/index.css +++ b/index.css @@ -5,11 +5,6 @@ src: url(data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAAC5cAA4AAAAAdigAAC3+AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACDKhEICoHQKIGbKAuDJgABNgIkA4ZIBCAFhwQHg0sMBxsLWRNuLDxsHDAWEA8WUTUaS/ZfJfAEJtXwF2CTqKhq1GldrigpJuuZzpbfwBe3GHB3cREO4XjwXN7Hx0/8dYQMuT0E2+yoMBGdGMMADEoUGyVaEAkDCQVRDKza5lxYsdS5vUtdZLnI+KrFp/zz/cXOf7sgURpYIlEzESceYJxgpFHAYXP5f+rg987dPz+xJoo4zKyKKORUdmFudWLp5ri1ToiVgzC5h5J686RIImzq1flJr8Yrh2sqbnTZMpg+Bqe0mSOBV7qkhXheHI7ccDdcL7vk6u7lz42YtAk2ECcHKZBSX4G08na1r5f5t81q/ydZIXtCTn1WyrPXi3hRbnnV8FlmmPkZZIgBq+QEWAVWI+8mk0FDBD8TXREhZ65kT1SaYstc2Z2XWpS1LsPpEijUN6dmtBPj7nzXv7F9nbHeRrRFq5SgfODNzgsAgAwY3J+wAAAArhJUSytL9LXzPy6KB4CvCQoCjtsAAt6gC6d1dC2CsHt+Tx8ABp56gnxw9vfv9PWqwzbUmp+6esAf+W372RYCgGm+f3iYF5yBeGDyBw52hwOLbAAmMOR/NlyPKXievK7u5m47mIbELP1cnCfz7ORDJpGDyaHkZPL2O43JtA7srGukW5RAnkEOlCWdVt975p6+UIer/m8wqu//++7Vvle7X+16tf3VyCsKTamFAYK0qDfVgoj+2jsAMDgCifKqlH+lDrR+7kNCw8IpVFpEZFQ0ncFksWM4sXHc+ITEpOSUVF5aOl8gFIklUplcocxQqTM1WVqd3pCdk5tnzDeZLdaCQhvo6OzuXTzyzbq16yc2TG7asnnrth3bd+7as2/v/oMHjh45dhzUFDlKbzStIasqBw0Fug7UAiUNAACgYsDGb7dXAgCAawGYZ3fh8jNnL05dvXbp8iHpFAC3b94CADT/XwcL+ub39wwODQ8sXQaWrFw1dvrchWrgfMv0OkOa5lFm3IEvcGlK/pRHfsBFgQ3/MfSm9WHgoCDBoE8Y9gahHLShLzy6kdBD8lj6RymFOBcc7EsjWIV0OPg0OdjNjsObHu0PLfxhlQyGnnDXRnBMGIoBQ78ILlhYD3SHXqhWMe6WH0p/+Vn+EiSE+Hi087FHBwBop/hEvvq3VokRVR1tjY911zVpq5XG73dRAK38m3xdPut7DbX+no2pp0PNT8ee7bc6N63vTWxOPenb1N6j1qi+Vbz0BbXa79a0dLZuCAVSDVs2gNVjcHVnq9WdWOfrtVqbQNN3/aBflbRAPwBdk99frFpVrPahAGxXAUpWtbYoHXF/IAL8iF1N5LGaqrWhpeYxqXxdvltQo/eL4RhQ+nw9zaJ+/9UXWqpfCvN7Nf0+S19jJQtX6uqQ3VySHtnvp72jtTXmU5YypD1AQB+lNqC0tPiUYpR7UKvZAFbzqd6AMRo6X7p9ltJh1XTq/lbXt+SVhYXuNlLylCzx+Z4we0HNX9IL543c2R2F01TJtbAhM/PSTWO2CWvDGKedIq69TEEDASoxAXuWXb2Z98LaQC3J3EWn0ttl4zrgprF4ChYMbSpCt0oXbIBDS302b8lJiNk2qOjNxJUvQwUFz/bCc+cuOdi1T9/djhzIMpTTTuM0ljOyC1RCojMtoD5LXPWiwxjxXCE+E5RfDcnDXt80Qzx6+qOmpfFzZENWKq5QqBUj6tI2cmefeDIWijx78msCUqeQiDIb4GHsAsIQp+sL016Ev71bwJ8mCXFH/YBPvGnrQsjYNTKWdtS+26nkJqUZqqF4TlMJIIPXFapoWTWnzUlZEkNFFZx4lrersSk1X2fq8/BWF8jvlaeJjhN53aAKfeoGIEAvGHn9iFygUVwylGPPeB1G9ICRG0VOUpdB0sy3yimNSdIiTWkRiNgavSrCR0KqOp40hVrE1uvVN6ZnQKVVwLSAM5a2YL+zNOMpEH/ipsDek9egjJuAjg3xUkHBmjJqLEcDK6Q/dNoSkFKnVB+dQkHVAwQU1di8EPjLBhAzizR1Op7fK2MxQLnEbK0AUXCO00RJxh7QoZ5ViVuMxA7gOoMrM0b0gs2lBTZIvHXyZTjOUC2dCWtTkroEyD2q1GEZJELA05bN7GUHonsRSVwXOGRNagJqUyho7g8xEFPt8ymOk2ysZBkHHKpcTdDvcb96HTDI2AiBzjOWEJA1uE1WauoU9MzGUovRMWTz9klcqWDoyfMiRflQCe89V7mhnzmKON2OnOq8+LZ4dgx7UzxlTMUMaTZOPPnkTCCd2l5ICBn4p98EChP/fnijntt8U8B2dac0okGu3ny//vS4jTK7wxiVf6B3cv5EBl9afpLv13RlMYRtwsY4hcHpJJNQtyaEvNtPbMa4tTyl3dL8Tu/APQ/Z55F+QsbyEjEZWhuokGRoJpVeiTllauGmCjG/YRPO8taApx6nAQFXSFIGqOj9djNkDRNn4CnulMLRXYs0hV19BWQ4XV7WziI2KM2Kg7izgdiaqc3iI8Z/hK6/jD1isgnnOl1/UsagXnAqOjXrBB6yy3kCxFy4T/bLN8gGJiHHEbvHW9ZzZRIIO5dBo9ncgPNsPl+JrOajEuvGTCI3ufIN41QgMopp4j7SRYV2Rouo1tzftESSfAl4WXYRIhNPnY/Sp2Rct7QtXFYoDM4GmL3AAKnI1YQxR43X5AUP2D0V2TnrxO8fQlgzV/YO9IhN5WxRTq3gqX5mcwo996RQaFmaDah5oh7NKIoaN8CVvvAyhcHX0xJyNBN9IwHXgNKSECdxq1GmA6ifXwzztfSmYcz15tAZQLxNnufe28AXYiJ8UZlvx6DnFgM7hIAhzTgt6RilND6iG8sEtymitxpx1yv8tqu/abM2vSTj7arNJHQsanQ0xaJfY6+PcZ6ZnizbWe4tTGKvWyQJcSxSy3Klz0iT3DG+4RNmkQs1S2E3G+swLgFyJoCOa9psIAtIRWVQ/5pSToJDDytUOCB/Uw72PyLGQN51Q6wqUjSKzvh/T5PHU6Hnw/iErlfDBYVKyCYeFpAyoL/OM0fBSdmgVo6QsiEentzxmnpVxm4FEEXfdrRDVw1CMlbb77bMkzPq9oemgjf5xOLUkW7ZANedsw0j5yu0P5679wNOrcsJ7Q2hkSxrJtOqp+Bx+pzYrU4wkNO/7HgrV3mF5lUWAYcGuZc5tbQzI+1Hqirj7ftDynRM9/O3nB2He9Ic2X3c7cXl3pGlIYxyvkO9dgRF+LC5ytEA74L3gxQC3yGD/C1nHMf0T8fmFBZqM8nxVb7dzMeROwLK27115JqqHunL4/MK5Rt5iU7GFzBkesUzREKaeipemf8eOFdoZ4tl2HVdnpI2lnQFeNfj1jfgWNb0Rki/Lv7pGssKVSTW/9iyI5ERsMNzZwyFO8PcyWbogMb+4agrgAD5P/D4iivT8aGYgAPcMR2+J6VFavRMgWrH+NqAW5jPbAzISFzKWwNLJLn3MlDUYtIGYF7qG/bjyO3ZMpCunXPhGlSbvFgWBrDbXQIOTVRlbG76xrMxmHfzQP0ofclzc7gxHm3wc+yaYgfLGTje+JE3FFwHYqe4gtcFV2xX3HRvSN8/ll+veVBySyJnsTewy+OUoZqXYmEh+3PBBvtH6Xq30ueWrc8Fs6p/37HQvUH+LIiPq4tPHgyGBKgYKOiPAtRUPyVhPLIX0gcnnHYUaOUs2Uez6aTLAhtsvSscrhnr4Vo+ncSL+smGzG4vzT7Pa4l1GvGIXbI8h0c50Vim6w/Iyq99FHuuassSHczx6ZVlsKaC05K/BZhM4sXgALlbNQXb6ZTnAnur6VsB/VBJRVFe6WOtUYW8nXMyp/GoyqKwGTDKFgduYq/Dc1UJdS/2cv8FUM52iQ/gIk0I7GUglbwjqJ/9DfxAOFNIDwSjHrNFHp2xghdsD5A1sctPpZEgN6VZ01Jty5Tro4xQiBA0mQS9W8mf8+wVRR3I2y2xRG5JyHmQbMuasTfs/daOnSOq9NqU45cnyWIDwB1hvgcGZxciYP+YhMjKVqz192zi2nhdVgDaQtiGMGM3mwV2FV3OVylL3BZpKvi66dby8LpF7DiJWBcst2ys9NE73FNqDjXwjqJ+TM+6KPDcL5F+zh1nLfdmVUXCrRDC+zyR2sAsRRbnXX3WB4soy71jLbVsJDw4nDd3IFCcMnn/xuj66OyWIU2hVpXKC2Ze9wBpa73cmdYBSU4FYiUcysfClMuxk4zd0EQZppDSpq8EEJembInXIiZNvhJVnWlT/6cb0Zk/4EixTIAoyfmV1GdeC2NPPgBlrw4Lee+rMWOMvhQ9ypLEA2RLbF6kzvJ0gIu1YHcgL2dPEZBvFWnL7bjYIK9Dz4xUsaasQdYw7NiQOxdNn4ovIOjKSb1euWlJp6EOEmfS1RTKyrZohZhMEP2c97Px2OXvl8bgoqbZFyX0PN+oZpPuLZs8a4MjoepLl2T80oNNpYTi3IWUeyQeK1gS8ztUz5uHZt6sG+WdYmCmLQlrUpNspD21O9RYXuhBWQcgYEm9ZMDmQuVNiqZN3Vsg3WMvbqNV7Hnr4hNaLAuJZ435dGuLLG7XSmdpoiBLOHt1JZHQZktNa7mkPBRf77ZZwZkowd956+ULTjdrVzzveG8NTKtTEbBXrhBzge0We/gE+9/biIcYPEm3jvCJUjjKUG1OkU7Z+mEZyIeFAQYvwiHFYOMLacf0XFnAqPNsHfXrzSmNK3LUvnewWOlagnBiuiA9i5JOVKoQVS/gnjwwVrRVLSLtsK8/plwwDuBQHmSGLxKx9Xc8duDofu8Xr5L4LHqFJqpj8TK3M5Iypws0RcFFqbLZlJaujVJ2Q7ak6P3TkmmknaVGCb/aoIgRBkAIKAZtFopNsSR5Uq42b6rFcsc0O3RejG2n/SrS67YuZP1C3fB05KinTF2kMa4zrqYOKFuK5hQqOsmUOTMi7be8xFKERfRq2Mi1FTJ2Oa7qQdSuZkHoFJCWFVkNChMIdGh1afS2VLxlknWlsDGXlnFdIt9oM6hF2zry4P7veGNDfOG4IW0wPxgIXYs7ErPd59H8WqJhrYgktQJQMDeULSXlJO2a0qZ+oWdejtJyVDtq76R2zPkpsxcV6PTdB35YoFsV4hF/1fIzhQUOM4YdZst5lP0OoYvUSl1QJoGkJD6NWI7Ext38AWPG4g6Pjc1/tkNUd5XcPjaaH3ePdh2FO0WxP1+niei9NQRcSVLsDTGkOBLf8MRmtjx8LzlGSXJgOQa3Sqj7OISnjtF2UoWYGK3DZWoFVkc8T0zHexgn3aTJnUUSrgfsiFPsi1Zw9TpZBFq21WsPo1Xq5UKSEryMRZsROzQbTKDFk/g7LYYkpiQOC74D2qHBI+71jdHIhnM8qnjXIonUjiqizzX9YzBAYOdeyAsIrNoLfenl8UYhZrFeGCBp6b1Bb6LHNtoXudYGXBt0iM3uZu2iAx2Q16HlfcLQjs5OkzYoH0IG9PLW4C4c2t8vew3uLCTN7YywS8Tq+xG0sH1IJRBqJyXB1ApLUH8xMaqej3p5E1ioGReSCAUuuTdR9FfcwW+kqITuJcWRMLPoGDUK+0+jC4FTsAMq1AAF6Wc061owqkBzfGDHsaqjJGYP05vzdC2qVQ53zihhDzCGuCKgfs8+hPMbDySr5V0YxDsZEy5t6v6BcDU0B66QCGToXBySnLq0YSLZn1x52pHnNEFA2oOJEZULChz3dDPYy4CEUPOktPrBeZI0vjyDZb9As+QIrtK3njg36mjjLDNhCO9iEUo+kW9JOQbv5iRoCXoa+cOkX5qxnZIuItkeULIYuapkebyBOxTXjFZceid/w2mQmLvxWI1g3jZ/I/HOjQRfFf10qQosRtcwaoX3sKyOEOma17jjHHOowiod1mZmr9IPnEd5jdCmE081DPuYpLFYr82ywFwwe0bNn8v8KaiBya0G9Teaa7F+QWAZZHxYyDaWQOfDuIxJzAaeKxcuF6pGIawcm8sN6WSdjg3zBA+1WXed2lOF8LQpggzAJrkL9qptKxWV59QOtP/EOn3a+km6zrta9/xPF3sL/e0B3aJmbnTnLAZpPR+O43iwxr3vduujZirB7DohYKOL18yFui/4lYVyLGqBkt4iGjy87HhXBW30MVV4NbRpqRyxx2NeBbRxmRxh879F6OD4VOMckAyI/4Pie6QG59y297cMiMeCCigwZ0frCk5AVb9t4OiA6hd3NULoG0G1sBFLipMLVnvG5ZQJycCIgI+pNTJ/NzVC4Esjdt5B7yi5PvD9iQBDDHJ1kXRnDCmDCV+QhQZ7knwPBXUUG7WbQkWFqmb2c8Sqc44e/k4vloyRyW4pbQwL/cFd75abbTZqDW6qwJZrZmbmOlBD//Codl03U3S4f2j6fJr+H9z21WdfnaCX+0/HfDe8UqW3DtTMFK+JDLs/f4RXVyciVgw6CyX1JiVcBMsXpaY3FYu8ZGLvL11qgca9ipTgvrjCrUipKUntL1AtkFXO8tiCVV0VgFD2bUHWWP/Jr+2yzyLzGBhq7PdAqAAWzRiSjr79a+lHfQxcWbPc6waSH78GRjBAJjwAbHiaAIHDYg63xMHhEMK07IQigccExECAraUg+Te8lp8TCs4v9zqJ4lPW9m8teGzLYTZs8TXxaP7O8w9EPW9HWx+NvDratKAV8erqtxL4QiWE32LPHgRrGByR88WYPESOHX2sP/ceaGbm6TENxkfunRagWecEuIQog9GLQvJqo2hOSsOeSc7KxBWA0bZgbGHzQECzf++C0QD0lSwoWMvv7+jg9ze/EcC9pwnOaJWXG0Xt1bzXM8ZNuyehy3HxWSjF0exNP4BWgDU5HoVF+OXnpBv5rkPbyL1w+O+rE5n5NTq9oJxpsIb9VBGxArkmd/ctHLPK3J6v0Bdkte1f6vsROJfRiDWZfvKE6wyhUiridq0mfNibnIfs3Ch1piza3ExaO7zVvah3aHTGOlF5kS7SSWS75cPFIIeAV32EUn8+/vsAKyaUzZqlfBDbYXkaFnyllP0bL9tWPc4pHvs5q4xTbWj8nAGgP4E6KvIb9LekkPbqMG5nSoJIkNgOcxA37xWrDR1dEqS+LA67oNjPCikpMBP7VZo4J+ToShPeU3hiXprHG9Tn/vxivUHXWJppMFoMjauv1saeyU1zTYIkxPJipegvElKRNiAsSy8Ku3+uLa6rw6AW7d1c7HMU0dcfJXJEm7RbQcvXf8GifTyXys5RLw+QVpj2SYql7Q0aa7LeUaNbmZzFyRw/xLiGHbZsRN/QSDjWeU28opjLW4P5OXRpJuCKudmwIl66iPJrI1vfiZqNeNtZfxh/t/Qm72XNfwW2mDZFSR+8pFcBeFe2oJ/aswe3NqGvvRSC357LkHKa1fWgt7BP0dYnmx9HDfiZRflTeULtz1a5uVqrVGvTjFYrMg+Wtdinj9DcHLKC2q05+vdc7KlcbFKfFFti3GLUCcKEk9aZjBIlQy7JeV4+MrxuUVqn64jCc/eWzbsvvyadJllYyxfdbfC0/1SBdxNbJzwlWWFsx0K/pMfclo3yJtUf9y3LN9eG0heDF1bMUjm+QDxIU6B2yBKvU1EC6BJu26SfrWkz7+3STbmb0TiryaMydSVyqG/3a3xMY3fIiOdK3O1+Cbf54uyuPfDq0Dg1iFtiQE1BV/0zq1qd3wSXG4WCrOVLc3zT9BSMTuzn+s6IOlQF+QnD1y9maRvkXyU8YbYwtEKHcojhuSnNtELeypNicA7Op+bmce4/l9fOicypzKX3XuHsb1tkIRWXeiNIeX0orf9QtKq9k1Oy/gBTCYo9/Vxj6ELEZ/6LAbXJC9O/+PWcMHkeBaObrGW+Q1tU8FxTn5Zj9jYs9o/F3dZuE+M1JWkDvo4e7LGbvMiAq0PzIjJKjXT69wlHM40olGJsU2SmMnbYmF/0XijMagBXcSqfE3N6grDa3SlFuGHoPN46JNCjXoAtYEXr/FIiW8PtxDw5e77tjPcOMHK+w5bgke5+IbUwK3myq61c6NvSMoJb8m52SGmpN8K/dCdC7d8T913PfE5OlxcQ9DuO4RXiQdqXXVOJ16kYBziuQC5RIMF9XGaJkIPNW4ithLydoNvxiveJVQFIJWRpBmZEhAZ7UWb6kuEEPBO/+dvCLakP/gSPnVjlSnwBHs36jI16PVG2g9cX2iC4YU85glILu/Zl7rfyM/uqLo+ijUTk8k/DxVWAG5aEHJO1XZIAeG/rdez9GLShZ8GORqr+nneSWbsOQyFCg9O3SuDEQu5wfFXdPugoRiLEfYY2gcdOfG47yugV/PvosqFF/rf7JT4O6HvOLqyO9TqdAOK8dPXvM4ts8FldWN1eBmFngvx/hGw2ufYZSdn2VAuIas8m8lA/MptP/fMQEde4YJgd/IQwVE/xGOe9XS5ClGoGP29u2afEYYClbQtavZr3dmATVeq5F2/bkHIMr6BcTZRVQEPmv92Kjdn8KVzpsPhNbocGcUX1PLurcPKvxfM8lc7ta77GgL1jcxZ79DxqZTYOrmy7trTzkLRkrNKxPT+lLsY0dzJN5R9LOtHt4/PAN3+HJzKc76q2g0YKTluHqXBZy9k4UFgo2b240uZRmI7P0GQMC7kK6h1U3nxc3py/TQL2osyZzpTmYMq1YUf6Ho/D5rgM1MQpx8XGCI1cxajRRimqv28m98otrWr9fuhmA3nWnMXoWVcbYhd8Oy+ceMY/PuSvw2BBKL7AyYltXUlZao70i8fqp1GvJ27qfcsDEVmWnR0hFLxcLe2WMDLC7Ki8AaxS977eb8tTtUfhNlIvUfQxvpuOVXqz5gpTZg9wHZrFix2aFGlbiyYgR+Xpk44o8ufKNhRLooTJQn2ajp+sDHNUrrJS5+CViSng7MheLxXlKfWfODFWaSdpEUhXEb7AipePdCbB+XC3tvMY8U/XNwbB58OxtdTr3KldX8IHxXjFyRS7TKMOT0ZfShI/N6Q2b0FbktuX4DAM9bVQiUvbx7sENqI2LX7L25eFVXSsPnC8UcPH4x81xmBIZXXpjySelloDeW97De8JLPTLy7k8+z31OmiA/VE+/BmlNj4qxCSeSBQmzaJs0f6sOMo8LUFRxqB77HqCngpvUk6R/gJntpgPo6fgPzNP985YtQ6tNQjY3eBcoW8Sj1C9TnqJ0/fsP8eV1bAS3AV1x+c9CBcP0jSoDd1VR0WKdfjMWrzDWu9zFN+0onIf+O5P0pYK+9+2j5EzL76y6lECmR04l7zJDV04YJadL3SL9EOv8yVyRook3oVhfKS38pM2KEKGuUJ1aWPys+kal+Omv98OmZ0t1trN2uX8Ms8zX3yGgoHbadcNB0b8L0bsuOFycyOqIVaqVQ0hugF/OmIPltEzM+vvv+D+1qh5Lt5MM83+EiTs7v3/BFS0j3zDNJ73XGH9KW/0BoB3bkEP27ObrE1gxv3FfotapXr5uuMuDBpUECzY02Zs0tNPWG04VssxJX4ZaU4iTRihJe+s4QQtOSaM7ZgnS2oZmT91EC1BFRcQjzR6hXgBCvJnpB31ycSXv4anKjj17S1C7xk6wtj2brF3ngSdFTjz/JG03UPs3IKQo0gRKaGzaAmELVRzlRyAT5Jcey40WGbaKmfdnn/IEagwuSMISLrFvzZcVxpAlRaPC2k3QRjyPbJuVsXDriNFgSSd91as029JBJ+iPorkB8X7ln1PkZXZyXzQOZ5StOj4bAXBs0fYeqhwMLmsfoemXuMDrHfI5vrGhVRyyg+fLiKW9aDV+69EoPpoSY/haFkHy4ypjj8aocT8xMeKWuE6/0KAT4peAS8/3BRei5M0LD66yq0blkts+LJkPc2LhyafSq4BJZ60+fERxvPjQ6xFq84bI+Ln05IOU1A1oOaT6/3kwBdErqcOjIxyOKlv362tzfN9gQFlxaMKTH1szEQLu/UBGmatHW1skWE+haFlDSZLO3Q7owmcIkUWV6Raud/zDfpDSc5DaFG5Y2HASEuSrLmB/zxQlDnCfgttLWDiWtkIVmD52JdKTMTR+FWLWHMwGvjjJFofKuLK/kZtc30e+TbTH6ZqB4yENN5nSj17xhGPOnNiKPuebTSJdM88ldgVKcPO9vXc6OnL+0hU2kj50V7xedImxUZuHKVZ/GdOiksj4ttpSRgD9IfAW7ASFtIobXh9CAQgmWn5r/1o9VxWl229ObKvrRXJ73R4pkovKDWuCJnVFdBF8lxWWh45sBejmctasXykJ5KUV++b6uzOWqDuiumaenp+C9iAa51zp/1Qib+zouFRww4nxfMtAx0hKZ8l4nPsj2mSK7I5IAr5EXlgoWKeUYfnszFpFxqQ8SFXkIJUNH/ZE3MYNEYSic6iB4MxeHXrDVT173bpZktE1gZZ9j+79rymSEcqi9hmj1WXwBvkF9d7ZL4xTspOVvxW/Y74MHmk7F76tWCepEsUGIhtcjmKQ7ciBfyB/TK8Tih5Ay4S+CYx30iLqEmelKbN5FZXrFee9B6Dy/0DFawsYYiflKuPlPJmppY1fUmv+uwROAdh9A8jg9o7ZP6GxnisTqKgO4uL/pKQYts2pAatjiXptvNVGF8DVtfvE+hfCK4FsHoxovsPI5DO0NReJlc0Y/t1htuF0ALISHfyqPUem6PFFVxhGuY3wDRPUFpy3mGEoBAxOf8caz5AJZXKni6FVSQ1j3xJNHGQAjGEVilcRh+b18m+ZzGBR8SLcRdvC1Xkb405CaQIbuOICROeSMpFm9SopJOvgzUXQzq4y1KW0U5AzgjmQHl4hWDStC1ik4Rbe3o5UdgTvKHpiE1eR6hjrtrGPqRAPL2qBD+gvow2J31TR5+7qWr5q7PmpOaNXxJT7H5ZHfq+IO+NuljXwyP3m1a2r/lj5HDSqOMeu8GNZRotWOOASv2A5oPnZHupFY2Tfy3AKPcv2gAo6M5KYv137/eSxPr76Pd/hDhT6/K2A/X03k1zmPeoKaSFkcYPJyHyW3jcQ7Xe60W7+dduvqn1csUwdS/rgjhzvxKIXs98wK5LOXozaYIVLsJzZB5UFmnSlKP/j7fjjz4W/VjEbRUdrqLfjqAfY/X9sYMHhi/6l9j3ID8HBfIhg2D24Ry9iTTJoso8OCJ8OIs0YR5X56UQCYbEzRVuCxkThAgppDyrdP/2sggg2FcePPUg2BTd5WVZslqKb9wZ1xuULGFiLFahDAU2xJ9bEKosmMp11Lo6cybM9vocRxDBO+VenehLIxo98S6xP+jElWs7rz0Gzm54KcTeaWG6ZVf36YiSXeBUCpHU8bzHwXzj/BxMg3EpFlk4HvphLxptsfrkI4DgWjHG6H51zV0cZgA7PqiyxtcR7GZMunHIkKUlrBt3lFIFeF1Rfh4iXNGx3D/FkGp7qAhHlDrnZLqpGH0Zho7L/w5lZE/PNeDtecBxVPQX6YpmffamOMI16kX9PmJYxPAniBNeG6tE9EBC3Uvh2vCMdEFA0wG6d7ODEvrsYgyUj5NBHoTpcW0w4PZvAhph2Xvogq//gmV6/xI7OIB4tOgBtKv+g/xhP1ESX5s6bFcJ4ACOvyDpVqEpqfANy9QUpuZm2Vri1JIfWvzht3K+CfJsoFku8DnaLQAVy5Lt2qew7841QIFT9c5/jh9Uh2RkGMcL+2Uz0gcAxPG7xrvS76S4+sA/r0gXp0hToyVfJK288Xn7zs877jy7PgPKuMEyoCYlnj5XAvfK/q/gg7/3a76brkMJm1JlBLHibVILjaQnBWoybzbrRQbRN6CKVDHOM5cbOD7WWJ2NLh6wDfHyC1RMn6J4kYYOTv7U0bRgM/qZPZsPXyj5Fvism6it/XfNVqN6zJaufEtPvqlLaoSG64SZUblxOGHUb76G0KV1KCycRgsNwdRBuxgGrJ1dh9t7JsEVvg8L0OuMiFnnGganoZ1gTr8muV6X/XeyLKU+Q/csa/eDLE+66aOGW0EUVge0Ze6Dq2fKVRTsvgV2/OwPTd6S8dFLViEG4/JHhoPsz0+hfJwuVVGYUBpYnjGzAXnwiM2Xkqe8LnwpzLDX2r0ngPpqgPvf8lVdqnfMMEOT90Qtw2dxLiKDWdyZ7dX4sPeK0Fbn9G4+nyFrk23BV9vBwgeL/BY70ZjjrnVyqR6cfUkQxyKlzcHE36UeNqzYyJ5ZZj/hf6LMxxh66J8uvAVzGqc3cjojDKFDk1maFV5UZyHphJviQB64tDgE7c+ZBpYTU9icljEVPhiX6L4EAeJsBPLSRbRlfcCSlrs5QYUetsWVxgmwJkAn/qmRu8e7YcCboddTys+0cZi9UU9Ul+n7l+hjvoKOv0V0ZqoXjigj5O5xD8FmGUVaS3ciuTG/xLOvAyUGbVBLwcqCHjVBrcldae6WBbrZQjkTC5JtHLJ+9SDXBM7MqRgvHcLrbNZYHwPHXH4k2DZkbceLNEXxPipmfgF4hKxI2HVXTsPE1DIZ8otfQxrtKM9tE6roEyk9a7hlsXTtsGCQ73bE3Pj3f9ZkrbzK8JrzxTm8RsfVQ7N7GFkisx3gQ3aAkcihB3In4JXsq+zXVLjA4ykkrs/POKLkT/JcULVXpIB23xsv9VMm7mkf1D65rsRslcdrlUZtmGMYLHvaDyEEXVfWa8OURm28fCtnCmHHf6+sVQ57WGJKEpt9DHKWfNpclMMg1Et2liWQ5vxJLAkmRnsN4JdbZzEVhTnc5KNvYJ/s01t6gz3nY2PjkKX+fcKJ2v0w1KkPTGNyju7M5ftEFzSuypFnqJ/pKotCqrO7W5BA9N7cGbu1JpvG6txMH7+s6GJDF6ISLmOz7lCg4CfsuQBYOyRHgZPOmf7d3G8XboBHfSk7jhur1aqf6crqKG5ZpzwEF+46wkCmbiUz9nBWMizorlYlU/8rqmA39SmZmk0XsXlukHgyY77gDXi3iHF5d4GVYYR98xSX7WTfYJnLsNMAun4acVkvYIp5BjdfPTliv4rvzt4P0+NKoGAAVVIFufx/5jqN6pnv0DmrqmHQWCViDLoC+Cy1zQaGQeUZA5s0tHoNkGN4sN7XVe/A4GCvZz6xiaKLSF53ZynBz5efhbZP3on1X4rptLdu1o2NSOqKjxzEja8cpoA7OL8esmJEV7PLVjIwAsZzEF9Xs8op9v7qPyUPWds7Tpov+oBuqTWUK5cByrCQDfJsKNkQtMFgkm4o8sMXsRHuyq+hRKJpBAZ74aTAYmDNw7qDt0BvTBhh+ZN6CU8qijBD/GJ9S+LFnIwFf8e7/8ZVm7zcNxnM5Gf8CYPGNGOfKn2D7mIOUzVmbN1sUtKk29m1v7BWrI37lbF5F+ghWLD4VpyoFa7xL2SRCwhnUKPUUF7uMhX6smANXTeveYsoVxOw5RICvV8spvbpbrEdc/F/ihuf+rhJdDucz/VDWqiBewPPAnxS1E0hbbyYKg0o1YXX+lvoSALC5K5wBM4/dLtyls0yU2h4fs35Q5wRmB8LJfS4ewbW25Dto+8Z6DSe1kvjCn3iCvXS0+A5+Zgh5zOknsCnEfQjaAPmJ+TL2ROz5RCOimHrwZp3vfoQpscpoKBjLnxtdS3Qw/ISu6kD87kwdgTCoT3iu6HZkv8MO/6fPXjcO8zv/7n3uIiQ/1K0/gLe4tDj5GuhV8kB9OfuIdfIb2el89rOHWhm6187T/NEgj8c9BnErmBB9QKbvsqJhu9+oUnxAxOsONMTWHhEwJzuxdOzPb6a2gGdDw1c7JW1rs5QYjHNfezBPLGYPsG65Ubs7voER143MetnRlOdsUub+lOvun4TzWEUwOWKOqLds2boqvHA+hY7NtwgrbhfZL9oze/Rss/7YaXbcM/AzqduLN3KwtWwlVbvLLfieu3s7Rda8QQFesSE9oGrSvVOE/6d9pwhjWp1hrRUGMm709ON2QGWLJRG9ASGgw3W7pzzxgCgtNnoT/bsppEmAKc9iRY57tXru6KscWbLMDh8C24lWc0qahN5MZbGmehsID7fSGuwy3k5Rsx0V6WHin3CpKpU938OtECk/Q99DM4WneHAo3yRQMCDPdMC1nbHZraK08ZScgqoprxmSNFEmB5X6FFwWiSmXuCrKW1SEGWn3DUIvm8EbTzv+1GM9BByb6wS8RLu5QGx4Jthq/fzHXi+g/ABzxdwHvxhiZUhbtHAA4B1nwPjs18Ay6B8N08g+mJZZb8Zp7NErxm6VZ1rzir7xnkL2Y1oHQAVZesTHTSFW7TmD8idxvSV+7WsrbHfxYK+zKNGb5d9GOP+6cg8Mk0JLmMC0zCq3bt5iwgVfxamhvScAgEAxMILpLlDGU/9AQ72rd6/fqbzL+Vs3xqRQQMG0hzQ1cc8aoBWXDPeHHfAfjnkhuJpfQPipEiPKsJZCX0rtHyc7QfpIrHHdop60XXjlrz7IFywwN0Dcew8PeUDsAOGQxp0gxyoglaYxpJllbBoeFRxkHtpxNHwOmYJ5XYlft/4XVSuvgkCOL5tSQxRnW8ZLSJto1ljcKmlU9idzhrKRxOYXidu79wq2uU23KCm7zfWonIzn77MdbazkPJd7SkpqwJEUsEhASdn/+0SANBjv4cHVHsxChDuNIbDov2IUTk6LroI+HEDdKOiu9f0VIvKHUS5Jjp3JHHYk4h29W9Cwf7rQx/H54bH9F9u95vD4IuOvp+BEZm8TJy9AQxnqXo2UgnIeDn1wz6xussbBYD+RVmJahwxqlh7EXkDkDtGVBwNyDIFb777gSQw/Y+KJ+UwAJAzsX5k4wJPGjFxBNMAqq4YwLcq9c8GAbEUINWrAcJY4vFDoyhaqLQCqV+ICIqFAMQjQFS1OELoFmvtVwsxnM7mCoZ06N7VtwBBqhcHdLpCAqm/KKlQQGP34P4zhQ6AazjR9Zrt2BIqAM7IIVfLs39OD6b7gAuK8N2aVxVudriyVi3i9gXRPdAlQYOck+yOwamBgQKG5Dm+Fs7uAHju4nmvnXO3YgICt7IIKNRyETBBdhNwBJ8JBLehEkje5dvdkdrQoWXIjgEA3FADAeErkYByX2YCJlkDAUd1g0AghSWQwos799zFNT8riVSr0apOGadSDcgiFIlExsEWI4Yx9lpnMoEqNmbSs6G9RDFXh8Vr1KjGNtTVMzufU2VqyWVcmIpUM5GhiUyHTP3YVKcJ7Q5kUsRVK+6cyabScycLJeBK2as41GHloSVocxiR5wf+SzlzUST5pwgPRVBlI+ttVSXFrrKammpLY0uz9ZTnO3XSIr2x+ZHMb2OEPqdGdSiXwlCBa7afKkedLTQ6KGS9KBSs3B80wGAHnZ0audhsDnwXvXQHIAYTGxtHimwZcmVKWRhiQRndBxeTbDYnJTsjNzNlOSTlGAuVtEFByQsQbqCOjDjnuLDUKzsW/MmEA5ryVSuuBrCRFa4bOmkNuEGNZCws9YpOckwN61qPqf5EMqpqdZxYNKRUwIeUq3+2uaW+zPVxw1tppCpwjWgG0ws3sKKB9s7HalP6tf+aAhBhQhkXvHgj8EHky88M/kgCBAoSbCayEKHChKOgookQKUo0OgYmFrYYHLHicMVLkChJshSpeNKk4xMQEhGTkJKRU1DKoKKWSSOLlo6eQbYcufIY5TMxs7AqUMhmgw6dThjxrS5D+o3bYkKfJxZa6nd/GDSqxzkv/Ga1rT7502fr7XDZlJ3siizicFWxS6646ZrrbvhOibtuuW0Xp18t9sA995X6wU96lStToZJLlbWq1apRp16jBk2afa/FLK1mazPHYeu0m2ue+X70s6Me2m2PR557bK99DjrkvP0OuKDbSacC+ji4v+thyHTnAnwQAAAA) format('woff2'); } -body { - border: 0; - height: 100%; -} - t { pointer-events: none; } @@ -76,10 +71,14 @@ input, button, select, a, textarea { outline: none; } -button, select, a, .pointer { +button, select, a { cursor: pointer; } +.pointer { + cursor: pointer !important; +} + #prec text { font-size: 32px; stroke: none; @@ -101,7 +100,7 @@ button, select, a, .pointer { fill-rule: evenodd; } -#lakes, #coastline, #armies, #ice { +#lakes, #coastline, #armies, #ice, #emblems { cursor: pointer; } @@ -130,9 +129,26 @@ button, select, a, .pointer { #armies text { pointer-events: none; user-select: none; + stroke: none; + fill: #fff; + text-shadow: 0 0 4px #000; + dominant-baseline: central; + text-anchor: middle; + font-family: Helvetica; + fill-opacity: 1; } -#statesBody, #provincesBody, #relig, #biomes, #cults { +#armies text.regimentIcon { + font-size: .8em; +} + +#statesBody, #provincesBody { + stroke-width: 2; + fill-rule: evenodd; + mask: url(#land); +} + +#relig, #biomes, #cults { stroke-width: .6; fill-rule: evenodd; mask: url(#land); @@ -141,7 +157,6 @@ button, select, a, .pointer { #statesHalo { fill: none; filter: url(#blur5); - /*animation: hideshow 3s infinite;*/ } #borders { @@ -156,11 +171,6 @@ button, select, a, .pointer { font-size: 9px; } -@keyframes hideshow { - 0% {stroke-width: 1;} - 50% {stroke-width: 10;} -} - #rivers { stroke: none; mask: url(#land); @@ -707,6 +717,15 @@ fieldset { width: 6%; } +.emblemShapePreview { + width: 1.5em; + height: 1.5em; + margin: -.4em .1em; + fill: #fff; + stroke: #000; + stroke-width: 5px; +} + #styleContent table { border-spacing: 0; margin-left: .2em; @@ -1334,7 +1353,7 @@ div.states>.small { } div.states>.cultureName { - width: 5em; + width: 7em; } div.states>.culturePopulation { @@ -1342,10 +1361,7 @@ div.states>.culturePopulation { cursor: pointer; } -div.states > .cultureBase, -div.states > .cultureType, -div.states > .stateCulture, -div.states > .diplomacyRelations { +div.states > select { width: 4.6em; cursor: pointer; border: 0; @@ -1355,10 +1371,7 @@ div.states > .diplomacyRelations { appearance: none; } -div.states > .cultureBase { - width: 6em; -} - +div.states > .cultureBase, div.states > .burgName, div.states > .burgState, div.states > .burgCulture { @@ -1391,6 +1404,18 @@ div.states>input.riverType { width: 5em; } +div.states > .coaIcon { + stroke-width: 3; + width: 1.4em; + height: 1.4em; + margin: -.3em 0; + cursor: pointer; +} + +div.states > .coaIcon > use { + pointer-events: none; +} + #diplomacyBodySection > div { cursor: pointer; } @@ -1422,9 +1447,13 @@ div.states>input.riverType { line-height: 1.4em; } +#burgBody div.label { + display: inline-block; + width: 6em; +} + #stateNameEditor div.label, #provinceNameEditor div.label, -#burgBody div.label, #regimentBody div.label { display: inline-block; width: 5.5em; @@ -1439,9 +1468,7 @@ div.states>input.riverType { } .burgFeature { - font-size: 1.2em; - padding: 1px 2px; - color: #555; + padding: 1px; cursor: pointer; } @@ -1492,12 +1519,10 @@ div.states.active { } div.states.Self { - border-color: #858b8e; - background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%); - font-style: italic; - font-weight: bold; margin-bottom: .2em; cursor: default !important; + padding: .2em 0 0 .5em; + font-weight: bold; } div.states button.selectCapital { @@ -1533,6 +1558,44 @@ rect.fillRect { width: 5em; } +#emblemBody > div { + padding: 1px 3px; + transition: all .3s ease-out; +} + +#emblemBody > div.active { + background-color: #54ca7728; +} + +#emblemArmiger { + text-align: center; + display: block; +} + +#emblemBody .label { + width: 6em; + display: inline-block; +} + +#emblemBody select { + width: 9em; +} + +#emblemsBottom { + margin-top: 4px; + text-align: center; +} + +#emblemUploadControl, +#emblemDownloadControl { + margin-top: .3em; + text-align: center; +} + +#emblemDownloadControl > input { + width: 4.1em; +} + #picker text { cursor: default; } @@ -1934,8 +1997,8 @@ svg.button { } #alertMessage ul { - padding-left: 15px; - margin: 10px 0; + padding-left: 1.2em; + margin: 1em 0; } .pseudoLink { @@ -2079,6 +2142,11 @@ svg.button { border: dashed 1px #5d4651; } +.speaker { + font-size: .9em; + cursor: pointer; +} + #prompt { position: absolute; left: 50%; diff --git a/index.html b/index.html index 7f787126..2218f7a5 100644 --- a/index.html +++ b/index.html @@ -32,8 +32,8 @@ #loading-text span:nth-child(3), #mapOverlay > span:nth-child(3) {animation-delay: 2s;} @keyframes blink {0% {opacity: 0;} 20% {opacity: 1;} 100% {opacity: .1;}} - - + + @@ -142,67 +142,56 @@ + - - - - - - - - - - - - - + @@ -213,16 +202,3171 @@ + - - - Port - - - - + + + + + + ? + + + + + + + + + + + + + + +
+
Azgaar's
+
Fantasy Map Generator
+
v. 1.5
+

LOADING...

+
+ +
+ +
+ + +
+ + + +
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ +
Сlick the arrow button for options. Zoom in to see the map in details
+ + + + + + + + + + + + + + + + + + Port + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -794,60 +3938,18 @@ - - - - - ? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + @@ -869,2830 +3971,17 @@ - + - +
- + - - - - + - - - - - - - -
-
Azgaar's
-
Fantasy Map Generator
-
v. 1.4
-

LOADING...

-
- -
- -
- - -
- - - -
- -
-
- -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-
- -
Сlick the arrow button for options. Zoom in to see the map in details
- - - - - @@ -3709,10 +3998,11 @@ + - + - + @@ -3720,7 +4010,7 @@ - + @@ -3751,9 +4041,11 @@ + + - + - + diff --git a/lang/lang-en.js b/lang/lang-en.js index d4da51a9..b3361d23 100644 --- a/lang/lang-en.js +++ b/lang/lang-en.js @@ -212,7 +212,7 @@ const sourceDataForReference = { burgEditIconStyle: "Edit icon style for burg group in Style Editor", burgEditAnchorStyle: "Edit port icon (anchor) style for burg group in Style Editor", burgSeeInMFCG: "Open burg in the Medieval Fantasy City Generator by Watabou. Ctrl + click to change the seed", - burgOpenCOA: "Open burg's COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed", + burgOpenCOA: "Open burg's COA. Ctrl + click to change the seed", burgRelocate: "Relocate burg", burglLegend: "Edit free text notes (legend) for this burg", burgRemove: "Remove non-capital burg. Shortcut: Delete", diff --git a/libs/alea.min.js b/libs/alea.min.js new file mode 100644 index 00000000..f4ecef67 --- /dev/null +++ b/libs/alea.min.js @@ -0,0 +1,3 @@ +/*https://github.com/macmcmeans/aleaPRNG/blob/master/aleaPRNG-1.1.js +©2010 Johannes Baagøe, MIT license; Derivative ©2017-2020 W. Mac" McMeans, BSD license.*/ +const aleaPRNG=function(){return function(n){"use strict";var r,t,e,o,a,u=new Uint32Array(3),i="";function c(n){var a=function(){var n=4022871197,r=function(r){r=r.toString();for(var t=0,e=r.length;t>>0,n=(o*=n)>>>0,n+=4294967296*(o-=n)}return 2.3283064365386963e-10*(n>>>0)};return r.version="Mash 0.9",r}();r=a(" "),t=a(" "),e=a(" "),o=1;for(var u=0;uarguments[1]&&(n=arguments[1],r=arguments[0]),f(n)&&f(r)?Math.floor(l()*(r-n+1))+n:l()*(r-n)+n},l.restart=function(){c(a)},l.seed=function(){c(Array.prototype.slice.call(arguments))},l.version=function(){return"aleaPRNG 1.1.0"},l.versions=function(){return"aleaPRNG 1.1.0, "+i},0===n.length&&(window.crypto.getRandomValues(u),n=[u[0],u[1],u[2]]),a=n,c(n),l}(Array.prototype.slice.call(arguments))}; \ No newline at end of file diff --git a/libs/jquery-ui.css b/libs/jquery-ui.css index 927bfc9b..8d1b6fcc 100644 --- a/libs/jquery-ui.css +++ b/libs/jquery-ui.css @@ -434,6 +434,9 @@ body .ui-dialog { font-family: Arial,Helvetica,sans-serif; font-size: 1em; } +.ui-widget button[class^="icon-"] { + padding: 1px 6px; +} .ui-widget.ui-widget-content { border: 1px solid #5e4fa2; color: #333333; diff --git a/libs/lineclip.js b/libs/lineclip.js deleted file mode 100644 index b43da33b..00000000 --- a/libs/lineclip.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; -// lineclip by mourner, https://github.com/mapbox/lineclip -// Cohen-Sutherland line clippign algorithm, adapted to efficiently -// handle polylines rather than just segments -function lineclip(points, bbox, result) { - var len = points.length, - codeA = bitCode(points[0], bbox), - part = [], - i, a, b, codeB, lastCode; - if (!result) result = []; - - for (i = 1; i < len; i++) { - a = points[i - 1]; - b = points[i]; - codeB = lastCode = bitCode(b, bbox); - - while (true) { - if (!(codeA | codeB)) { // accept - part.push(a); - - if (codeB !== lastCode) { // segment went outside - part.push(b); - if (i < len - 1) { // start a new line - result.push(part); - part = []; - } - } else if (i === len - 1) { - part.push(b); - } - break; - - } else if (codeA & codeB) { // trivial reject - break; - } else if (codeA) { // a outside, intersect with clip edge - a = intersect(a, b, codeA, bbox); - codeA = bitCode(a, bbox); - } else { // b outside - b = intersect(a, b, codeB, bbox); - codeB = bitCode(b, bbox); - } - } - codeA = lastCode; - } - - if (part.length) result.push(part); - - return result; -} - -// Sutherland-Hodgeman polygon clipping algorithm -function polygonclip(points, bbox, secure = 0) { - var result, edge, prev, prevInside, inter, i, p, inside; - - // clip against each side of the clip rectangle - for (edge = 1; edge <= 8; edge *= 2) { - result = []; - prev = points[points.length-1]; - prevInside = !(bitCode(prev, bbox) & edge); - - for (i = 0; i < points.length; i++) { - p = points[i]; - inside = !(bitCode(p, bbox) & edge); - inter = inside !== prevInside; // segment goes through the clip window - - const pi = intersect(prev, p, edge, bbox); - if (inter) result.push(pi); // add an intersection point - if (secure && inter) result.push(pi, pi); // add additional intersection points to secure correct d3 curve - if (inside) result.push(p); // add a point if it's inside - - prev = p; - prevInside = inside; - } - points = result; - if (!points.length) break; - } - //result.forEach(p => debug.append("circle").attr("cx", p[0]).attr("cy", p[1]).attr("r", .6).attr("fill", "red")); - return result; -} - -// intersect a segment against one of the 4 lines that make up the bbox -function intersect(a, b, edge, bbox) { - return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top - edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom - edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right - edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : null; // left -} - -// bit code reflects the point position relative to the bbox: -// left mid right -// top 1001 1000 1010 -// mid 0001 0000 0010 -// bottom 0101 0100 0110 -function bitCode(p, bbox) { - var code = 0; - - if (p[0] < bbox[0]) code |= 1; // left - else if (p[0] > bbox[2]) code |= 2; // right - - if (p[1] < bbox[1]) code |= 4; // bottom - else if (p[1] > bbox[3]) code |= 8; // top - - return code; -} \ No newline at end of file diff --git a/libs/lineclip.min.js b/libs/lineclip.min.js new file mode 100644 index 00000000..d1796476 --- /dev/null +++ b/libs/lineclip.min.js @@ -0,0 +1,2 @@ +// lineclip by mourner, https://github.com/mapbox/lineclip +"use strict";function lineclip(t,e,n){var r,i,u,o,s,h=t.length,c=bitCode(t[0],e),f=[];for(n=n||[],r=1;re[2]&&(n|=2),t[1]e[3]&&(n|=8),n} \ No newline at end of file diff --git a/libs/pell.min.js b/libs/pell.min.js new file mode 100644 index 00000000..c5be57e5 --- /dev/null +++ b/libs/pell.min.js @@ -0,0 +1,2 @@ +// https://github.com/jaredreich/pell, MIT License +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Pell=t()}(this,function(){"use strict";const e=(e,t,n)=>e.addEventListener(t,n),t=(e,t)=>e.appendChild(t),n=e=>document.createElement(e),i=e=>document.queryCommandState(e),o=(e,t=null)=>document.execCommand(e,!1,t),l={bold:{icon:"B",title:"Bold",state:()=>i("bold"),result:()=>o("bold")},italic:{icon:"I",title:"Italic",state:()=>i("italic"),result:()=>o("italic")},underline:{icon:"U",title:"Underline",state:()=>i("underline"),result:()=>o("underline")},strikethrough:{icon:"S",title:"Strike-through",state:()=>i("strikeThrough"),result:()=>o("strikeThrough")},heading1:{icon:"H1",title:"Heading 1",result:()=>o("formatBlock","

")},heading2:{icon:"H2",title:"Heading 2",result:()=>o("formatBlock","

")},paragraph:{icon:"¶",title:"Paragraph",result:()=>o("formatBlock","

")},quote:{icon:"“ ”",title:"Quote",result:()=>o("formatBlock","

")},olist:{icon:"#",title:"Ordered List",result:()=>o("insertOrderedList")},ulist:{icon:"•",title:"Unordered List",result:()=>o("insertUnorderedList")},code:{icon:"</>",title:"Code",result:()=>o("formatBlock","
")},line:{icon:"―",title:"Horizontal Line",result:()=>o("insertHorizontalRule")},link:{icon:"🔗",title:"Link",result:()=>navigator.clipboard.readText().then(e=>o("createLink",e))},image:{icon:"📷",title:"Image",result:()=>{navigator.clipboard.readText().then(e=>o("insertImage",e)),o("enableObjectResizing")}}},r={actionbar:"pell-actionbar",button:"pell-button",content:"pell-content",selected:"pell-button-selected"};return{exec:o,init:i=>{const a=i.actions?i.actions.map(e=>"string"==typeof e?l[e]:l[e.name]?{...l[e.name],...e}:e):Object.keys(l).map(e=>l[e]),s={...r,...i.classes},c=i.defaultParagraphSeparator||"div",u=n("div");u.className=s.actionbar,t(i.element,u);const d=i.element.content=n("div");return d.contentEditable=!0,d.className=s.content,d.oninput=(({target:{firstChild:e}})=>{e&&3===e.nodeType?o("formatBlock",`<${c}>`):"
"===d.innerHTML&&(d.innerHTML=""),i.onChange(d.innerHTML)}),d.onkeydown=(e=>{"Enter"===e.key&&"blockquote"===(e=>document.queryCommandValue(e))("formatBlock")&&setTimeout(()=>o("formatBlock",`<${c}>`),0)}),t(i.element,d),a.forEach(i=>{const o=n("button");if(o.className=s.button,o.innerHTML=i.icon,o.title=i.title,o.setAttribute("type","button"),o.onclick=(()=>i.result()&&d.focus()),i.state){const t=()=>o.classList[i.state()?"add":"remove"](s.selected);e(d,"keyup",t),e(d,"mouseup",t),e(o,"click",t)}t(u,o)}),i.styleWithCSS&&o("styleWithCSS"),o("defaultParagraphSeparator",c),i.element}}}); \ No newline at end of file diff --git a/libs/rgbquant.js b/libs/rgbquant.js deleted file mode 100644 index bc0bd1b9..00000000 --- a/libs/rgbquant.js +++ /dev/null @@ -1,935 +0,0 @@ -/* -* Copyright (c) 2015, Leon Sorokin -* All rights reserved. (MIT Licensed) -* -* RgbQuant.js - an image quantization lib -*/ - -(function(){ - function RgbQuant(opts) { - opts = opts || {}; - - // 1 = by global population, 2 = subregion population threshold - this.method = opts.method || 2; - // desired final palette size - this.colors = opts.colors || 256; - // # of highest-frequency colors to start with for palette reduction - this.initColors = opts.initColors || 4096; - // color-distance threshold for initial reduction pass - this.initDist = opts.initDist || 0.01; - // subsequent passes threshold - this.distIncr = opts.distIncr || 0.005; - // palette grouping - this.hueGroups = opts.hueGroups || 10; - this.satGroups = opts.satGroups || 10; - this.lumGroups = opts.lumGroups || 10; - // if > 0, enables hues stats and min-color retention per group - this.minHueCols = opts.minHueCols || 0; - // HueStats instance - this.hueStats = this.minHueCols ? new HueStats(this.hueGroups, this.minHueCols) : null; - - // subregion partitioning box size - this.boxSize = opts.boxSize || [64,64]; - // number of same pixels required within box for histogram inclusion - this.boxPxls = opts.boxPxls || 2; - // palette locked indicator - this.palLocked = false; - // palette sort order -// this.sortPal = ['hue-','lum-','sat-']; - - // dithering/error diffusion kernel name - this.dithKern = opts.dithKern || null; - // dither serpentine pattern - this.dithSerp = opts.dithSerp || false; - // minimum color difference (0-1) needed to dither - this.dithDelta = opts.dithDelta || 0; - - // accumulated histogram - this.histogram = {}; - // palette - rgb triplets - this.idxrgb = opts.palette ? opts.palette.slice(0) : []; - // palette - int32 vals - this.idxi32 = []; - // reverse lookup {i32:idx} - this.i32idx = {}; - // {i32:rgb} - this.i32rgb = {}; - // enable color caching (also incurs overhead of cache misses and cache building) - this.useCache = opts.useCache !== false; - // min color occurance count needed to qualify for caching - this.cacheFreq = opts.cacheFreq || 10; - // allows pre-defined palettes to be re-indexed (enabling palette compacting and sorting) - this.reIndex = opts.reIndex || this.idxrgb.length == 0; - // selection of color-distance equation - this.colorDist = opts.colorDist == "manhattan" ? distManhattan : distEuclidean; - - // if pre-defined palette, build lookups - if (this.idxrgb.length > 0) { - var self = this; - this.idxrgb.forEach(function(rgb, i) { - var i32 = ( - (255 << 24) | // alpha - (rgb[2] << 16) | // blue - (rgb[1] << 8) | // green - rgb[0] // red - ) >>> 0; - - self.idxi32[i] = i32; - self.i32idx[i32] = i; - self.i32rgb[i32] = rgb; - }); - } - } - - // gathers histogram info - RgbQuant.prototype.sample = function sample(img, width) { - if (this.palLocked) - throw "Cannot sample additional images, palette already assembled."; - - var data = getImageData(img, width); - - switch (this.method) { - case 1: this.colorStats1D(data.buf32); break; - case 2: this.colorStats2D(data.buf32, data.width); break; - } - }; - - // image quantizer - // todo: memoize colors here also - // @retType: 1 - Uint8Array (default), 2 - Indexed array, 3 - Match @img type (unimplemented, todo) - RgbQuant.prototype.reduce = function reduce(img, retType, dithKern, dithSerp) { - if (!this.palLocked) - this.buildPal(); - - dithKern = dithKern || this.dithKern; - dithSerp = typeof dithSerp != "undefined" ? dithSerp : this.dithSerp; - - retType = retType || 1; - - // reduce w/dither - if (dithKern) - var out32 = this.dither(img, dithKern, dithSerp); - else { - var data = getImageData(img), - buf32 = data.buf32, - len = buf32.length, - out32 = new Uint32Array(len); - - for (var i = 0; i < len; i++) { - var i32 = buf32[i]; - out32[i] = this.nearestColor(i32); - } - } - - if (retType == 1) - return new Uint8Array(out32.buffer); - - if (retType == 2) { - var out = [], - len = out32.length; - - for (var i = 0; i < len; i++) { - var i32 = out32[i]; - out[i] = this.i32idx[i32]; - } - - return out; - } - }; - - // adapted from http://jsbin.com/iXofIji/2/edit by PAEz - RgbQuant.prototype.dither = function(img, kernel, serpentine) { - // http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/ - var kernels = { - FloydSteinberg: [ - [7 / 16, 1, 0], - [3 / 16, -1, 1], - [5 / 16, 0, 1], - [1 / 16, 1, 1] - ], - FalseFloydSteinberg: [ - [3 / 8, 1, 0], - [3 / 8, 0, 1], - [2 / 8, 1, 1] - ], - Stucki: [ - [8 / 42, 1, 0], - [4 / 42, 2, 0], - [2 / 42, -2, 1], - [4 / 42, -1, 1], - [8 / 42, 0, 1], - [4 / 42, 1, 1], - [2 / 42, 2, 1], - [1 / 42, -2, 2], - [2 / 42, -1, 2], - [4 / 42, 0, 2], - [2 / 42, 1, 2], - [1 / 42, 2, 2] - ], - Atkinson: [ - [1 / 8, 1, 0], - [1 / 8, 2, 0], - [1 / 8, -1, 1], - [1 / 8, 0, 1], - [1 / 8, 1, 1], - [1 / 8, 0, 2] - ], - Jarvis: [ // Jarvis, Judice, and Ninke / JJN? - [7 / 48, 1, 0], - [5 / 48, 2, 0], - [3 / 48, -2, 1], - [5 / 48, -1, 1], - [7 / 48, 0, 1], - [5 / 48, 1, 1], - [3 / 48, 2, 1], - [1 / 48, -2, 2], - [3 / 48, -1, 2], - [5 / 48, 0, 2], - [3 / 48, 1, 2], - [1 / 48, 2, 2] - ], - Burkes: [ - [8 / 32, 1, 0], - [4 / 32, 2, 0], - [2 / 32, -2, 1], - [4 / 32, -1, 1], - [8 / 32, 0, 1], - [4 / 32, 1, 1], - [2 / 32, 2, 1], - ], - Sierra: [ - [5 / 32, 1, 0], - [3 / 32, 2, 0], - [2 / 32, -2, 1], - [4 / 32, -1, 1], - [5 / 32, 0, 1], - [4 / 32, 1, 1], - [2 / 32, 2, 1], - [2 / 32, -1, 2], - [3 / 32, 0, 2], - [2 / 32, 1, 2], - ], - TwoSierra: [ - [4 / 16, 1, 0], - [3 / 16, 2, 0], - [1 / 16, -2, 1], - [2 / 16, -1, 1], - [3 / 16, 0, 1], - [2 / 16, 1, 1], - [1 / 16, 2, 1], - ], - SierraLite: [ - [2 / 4, 1, 0], - [1 / 4, -1, 1], - [1 / 4, 0, 1], - ], - }; - - if (!kernel || !kernels[kernel]) { - throw 'Unknown dithering kernel: ' + kernel; - } - - var ds = kernels[kernel]; - - var data = getImageData(img), -// buf8 = data.buf8, - buf32 = data.buf32, - width = data.width, - height = data.height, - len = buf32.length; - - var dir = serpentine ? -1 : 1; - - for (var y = 0; y < height; y++) { - if (serpentine) - dir = dir * -1; - - var lni = y * width; - - for (var x = (dir == 1 ? 0 : width - 1), xend = (dir == 1 ? width : 0); x !== xend; x += dir) { - // Image pixel - var idx = lni + x, - i32 = buf32[idx], - r1 = (i32 & 0xff), - g1 = (i32 & 0xff00) >> 8, - b1 = (i32 & 0xff0000) >> 16; - - // Reduced pixel - var i32x = this.nearestColor(i32), - r2 = (i32x & 0xff), - g2 = (i32x & 0xff00) >> 8, - b2 = (i32x & 0xff0000) >> 16; - - buf32[idx] = - (255 << 24) | // alpha - (b2 << 16) | // blue - (g2 << 8) | // green - r2; - - // dithering strength - if (this.dithDelta) { - var dist = this.colorDist([r1, g1, b1], [r2, g2, b2]); - if (dist < this.dithDelta) - continue; - } - - // Component distance - var er = r1 - r2, - eg = g1 - g2, - eb = b1 - b2; - - for (var i = (dir == 1 ? 0 : ds.length - 1), end = (dir == 1 ? ds.length : 0); i !== end; i += dir) { - var x1 = ds[i][1] * dir, - y1 = ds[i][2]; - - var lni2 = y1 * width; - - if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) { - var d = ds[i][0]; - var idx2 = idx + (lni2 + x1); - - var r3 = (buf32[idx2] & 0xff), - g3 = (buf32[idx2] & 0xff00) >> 8, - b3 = (buf32[idx2] & 0xff0000) >> 16; - - var r4 = Math.max(0, Math.min(255, r3 + er * d)), - g4 = Math.max(0, Math.min(255, g3 + eg * d)), - b4 = Math.max(0, Math.min(255, b3 + eb * d)); - - buf32[idx2] = - (255 << 24) | // alpha - (b4 << 16) | // blue - (g4 << 8) | // green - r4; // red - } - } - } - } - - return buf32; - }; - - // reduces histogram to palette, remaps & memoizes reduced colors - RgbQuant.prototype.buildPal = function buildPal(noSort) { - if (this.palLocked || this.idxrgb.length > 0 && this.idxrgb.length <= this.colors) return; - - var histG = this.histogram, - sorted = sortedHashKeys(histG, true); - - if (sorted.length == 0) - throw "Nothing has been sampled, palette cannot be built."; - - switch (this.method) { - case 1: - var cols = this.initColors, - last = sorted[cols - 1], - freq = histG[last]; - - var idxi32 = sorted.slice(0, cols); - - // add any cut off colors with same freq as last - var pos = cols, len = sorted.length; - while (pos < len && histG[sorted[pos]] == freq) - idxi32.push(sorted[pos++]); - - // inject min huegroup colors - if (this.hueStats) - this.hueStats.inject(idxi32); - - break; - case 2: - var idxi32 = sorted; - break; - } - - // int32-ify values - idxi32 = idxi32.map(function(v){return +v;}); - - this.reducePal(idxi32); - - if (!noSort && this.reIndex) - this.sortPal(); - - // build cache of top histogram colors - if (this.useCache) - this.cacheHistogram(idxi32); - - this.palLocked = true; - }; - - RgbQuant.prototype.palette = function palette(tuples, noSort) { - this.buildPal(noSort); - return tuples ? this.idxrgb : new Uint8Array((new Uint32Array(this.idxi32)).buffer); - }; - - RgbQuant.prototype.prunePal = function prunePal(keep) { - var i32; - - for (var j = 0; j < this.idxrgb.length; j++) { - if (!keep[j]) { - i32 = this.idxi32[j]; - this.idxrgb[j] = null; - this.idxi32[j] = null; - delete this.i32idx[i32]; - } - } - - // compact - if (this.reIndex) { - var idxrgb = [], - idxi32 = [], - i32idx = {}; - - for (var j = 0, i = 0; j < this.idxrgb.length; j++) { - if (this.idxrgb[j]) { - i32 = this.idxi32[j]; - idxrgb[i] = this.idxrgb[j]; - i32idx[i32] = i; - idxi32[i] = i32; - i++; - } - } - - this.idxrgb = idxrgb; - this.idxi32 = idxi32; - this.i32idx = i32idx; - } - }; - - // reduces similar colors from an importance-sorted Uint32 rgba array - RgbQuant.prototype.reducePal = function reducePal(idxi32) { - // if pre-defined palette's length exceeds target - if (this.idxrgb.length > this.colors) { - // quantize histogram to existing palette - var len = idxi32.length, keep = {}, uniques = 0, idx, pruned = false; - - for (var i = 0; i < len; i++) { - // palette length reached, unset all remaining colors (sparse palette) - if (uniques == this.colors && !pruned) { - this.prunePal(keep); - pruned = true; - } - - idx = this.nearestIndex(idxi32[i]); - - if (uniques < this.colors && !keep[idx]) { - keep[idx] = true; - uniques++; - } - } - - if (!pruned) { - this.prunePal(keep); - pruned = true; - } - } - // reduce histogram to create initial palette - else { - // build full rgb palette - var idxrgb = idxi32.map(function(i32) { - return [ - (i32 & 0xff), - (i32 & 0xff00) >> 8, - (i32 & 0xff0000) >> 16, - ]; - }); - - var len = idxrgb.length, - palLen = len, - thold = this.initDist; - - // palette already at or below desired length - if (palLen > this.colors) { - while (palLen > this.colors) { - var memDist = []; - - // iterate palette - for (var i = 0; i < len; i++) { - var pxi = idxrgb[i], i32i = idxi32[i]; - if (!pxi) continue; - - for (var j = i + 1; j < len; j++) { - var pxj = idxrgb[j], i32j = idxi32[j]; - if (!pxj) continue; - - var dist = this.colorDist(pxi, pxj); - - if (dist < thold) { - // store index,rgb,dist - memDist.push([j, pxj, i32j, dist]); - - // kill squashed value - delete(idxrgb[j]); - palLen--; - } - } - } - - // palette reduction pass - // console.log("palette length: " + palLen); - - // if palette is still much larger than target, increment by larger initDist - thold += (palLen > this.colors * 3) ? this.initDist : this.distIncr; - } - - // if palette is over-reduced, re-add removed colors with largest distances from last round - if (palLen < this.colors) { - // sort descending - sort.call(memDist, function(a,b) { - return b[3] - a[3]; - }); - - var k = 0; - while (palLen < this.colors) { - // re-inject rgb into final palette - idxrgb[memDist[k][0]] = memDist[k][1]; - - palLen++; - k++; - } - } - } - - var len = idxrgb.length; - for (var i = 0; i < len; i++) { - if (!idxrgb[i]) continue; - - this.idxrgb.push(idxrgb[i]); - this.idxi32.push(idxi32[i]); - - this.i32idx[idxi32[i]] = this.idxi32.length - 1; - this.i32rgb[idxi32[i]] = idxrgb[i]; - } - } - }; - - // global top-population - RgbQuant.prototype.colorStats1D = function colorStats1D(buf32) { - var histG = this.histogram, - num = 0, col, - len = buf32.length; - - for (var i = 0; i < len; i++) { - col = buf32[i]; - - // skip transparent - if ((col & 0xff000000) >> 24 == 0) continue; - - // collect hue stats - if (this.hueStats) - this.hueStats.check(col); - - if (col in histG) - histG[col]++; - else - histG[col] = 1; - } - }; - - // population threshold within subregions - // FIXME: this can over-reduce (few/no colors same?), need a way to keep - // important colors that dont ever reach local thresholds (gradients?) - RgbQuant.prototype.colorStats2D = function colorStats2D(buf32, width) { - var boxW = this.boxSize[0], - boxH = this.boxSize[1], - area = boxW * boxH, - boxes = makeBoxes(width, buf32.length / width, boxW, boxH), - histG = this.histogram, - self = this; - - boxes.forEach(function(box) { - var effc = Math.max(Math.round((box.w * box.h) / area) * self.boxPxls, 2), - histL = {}, col; - - iterBox(box, width, function(i) { - col = buf32[i]; - - // skip transparent - if ((col & 0xff000000) >> 24 == 0) return; - - // collect hue stats - if (self.hueStats) - self.hueStats.check(col); - - if (col in histG) - histG[col]++; - else if (col in histL) { - if (++histL[col] >= effc) - histG[col] = histL[col]; - } - else - histL[col] = 1; - }); - }); - - if (this.hueStats) - this.hueStats.inject(histG); - }; - - // TODO: group very low lum and very high lum colors - // TODO: pass custom sort order - RgbQuant.prototype.sortPal = function sortPal() { - var self = this; - - this.idxi32.sort(function(a,b) { - var idxA = self.i32idx[a], - idxB = self.i32idx[b], - rgbA = self.idxrgb[idxA], - rgbB = self.idxrgb[idxB]; - - var hslA = rgb2hsl(rgbA[0],rgbA[1],rgbA[2]), - hslB = rgb2hsl(rgbB[0],rgbB[1],rgbB[2]); - - // sort all grays + whites together - var hueA = (rgbA[0] == rgbA[1] && rgbA[1] == rgbA[2]) ? -1 : hueGroup(hslA.h, self.hueGroups); - var hueB = (rgbB[0] == rgbB[1] && rgbB[1] == rgbB[2]) ? -1 : hueGroup(hslB.h, self.hueGroups); - - var hueDiff = hueB - hueA; - if (hueDiff) return -hueDiff; - - var lumDiff = lumGroup(+hslB.l.toFixed(2)) - lumGroup(+hslA.l.toFixed(2)); - if (lumDiff) return -lumDiff; - - var satDiff = satGroup(+hslB.s.toFixed(2)) - satGroup(+hslA.s.toFixed(2)); - if (satDiff) return -satDiff; - }); - - // sync idxrgb & i32idx - this.idxi32.forEach(function(i32, i) { - self.idxrgb[i] = self.i32rgb[i32]; - self.i32idx[i32] = i; - }); - }; - - // TOTRY: use HUSL - http://boronine.com/husl/ - RgbQuant.prototype.nearestColor = function nearestColor(i32) { - var idx = this.nearestIndex(i32); - return idx === null ? 0 : this.idxi32[idx]; - }; - - // TOTRY: use HUSL - http://boronine.com/husl/ - RgbQuant.prototype.nearestIndex = function nearestIndex(i32) { - // alpha 0 returns null index - if ((i32 & 0xff000000) >> 24 == 0) - return null; - - if (this.useCache && (""+i32) in this.i32idx) - return this.i32idx[i32]; - - var min = 1000, - idx, - rgb = [ - (i32 & 0xff), - (i32 & 0xff00) >> 8, - (i32 & 0xff0000) >> 16, - ], - len = this.idxrgb.length; - - for (var i = 0; i < len; i++) { - if (!this.idxrgb[i]) continue; // sparse palettes - - var dist = this.colorDist(rgb, this.idxrgb[i]); - - if (dist < min) { - min = dist; - idx = i; - } - } - - return idx; - }; - - RgbQuant.prototype.cacheHistogram = function cacheHistogram(idxi32) { - for (var i = 0, i32 = idxi32[i]; i < idxi32.length && this.histogram[i32] >= this.cacheFreq; i32 = idxi32[i++]) - this.i32idx[i32] = this.nearestIndex(i32); - }; - - function HueStats(numGroups, minCols) { - this.numGroups = numGroups; - this.minCols = minCols; - this.stats = {}; - - for (var i = -1; i < numGroups; i++) - this.stats[i] = {num: 0, cols: []}; - - this.groupsFull = 0; - } - - HueStats.prototype.check = function checkHue(i32) { - if (this.groupsFull == this.numGroups + 1) - this.check = function() {return;}; - - var r = (i32 & 0xff), - g = (i32 & 0xff00) >> 8, - b = (i32 & 0xff0000) >> 16, - hg = (r == g && g == b) ? -1 : hueGroup(rgb2hsl(r,g,b).h, this.numGroups), - gr = this.stats[hg], - min = this.minCols; - - gr.num++; - - if (gr.num > min) - return; - if (gr.num == min) - this.groupsFull++; - - if (gr.num <= min) - this.stats[hg].cols.push(i32); - }; - - HueStats.prototype.inject = function injectHues(histG) { - for (var i = -1; i < this.numGroups; i++) { - if (this.stats[i].num <= this.minCols) { - switch (typeOf(histG)) { - case "Array": - this.stats[i].cols.forEach(function(col){ - if (histG.indexOf(col) == -1) - histG.push(col); - }); - break; - case "Object": - this.stats[i].cols.forEach(function(col){ - if (!histG[col]) - histG[col] = 1; - else - histG[col]++; - }); - break; - } - } - } - }; - - // Rec. 709 (sRGB) luma coef - var Pr = .2126, - Pg = .7152, - Pb = .0722; - - // http://alienryderflex.com/hsp.html - function rgb2lum(r,g,b) { - return Math.sqrt( - Pr * r*r + - Pg * g*g + - Pb * b*b - ); - } - - var rd = 255, - gd = 255, - bd = 255; - - var euclMax = Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd); - // perceptual Euclidean color distance - function distEuclidean(rgb0, rgb1) { - var rd = rgb1[0]-rgb0[0], - gd = rgb1[1]-rgb0[1], - bd = rgb1[2]-rgb0[2]; - - return Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd) / euclMax; - } - - var manhMax = Pr*rd + Pg*gd + Pb*bd; - // perceptual Manhattan color distance - function distManhattan(rgb0, rgb1) { - var rd = Math.abs(rgb1[0]-rgb0[0]), - gd = Math.abs(rgb1[1]-rgb0[1]), - bd = Math.abs(rgb1[2]-rgb0[2]); - - return (Pr*rd + Pg*gd + Pb*bd) / manhMax; - } - - // http://rgb2hsl.nichabi.com/javascript-function.php - function rgb2hsl(r, g, b) { - var max, min, h, s, l, d; - r /= 255; - g /= 255; - b /= 255; - max = Math.max(r, g, b); - min = Math.min(r, g, b); - l = (max + min) / 2; - if (max == min) { - h = s = 0; - } else { - d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break - } - h /= 6; - } -// h = Math.floor(h * 360) -// s = Math.floor(s * 100) -// l = Math.floor(l * 100) - return { - h: h, - s: s, - l: rgb2lum(r,g,b), - }; - } - - function hueGroup(hue, segs) { - var seg = 1/segs, - haf = seg/2; - - if (hue >= 1 - haf || hue <= haf) - return 0; - - for (var i = 1; i < segs; i++) { - var mid = i*seg; - if (hue >= mid - haf && hue <= mid + haf) - return i; - } - } - - function satGroup(sat) { - return sat; - } - - function lumGroup(lum) { - return lum; - } - - function typeOf(val) { - return Object.prototype.toString.call(val).slice(8,-1); - } - - var sort = isArrSortStable() ? Array.prototype.sort : stableSort; - - // must be used via stableSort.call(arr, fn) - function stableSort(fn) { - var type = typeOf(this[0]); - - if (type == "Number" || type == "String") { - var ord = {}, len = this.length, val; - - for (var i = 0; i < len; i++) { - val = this[i]; - if (ord[val] || ord[val] === 0) continue; - ord[val] = i; - } - - return this.sort(function(a,b) { - return fn(a,b) || ord[a] - ord[b]; - }); - } - else { - var ord = this.map(function(v){return v}); - - return this.sort(function(a,b) { - return fn(a,b) || ord.indexOf(a) - ord.indexOf(b); - }); - } - } - - // test if js engine's Array#sort implementation is stable - function isArrSortStable() { - var str = "abcdefghijklmnopqrstuvwxyz"; - - return "xyzvwtursopqmnklhijfgdeabc" == str.split("").sort(function(a,b) { - return ~~(str.indexOf(b)/2.3) - ~~(str.indexOf(a)/2.3); - }).join(""); - } - - // returns uniform pixel data from various img - // TODO?: if array is passed, createimagedata, createlement canvas? take a pxlen? - function getImageData(img, width) { - var can, ctx, imgd, buf8, buf32, height; - - switch (typeOf(img)) { - case "HTMLImageElement": - can = document.createElement("canvas"); - can.width = img.naturalWidth; - can.height = img.naturalHeight; - ctx = can.getContext("2d"); - ctx.drawImage(img,0,0); - case "Canvas": - case "HTMLCanvasElement": - can = can || img; - ctx = ctx || can.getContext("2d"); - case "CanvasRenderingContext2D": - ctx = ctx || img; - can = can || ctx.canvas; - imgd = ctx.getImageData(0, 0, can.width, can.height); - case "ImageData": - imgd = imgd || img; - width = imgd.width; - if (typeOf(imgd.data) == "CanvasPixelArray") - buf8 = new Uint8Array(imgd.data); - else - buf8 = imgd.data; - case "Array": - case "CanvasPixelArray": - buf8 = buf8 || new Uint8Array(img); - case "Uint8Array": - case "Uint8ClampedArray": - buf8 = buf8 || img; - buf32 = new Uint32Array(buf8.buffer); - case "Uint32Array": - buf32 = buf32 || img; - buf8 = buf8 || new Uint8Array(buf32.buffer); - width = width || buf32.length; - height = buf32.length / width; - } - - return { - can: can, - ctx: ctx, - imgd: imgd, - buf8: buf8, - buf32: buf32, - width: width, - height: height, - }; - } - - // partitions a rect of wid x hgt into - // array of bboxes of w0 x h0 (or less) - function makeBoxes(wid, hgt, w0, h0) { - var wnum = ~~(wid/w0), wrem = wid%w0, - hnum = ~~(hgt/h0), hrem = hgt%h0, - xend = wid-wrem, yend = hgt-hrem; - - var bxs = []; - for (var y = 0; y < hgt; y += h0) - for (var x = 0; x < wid; x += w0) - bxs.push({x:x, y:y, w:(x==xend?wrem:w0), h:(y==yend?hrem:h0)}); - - return bxs; - } - - // iterates @bbox within a parent rect of width @wid; calls @fn, passing index within parent - function iterBox(bbox, wid, fn) { - var b = bbox, - i0 = b.y * wid + b.x, - i1 = (b.y + b.h - 1) * wid + (b.x + b.w - 1), - cnt = 0, incr = wid - b.w + 1, i = i0; - - do { - fn.call(this, i); - i += (++cnt % b.w == 0) ? incr : 1; - } while (i <= i1); - } - - // returns array of hash keys sorted by their values - function sortedHashKeys(obj, desc) { - var keys = []; - - for (var key in obj) - keys.push(key); - - return sort.call(keys, function(a,b) { - return desc ? obj[b] - obj[a] : obj[a] - obj[b]; - }); - } - - // expose - this.RgbQuant = RgbQuant; - - // expose to commonJS - if (typeof module !== 'undefined' && module.exports) { - module.exports = RgbQuant; - } - -}).call(this); \ No newline at end of file diff --git a/libs/rgbquant.min.js b/libs/rgbquant.min.js new file mode 100644 index 00000000..1cc0104e --- /dev/null +++ b/libs/rgbquant.min.js @@ -0,0 +1,2 @@ +// © 2015, Leon Sorokin, MIT +(function(){function t(t){var s;t=t||{},this.method=t.method||2,this.colors=t.colors||256,this.initColors=t.initColors||4096,this.initDist=t.initDist||.01,this.distIncr=t.distIncr||.005,this.hueGroups=t.hueGroups||10,this.satGroups=t.satGroups||10,this.lumGroups=t.lumGroups||10,this.minHueCols=t.minHueCols||0,this.hueStats=this.minHueCols?new i(this.hueGroups,this.minHueCols):null,this.boxSize=t.boxSize||[64,64],this.boxPxls=t.boxPxls||2,this.palLocked=!1,this.dithKern=t.dithKern||null,this.dithSerp=t.dithSerp||!1,this.dithDelta=t.dithDelta||0,this.histogram={},this.idxrgb=t.palette?t.palette.slice(0):[],this.idxi32=[],this.i32idx={},this.i32rgb={},this.useCache=!1!==t.useCache,this.cacheFreq=t.cacheFreq||10,this.reIndex=t.reIndex||0==this.idxrgb.length,this.colorDist="manhattan"==t.colorDist?n:r,0>>0;s.idxi32[i]=r,s.i32idx[r]=i,s.i32rgb[r]=t})}function i(t,i){this.numGroups=t,this.minCols=i,this.stats={};for(var r=-1;r>8,b=(16711680&x)>>16,m=this.nearestColor(x),v=255&m,x=(65280&m)>>8,m=(16711680&m)>>16;if(h[f]=255<<24|m<<16|x<<8|v,this.dithDelta)if(this.colorDist([p,g,b],[v,x,m])>8,A=(16711680&h[D])>>16,I=Math.max(0,Math.min(255,I+y*M)),P=Math.max(0,Math.min(255,P+w*M)),M=Math.max(0,Math.min(255,A+S*M)),h[D]=255<<24|M<<16|P<<8|I)}}}return h},t.prototype.buildPal=function(t){if(!(this.palLocked||0this.colors){for(var i,r=t.length,s={},e=0,h=!1,n=0;n>8,(16711680&t)>>16]}),o=r=a.length,u=this.initDist;if(o>this.colors){for(;o>this.colors;){for(var l=[],n=0;n3*this.colors?this.initDist:this.distIncr}if(o>24!=0&&(this.hueStats&&this.hueStats.check(i),i in r?r[i]++:r[i]=1)},t.prototype.colorStats2D=function(e,h){var t=this.boxSize[0],i=this.boxSize[1],n=t*i,i=function(t,i,r,s){for(var e=t%r,h=i%s,n=t-e,a=i-h,o=[],u=0;u>24!=0&&(o.hueStats&&o.hueStats.check(i),i in a?a[i]++:i in s?++s[i]>=r&&(a[i]=s[i]):s[i]=1)})}),this.hueStats&&this.hueStats.inject(a)},t.prototype.sortPal=function(){var e=this;this.idxi32.sort(function(t,i){var r=e.i32idx[t],s=e.i32idx[i],t=e.idxrgb[r],i=e.idxrgb[s],r=a(t[0],t[1],t[2]),s=a(i[0],i[1],i[2]),t=t[0]==t[1]&&t[1]==t[2]?-1:c(r.h,e.hueGroups),t=(i[0]==i[1]&&i[1]==i[2]?-1:c(s.h,e.hueGroups))-t;if(t)return-t;t=+s.l.toFixed(2)-+r.l.toFixed(2);if(t)return-t;r=+s.s.toFixed(2)-+r.s.toFixed(2);return r?-r:void 0}),this.idxi32.forEach(function(t,i){e.idxrgb[i]=e.i32rgb[t],e.i32idx[t]=i})},t.prototype.nearestColor=function(t){t=this.nearestIndex(t);return null===t?0:this.idxi32[t]},t.prototype.nearestIndex=function(t){if((4278190080&t)>>24==0)return null;if(this.useCache&&""+t in this.i32idx)return this.i32idx[t];for(var i,r,s=1e3,e=[255&t,(65280&t)>>8,(16711680&t)>>16],h=this.idxrgb.length,n=0;n=this.cacheFreq;r=t[i++])this.i32idx[r]=this.nearestIndex(r)},i.prototype.check=function(t){this.groupsFull==this.numGroups+1&&(this.check=function(){});var i=255&t,r=(65280&t)>>8,s=(16711680&t)>>16,i=i==r&&r==s?-1:c(a(i,r,s).h,this.numGroups),r=this.stats[i],s=this.minCols;r.num++,r.num>s||(r.num==s&&this.groupsFull++,r.num<=s&&this.stats[i].cols.push(t))},i.prototype.inject=function(i){for(var t=-1;t>>=1;return(n+t)/r};return m.int32=function(){return 0|p.g(4)},m.quick=function(){return p.g(4)/4294967296},m.double=m,q(s(p.S),a),(t.pass||e||function(n,r,t,e){return e&&(e.S&&o(e,p),n.state=function(){return o(p,{})}),t?(b[g]=n,r):n})(m,f,"global"in t?t.global:this==b,t.state)}function n(n){var r,t=n.length,u=this,e=0,o=u.i=u.j=0,i=u.S=[];for(t||(n=[t++]);e tip("Click to open Units Editor")); +scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits()); legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend()); // main data variables @@ -336,37 +338,28 @@ function applyDefaultBiomesSystem() { } function showWelcomeMessage() { - const post = link("https://www.reddit.com/r/FantasyMapGenerator/comments/ft5b41/update_new_version_is_published_into_the_battle_v14/", "Main changes:"); // announcement on Reddit + const post = "Main changes:" //link("https://www.reddit.com/r/FantasyMapGenerator/comments/ft5b41/update_v15/", "Main changes:"); const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version"); const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community"); const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server"); const patreon = link("https://www.patreon.com/azgaar", "Patreon"); - const desktop = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A#is-there-a-desktop-version", "desktop application"); alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version ${version}. This version is compatible with ${changelog}, loaded .map files will be auto-updated. -
    ${post} -
  • Military forces changes (${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Military-Forces", "detailed description")})
  • -
  • Battle simulation (${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Battle-Simulator", "detailed description")})
  • -
  • Ice layer and Ice editor
  • -
  • Route and River Elevation profile (by EvolvedExperiment)
  • -
  • Image Converter enhancement
  • -
  • Name generator improvement
  • -
  • Improved integration with City Generator
  • -
  • Fogging restyle
  • +
  • State, province and burg Emblems generation
  • +
  • Emblem editor integrated with ${link("https://azgaar.github.io/Armoria", "Armoria")} — our new dedicated Heraldry generator and editor
  • +
  • Burg editor screen update
  • +
  • Speak name functionality
- -

You can can also download a ${desktop}.

- + Armoria preview

Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.

- Thanks for all supporters on ${patreon}!`; $("#alert").dialog( {resizable: false, title: "Fantasy Map Generator update", width: "28em", buttons: {OK: function() {$(this).dialog("close")}}, - position: {my: "center", at: "center", of: "svg"}, + position: {my: "center center-80", at: "center", of: "svg"}, close: () => localStorage.setItem("version", version)} ); } @@ -436,7 +429,18 @@ function invokeActiveZooming() { const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1); this.getAttribute("font-size", relative); const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 50); + if (hidden) this.classList.add("hidden"); + else this.classList.remove("hidden"); + }); + } + + // rescale emblems on zoom + if (emblems.style("display") !== "none") { + emblems.selectAll("g").each(function() { + const size = this.getAttribute("font-size") * scale; + const hidden = hideEmblems.checked && (size < 25 || size > 300); if (hidden) this.classList.add("hidden"); else this.classList.remove("hidden"); + if (!hidden && window.COArenderer && this.children.length && !this.children[0].getAttribute("href")) renderGroupCOAs(this); }); } @@ -468,6 +472,18 @@ function invokeActiveZooming() { } } +async function renderGroupCOAs(g) { + const [group, type] = g.id === "burgEmblems" ? [pack.burgs, "burg"] : + g.id === "provinceEmblems" ? [pack.provinces, "province"] : + [pack.states, "state"]; + for (let use of g.children) { + const i = +use.dataset.i; + const id = type+"COA"+i; + COArenderer.trigger(id, group[i].coa); + use.setAttribute("href", "#"+id); + } +} + // add drag to upload logic, pull request from @evyatron void function addDragToUpload() { document.addEventListener("dragover", function(e) { @@ -586,7 +602,7 @@ function generateSeed() { else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value; else seed = Math.floor(Math.random() * 1e9).toString(); optionsSeed.value = seed; - Math.seedrandom(seed); + Math.random = aleaPRNG(seed); } // Place points to calculate Voronoi diagram @@ -620,7 +636,8 @@ function calculateVoronoi(graph, points) { // Mark features (ocean, lakes, islands) function markFeatures() { TIME && console.time("markFeatures"); - Math.seedrandom(seed); // restart Math.random() to get the same result on heightmap edit in Erase mode + Math.random = aleaPRNG(seed); // restart Math.random() to get the same result on heightmap edit in Erase mode + const cells = grid.cells, heights = grid.cells.h; cells.f = new Uint16Array(cells.i.length); // cell feature number cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast; @@ -1718,7 +1735,8 @@ const regenerateMap = debounce(function() { // clear the map function undraw() { viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #armies > g, #ruler > g").remove(); - defs.selectAll("path, clipPath").remove(); + document.getElementById("deftemp").querySelectorAll("path, clipPath, svg").forEach(el => el.remove()); + document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems notes = []; unfog(); } diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 924280e3..d3d75e97 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -88,9 +88,11 @@ const expansionism = rn(Math.random() * powerInput.value + 1, 1); const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCultureShort(b.culture); const name = Names.getState(basename, b.culture); - const nomadic = [1, 2, 3, 4].includes(cells.biome[b.cell]); - const type = nomadic ? "Nomadic" : cultures[b.culture].type === "Nomadic" ? "Generic" : cultures[b.culture].type; - states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture}); + const type = cultures[b.culture].type; + + const coa = COA.generate(null, null, null, type); + coa.shield = COA.getShield(b.culture, null); + states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture, coa}); cells.burg[b.cell] = i; }); @@ -137,7 +139,7 @@ } } - // define burg coordinates, port status and define details + // define burg coordinates, coa, port status and define details const specifyBurgs = function() { TIME && console.time("specifyBurgs"); const cells = pack.cells, vertices = pack.vertices, features = pack.features, temp = grid.cells.temp; @@ -175,6 +177,18 @@ if (i%2) b.x = rn(b.x + shift, 2); else b.x = rn(b.x - shift, 2); if (cells.r[i]%2) b.y = rn(b.y + shift, 2); else b.y = rn(b.y - shift, 2); } + + // define emblem + const state = pack.states[b.state]; + const stateCOA = state.coa; + let kinship = .25; + if (b.capital) kinship += .1; + else if (b.port) kinship -= .1; + if (b.culture !== state.culture) kinship -= .25; + b.type = getType(i, b.port); + const type = b.capital && P(.2) ? "Capital" : b.type === "Generic" ? "City" : b.type; + b.coa = COA.generate(stateCOA, kinship, null, type); + b.coa.shield = COA.getShield(b.culture, b.state); } // de-assign port status if it's the only one on feature @@ -188,14 +202,30 @@ TIME && console.timeEnd("specifyBurgs"); } + const getType = function(i, port) { + const cells = pack.cells; + if (port) return "Naval"; + 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 (!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"; + } + const defineBurgFeatures = function(newburg) { + const cells = pack.cells; pack.burgs.filter(b => newburg ? b.i == newburg.i : (b.i && !b.removed)).forEach(b => { const pop = b.population; b.citadel = b.capital || pop > 50 && P(.75) || P(.5) ? 1 : 0; b.plaza = pop > 50 || pop > 30 && P(.75) || pop > 10 && P(.5) || P(.25) ? 1 : 0; b.walls = b.capital || pop > 30 || pop > 20 && P(.75) || pop > 10 && P(.5) || P(.2) ? 1 : 0; b.shanty = pop > 30 || pop > 20 && P(.75) || b.walls && P(.75) ? 1 : 0; - const religion = pack.cells.religion[b.cell]; + const religion = cells.religion[b.cell]; const theocracy = pack.states[b.state].form === "Theocracy"; b.temple = religion && theocracy || pop > 50 || pop > 35 && P(.75) || pop > 20 && P(.5) ? 1 : 0; }); @@ -348,7 +378,6 @@ if (buddies.length > 2) continue; if (adversaries.length <= buddies.length) continue; cells.state[i] = cells.state[adversaries[0]]; - //debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5).attr("fill", "red"); } TIME && console.timeEnd("normalizeStates"); } @@ -399,11 +428,6 @@ const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length); paths.push([s.i, relaxed]); - // if (s.i == 13) debug.selectAll(".circle").data(points).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", "red"); - // if (s.i == 13) d3.select("#cells").selectAll(".polygon").data(d3.range(voronoi.cells.v.length)).enter().append("polygon").attr("points", d => voronoi.cells.v[d] ? voronoi.cells.v[d].map(v => c.p[v]) : ""); - // if (s.i == 13) debug.append("path").attr("d", round(lineGen(relaxed))).attr("fill", "none").attr("stroke", "blue").attr("stroke-width", .5); - // if (s.i == 13) debug.selectAll(".circle").data(chain).enter().append("circle").attr("cx", d => c.p[d][0]).attr("cy", d => c.p[d][1]).attr("r", 1); - function getHull(start, state, maxLake) { const queue = [start], hull = new Set(); @@ -438,7 +462,6 @@ const h = c.p.length < 200 ? 0 : c.p.length < 600 ? .5 : 1; // power of horyzontality shift const end = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[a][0] - c.p[b][0]) + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h)]; // left point const start = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[b][0] - c.p[a][0]) - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h)]; // right point - //debug.append("line").attr("x1", c.p[start][0]).attr("y1", c.p[start][1]).attr("x2", c.p[end][0]).attr("y2", c.p[end][1]).attr("stroke", "#00dd00"); // connect leftmost and rightmost points with shortest path const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); @@ -540,7 +563,7 @@ el.insertAdjacentHTML("afterbegin", spans.join("")); if (lines.length < 2) return; - // check whether multilined label is generally inside the strate. If no, replace with short name label + // check whether multilined label is generally inside the state. If no, replace with short name label const cs = pack.cells.state, b = el.parentNode.getBBox(); const c1 = () => +cs[findCell(b.x, b.y)] === id; const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id; @@ -571,6 +594,7 @@ TIME && console.time("collectStatistics"); const cells = pack.cells, states = pack.states; states.forEach(s => { + if (s.removed) return; s.cells = s.area = s.burgs = s.rural = s.urban = 0; s.neighbors = new Set(); }); @@ -593,7 +617,10 @@ } // convert neighbors Set object into array - states.forEach(s => s.neighbors = Array.from(s.neighbors)); + states.forEach(s => { + if (!s.neighbors) return; + s.neighbors = Array.from(s.neighbors); + }); TIME && console.timeEnd("collectStatistics"); } @@ -794,9 +821,6 @@ const generic = {Monarchy:25, Republic:2, Union:1}; const naval = {Monarchy:25, Republic:8, Union:3}; - const genericArray = [], navalArray = []; // turn weighted array into simple array - for (const t in generic) {for (let j=0; j < generic[t]; j++) {genericArray.push(t);}} - for (const t in naval) {for (let j=0; j < naval[t]; j++) {navalArray.push(t);}} const median = d3.median(pack.states.map(s => s.area)); const empireMin = states.map(s => s.area).sort((a, b) => b - a)[Math.max(Math.ceil(states.length ** .4) - 2, 0)]; @@ -809,21 +833,19 @@ const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier const republic = {Republic:75, Federation:4, Oligarchy:2, Tetrarchy:1, Triumvirate:1, Diarchy:1, "Trade Company":4, Junta:1}; // weighted random const union = {Union:3, League:4, Confederation:1, "United Kingdom":1, "United Republic":1, "United Provinces":2, Commonwealth:1, Heptarchy:1}; // weighted random + const theocracy = {Theocracy: 20, Brotherhood:1, Thearchy:2, See:1}; + const anarchy = {"Free Territory":2, Council:3, Commune:1, Community:1}; for (const s of states) { if (list && !list.includes(s.i)) continue; - // some nomadic states - if (s.type === "Nomadic" && P(.8)) { - s.form = "Horde"; - s.formName = expTiers[s.i] > 2 ? "United Hordes" : "Horde"; - s.fullName = getFullName(s); - continue; - } - const religion = pack.cells.religion[s.center]; - const theocracy = religion && pack.religions[religion].expansion === "state" || (P(.1) && pack.religions[religion].type === "Organized"); - s.form = theocracy ? "Theocracy" : s.type === "Naval" ? ra(navalArray) : ra(genericArray); + const isTheocracy = religion && pack.religions[religion].expansion === "state" || (P(.1) && ["Organized", "Cult"].includes(pack.religions[religion].type)); + const isAnarchy = P(.01 - expTiers[s.i]/500); + + if (isTheocracy) s.form = "Theocracy"; + else if (isAnarchy) s.form = "Anarchy"; + else s.form = s.type === "Naval" ? rw(naval) : rw(generic); s.formName = selectForm(s); s.fullName = getFullName(s); } @@ -841,13 +863,13 @@ if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian - if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Mongolian + if ([16, 31].includes(base) && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic, Mongolian if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian - if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Beylik"; // Turkic + if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Horde"; // Turkic if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian return form; } @@ -865,34 +887,34 @@ } if (s.form === "Union") return rw(union); + if (s.form === "Anarchy") return rw(anarchy); if (s.form === "Theocracy") { - // default name is "Theocracy" if (P(.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean if (P(.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian if (P(.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish if (P(.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili - if (P(.02)) return "Thearchy"; // "Thearchy" in very rare case - if (P(.05)) return "See"; // "See" in rare case - return "Theocracy"; + return rw(theocracy); } } TIME && console.timeEnd("defineStateForms"); } + // state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name + const adjForms = ["Empire", "Sultanate", "Khaganate", "Shogunate", "Caliphate", "Despotate", "Theocracy", "Oligarchy", "Union", "Confederation", "Trade Company", "League", "Tetrarchy", "Triumvirate", "Diarchy", "Horde", "Marches"]; + const getFullName = function(s) { if (!s.formName) return s.name; if (!s.name && s.formName) return "The " + s.formName; - // state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name - const adj = ["Empire", "Sultanate", "Khaganate", "Shogunate", "Caliphate", "Despotate", "Theocracy", "Oligarchy", "Union", "Confederation", "Trade Company", "League", "Tetrarchy", "Triumvirate", "Diarchy", "Horde"]; - return adj.includes(s.formName) ? getAdjective(s.name) + " " + s.formName : s.formName + " of " + s.name; + const adjName = adjForms.includes(s.formName) && !(/-| /).test(s.name); + return adjName ? `${getAdjective(s.name)} ${s.formName}` : `${s.formName} of ${s.name}`; } const generateProvinces = function(regenerate) { TIME && console.time("generateProvinces"); const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed; - Math.seedrandom(localSeed); + Math.random = aleaPRNG(localSeed); const cells = pack.cells, states = pack.states, burgs = pack.burgs; const provinces = pack.provinces = [0]; @@ -903,15 +925,14 @@ const forms = { Monarchy:{County:11, Earldom:3, Shire:1, Landgrave:1, Margrave:1, Barony:1}, - Republic:{Province:6, Department:2, Governorate:2, State:1, Canton:1, Prefecture:1}, - Theocracy:{Parish:5, Deanery:3, Province:2, Council:1, District:1}, - Union:{Province:2, State:1, Canton:1, Republic:1, County:1}, - Wild:{Territory:10, Land:5, Province:2, Region:2, Tribe:1, Clan:1}, - Horde:{Horde:1} + Republic:{Province:6, Department:2, Governorate:2, District:1, Canton:1, Prefecture:1}, + Theocracy:{Parish:3, Deanery:1}, + Union:{Province:1, State:1, Canton:1, Republic:1, County:1, Council:1}, + Wild:{Territory:10, Land:5, Region:2, Tribe:1, Clan:1, Dependency:1, Area: 1} } // generate provinces for a selected burgs - Math.seedrandom(localSeed); + Math.random = aleaPRNG(localSeed); states.forEach(s => { s.provinces = []; if (!s.i || s.removed) return; @@ -928,12 +949,17 @@ const center = stateBurgs[i].cell; const burg = stateBurgs[i].i; const c = stateBurgs[i].culture; - const name = P(.5) ? Names.getState(Names.getCultureShort(c), c) : stateBurgs[i].name; + const nameByBurg = P(.5); + const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c); const formName = rw(form); form[formName] += 5; const fullName = name + " " + formName; const color = getMixedColor(s.color); - provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color}); + const kinship = nameByBurg ? .8 : .4; + const type = getType(center, burg.port); + const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); + coa.shield = COA.getShield(c, s.i); + provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color, coa}); } }); @@ -945,7 +971,6 @@ cells.province[p.center] = p.i; queue.queue({e:p.center, p:0, province:p.i, state:p.state}); cost[p.center] = 1; - //debug.append("circle").attr("cx", cells.p[p.center][0]).attr("cy", cells.p[p.center][1]).attr("r", .3).attr("fill", "red"); }); while (queue.length) { @@ -1017,7 +1042,8 @@ // generate "wild" province name const c = cells.culture[center]; - const name = burgCell && P(.5) ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c); + const nameByBurg = burgCell && P(.5); + const name = nameByBurg ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c); const f = pack.features[cells.f[center]]; const provCells = stateNoProvince.filter(i => cells.province[i] === province); const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); @@ -1026,7 +1052,12 @@ const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]); const fullName = name + " " + formName; const color = getMixedColor(s.color); - provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color}); + const dominion = colony ? P(.95) : singleIsle || isleGroup ? P(.7) : P(.3); + const kinship = dominion ? 0 : .4; + const type = getType(center, burgs[burg]?.port); + const coa = COA.generate(s.coa, kinship, dominion, type); + coa.shield = COA.getShield(c, s.i); + provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color, coa}); s.provinces.push(province); // check if there is a land way within the same state between two cells @@ -1054,7 +1085,7 @@ } return {generate, expandStates, normalizeStates, assignColors, - drawBurgs, specifyBurgs, defineBurgFeatures, drawStateLabels, collectStatistics, + drawBurgs, specifyBurgs, defineBurgFeatures, getType, drawStateLabels, collectStatistics, generateCampaigns, generateDiplomacy, defineStateForms, getFullName, generateProvinces, updateCultures}; }))); diff --git a/modules/coa-generator.js b/modules/coa-generator.js new file mode 100644 index 00000000..22265084 --- /dev/null +++ b/modules/coa-generator.js @@ -0,0 +1,501 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.COA = factory()); +}(this, (function () {'use strict'; + + const tinctures = { + field: { metals: 3, colours: 4, stains: +P(.03), patterns: 1 }, + division: { metals: 5, colours: 8, stains: +P(.03), patterns: 1 }, + charge: { metals: 2, colours: 3, stains: +P(.05), patterns: 0 }, + metals: { argent: 3, or: 2 }, + colours: { gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2 }, + stains: { murrey: 1, sanguine: 1, tenné: 1 }, + patterns: { + semy: 8, ermine: 6, + vair: 4, counterVair: 1, vairInPale: 1, vairEnPointe: 2, vairAncien: 2, + potent: 2, counterPotent: 1, potentInPale: 1, potentEnPointe: 1, + chequy: 8, lozengy: 5, fusily: 2, pally: 8, barry: 10, gemelles: 1, + bendy: 8, bendySinister: 4, palyBendy: 2, barryBendy: 1, + pappellony: 2, pappellony2: 3, scaly: 1, plumetty: 1, + masoned: 6, fretty: 3, grillage: 1, chainy: 1, maily: 2, honeycombed: 1 } + } + + const charges = { + // categories selection + types: { conventional: 30, crosses: 8, animals: 2, animalHeads: 1, birds: 2, fantastic: 3, plants: 1, agriculture: 1, arms: 3, bodyparts: 1, people: 1, architecture: 1, miscellaneous: 3, inescutcheon: 3 }, + single: { conventional: 12, crosses: 8, plants: 2, animals: 10, animalHeads: 2, birds: 4, fantastic: 7, agriculture: 1, arms: 6, bodyparts: 1, people: 1, architecture: 1, miscellaneous: 9, inescutcheon: 5 }, + semy: { conventional: 12, crosses: 3, plants: 1 }, + // generic categories + 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, carreau: 1, trefle: 2, + fleurDeLis: 6, sun: 3, sunInSplendour: 1, crescent: 5, fountain: 1 + }, + crosses: { + crossHummetty: 15, crossVoided: 1, crossPattee: 3, crossPotent: 2, crossClechy: 3, crosslet: 1, crossBottony: 1, crossFleury: 3, + crossPatonce: 1, crossPommy: 1, crossGamma: 1, crossArrowed: 1, crossFitchy: 1, crossCercelee: 1, crossMoline: 2, crossFourchy: 1, + crossAvellane: 1, crossErminee: 1, crossMaltese: 3, crossCeltic: 1, crossOccitan: 1, crossSaltire: 3, crossTau: 1 + }, + animals: { + lionRampant: 5, lionPassant: 2, wolfRampant: 1, wolfPassant: 1, wolfStatant: 1, greyhoundCourant: 1, boarRampant: 1, + horseRampant: 2, horseSalient: 1, bearRampant: 2, bearPassant: 1, bullPassant: 1, goat: 1, lamb: 1, elephant: 1 + }, + animalHeads: { wolfHeadErased: 1, bullHeadCaboshed: 1, deerHeadCaboshed: 1 }, + fantastic: { dragonPassant: 2, dragonRampant: 2, wyvern: 1, wyvernWithWingsDisplayed: 1, griffinPassant: 1, griffinRampant: 1, eagleTwoHeards: 2, unicornRampant: 1, pegasus: 1, serpent: 1 }, + birds: { eagle: 9, raven: 1, cock: 3, parrot: 1, swan: 2, swanErased: 1, heron: 1 }, + plants: { tree: 1, cinquefoil: 1, rose: 1 }, + agriculture: { garb: 1, rake: 1 }, + arms: { sword: 5, sabre: 1, sabresCrossed: 1, hatchet: 2, lochaberAxe: 1, mallet: 1, bowWithArrow: 2, bow: 1, arrow: 1, arrowsSheaf: 1 }, + bodyparts: { hand: 4, head: 1, headWreathed: 1 }, + people: { cavalier: 1 }, + architecture: { tower: 1, castle: 1 }, + miscellaneous: { + crown: 3, orb: 1, chalice: 1, key: 1, buckle: 1, bugleHorn: 1, bell: 1, pot: 1, horseshoe: 3, stagsAttires: 1, cowHorns: 2, wing: 1, wingSword: 1, + lute: 1, harp: 1, wheel: 2, crosier: 1, log: 1}, + // selection based on culture type: + Naval: { anchor: 3, boat: 1, lymphad: 2, armillarySphere: 1, escallop: 1, dolphin: 1 }, + Highland: { tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1,}, + River: { tower: 1, garb: 1, rake: 1, boat: 1, pike: 2, bullHeadCaboshed: 1 }, + Lake: { cancer: 2, escallop: 1, pike: 2, heron: 1, boat: 1 }, + Nomadic: { pot: 1, buckle: 1, wheel: 2, sabre: 2, sabresCrossed: 1, bow: 2, arrow: 1, horseRampant: 1, horseSalient: 1, crescent: 1 }, + Hunting: { bugleHorn: 3, stagsAttires: 2, hatchet: 1, bowWithArrow: 2, arrowsSheaf: 1, deerHeadCaboshed: 1, wolfStatant: 1 }, + // selection based on type + City: { key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 1 }, + Capital: { crown: 4, orb: 1, lute: 1, castle: 3, tower: 1 }, + Сathedra: { chalice: 1, orb: 1, crosier: 3, lamb: 1 }, + // specific cases + natural: { fountain: "azure", garb: "or", raven: "sable" }, // charges to mainly use predefined colours + sinister: ["crossGamma", "lionRampant", "lionPassant", "wolfRampant", "wolfPassant", "wolfStatant", "wolfHeadErased", "greyhoundСourant", "boarRampant", "horseRampant", "horseSalient", "bullPassant", + "bearRampant", "bearPassant", "goat", "lamb", "elephant", + "eagle", "raven", "cock", "parrot", "swan", "swanErased", "heron", "pike", "dragonPassant", "dragonRampant", "wyvern", "wyvernWithWingsDisplayed", "griffinPassant", "griffinRampant", "unicornRampant", + "pegasus", "serpent", "hatchet", "lochaberAxe", "hand", "wing", "wingSword", "lute", "harp", "bow", "head", "headWreathed", "knight", "lymphad", "log", + "crosier", "dolphin", "sabre"], // charges that can be sinister + reversed: ["goutte", "mullet", "mullet7", "crescent", "crossTau", "cancer", "sword", "sabresCrossed", "hand", "horseshoe", "bowWithArrow", "arrow", "arrowsSheaf", "rake"] // charges that can be reversed + } + + const positions = { + conventional: { e: 20, abcdefgzi: 3, beh: 3, behdf: 2, acegi: 1, kn: 3, bhdf: 1, jeo: 1, abc: 3, jln: 6, jlh: 3, kmo: 2, jleh: 1, def: 3, abcpqh: 4, ABCDEFGHIJKL: 1 }, + complex: { e: 40, beh: 1, kn: 1, jeo: 1, abc: 2, jln: 7, jlh: 2, def: 1, abcpqh: 1 }, + divisions: { + perPale: { e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 1 }, + perFess: { e: 12, kn: 4, jkl: 2, gizgiz: 1, jlh: 3, kmo: 1, ABCDEFGHIJKL: 1 }, + perBend: { e: 5, lm: 5, bcfdgh: 1 }, + perBendSinister: { e: 1, jo: 1 }, + perCross: { e: 4, jlmo: 1, j: 1, jo: 2, jl: 1 }, + perChevron: { e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1 }, + perChevronReversed: { e: 1, mok: 2, dfh: 2, dfbh: 1, bdefh: 1 }, + perSaltire: { bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1 }, + perPile: { ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1 } + }, + ordinariesOn: { + pale: { ee: 12, beh: 10, kn: 3, bb: 1 }, + fess: { ee: 1, def: 3 }, + bar: { defdefdef: 1 }, + fessCotissed: { ee: 1, def: 3 }, + fessDoubleCotissed: { ee: 1, defdef: 3 }, + bend: { ee: 2, jo: 1, joe: 1 }, + bendSinister: { ee: 1, lm: 1, lem: 4 }, + bendlet: { joejoejoe: 1 }, + bendletSinister: { lemlemlem: 1 }, + bordure: { ABCDEFGHIJKL: 1 }, + chief: { abc: 5, bbb: 1 }, + quarter: { jjj: 1 }, + canton: { yyyy: 1 }, + cross: { eeee: 1, behdfbehdf: 3, behbehbeh: 2 }, + crossParted: { e: 5, ee: 1 }, + saltire: { ee: 5, jlemo: 1 }, + saltireParted: { e: 5, ee: 1 }, + pall: { ee: 1, jleh: 5, jlhh: 3 }, + pallReversed: { ee: 1, bemo: 5 }, + pile: { bbb: 1 }, + pileInBend: { eeee: 1, eeoo: 1 }, + pileInBendSinister: { eeee: 1, eemm: 1 } + }, + ordinariesOff: { + pale: { yyy: 1 }, + fess: { abc: 3, abcz: 1 }, + bar: { abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1 }, + gemelle: { abc: 1 }, + bend: { ccg: 2, ccc: 1 }, + bendSinister: { aai: 2, aaa: 1 }, + bendlet: { ccg: 2, ccc: 1 }, + bendletSinister: { aai: 2, aaa: 1 }, + bordure: { e: 4, jleh:2, kenken: 1, peqpeq: 1 }, + orle: { e: 4, jleh: 1, kenken: 1, peqpeq: 1 }, + chief: { emo: 2, emoz: 1, ez: 2 }, + terrace: { e: 5, def: 1, bdf: 3 }, + mount: { e: 5, def: 1, bdf: 3 }, + point: { e: 2, def: 1, bdf: 3, acbdef: 1 }, + flaunches: { e: 3, kn: 1, beh: 3 }, + gyron: { bh: 1 }, + quarter: { e: 1 }, + canton: { e: 5, beh: 1, def: 1, bdefh: 1, kn: 1 }, + cross: { acgi: 1 }, + pall: { BCKFEILGJbdmfo: 1 }, + pallReversed: { aczac: 1 }, + chevron: { ach: 3, hhh: 1 }, + chevronReversed: { bbb: 1 }, + pile: { acdfgi: 1, acac: 1 }, + pileInBend: { cg: 1 }, + pileInBendSinister: { ai: 1 }, + label: { defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5 } + }, + // charges + inescutcheon: { e: 4, jln: 1 }, + mascle: { e: 15, abcdefgzi: 3, beh: 3, bdefh: 4, acegi: 1, kn: 3, joe: 2, abc: 3, jlh: 8, jleh: 1, df: 3, abcpqh: 4, pqe: 3, eknpq: 3 }, + lionRampant: { e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1 }, + lionPassant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 }, + wolfPassant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 }, + greyhoundСourant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 }, + griffinRampant: { e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1 }, + griffinPassant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 }, + boarRampant: { e: 12, beh: 1, kn: 1, jln: 2 }, + eagle: { e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1 }, + raven: { e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1 }, + wyvern: { e: 10, jln: 1 }, + garb: { e: 1, def: 3, abc: 2, beh: 1, kn: 1, jln: 3, jleh: 1, abcpqh: 1, joe: 1, lme: 1 }, + crown: { e: 10, abcdefgzi: 1, beh: 3, behdf: 2, acegi: 1, kn: 1, pq: 2, abc: 1, jln: 4, jleh: 1, def: 2, abcpqh: 3 }, + hand: { e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1 }, + armillarySphere: {e: 1}, + tree: {e: 1}, + lymphad: {e: 1}, + head: {e: 1}, + headWreathed: {e: 1} + }; + + const lines = { + straight: 50, wavy: 8, engrailed: 4, invecked: 3, rayonne: 3, embattled: 1, raguly: 1, urdy: 1, dancetty: 1, indented: 2, + dentilly: 1, bevilled: 1, angled: 1, flechy: 1, barby: 1, enclavy: 1, escartely: 1, arched: 2, archedReversed: 1, nowy: 1, nowyReversed: 1, + embattledGhibellin: 1, embattledNotched: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1, + potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 2, seaWaves: 1, dragonTeeth: 1, firTrees: 1 + }; + + const divisions = { + variants: { perPale: 5, perFess: 5, perBend: 2, perBendSinister: 1, perChevron: 1, perChevronReversed: 1, perCross: 5, perPile: 1, perSaltire: 1, gyronny: 1, chevronny: 1 }, + perPale: lines, + perFess: lines, + perBend: lines, + perBendSinister: lines, + perChevron: lines, + perChevronReversed: lines, + perCross: { straight: 20, wavy: 5, engrailed: 4, invecked: 3, rayonne: 1, embattled: 1, raguly: 1, urdy: 1, indented: 2, dentilly: 1, bevilled: 1, angled: 1, embattledGhibellin: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1, potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 1 }, + perPile: lines + }; + + const ordinaries = { + lined: { + pale: 7, fess: 5, bend: 3, bendSinister: 2, chief: 5, bar: 2, gemelle: 1, fessCotissed: 1, fessDoubleCotissed: 1, + bendlet: 2, bendletSinister: 1, terrace: 3, cross: 6, crossParted: 1, saltire: 2, saltireParted: 1 + }, + straight: { + bordure: 8, orle: 4, mount: 1, point: 2, flaunches: 1, gore: 1, + gyron: 1, quarter: 1, canton: 2, pall: 3, pallReversed: 2, chevron: 4, chevronReversed: 3, + pile: 2, pileInBend: 2, pileInBendSinister: 1, piles: 1, pilesInPoint: 2, label: 1 + } + }; + + 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 = []; + + const t1 = P(kinship) ? parent.t1 : getTincture("field"); + if (t1.includes("-")) usedPattern = t1; + const coa = {t1}; + + let charge = P(usedPattern ? .5 : .93) ? true : false; // 80% for charge + const linedOrdinary = charge && P(.3) || P(.5) ? parent?.ordinaries && P(kinship) ? parent.ordinaries[0].ordinary : rw(ordinaries.lined) : null; + const ordinary = !charge && P(.65) || P(.3) ? linedOrdinary ? linedOrdinary : rw(ordinaries.straight) : null; // 36% for ordinary + const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary); + const divisioned = rareDivided ? P(.03) : charge && ordinary ? P(.03) : charge ? P(.3) : ordinary ? P(.7) : P(.995); // 33% for division + const division = divisioned ? parent?.division && P(kinship - .1) ? parent.division.division : rw(divisions.variants) : null; + if (charge) charge = + parent?.charges && P(kinship - .1) ? parent.charges[0].charge : + type && type !== "Generic" && P(.2) ? rw(charges[type]) : + selectCharge(); + + if (division) { + const t = getTincture("division", usedTinctures, P(.98) ? coa.t1 : null); + coa.division = {division, t}; + if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(.7)) ? "straight" : rw(divisions[division]); + } + + if (ordinary) { + coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}]; + if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(.7)) ? "straight" : rw(lines); + if (division && !charge && !usedPattern && P(.5) && ordinary !== "bordure" && ordinary !== "orle") { + if (P(.8)) coa.ordinaries[0].divided = "counter"; // 40% + else if (P(.6)) coa.ordinaries[0].divided = "field"; // 6% + else coa.ordinaries[0].divided = "division"; // 4% + } + } + + if (charge) { + let p = "e", t = "gules"; + const ordinaryT = coa.ordinaries ? coa.ordinaries[0].t : null; + if (positions.ordinariesOn[ordinary] && P(.8)) { + // place charge over ordinary (use tincture of field type) + p = rw(positions.ordinariesOn[ordinary]); + while (charges.natural[charge] === ordinaryT) charge = selectCharge(); + t = !usedPattern && P(.3) ? coa.t1 : getTincture("charge", [], ordinaryT); + } else if (positions.ordinariesOff[ordinary] && P(.95)) { + // place charge out of ordinary (use tincture of ordinary type) + p = rw(positions.ordinariesOff[ordinary]); + while (charges.natural[charge] === coa.t1) charge = selectCharge(); + t = !usedPattern && P(.3) ? ordinaryT : getTincture("charge", usedTinctures, coa.t1); + } else if (positions.divisions[division]) { + // place charge in fields made by division + p = rw(positions.divisions[division]); + while (charges.natural[charge] === coa.t1) charge = selectCharge(); + t = getTincture("charge", ordinaryT ? usedTinctures.concat(ordinaryT) : usedTinctures, coa.t1); + } else if (positions[charge]) { + // place charge-suitable position + p = rw(positions[charge]); + while (charges.natural[charge] === coa.t1) charge = selectCharge(); + t = getTincture("charge", usedTinctures, coa.t1); + } else { + // place in standard position (use new tincture) + p = usedPattern ? "e" : charges.conventional[charge] ? rw(positions.conventional) : rw(positions.complex); + while (charges.natural[charge] === coa.t1) charge = selectCharge(); + t = getTincture("charge", usedTinctures.concat(ordinaryT), coa.t1); + } + + if (charges.natural[charge]) t = charges.natural[charge]; // natural tincture + coa.charges = [{charge, t, p}]; + + if (p === "ABCDEFGHIKL" && P(.95)) { + // add central charge if charge is in bordure + coa.charges[0].charge = rw(charges.conventional); + const charge = selectCharge(charges.single); + const t = getTincture("charge", usedTinctures, coa.t1); + coa.charges.push({charge, t, p: "e"}); + } else if (P(.8) && charge === "inescutcheon") { + // add charge to inescutcheon + const charge = selectCharge(charges.types); + const t2 = getTincture("charge", [], t); + coa.charges.push({charge, t: t2, p, size:.5}); + } else if (division && !ordinary) { + const allowCounter = !usedPattern && (!coa.line || coa.line === "straight"); + + // dimidiation: second charge at division basic positons + if (P(.3) && ["perPale", "perFess"].includes(division) && coa.line === "straight") { + coa.charges[0].divided = "field"; + if (P(.95)) { + const p2 = p === "e" || P(.5) ? "e" : rw(positions.divisions[division]); + const charge = selectCharge(charges.single); + const t = getTincture("charge", usedTinctures, coa.division.t); + coa.charges.push({charge, t, p: p2, divided: "division"}); + } + } + else if (allowCounter && P(.4)) coa.charges[0].divided = "counter"; // counterchanged, 40% + else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(.8)) { // place 2 charges in division standard positions + const [p1, p2] = division === "perPale" ? ["p", "q"] : + division === "perFess" ? ["k", "n"] : + division === "perBend" ? ["l", "m"] : + ["j", "o"]; // perBendSinister + coa.charges[0].p = p1; + + const charge = selectCharge(charges.single); + const t = getTincture("charge", usedTinctures, coa.division.t); + coa.charges.push({charge, t, p: p2}); + } + else if (["perCross", "perSaltire"].includes(division) && P(.5)) { // place 4 charges in division standard positions + const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"]; + coa.charges[0].p = p1; + + const c2 = selectCharge(charges.single); + const t2 = getTincture("charge", [], coa.division.t); + + const c3 = selectCharge(charges.single); + const t3 = getTincture("charge", [], coa.division.t); + + const c4 = selectCharge(charges.single); + const t4 = getTincture("charge", [], coa.t1); + coa.charges.push({charge: c2, t: t2, p: p2}, {charge: c3, t: t3, p: p3}, {charge: c4, t: t4, p: p4}); + } + else if (allowCounter && p.length > 1) coa.charges[0].divided = "counter"; // counterchanged, 40% + } + + coa.charges.forEach(c => defineChargeAttributes(c)); + function defineChargeAttributes(c) { + // define size + c.size = (c.size || 1) * getSize(c.p, ordinary, division); + + // clean-up position + c.p = [...new Set(c.p)].join(""); + + // define orientation + if (P(.02) && charges.sinister.includes(c.charge)) c.sinister = 1; + if (P(.02) && charges.reversed.includes(c.charge)) c.reversed = 1; + } + } + + // dominions have canton with parent coa + if (P(dominion) && parent.charges) { + const invert = isSameType(parent.t1, coa.t1); + const t = invert ? getTincture("division", usedTinctures, coa.t1) : parent.t1; + const canton = {ordinary: "canton", t}; + if (coa.charges) { + coa.charges.forEach((charge, i) => { + if (charge.size === 1.5) charge.size = 1.4; + if (charge.p.includes("a")) charge.p = charge.p.replaceAll("a", ""); + if (charge.p.includes("j")) charge.p = charge.p.replaceAll("j", ""); + if (charge.p.includes("y")) charge.p = charge.p.replaceAll("y", ""); + if (!charge.p) coa.charges.splice(i, 1); + }); + } + + let charge = parent.charges[0].charge; + if (charge === "inescutcheon" && parent.charges[1]) charge = parent.charges[1].charge; + + let t2 = invert ? parent.t1 : parent.charges[0].t; + if (isSameType(t, t2)) t2 = getTincture("charge", usedTinctures, t); + + if (!coa.charges) coa.charges = []; + coa.charges.push({charge, t: t2, p: "y", size: 0.5}); + + coa.ordinaries ? coa.ordinaries.push(canton) : coa.ordinaries = [canton]; + } + + function selectCharge(set) { + const type = set ? rw(set) : ordinary || divisioned ? rw(charges.types): rw(charges.single); + return type === "inescutcheon" ? "inescutcheon" : rw(charges[type]); + } + + // select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT + function getTincture(element, fields = [], RoT) { + const base = RoT ? RoT.includes("-") ? RoT.split("-")[1] : RoT : null; + + let type = rw(tinctures[element]); // metals, colours, stains, patterns + if (RoT && type !== "patterns") type = getType(base) === "metals" ? "colours" : "metals"; // follow RoT + if (type === "metals" && fields.includes("or") && fields.includes("argent")) type = "colours"; // exclude metals overuse + let tincture = rw(tinctures[type]); + + while (tincture === base || fields.includes(tincture)) {tincture = rw(tinctures[type]);} // follow RoT + + if (type !== "patterns" && element !== "charge") usedTinctures.push(tincture); // add field tincture + + if (type === "patterns") { + usedPattern = tincture; + tincture = definePattern(tincture, element); + } + + return tincture; + } + + function getType(t) { + const tincture = t.includes("-") ? t.split("-")[1] : t; + if (Object.keys(tinctures.metals).includes(tincture)) return "metals"; + if (Object.keys(tinctures.colours).includes(tincture)) return "colours"; + if (Object.keys(tinctures.stains).includes(tincture)) return "stains"; + } + + function isSameType(t1, t2) { + return type(t1) === type(t2); + + function type(tincture) { + if (Object.keys(tinctures.metals).includes(tincture)) return "metals"; + if (Object.keys(tinctures.colours).includes(tincture)) return "colours"; + if (Object.keys(tinctures.stains).includes(tincture)) return "stains"; + else return "pattern"; + } + + } + + function definePattern(pattern, element, size = "") { + let t1 = null, t2 = null; + 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";} + else if (P(.8) && pattern === "ermine") {t1 = "argent"; t2 = "sable";} + else if (pattern === "pappellony") { + if (P(.2)) {t1 = "gules"; t2 = "or";} + else if (P(.2)) {t1 = "argent"; t2 = "sable";} + else if (P(.2)) {t1 = "azure"; t2 = "argent";} + } + else if (pattern === "masoned") { + if (P(.3)) {t1 = "gules"; t2 = "argent";} + else if (P(.3)) {t1 = "argent"; t2 = "sable";} + else if (P(.1)) {t1 = "or"; t2 = "sable";} + } + else if (pattern === "fretty") { + if (t2 === "sable" || P(.35)) {t1 = "argent"; t2 = "gules";} + else if (P(.25)) {t1 = "sable"; t2 = "or";} + else if (P(.15)) {t1 = "gules"; t2 = "argent";} + } + else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy); + + + if (!t1 || !t2) { + const startWithMetal = P(.7); + t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours); + t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals); + } + + // division should not be the same tincture as base field + if (element === "division") { + if (usedTinctures.includes(t1)) t1 = replaceTincture(t1); + if (usedTinctures.includes(t2)) t2 = replaceTincture(t2); + } + + usedTinctures.push(t1, t2); + return `${pattern}-${t1}-${t2}${size}`; + } + + function replaceTincture(t, n) { + const type = getType(t); + while (!n || n === t) {n = rw(tinctures[type]);} + return n; + } + + function getSize(p, o = null, d = null) { + if (p === "e" && (o === "bordure" || o === "orle")) return 1.1; + if (p === "e") return 1.5; + if (p === "jln" || p === "jlh") return .7; + if (p === "abcpqh" || p === "ez" || p === "be") return .5; + if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return .5; + if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return .6; + if (p.length > 10) return .18; // >10 (bordure) + if (p.length > 7) return .3; // 8, 9, 10 + if (p.length > 4) return .4; // 5, 6, 7 + if (p.length > 2) return .5; // 3, 4 + return .7; // 1, 2 + } + + return coa; + } + + const getShield = function(culture, state) { + 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("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, shields}; + +}))); \ No newline at end of file diff --git a/modules/coa-renderer.js b/modules/coa-renderer.js new file mode 100644 index 00000000..3a451782 --- /dev/null +++ b/modules/coa-renderer.js @@ -0,0 +1,1094 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.COArenderer = factory()); +}(this, (function () {'use strict'; + + const colors = { + argent: "#fafafa", + or: "#ffe066", + gules: "#d7374a", + sable: "#333333", + azure: "#377cd7", + vert: "#26c061", + purpure: "#522d5b", + murrey: "#85185b", + sanguine: "#b63a3a", + tenné: "#cc7f19" + } + + const shieldPositions = { + // shield-specific position: [x, y] (relative to center) + heater: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-32.25, 37.5], h: [0, 50], i: [32.25, 37.5], + y: [-50, -50], z: [0, 62.5], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-30, 30], n: [0, 42.5], o: [30, 30], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.2, -66.6], B: [-22, -66.6], C: [22, -66.6], D: [66.2, -66.6], + K: [-66.2, -20], E: [66.2, -20], + J: [-55.5, 26], F: [55.5, 26], + I: [-33, 62], G: [33, 62], + H: [0, 89.5] + }, + spanish: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 50], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.2, -66.6], B: [-22, -66.6], C: [22, -66.6], D: [66.2, -66.6], + K: [-66.4, -20], E: [66.4, -20], + J: [-66.4, 26], F: [66.4, 26], + I: [-49, 70], G: [49, 70], + H: [0, 92] + }, + french: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 65], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.2, -66.6], B: [-22, -66.6], C: [22, -66.6], D: [66.2, -66.6], + K: [-66.4, -20], E: [66.4, -20], + J: [-66.4, 26], F: [66.4, 26], + I: [-65.4, 70], G: [65.4, 70], + H: [0, 89] + }, + horsehead: { + a: [-43.75, -47.5], b: [0, -50], c: [43.75, -47.5], + d: [-35, 0], e: [0, 0], f: [35, 0], + h: [0, 50], + y: [-50, -50], z: [0, 55], + j: [-35, -35], k: [0, -40], l: [35, -35], + m: [-30, 30], n: [0, 40], o: [30, 30], + p: [-27.5, 0], q: [27.5, 0], + A: [-71, -52], B: [-24, -73], C: [24, -73], D: [71, -52], + K: [-62, -16], E: [62, -16], + J: [-39, 20], F: [39, 20], + I: [-33.5, 60], G: [33.5, 60], + H: [0, 91.5] + }, + horsehead2: { + a: [-37.5, -47.5], b: [0, -50], c: [37.5, -47.5], + d: [-35, 0], e: [0, 0], f: [35, 0], + g: [-35, 47.5], h: [0, 50], i: [35, 47.5], + y: [-50, -50], z: [0, 55], + j: [-30, -30], k: [0, -40], l: [30, -30], + m: [-30, 30], n: [0, 40], o: [30, 30], + p: [-27.5, 0], q: [27.5, 0], + A: [-49, -39], B: [-22, -70], C: [22, -70], D: [49, -39], + K: [-51, -2], E: [51, -2], + J: [-38.5, 31], F: [38.5, 31], + I: [-35, 67], G: [35, 67], + H: [0, 85] + }, + polish: { + a: [-35, -50], b: [0, -50], c: [35, -50], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-37.5, 50], h: [0, 50], i: [37.5, 50], + y: [-50, -50], z: [0, 65], + j: [-27.5, -27.5], k: [0, -45], l: [27.5, -27.5], + m: [-27.5, 27.5], n: [0, 45], o: [27.5, 27.5], + p: [-32.5, 0], q: [32.5, 0], + A: [-48, -52], B: [-23, -80], C: [23, -80], D: [48, -52], + K: [-47, -10], E: [47, -10], + J: [-62, 32], F: [62, 32], + I: [-37, 68], G: [37, 68], + H: [0, 86] + }, + hessen: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 52.5], + j: [-40, -40], k: [0, -40], l: [40, -40], + m: [-40, 40], n: [0, 40], o: [40, 40], + p: [-40, 0], q: [40, 0], + A: [-69, -64], B: [-22, -76], C: [22, -76], D: [69, -64], + K: [-66.4, -20], E: [66.4, -20], + J: [-62, 26], F: [62, 26], + I: [-46, 70], G: [46, 70], + H: [0, 91.5] + }, + swiss: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-32, 37.5], h: [0, 50], i: [32, 37.5], + y: [-50, -50], z: [0, 62.5], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-32, 32.5], n: [0, 42.5], o: [32, 32.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.2, -66.6], B: [-22, -66], C: [22, -66], D: [66.2, -66.6], + K: [-63, -20], E: [63, -20], + J: [-50, 26], F: [50, 26], + I: [-29, 62], G: [29, 62], + H: [0, 89.5] + }, + boeotian: { + a: [-37.5, -47.5], b: [0, -47.5], c: [37.5, -47.5], + d: [-25, 0], e: [0, 0], f: [25, 0], + g: [-37.5, 47.5], h: [0, 47.5], i: [37.5, 47.5], + y: [-48, -48], z: [0, 60], + j: [-32.5, -37.5], k: [0, -45], l: [32.5, -37.5], + m: [-32.5, 37.5], n: [0, 45], o: [32.5, 37.5], + p: [-20, 0], q: [20, 0], + A: [-45, -55], B: [-20, -77], C: [20, -77], D: [45, -55], + K: [-59, -25], E: [59, -25], + J: [-58, 27], F: [58, 27], + I: [-39, 63], G: [39, 63], + H: [0, 81] + }, + roman: { + a: [-40, -52.5], b: [0, -52.5], c: [40, -52.5], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-40, 52.5], h: [0, 52.5], i: [40, 52.5], + y: [-42.5, -52.5], z: [0, 65], + j: [-30, -37.5], k: [0, -37.5], l: [30, -37.5], + m: [-30, 37.5], n: [0, 37.5], o: [30, 37.5], + p: [-30, 0], q: [30, 0], + A: [-51.5, -65], B: [-17, -75], C: [17, -75], D: [51.5, -65], + K: [-51.5, -21], E: [51.5, -21], + J: [-51.5, 21], F: [51.5, 21], + I: [-51.5, 65], G: [51.5, 65], + H: [-17, 75], L: [17, 75] + }, + kite: { + b: [0, -65], e: [0, -15], h: [0, 35], + z: [0, 35], k: [0, -50], n: [0, 20], + p: [-20, -15], q: [20, -15], + A: [-38, -52], B: [-29, -78], C: [29, -78], D: [38, -52], + K: [-33, -20], E: [33, -20], + J: [-25, 11], F: [25, 11], + I: [-15, 42], G: [15, 42], + H: [0, 73], L: [0, -91] + }, + oldFrench: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-37.5, 50], h: [0, 50], i: [37.5, 50], + y: [-50, -50], z: [0, 62.5], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 45], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.2, -66.6], B: [-22, -66.6], C: [22, -66.6], D: [66.2, -66.6], + K: [-66.2, -20], E: [66.2, -20], + J: [-64, 26], F: [64, 26], + I: [-45, 62], G: [45, 62], + H: [0, 91], + }, + renaissance: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-41.5, 0], e: [0, 0], f: [41.5, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 62.5], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-61, -55], B: [-23, -67], C: [23, -67], D: [61, -55], + K: [-55, -11], E: [55, -11], + J: [-65, 31], F: [65, 31], + I: [-45, 76], G: [45, 76], + H: [0, 87] + }, + baroque: { + a: [-43.75, -45], b: [0, -45], c: [43.75, -45], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 60], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-65, -54.5], B: [-22, -65], C: [22, -65], D: [65, -54.5], + K: [-58.5, -15], E: [58.5, -15], + J: [-65, 31], F: [66, 31], + I: [-35, 73], G: [35, 73], + H: [0, 89] + }, + targe: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 50], + j: [-40, -40], k: [0, -40], l: [40, -40], + m: [-40, 40], n: [0, 40], o: [40, 40], + p: [-32.5, 0], q: [32.5, 0], + A: [-66.2, -60], B: [-22, -77], C: [22, -86], D: [60, -66.6], + K: [-28, -20], E: [57, -20], + J: [-61, 26], F: [61, 26], + I: [-49, 63], G: [49, 59], + H: [0, 80] + }, + targe2: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-43.75, 50], h: [0, 50], i: [43.75, 50], + y: [-50, -50], z: [0, 60], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-32.5, 0], q: [32.5, 0], + A: [-55, -59], B: [-15, -59], C: [24, -79], D: [51, -58], + K: [-40, -14], E: [51, -14], + J: [-64, 26], F: [62, 26], + I: [-46, 66], G: [48, 67], + H: [0, 83] + }, + pavise: { + a: [-40, -52.5], b: [0, -52.5], c: [40, -52.5], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-40, 52.5], h: [0, 52.5], i: [40, 52.5], + y: [-42.5, -52.5], z: [0, 60], + j: [-30, -35], k: [0, -37.5], l: [30, -35], + m: [-30, 35], n: [0, 37.5], o: [30, 35], + p: [-30, 0], q: [30, 0], + A: [-57, -55], B: [-22, -74], C: [22, -74], D: [57, -55], + K: [-54, -11], E: [54, -11], + J: [-50, 36], F: [50, 36], + I: [-46, 81], G: [46, 81], + H: [0, 81] + }, + wedged: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.75, 0], e: [0, 0], f: [43.75, 0], + g: [-32.25, 37.5], h: [0, 50], i: [32.25, 37.5], + y: [-50, -50], z: [0, 62.5], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-32.5, 32.5], n: [0, 42.5], o: [32.5, 32.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66, -53], B: [-22, -72.5], C: [22, -72.5], D: [66, -53], + K: [-62.6, -13], E: [62.6, -13], + J: [-50, 26], F: [50, 26], + I: [-27, 62], G: [27, 62], + H: [0, 87] + }, + flag: { + a: [-60, -40], b: [0, -40], c: [60, -40], + d: [-60, 0], e: [0, 0], f: [60, 0], + g: [-60, 40], h: [0, 40], i: [60, 40], + y: [-60, -42.5], z: [0, 40], + j: [-45, -30], k: [0, -30], l: [45, -30], + m: [-45, 30], n: [0, 30], o: [45, 30], + p: [-45, 0], q: [45, 0], + A: [-81, -51], B: [-27, -51], C: [27, -51], D: [81, -51], + K: [-81, -17], E: [81, -17], + J: [-81, 17], F: [81, 17], + I: [-81, 51], G: [81, 51], + H: [-27, 51], L: [27, 51] + }, + pennon: { + a: [-75, -40], + d: [-75, 0], e: [-25, 0], f: [25, 0], + g: [-75, 40], + y: [-70, -42.5], + j: [-60, -30], + m: [-60, 30], + p: [-60, 0], q: [5, 0], + A: [-81, -48], B: [-43, -36], C: [-4.5, -24], D: [33, -12], + E: [72, 0], + F: [33, 12], G: [-4.5, 24], H: [-43, 36], I: [-81, 48], + J: [-81, 17], K: [-81, -17] + }, + guidon: { + a: [-60, -40], b: [0, -40], c: [60, -40], + d: [-60, 0], e: [0, 0], + g: [-60, 40], h: [0, 40], i: [60, 40], + y: [-60, -42.5], z: [0, 40], + j: [-45, -30], k: [0, -30], l: [45, -30], + m: [-45, 30], n: [0, 30], o: [45, 30], + p: [-45, 0], + A: [-81, -51], B: [-27, -51], C: [27, -51], D: [78, -51], + K: [-81, -17], E: [40.5, -17], + J: [-81, 17], F: [40.5, 17], + I: [-81, 51], G: [78, 51], + H: [-27, 51], L: [27, 51] + }, + banner: { + a: [-50, -50], b: [0, -50], c: [50, -50], + d: [-50, 0], e: [0, 0], f: [50, 0], + g: [-50, 40], h: [0, 40], i: [50, 40], + y: [-50, -50], z: [0, 40], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 27.5], n: [0, 27.5], o: [37.5, 27.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.5, -66.5], B: [-22, -66.5], C: [22, -66.5], D: [66.5, -66.5], + K: [-66.5, -20], E: [66.5, -20], + J: [-66.5, 26], F: [66.5, 26], + I: [-66.5, 66.5], G: [66.5, 66.5], + H: [-25, 75], L: [25, 75] + }, + dovetail: { + a: [-49.75, -50], b: [0, -50], c: [49.75, -50], + d: [-49.75, 0], e: [0, 0], f: [49.75, 0], + g: [-49.75, 50], i: [49.75, 50], + y: [-50, -50], z: [0, 40], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 32.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.5, -66.5], B: [-22, -66.5], C: [22, -66.5], D: [66.5, -66.5], + K: [-66.5, -16.5], E: [66.5, -16.5], + J: [-66.5, 34.5], F: [66.5, 34.5], + I: [-66.5, 84.5], G: [66.5, 84.5], + H: [-25, 64], L: [25, 64] + }, + gonfalon: { + a: [-49.75, -50], b: [0, -50], c: [49.75, -50], + d: [-49.75, 0], e: [0, 0], f: [49.75, 0], + g: [-49.75, 50], h: [0, 50], i: [49.75, 50], + y: [-50, -50], z: [0, 50], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.5, -66.5], B: [-22, -66.5], C: [22, -66.5], D: [66.5, -66.5], + K: [-66.5, -20], E: [66.5, -20], + J: [-66.5, 26], F: [66.5, 26], + I: [-40, 63], G: [40, 63], + H: [0, 88] + }, + pennant: { + a: [-45, -50], b: [0, -50], c: [45, -50], + e: [0, 0], h: [0, 50], + y: [-50, -50], z: [0, 50], + j: [-32.5, -37.5], k: [0, -37.5], l: [32.5, -37.5], + n: [0, 37.5], + A: [-60, -76], B: [-22, -76], C: [22, -76], D: [60, -76], + K: [-46, -38], E: [46, -38], + J: [-31, 0], F: [31, 0], + I: [-16, 38], G: [16, 38], + H: [0, 76] + }, + round: { + a: [-40, -40], b: [0, -40], c: [40, -40], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-40, 40], h: [0, 40], i: [40, 40], + y: [-48, -48], z: [0, 57.5], + j: [-35.5, -35.5], k: [0, -37.5], l: [35.5, -35.5], + m: [-35.5, 35.5], n: [0, 37.5], o: [35.5, 35.5], + p: [-36.5, 0], q: [36.5, 0], + A: [-59, -48], B: [-23, -73], C: [23, -73], D: [59, -48], + K: [-76, -10], E: [76, -10], + J: [-70, 31], F: [70, 31], + I: [-42, 64], G: [42, 64], + H: [0, 77] + }, + oval: { + a: [-37.5, -50], b: [0, -50], c: [37.5, -50], + d: [-43, 0], e: [0, 0], f: [43, 0], + g: [-37.5, 50], h: [0, 50], i: [37.5, 50], + y: [-48, -48], z: [0, 60], + j: [-35.5, -37.5], k: [0, -37.5], l: [35.5, -37.5], + m: [-35.5, 37.5], n: [0, 50], o: [35.5, 37.5], + p: [-36.5, 0], q: [36.5, 0], + A: [-48, -48], B: [-23, -78], C: [23, -78], D: [48, -48], + K: [-59, -10], E: [59, -10], + J: [-55, 31], F: [55, 31], + I: [-36, 68], G: [36, 68], + H: [0, 85] + }, + vesicaPiscis: { + a: [-32, -37], b: [0, -50], c: [32, -37], + d: [-32, 0], e: [0, 0], f: [32, 0], + g: [-32, 37], h: [0, 50], i: [32, 37], + y: [-50, -50], z: [0, 62], + j: [-27.5, -27.5], k: [0, -37], l: [27.5, -27.5], + m: [-27.5, 27.5], n: [0, 42], o: [27.5, 27.5], + p: [-27.5, 0], q: [27.5, 0], + A: [-45, -32], B: [-29, -63], C: [29, -63], D: [45, -32], + K: [-50, 0], E: [50, 0], + J: [-45, 32], F: [45, 32], + I: [-29, 63], G: [29, 63], + H: [0, 89], L: [0, -89] + }, + square: { + a: [-49.75, -50], b: [0, -50], c: [49.75, -50], + d: [-49.75, 0], e: [0, 0], f: [49.75, 0], + g: [-49.75, 50], h: [0, 50], i: [49.75, 50], + y: [-50, -50], z: [0, 50], + j: [-37.5, -37.5], k: [0, -37.5], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 37.5], o: [37.5, 37.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-66.5, -66.5], B: [-22, -66.5], C: [22, -66.5], D: [66.5, -66.5], + K: [-66.5, -20], E: [66.5, -20], + J: [-66.5, 26], F: [66.5, 26], + I: [-66.5, 66.5], G: [66.5, 66.5], + H: [-22, 66.5], L: [22, 66.5] + }, + diamond: { + a: [-32, -37], b: [0, -50], c: [32, -37], + d: [-43, 0], e: [0, 0], f: [43, 0], + g: [-32, 37], h: [0, 50], i: [32, 37], + y: [-50, -50], z: [0, 62], + j: [-27.5, -27.5], k: [0, -37], l: [27.5, -27.5], + m: [-27.5, 27.5], n: [0, 42], o: [27.5, 27.5], + p: [-37, 0], q: [37, 0], + A: [-43, -28], B: [-22, -56], C: [22, -56], D: [43, -28], + K: [-63, 0], E: [63, 0], + J: [-42, 28], F: [42, 28], + I: [-22, 56], G: [22, 56], + H: [0, 83], L: [0, -82] + }, + no: { + a: [-66.5, -66.5], b: [0, -66.5], c: [66.5, -66.5], + d: [-66.5, 0], e: [0, 0], f: [66.5, 0], + g: [-66.5, 66.5], h: [0, 66.5], i: [66.5, 66.5], + y: [-50, -50], z: [0, 75], + j: [-50, -50], k: [0, -50], l: [50, -50], + m: [-50, 50], n: [0, 50], o: [50, 50], + p: [-50, 0], q: [50, 0], + A: [-91.5, -91.5], B: [-30.5, -91.5], C: [30.5, -91.5], D: [91.5, -91.5], + K: [-91.5, -30.5], E: [91.5, -30.5], + J: [-91.5, 30.5], F: [91.5, 30.5], + I: [-91.5, 91.5], G: [91.5, 91.5], + H: [-30.5, 91.5], L: [30.5, 91.5] + }, + fantasy1: { + a: [-45, -45], b: [0, -50], c: [45, -45], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-36, 42.5], h: [0, 50], i: [36, 42.5], + y: [-50, -50], z: [0, 60], + j: [-37, -37], k: [0, -40], l: [37, -37], + m: [-32, 32], n: [0, 40], o: [32, 32], + p: [-28.5, 0], q: [28.5, 0], + A: [-66, -55], B: [-22, -67], C: [22, -67], D: [66, -55], + K: [-53, -20], E: [53, -20], + J: [-46, 26], F: [46, 26], + I: [-29, 62], G: [29, 62], + H: [0, 84] + }, + fantasy2: { + a: [-45, -45], b: [0, -45], c: [45, -45], + d: [-35, 0], e: [0, 0], f: [35, 0], + g: [-36, 42.5], h: [0, 45], i: [36, 42.5], + y: [-50, -50], z: [0, 55], + j: [-32.5, -32.5], k: [0, -40], l: [32.5, -32.5], + m: [-30, 30], n: [0, 40], o: [30, 30], + p: [-27.5, 0], q: [27.5, 0], + A: [-58, -35], B: [-44, -67], C: [44, -67], D: [58, -35], + K: [-39, -5], E: [39, -5], + J: [-57, 26], F: [57, 26], + I: [-32, 58], G: [32, 58], + H: [0, 83], L: [0, -72] + }, + fantasy3: { + a: [-40, -45], b: [0, -50], c: [40, -45], + d: [-35, 0], e: [0, 0], f: [35, 0], + g: [-36, 42.5], h: [0, 50], i: [36, 42.5], + y: [-50, -50], z: [0, 55], + j: [-32.5, -32.5], k: [0, -40], l: [32.5, -32.5], + m: [-30, 30], n: [0, 40], o: [30, 30], + p: [-27.5, 0], q: [27.5, 0], + A: [-56, -42], B: [-22, -72], C: [22, -72], D: [56, -42], + K: [-37, -11], E: [37, -11], + J: [-60, 20], F: [60, 20], + I: [-34, 56], G: [34, 56], + H: [0, 83] + }, + fantasy4: { + a: [-50, -45], b: [0, -50], c: [50, -45], + d: [-45, 0], e: [0, 0], f: [45, 0], + g: [-40, 45], h: [0, 50], i: [40, 45], + y: [-50, -50], z: [0, 62.5], + j: [-37.5, -37.5], k: [0, -45], l: [37.5, -37.5], + m: [-37.5, 37.5], n: [0, 45], o: [37.5, 37.5], + p: [-35, 0], q: [35, 0], + A: [-75, -56], B: [-36, -61], C: [36, -61], D: [75, -56], + K: [-67, -12], E: [67, -12], + J: [-63, 32], F: [63, 32], + I: [-42, 75], G: [42, 75], + H: [0, 91.5], L: [0, -79] + }, + fantasy5: { + a: [-45, -50], b: [0, -50], c: [45, -50], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-30, 45], h: [0, 50], i: [30, 45], + y: [-50, -50], z: [0, 60], + j: [-37, -37], k: [0, -40], l: [37, -37], + m: [-32, 32], n: [0, 40], o: [32, 32], + p: [-28.5, 0], q: [28.5, 0], + A: [-61, -67], B: [-22, -76], C: [22, -76], D: [61, -67], + K: [-58, -25], E: [58, -25], + J: [-48, 20], F: [48, 20], + I: [-28.5, 60], G: [28.5, 60], + H: [0, 89] + }, + noldor: { + b: [0, -65], e: [0, -15], h: [0, 35], + z: [0, 35], k: [0, -50], n: [0, 30], + p: [-20, -15], q: [20, -15], + A: [-34, -47], B: [-20, -68], C: [20, -68], D: [34, -47], + K: [-18, -20], E: [18, -20], + J: [-26, 11], F: [26, 11], + I: [-14, 43], G: [14, 43], + H: [0, 74], L: [0, -85] + }, + gondor: { + a: [-32.5, -50], b: [0, -50], c: [32.5, -50], + d: [-32.5, 0], e: [0, 0], f: [32.5, 0], + g: [-32.5, 50], h: [0, 50], i: [32.5, 50], + y: [-42.5, -52.5], z: [0, 65], + j: [-25, -37.5], k: [0, -37.5], l: [25, -37.5], + m: [-25, 30], n: [0, 37.5], o: [25, 30], + p: [-25, 0], q: [25, 0], + A: [-42, -52], B: [-17, -75], C: [17, -75], D: [42, -52], + K: [-42, -15], E: [42, -15], + J: [-42, 22], F: [42, 22], + I: [-26, 60], G: [26, 60], + H: [0, 87] + }, + easterling: { + a: [-40, -47.5], b: [0, -47.5], c: [40, -47.5], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-40, 47.5], h: [0, 47.5], i: [40, 47.5], + y: [-42.5, -52.5], z: [0, 65], + j: [-30, -37.5], k: [0, -37.5], l: [30, -37.5], + m: [-30, 37.5], n: [0, 37.5], o: [30, 37.5], + p: [-30, 0], q: [30, 0], + A: [-52, -72], B: [0, -65], D: [52, -72], + K: [-52, -24], E: [52, -24], + J: [-52, 24], F: [52, 24], + I: [-52, 72], G: [52, 72], + H: [0, 65] + }, + erebor: { + a: [-40, -40], b: [0, -55], c: [40, -40], + d: [-40, 0], e: [0, 0], f: [40, 0], + g: [-40, 40], h: [0, 55], i: [40, 40], + y: [-50, -50], z: [0, 50], + j: [-35, -35], k: [0, -45], l: [35, -35], + m: [-35, 35], n: [0, 45], o: [35, 35], + p: [-37.5, 0], q: [37.5, 0], + A: [-47, -46], B: [-22, -81], C: [22, -81], D: [47, -46], + K: [-66.5, 0], E: [66.5, 0], + J: [-47, 46], F: [47, 46], + I: [-22, 81], G: [22, 81] + }, + ironHills: { + a: [-43.75, -50], b: [0, -50], c: [43.75, -50], + d: [-43.25, 0], e: [0, 0], f: [43.25, 0], + g: [-42.5, 42.5], h: [0, 50], i: [42.5, 42.5], + y: [-50, -50], z: [0, 62.5], + j: [-32.5, -32.5], k: [0, -40], l: [32.5, -32.5], + m: [-32.5, 32.5], n: [0, 40], o: [32.5, 32.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-61, -67], B: [-22, -74], C: [22, -74], D: [61, -67], + K: [-59, -20], E: [59, -20], + J: [-57, 26], F: [57, 26], + I: [-33, 64], G: [33, 64], + H: [0, 88] + }, + urukHai: { + a: [-40, -45], b: [0, -45], c: [40, -45], + d: [-36, 0], e: [0, 0], f: [36, 0], + g: [-32.25, 40], h: [0, 40], i: [32.25, 40], + y: [-50, -50], z: [0, 40], + j: [-32.5, -32.5], k: [0, -37.5], l: [32.5, -32.5], + m: [-27.5, 27.5], n: [0, 32.5], o: [27.5, 27.5], + p: [-37.5, 0], q: [37.5, 0], + A: [-31, -79], B: [-1, -90], C: [31, -74], D: [61, -57], + K: [-55, -19], E: [53, -19], + J: [-45, 19], F: [45, 19], + I: [-33, 57], G: [35, 57], + H: [0, 57], L: [-39, -50] + }, + moriaOrc: { + a: [-37.5, -37.5], b: [0, -37.5], c: [37.5, -37.5], + d: [-37.5, 0], e: [0, 0], f: [37.5, 0], + g: [-37.5, 37.5], h: [0, 37.5], i: [37.5, 37.5], + y: [-50, -50], z: [0, 40], + j: [-30, -30], k: [0, -30], l: [30, -30], + m: [-30, 30], n: [0, 30], o: [30, 30], + p: [-30, 0], q: [30, 0], + A: [-48, -48], B: [-16, -50], C: [16, -46], D: [39, -61], + K: [-52, -19], E: [52, -26], + J: [-42, 9], F: [52, 9], + I: [-31, 40], G: [40, 43], + H: [4, 47] + } + } + + const shieldSize = { + horsehead: .9, horsehead2: .9, polish: .85, swiss: .95, + boeotian: .75, roman: .95, kite: .65, targe2: .9, pavise: .9, wedged: .95, + flag: .7, pennon: .5, guidon: .65, banner: .8, dovetail: .8, pennant: .6, + oval: .95, vesicaPiscis: .8, diamond: .8, no: 1.2, + fantasy1: .8, fantasy2: .7, fantasy3: .7, fantasy5: .9, + noldor: .5, gondor: .75, easterling: .8, erebor: .9, urukHai: .8, moriaOrc: .7 + } + + const shieldBox = { + heater: "0 10 200 200", + spanish: "0 10 200 200", + french: "0 10 200 200", + + horsehead: "0 10 200 200", + horsehead2: "0 10 200 200", + polish: "0 0 200 200", + hessen: "0 5 200 200", + swiss: "0 10 200 200", + + boeotian: "0 0 200 200", + roman: "0 0 200 200", + kite: "0 0 200 200", + oldFrench: "0 10 200 200", + renaissance: "0 5 200 200", + baroque: "0 10 200 200", + + targe: "0 0 200 200", + targe2: "0 0 200 200", + pavise: "0 0 200 200", + wedged: "0 10 200 200", + + flag: "0 0 200 200", + pennon: "2.5 0 200 200", + guidon: "2.5 0 200 200", + banner: "0 10 200 200", + dovetail: "0 10 200 200", + gonfalon: "0 10 200 200", + pennant: "0 0 200 200", + + round: "0 0 200 200", + oval: "0 0 200 200", + vesicaPiscis: "0 0 200 200", + square: "0 0 200 200", + diamond: "0 0 200 200", + no: "0 0 200 200", + + fantasy1: "0 0 200 200", + fantasy2: "0 5 200 200", + fantasy3: "0 5 200 200", + fantasy4: "0 5 200 200", + fantasy5: "0 0 200 200", + + noldor: "0 0 200 200", + gondor: "0 5 200 200", + easterling: "0 0 200 200", + erebor: "0 0 200 200", + ironHills: "0 5 200 200", + urukHai: "0 0 200 200", + moriaOrc: "0 0 200 200" + } + + const shieldPaths = { + heater: "m25,25 h150 v50 a150,150,0,0,1,-75,125 a150,150,0,0,1,-75,-125 z", + spanish: "m25,25 h150 v100 a75,75,0,0,1,-150,0 z", + french: "m 25,25 h 150 v 139.15 c 0,41.745 -66,18.15 -75,36.3 -9,-18.15 -75,5.445 -75,-36.3 v 0 z", + horsehead: "m 20,40 c 0,60 40,80 40,100 0,10 -4,15 -0.35,30 C 65,185.7 81,200 100,200 c 19.1,0 35.3,-14.6 40.5,-30.4 C 144.2,155 140,150 140,140 140,120 180,100 180,40 142.72,40 150,15 100,15 55,15 55,40 20,40 Z", + horsehead2: "M60 20c-5 20-10 35-35 55 25 35 35 65 30 100 20 0 35 10 45 26 10-16 30-26 45-26-5-35 5-65 30-100a87 87 0 01-35-55c-25 3-55 3-80 0z", + polish: "m 90.3,6.3 c -12.7,0 -20.7,10.9 -40.5,14 0,11.8 -4.9,23.5 -11.4,31.1 0,0 12.7,6 12.7,19.3 C 51.1,90.8 30,90.8 30,90.8 c 0,0 -3.6,7.4 -3.6,22.4 0,34.3 23.1,60.2 40.7,68.2 17.6,8 27.7,11.4 32.9,18.6 5.2,-7.3 15.3,-10.7 32.8,-18.6 17.6,-8 40.7,-33.9 40.7,-68.2 0,-15 -3.6,-22.4 -3.6,-22.4 0,0 -21.1,0 -21.1,-20.1 0,-13.3 12.7,-19.3 12.7,-19.3 C 155.1,43.7 150.2,32.1 150.2,20.3 130.4,17.2 122.5,6.3 109.7,6.3 102.5,6.3 100,10 100,10 c 0,0 -2.5,-3.7 -9.7,-3.7 z", + hessen: "M170 20c4 5 8 13 15 20 0 0-10 0-10 15 0 100-15 140-75 145-65-5-75-45-75-145 0-15-10-15-10-15l15-20c0 15 10-5 70-5s70 20 70 5z", + swiss: "m 25,20 c -0.1,0 25.2,8.5 37.6,8.5 C 75.1,28.5 99.1,20 100,20 c 0.6,0 24.9,8.5 37.3,8.5 C 149.8,28.5 174.4,20 175,20 l -0.3,22.6 C 173.2,160.3 100,200 100,200 100,200 26.5,160.9 25.2,42.6 Z", + boeotian: "M150 115c-5 0-10-5-10-15s5-15 10-15c10 0 7 10 15 10 10 0 0-30 0-30-10-25-30-55-65-55S45 40 35 65c0 0-10 30 0 30 8 0 5-10 15-10 5 0 10 5 10 15s-5 15-10 15c-10 0-7-10-15-10-10 0 0 30 0 30 10 25 30 55 65 55s55-30 65-55c0 0 10-30 0-30-8 0-5 10-15 10z", + roman: "m 160,170 c -40,20 -80,20 -120,0 V 30 C 80,10 120,10 160,30 Z", + kite: "m 53.3,46.4 c 0,4.1 1,12.3 1,12.3 7.1,55.7 45.7,141.3 45.7,141.3 0,0 38.6,-85.6 45.7,-141.2 0,0 1,-8.1 1,-12.3 C 146.7,20.9 125.8,0.1 100,0.1 74.2,0.1 53.3,20.9 53.3,46.4 Z", + oldFrench: "m25,25 h150 v75 a100,100,0,0,1,-75,100 a100,100,0,0,1,-75,-100 z", + renaissance: "M 25,33.9 C 33.4,50.3 36.2,72.9 36.2,81.7 36.2,109.9 25,122.6 25,141 c 0,29.4 24.9,44.1 40.2,47.7 15.3,3.7 29.3,0 34.8,11.3 5.5,-11.3 19.6,-7.6 34.8,-11.3 C 150.1,185 175,170.3 175,141 c 0,-18.4 -11.2,-31.1 -11.2,-59.3 0,-8.8 2.8,-31.3 11.2,-47.7 L 155.7,14.4 C 138.2,21.8 119.3,25.7 100,25.7 c -19.3,0 -38.2,-3.9 -55.7,-11.3 z", + baroque: "m 100,25 c 18,0 50,2 75,14 v 37 l -2.7,3.2 c -4.9,5.4 -6.6,9.6 -6.7,16.2 0,6.5 2,11.6 6.9,17.2 l 2.8,3.1 v 10.2 c 0,17.7 -2.2,27.7 -7.8,35.9 -5,7.3 -11.7,11.3 -32.3,19.4 -12.6,5 -20.2,8.8 -28.6,14.5 C 103.3,198 100,200 100,200 c 0,0 -2.8,-2.3 -6.4,-4.7 C 85.6,189.8 78,186 65,180.9 32.4,168.1 26.9,160.9 25.8,129.3 L 25,116 l 3.3,-3.3 c 4.8,-5.2 7,-10.7 7,-17.3 0,-6.8 -1.8,-11.1 -6.5,-16.1 L 25,76 V 39 C 50,27 82,25 100,25 Z", + targe: "m 20,35 c 15,0 115,-60 155,-10 -5,10 -15,15 -10,50 5,45 10,70 -10,90 C 125,195 75,195 50,175 25,150 30,130 35,85 50,95 65,85 65,70 65,50 50,45 40,50 30,55 27,65 30,70 23,73 20,70 14,70 11,60 20,45 20,35 Z", + targe2: "m 84,32.2 c 6.2,-1 19.5,-31.4 94.1,-20.2 -30.57,33.64 -21.66,67.37 -11.2,95 20.2,69.5 -41.17549,84.7 -66.88,84.7 C 74.32,191.7071 8.38,168.95 32,105.9 36.88,92.88 31,89 31,82.6 35.15,82.262199 56.79,86.17 56.5,69.8 56.20,52.74 42.2,47.9 25.9,55.2 25.9,51.4 39.8,6.7 84,32.2 Z", + pavise: "M95 7L39.9 37.3a10 10 0 00-5.1 9.5L46 180c.4 5.2 3.7 10 9 10h90c5.3 0 9.6-4.8 10-10l10.6-133.2a10 10 0 00-5-9.5L105 7c-4.2-2.3-6.2-2.3-10 0z", + wedged: "m 51.2,19 h 96.4 c 3.1,12.7 10.7,20.9 26.5,20.8 C 175.7,94.5 165.3,144.3 100,200 43.5,154.2 22.8,102.8 25.1,39.7 37,38.9 47.1,34.7 51.2,19 Z", + round: "m 185,100 a 85,85 0 0 1 -85,85 85,85 0 0 1 -85,-85 85,85 0 0 1 85,-85 85,85 0 0 1 85,85", + oval: "m 32.3,99.5 a 67.7,93.7 0 1 1 0,1.3 z", + vesicaPiscis: "M 100,0 C 63.9,20.4 41,58.5 41,100 c 0,41.5 22.9,79.6 59,100 36.1,-20.4 59,-58.5 59,-100 C 159,58.5 136.1,20.4 100,0 Z", + square: "M 25,25 H 175 V 175 H 25 Z", + diamond: "M 25,100 100,200 175,100 100,0 Z", + no: "m0,0 h200 v200 h-200 z", + flag: "M 10,40 h180 v120 h-180 Z", + pennon: "M 10,40 l190,60 -190,60 Z", + guidon: "M 10,40 h190 l-65,60 65,60 h-190 Z", + banner: "m 25,25 v 170 l 25,-40 25,40 25,-40 25,40 25,-40 25,40 V 25 Z", + dovetail: "m 25,25 v 175 l 75,-40 75,40 V 25 Z", + gonfalon: "m 25,25 v 125 l 75,50 75,-50 V 25 Z", + pennant: "M 25,15 100,200 175,15 Z", + fantasy1: "M 100,5 C 85,30 40,35 15,40 c 40,35 20,90 40,115 15,25 40,30 45,45 5,-15 30,-20 45,-45 20,-25 0,-80 40,-115 C 160,35 115,30 100,5 Z", + fantasy2: "m 152,21 c 0,0 -27,14 -52,-4 C 75,35 48,21 48,21 50,45 30,55 30,75 60,75 60,115 32,120 c 3,40 53,50 68,80 15,-30 65,-40 68,-80 -28,-5 -28,-45 2,-45 C 170,55 150,45 152,21 Z", + fantasy3: "M 167,67 C 165,0 35,0 33,67 c 32,-7 27,53 -3,43 -5,45 60,65 70,90 10,-25 75,-47.51058 70,-90 -30,10 -35,-50 -3,-43 z", + fantasy4: "M100 9C55 48 27 27 13 39c23 50 3 119 49 150 14 9 28 11 38 11s27-4 38-11c55-39 24-108 49-150-14-12-45 7-87-30z", + fantasy5: "M 100,0 C 75,25 30,25 30,25 c 0,69 20,145 70,175 50,-30 71,-106 70,-175 0,0 -45,0 -70,-25 z", + noldor: "m 55,75 h 2 c 3,-25 38,-10 3,20 15,50 30,75 40,105 10,-30 25,-55 40,-105 -35,-30 0,-45 3,-20 h 2 C 150,30 110,20 100,0 90,20 50,30 55,75 Z", + gondor: "m 100,200 c 15,-15 38,-35 45,-60 h 5 V 30 h -5 C 133,10 67,10 55,30 h -5 v 110 h 5 c 7,25 30,45 45,60 z", + easterling: "M 160,185 C 120,170 80,170 40,185 V 15 c 40,15 80,15 120,0 z", + erebor: "M25 135 V60 l22-13 16-37 h75 l15 37 22 13 v75l-22 18-16 37 H63l-16-37z", + ironHills: "m 30,25 60,-10 10,10 10,-10 60,10 -5,125 -65,50 -65,-50 z", + urukHai: "M 30,60 C 40,60 60,50 60,20 l -5,-3 45,-17 75,40 -5,5 -35,155 -5,-35 H 70 v 35 z", + moriaOrc: "M45 35c5 3 7 10 13 9h19c4-2 7-4 9-9 6 1 9 9 16 11 7-2 14 0 21 0 6-3 6-10 10-15 2-5 1-10-2-15-2-4-5-14-4-16 3 6 7 11 12 14 7 3 3 12 7 16 3 6 4 12 9 18 2 4 6 8 5 14 0 6-1 12 3 18-3 6-2 13-1 20 1 6-2 12-1 18 0 6-3 13 0 18 8 4 0 8-5 7-4 3-9 3-13 9-5 5-5 13-8 19 0 6 0 15-7 16-1 6-7 6-10 12-1-6 0-6-2-9l2-19c2-4 5-12-3-12-4-5-11-5-15 1l-13-18c-3-4-2 9-3 12 2 2-4-6-7-5-8-2-8 7-11 11-2 4-5 10-8 9 3-10 3-16 1-23-1-4 2-9-4-11 0-6 1-13-2-19-4-2-9-6-13-7V91c4-7-5-13 0-19-3-7 2-11 2-18-1-6 1-12 3-17v-1z" + } + + const lines = { + straight: "m 0,100 v15 h 200 v -15 z", + engrailed: "m 0,95 a 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 v 20 H 0 Z", + invecked: "M0,102.5 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 v12.5 H0 z", + embattled: "M 0,105 H 2.5 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 2.5 v 10 H 0 Z", + wavy: "m 200,115 v -15 c -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 v 15 z", + raguly: "m 200,95 h -3 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 H 97 l -5,10 H 82 L 87,95 H 77 l -5,10 H 62 L 67,95 H 57 l -5,10 H 42 L 47,95 H 37 l -5,10 H 22 L 27,95 H 17 l -5,10 H 2 L 7,95 H 0 v 20 h 200 z", + dancetty: "m 0,105 10,-15 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 10,15 v 10 H 0 Z", + dentilly: "M 180,105 170,95 v 10 L 160,95 v 10 L 150,95 v 10 L 140,95 v 10 L 130,95 v 10 L 120,95 v 10 L 110,95 v 10 L 100,95 v 10 L 90,95 v 10 L 80,95 v 10 L 70,95 v 10 L 60,95 v 10 L 50,95 v 10 L 40,95 v 10 L 30,95 v 10 L 20,95 v 10 L 10,95 v 10 L 0,95 v 20 H 200 V 105 L 190,95 v 10 L 180,95 Z", + angled: "m 0,95 h 100 v 10 h 100 v 10 H 0 Z", + urdy: "m 200,90 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 L 0,90 v 25 h 200", + indented: "m 100,95 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 v 20 H 0 V 95 l 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 z", + bevilled: "m 0,92.5 h 110 l -20,15 H 200 V 115 H 0 Z", + nowy: "m 0,95 h 80 c 0,0 0.1,20.1 20,20 19.9,-0.1 20,-20 20,-20 h 80 v 20 H 0 Z", + nowyReversed: "m 200,105 h -80 c 0,0 -0.1,-20.1 -20,-20 -19.9,0.1 -20,20 -20,20 H 0 v 10 h 200 z", + potenty: "m 3,95 v 5 h 5 v 5 H 0 v 10 h 200 l 0.5,-10 H 193 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 H 100.5 93 v -5 h 5 V 95 H 83 v 5 h 5 v 5 H 73 v -5 h 5 V 95 H 63 v 5 h 5 v 5 H 53 v -5 h 5 V 95 H 43 v 5 h 5 v 5 H 33 v -5 h 5 V 95 H 23 v 5 h 5 v 5 H 13 v -5 h 5 v -5 z", + potentyDexter: "m 200,105 h -2 v -10 0 0 h -10 v 5 h 5 v 5 H 183 V 95 h -10 v 5 h 5 v 5 H 168 V 95 h -10 v 5 h 5 v 5 H 153 V 95 h -10 v 5 h 5 v 5 H 138 V 95 h -10 v 5 h 5 v 5 H 123 V 95 h -10 v 5 h 5 v 5 h -10 v 0 0 -10 H 98 v 5 h 5 v 5 H 93 V 95 H 83 v 5 h 5 v 5 H 78 V 95 H 68 v 5 h 5 v 5 H 63 V 95 H 53 v 5 h 5 v 5 H 48 V 95 H 38 v 5 h 5 v 5 H 33 V 95 H 23 v 5 h 5 v 5 H 18 V 95 H 8 v 5 h 5 v 5 H 3 V 95 H 0 v 20 h 200 z", + potentySinister: "m 2.5,95 v 10 H 0 v 10 h 202.5 v -15 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 z", + embattledGhibellin: "M 200,200 V 100 l -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 v 15 h 200", + embattledNotched: "m 200,105 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 H 90 V 95 l -5,5 -5,-5 v 10 H 75 V 95 l -5,5 -5,-5 v 10 H 60 V 95 l -5,5 -5,-5 v 10 H 45 V 95 l -5,5 -5,-5 v 10 H 30 V 95 l -5,5 -5,-5 v 10 H 15 V 95 l -5,5 -5,-5 v 10 H 0 v 10 h 200", + embattledGrady: "m 0,95 v 20 H 200 V 95 h -2.5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 z", + dovetailed: "m 200,95 h -7 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 H 93 l 4,10 H 83 L 87,95 H 73 l 4,10 H 63 L 67,95 H 53 l 4,10 H 43 L 47,95 H 33 l 4,10 H 23 L 27,95 H 13 l 4,10 H 3 L 7,95 H 0 v 20 h 200", + dovetailedIndented: "m 200,100 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 v 15 h 200", + nebuly: "m 13.1,89.8 c -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.2,4.5 -7.3,4.5 -0.5,0 -2.2,-0.2 -2.2,-0.2 V 115 h 200 v -10.1 c -3.7,-0.2 -6.7,-2.2 -6.7,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 -1.5,4.1 -4.2,4.4 -8.8,4.5 -4.7,-0.1 -8.7,-1.5 -8.9,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 z", + rayonne: "M0 115l-.1-6 .2.8c1.3-1 2.3-2.5 2.9-4.4.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4A9 9 0 015.5 90c-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 2.1 3.1 3.1 4.6 1 1.6 2.4 3.1 2.7 4.8.3 1.7.3 3.3 0 5.2 1.3-1 2.6-2.7 3.2-4.6.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.75 2.79 2.72 4.08 4.45 5.82L200 115z", + seaWaves: "m 28.83,94.9 c -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.33,-2.03 -2.19,-3.56 -4.45,-3.56 -4.24,0 -6.91,3.13 -8.5,5.13 V 115 h 200 v -14.89 c -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -6.6,3.09 -8.19,5.09 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 z", + dragonTeeth: "M 9.4,85 C 6.5,88.1 4.1,92.9 3,98.8 1.9,104.6 2.3,110.4 3.8,115 2.4,113.5 0,106.6 0,109.3 v 5.7 h 200 v -5.7 c -1.1,-2.4 -2,-5.1 -2.6,-8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -1.4,-1.5 -2.8,-3.9 -3.8,-6.1 -1.1,-2.4 -2.3,-6.1 -2.6,-7.7 -0.2,-5.9 0.2,-11.7 1.7,-16.3 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 C 63,95.4 63.4,89.6 64.9,85 c -2.9,3.1 -5.3,7.9 -6.3,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.6,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 C 18.6,95.4 19,89.6 20.5,85 17.6,88.1 15.2,92.9 14.1,98.8 13,104.6 13.4,110.4 14.9,115 12,111.9 9.6,107.1 8.6,101.2 7.5,95.4 7.9,89.6 9.4,85 Z", + firTrees: "m 3.9,90 -4,7 2,-0.5 L 0,100 v 15 h 200 v -15 l -1.9,-3.5 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 z", + flechy: "m 0,100 h 85 l 15,-15 15,15 h 85 v 15 H 0 Z", + barby: "m 0,100 h 85 l 15,15 15,-15 h 85 v 15 H 0 Z", + enclavy: "M 0,100 H 85 V 85 h 30 v 15 h 85 v 15 H 0 Z", + escartely: "m 0,100 h 85 v 15 h 30 v -15 h 85 v 15 H 0 Z", + arched: "m 100,95 c 40,-0.2 100,20 100,20 H 0 c 0,0 60,-19.8 100,-20 z", + archedReversed: "m 0,85 c 0,0 60,20.2 100,20 40,-0.2 100,-20 100,-20 v 30 H 0 Z" + } + + const templates = { + // straight divisions + perFess: ``, + perPale: ``, + perBend: ``, + perBendSinister: ``, + perChevron: ``, + perChevronReversed: ``, + perCross: ``, + perPile: ``, + perSaltire: ``, + gyronny: ``, + chevronny: ``, + // lined divisions + perFessLined: line => ``, + perPaleLined: line => ``, + perBendLined: line => ``, + perBendSinisterLined: line => ``, + perChevronLined: line => ``, + perChevronReversedLined: line => ``, + perCrossLined: line => ``, + perPileLined: line => ``, + // straight ordinaries + fess: ``, + pale: ``, + bend: ``, + bendSinister: ``, + chief: ``, + bar: ``, + gemelle: ``, + fessCotissed: ``, + fessDoubleCotissed: ``, + bendlet: ``, + bendletSinister: ``, + terrace: ``, + cross: ``, + crossParted: ``, + saltire: ``, + saltireParted: ``, + mount: ``, + point: ``, + flaunches: ``, + gore: ``, + pall: ``, + pallReversed: ``, + chevron: ``, + chevronReversed: ``, + gyron: ``, + quarter: ``, + canton: ``, + pile: ``, + pileInBend: ``, + pileInBendSinister: ``, + piles: ``, + pilesInPoint: ``, + label: ``, + // lined ordinaries + fessLined: line => ``, + paleLined: line => ``, + bendLined: line => ``, + bendSinisterLined: line => ``, + chiefLined: line => ``, + barLined: line => ``, + gemelleLined: line => ``, + fessCotissedLined: line => ``, + fessDoubleCotissedLined: line => ``, + bendletLined: line => ``, + bendletSinisterLined: line => ``, + terraceLined: line => ``, + crossLined: line => ``, + crossPartedLined: line => ``, + saltireLined: line => ``, + saltirePartedLined: line => `` + } + + const patterns = { + semy: (p, c1, c2, size, chargeId) => ``, + vair: (p, c1, c2, size) => ``, + counterVair: (p, c1, c2, size) => ``, + vairInPale: (p, c1, c2, size) => ``, + vairEnPointe: (p, c1, c2, size) => ``, + vairAncien: (p, c1, c2, size) => ``, + potent: (p, c1, c2, size) => ``, + counterPotent: (p, c1, c2, size) => ``, + potentInPale: (p, c1, c2, size) => ``, + potentEnPointe: (p, c1, c2, size) => ``, + ermine: (p, c1, c2, size) => ``, + chequy: (p, c1, c2, size) => ``, + lozengy: (p, c1, c2, size) => ``, + fusily: (p, c1, c2, size) => ``, + pally: (p, c1, c2, size) => ``, + barry: (p, c1, c2, size) => ``, + gemelles: (p, c1, c2, size) => ``, + bendy: (p, c1, c2, size) => ``, + bendySinister: (p, c1, c2, size) => ``, + palyBendy: (p, c1, c2, size) => ``, + barryBendy: (p, c1, c2, size) => ``, + pappellony: (p, c1, c2, size) => ``, + pappellony2: (p, c1, c2, size) => ``, + scaly: (p, c1, c2, size) => ``, + plumetty: (p, c1, c2, size) => ``, + masoned: (p, c1, c2, size) => ``, + fretty: (p, c1, c2, size) => ``, + grillage: (p, c1, c2, size) => ``, + chainy: (p, c1, c2, size) => ``, + maily: (p, c1, c2, size) => ``, + honeycombed: (p, c1, c2, size) => `` + } + + const draw = async function(id, coa) { + const {shield, division, ordinaries = [], charges = []} = coa; + + const ordinariesRegular = ordinaries.filter(o => !o.above); + const ordinariesAboveCharges = ordinaries.filter(o => o.above); + const shieldPath = shieldPaths[shield]; + const tDiv = division ? (division.t.includes("-") ? division.t.split("-")[1] : division.t) : null; + const positions = shieldPositions[shield]; + const sizeModifier = shieldSize[shield] || 1; + const viewBox = shieldBox[shield] || "0 0 200 200"; + + const shieldClip = ``; + const divisionClip = division ? `${getTemplate(division.division, division.line)}` : ""; + const loadedCharges = await getCharges(coa, id, shieldPath); + const loadedPatterns = getPatterns(coa, id); + const blacklight = ``; + const field = ``; + const divisionGroup = division ? templateDivision() : ""; + const overlay = ``; + + const svg= ` + ${shieldClip}${divisionClip}${loadedCharges}${loadedPatterns}${blacklight} + ${field}${divisionGroup}${templateAboveAll()} + ${overlay}`; + + // insert coa svg to defs + document.getElementById("coas").insertAdjacentHTML("beforeend", svg); + return true; + + function templateDivision() { + let svg = ""; + + // In field part + for (const ordinary of ordinariesRegular) { + if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t); + else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv); + } + + for (const charge of charges) { + if (charge.divided === "field") svg += templateCharge(charge, charge.t); + else if (charge.divided === "counter") svg += templateCharge(charge, tDiv); + } + + for (const ordinary of ordinariesAboveCharges) { + if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t); + else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv); + } + + // In division part + svg += ``; + + for (const ordinary of ordinariesRegular) { + if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t); + else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1); + } + + for (const charge of charges) { + if (charge.divided === "division") svg += templateCharge(charge, charge.t); + else if (charge.divided === "counter") svg += templateCharge(charge, coa.t1); + } + + for (const ordinary of ordinariesAboveCharges) { + if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t); + else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1); + } + + return (svg += ``); + } + + function templateAboveAll() { + let svg = ""; + + ordinariesRegular.filter(o => !o.divided) + .forEach(ordinary => { + svg += templateOrdinary(ordinary, ordinary.t); + }); + + charges.filter(o => !o.divided || !division) + .forEach(charge => { + svg += templateCharge(charge, charge.t); + }); + + ordinariesAboveCharges.filter(o => !o.divided) + .forEach(ordinary => { + svg += templateOrdinary(ordinary, ordinary.t); + }); + + return svg; + } + + function templateOrdinary(ordinary, tincture) { + const fill = clr(tincture); + let svg = ``; + if (ordinary.ordinary === "bordure") svg += ``; + else if (ordinary.ordinary === "orle") svg += ``; + else svg += getTemplate(ordinary.ordinary, ordinary.line); + return svg + ``; + } + + function templateCharge(charge, tincture) { + const fill = clr(tincture); + const chargePositions = [...new Set(charge.p)].filter(position => positions[position]); + + let svg = ""; + svg += ``; + for (const p of chargePositions) { + const transform = getElTransform(charge, p); + svg += ``; + } + return svg + ``; + + function getElTransform(c, p) { + const s = (c.size || 1) * sizeModifier; + const sx = c.sinister ? -s : s; + const sy = c.reversed ? -s : s; + let [x, y] = positions[p]; + x = x - 100 * (sx - 1); + y = y - 100 * (sy - 1); + const scale = c.sinister || c.reversed ? `${sx} ${sy}` : s; + return `translate(${x} ${y}) scale(${scale})`; + } + } + } + + async function getCharges(coa, id, shieldPath) { + let charges = coa.charges ? coa.charges.map(charge => charge.charge) : []; // add charges + if (semy(coa.t1)) charges.push(semy(coa.t1)); // add field semy charge + if (semy(coa.division?.t)) charges.push(semy(coa.division.t)); // add division semy charge + + const uniqueCharges = [...new Set(charges)]; + const fetchedCharges = await Promise.all( + uniqueCharges.map(async charge => { + if (charge === "inescutcheon") return ``; + const fetched = await fetchCharge(charge, id); + return fetched; + }) + ); + return fetchedCharges.join(""); + } + + const url = PRODUCTION ? "./charges/" : "http://armoria.herokuapp.com/charges/"; // on local machine fetch files from server + async function fetchCharge(charge, id) { + const fetched = fetch(url + charge + ".svg").then(res => { + if (res.ok) return res.text(); + else throw new Error("Cannot fetch charge"); + }).then(text => { + const html = document.createElement("html"); + html.innerHTML = text; + const g = html.querySelector("g"); + g.setAttribute("id", charge + "_" + id); + return g.outerHTML; + }).catch(err => console.error(err)); + return fetched; + } + + function getPatterns(coa, id) { + const isPattern = string => string.includes("-"); + let patternsToAdd = []; + if (coa.t1.includes("-")) patternsToAdd.push(coa.t1); // add field pattern + if (coa.division && isPattern(coa.division.t)) patternsToAdd.push(coa.division.t); // add division pattern + if (coa.ordinaries) coa.ordinaries.filter(ordinary => isPattern(ordinary.t)).forEach(ordinary => patternsToAdd.push(ordinary.t)); // add ordinaries pattern + if (coa.charges) coa.charges.filter(charge => isPattern(charge.t)).forEach(charge => patternsToAdd.push(charge.t)); // add charges pattern + if (!patternsToAdd.length) return ""; + + return [...new Set(patternsToAdd)].map(patternString => { + const [pattern, t1, t2, size] = patternString.split("-"); + const charge = semy(patternString); + if (charge) return patterns.semy(patternString, clr(t1), clr(t2), getSizeMod(size), charge + "_" + id); + return patterns[pattern](patternString, clr(t1), clr(t2), getSizeMod(size), charge); + }).join(""); + } + + function getSizeMod(size) { + 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(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 + function clr(tincture) { + if (colors[tincture]) return colors[tincture]; + return `url(#${tincture})`; + } + + // get charge is string starts with "semy" + function semy(string) { + const isSemy = /^semy/.test(string); + if (!isSemy) return false; + return string.match(/semy_of_(.*?)-/)[1]; + } + + // render coa if does not exist + const trigger = async function(id, coa) { + if (coa === "custom") { + console.warn("Cannot render custom emblem", coa); + return; + } + if (!coa) { + console.warn(`Emblem ${id} is undefined`); + return; + } + if (!document.getElementById(id)) return draw(id, coa); + } + + const add = function(type, i, coa, x, y) { + const id = type + "COA" + i; + const g = document.getElementById(type+"Emblems"); + + if (emblems.selectAll("use").size()) { + const size = +g.getAttribute("font-size") || 50; + const use = ``; + g.insertAdjacentHTML("beforeend", use); + } + if (layerIsOn("toggleEmblems")) trigger(id, coa); + } + + return {trigger, add, shieldPaths}; + +}))); diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js index 8e05018f..bf9d2620 100644 --- a/modules/cultures-generator.js +++ b/modules/cultures-generator.js @@ -17,7 +17,7 @@ count = Math.floor(populated.length / 50); if (!count) { WARN && console.warn(`There are no populated cells. Cannot generate cultures`); - pack.cultures = [{name:"Wildlands", i:0, base:1}]; + pack.cultures = [{name:"Wildlands", i:0, base:1, shield:"round"}]; alertMessage.innerHTML = ` The climate is harsh and people cannot live in this world.
No cultures, states and burgs will be created.
@@ -41,6 +41,7 @@ const cultures = pack.cultures = getRandomCultures(count); const centers = d3.quadtree(); const colors = getColors(count); + const emblemShape = document.getElementById("emblemShape").value; cultures.forEach(function(c, i) { const cell = c.center = placeCenter(c.sort ? c.sort : (i) => cells.s[i]); @@ -54,6 +55,7 @@ c.origin = 0; c.code = getCode(c.name); cells.culture[cell] = i+1; + if (emblemShape === "random") c.shield = getRandomShield(); }); function placeCenter(v) { @@ -65,10 +67,14 @@ } // the first culture with id 0 is for wildlands - cultures.unshift({name:"Wildlands", i:0, base:1, origin:null}); + cultures.unshift({name:"Wildlands", i:0, base:1, origin:null, shield:"round"}); // make sure all bases exist in nameBases - if (!nameBases.length) {ERROR && console.error("Name base is empty, default nameBases will be applied"); nameBases = Names.getNameBases();} + if (!nameBases.length) { + ERROR && console.error("Name base is empty, default nameBases will be applied"); + nameBases = Names.getNameBases(); + } + cultures.forEach(c => c.base = c.base % nameBases.length); function getRandomCultures(c) { @@ -113,6 +119,7 @@ // assign a unique two-letters code (abbreviation) function getCode(name) { + name = name.replace(/[()]/g, ""); const words = name.split(" "), letters = words.join(""); let code = words.length === 2 ? words[0][0]+words[1][0] : letters.slice(0,2); for (let i=1; i < letters.length-1 && pack.cultures.some(c => c.code === code); i++) { @@ -138,7 +145,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) { @@ -148,164 +161,145 @@ const td = (cell, goal) => {const d = Math.abs(temp[cells.g[cell]] - goal); return d ? d+1 : 1;} // temperature difference fee const bd = (cell, biomes, fee = 4) => biomes.includes(cells.biome[cell]) ? 1 : fee; // biome difference fee const sf = (cell, fee = 4) => cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee; // not on sea coast fee - // https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature if (culturesSet.value === "european") { return [ - {name:"Shwazen", base:0, odd: 1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8])}, - {name:"Angshire", base:1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i)}, - {name:"Luari", base:2, odd: 1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8])}, - {name:"Tallian", base:3, odd: 1, sort: i => n(i) / td(i, 15)}, - {name:"Astellian", base:4, odd: 1, sort: i => n(i) / td(i, 16)}, - {name:"Slovan", base:5, odd: 1, sort: i => n(i) / td(i, 6) * t[i]}, - {name:"Norse", base:6, odd: 1, sort: i => n(i) / td(i, 5)}, - {name:"Elladan", base:7, odd: 1, sort: i => n(i) / td(i, 18) * h[i]}, - {name:"Romian", base:8, odd: .2, sort: i => n(i) / td(i, 15) / t[i]}, - {name:"Soumi", base:9, odd: 1, sort: i => n(i) / td(i, 5) / bd(i, [9]) * t[i]}, - {name:"Portuzian", base:13, odd: 1, sort: i => n(i) / td(i, 17) / sf(i)}, - {name:"Vengrian", base: 15, odd: 1, sort: i => n(i) / td(i, 11) / bd(i, [4]) * t[i]}, - {name:"Turchian", base: 16, odd: .05, sort: i => n(i) / td(i, 14)}, - {name:"Euskati", base: 20, odd: .05, sort: i => n(i) / td(i, 15) * h[i]}, - {name:"Keltan", base: 22, odd: .05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i]} + {name:"Shwazen", base:0, odd: 1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"swiss"}, + {name:"Angshire", base:1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield:"wedged"}, + {name:"Luari", base:2, odd: 1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield:"french"}, + {name:"Tallian", base:3, odd: 1, sort: i => n(i) / td(i, 15), shield:"horsehead"}, + {name:"Astellian", base:4, odd: 1, sort: i => n(i) / td(i, 16), shield:"spanish"}, + {name:"Slovan", base:5, odd: 1, sort: i => n(i) / td(i, 6) * t[i], shield:"polish"}, + {name:"Norse", base:6, odd: 1, sort: i => n(i) / td(i, 5), shield:"heater"}, + {name:"Elladan", base:7, odd: 1, sort: i => n(i) / td(i, 18) * h[i], shield:"boeotian"}, + {name:"Romian", base:8, odd: .2, sort: i => n(i) / td(i, 15) / t[i], shield:"roman"}, + {name:"Soumi", base:9, odd: 1, sort: i => n(i) / td(i, 5) / bd(i, [9]) * t[i], shield:"pavise"}, + {name:"Portuzian", base:13, odd: 1, sort: i => n(i) / td(i, 17) / sf(i), shield:"renaissance"}, + {name:"Vengrian", base: 15, odd: 1, sort: i => n(i) / td(i, 11) / bd(i, [4]) * t[i], shield:"horsehead2"}, + {name:"Turchian", base: 16, odd: .05, sort: i => n(i) / td(i, 14), shield:"round"}, + {name:"Euskati", base: 20, odd: .05, sort: i => n(i) / td(i, 15) * h[i], shield:"oldFrench"}, + {name:"Keltan", base: 22, odd: .05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i], shield:"oval"} ]; } if (culturesSet.value === "oriental") { return [ - {name:"Koryo", base:10, odd: 1, sort: i => n(i) / td(i, 12) / t[i]}, - {name:"Hantzu", base:11, odd: 1, sort: i => n(i) / td(i, 13)}, - {name:"Yamoto", base:12, odd: 1, sort: i => n(i) / td(i, 15) / t[i]}, - {name:"Turchian", base: 16, odd: 1, sort: i => n(i) / td(i, 12)}, - {name:"Berberan", base: 17, odd: .2, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i]}, - {name:"Eurabic", base: 18, odd: 1, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i]}, - {name:"Efratic", base: 23, odd: .1, sort: i => n(i) / td(i, 22) * t[i]}, - {name:"Tehrani", base: 24, odd: 1, sort: i => n(i) / td(i, 18) * h[i]}, - {name:"Maui", base: 25, odd: .2, sort: i => n(i) / td(i, 24) / sf(i) / t[i]}, - {name:"Carnatic", base: 26, odd: .5, sort: i => n(i) / td(i, 26)}, - {name:"Vietic", base: 29, odd: .8, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i]}, - {name:"Guantzu", base:30, odd: .5, sort: i => n(i) / td(i, 17)}, - {name:"Ulus", base:31, odd: 1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i]} + {name:"Koryo", base:10, odd: 1, sort: i => n(i) / td(i, 12) / t[i], shield:"round"}, + {name:"Hantzu", base:11, odd: 1, sort: i => n(i) / td(i, 13), shield:"banner"}, + {name:"Yamoto", base:12, odd: 1, sort: i => n(i) / td(i, 15) / t[i], shield:"round"}, + {name:"Turchian", base: 16, odd: 1, sort: i => n(i) / td(i, 12), shield:"round"}, + {name:"Berberan", base: 17, odd: .2, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i], shield:"oval"}, + {name:"Eurabic", base: 18, odd: 1, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i], shield:"oval"}, + {name:"Efratic", base: 23, odd: .1, sort: i => n(i) / td(i, 22) * t[i], shield:"round"}, + {name:"Tehrani", base: 24, odd: 1, sort: i => n(i) / td(i, 18) * h[i], shield:"round"}, + {name:"Maui", base: 25, odd: .2, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield:"vesicaPiscis"}, + {name:"Carnatic", base: 26, odd: .5, sort: i => n(i) / td(i, 26), shield:"round"}, + {name:"Vietic", base: 29, odd: .8, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield:"banner"}, + {name:"Guantzu", base:30, odd: .5, sort: i => n(i) / td(i, 17), shield:"banner"}, + {name:"Ulus", base:31, odd: 1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"banner"} ]; } if (culturesSet.value === "english") { const getName = () => Names.getBase(1, 5, 9, "", 0); return [ - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1}, - {name:getName(), base:1, odd: 1} + {name:getName(), base:1, odd: 1, shield:"heater"}, + {name:getName(), base:1, odd: 1, shield:"wedged"}, + {name:getName(), base:1, odd: 1, shield:"swiss"}, + {name:getName(), base:1, odd: 1, shield:"oldFrench"}, + {name:getName(), base:1, odd: 1, shield:"swiss"}, + {name:getName(), base:1, odd: 1, shield:"spanish"}, + {name:getName(), base:1, odd: 1, shield:"hessen"}, + {name:getName(), base:1, odd: 1, shield:"fantasy5"}, + {name:getName(), base:1, odd: 1, shield:"fantasy4"}, + {name:getName(), base:1, odd: 1, shield:"fantasy1"} ]; } if (culturesSet.value === "antique") { return [ - {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 14) / t[i]}, // Roman - {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 15) / sf(i)}, // Roman - {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 16) / sf(i)}, // Roman - {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 17) / t[i]}, // Roman - {name:"Hellenic", base:7, odd: 1, sort: i => n(i) / td(i, 18) / sf(i) * h[i]}, // Greek - {name:"Hellenic", base:7, odd: 1, sort: i => n(i) / td(i, 19) / sf(i) * h[i]}, // Greek - {name:"Macedonian", base:7, odd: .5, sort: i => n(i) / td(i, 12) * h[i]}, // Greek - {name:"Celtic", base:22, odd: 1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8])}, - {name:"Germanic", base:0, odd: 1, sort: i => n(i) / td(i, 10) ** .5 / bd(i, [6, 8])}, - {name:"Persian", base:24, odd: .8, sort: i => n(i) / td(i, 18) * h[i]}, // Iranian - {name:"Scythian", base:24, odd: .5, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [4])}, // Iranian - {name:"Cantabrian", base: 20, odd: .5, sort: i => n(i) / td(i, 16) * h[i]}, // Basque - {name:"Estian", base: 9, odd: .2, sort: i => n(i) / td(i, 5) * t[i]}, // Finnic - {name:"Carthaginian", base: 17, odd: .3, sort: i => n(i) / td(i, 19) / sf(i)}, // Berber - {name:"Mesopotamian", base: 23, odd: .2, sort: i => n(i) / td(i, 22) / bd(i, [1, 2, 3])} // Mesopotamian + {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 14) / t[i], shield:"roman"}, // Roman + {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 15) / sf(i), shield:"roman"}, // Roman + {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 16) / sf(i), shield:"roman"}, // Roman + {name:"Roman", base:8, odd: 1, sort: i => n(i) / td(i, 17) / t[i], shield:"roman"}, // Roman + {name:"Hellenic", base:7, odd: 1, sort: i => n(i) / td(i, 18) / sf(i) * h[i], shield:"boeotian"}, // Greek + {name:"Hellenic", base:7, odd: 1, sort: i => n(i) / td(i, 19) / sf(i) * h[i], shield:"boeotian"}, // Greek + {name:"Macedonian", base:7, odd: .5, sort: i => n(i) / td(i, 12) * h[i], shield:"round"}, // Greek + {name:"Celtic", base:22, odd: 1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8]), shield:"round"}, + {name:"Germanic", base:0, odd: 1, sort: i => n(i) / td(i, 10) ** .5 / bd(i, [6, 8]), shield:"round"}, + {name:"Persian", base:24, odd: .8, sort: i => n(i) / td(i, 18) * h[i], shield:"oval"}, // Iranian + {name:"Scythian", base:24, odd: .5, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [4]), shield:"round"}, // Iranian + {name:"Cantabrian", base: 20, odd: .5, sort: i => n(i) / td(i, 16) * h[i], shield:"oval"}, // Basque + {name:"Estian", base: 9, odd: .2, sort: i => n(i) / td(i, 5) * t[i], shield:"pavise"}, // Finnic + {name:"Carthaginian", base: 17, odd: .3, sort: i => n(i) / td(i, 19) / sf(i), shield:"oval"}, // Berber + {name:"Mesopotamian", base: 23, odd: .2, sort: i => n(i) / td(i, 22) / bd(i, [1, 2, 3]), shield:"oval"} // Mesopotamian ]; } if (culturesSet.value === "highFantasy") { return [ // fantasy races - {name:"Quenian", base: 33, odd: 1, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i]}, // Elves - {name:"Eldar", base: 33, odd: 1, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i]}, // Elves - {name:"Lorian", base: 33, odd: .5, sort: i => n(i) / bd(i, [6,7,8,9], 10)}, // Elves - {name:"Trow", base: 34, odd: .9, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i]}, // Dark Elves - {name:"Dokalfar", base: 34, odd: .3, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i]}, // Dark Elves - {name:"Durinn", base: 35, odd: 1, sort: i => n(i) + h[i]}, // Dwarven - {name:"Khazadur", base: 35, odd: 1, sort: i => n(i) + h[i]}, // Dwarven - {name:"Kobblin", base: 36, odd: 1, sort: i => t[i] - s[i]}, // Goblin - {name:"Uruk", base: 37, odd: 1, sort: i => h[i] * t[i]}, // Orc - {name:"Ugluk", base: 37, odd: .7, sort: i => h[i] * t[i] / bd(i, [1,2,10,11])}, // Orc - {name:"Yotunn", base: 38, odd: .9, sort: i => td(i, -10)}, // Giant - {name:"Drake", base: 39, odd: .7, sort: i => -s[i]}, // Draconic - {name:"Rakhnid", base: 40, odd: .9, sort: i => t[i] - s[i]}, // Arachnid - {name:"Aj'Snaga", base: 41, odd: .9, sort: i => n(i) / bd(i, [12], 10)}, // Serpents - // common fantasy human - {name:"Gozdor", base:32, odd: 1, sort: i => n(i) / td(i, 18)}, - {name:"Anor", base:32, odd: 1, sort: i => n(i) / td(i, 10)}, - {name:"Dail", base:32, odd: 1, sort: i => n(i) / td(i, 13)}, - {name:"Duland", base:32, odd: 1, sort: i => n(i) / td(i, 14)}, - {name:"Rohand", base:32, odd: 1, sort: i => n(i) / td(i, 16)}, - // rare real-world western - {name:"Norse", base:6, odd: .5, sort: i => n(i) / td(i, 5) / sf(i)}, - {name:"Izenlute", base:0, odd: .1, sort: i => n(i) / td(i, 5)}, - {name:"Lurian", base:2, odd: .1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8])}, - {name:"Getalian", base:3, odd: .1, sort: i => n(i) / td(i, 15)}, - {name:"Astelan", base:4, odd: .05, sort: i => n(i) / td(i, 16)}, - // rare real-world exotic - {name:"Yoruba", base:21, odd: .05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7])}, - {name:"Ryoko", base:10, odd: .05, sort: i => n(i) / td(i, 12) / t[i]}, - {name:"Toyamo", base:12, odd: .05, sort: i => n(i) / td(i, 15) / t[i]}, - {name:"Guan-Tsu", base:30, odd: .05, sort: i => n(i) / td(i, 17)}, - {name:"Ulus-Khan", base:31, odd: .05, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i]}, - {name:"Turan", base: 16, odd: .05, sort: i => n(i) / td(i, 13)}, - {name:"Al'Uma", base: 18, odd: .05, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i]}, - {name:"Druidas", base: 22, odd: .05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i]}, - {name:"Gorodian", base:5, odd: .05, sort: i => n(i) / td(i, 6) * t[i]} + {name:"Quenian (Elfish)", base: 33, odd: 1, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i], shield:"gondor"}, // Elves + {name:"Eldar (Elfish)", base: 33, odd: 1, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i], shield:"noldor"}, // Elves + {name:"Trow (Dark Elfish)", base: 34, odd: .9, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"hessen"}, // Dark Elves + {name:"Lothian (Dark Elfish)", base: 34, odd: .3, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"wedged"}, // Dark Elves + {name:"Dunirr (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield:"ironHills"}, // Dwarfs + {name:"Khazadur (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield:"erebor"}, // Dwarfs + {name:"Kobold (Goblin)", base: 36, odd: 1, sort: i => t[i] - s[i], shield:"moriaOrc"}, // Goblin + {name:"Uruk (Orkish)", base: 37, odd: 1, sort: i => h[i] * t[i], shield:"urukHai"}, // Orc + {name:"Ugluk (Orkish)", base: 37, odd: .5, sort: i => h[i] * t[i] / bd(i, [1,2,10,11]), shield:"moriaOrc"}, // Orc + {name:"Yotunn (Giants)", base: 38, odd: .7, sort: i => td(i, -10), shield:"pavise"}, // Giant + {name:"Rake (Drakonic)", base: 39, odd: .7, sort: i => -s[i], shield:"fantasy2"}, // Draconic + {name:"Arago (Arachnid)", base: 40, odd: .7, sort: i => t[i] - s[i], shield:"horsehead2"}, // Arachnid + {name:"Aj'Snaga (Serpents)", base: 41, odd: .7, sort: i => n(i) / bd(i, [12], 10), shield:"fantasy1"}, // Serpents + // fantasy human + {name:"Anor (Human)", base:32, odd: 1, sort: i => n(i) / td(i, 10), shield:"fantasy5"}, + {name:"Dail (Human)", base:32, odd: 1, sort: i => n(i) / td(i, 13), shield:"roman"}, + {name:"Rohand (Human)", base:16, odd: 1, sort: i => n(i) / td(i, 16), shield:"round"}, + {name:"Dulandir (Human)", base:31, odd: 1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"easterling"}, ]; } if (culturesSet.value === "darkFantasy") { return [ // common real-world English - {name:"Angshire", base:1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i)}, - {name:"Enlandic", base:1, odd: 1, sort: i => n(i) / td(i, 12)}, - {name:"Westen", base:1, odd: 1, sort: i => n(i) / td(i, 10)}, - {name:"Nortumbic", base:1, odd: 1, sort: i => n(i) / td(i, 7)}, - {name:"Mercian", base:1, odd: 1, sort: i => n(i) / td(i, 9)}, - {name:"Kentian", base:1, odd: 1, sort: i => n(i) / td(i, 12)}, + {name:"Angshire", base:1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield:"heater"}, + {name:"Enlandic", base:1, odd: 1, sort: i => n(i) / td(i, 12), shield:"heater"}, + {name:"Westen", base:1, odd: 1, sort: i => n(i) / td(i, 10), shield:"heater"}, + {name:"Nortumbic", base:1, odd: 1, sort: i => n(i) / td(i, 7), shield:"heater"}, + {name:"Mercian", base:1, odd: 1, sort: i => n(i) / td(i, 9), shield:"heater"}, + {name:"Kentian", base:1, odd: 1, sort: i => n(i) / td(i, 12), shield:"heater"}, // rare real-world western - {name:"Norse", base:6, odd: .7, sort: i => n(i) / td(i, 5) / sf(i)}, - {name:"Schwarzen", base:0, odd: .3, sort: i => n(i) / td(i, 10) / bd(i, [6, 8])}, - {name:"Luarian", base:2, odd: .3, sort: i => n(i) / td(i, 12) / bd(i, [6, 8])}, - {name:"Hetallian", base:3, odd: .3, sort: i => n(i) / td(i, 15)}, - {name:"Astellian", base:4, odd: .3, sort: i => n(i) / td(i, 16)}, + {name:"Norse", base:6, odd: .7, sort: i => n(i) / td(i, 5) / sf(i), shield:"oldFrench"}, + {name:"Schwarzen", base:0, odd: .3, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"gonfalon"}, + {name:"Luarian", base:2, odd: .3, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield:"oldFrench"}, + {name:"Hetallian", base:3, odd: .3, sort: i => n(i) / td(i, 15), shield:"oval"}, + {name:"Astellian", base:4, odd: .3, sort: i => n(i) / td(i, 16), shield:"spanish"}, // rare real-world exotic - {name:"Kiswaili", base:28, odd: .05, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7])}, - {name:"Yoruba", base:21, odd: .05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7])}, - {name:"Koryo", base:10, odd: .05, sort: i => n(i) / td(i, 12) / t[i]}, - {name:"Hantzu", base:11, odd: .05, sort: i => n(i) / td(i, 13)}, - {name:"Yamoto", base:12, odd: .05, sort: i => n(i) / td(i, 15) / t[i]}, - {name:"Guantzu", base:30, odd: .05, sort: i => n(i) / td(i, 17)}, - {name:"Ulus", base:31, odd: .05, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i]}, - {name:"Turan", base: 16, odd: .05, sort: i => n(i) / td(i, 12)}, - {name:"Berberan", base: 17, odd: .05, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i]}, - {name:"Eurabic", base: 18, odd: .05, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i]}, - {name:"Slovan", base:5, odd: .05, sort: i => n(i) / td(i, 6) * t[i]}, - {name:"Keltan", base: 22, odd: .1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8])}, - {name:"Elladan", base:7, odd: .2, sort: i => n(i) / td(i, 18) / sf(i) * h[i]}, - {name:"Romian", base:8, odd: .2, sort: i => n(i) / td(i, 14) / t[i]}, + {name:"Kiswaili", base:28, odd: .05, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield:"vesicaPiscis"}, + {name:"Yoruba", base:21, odd: .05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield:"vesicaPiscis"}, + {name:"Koryo", base:10, odd: .05, sort: i => n(i) / td(i, 12) / t[i], shield:"round"}, + {name:"Hantzu", base:11, odd: .05, sort: i => n(i) / td(i, 13), shield:"banner"}, + {name:"Yamoto", base:12, odd: .05, sort: i => n(i) / td(i, 15) / t[i], shield:"round"}, + {name:"Guantzu", base:30, odd: .05, sort: i => n(i) / td(i, 17), shield:"banner"}, + {name:"Ulus", base:31, odd: .05, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"banner"}, + {name:"Turan", base: 16, odd: .05, sort: i => n(i) / td(i, 12), shield:"round"}, + {name:"Berberan", base: 17, odd: .05, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i], shield:"round"}, + {name:"Eurabic", base: 18, odd: .05, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i], shield:"round"}, + {name:"Slovan", base:5, odd: .05, sort: i => n(i) / td(i, 6) * t[i], shield:"round"}, + {name:"Keltan", base: 22, odd: .1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8]), shield:"vesicaPiscis"}, + {name:"Elladan", base:7, odd: .2, sort: i => n(i) / td(i, 18) / sf(i) * h[i], shield:"boeotian"}, + {name:"Romian", base:8, odd: .2, sort: i => n(i) / td(i, 14) / t[i], shield:"roman"}, // fantasy races - {name:"Eldar", base: 33, odd: .5, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i]}, // Elves - {name:"Trow", base: 34, odd: .8, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i]}, // Dark Elves - {name:"Durinn", base: 35, odd: .8, sort: i => n(i) + h[i]}, // Dwarven - {name:"Kobblin", base: 36, odd: .8, sort: i => t[i] - s[i]}, // Goblin - {name:"Uruk", base: 37, odd: .8, sort: i => h[i] * t[i] / bd(i, [1,2,10,11])}, // Orc - {name:"Yotunn", base: 38, odd: .8, sort: i => td(i, -10)}, // Giant - {name:"Drake", base: 39, odd: .9, sort: i => -s[i]}, // Draconic - {name:"Rakhnid", base: 40, odd: .9, sort: i => t[i] - s[i]}, // Arachnid - {name:"Aj'Snaga", base: 41, odd: .9, sort: i => n(i) / bd(i, [12], 10)}, // Serpents + {name:"Eldar", base: 33, odd: .5, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i], shield:"fantasy5"}, // Elves + {name:"Trow", base: 34, odd: .8, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"hessen"}, // Dark Elves + {name:"Durinn", base: 35, odd: .8, sort: i => n(i) + h[i], shield:"erebor"}, // Dwarven + {name:"Kobblin", base: 36, odd: .8, sort: i => t[i] - s[i], shield:"moriaOrc"}, // Goblin + {name:"Uruk", base: 37, odd: .8, sort: i => h[i] * t[i] / bd(i, [1,2,10,11]), shield:"urukHai"}, // Orc + {name:"Yotunn", base: 38, odd: .8, sort: i => td(i, -10), shield:"pavise"}, // Giant + {name:"Drake", base: 39, odd: .9, sort: i => -s[i], shield:"fantasy2"}, // Draconic + {name:"Rakhnid", base: 40, odd: .9, sort: i => t[i] - s[i], shield:"horsehead2"}, // Arachnid + {name:"Aj'Snaga", base: 41, odd: .9, sort: i => n(i) / bd(i, [12], 10), shield:"fantasy1"}, // Serpents ] } @@ -318,38 +312,38 @@ // all-world return [ - {name:"Shwazen", base:0, odd: .7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8])}, - {name:"Angshire", base:1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i)}, - {name:"Luari", base:2, odd: .6, sort: i => n(i) / td(i, 12) / bd(i, [6, 8])}, - {name:"Tallian", base:3, odd: .6, sort: i => n(i) / td(i, 15)}, - {name:"Astellian", base:4, odd: .6, sort: i => n(i) / td(i, 16)}, - {name:"Slovan", base:5, odd: .7, sort: i => n(i) / td(i, 6) * t[i]}, - {name:"Norse", base:6, odd: .7, sort: i => n(i) / td(i, 5)}, - {name:"Elladan", base:7, odd: .7, sort: i => n(i) / td(i, 18) * h[i]}, - {name:"Romian", base:8, odd: .7, sort: i => n(i) / td(i, 15)}, - {name:"Soumi", base:9, odd: .3, sort: i => n(i) / td(i, 5) / bd(i, [9]) * t[i]}, - {name:"Koryo", base:10, odd: .1, sort: i => n(i) / td(i, 12) / t[i]}, - {name:"Hantzu", base:11, odd: .1, sort: i => n(i) / td(i, 13)}, - {name:"Yamoto", base:12, odd: .1, sort: i => n(i) / td(i, 15) / t[i]}, - {name:"Portuzian", base:13, odd: .4, sort: i => n(i) / td(i, 17) / sf(i)}, - {name:"Nawatli", base:14, odd: .1, sort: i => h[i] / td(i, 18) / bd(i, [7])}, - {name:"Vengrian", base: 15, odd: .2, sort: i => n(i) / td(i, 11) / bd(i, [4]) * t[i]}, - {name:"Turchian", base: 16, odd: .2, sort: i => n(i) / td(i, 13)}, - {name:"Berberan", base: 17, odd: .1, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i]}, - {name:"Eurabic", base: 18, odd: .2, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i]}, - {name:"Inuk", base: 19, odd: .05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i)}, - {name:"Euskati", base: 20, odd: .05, sort: i => n(i) / td(i, 15) * h[i]}, - {name:"Yoruba", base: 21, odd: .05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7])}, - {name:"Keltan", base: 22, odd: .05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i]}, - {name:"Efratic", base: 23, odd: .05, sort: i => n(i) / td(i, 22) * t[i]}, - {name:"Tehrani", base: 24, odd: .1, sort: i => n(i) / td(i, 18) * h[i]}, - {name:"Maui", base: 25, odd: .05, sort: i => n(i) / td(i, 24) / sf(i) / t[i]}, - {name:"Carnatic", base: 26, odd: .05, sort: i => n(i) / td(i, 26)}, - {name:"Inqan", base: 27, odd: .05, sort: i => h[i] / td(i, 13)}, - {name:"Kiswaili", base: 28, odd: .1, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7])}, - {name:"Vietic", base: 29, odd: .1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i]}, - {name:"Guantzu", base:30, odd: .1, sort: i => n(i) / td(i, 17)}, - {name:"Ulus", base:31, odd: .1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i]} + {name:"Shwazen", base:0, odd: .7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"hessen"}, + {name:"Angshire", base:1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield:"heater"}, + {name:"Luari", base:2, odd: .6, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield:"oldFrench"}, + {name:"Tallian", base:3, odd: .6, sort: i => n(i) / td(i, 15), shield:"horsehead2"}, + {name:"Astellian", base:4, odd: .6, sort: i => n(i) / td(i, 16), shield:"spanish"}, + {name:"Slovan", base:5, odd: .7, sort: i => n(i) / td(i, 6) * t[i], shield:"round"}, + {name:"Norse", base:6, odd: .7, sort: i => n(i) / td(i, 5), shield:"heater"}, + {name:"Elladan", base:7, odd: .7, sort: i => n(i) / td(i, 18) * h[i], shield:"boeotian"}, + {name:"Romian", base:8, odd: .7, sort: i => n(i) / td(i, 15), shield:"roman"}, + {name:"Soumi", base:9, odd: .3, sort: i => n(i) / td(i, 5) / bd(i, [9]) * t[i], shield:"pavise"}, + {name:"Koryo", base:10, odd: .1, sort: i => n(i) / td(i, 12) / t[i], shield:"round"}, + {name:"Hantzu", base:11, odd: .1, sort: i => n(i) / td(i, 13), shield:"banner"}, + {name:"Yamoto", base:12, odd: .1, sort: i => n(i) / td(i, 15) / t[i], shield:"round"}, + {name:"Portuzian", base:13, odd: .4, sort: i => n(i) / td(i, 17) / sf(i), shield:"spanish"}, + {name:"Nawatli", base:14, odd: .1, sort: i => h[i] / td(i, 18) / bd(i, [7]), shield:"square"}, + {name:"Vengrian", base: 15, odd: .2, sort: i => n(i) / td(i, 11) / bd(i, [4]) * t[i], shield:"wedged"}, + {name:"Turchian", base: 16, odd: .2, sort: i => n(i) / td(i, 13), shield:"round"}, + {name:"Berberan", base: 17, odd: .1, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i], shield:"round"}, + {name:"Eurabic", base: 18, odd: .2, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i], shield:"round"}, + {name:"Inuk", base: 19, odd: .05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield:"square"}, + {name:"Euskati", base: 20, odd: .05, sort: i => n(i) / td(i, 15) * h[i], shield:"spanish"}, + {name:"Yoruba", base: 21, odd: .05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield:"vesicaPiscis"}, + {name:"Keltan", base: 22, odd: .05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i], shield:"vesicaPiscis"}, + {name:"Efratic", base: 23, odd: .05, sort: i => n(i) / td(i, 22) * t[i], shield:"diamond"}, + {name:"Tehrani", base: 24, odd: .1, sort: i => n(i) / td(i, 18) * h[i], shield:"round"}, + {name:"Maui", base: 25, odd: .05, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield:"round"}, + {name:"Carnatic", base: 26, odd: .05, sort: i => n(i) / td(i, 26), shield:"round"}, + {name:"Inqan", base: 27, odd: .05, sort: i => h[i] / td(i, 13), shield:"square"}, + {name:"Kiswaili", base: 28, odd: .1, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield:"vesicaPiscis"}, + {name:"Vietic", base: 29, odd: .1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield:"banner"}, + {name:"Guantzu", base:30, odd: .1, sort: i => n(i) / td(i, 17), shield:"banner"}, + {name:"Ulus", base:31, odd: .1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"banner"} ]; } @@ -384,14 +378,10 @@ if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell cost[e] = totalCost; queue.queue({e, p:totalCost, c}); - - //debug.append("text").attr("x", (cells.p[n][0]+cells.p[e][0])/2 - 1).attr("y", (cells.p[n][1]+cells.p[e][1])/2 - 1).text(rn(totalCost-p)).attr("font-size", .8); - //const points = [cells.p[n][0], cells.p[n][1], (cells.p[n][0]+cells.p[e][0])/2, (cells.p[n][1]+cells.p[e][1])/2, cells.p[e][0], cells.p[e][1]]; - //debug.append("polyline").attr("points", points.toString()).attr("marker-mid", "url(#arrow)").attr("opacity", .6); } }); } - //debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2); + TIME && console.timeEnd('expandCultures'); } @@ -429,6 +419,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}; }))); diff --git a/modules/military-generator.js b/modules/military-generator.js index 4dd19d3f..2102e2b9 100644 --- a/modules/military-generator.js +++ b/modules/military-generator.js @@ -52,7 +52,7 @@ for (const unit of options.military) { if (!stateModifier[unit.type]) continue; let modifier = stateModifier[unit.type][s.type] || 1; - if (unit.type === "mounted" && s.form === "Horde") modifier *= 2; else + if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2; else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2; temp[unit.name] = modifier * s.alert; } diff --git a/modules/relief-icons.js b/modules/relief-icons.js index ab1916ce..ab6d5490 100644 --- a/modules/relief-icons.js +++ b/modules/relief-icons.js @@ -35,7 +35,7 @@ let h = rn((4 + Math.random()) * size, 2); const icon = getBiomeIcon(i, biomesData.icons[b]); if (icon === "#relief-grass-1") h *= 1.3; - relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2}); + relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: rn(h*2, 2)}); } } @@ -45,7 +45,7 @@ for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) { if (!d3.polygonContains(polygon, [cx, cy])) continue; - relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2}); + relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: rn(h*2, 2)}); } } @@ -64,7 +64,7 @@ void function renderRelief() { let reliefHTML = ""; for (const r of relief) { - reliefHTML += ``; + reliefHTML += ``; } terrain.html(reliefHTML); }() diff --git a/modules/river-generator.js b/modules/river-generator.js index 54fc1b3a..2e6c5022 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -1,376 +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.seedrandom(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])]; } - }() - - // 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 - }); - - 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 { - 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; - } - } - 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]); - } - - // 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); - } - } - - // 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]); - }() - - // 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 - 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]]; + outlets[l.i] = outlet; + delete l.shoreline // cleanup temp data once used }); - 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; + 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 { + 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 } - } - 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; + } 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 } } - return depressed; + 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; + } + } + 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]); + } + + // 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); + } + } + + // 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]); + }() + + // 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 + 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; + } + } } - // 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 + return depressed; +} - 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]); +// 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 - if (s+1 === segments.length) break; // do not enhance last segment + 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]); - 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]); - } + 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]); } - return riverEnhanced; + } + 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; +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]); + // 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); - 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]); - } - - // 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); + // 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.seedrandom(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}; - -}))); +}))); \ No newline at end of file diff --git a/modules/save-and-load.js b/modules/save-and-load.js index 86d7e6db..f725a6e8 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -8,7 +8,6 @@ async function saveSVG() { const link = document.createElement("a"); link.download = getFileName() + ".svg"; link.href = url; - document.body.appendChild(link); link.click(); tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000); @@ -33,7 +32,6 @@ async function savePNG() { link.download = getFileName() + ".png"; canvas.toBlob(function(blob) { link.href = window.URL.createObjectURL(blob); - document.body.appendChild(link); link.click(); window.setTimeout(function() { canvas.remove(); @@ -64,7 +62,6 @@ async function saveJPEG() { const link = document.createElement("a"); link.download = getFileName() + ".jpeg"; link.href = URL; - document.body.appendChild(link); link.click(); tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000); @@ -81,6 +78,9 @@ async function getMapURL(type, subtype) { const clone = d3.select(cloneEl); clone.select("#debug").remove(); + const cloneDefs = cloneEl.getElementsByTagName("defs")[0]; + const svgDefs = document.getElementById("defElements"); + const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove(); if (subtype === "globe") clone.select("#scaleBar").remove(); @@ -94,14 +94,89 @@ async function getMapURL(type, subtype) { if (customization && type === "mesh") updateMeshCells(clone); inlineStyle(clone); - const fontStyle = await GFontToDataURI(getFontsToLoad()); // load non-standard fonts - if (fontStyle) clone.select("defs").append("style").text(fontStyle.join('\n')); // add font to style + // remove unused filters + const filters = cloneEl.querySelectorAll("filter"); + for (let i=0; i < filters.length; i++) { + const id = filters[i].id; + if (cloneEl.querySelector("[filter='url(#"+id+")']")) continue; + if (cloneEl.getAttribute("filter") === "url(#"+id+")") continue; + filters[i].remove(); + } - clone.append("metadata").text("image/svg+xml"); - const serialized = (new XMLSerializer()).serializeToString(clone.node()); - const svg_xml = `` + serialized; + // remove unused patterns + const patterns = cloneEl.querySelectorAll("pattern"); + for (let i=0; i < patterns.length; i++) { + const id = patterns[i].id; + if (cloneEl.querySelector("[fill='url(#"+id+")']")) continue; + patterns[i].remove(); + } + + // remove unused symbols + const symbols = cloneEl.querySelectorAll("symbol"); + for (let i=0; i < symbols.length; i++) { + const id = symbols[i].id; + if (cloneEl.querySelector("use[href='#"+id+"']")) continue; + symbols[i].remove(); + } + + // add displayed emblems + if (layerIsOn("toggleEmblems")) { + Array.from(cloneEl.getElementById("emblems").querySelectorAll("use")).forEach(el => { + const href = el.getAttribute("href"); + if (!href) return; + const emblem = document.getElementById(href.slice(1)).cloneNode(true); // clone emblem + cloneDefs.append(emblem); + }); + } + + // add ocean pattern + if (cloneEl.getElementById("oceanicPattern")) { + const patternId = cloneEl.getElementById("oceanicPattern").getAttribute("filter").slice(5,-1); + const pattern = svgDefs.getElementById(patternId); + if (patternId) cloneDefs.appendChild(pattern.cloneNode(true)); + } + + // add relief icons + if (cloneEl.getElementById("terrain")) { + const uniqueElements = new Set(); + const terrainElements = cloneEl.getElementById("terrain").childNodes; + for (let i=0; i < terrainElements.length; i++) { + uniqueElements.add(terrainElements[i].getAttribute("href")); + } + + const defsRelief = svgDefs.getElementById("defs-relief"); + for (const terrain of [...uniqueElements]) { + const element = defsRelief.querySelector(terrain); + if (element) cloneDefs.appendChild(element.cloneNode(true)); + } + } + + // add wind rose + if (cloneEl.getElementById("compass")) { + const rose = svgDefs.getElementById("rose"); + if (rose) cloneDefs.appendChild(rose.cloneNode(true)); + } + + // add port icon + if (cloneEl.getElementById("anchors")) { + const anchor = svgDefs.getElementById("icon-anchor"); + if (anchor) cloneDefs.appendChild(anchor.cloneNode(true)); + } + + if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching").remove(); //remove unused hatching group + if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog").remove(); //remove unused fog + if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths").remove(); // removed unused statePaths + if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths").remove(); // removed unused textPaths + + // add armies style + if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", ""); + + const fontStyle = await GFontToDataURI(getFontsToLoad(clone)); // load non-standard fonts + if (fontStyle) clone.select("defs").append("style").text(fontStyle.join('\n')); // add font to style clone.remove(); - const blob = new Blob([svg_xml], {type: 'image/svg+xml;charset=utf-8'}); + + const serialized = `` + (new XMLSerializer()).serializeToString(cloneEl); + const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'}); const url = window.URL.createObjectURL(blob); window.setTimeout(() => window.URL.revokeObjectURL(url), 5000); return url; @@ -115,7 +190,7 @@ function removeUnusedElements(clone) { for (let empty = 1; empty;) { empty = 0; clone.selectAll("g").each(function() { - if (!this.hasChildNodes() || this.style.display === "none") {empty++; this.remove();} + if (!this.hasChildNodes() || this.style.display === "none" || this.classList.contains("hidden")) {empty++; this.remove();} if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display"); }); } @@ -170,11 +245,11 @@ function inlineStyle(clone) { } // get non-standard fonts used for labels to fetch them from web -function getFontsToLoad() { +function getFontsToLoad(clone) { const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; // fonts to not fetch const fontsInUse = new Set(); // to store fonts currently in use - labels.selectAll("g").each(function() { + clone.selectAll("#labels > g").each(function() { if (!this.hasChildNodes()) return; const font = this.dataset.font; if (!font || webSafe.includes(font)) return; @@ -246,12 +321,14 @@ function getMapData() { const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|"); const notesData = JSON.stringify(notes); - const cloneEl = document.getElementById("map").cloneNode(true); // clone svg + // clone svg + const cloneEl = document.getElementById("map").cloneNode(true); // set transform values to default cloneEl.setAttribute("width", graphWidth); cloneEl.setAttribute("height", graphHeight); cloneEl.querySelector("#viewbox").removeAttribute("transform"); + const svg_xml = (new XMLSerializer()).serializeToString(cloneEl); const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features}); @@ -298,7 +375,6 @@ async function saveMap() { const link = document.createElement("a"); link.download = getFileName() + ".map"; link.href = URL; - document.body.appendChild(link); link.click(); tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); window.URL.revokeObjectURL(URL); @@ -423,7 +499,7 @@ async function quickSave() { if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;} const blob = await getMapData(); if (blob) ldb.set("lastMap", blob); // auto-save map - tip("Map is saved to browser memory", true, "success", 2000); + tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000); } function quickLoad() { @@ -501,6 +577,8 @@ function uploadMap(file, callback) { const fileReader = new FileReader(); fileReader.onload = function(fileLoadedEvent) { if (callback) callback(); + document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems + const dataLoaded = fileLoadedEvent.target.result; const data = dataLoaded.split("\r\n"); @@ -519,7 +597,7 @@ function uploadMap(file, callback) { } else { load = true; message = `The map version (${mapVersion}) does not match the Generator version (${version}). -
Click OK to get map auto-updated. In case of issues please keep using an ${archive} of the Generator`; +
Click OK to get map auto-updated. In case of issues please keep using an ${archive} of the Generator`; } alertMessage.innerHTML = message; $("#alert").dialog({title: "Version conflict", width: "38em", buttons: { @@ -638,6 +716,7 @@ function parseLoadedData(data) { coastline = viewbox.select("#coastline"); prec = viewbox.select("#prec"); population = viewbox.select("#population"); + emblems = viewbox.select("#emblems"); labels = viewbox.select("#labels"); icons = viewbox.select("#icons"); burgIcons = icons.select("#burgIcons"); @@ -698,36 +777,43 @@ function parseLoadedData(data) { } }() - void function restoreLayersState() { - if (texture.style("display") !== "none" && texture.select("image").size()) turnButtonOn("toggleTexture"); else turnButtonOff("toggleTexture"); - if (terrs.selectAll("*").size()) turnButtonOn("toggleHeight"); else turnButtonOff("toggleHeight"); - if (biomes.selectAll("*").size()) turnButtonOn("toggleBiomes"); else turnButtonOff("toggleBiomes"); - if (cells.selectAll("*").size()) turnButtonOn("toggleCells"); else turnButtonOff("toggleCells"); - if (gridOverlay.selectAll("*").size()) turnButtonOn("toggleGrid"); else turnButtonOff("toggleGrid"); - if (coordinates.selectAll("*").size()) turnButtonOn("toggleCoordinates"); else turnButtonOff("toggleCoordinates"); - if (compass.style("display") !== "none" && compass.select("use").size()) turnButtonOn("toggleCompass"); else turnButtonOff("toggleCompass"); - if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers"); - if (terrain.style("display") !== "none" && terrain.selectAll("*").size()) turnButtonOn("toggleRelief"); else turnButtonOff("toggleRelief"); - if (relig.selectAll("*").size()) turnButtonOn("toggleReligions"); else turnButtonOff("toggleReligions"); - if (cults.selectAll("*").size()) turnButtonOn("toggleCultures"); else turnButtonOff("toggleCultures"); - if (statesBody.selectAll("*").size()) turnButtonOn("toggleStates"); else turnButtonOff("toggleStates"); - if (provs.selectAll("*").size()) turnButtonOn("toggleProvinces"); else turnButtonOff("toggleProvinces"); - if (zones.selectAll("*").size() && zones.style("display") !== "none") turnButtonOn("toggleZones"); else turnButtonOff("toggleZones"); - if (borders.style("display") !== "none") turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders"); - if (routes.style("display") !== "none" && routes.selectAll("path").size()) turnButtonOn("toggleRoutes"); else turnButtonOff("toggleRoutes"); - if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp"); - if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec"); - if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels"); - if (icons.style("display") !== "none") turnButtonOn("toggleIcons"); else turnButtonOff("toggleIcons"); - if (armies.selectAll("*").size() && armies.style("display") !== "none") turnButtonOn("toggleMilitary"); else turnButtonOff("toggleMilitary"); - if (markers.selectAll("*").size() && markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers"); - if (ruler.style("display") !== "none") turnButtonOn("toggleRulers"); else turnButtonOff("toggleRulers"); - if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar"); + const notHidden = selection => selection.style("display") !== "none"; + const hasChildren = selection => selection.node()?.hasChildNodes(); + const hasChild = (selection, selector) => selection.node()?.querySelector(selector); + const turnOn = el => document.getElementById(el).classList.remove("buttonoff"); - // special case for population bars - const populationIsOn = population.selectAll("line").size(); - if (populationIsOn) drawPopulation(); - if (populationIsOn) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation"); + void function restoreLayersState() { + // turn all layers off + document.getElementById("mapLayers").querySelectorAll("li").forEach(el => el.classList.add("buttonoff")); + + // turn on active layers + if (notHidden(texture) && hasChild(texture, "image")) turnOn("toggleTexture"); + if (hasChildren(terrs)) turnOn("toggleHeight"); + if (hasChildren(biomes)) turnOn("toggleBiomes"); + if (hasChildren(cells)) turnOn("toggleCells"); + if (hasChildren(gridOverlay)) turnOn("toggleGrid"); + if (hasChildren(coordinates)) turnOn("toggleCoordinates"); + if (notHidden(compass) && hasChild(compass, "use")) turnOn("toggleCompass"); + if (notHidden(rivers)) turnOn("toggleRivers"); + if (notHidden(terrain) && hasChildren(terrain)) turnOn("toggleRelief"); + if (hasChildren(relig)) turnOn("toggleReligions"); + if (hasChildren(cults)) turnOn("toggleCultures"); + if (hasChildren(statesBody)) turnOn("toggleStates"); + if (hasChildren(provs)) turnOn("toggleProvinces"); + if (hasChildren(zones) && notHidden(zones)) turnOn("toggleZones"); + if (notHidden(borders) && hasChild(compass, "use")) turnOn("toggleBorders"); + if (notHidden(routes) && hasChild(routes, "path")) turnOn("toggleRoutes"); + if (hasChildren(temperature)) turnOn("toggleTemp"); + if (hasChild(population, "line")) turnOn("togglePopulation"); + if (hasChildren(ice)) turnOn("toggleIce"); + if (hasChild(prec, "circle")) turnOn("togglePrec"); + if (hasChild(emblems, "use")) turnOn("toggleEmblems"); + if (notHidden(labels)) turnOn("toggleLabels"); + if (notHidden(icons)) turnOn("toggleIcons"); + if (hasChildren(armies) && notHidden(armies)) turnOn("toggleMilitary"); + if (hasChildren(markers) && notHidden(markers)) turnOn("toggleMarkers"); + if (notHidden(ruler)) turnOn("toggleRulers"); + if (notHidden(scaleBar)) turnOn("toggleScaleBar"); getCurrentPreset(); }() @@ -741,7 +827,7 @@ function parseLoadedData(data) { ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd)); ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd)); - scaleBar.on("mousemove", () => tip("Click to open Units Editor")); + scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits()); legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend()); }() @@ -917,13 +1003,6 @@ function parseLoadedData(data) { } if (version < 1.22) { - // v 1.21 had incorrect style formatting - localStorage.removeItem("styleClean"); - localStorage.removeItem("styleGloom"); - localStorage.removeItem("styleAncient"); - localStorage.removeItem("styleMonochrome"); - addDefaulsStyles(); - // v 1.22 changed state neighbors from Set object to array BurgsAndStates.collectStatistics(); } @@ -941,7 +1020,6 @@ function parseLoadedData(data) { BurgsAndStates.generateCampaigns(); // v 1.3 added militry layer - svg.select("defs").append("style").text(armiesStyle()); // add armies style armies = viewbox.insert("g", "#icons").attr("id", "armies"); armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", .3); turnButtonOn("toggleMilitary"); @@ -981,6 +1059,35 @@ function parseLoadedData(data) { pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => r.state = s.i)); } + if (version < 1.5) { + // 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); + }); + + // 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(); + } + }() void function checkDataIntegrity() { @@ -1041,6 +1148,9 @@ function parseLoadedData(data) { changeMapSize(); + // remove href from emblems, to trigger rendering on load + emblems.selectAll("use").attr("href", null); + // set options yearInput.value = options.year; eraInput.value = options.era; diff --git a/modules/ui/3d.js b/modules/ui/3d.js index bd88decc..c7fa9aa0 100644 --- a/modules/ui/3d.js +++ b/modules/ui/3d.js @@ -121,7 +121,6 @@ const saveScreenshot = async function() { const link = document.createElement("a"); link.download = getFileName() + ".jpeg"; link.href = URL; - document.body.appendChild(link); link.click(); tip(`Screenshot is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000); diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index 9a3af95c..2b384ee5 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -31,8 +31,10 @@ function editBurg(id) { document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup); document.getElementById("burgName").addEventListener("input", changeName); - document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture); document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom); + document.getElementById("burgType").addEventListener("input", changeType); + document.getElementById("burgCulture").addEventListener("input", changeCulture); + document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture); document.getElementById("burgPopulation").addEventListener("change", changePopulation); burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature)); @@ -43,7 +45,7 @@ function editBurg(id) { document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle); document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG); - document.getElementById("burgOpenCOA").addEventListener("click", openInIAHG); + document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit); document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg); document.getElementById("burglLegend").addEventListener("click", editBurgLegend); document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg); @@ -51,10 +53,27 @@ function editBurg(id) { function updateBurgValues() { const id = +elSelected.attr("data-id"); const b = pack.burgs[id]; + const province = pack.cells.province[b.cell]; + const provinceName = province ? pack.provinces[province].fullName + ", " : ""; + const stateName = pack.states[b.state].fullName || pack.states[b.state].name; + document.getElementById("burgProvinceAndState").innerHTML = provinceName + stateName; + document.getElementById("burgName").value = b.name; + document.getElementById("burgType").value = b.type || "Generic"; document.getElementById("burgPopulation").value = rn(b.population * populationRate.value * urbanization.value); document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none"; + // update list and select culture + const cultureSelect = document.getElementById("burgCulture"); + cultureSelect.options.length = 0; + const cultures = pack.cultures.filter(c => !c.removed); + cultures.forEach(c => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture))); + + const temperature = grid.cells.temp[pack.cells.g[b.cell]]; + document.getElementById("burgTemperature").innerHTML = convertTemperature(temperature); + document.getElementById("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature); + document.getElementById("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]); + // toggle features if (b.capital) document.getElementById("burgCapital").classList.remove("inactive"); else document.getElementById("burgCapital").classList.add("inactive"); @@ -79,6 +98,23 @@ function editBurg(id) { burgLabels.selectAll("g").each(function() { select.options.add(new Option(this.id, this.id, false, this.id === group)); }); + + // set emlem image + const coaID = "burgCOA"+id; + COArenderer.trigger(coaID, b.coa); + document.getElementById("burgEmblem").setAttribute("href", "#" + coaID); + } + + // in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature + function getTemperatureLikeness(temperature) { + if (temperature < -5) return "Yakutsk"; + const cities = [ + "Snag (Yukon)", "Yellowknife (Canada)", "Okhotsk (Russia)", "Fairbanks (Alaska)", "Nuuk (Greenland)", "Murmansk", // -5 - 0 + "Arkhangelsk", "Anchorage", "Tromsø", "Reykjavik", "Riga", "Stockholm", "Halifax", "Prague", "Copenhagen", "London", // 1 - 10 + "Antwerp", "Paris", "Milan", "Batumi", "Rome", "Dubrovnik", "Lisbon", "Barcelona", "Marrakesh", "Alexandria", // 11 - 20 + "Tegucigalpa", "Guangzhou", "Rio de Janeiro", "Dakar", "Miami", "Jakarta", "Mogadishu", "Bangkok", "Aden", "Khartoum"]; // 21 - 30 + if (temperature > 30) return "Mecca"; + return cities[temperature+5] || null; } function dragBurgLabel() { @@ -220,6 +256,22 @@ function editBurg(id) { elSelected.text(burgName.value); } + function generateNameRandom() { + const base = rand(nameBases.length-1); + burgName.value = Names.getBase(base); + changeName(); + } + + function changeType() { + const id = +elSelected.attr("data-id"); + pack.burgs[id].type = this.value; + } + + function changeCulture() { + const id = +elSelected.attr("data-id"); + pack.burgs[id].culture = +this.value; + } + function generateNameCulture() { const id = +elSelected.attr("data-id"); const culture = pack.burgs[id].culture; @@ -227,12 +279,6 @@ function editBurg(id) { changeName(); } - function generateNameRandom() { - const base = rand(nameBases.length-1); - burgName.value = Names.getBase(base); - changeName(); - } - function changePopulation() { const id = +elSelected.attr("data-id"); pack.burgs[id].population = rn(burgPopulation.value / populationRate.value / urbanization.value, 4); @@ -296,7 +342,8 @@ function editBurg(id) { if (!seed && burg.MFCGlink) {openURL(burg.MFCGlink); return;} const cells = pack.cells; const name = elSelected.text(); - const size = Math.max(Math.min(rn(burg.population), 65), 6); + const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done + const population = rn(burg.population * populationRate.value * urbanization.value); const s = burg.MFCG || defSeed; const cell = burg.cell; @@ -321,22 +368,14 @@ function editBurg(id) { } const site = "http://fantasycities.watabou.ru/?random=0&continuous=0"; - const url = `${site}&name=${name}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`; + const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`; openURL(url); } } - function openInIAHG(event) { - const id = elSelected.attr("data-id"), burg = pack.burgs[id], defSeed = `${seed}-b${id}`; - const openIAHG = () => openURL("https://ironarachne.com/#/heraldry/" + (burg.IAHG || defSeed)); - - if (isCtrlClick(event)) { - prompt(`Please provide an Iron Arachne Heraldry Generator seed.
Default seed is a combination of FMG map seed and burg id (${defSeed})`, - {default:burg.IAHG || defSeed}, v => { - if (v && v != defSeed) burg.IAHG = v; - openIAHG(); - }); - } else openIAHG(); + function openEmblemEdit() { + const id = +elSelected.attr("data-id"), burg = pack.burgs[id]; + editEmblem("burg", "burgCOA"+id, burg); } function toggleRelocateBurg() { diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js index 35d668a6..d5e37f79 100644 --- a/modules/ui/burgs-overview.js +++ b/modules/ui/burgs-overview.js @@ -456,7 +456,7 @@ function overviewBurgs() { function triggerAllBurgsRemove() { alertMessage.innerHTML = `Are you sure you want to remove all burgs except of capitals? -
To remove a capital you have to remove its state first`; +
To remove a capital you have to remove a state first`; $("#alert").dialog({resizable: false, title: "Remove all burgs", buttons: { Remove: function() { @@ -472,5 +472,4 @@ function overviewBurgs() { pack.burgs.filter(b => b.i && !b.capital).forEach(b => removeBurg(b.i)); burgsOverviewAddLines(); } - } diff --git a/modules/ui/cultures-editor.js b/modules/ui/cultures-editor.js index 8c4e7965..5f25fb38 100644 --- a/modules/ui/cultures-editor.js +++ b/modules/ui/cultures-editor.js @@ -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 += `
+ data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}"> @@ -86,26 +89,28 @@ function editCultures() {
${si(population)}
- + + ${selectShape ? `` : ""}
`; continue; } lines += `
+ data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
${c.cells}
- +
${si(area) + unit}
${si(population)}
- + + ${selectShape ? `` : ""}
`; } @@ -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 => ``); + } + 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(); @@ -492,9 +537,9 @@ function editCultures() { debug.select("#cultureCenters").style("display", "none"); culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); - culturesHeader.querySelector("div[data-sortby='type']").style.left = "6.8em"; + 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 = "21px"; 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"}}); @@ -588,9 +633,9 @@ function editCultures() { document.getElementById("culturesManuallyButtons").style.display = "none"; culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden")); - culturesHeader.querySelector("div[data-sortby='type']").style.left = "15.8em"; + 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"; diff --git a/modules/ui/diplomacy-editor.js b/modules/ui/diplomacy-editor.js index 67dc6173..c5f5e986 100644 --- a/modules/ui/diplomacy-editor.js +++ b/modules/ui/diplomacy-editor.js @@ -54,8 +54,10 @@ function editDiplomacy() { const selName = states[sel].fullName; diplomacySelect.style.display = "none"; - let lines = `
-
${selName}
+ COArenderer.trigger("stateCOA"+sel, states[sel].coa); + let lines = `
+
${selName}
+
`; for (const s of states) { @@ -66,9 +68,10 @@ function editDiplomacy() { const tip = s.fullName + description[index] + selName; const tipSelect = `${tip}. Click to see relations to ${s.name}`; const tipChange = `${tip}. Click to change relations to ${selName}`; + COArenderer.trigger("stateCOA"+s.i, s.coa); lines += `
- +
${s.fullName}
diff --git a/modules/ui/editors.js b/modules/ui/editors.js index a67873ff..5e1b6c1f 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -21,7 +21,8 @@ function clicked() { const p = d3.mouse(this); const i = findCell(p[0], p[1]); - if (parent.id === "rivers") editRiver(); + if (grand.id === "emblems") editEmblem(); + else if (parent.id === "rivers") editRiver(); else if (grand.id === "routes") editRoute(); else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else if (grand.id === "burgLabels") editBurg(); @@ -123,8 +124,14 @@ function addBurg(point) { const temple = pack.states[state].form === "Theocracy"; const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + cell % 100 / 1000, .1); + const type = BurgsAndStates.getType(cell, false); - pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: 0, port: 0, temple, population}); + // generate emblem + const coa = COA.generate(pack.states[state].coa, .25, null, type); + coa.shield = COA.getShield(culture, state); + COArenderer.add("burg", i, coa, x, y); + + pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: 0, port: 0, temple, population, coa, type}); cells.burg[cell] = i; const townSize = burgIcons.select("#towns").attr("size") || 0.5; @@ -171,6 +178,13 @@ function removeBurg(id) { const cells = pack.cells, burg = pack.burgs[id]; burg.removed = true; cells.burg[burg.cell] = 0; + + if (burg.coa) { + const coaId = "burgCOA" + id; + if (document.getElementById(coaId)) document.getElementById(coaId).remove(); + emblems.select(`#burgEmblems > use[data-i='${id}']`).remove(); + delete burg.coa; // remove to save data + } } function toggleCapital(burg) { @@ -544,9 +558,7 @@ function unfog(id) { } function getFileName(dataType) { - const formatTime = (time) => { - return (time < 10) ? "0" + time : time; - }; + const formatTime = time => time < 10 ? "0" + time : time; const name = mapName.value; const type = dataType ? dataType + " " : ""; const date = new Date(); diff --git a/modules/ui/emblems-editor.js b/modules/ui/emblems-editor.js new file mode 100644 index 00000000..a2a14520 --- /dev/null +++ b/modules/ui/emblems-editor.js @@ -0,0 +1,391 @@ +"use strict"; +function editEmblem(type, id, el) { + if (customization) return; + if (!id && d3.event) defineEmblemData(d3.event); + + emblems.selectAll("use").call(d3.drag().on("drag", dragEmblem)).classed("draggable", true); + + const emblemStates = document.getElementById("emblemStates"); + const emblemProvinces = document.getElementById("emblemProvinces"); + const emblemBurgs = document.getElementById("emblemBurgs"); + const emblemShapeSelector = document.getElementById("emblemShapeSelector"); + + updateElementSelectors(type, id, el); + + $("#emblemEditor").dialog({ + 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 + }); + + // add listeners,then remove on closure + emblemStates.oninput = selectState; + emblemProvinces.oninput = selectProvince; + emblemBurgs.oninput = selectBurg; + emblemShapeSelector.oninput = changeShape; + document.getElementById("emblemsRegenerate").onclick = regenerate; + document.getElementById("emblemsArmoria").onclick = openInArmoria; + document.getElementById("emblemsUpload").onclick = toggleUpload; + document.getElementById("emblemsUploadImage").onclick = () => emblemImageToLoad.click(); + document.getElementById("emblemsUploadSVG").onclick = () => emblemSVGToLoad.click(); + document.getElementById("emblemImageToLoad").onchange = () => upload("image"); + document.getElementById("emblemSVGToLoad").onchange = () => upload("svg"); + document.getElementById("emblemsDownload").onclick = toggleDownload; + document.getElementById("emblemsDownloadSVG").onclick = () => download("svg"); + document.getElementById("emblemsDownloadPNG").onclick = () => download("png"); + document.getElementById("emblemsDownloadJPG").onclick = () => download("jpeg"); + document.getElementById("emblemsGallery").onclick = downloadGallery; + document.getElementById("emblemsFocus").onclick = showArea; + + function defineEmblemData(e) { + const parent = e.target.parentNode; + const [g, t] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] : + parent.id === "provinceEmblems" ? [pack.provinces, "province"] : + [pack.states, "state"]; + const i = +e.target.dataset.i; + type = t; + id = type+"COA"+i; + el = g[i]; + } + + function updateElementSelectors(type, id, el) { + let state = 0, province = 0, burg = 0; + + // set active type + emblemStates.parentElement.className = type === "state" ? "active" : ""; + emblemProvinces.parentElement.className = type === "province" ? "active" : ""; + emblemBurgs.parentElement.className = type === "burg" ? "active" : ""; + + // define selected values + if (type === "state") state = el.i; + else if (type === "province") { + province = el.i + state = pack.states[el.state].i; + } else { + burg = el.i; + province = pack.cells.province[el.cell] ? pack.provinces[pack.cells.province[el.cell]].i : 0; + state = el.state; + } + + const validBurgs = pack.burgs.filter(burg => burg.i && !burg.removed && burg.coa); + + // update option list and select actual values + emblemStates.options.length = 0; + const neutralBurgs = validBurgs.filter(burg => !burg.state); + if (neutralBurgs.length) emblemStates.options.add(new Option(pack.states[0].name, 0, false, !state)); + const stateList = pack.states.filter(state => state.i && !state.removed); + stateList.forEach(s => emblemStates.options.add(new Option(s.name, s.i, false, s.i === state))); + + emblemProvinces.options.length = 0; + emblemProvinces.options.add(new Option("", 0, false, !province)); + const provinceList = pack.provinces.filter(province => !province.removed && province.state === state); + provinceList.forEach(p => emblemProvinces.options.add(new Option(p.name, p.i, false, p.i === province))); + + emblemBurgs.options.length = 0; + emblemBurgs.options.add(new Option("", 0, false, !burg)); + const burgList = validBurgs.filter(burg => province ? pack.cells.province[burg.cell] === province : burg.state === state); + burgList.forEach(b => emblemBurgs.options.add(new Option(b.capital ? "👑 " + b.name : b.name, b.i, false, b.i === burg))); + emblemBurgs.options[0].disabled = true; + + COArenderer.trigger(id, el.coa); + updateEmblemData(type, id, el); + } + + function updateEmblemData(type, id, el) { + if (!el.coa) return; + document.getElementById("emblemImage").setAttribute("href", "#" + id); + let name = el.fullName || el.name; + if (type === "burg") name = "Burg of " + name; + document.getElementById("emblemArmiger").innerText = name; + + if (el.coa === "custom") emblemShapeSelector.disabled = true; + else { + emblemShapeSelector.disabled = false; + emblemShapeSelector.value = el.coa.shield; + } + } + + function selectState() { + const state = +this.value; + if (state) { + type = "state"; + el = pack.states[state]; + id = "stateCOA"+ state; + } else { + // select neutral burg if state is changed to Neutrals + const neutralBurgs = pack.burgs.filter(burg => burg.i && !burg.removed && !burg.state); + if (!neutralBurgs.length) return; + type = "burg"; + el = neutralBurgs[0]; + id = "burgCOA"+ neutralBurgs[0].i; + } + updateElementSelectors(type, id, el); + } + + function selectProvince() { + const province = +this.value; + + if (province) { + type = "province"; + el = pack.provinces[province]; + id = "provinceCOA"+ province; + } else { + // select state if province is changed to null value + const state = +emblemStates.value; + type = "state"; + el = pack.states[state]; + id = "stateCOA"+ state; + } + + updateElementSelectors(type, id, el); + } + + function selectBurg() { + const burg = +this.value; + type = "burg"; + el = pack.burgs[burg]; + id = "burgCOA"+ burg; + updateElementSelectors(type, id, el); + } + + function changeShape() { + el.coa.shield = this.value; + const coaEl = document.getElementById(id); + if (coaEl) coaEl.remove(); + COArenderer.trigger(id, el.coa); + } + + function showArea() { + highlightEmblemElement(type, el); + } + + function regenerate() { + let parent = null; + if (type === "province") parent = pack.states[el.state]; + else if (type === "burg") { + const province = pack.cells.province[el.cell]; + parent = province ? pack.provinces[province] : pack.states[el.state]; + } + + const shield = el.coa.shield || COA.getShield(el.culture || parent?.culture || 0, el.state); + el.coa = COA.generate(parent ? parent.coa : null, .3, .1, null); + el.coa.shield = shield; + emblemShapeSelector.disabled = false; + emblemShapeSelector.value = el.coa.shield; + + const coaEl = document.getElementById(id); + if (coaEl) coaEl.remove(); + COArenderer.trigger(id, el.coa); + } + + function openInArmoria() { + const coa = el.coa && el.coa !== "custom" ? el.coa : {t1: "sable"}; + const json = JSON.stringify(coa).replaceAll("#", "%23"); + const url = `https://azgaar.github.io/Armoria/?coa=${json}&from=FMG`; + openURL(url); + } + + function toggleUpload() { + document.getElementById("emblemDownloadControl").classList.add("hidden"); + const buttons = document.getElementById("emblemUploadControl"); + buttons.classList.toggle("hidden"); + } + + function upload(type) { + const input = type === "image" ? document.getElementById("emblemImageToLoad") : document.getElementById("emblemSVGToLoad"); + const file = input.files[0]; + input.value = ""; + + if (file.size > 500000) { + tip(`File is too big, please optimize file size up to 500kB and re-upload. Recommended size is 200x200 px and up to 100kB`, true, "error", 5000); + return; + } + + const reader = new FileReader(); + + reader.onload = function(readerEvent) { + const result = readerEvent.target.result; + const defs = document.getElementById("defs-emblems"); + const coa = document.getElementById(id); // old emblem + + if (type === "image") { + const svg = ``; + defs.insertAdjacentHTML("beforeend", svg); + } else { + defs.insertAdjacentHTML("beforeend", result); + const newEmblem = defs.lastChild; // new coa + newEmblem.id = id; + newEmblem.setAttribute("width", 200); + newEmblem.setAttribute("height", 200); + } + + if (coa) coa.remove(); // remove old emblem + el.coa = "custom"; + emblemShapeSelector.disabled = true; + }; + + if (type === "image") reader.readAsDataURL(file); else reader.readAsText(file); + } + + function toggleDownload() { + document.getElementById("emblemUploadControl").classList.add("hidden"); + const buttons = document.getElementById("emblemDownloadControl"); + buttons.classList.toggle("hidden"); + } + + async function download(format) { + const coa = document.getElementById(id); + const size = +emblemsDownloadSize.value; + const url = await getURL(coa, size); + const link = document.createElement("a"); + link.download = getFileName(`Emblem ${el.fullName || el.name}`) + "." + format; + + if (format === "svg") downloadSVG(url, link); else downloadRaster(format, url, link, size); + document.getElementById("emblemDownloadControl").classList.add("hidden"); + } + + function downloadSVG(url, link) { + link.href = url; + link.click(); + } + + function downloadRaster(format, url, link, size) { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = size; + canvas.height = size; + + const img = new Image(); + img.src = url; + img.onload = function() { + if (format === "jpeg") { + ctx.fillStyle = "#fff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const dataURL = canvas.toDataURL("image/" + format, .92); + link.href = dataURL; + link.click(); + window.setTimeout(() => window.URL.revokeObjectURL(dataURL), 6000); + } + } + + 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.setAttribute("width", size); + clone.setAttribute("height", size); + return (new XMLSerializer()).serializeToString(clone); + } + + 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); + await renderAllEmblems(validStates, validProvinces, validBurgs); + runDownload(); + + function runDownload() { + const back = `Go Back`; + + const stateSection = `

States

` + validStates.map(state => { + const el = document.getElementById("stateCOA"+state.i); + return `
${state.fullName}
${getSVG(el, 200)}
`; + }).join("") + `
`; + + 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); + return `
${province.fullName}
${getSVG(el, 200)}
`; + }).join(""); + return stateProvinces.length ? `
${back}

${state.fullName} provinces

${figures}
` : ""; + }).join(""); + + const burgSections = validStates.map(state => { + const stateBurgs = validBurgs.filter(b => b.state === state.i); + let stateBurgSections = validProvinces.filter(p => p.state === state.i).map(province => { + const provinceBurgs = stateBurgs.filter(b => pack.cells.province[b.cell] === province.i); + const provinceBurgFigures = provinceBurgs.map(burg => { + const el = document.getElementById("burgCOA"+burg.i); + return `
${burg.name}
${getSVG(el, 200)}
`; + }).join(""); + return provinceBurgs.length ? `
${back}

${province.fullName} burgs

${provinceBurgFigures}
` : ""; + }).join(""); + + const stateBurgOutOfProvinces = stateBurgs.filter(b => !pack.cells.province[b.cell]); + const stateBurgOutOfProvincesFigures = stateBurgOutOfProvinces.map(burg => { + const el = document.getElementById("burgCOA"+burg.i); + return `
${burg.name}
${getSVG(el, 200)}
`; + }).join(""); + if (stateBurgOutOfProvincesFigures) stateBurgSections += `

${state.fullName} burgs under direct control

${stateBurgOutOfProvincesFigures}
`; + return stateBurgSections; + }).join(""); + + const neutralBurgs = validBurgs.filter(b => !b.state); + const neutralsSection = neutralBurgs.length ? "

Independent burgs

" + neutralBurgs.map(burg => { + const el = document.getElementById("burgCOA"+burg.i); + return `
${burg.name}
${getSVG(el, 200)}
`; + }).join("") + "
" : ""; + + const FMG = `Azgaar's Fantasy Map Generator`; + const license = `the license`; + const html = `${mapName.value} Emblems Gallery + + + +

${mapName.value} Emblems Gallery

+ ${stateSection} + ${provinceSections} + ${burgSections} + ${neutralsSection} +
Generated by ${FMG}. The tool is free, but images may be copyrighted, see ${license}
+ `; + downloadFile(html, name + ".html", "text/plain"); + } + } + + 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() { + const tr = parseTransform(this.getAttribute("transform")); + const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y; + + d3.event.on("drag", function() { + const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`; + this.setAttribute("transform", transform); + }); + } + + function closeEmblemEditor() { + emblems.selectAll("use").call(d3.drag().on("drag", null)).attr("class", null); + } +} \ No newline at end of file diff --git a/modules/ui/general.js b/modules/ui/general.js index ca6ee9f7..5e6d671f 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -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)"; @@ -94,6 +100,23 @@ function showMapTooltip(point, e, i, g) { tip(e.target.parentNode.dataset.name + ". Click to edit"); return; } + + if (group === "emblems" && e.target.tagName === "use") { + const parent = e.target.parentNode; + const [g, type] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] : + parent.id === "provinceEmblems" ? [pack.provinces, "province"] : + [pack.states, "state"]; + const i = +e.target.dataset.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. Hold Shift to show associated area or place`); + return; + } + if (group === "rivers") { const river = +e.target.id.slice(5); const r = pack.rivers.find(r => r.i === river); @@ -171,7 +194,7 @@ function highlightEditorLine(editor, id, timeout = 15000) { Array.from(editor.getElementsByClassName("states hovered")).forEach(el => el.classList.remove("hovered")); // clear all hovered const hovered = Array.from(editor.querySelectorAll("div")).find(el => el.dataset.id == id); if (hovered) hovered.classList.add("hovered"); // add hovered class - if (timeout) setTimeout(() => hovered.classList.remove("hovered"), timeout); + if (timeout) setTimeout(() => {hovered && hovered.classList.remove("hovered")}, timeout); } // get cell info on mouse move @@ -281,6 +304,31 @@ function getPopulationTip(i) { return `Cell population: ${si(rural+urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`; } +function highlightEmblemElement(type, el) { + const i = el.i, cells = pack.cells; + const animation = d3.transition().duration(1000).ease(d3.easeSinIn); + + if (type === "burg") { + const {x, y} = el; + 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; + } + + const [x, y] = el.pole; + const obj = type === "state" ? cells.state : cells.province; + 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)]); + + 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]) + .transition(animation).attr("stroke-dashoffset", 0).attr("opacity", 1) + .transition(animation).delay(1000).attr("stroke-dashoffset", d => d[2]).attr("opacity", 0).remove(); +} + // assign lock behavior document.querySelectorAll("[data-locked]").forEach(function(e) { e.addEventListener("mouseover", function(event) { @@ -289,7 +337,7 @@ document.querySelectorAll("[data-locked]").forEach(function(e) { event.stopPropagation(); }); - e.addEventListener("click", function(event) { + e.addEventListener("click", function() { const id = (this.id).slice(5); if (this.className === "icon-lock") unlock(id); else lock(id); @@ -326,6 +374,22 @@ function stored(option) { return localStorage.getItem(option); } +// assign skeaker behaviour +Array.from(document.getElementsByClassName("speaker")).forEach(el => { + const input = el.previousElementSibling; + el.addEventListener("click", () => speak(input.value)); +}); + +function speak(text) { + const speaker = new SpeechSynthesisUtterance(text); + const voices = speechSynthesis.getVoices(); + if (voices.length) { + const voiceId = +document.getElementById("speakerVoice").value; + speaker.voice = voices[voiceId]; + } + speechSynthesis.speak(speaker); +} + // apply drop-down menu option. If the value is not in options, add it function applyOption(select, id, name = id) { const custom = !Array.from(select.options).some(o => o.value == id); @@ -339,6 +403,7 @@ function showInfo() { const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit") const Patreon = link("https://www.patreon.com/azgaar", "Patreon"); const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello"); + const Armoria = link("https://azgaar.github.io/Armoria", "Armoria"); const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial"); const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page"); @@ -353,9 +418,11 @@ function showInfo() {

The best way to get help is to contact the community on ${Discord} and ${Reddit}. Before asking questions, please check out the ${QuickStart} and the ${QAA}.

-

You can track the development process on ${Trello}.

+

Track the development process on ${Trello}.

- Links: +

Check out our new project: ${Armoria}, heraldry generator and editor.

+ + Links:
  • ${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}
  • ${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}
  • @@ -372,6 +439,7 @@ function showInfo() { // prevent default browser behavior for FMG-used hotkeys document.addEventListener("keydown", event => { if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations + if (event.ctrlKey && event.code === "KeyS") event.preventDefault(); // disallow CTRL + C if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab }); @@ -400,6 +468,7 @@ document.addEventListener("keyup", event => { else if (key === 79 && canvas3d) toggle3dOptions(); // "O" to toggle 3d options else if (ctrl && key === 81) toggleSaveReminder(); // Ctrl + "Q" to toggle save reminder + else if (ctrl && key === 83) saveMap(); // Ctrl + "S" to save .map file else if (undo.offsetParent && ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo else if (redo.offsetParent && ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo @@ -412,6 +481,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 @@ -451,6 +521,7 @@ document.addEventListener("keyup", event => { else if (key === 78) togglePopulation(); // "N" to toggle Population layer else if (key === 74) toggleIce(); // "J" to toggle Ice layer else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer + else if (key === 89) toggleEmblems(); // "Y" to toggle Emblems layer else if (key === 76) toggleLabels(); // "L" to toggle Labels layer else if (key === 73) toggleIcons(); // "I" to toggle Icons layer else if (key === 77) toggleMilitary(); // "M" to toggle Military layer diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index b2f7712a..34a77e1d 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -287,9 +287,7 @@ function editHeightmap() { reGraph(); drawCoastline(); - if (changeHeights.checked) { - 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; @@ -1308,7 +1306,6 @@ function editHeightmap() { const link = document.createElement("a"); link.download = getFileName("Heightmap") + ".png"; link.href = imgBig; - document.body.appendChild(link); link.click(); canvas.remove(); } diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 6f66e108..ef60400e 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -1,29 +1,6 @@ // UI module stub to control map layers "use strict"; -// on map regeneration restore layers if they was turned on -function restoreLayers() { - if (layerIsOn("toggleHeight")) drawHeightmap(); - if (layerIsOn("toggleCells")) drawCells(); - if (layerIsOn("toggleGrid")) drawGrid(); - if (layerIsOn("toggleCoordinates")) drawCoordinates(); - if (layerIsOn("toggleCompass")) compass.style("display", "block"); - if (layerIsOn("toggleTemp")) drawTemp(); - if (layerIsOn("togglePrec")) drawPrec(); - if (layerIsOn("togglePopulation")) drawPopulation(); - if (layerIsOn("toggleBiomes")) drawBiomes(); - if (layerIsOn("toggleRelief")) ReliefIcons(); - if (layerIsOn("toggleCultures")) drawCultures(); - if (layerIsOn("toggleProvinces")) drawProvinces(); - if (layerIsOn("toggleReligions")) drawReligions(); - if (layerIsOn("toggleIce")) drawIce(); - - // states are getting rendered each time, if it's not required than layers should be hidden - if (!layerIsOn("toggleBorders")) $('#borders').fadeOut(); - if (!layerIsOn("toggleStates")) regions.style("display", "none").selectAll("path").remove(); -} - -restoreLayers(); // run on-load let presets = {}; // global object restoreCustomPresets(); // run on-load @@ -38,6 +15,7 @@ function getDefaultPresets() { "physical": ["toggleCoordinates", "toggleHeight", "toggleIce", "toggleRivers", "toggleScaleBar"], "poi": ["toggleBorders", "toggleHeight", "toggleIce", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"], "military": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleMilitary", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"], + "emblems": ["toggleBorders", "toggleIcons", "toggleIce", "toggleEmblems", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"], "landmass": ["toggleScaleBar"] } } @@ -55,9 +33,10 @@ function restoreCustomPresets() { presets = storedPresets; } +// run on map generation function applyPreset() { - const selected = localStorage.getItem("preset"); - if (selected) changePreset(selected); + const preset = localStorage.getItem("preset") || document.getElementById("layersPreset").value; + changePreset(preset); } // toggle layers on preset change @@ -117,6 +96,29 @@ function getCurrentPreset() { savePresetButton.style.display = "inline-block"; } +// run on map regeneration +function restoreLayers() { + if (layerIsOn("toggleHeight")) drawHeightmap(); + if (layerIsOn("toggleCells")) drawCells(); + if (layerIsOn("toggleGrid")) drawGrid(); + if (layerIsOn("toggleCoordinates")) drawCoordinates(); + if (layerIsOn("toggleCompass")) compass.style("display", "block"); + if (layerIsOn("toggleTemp")) drawTemp(); + if (layerIsOn("togglePrec")) drawPrec(); + if (layerIsOn("togglePopulation")) drawPopulation(); + if (layerIsOn("toggleBiomes")) drawBiomes(); + if (layerIsOn("toggleRelief")) ReliefIcons(); + if (layerIsOn("toggleCultures")) drawCultures(); + if (layerIsOn("toggleProvinces")) drawProvinces(); + if (layerIsOn("toggleReligions")) drawReligions(); + if (layerIsOn("toggleIce")) drawIce(); + if (layerIsOn("toggleEmblems")) drawEmblems(); + + // states are getting rendered each time, if it's not required than layers should be hidden + if (!layerIsOn("toggleBorders")) $('#borders').fadeOut(); + if (!layerIsOn("toggleStates")) regions.style("display", "none").selectAll("path").remove(); +} + function toggleHeight(event) { if (!terrs.selectAll("*").size()) { turnButtonOn("toggleHeight"); @@ -457,7 +459,7 @@ function drawCells() { cells.append("path").attr("d", path); } -function toggleIce() { +function toggleIce(event) { if (!layerIsOn("toggleIce")) { turnButtonOn("toggleIce"); $('#ice').fadeIn(); @@ -473,7 +475,7 @@ function toggleIce() { function drawIce() { const cells = grid.cells, vertices = grid.vertices, n = cells.i.length, temp = cells.temp, h = cells.h; const used = new Uint8Array(cells.i.length); - Math.seedrandom(seed); + Math.random = aleaPRNG(seed); const shieldMin = -6; // max temp to form ice shield (glacier) const icebergMax = 2; // max temp to form an iceberg @@ -713,15 +715,16 @@ function drawStates() { }); const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]); - statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]); const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]); - statesBody.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "state-gap"+d[1]); - defs.select("#statePaths").selectAll("clipPath").remove(); - defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]); - statesHalo.selectAll(".path").data(bodyData).enter().append("path") - .attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666") - .attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")"); + const bodyString = bodyData.map(d => ``).join(""); + const gapString = gapData.map(d => ``).join(""); + const clipString = bodyData.map(d => ``).join(""); + const haloString = bodyData.map(d => ``).join(""); + + statesBody.html(bodyString + gapString); + defs.select("#statePaths").html(clipString); + statesHalo.html(haloString); // connect vertices to chain function connectVertices(start, t, state) { @@ -877,6 +880,26 @@ function drawProvinces() { const labelsOn = provs.attr("data-labels") == 1; provs.selectAll("*").remove(); + const provinces = pack.provinces; + const {body, gap} = getProvincesVertices(); + + const g = provs.append("g").attr("id", "provincesBody"); + const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]); + g.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "province"+d[1]); + const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]); + g.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "province-gap"+d[1]); + + const labels = provs.append("g").attr("id", "provinceLabels"); + labels.style("display", `${labelsOn ? "block" : "none"}`); + const labelData = provinces.filter(p => p.i && !p.removed); + labels.selectAll(".path").data(labelData).enter().append("text") + .attr("x", d => d.pole[0]).attr("y", d => d.pole[1]) + .attr("id", d => "provinceLabel"+d.i).text(d => d.name); + + TIME && console.timeEnd("drawProvinces"); +} + +function getProvincesVertices() { const cells = pack.cells, vertices = pack.vertices, provinces = pack.provinces, n = cells.i.length; const used = new Uint8Array(cells.i.length); const vArray = new Array(provinces.length); // store vertices array @@ -906,18 +929,7 @@ function drawProvinces() { provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility }); - const g = provs.append("g").attr("id", "provincesBody"); - const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]); - g.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "province"+d[1]); - const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]); - g.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "province-gap"+d[1]); - - const labels = provs.append("g").attr("id", "provinceLabels"); - labels.style("display", `${labelsOn ? "block" : "none"}`); - const labelData = provinces.filter(p => p.i && !p.removed); - labels.selectAll(".path").data(labelData).enter().append("text") - .attr("x", d => d.pole[0]).attr("y", d => d.pole[1]) - .attr("id", d => "provinceLabel"+d.i).text(d => d.name); + return {body, gap}; // connect vertices to chain function connectVertices(start, t, province) { @@ -942,7 +954,6 @@ function drawProvinces() { chain.push([start, province, land]); // add starting vertex to sequence to close the path return chain; } - TIME && console.timeEnd("drawProvinces"); } function toggleGrid(event) { @@ -1214,7 +1225,98 @@ function toggleZones(event) { if (event && isCtrlClick(event)) {editStyle("zones"); return;} turnButtonOff("toggleZones"); $('#zones').fadeOut(); - } + } +} + +function toggleEmblems(event) { + if (!layerIsOn("toggleEmblems")) { + turnButtonOn("toggleEmblems"); + if (!emblems.selectAll("use").size()) drawEmblems(); + $('#emblems').fadeIn(); + if (event && isCtrlClick(event)) editStyle("emblems"); + } else { + if (event && isCtrlClick(event)) {editStyle("emblems"); return;} + $('#emblems').fadeOut(); + turnButtonOff("toggleEmblems"); + } +} + +function drawEmblems() { + TIME && console.time("drawEmblems"); + const {states, provinces, burgs} = pack; + + const validStates = states.filter(s => s.i && !s.removed && s.coa); + const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa); + const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa); + + const getStateEmblemsSize = () => { + const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100); + const statesMod = (1 + validStates.length / 100) - (15 - validStates.length) / 200; // states number modifier + const sizeMod = +document.getElementById("styleEmblemsStateSizeInput").value || 1; + return rn(startSize / statesMod * sizeMod); // target size ~50px on 1536x754 map with 15 states + }; + + const getProvinceEmblemsSize = () => { + const startSize = Math.min(Math.max((graphHeight + graphWidth) / 80, 5), 75); + const provincesMod = (1 + validProvinces.length / 1000) - (115 - validProvinces.length) / 1000; // states number modifier + const sizeMod = +document.getElementById("styleEmblemsProvinceSizeInput").value || 1; + return rn(startSize / provincesMod * sizeMod); // target size ~26px on 1536x754 map with 115 provinces + } + + const getBurgEmblemSize = () => { + const startSize = Math.min(Math.max((graphHeight + graphWidth) / 150, 5), 50); + const burgsMod = (1 + validBurgs.length / 1000) - (450 - validBurgs.length) / 1000; // states number modifier + const sizeMod = +document.getElementById("styleEmblemsBurgSizeInput").value || 1; + return rn(startSize / burgsMod * sizeMod); // target size ~10px on 1536x754 map with 450 burgs + } + + const sizeBurgs = getBurgEmblemSize(); + const burgCOAs = validBurgs.map(burg => { + const {x, y} = burg; + return {type: "burg", i: burg.i, x, y, size: sizeBurgs}; + }); + + const sizeProvinces = getProvinceEmblemsSize(); + const provinceCOAs = validProvinces.map(province => { + if (!province.pole) getProvincesVertices(); + const [x, y] = province.pole; + return {type: "province", i: province.i, x, y, size: sizeProvinces}; + }); + + const sizeStates = getStateEmblemsSize(); + const stateCOAs = validStates.map(state => { + const [x, y] = state.pole; + return {type: "state", i: state.i, x, y, size: sizeStates}; + }); + + const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs); + const simulation = d3.forceSimulation(nodes) + .alphaMin(.6).alphaDecay(.2).velocityDecay(.6) + .force('collision', d3.forceCollide().radius(d => d.size/2)) + .stop(); + + d3.timeout(function() { + const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); + for (let i = 0; i < n; ++i) { + simulation.tick(); + } + + const burgNodes = nodes.filter(node => node.type === "burg"); + const burgString = burgNodes.map(d => ``).join(""); + emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString); + + const provinceNodes = nodes.filter(node => node.type === "province"); + const provinceString = provinceNodes.map(d => ``).join(""); + emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString); + + const stateNodes = nodes.filter(node => node.type === "state"); + const stateString = stateNodes.map(d => ``).join(""); + emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString); + + invokeActiveZooming(); + }); + + TIME && console.timeEnd("drawEmblems"); } function layerIsOn(el) { @@ -1262,6 +1364,7 @@ function getLayer(id) { if (id === "togglePopulation") return $("#population"); if (id === "toggleIce") return $("#ice"); if (id === "toggleTexture") return $("#texture"); + if (id === "toggleEmblems") return $("#emblems"); if (id === "toggleLabels") return $("#labels"); if (id === "toggleIcons") return $("#icons"); if (id === "toggleMarkers") return $("#markers"); diff --git a/modules/ui/namesbase-editor.js b/modules/ui/namesbase-editor.js index baf0acb5..03149a13 100644 --- a/modules/ui/namesbase-editor.js +++ b/modules/ui/namesbase-editor.js @@ -22,6 +22,7 @@ function editNamesbase() { document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload); document.getElementById("namesbaseUpload").addEventListener("click", () => namesbaseToLoad.click()); document.getElementById("namesbaseToLoad").addEventListener("change", function() {uploadFile(this, namesbaseUpload)}); + document.getElementById("namesbaseSpeak").addEventListener("click", () => speak(namesbaseExamples.textContent)); createBasesList(); updateInputs(); diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js index c273c61e..04facf08 100644 --- a/modules/ui/notes-editor.js +++ b/modules/ui/notes-editor.js @@ -38,7 +38,7 @@ function editNotes(id, name) { // open a dialog $("#notesEditor").dialog({ - title: "Notes Editor", minWidth: "40em", + title: "Notes Editor", minWidth: "40em", width: "50vw", position: {my: "center", at: "center", of: "svg"}, close: () => notesText.innerHTML = "" }); @@ -50,6 +50,7 @@ function editNotes(id, name) { document.getElementById("notesSelect").addEventListener("change", changeObject); document.getElementById("notesName").addEventListener("input", changeName); document.getElementById("notesPin").addEventListener("click", () => options.pinNotes = !options.pinNotes); + document.getElementById("notesSpeak").addEventListener("click", () => speak(editor.content.innerHTML)); document.getElementById("notesFocus").addEventListener("click", validateHighlightElement); document.getElementById("notesDownload").addEventListener("click", downloadLegends); document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click()); @@ -125,6 +126,7 @@ function editNotes(id, name) { notes.splice(index, 1); select.options.length = 0; if (!notes.length) {$("#notesEditor").dialog("close"); return;} + notesText.innerHTML = ""; editNotes(notes[0].id, notes[0].name); } diff --git a/modules/ui/options.js b/modules/ui/options.js index 24434e2d..875438be 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -87,8 +87,9 @@ function showSupporters() { Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta, Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR, ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill, - Char, Jack, Barna Csíkos, Ian Rousseau, Nicholas Grabstas, Tom Van Orden jr, Bryan Brake, Akylos, Riley Seaman, - MaxOliver, Evan-DiLeo, Alex Debus, Joshua Vaught, Kyle S, Eric Moore, Dean Dunakin, Uniquenameosaurus, WarWizardGames`; + Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,Alex Debus,Joshua Vaught, + Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6, + Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen`; const array = supporters.replace(/(?:\r\n|\r|\n)/g, "").split(",").map(v => capitalize(v.trim())).sort(); alertMessage.innerHTML = "
      " + array.map(n => `
    • ${n}
    • `).join("") + "
    "; @@ -114,6 +115,7 @@ optionsContent.addEventListener("input", function(event) { else if (id === "neutralOutput") neutralInput.value = value; else if (id === "manorsInput") changeBurgsNumberSlider(value); else if (id === "religionsInput") religionsOutput.value = value; + else if (id === "emblemShape") changeEmblemShape(value); else if (id === "uiSizeInput") uiSizeOutput.value = value; else if (id === "uiSizeOutput") changeUIsize(value); else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value); @@ -139,6 +141,7 @@ optionsContent.addEventListener("click", function(event) { else if (id === "optionsEraRegenerate") regenerateEra(); else if (id === "zoomExtentDefault") restoreDefaultZoomExtent(); else if (id === "translateExtent") toggleTranslateExtent(event.target); + else if (id === "speakerTest") testSpeaker(); }); function mapSizeInputChange() { @@ -197,6 +200,30 @@ function toggleTranslateExtent(el) { else zoom.translateExtent([[0, 0], [graphWidth, graphHeight]]); } +// add voice options +const voiceInterval = setInterval(function() { + const voices = speechSynthesis.getVoices(); + if (voices.length) clearInterval(voiceInterval); else return; + + const select = document.getElementById("speakerVoice"); + voices.forEach((voice, i) => { + select.options.add(new Option(voice.name, i, false)); + }); + if (stored("speakerVoice")) select.value = localStorage.getItem("speakerVoice"); // se voice to store + else select.value = voices.findIndex(voice => voice.lang === "en-US"); // or to first found English-US +}, 1000); + +function testSpeaker() { + const text = `${mapName.value}, ${options.year} ${options.era}`; + const speaker = new SpeechSynthesisUtterance(text); + const voices = speechSynthesis.getVoices(); + if (voices.length) { + const voiceId = +document.getElementById("speakerVoice").value; + speaker.voice = voices[voiceId]; + } + speechSynthesis.speak(speaker); +} + function generateMapWithSeed() { if (optionsSeed.value == seed) { tip("The current map already has this seed", false, "error"); @@ -218,7 +245,7 @@ function showSeedHistoryDialog() { }); } -// generate map with historycal seed +// generate map with historical seed function restoreSeed(id) { if (mapHistory[id].seed == seed) { tip("The current map is already generated with this seed", null, "error"); @@ -262,6 +289,47 @@ function changeCultureSet() { if (+culturesOutput.value > +max) culturesInput.value = culturesOutput.value = max; } +function changeEmblemShape(emblemShape) { + const image = document.getElementById("emblemShapeImage"); + 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) { regionsInput.value = regionsOutput.value = value; regionsOutput.style.color = +value ? null : "#b12117"; @@ -319,6 +387,7 @@ function applyStoredOptions() { for (let i=0; i < localStorage.length; i++) { const stored = localStorage.key(i), value = localStorage.getItem(stored); + if (stored === "speakerVoice") continue; const input = document.getElementById(stored+"Input") || document.getElementById(stored); const output = document.getElementById(stored+"Output"); if (input) input.value = value; @@ -350,7 +419,7 @@ function applyStoredOptions() { // randomize options if randomization is allowed (not locked or options='default') function randomizeOptions() { - Math.seedrandom(seed); // reset seed to initial one + Math.random = aleaPRNG(seed); // reset seed to initial one const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options // 'Options' settings @@ -403,14 +472,14 @@ function randomizeHeightmapTemplate() { // select culture set pseudo-randomly function randomizeCultureSet() { const sets = { - "world": 25, - "european": 20, - "oriental": 10, - "english": 10, - "antique": 5, - "highFantasy": 22, - "darkFantasy": 6, - "random": 2}; + "world": 10, + "european": 10, + "oriental": 2, + "english": 5, + "antique": 3, + "highFantasy": 11, + "darkFantasy": 3, + "random": 1}; culturesSet.value = rw(sets); changeCultureSet(); } diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js index c794386a..adda7d3e 100644 --- a/modules/ui/provinces-editor.js +++ b/modules/ui/provinces-editor.js @@ -38,7 +38,7 @@ function editProvinces() { const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id; if (cl.contains("fillRect")) changeFill(el); else if (cl.contains("name")) editProvinceName(p); else - if (cl.contains("icon-coa")) provinceOpenCOA(ev, p); else + if (cl.contains("coaIcon")) editEmblem("province", "provinceCOA"+p, pack.provinces[p]); else if (cl.contains("icon-star-empty")) capitalZoomIn(p); else if (cl.contains("icon-flag-empty")) triggerIndependencePromps(p); else if (cl.contains("culturePopulation")) changePopulation(p); else @@ -114,10 +114,11 @@ function editProvinces() { const capital = p.burg ? pack.burgs[p.burg].name : ''; const separable = p.burg && p.burg !== pack.states[p.state].capital; const focused = defs.select("#fog #focusProvince"+p.i).size(); + COArenderer.trigger("provinceCOA"+p.i, p.coa); lines += `
    - + @@ -192,19 +193,6 @@ function editProvinces() { openPicker(currentFill, callback); } - function provinceOpenCOA(event, p) { - const defSeed = `${seed}-p${p}`; - const openIAHG = () => openURL("https://ironarachne.com/#/heraldry/" + (pack.provinces[p].IAHG || defSeed)); - - if (isCtrlClick(event)) { - prompt(`Please provide an Iron Arachne Heraldry Generator seed.
    Default seed is a combination of FMG map seed and province id (${defSeed})`, - {default:pack.provinces[p].IAHG || defSeed}, v => { - if (v && v != defSeed) pack.provinces[p].IAHG = v; - openIAHG(); - }); - } else openIAHG(); - } - function capitalZoomIn(p) { const capital = pack.provinces[p].burg; const l = burgLabels.select("[data-id='" + capital + "']"); @@ -247,10 +235,15 @@ function editProvinces() { const name = provinces[p].name; const color = getRandomColor(); + const coa = provinces[p].coa; + const coaEl = document.getElementById("provinceCOA"+p); + if (coaEl) coaEl.id = "stateCOA"+newState; + emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove(); + // update cells cells.i.filter(i => cells.province[i] === p).forEach(i => { - cells.province[i] = 0; - cells.state[i] = newState; + cells.province[i] = 0; + cells.state[i] = newState; }); // update diplomacy and reverse relations @@ -272,7 +265,7 @@ function editProvinces() { states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldState].name}`]); // create new state - states.push({i:newState, name, diplomacy, provinces:[], color, expansionism:.5, capital:burg, type:"Generic", center, culture, military:[], alert:1}); + states.push({i:newState, name, diplomacy, provinces:[], color, expansionism:.5, capital:burg, type:"Generic", center, culture, military:[], alert:1, coa}); BurgsAndStates.collectStatistics(); BurgsAndStates.defineStateForms([newState]); @@ -284,7 +277,10 @@ function editProvinces() { // remove old province unfog("focusProvince"+p); if (states[oldState].provinces.includes(p)) states[oldState].provinces.splice(states[oldState].provinces.indexOf(p), 1); - provinces[p].removed = true; + provinces[p] = {i:p, removed: true}; + + // draw emblem + COArenderer.add("state", newState, coa, pack.states[newState].pole[0], pack.states[newState].pole[1]); closeDialogs(); editStates(); @@ -344,7 +340,6 @@ function editProvinces() { refreshProvincesEditor(); } - } function toggleFog(p, cl) { @@ -354,17 +349,24 @@ function editProvinces() { } function removeProvince(p) { - alertMessage.innerHTML = `Are you sure you want to remove the province?
    This action cannot be reverted`; $("#alert").dialog({resizable: false, title: "Remove province", buttons: { Remove: function() { - pack.cells.province.forEach((province, i) => {if(province === p) pack.cells.province[i] = 0;}); - const state = pack.provinces[p].state; - if (pack.states[state].provinces.includes(p)) pack.states[state].provinces.splice(pack.states[state].provinces.indexOf(p), 1); - pack.provinces[p].removed = true; + pack.cells.province.forEach((province, i) => { + if(province === p) pack.cells.province[i] = 0; + }); + const s = province.state, state = pack.states[s]; + if (state.provinces.includes(p)) state.provinces.splice(state.provinces.indexOf(p), 1); + unfog("focusProvince"+p); - + + const coaId = "provinceCOA" + p; + if (document.getElementById(coaId)) document.getElementById(coaId).remove(); + emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove(); + + pack.provinces[p] = {i: p, removed: true}; + const g = provs.select("#provincesBody"); g.select("#province"+p).remove(); g.select("#province-gap"+p).remove(); @@ -375,8 +377,6 @@ function editProvinces() { Cancel: function() {$(this).dialog("close");} } }); - - } function editProvinceName(province) { @@ -387,7 +387,7 @@ function editProvinces() { document.getElementById("provinceNameEditorFull").value = p.fullName; $("#provinceNameEditor").dialog({ - resizable: false, title: "Change province name", width: "22em", buttons: { + resizable: false, title: "Change province name", buttons: { Apply: function() {applyNameChange(p); $(this).dialog("close");}, Cancel: function() {$(this).dialog("close");} }, position: {my: "center", at: "center", of: "svg"} @@ -771,7 +771,16 @@ function editProvinces() { const fullName = name + " " + formName; const stateColor = pack.states[state].color, rndColor = getRandomColor(); const color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(.2)).hex() : rndColor; - provinces.push({i:province, state, center, burg, name, formName, fullName, color}); + + // generate emblem + const kinship = burg ? .8 : .4; + const parent = burg ? pack.burgs[burg].coa : pack.states[state].coa; + const type = BurgsAndStates.getType(center, parent.port); + const coa = COA.generate(parent, kinship, P(.1), type); + coa.shield = COA.getShield(c, state); + COArenderer.add("province", province, coa, point[0], point[1]); + + provinces.push({i:province, state, center, burg, name, formName, fullName, color, coa}); cells.province[center] = province; cells.c[center].forEach(c => { @@ -837,13 +846,17 @@ function editProvinces() { buttons: { Remove: function() { $(this).dialog("close"); - pack.provinces.filter(p => p.i).forEach(p => { - p.removed = true; - unfog("focusProvince"+p.i); - }); - pack.cells.i.forEach(i => pack.cells.province[i] = 0); - pack.states.filter(s => s.i && !s.removed).forEach(s => s.provinces = []); + // remove emblems + document.querySelectorAll("[id^='provinceCOA']").forEach(el => el.remove()); + emblems.select("#provinceEmblems").selectAll("*").remove(); + + // remove data + pack.provinces = [0]; + pack.cells.province = new Uint16Array(pack.cells.i.length); + pack.states.forEach(s => s.provinces = []); + + unfog(); if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); provs.select("#provincesBody").remove(); turnButtonOff("toggleProvinces"); diff --git a/modules/ui/relief-editor.js b/modules/ui/relief-editor.js index a3653aa2..4ed05a4e 100644 --- a/modules/ui/relief-editor.js +++ b/modules/ui/relief-editor.js @@ -54,7 +54,7 @@ function editReliefIcon() { } function updateReliefIconSelected() { - const type = elSelected.attr("data-type"); + const type = elSelected.attr("href"); const button = reliefIconsDiv.querySelector("svg[data-type='"+type+"']"); reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed")); @@ -65,7 +65,7 @@ function editReliefIcon() { } function updateReliefSizeInput() { - const size = +elSelected.attr("data-size"); + const size = +elSelected.attr("width"); reliefSize.value = reliefSizeNumber.value = rn(size); } @@ -146,14 +146,15 @@ function editReliefIcon() { const x = rn(cx-h, 2); const y = rn(cy-h, 2); const z = y + h * 2; + const s = rn(h*2, 2); let nth = 1; while (positions[nth] && z > positions[nth]) {nth++;} tree.add([cx, cy]); positions.push(z); - terrain.insert("use", ":nth-child("+nth+")").attr("xlink:href", type).attr("data-type", type) - .attr("x", x).attr("y", y).attr("data-size", h*2).attr("width", h*2).attr("height", h*2); + terrain.insert("use", ":nth-child("+nth+")").attr("href", type) + .attr("x", x).attr("y", y).attr("width", s).attr("height", s); }); }); @@ -178,7 +179,7 @@ function editReliefIcon() { const r = +reliefRadiusNumber.value; const type = pressed.dataset.type; - const icons = type ? terrain.selectAll("use[data-type='"+type+"']") : terrain.selectAll("use"); + const icons = type ? terrain.selectAll("use[href='"+type+"']") : terrain.selectAll("use"); const tree = d3.quadtree(); icons.each(function() { const x = +this.getAttribute("x") + this.getAttribute("width") / 2; @@ -198,7 +199,7 @@ function editReliefIcon() { if (!reliefIndividual.classList.contains("pressed")) return; const shift = (size - +elSelected.attr("width")) / 2; - elSelected.attr("width", size).attr("height", size).attr("data-size", size); + elSelected.attr("width", size).attr("height", size); const x = +elSelected.attr("x"), y = +elSelected.attr("y"); elSelected.attr("x", x-shift).attr("y", y-shift); } @@ -217,7 +218,7 @@ function editReliefIcon() { if (reliefIndividual.classList.contains("pressed")) { const type = this.dataset.type; - elSelected.attr("xlink:href", type).attr("data-type", type); + elSelected.attr("href", type); } } diff --git a/modules/ui/states-editor.js b/modules/ui/states-editor.js index 43e00888..bcaea20e 100644 --- a/modules/ui/states-editor.js +++ b/modules/ui/states-editor.js @@ -42,7 +42,7 @@ function editStates() { const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id; if (cl.contains("fillRect")) stateChangeFill(el); else if (cl.contains("name")) editStateName(state); else - if (cl.contains("icon-coa")) stateOpenCOA(ev, state); else + if (cl.contains("coaIcon")) editEmblem("state", "stateCOA"+state, pack.states[state]); else if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else if (cl.contains("culturePopulation")) changePopulation(state); else if (cl.contains("icon-pin")) toggleFog(state, cl); else @@ -90,7 +90,7 @@ function editStates() { data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism=""> - + @@ -109,12 +109,14 @@ function editStates() {
    `; continue; } + const capital = pack.burgs[s.capital].name; + COArenderer.trigger("stateCOA"+s.i, s.coa); lines += `
    - + @@ -230,7 +232,7 @@ function editStates() { document.getElementById("stateNameEditorFull").value = s.fullName || ""; $("#stateNameEditor").dialog({ - resizable: false, title: "Change state name", width: "22em", buttons: { + resizable: false, title: "Change state name", buttons: { Apply: function() {applyNameChange(s); $(this).dialog("close");}, Cancel: function() {$(this).dialog("close");} }, position: {my: "center", at: "center", of: "svg"} @@ -293,7 +295,8 @@ function editStates() { const changed = nameChanged || formChanged || fullNameChanged; if (formChanged) { - const form = formSelect.selectedOptions[0].dataset.form || null; + const selected = formSelect.selectedOptions[0]; + const form = selected.parentElement.label || null; if (form) s.form = form; } @@ -313,19 +316,6 @@ function editStates() { document.querySelector("#burgLabel"+capital).textContent = value; } - function stateOpenCOA(event, state) { - const defSeed = `${seed}-s${state}`; - const openIAHG = () => openURL("https://ironarachne.com/#/heraldry/" + (pack.states[state].IAHG || defSeed)); - - if (isCtrlClick(event)) { - prompt(`Please provide an Iron Arachne Heraldry Generator seed.
    Default seed is a combination of FMG map seed and state id (${defSeed})`, - {default:pack.states[state].IAHG || defSeed}, v => { - if (v && v != defSeed) pack.states[state].IAHG = v; - openIAHG(); - }); - } else openIAHG(); - } - function changePopulation(state) { const s = pack.states[state]; if (!s.cells) {tip("State does not have any cells, cannot change population", false, "error"); return;} @@ -433,17 +423,28 @@ function editStates() { statesBody.select("#state"+state).remove(); statesBody.select("#state-gap"+state).remove(); statesHalo.select("#state-border"+state).remove(); + labels.select("#stateLabel"+state).remove(); + defs.select("#textPath_stateLabel"+state).remove(); + unfog("focusState"+state); - const label = document.querySelector("#stateLabel"+state); - if (label) label.remove(); pack.burgs.forEach(b => {if(b.state === state) b.state = 0;}); pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;}); - pack.states[state].removed = true; + + // remove emblem + const coaId = "stateCOA" + state; + document.getElementById(coaId).remove(); + emblems.select(`#stateEmblems > use[data-i='${state}']`).remove(); // remove provinces pack.states[state].provinces.forEach(p => { - pack.provinces[p].removed = true; + pack.provinces[p] = {i: p, removed: true}; pack.cells.province.forEach((pr, i) => {if(pr === p) pack.cells.province[i] = 0;}); + const coaId = "provinceCOA" + p; + if (document.getElementById(coaId)) document.getElementById(coaId).remove(); + emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove(); + const g = provs.select("#provincesBody"); + g.select("#province"+p).remove(); + g.select("#province-gap"+p).remove(); }); // remove military @@ -459,8 +460,7 @@ function editStates() { pack.burgs[capital].state = 0; moveBurgToGroup(capital, "towns"); - // clean state object - pack.states[state].military = []; + pack.states[state] = {i: state, removed: true}; debug.selectAll(".highlight").remove(); if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); @@ -843,6 +843,11 @@ function editStates() { const name = Names.getState(basename, culture); const color = getRandomColor(); + // generate emblem + const cultureType = pack.cultures[culture].type; + const coa = COA.generate(burgs[burg].coa, .4, null, cultureType); + coa.shield = COA.getShield(culture, null); + // update diplomacy and reverse relations const diplomacy = states.map(s => { if (!s.i) return "x"; @@ -870,7 +875,9 @@ function editStates() { const affectedProvinces = [cells.province[center]]; cells.state[center] = newState; cells.province[center] = 0; - cells.c[center].forEach(c => { + + const cellsToCheck = [...new Set(cells.c[center].map(c => cells.c[c].map(c => cells.c[c])).flat(2))]; + cellsToCheck.forEach(c => { if (cells.h[c] < 20) return; if (cells.burg[c]) return; affectedStates.push(cells.state[c]); @@ -878,7 +885,8 @@ function editStates() { cells.state[c] = newState; cells.province[c] = 0; }); - states.push({i:newState, name, diplomacy, provinces:[], color, expansionism:.5, capital:burg, type:"Generic", center, culture, military:[], alert:1}); + + states.push({i:newState, name, diplomacy, provinces:[], color, expansionism:.5, capital:burg, type:"Generic", center, culture, military:[], alert:1, coa}); BurgsAndStates.collectStatistics(); BurgsAndStates.defineStateForms([newState]); adjustProvinces([...new Set(affectedProvinces)]); @@ -887,6 +895,7 @@ function editStates() { if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); BurgsAndStates.drawStateLabels([...new Set(affectedStates)]); + COArenderer.add("state", newState, coa, states[newState].pole[0], states[newState].pole[1]); statesEditorAddLines(); } diff --git a/modules/ui/style.js b/modules/ui/style.js index db3b1db8..97b766e9 100644 --- a/modules/ui/style.js +++ b/modules/ui/style.js @@ -1,6 +1,11 @@ // UI module to control the style "use strict"; +// store some style inputs as options +styleElements.addEventListener("change", function(ev) { + if (ev.target.dataset.stored) lock(ev.target.dataset.stored); +}); + // select element to be edited function editStyle(element, group) { showOptions(); @@ -226,6 +231,12 @@ function selectStyleElement() { styleArmiesSize.value = styleArmiesSizeOutput.value = el.attr("box-size"); } + if (sel === "emblems") { + styleEmblems.style.display = "block"; + styleStrokeWidth.style.display = "block"; + styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 1; + } + // update group options styleGroupSelect.options.length = 0; // remove all options if (sel === "routes" || sel === "labels" || sel === "coastline" || sel === "lakes" || sel === "anchors" || sel === "burgIcons" || sel === "borders") { @@ -614,6 +625,21 @@ styleArmiesSize.addEventListener("input", function() { }); }); +styleEmblemsStateSizeInput.addEventListener("input", function() { + styleEmblemsStateSizeOutput.value = this.value; + drawEmblems(); +}); + +styleEmblemsProvinceSizeInput.addEventListener("input", function() { + styleEmblemsProvinceSizeOutput.value = this.value; + drawEmblems(); +}); + +styleEmblemsBurgSizeInput.addEventListener("input", function() { + styleEmblemsBurgSizeOutput.value = this.value; + drawEmblems(); +}); + // request a URL to image to be used as a texture function textureProvideURL() { alertMessage.innerHTML = `Provide an image URL to be used as a texture: @@ -664,17 +690,17 @@ function fetchTextureURL(url) { img.src = url; } -function armiesStyle() { - return `#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}`; +const defaultStyles = { + styleAncient: `{"#map":{"background-color":"#000000","filter":"url(#filter-sepia)","data-filter":"sepia"},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0.05,"opacity":0.8,"fill-opacity":0.8,"filter":null},"#biomes":{"opacity":null,"filter":null,"mask":null},"#stateBorders":{"opacity":0.8,"stroke":"#56566d","stroke-width":1,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null},"#provinceBorders":{"opacity":0.8,"stroke":"#56566d","stroke-width":0.2,"stroke-dasharray":1,"stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.1,"filter":null,"mask":null},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":12,"font-size":12,"stroke":"#d4d4d4","stroke-width":1,"stroke-dasharray":5,"stroke-linecap":null,"filter":null,"mask":null},"#compass":{"opacity":0.8,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":"translate(80 80) scale(.25)"},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":0.7,"filter":null},"#cults":{"opacity":0.6,"stroke":"#777777","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#eee9d7","filter":null},"#markers":{"opacity":null,"rescale":1,"filter":"url(#dropShadow01)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0.1,"fill":"#003dff","filter":null},"#population":{"opacity":null,"stroke-width":1.6,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null},"#rural":{"stroke":"#0000ff"},"#urban":{"stroke":"#ff0000"},"#freshwater":{"opacity":0.5,"fill":"#a6c1fd","stroke":"#5f799d","stroke-width":0.7,"filter":null},"#salt":{"opacity":0.5,"fill":"#409b8a","stroke":"#388985","stroke-width":0.7,"filter":null},"#sinkhole":{"opacity":1,"fill":"#5bc9fd","stroke":"#53a3b0","stroke-width":0.7,"filter":null},"#frozen":{"opacity":0.95,"fill":"#cdd4e7","stroke":"#cfe0eb","stroke-width":0,"filter":null},"#lava":{"opacity":0.7,"fill":"#90270d","stroke":"#f93e0c","stroke-width":2,"filter":"url(#crumpled)"},"#dry":{"opacity":0.7,"fill":"#c9bfa7","stroke":"#8e816f","stroke-width":0.7,"filter":null},"#sea_island":{"opacity":0.5,"stroke":"#1f3846","stroke-width":0.7,"filter":"url(#dropShadow)","auto-filter":1},"#lake_island":{"opacity":1,"stroke":"#7c8eaf","stroke-width":0.35,"filter":null},"#terrain":{"opacity":null,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":null,"filter":null,"fill":"#5d97bb"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":0.8,"stroke":"#2e1607","stroke-width":1.23,"stroke-dasharray":3,"stroke-linecap":"inherit","filter":null,"mask":null},"#trails":{"opacity":0.8,"stroke":"#331809","stroke-width":0.5,"stroke-dasharray":"1 2","stroke-linecap":"butt","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#ffffff","stroke-width":0.8,"stroke-dasharray":"1 2","stroke-linecap":"round","filter":null,"mask":null},"#regions":{"opacity":0.4,"filter":""},"#statesHalo":{"opacity":1,"data-width":10,"stroke-width":10},"#provs":{"opacity":0.6,"filter":null},"#temperature":{"opacity":null,"font-size":"8px","fill":"#000000","fill-opacity":0.3,"stroke":null,"stroke-width":1.8,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.8,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#outline)"},"#emblems":{"opacity":0.6,"stroke-width":0.8,"filter":"url(#dropShadow05)"},"#texture":{"opacity":null,"filter":null,"mask":"url(#land)"},"#textureImage":{},"#zones":{"opacity":0.6,"stroke":"#333333","stroke-width":0,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null,"mask":null},"#ocean":{"opacity":1},"#oceanLayers":{"filter":"url(#blur5)","layers":"-6,-4,-2"},"#oceanBase":{"fill":"#a7a01f"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#pattern1)"},"#terrs":{"opacity":null,"scheme":"light","terracing":0,"skip":0,"relax":0,"curve":0,"filter":null,"mask":"url(#land)"},"#legend":{"data-size":13,"font-size":13,"data-font":"Almendra+SC","font-family":"Almendra SC","stroke":"#812929","stroke-width":2.5,"stroke-dasharray":"0 4 10 4","stroke-linecap":"round","data-x":99,"data-y":93,"data-columns":8},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#3e3e4b","data-size":8,"font-size":8,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #cities":{"opacity":1,"fill":"#fdfab9","fill-opacity":0.7,"size":1,"stroke":"#54251d","stroke-width":0.3,"stroke-dasharray":".3 .4","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":1,"fill":"#ffffff","size":2,"stroke":"#3e3e4b","stroke-width":1.2},"#burgLabels > #towns":{"opacity":1,"fill":"#3e3e4b","data-size":4,"font-size":4,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #towns":{"opacity":1,"fill":"#fef4d8","fill-opacity":0.7,"size":0.5,"stroke":"#463124","stroke-width":0.12,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1,"stroke":"#3e3e4b","stroke-width":1.2},"#labels > #states":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":22,"font-size":22,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#labels > #addedLabels":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#fogging":{"opacity":0.98,"fill":"#30426f","filter":null}}`, + styleGloom: `{"#map":{"background-color":"#000000","filter":null,"data-filter":null},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0.3,"opacity":1,"fill-opacity":1,"filter":null},"#biomes":{"opacity":null,"filter":"url(#blur5)","mask":"url(#land)"},"#stateBorders":{"opacity":1,"stroke":"#56566d","stroke-width":1,"stroke-dasharray":2,"stroke-linecap":"butt","filter":""},"#provinceBorders":{"opacity":1,"stroke":"#56566d","stroke-width":0.3,"stroke-dasharray":".7 1","stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.1,"filter":null,"mask":null},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":14,"font-size":14,"stroke":"#4a4a4a","stroke-width":1,"stroke-dasharray":6,"stroke-linecap":null,"filter":"","mask":""},"#compass":{"opacity":0.6,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":"translate(100 100) scale(0.3)"},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":1,"filter":null},"#cults":{"opacity":0.7,"stroke":"#777777","stroke-width":1.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#e0e0e0","filter":null},"#markers":{"opacity":0.8,"rescale":1,"filter":"url(#dropShadow05)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0.1,"fill":"#003dff","filter":null},"#population":{"opacity":null,"stroke-width":1.6,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null},"#rural":{"stroke":"#0000aa"},"#urban":{"stroke":"#9d0000"},"#freshwater":{"opacity":0.5,"fill":"#a6c1fd","stroke":"#5f799d","stroke-width":0.7,"filter":null},"#salt":{"opacity":0.5,"fill":"#409b8a","stroke":"#388985","stroke-width":0.7,"filter":null},"#sinkhole":{"opacity":1,"fill":"#5bc9fd","stroke":"#53a3b0","stroke-width":0.7,"filter":null},"#frozen":{"opacity":0.95,"fill":"#cdd4e7","stroke":"#cfe0eb","stroke-width":0,"filter":null},"#lava":{"opacity":0.7,"fill":"#90270d","stroke":"#f93e0c","stroke-width":2,"filter":"url(#crumpled)"},"#dry":{"opacity":0.7,"fill":"#c9bfa7","stroke":"#8e816f","stroke-width":0.7,"filter":null},"#sea_island":{"opacity":0.6,"stroke":"#1f3846","stroke-width":0.7,"filter":"url(#dropShadow)","auto-filter":1},"#lake_island":{"opacity":1,"stroke":"#7c8eaf","stroke-width":0.35,"filter":null},"#terrain":{"opacity":0.9,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":null,"filter":"","fill":"#779582"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":1,"stroke":"#8b4418","stroke-width":0.9,"stroke-dasharray":"2 3","stroke-linecap":"round","filter":"","mask":null},"#trails":{"opacity":1,"stroke":"#844017","stroke-width":0.2,"stroke-dasharray":".5 1","stroke-linecap":"round","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#5e1865","stroke-width":0.6,"stroke-dasharray":"1.2 2.4","stroke-linecap":"round","filter":null,"mask":null},"#regions":{"opacity":0.4,"filter":"url(#dropShadow)"},"#statesHalo":{"opacity":1,"data-width":10.2,"stroke-width":10.2},"#provs":{"opacity":0.5,"filter":""},"#temperature":{"opacity":1,"font-size":"11px","fill":"#62001b","fill-opacity":0.3,"stroke":null,"stroke-width":2,"stroke-dasharray":2,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.9,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#dropShadow05)"},"#emblems": {"opacity":0.6,"stroke-width":0.5,"filter":null},"#texture":{"opacity":null,"filter":null,"mask":"url(#land)"},"#textureImage":{"x":0,"y":0},"#zones":{"opacity":0.5,"stroke":"#333333","stroke-width":0,"stroke-dasharray":null,"stroke-linecap":"butt","filter":"url(#dropShadow01)","mask":null},"#ocean":{"opacity":1},"#oceanLayers":{"filter":null,"layers":"-6,-4,-2"},"#oceanBase":{"fill":"#4e6964"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#pattern3)"},"#terrs":{"opacity":1,"scheme":"bright","terracing":0,"skip":0,"relax":1,"curve":1,"filter":"url(#filter-grayscale)","mask":"url(#land)"},"#legend":{"data-size":13,"font-size":13,"data-font":"Almendra+SC","font-family":"Almendra SC","stroke":"#812929","stroke-width":2.5,"stroke-dasharray":"0 4 10 4","stroke-linecap":"round","data-x":99,"data-y":93,"data-columns":8},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#3e3e4b","data-size":7,"font-size":7,"data-font":"Bitter","font-family":"Bitter"},"#burgIcons > #cities":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":2,"stroke":"#444444","stroke-width":0.25,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":0.8,"fill":"#ffffff","size":4,"stroke":"#3e3e4b","stroke-width":1},"#burgLabels > #towns":{"opacity":1,"fill":"#3e3e4b","data-size":3,"font-size":3,"data-font":"Bitter","font-family":"Bitter"},"#burgIcons > #towns":{"opacity":0.95,"fill":"#ffffff","fill-opacity":0.7,"size":0.8,"stroke":"#3e3e4b","stroke-width":0.2,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1.6,"stroke":"#3e3e4b","stroke-width":1.2},"#labels > #states":{"opacity":1,"fill":"#4e4e4e","stroke":"#b5b5b5","stroke-width":0,"data-size":22,"font-size":22,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":""},"#labels > #addedLabels":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#fogging":{"opacity":0.98,"fill":"#1b1423","filter":null}}`, + styleClean: `{"#map":{"background-color":"#000000","filter":null,"data-filter":null},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0,"opacity":1,"fill-opacity":1,"filter":null},"#biomes":{"opacity":0.5,"filter":"url(#blur7)","mask":"url(#land)"},"#stateBorders":{"opacity":0.8,"stroke":"#414141","stroke-width":0.7,"stroke-dasharray":0,"stroke-linecap":"butt","filter":""},"#provinceBorders":{"opacity":0.8,"stroke":"#414141","stroke-width":0.45,"stroke-dasharray":1,"stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.09,"filter":null,"mask":"url(#land)"},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":12,"font-size":12,"stroke":"#414141","stroke-width":0.45,"stroke-dasharray":3,"stroke-linecap":null,"filter":null,"mask":null},"#compass":{"opacity":0.8,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":null},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":0.7,"filter":null},"#cults":{"opacity":0.6,"stroke":"#777777","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#eeedeb","filter":null},"#markers":{"opacity":null,"rescale":null,"filter":"url(#dropShadow01)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0,"fill":"#0080ff","filter":null},"#population":{"opacity":null,"stroke-width":2.58,"stroke-dasharray":0,"stroke-linecap":"butt","filter":"url(#blur3)"},"#rural":{"stroke":"#ff0000"},"#urban":{"stroke":"#800000"},"#freshwater":{"opacity":0.5,"fill":"#aadaff","stroke":"#5f799d","stroke-width":0,"filter":null},"#salt":{"opacity":0.5,"fill":"#409b8a","stroke":"#388985","stroke-width":0.7,"filter":null},"#sinkhole":{"opacity":1,"fill":"#5bc9fd","stroke":"#53a3b0","stroke-width":0.7,"filter":null},"#frozen":{"opacity":0.95,"fill":"#cdd4e7","stroke":"#cfe0eb","stroke-width":0,"filter":null},"#lava":{"opacity":0.7,"fill":"#90270d","stroke":"#f93e0c","stroke-width":2,"filter":"url(#crumpled)"},"#dry":{"opacity":0.7,"fill":"#c9bfa7","stroke":"#8e816f","stroke-width":0.7,"filter":null},"#sea_island":{"opacity":0.6,"stroke":"#595959","stroke-width":0.4,"filter":"","auto-filter":0},"#lake_island":{"opacity":0,"stroke":"#7c8eaf","stroke-width":0,"filter":null},"#terrain":{"opacity":null,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":null,"filter":null,"fill":"#aadaff"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":0.9,"stroke":"#f6d068","stroke-width":0.7,"stroke-dasharray":0,"stroke-linecap":"inherit","filter":null,"mask":null},"#trails":{"opacity":1,"stroke":"#ffffff","stroke-width":0.25,"stroke-dasharray":"","stroke-linecap":"round","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#4f82c6","stroke-width":0.45,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null,"mask":"url(#water)"},"#regions":{"opacity":0.4,"filter":null},"#statesHalo":{"opacity":0,"data-width":null,"stroke-width":0},"#provs":{"opacity":0.6,"filter":null},"#temperature":{"opacity":null,"font-size":"8px","fill":"#000000","fill-opacity":0.3,"stroke":null,"stroke-width":1.8,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.9,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#dropShadow01)"},"#emblems":{"opacity":1,"stroke-width":1,"filter":null},"#texture":{"opacity":null,"filter":null,"mask":"url(#land)"},"#textureImage":{},"#zones":{"opacity":0.7,"stroke":"#ff6262","stroke-width":0,"stroke-dasharray":"","stroke-linecap":"butt","filter":null,"mask":null},"#ocean":{"opacity":null},"#oceanLayers":{"filter":"","layers":"none"},"#oceanBase":{"fill":"#aadaff"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#emptyImage)"},"#terrs":{"opacity":0.5,"scheme":"bright","terracing":0,"skip":5,"relax":0,"curve":0,"filter":"","mask":"url(#land)"},"#legend":{"data-size":12.74,"font-size":12.74,"data-font":"Arial","font-family":"Arial","stroke":"#909090","stroke-width":1.13,"stroke-dasharray":0,"stroke-linecap":"round","data-x":98.39,"data-y":12.67,"data-columns":null},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#414141","data-size":7,"font-size":7,"data-font":"Arial","font-family":"Arial"},"#burgIcons > #cities":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":1,"stroke":"#3e3e4b","stroke-width":0.24,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":1,"fill":"#ffffff","size":2,"stroke":"#303030","stroke-width":1.7},"#burgLabels > #towns":{"opacity":1,"fill":"#414141","data-size":3,"font-size":3,"data-font":"Arial","font-family":"Arial"},"#burgIcons > #towns":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":0.5,"stroke":"#3e3e4b","stroke-width":0.12,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1,"stroke":"#3e3e4b","stroke-width":1.06},"#labels > #states":{"opacity":1,"fill":"#292929","stroke":"#303030","stroke-width":0,"data-size":10,"font-size":10,"data-font":"Arial","font-family":"Arial","filter":null},"#labels > #addedLabels":{"opacity":1,"fill":"#414141","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Arial","font-family":"Arial","filter":null},"#fogging":{"opacity":1,"fill":"#ffffff","filter":null}}`, + styleMonochrome: `{"#map":{"background-color":"#000000","filter":"url(#filter-grayscale)","data-filter":"grayscale"},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0.3,"opacity":1,"fill-opacity":1,"filter":null},"#biomes":{"opacity":null,"filter":"url(#blur5)","mask":null},"#stateBorders":{"opacity":1,"stroke":"#56566d","stroke-width":1,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null},"#provinceBorders":{"opacity":1,"stroke":"#56566d","stroke-width":0.4,"stroke-dasharray":1,"stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.1,"filter":null,"mask":null},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":12,"font-size":12,"stroke":"#d4d4d4","stroke-width":1,"stroke-dasharray":5,"stroke-linecap":null,"filter":null,"mask":null},"#compass":{"opacity":0.8,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":null},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":0.7,"filter":null},"#cults":{"opacity":0.6,"stroke":"#777777","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#000000","filter":null},"#markers":{"opacity":null,"rescale":1,"filter":"url(#dropShadow01)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0.1,"fill":"#003dff","filter":null},"#population":{"opacity":null,"stroke-width":1.6,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null},"#rural":{"stroke":"#0000ff"},"#urban":{"stroke":"#ff0000"},"#freshwater":{"opacity":1,"fill":"#000000","stroke":"#515151","stroke-width":0,"filter":null},"#salt":{"opacity":1,"fill":"#000000","stroke":"#484848","stroke-width":0,"filter":null},"#sinkhole":{"opacity":1,"fill":"#000000","stroke":"#5f5f5f","stroke-width":0.5,"filter":null},"#frozen":{"opacity":1,"fill":"#000000","stroke":"#6f6f6f","stroke-width":0,"filter":null},"#lava":{"opacity":1,"fill":"#000000","stroke":"#5d5d5d","stroke-width":0,"filter":""},"#sea_island":{"opacity":1,"stroke":"#1f3846","stroke-width":0,"filter":"","auto-filter":0},"#lake_island":{"opacity":0,"stroke":"#7c8eaf","stroke-width":0,"filter":null},"#terrain":{"opacity":null,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":0.2,"filter":"url(#blur1)","fill":"#000000"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":0.9,"stroke":"#d06324","stroke-width":0.7,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null,"mask":null},"#trails":{"opacity":0.9,"stroke":"#d06324","stroke-width":0.25,"stroke-dasharray":".8 1.6","stroke-linecap":"butt","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#ffffff","stroke-width":0.45,"stroke-dasharray":"1 2","stroke-linecap":"round","filter":null,"mask":null},"#regions":{"opacity":0.4,"filter":null},"#statesHalo":{"opacity":1,"data-width":10,"stroke-width":10},"#provs":{"opacity":0.6,"filter":null},"#temperature":{"opacity":null,"font-size":"8px","fill":"#000000","fill-opacity":0.3,"stroke":null,"stroke-width":1.8,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.9,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#dropShadow05)"},"#texture":{"opacity":1,"filter":null,"mask":"url(#land)"},"#emblems": {"opacity": 0.5,"stroke-width": 0.5,"filter": null},"#textureImage":{},"#zones":{"opacity":0.6,"stroke":"#333333","stroke-width":0,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null,"mask":null},"#ocean":{"opacity":0},"#oceanLayers":{"filter":null,"layers":"none"},"#oceanBase":{"fill":"#000000"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#emptyImage)"},"#terrs":{"opacity":1,"scheme":"monochrome","terracing":0,"skip":5,"relax":0,"curve":0,"filter":"url(#blur3)","mask":"url(#land)"},"#legend":{"data-size":13,"font-size":13,"data-font":"Almendra+SC","font-family":"Almendra SC","stroke":"#812929","stroke-width":2.5,"stroke-dasharray":"0 4 10 4","stroke-linecap":"round","data-x":99,"data-y":93,"data-columns":8},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#3e3e4b","data-size":7,"font-size":7,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #cities":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":1,"stroke":"#3e3e4b","stroke-width":0.24,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":1,"fill":"#ffffff","size":2,"stroke":"#3e3e4b","stroke-width":1.2},"#burgLabels > #towns":{"opacity":1,"fill":"#3e3e4b","data-size":4,"font-size":4,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #towns":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":0.5,"stroke":"#3e3e4b","stroke-width":0.12,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1,"stroke":"#3e3e4b","stroke-width":1.2},"#labels > #states":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":22,"font-size":22,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#labels > #addedLabels":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#fogging":{"opacity":0.98,"fill":"#30426f","filter":null}}` } // apply default or custom style settings on load function applyStyleOnLoad() { - addDefaulsStyles(); // add FMG system styles to localStorage - svg.select("defs").append("style").text(armiesStyle()); // add armies style - const preset = localStorage.getItem("presetStyle"); - const style = preset ? localStorage.getItem(preset) : null; + const style = preset && (defaultStyles[preset] || localStorage.getItem(preset)); if (preset && style && JSON.isValid(style)) { applyStyle(JSON.parse(style)); @@ -683,42 +709,21 @@ function applyStyleOnLoad() { stylePreset.value = preset; stylePreset.dataset.old = preset; } else { + if (preset && preset !== "styleDefault" && ERROR) console.error(`Style preset ${preset} is not available in localStorage, applying default style`); stylePreset.value = "styleDefault"; stylePreset.dataset.old = preset; applyDefaultStyle(); } } -function addDefaulsStyles() { - if (!localStorage.getItem("styleClean")) { - const clean = `{"#map":{"background-color":"#000000","filter":null,"data-filter":null},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0,"opacity":1,"fill-opacity":1,"filter":null},"#biomes":{"opacity":0.5,"filter":"url(#blur7)","mask":"url(#land)"},"#stateBorders":{"opacity":0.8,"stroke":"#414141","stroke-width":0.7,"stroke-dasharray":0,"stroke-linecap":"butt","filter":""},"#provinceBorders":{"opacity":0.8,"stroke":"#414141","stroke-width":0.45,"stroke-dasharray":1,"stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.09,"filter":null,"mask":"url(#land)"},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":12,"font-size":12,"stroke":"#414141","stroke-width":0.45,"stroke-dasharray":3,"stroke-linecap":null,"filter":null,"mask":null},"#compass":{"opacity":0.8,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":null},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":0.7,"filter":null},"#cults":{"opacity":0.6,"stroke":"#777777","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#eeedeb","filter":null},"#markers":{"opacity":null,"rescale":null,"filter":"url(#dropShadow01)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0,"fill":"#0080ff","filter":null},"#population":{"opacity":null,"stroke-width":2.58,"stroke-dasharray":0,"stroke-linecap":"butt","filter":"url(#blur3)"},"#rural":{"stroke":"#ff0000"},"#urban":{"stroke":"#800000"},"#freshwater":{"opacity":0.5,"fill":"#aadaff","stroke":"#5f799d","stroke-width":0,"filter":null},"#salt":{"opacity":0.5,"fill":"#409b8a","stroke":"#388985","stroke-width":0.7,"filter":null},"#sinkhole":{"opacity":1,"fill":"#5bc9fd","stroke":"#53a3b0","stroke-width":0.7,"filter":null},"#frozen":{"opacity":0.95,"fill":"#cdd4e7","stroke":"#cfe0eb","stroke-width":0,"filter":null},"#lava":{"opacity":0.7,"fill":"#90270d","stroke":"#f93e0c","stroke-width":2,"filter":"url(#crumpled)"},"#dry":{"opacity":0.7,"fill":"#c9bfa7","stroke":"#8e816f","stroke-width":0.7,"filter":null},"#sea_island":{"opacity":0.6,"stroke":"#595959","stroke-width":0.4,"filter":"","auto-filter":0},"#lake_island":{"opacity":0,"stroke":"#7c8eaf","stroke-width":0,"filter":null},"#terrain":{"opacity":null,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":null,"filter":null,"fill":"#aadaff"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":0.9,"stroke":"#f6d068","stroke-width":0.7,"stroke-dasharray":0,"stroke-linecap":"inherit","filter":null,"mask":null},"#trails":{"opacity":1,"stroke":"#ffffff","stroke-width":0.25,"stroke-dasharray":"","stroke-linecap":"round","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#4f82c6","stroke-width":0.45,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null,"mask":"url(#water)"},"#regions":{"opacity":0.4,"filter":null},"#statesHalo":{"opacity":0,"data-width":null,"stroke-width":0},"#provs":{"opacity":0.6,"filter":null},"#temperature":{"opacity":null,"font-size":"8px","fill":"#000000","fill-opacity":0.3,"stroke":null,"stroke-width":1.8,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.8,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#dropShadow01)"},"#texture":{"opacity":null,"filter":null,"mask":"url(#land)"},"#textureImage":{},"#zones":{"opacity":0.7,"stroke":"#ff6262","stroke-width":0,"stroke-dasharray":"","stroke-linecap":"butt","filter":null,"mask":null},"#ocean":{"opacity":null},"#oceanLayers":{"filter":"","layers":"none"},"#oceanBase":{"fill":"#aadaff"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#emptyImage)"},"#terrs":{"opacity":0.5,"scheme":"bright","terracing":0,"skip":5,"relax":0,"curve":0,"filter":"","mask":"url(#land)"},"#legend":{"data-size":12.74,"font-size":12.74,"data-font":"Arial","font-family":"Arial","stroke":"#909090","stroke-width":1.13,"stroke-dasharray":0,"stroke-linecap":"round","data-x":98.39,"data-y":12.67,"data-columns":null},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#414141","data-size":7,"font-size":7,"data-font":"Arial","font-family":"Arial"},"#burgIcons > #cities":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":1,"stroke":"#3e3e4b","stroke-width":0.24,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":1,"fill":"#ffffff","size":2,"stroke":"#303030","stroke-width":1.7},"#burgLabels > #towns":{"opacity":1,"fill":"#414141","data-size":3,"font-size":3,"data-font":"Arial","font-family":"Arial"},"#burgIcons > #towns":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":0.5,"stroke":"#3e3e4b","stroke-width":0.12,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1,"stroke":"#3e3e4b","stroke-width":1.06},"#labels > #states":{"opacity":1,"fill":"#292929","stroke":"#303030","stroke-width":0,"data-size":10,"font-size":10,"data-font":"Arial","font-family":"Arial","filter":null},"#labels > #addedLabels":{"opacity":1,"fill":"#414141","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Arial","font-family":"Arial","filter":null},"#fogging":{"opacity":1,"fill":"#ffffff","filter":null}}`; - localStorage.setItem("styleClean", clean); - } - - if (!localStorage.getItem("styleGloom")) { - const gloom = `{"#map":{"background-color":"#000000","filter":null,"data-filter":null},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0.3,"opacity":1,"fill-opacity":1,"filter":null},"#biomes":{"opacity":null,"filter":"url(#blur5)","mask":"url(#land)"},"#stateBorders":{"opacity":1,"stroke":"#56566d","stroke-width":1,"stroke-dasharray":2,"stroke-linecap":"butt","filter":""},"#provinceBorders":{"opacity":1,"stroke":"#56566d","stroke-width":0.3,"stroke-dasharray":".7 1","stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.1,"filter":null,"mask":null},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":14,"font-size":14,"stroke":"#4a4a4a","stroke-width":1,"stroke-dasharray":6,"stroke-linecap":null,"filter":"","mask":""},"#compass":{"opacity":0.6,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":"translate(100 100) scale(0.3)"},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":1,"filter":null},"#cults":{"opacity":0.7,"stroke":"#777777","stroke-width":1.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#e0e0e0","filter":null},"#markers":{"opacity":0.8,"rescale":1,"filter":"url(#dropShadow05)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0.1,"fill":"#003dff","filter":null},"#population":{"opacity":null,"stroke-width":1.6,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null},"#rural":{"stroke":"#0000aa"},"#urban":{"stroke":"#9d0000"},"#freshwater":{"opacity":0.5,"fill":"#a6c1fd","stroke":"#5f799d","stroke-width":0.7,"filter":null},"#salt":{"opacity":0.5,"fill":"#409b8a","stroke":"#388985","stroke-width":0.7,"filter":null},"#sinkhole":{"opacity":1,"fill":"#5bc9fd","stroke":"#53a3b0","stroke-width":0.7,"filter":null},"#frozen":{"opacity":0.95,"fill":"#cdd4e7","stroke":"#cfe0eb","stroke-width":0,"filter":null},"#lava":{"opacity":0.7,"fill":"#90270d","stroke":"#f93e0c","stroke-width":2,"filter":"url(#crumpled)"},"#dry":{"opacity":0.7,"fill":"#c9bfa7","stroke":"#8e816f","stroke-width":0.7,"filter":null},"#sea_island":{"opacity":0.6,"stroke":"#1f3846","stroke-width":0.7,"filter":"url(#dropShadow)","auto-filter":1},"#lake_island":{"opacity":1,"stroke":"#7c8eaf","stroke-width":0.35,"filter":null},"#terrain":{"opacity":0.9,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":null,"filter":"","fill":"#779582"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":1,"stroke":"#8b4418","stroke-width":0.9,"stroke-dasharray":"2 3","stroke-linecap":"round","filter":"","mask":null},"#trails":{"opacity":1,"stroke":"#844017","stroke-width":0.2,"stroke-dasharray":".5 1","stroke-linecap":"round","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#5e1865","stroke-width":0.6,"stroke-dasharray":"1.2 2.4","stroke-linecap":"round","filter":null,"mask":null},"#regions":{"opacity":0.4,"filter":"url(#dropShadow)"},"#statesHalo":{"opacity":1,"data-width":10.2,"stroke-width":10.2},"#provs":{"opacity":0.5,"filter":""},"#temperature":{"opacity":1,"font-size":"11px","fill":"#62001b","fill-opacity":0.3,"stroke":null,"stroke-width":2,"stroke-dasharray":2,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.8,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#dropShadow05)"},"#texture":{"opacity":null,"filter":null,"mask":"url(#land)"},"#textureImage":{"x":0,"y":0},"#zones":{"opacity":0.5,"stroke":"#333333","stroke-width":0,"stroke-dasharray":null,"stroke-linecap":"butt","filter":"url(#dropShadow01)","mask":null},"#ocean":{"opacity":1},"#oceanLayers":{"filter":null,"layers":"-6,-4,-2"},"#oceanBase":{"fill":"#4e6964"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#pattern3)"},"#terrs":{"opacity":1,"scheme":"bright","terracing":0,"skip":0,"relax":1,"curve":1,"filter":"url(#filter-grayscale)","mask":"url(#land)"},"#legend":{"data-size":13,"font-size":13,"data-font":"Almendra+SC","font-family":"Almendra SC","stroke":"#812929","stroke-width":2.5,"stroke-dasharray":"0 4 10 4","stroke-linecap":"round","data-x":99,"data-y":93,"data-columns":8},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#3e3e4b","data-size":7,"font-size":7,"data-font":"Bitter","font-family":"Bitter"},"#burgIcons > #cities":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":2,"stroke":"#444444","stroke-width":0.25,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":0.8,"fill":"#ffffff","size":4,"stroke":"#3e3e4b","stroke-width":1},"#burgLabels > #towns":{"opacity":1,"fill":"#3e3e4b","data-size":3,"font-size":3,"data-font":"Bitter","font-family":"Bitter"},"#burgIcons > #towns":{"opacity":0.95,"fill":"#ffffff","fill-opacity":0.7,"size":0.8,"stroke":"#3e3e4b","stroke-width":0.2,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1.6,"stroke":"#3e3e4b","stroke-width":1.2},"#labels > #states":{"opacity":1,"fill":"#4e4e4e","stroke":"#b5b5b5","stroke-width":0,"data-size":22,"font-size":22,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":""},"#labels > #addedLabels":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#fogging":{"opacity":0.98,"fill":"#1b1423","filter":null}}`; - localStorage.setItem("styleGloom", gloom); - } - - if (!localStorage.getItem("styleAncient")) { - const ancient = `{"#map":{"background-color":"#000000","filter":"url(#filter-sepia)","data-filter":"sepia"},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0.05,"opacity":0.8,"fill-opacity":0.8,"filter":null},"#biomes":{"opacity":null,"filter":null,"mask":null},"#stateBorders":{"opacity":0.8,"stroke":"#56566d","stroke-width":1,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null},"#provinceBorders":{"opacity":0.8,"stroke":"#56566d","stroke-width":0.2,"stroke-dasharray":1,"stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.1,"filter":null,"mask":null},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":12,"font-size":12,"stroke":"#d4d4d4","stroke-width":1,"stroke-dasharray":5,"stroke-linecap":null,"filter":null,"mask":null},"#compass":{"opacity":0.8,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":"translate(80 80) scale(.25)"},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":0.7,"filter":null},"#cults":{"opacity":0.6,"stroke":"#777777","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#eee9d7","filter":null},"#markers":{"opacity":null,"rescale":1,"filter":"url(#dropShadow01)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0.1,"fill":"#003dff","filter":null},"#population":{"opacity":null,"stroke-width":1.6,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null},"#rural":{"stroke":"#0000ff"},"#urban":{"stroke":"#ff0000"},"#freshwater":{"opacity":0.5,"fill":"#a6c1fd","stroke":"#5f799d","stroke-width":0.7,"filter":null},"#salt":{"opacity":0.5,"fill":"#409b8a","stroke":"#388985","stroke-width":0.7,"filter":null},"#sinkhole":{"opacity":1,"fill":"#5bc9fd","stroke":"#53a3b0","stroke-width":0.7,"filter":null},"#frozen":{"opacity":0.95,"fill":"#cdd4e7","stroke":"#cfe0eb","stroke-width":0,"filter":null},"#lava":{"opacity":0.7,"fill":"#90270d","stroke":"#f93e0c","stroke-width":2,"filter":"url(#crumpled)"},"#dry":{"opacity":0.7,"fill":"#c9bfa7","stroke":"#8e816f","stroke-width":0.7,"filter":null},"#sea_island":{"opacity":0.5,"stroke":"#1f3846","stroke-width":0.7,"filter":"url(#dropShadow)","auto-filter":1},"#lake_island":{"opacity":1,"stroke":"#7c8eaf","stroke-width":0.35,"filter":null},"#terrain":{"opacity":null,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":null,"filter":null,"fill":"#5d97bb"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":0.8,"stroke":"#2e1607","stroke-width":1.23,"stroke-dasharray":3,"stroke-linecap":"inherit","filter":null,"mask":null},"#trails":{"opacity":0.8,"stroke":"#331809","stroke-width":0.5,"stroke-dasharray":"1 2","stroke-linecap":"butt","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#ffffff","stroke-width":0.8,"stroke-dasharray":"1 2","stroke-linecap":"round","filter":null,"mask":null},"#regions":{"opacity":0.4,"filter":""},"#statesHalo":{"opacity":1,"data-width":10,"stroke-width":10},"#provs":{"opacity":0.6,"filter":null},"#temperature":{"opacity":null,"font-size":"8px","fill":"#000000","fill-opacity":0.3,"stroke":null,"stroke-width":1.8,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.7,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#outline)"},"#texture":{"opacity":null,"filter":null,"mask":"url(#land)"},"#textureImage":{},"#zones":{"opacity":0.6,"stroke":"#333333","stroke-width":0,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null,"mask":null},"#ocean":{"opacity":1},"#oceanLayers":{"filter":"url(#blur5)","layers":"-6,-4,-2"},"#oceanBase":{"fill":"#a7a01f"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#pattern1)"},"#terrs":{"opacity":null,"scheme":"light","terracing":0,"skip":0,"relax":0,"curve":0,"filter":null,"mask":"url(#land)"},"#legend":{"data-size":13,"font-size":13,"data-font":"Almendra+SC","font-family":"Almendra SC","stroke":"#812929","stroke-width":2.5,"stroke-dasharray":"0 4 10 4","stroke-linecap":"round","data-x":99,"data-y":93,"data-columns":8},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#3e3e4b","data-size":8,"font-size":8,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #cities":{"opacity":1,"fill":"#fdfab9","fill-opacity":0.7,"size":1,"stroke":"#54251d","stroke-width":0.3,"stroke-dasharray":".3 .4","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":1,"fill":"#ffffff","size":2,"stroke":"#3e3e4b","stroke-width":1.2},"#burgLabels > #towns":{"opacity":1,"fill":"#3e3e4b","data-size":4,"font-size":4,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #towns":{"opacity":1,"fill":"#fef4d8","fill-opacity":0.7,"size":0.5,"stroke":"#463124","stroke-width":0.12,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1,"stroke":"#3e3e4b","stroke-width":1.2},"#labels > #states":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":22,"font-size":22,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#labels > #addedLabels":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#fogging":{"opacity":0.98,"fill":"#30426f","filter":null}}`; - localStorage.setItem("styleAncient", ancient); - } - - if (!localStorage.getItem("styleMonochrome")) { - const monochrome = `{"#map":{"background-color":"#000000","filter":"url(#filter-grayscale)","data-filter":"grayscale"},"#armies":{"font-size":6,"box-size":3,"stroke":"#000","stroke-width":0.3,"opacity":1,"fill-opacity":1,"filter":null},"#biomes":{"opacity":null,"filter":"url(#blur5)","mask":null},"#stateBorders":{"opacity":1,"stroke":"#56566d","stroke-width":1,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null},"#provinceBorders":{"opacity":1,"stroke":"#56566d","stroke-width":0.4,"stroke-dasharray":1,"stroke-linecap":"butt","filter":null},"#cells":{"opacity":null,"stroke":"#808080","stroke-width":0.1,"filter":null,"mask":null},"#gridOverlay":{"opacity":0.8,"size":10,"type":"pointyHex","stroke":"#808080","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"transform":null,"filter":null,"mask":null},"#coordinates":{"opacity":1,"data-size":12,"font-size":12,"stroke":"#d4d4d4","stroke-width":1,"stroke-dasharray":5,"stroke-linecap":null,"filter":null,"mask":null},"#compass":{"opacity":0.8,"transform":null,"filter":null,"mask":"url(#water)","shape-rendering":"optimizespeed"},"#rose":{"transform":null},"#relig":{"opacity":0.7,"stroke":"#404040","stroke-width":0.7,"filter":null},"#cults":{"opacity":0.6,"stroke":"#777777","stroke-width":0.5,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#landmass":{"opacity":1,"fill":"#000000","filter":null},"#markers":{"opacity":null,"rescale":1,"filter":"url(#dropShadow01)"},"#prec":{"opacity":null,"stroke":"#000000","stroke-width":0.1,"fill":"#003dff","filter":null},"#population":{"opacity":null,"stroke-width":1.6,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null},"#rural":{"stroke":"#0000ff"},"#urban":{"stroke":"#ff0000"},"#freshwater":{"opacity":1,"fill":"#000000","stroke":"#515151","stroke-width":0,"filter":null},"#salt":{"opacity":1,"fill":"#000000","stroke":"#484848","stroke-width":0,"filter":null},"#sinkhole":{"opacity":1,"fill":"#000000","stroke":"#5f5f5f","stroke-width":0.5,"filter":null},"#frozen":{"opacity":1,"fill":"#000000","stroke":"#6f6f6f","stroke-width":0,"filter":null},"#lava":{"opacity":1,"fill":"#000000","stroke":"#5d5d5d","stroke-width":0,"filter":""},"#sea_island":{"opacity":1,"stroke":"#1f3846","stroke-width":0,"filter":"","auto-filter":0},"#lake_island":{"opacity":0,"stroke":"#7c8eaf","stroke-width":0,"filter":null},"#terrain":{"opacity":null,"set":"simple","size":1,"density":0.4,"filter":null,"mask":null},"#rivers":{"opacity":0.2,"filter":"url(#blur1)","fill":"#000000"},"#ruler":{"opacity":null,"filter":null},"#roads":{"opacity":0.9,"stroke":"#d06324","stroke-width":0.7,"stroke-dasharray":2,"stroke-linecap":"butt","filter":null,"mask":null},"#trails":{"opacity":0.9,"stroke":"#d06324","stroke-width":0.25,"stroke-dasharray":".8 1.6","stroke-linecap":"butt","filter":null,"mask":null},"#searoutes":{"opacity":0.8,"stroke":"#ffffff","stroke-width":0.45,"stroke-dasharray":"1 2","stroke-linecap":"round","filter":null,"mask":null},"#regions":{"opacity":0.4,"filter":null},"#statesHalo":{"opacity":1,"data-width":10,"stroke-width":10},"#provs":{"opacity":0.6,"filter":null},"#temperature":{"opacity":null,"font-size":"8px","fill":"#000000","fill-opacity":0.3,"stroke":null,"stroke-width":1.8,"stroke-dasharray":null,"stroke-linecap":null,"filter":null},"#ice":{"opacity":0.8,"fill":"#e8f0f6","stroke":"#e8f0f6","stroke-width":1,"filter":"url(#dropShadow05)"},"#texture":{"opacity":1,"filter":null,"mask":"url(#land)"},"#textureImage":{},"#zones":{"opacity":0.6,"stroke":"#333333","stroke-width":0,"stroke-dasharray":null,"stroke-linecap":"butt","filter":null,"mask":null},"#ocean":{"opacity":0},"#oceanLayers":{"filter":null,"layers":"none"},"#oceanBase":{"fill":"#000000"},"#oceanPattern":{"opacity":null},"#oceanicPattern":{"filter":"url(#emptyImage)"},"#terrs":{"opacity":1,"scheme":"monochrome","terracing":0,"skip":5,"relax":0,"curve":0,"filter":"url(#blur3)","mask":"url(#land)"},"#legend":{"data-size":13,"font-size":13,"data-font":"Almendra+SC","font-family":"Almendra SC","stroke":"#812929","stroke-width":2.5,"stroke-dasharray":"0 4 10 4","stroke-linecap":"round","data-x":99,"data-y":93,"data-columns":8},"#legendBox":{},"#burgLabels > #cities":{"opacity":1,"fill":"#3e3e4b","data-size":7,"font-size":7,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #cities":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":1,"stroke":"#3e3e4b","stroke-width":0.24,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #cities":{"opacity":1,"fill":"#ffffff","size":2,"stroke":"#3e3e4b","stroke-width":1.2},"#burgLabels > #towns":{"opacity":1,"fill":"#3e3e4b","data-size":4,"font-size":4,"data-font":"Almendra+SC","font-family":"Almendra SC"},"#burgIcons > #towns":{"opacity":1,"fill":"#ffffff","fill-opacity":0.7,"size":0.5,"stroke":"#3e3e4b","stroke-width":0.12,"stroke-dasharray":"","stroke-linecap":"butt"},"#anchors > #towns":{"opacity":1,"fill":"#ffffff","size":1,"stroke":"#3e3e4b","stroke-width":1.2},"#labels > #states":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":22,"font-size":22,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#labels > #addedLabels":{"opacity":1,"fill":"#3e3e4b","stroke":"#3a3a3a","stroke-width":0,"data-size":18,"font-size":18,"data-font":"Almendra+SC","font-family":"Almendra SC","filter":null},"#fogging":{"opacity":0.98,"fill":"#30426f","filter":null}}`; - localStorage.setItem("styleMonochrome", monochrome); - } -} - // set default style function applyDefaultStyle() { armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", .3); biomes.attr("opacity", null).attr("filter", null).attr("mask", null); - ice.attr("opacity", .8).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)"); + ice.attr("opacity", .9).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)"); stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null); - provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .2).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt").attr("filter", null); + provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "0 2").attr("stroke-linecap", "round").attr("filter", null); cells.attr("opacity", null).attr("stroke", "#808080").attr("stroke-width", .1).attr("filter", null).attr("mask", null); gridOverlay.attr("opacity", .8).attr("type", "pointyHex").attr("size", 20).attr("stroke", "#808080").attr("stroke-width", .5).attr("stroke-dasharray", null).attr("transform", null).attr("filter", null).attr("mask", null); @@ -795,6 +800,7 @@ function applyDefaultStyle() { labels.select("#addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null); fogging.attr("opacity", .98).attr("fill", "#30426f"); + emblems.attr("opacity", .9).attr("stroke-width", 1).attr("filter", null); } // apply style settings in JSON @@ -817,10 +823,16 @@ function changeStylePreset(preset) { $("#alert").dialog({resizable: false, title: "Change style preset", width: "23em", buttons: { Change: function() { - const stored = localStorage.getItem(preset); - const style = JSON.isValid(stored) ? JSON.parse(stored) : null; - if (preset === "styleDefault" || !style) applyDefaultStyle(); else applyStyle(style); - if (preset !== "styleDefault" && !style) tip("Cannot parse stored style JSON. Default style is applied", false, "error", 5000); + const customPreset = localStorage.getItem(preset); + if (customPreset) { + if (JSON.isValid(customPreset)) applyStyle(JSON.parse(customPreset)); + else { + tip("Cannot parse stored style JSON. Default style applied", false, "error", 5000); + applyDefaultStyle(); + } + } else if (defaultStyles[preset]) applyStyle(JSON.parse(defaultStyles[preset])); + else applyDefaultStyle(); + removeStyleButton.style.display = stylePreset.selectedOptions[0].dataset.system ? "none" : "inline-block"; updateElements(); // change elements selectStyleElement(); // re-select element to trigger values update @@ -924,6 +936,7 @@ function addStylePreset() { "#provs":["opacity", "filter"], "#temperature":["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#ice":["opacity", "fill", "stroke", "stroke-width", "filter"], + "#emblems":["opacity", "stroke-width", "filter"], "#texture":["opacity", "filter", "mask"], "#textureImage":["x", "y"], "#zones":["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"], @@ -1094,7 +1107,7 @@ function addFonts(url) { let family = rule.style.getPropertyValue('font-family'); let font = family.replace(/['"]+/g, '').replace(/ /g, "+"); let weight = rule.style.getPropertyValue('font-weight'); - if (weight !== "400") font += ":" + weight; + if (weight && weight !== "400") font += ":" + weight; if (fonts.indexOf(font) == -1) { fonts.push(font); fetched++ diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 15988bf8..51abcab0 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -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 @@ -60,9 +61,10 @@ function processFeatureRegeneration(event, button) { if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else if (button === "regenerateRivers") regenerateRivers(); else if (button === "regeneratePopulation") recalculatePopulation(); else - if (button === "regenerateBurgs") regenerateBurgs(); else if (button === "regenerateStates") regenerateStates(); else if (button === "regenerateProvinces") regenerateProvinces(); else + if (button === "regenerateBurgs") regenerateBurgs(); else + if (button === "regenerateEmblems") regenerateEmblems(); else if (button === "regenerateReligions") regenerateReligions(); else if (button === "regenerateCultures") regenerateCultures(); else if (button === "regenerateMilitary") regenerateMilitary(); else @@ -71,6 +73,26 @@ function processFeatureRegeneration(event, button) { if (button === "regenerateZones") regenerateZones(event); } +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(); @@ -90,6 +112,116 @@ function recalculatePopulation() { }); } +function regenerateStates() { + const localSeed = Math.floor(Math.random() * 1e9); // new random seed + Math.random = aleaPRNG(localSeed); + const burgs = pack.burgs.filter(b => b.i && !b.removed); + if (!burgs.length) { + tip("No burgs to generate states. Please create burgs first", false, "error"); + return; + } + if (burgs.length < +regionsInput.value) { + tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn"); + } + + // burg local ids sorted by a bit randomized population: + const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]); + const capitalsTree = d3.quadtree(); + + // turn all old capitals into towns + burgs.filter(b => b.capital).forEach(b => { + moveBurgToGroup(b.i, "towns"); + b.capital = 0; + }); + + // remove emblems + document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove()); + document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove()); + emblems.selectAll("use").remove(); + + unfog(); + + // if desired states number is 0 + if (regionsInput.value == 0) { + tip(`Cannot generate zero states. Please check the States Number option`, false, "warn"); + pack.states = pack.states.slice(0,1); // remove all except of neutrals + pack.states[0].diplomacy = []; // clear diplomacy + pack.provinces = [0]; // remove all provinces + pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data + borders.selectAll("path").remove(); // remove borders + regions.selectAll("path").remove(); // remove states fill + labels.select("#states").selectAll("text"); // remove state labels + defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths + + if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click(); + if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click(); + return; + } + + const neutral = pack.states[0].name; + const count = Math.min(+regionsInput.value, burgs.length); + let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals + pack.states = d3.range(count).map(i => { + if (!i) return {i, name: neutral}; + + let capital = null, x = 0, y = 0; + for (const i of sorted) { + capital = burgs[i]; + x = capital.x, y = capital.y; + if (capitalsTree.find(x, y, spacing) === undefined) break; + spacing = Math.max(spacing - 1, 1); + } + + capitalsTree.add([x, y]); + capital.capital = 1; + moveBurgToGroup(capital.i, "cities"); + + const culture = capital.culture; + const basename = capital.name.length < 9 && capital.cell%5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0); + const name = Names.getState(basename, culture); + const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]); + const type = nomadic ? "Nomadic" : pack.cultures[culture].type === "Nomadic" ? "Generic" : pack.cultures[culture].type; + const expansionism = rn(Math.random() * powerInput.value + 1, 1); + + const cultureType = pack.cultures[culture].type; + const coa = COA.generate(capital.coa, .3, null, cultureType); + coa.shield = capital.coa.shield; + + return {i, name, type, capital:capital.i, center:capital.cell, culture, expansionism, coa}; + }); + + BurgsAndStates.expandStates(); + BurgsAndStates.normalizeStates(); + BurgsAndStates.collectStatistics(); + BurgsAndStates.assignColors(); + BurgsAndStates.generateCampaigns(); + BurgsAndStates.generateDiplomacy(); + BurgsAndStates.defineStateForms(); + BurgsAndStates.generateProvinces(true); + if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); + if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); + BurgsAndStates.drawStateLabels(); + Military.generate(); + if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems + + if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click(); + if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click(); + if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click(); +} + +function regenerateProvinces() { + unfog(); + + BurgsAndStates.generateProvinces(true); + drawBorders(); + if (layerIsOn("toggleProvinces")) drawProvinces(); + + // remove emblems + document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove()); + emblems.selectAll("use").remove(); + if (layerIsOn("toggleEmblems")) drawEmblems(); +} + function regenerateBurgs() { const cells = pack.cells, states = pack.states; rankCells(); @@ -139,100 +271,65 @@ function regenerateBurgs() { BurgsAndStates.drawBurgs(); Routes.regenerate(); + // remove emblems + document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove()); + emblems.selectAll("use").remove(); + if (layerIsOn("toggleEmblems")) drawEmblems(); + if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click(); if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click(); } -function regenerateStates() { - Math.seedrandom(Math.floor(Math.random() * 1e9)); // new random seed - const burgs = pack.burgs.filter(b => b.i && !b.removed); - if (!burgs.length) { - tip("No burgs to generate states. Please create burgs first", false, "error"); - return; - } - if (burgs.length < +regionsInput.value) { - tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn"); - } +function regenerateEmblems() { + // remove old emblems + document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove()); + document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove()); + document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove()); + emblems.selectAll("use").remove(); - // burg local ids sorted by a bit randomized population: - const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]); - const capitalsTree = d3.quadtree(); - - // turn all old capitals into towns - burgs.filter(b => b.capital).forEach(b => { - moveBurgToGroup(b.i, "towns"); - b.capital = 0; + // generate new emblems + pack.states.forEach(state => { + if (!state.i || state.removed) return; + const cultureType = pack.cultures[state.culture].type; + state.coa = COA.generate(null, null, null, cultureType); + state.coa.shield = COA.getShield(state.culture, null); }); - unfog(); + pack.burgs.forEach(burg => { + if (!burg.i || burg.removed) return; + const state = pack.states[burg.state]; - // if desired states number is 0 - if (regionsInput.value == 0) { - tip(`Cannot generate zero states. Please check the States Number option`, false, "warn"); - pack.states = pack.states.slice(0,1); // remove all except of neutrals - pack.states[0].diplomacy = []; // clear diplomacy - pack.provinces = [0]; // remove all provinces - pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data - borders.selectAll("path").remove(); // remove borders - regions.selectAll("path").remove(); // remove states fill - labels.select("#states").selectAll("text"); // remove state labels - defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths + let kinship = .25; + if (burg.capital) kinship += .1; + else if (burg.port) kinship -= .1; + if (burg.culture !== state.culture) kinship -= .25; + burg.coa = COA.generate(state.coa, kinship, null, burg.type); + burg.coa.shield = COA.getShield(burg.culture, burg.state); + }); - if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click(); - if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click(); - return; - } + pack.provinces.forEach(province => { + if (!province.i || province.removed) return; + const parent = province.burg ? pack.burgs[province.burg] : pack.states[province.state]; - const neutral = pack.states[0].name; - const count = Math.min(+regionsInput.value, burgs.length); - let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals - pack.states = d3.range(count).map(i => { - if (!i) return {i, name: neutral}; - - let capital = null, x = 0, y = 0; - for (const i of sorted) { - capital = burgs[i]; - x = capital.x, y = capital.y; - if (capitalsTree.find(x, y, spacing) === undefined) break; - spacing = Math.max(spacing - 1, 1); + let dominion = false; + if (!province.burg) { + dominion = P(.2); + if (province.formName === "Colony") dominion = P(.95); else + if (province.formName === "Island") dominion = P(.6); else + if (province.formName === "Islands") dominion = P(.5); else + if (province.formName === "Territory") dominion = P(.4); else + if (province.formName === "Land") dominion = P(.3); } - capitalsTree.add([x, y]); - capital.capital = 1; - moveBurgToGroup(capital.i, "cities"); - - const culture = capital.culture; - const basename = capital.name.length < 9 && capital.cell%5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0); - const name = Names.getState(basename, culture); - const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]); - const type = nomadic ? "Nomadic" : pack.cultures[culture].type === "Nomadic" ? "Generic" : pack.cultures[culture].type; - const expansionism = rn(Math.random() * powerInput.value + 1, 1); - return {i, name, type, capital:capital.i, center:capital.cell, culture, expansionism}; + const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3); + const kinship = dominion ? 0 : nameByBurg ? .8 : .4; + const culture = pack.cells.culture[province.center]; + const type = BurgsAndStates.getType(province.center, parent.port); + province.coa = COA.generate(parent.coa, kinship, dominion, type); + province.coa.shield = COA.getShield(culture, province.state); }); - BurgsAndStates.expandStates(); - BurgsAndStates.normalizeStates(); - BurgsAndStates.collectStatistics(); - BurgsAndStates.assignColors(); - BurgsAndStates.generateCampaigns(); - BurgsAndStates.generateDiplomacy(); - BurgsAndStates.defineStateForms(); - BurgsAndStates.generateProvinces(true); - if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); - if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); - BurgsAndStates.drawStateLabels(); - Military.generate(); - - if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click(); - if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click(); - if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click(); -} - -function regenerateProvinces() { - unfog(); - BurgsAndStates.generateProvinces(true); - drawBorders(); - if (layerIsOn("toggleProvinces")) drawProvinces(); + if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems } function regenerateReligions() { diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js index fc3f93b6..913e4f77 100644 --- a/modules/ui/world-configurator.js +++ b/modules/ui/world-configurator.js @@ -2,10 +2,10 @@ function editWorld() { if (customization) return; $("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: "42em", buttons: { - "Whole World": () => applyPreset(100, 50), - "Northern": () => applyPreset(33, 25), - "Tropical": () => applyPreset(33, 50), - "Southern": () => applyPreset(33, 75), + "Whole World": () => applyWorldPreset(100, 50), + "Northern": () => applyWorldPreset(33, 25), + "Tropical": () => applyWorldPreset(33, 50), + "Southern": () => applyWorldPreset(33, 75), "Restore Winds": restoreDefaultWinds }, open: function() { const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button"); @@ -124,7 +124,7 @@ function editWorld() { if (update) updateWorld(); } - function applyPreset(size, lat) { + function applyWorldPreset(size, lat) { document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size; document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat; lock("mapSize"); diff --git a/modules/utils.js b/modules/utils.js index 005d0738..3e34336c 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -232,7 +232,7 @@ function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) { return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round); } -/** This is a description of the foo function. */ +// probability shorthand for floats function Pint(float) { return ~~float + +P(float % 1); } diff --git a/modules/voronoi.js b/modules/voronoi.js index 7889b91e..f5e4b45c 100644 --- a/modules/voronoi.js +++ b/modules/voronoi.js @@ -13,7 +13,6 @@ class Voronoi { 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 - // 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. diff --git a/run_python_server.bat b/run_python_server.bat new file mode 100644 index 00000000..b74d34c1 --- /dev/null +++ b/run_python_server.bat @@ -0,0 +1,3 @@ +start chrome.exe http://localhost:8000/ +@echo off +python -m http.server 8000 \ No newline at end of file