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