Icons for genres with backgrounds, text, and ability to specify text color and font family (#34)

This commit is contained in:
Simon J
2021-08-27 18:14:09 +10:00
committed by GitHub
parent d1f00f549c
commit 29493e090a
38 changed files with 902 additions and 144 deletions

View File

@@ -1,11 +1,24 @@
import libxmljs, { Element, Attribute } from "libxmljs2";
import _ from "underscore";
import { Clock, isChristmas, isCNY, isHalloween, isHoli, SystemClock } from "./clock";
import fs from "fs";
import {
Clock,
isChristmas,
isCNY,
isHalloween,
isHoli,
SystemClock,
} from "./clock";
import path from "path";
export type Transformation = {
viewPortIncreasePercent: number | undefined;
backgroundColor: string | undefined;
foregroundColor: string | undefined;
text: string | undefined;
fontColor: string | undefined;
fontFamily: string | undefined;
};
const SVG_NS = {
@@ -43,13 +56,23 @@ export interface Icon {
export class ColorOverridingIcon implements Icon {
rule: () => Boolean;
newColors: () => Pick<Transformation, "backgroundColor" | "foregroundColor">;
newColors: () => Partial<
Pick<
Transformation,
"backgroundColor" | "foregroundColor" | "fontColor" | "fontFamily"
>
>;
icon: Icon;
constructor(
icon: Icon,
rule: () => Boolean,
newColors: () => Pick<Transformation, "backgroundColor" | "foregroundColor">
newColors: () => Partial<
Pick<
Transformation,
"backgroundColor" | "foregroundColor" | "fontColor" | "fontFamily"
>
>
) {
this.icon = icon;
this.rule = rule;
@@ -74,6 +97,9 @@ export class SvgIcon implements Icon {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
text: undefined,
fontColor: undefined,
fontFamily: undefined,
}
) {
this.svg = svg;
@@ -106,26 +132,62 @@ export class SvgIcon implements Icon {
y: `${viewBox.minY}`,
width: `${Math.abs(viewBox.minX) + viewBox.width}`,
height: `${Math.abs(viewBox.minY) + viewBox.height}`,
style: `fill:${this.transformation.backgroundColor}`,
fill: this.transformation.backgroundColor,
})
);
}
if (this.transformation.foregroundColor) {
(xml.find("//svg:path", SVG_NS) as Element[]).forEach((path) =>
path.attr({ style: `fill:${this.transformation.foregroundColor}` })
(xml.find("//svg:path", SVG_NS) as Element[]).forEach((path) => {
if (path.attr("fill"))
path.attr({ stroke: this.transformation.foregroundColor! });
else path.attr({ fill: this.transformation.foregroundColor! });
});
}
if (this.transformation.text) {
const w = Math.abs(viewBox.minX) + Math.abs(viewBox.width);
const h = Math.abs(viewBox.minY) + Math.abs(viewBox.height);
const i = Math.floor(0.1 * w);
let attr: any = {
"font-size": `${Math.floor(h / 4)}`,
"font-weight": "bold",
};
if (this.transformation.fontFamily)
attr = { ...attr, "font-family": this.transformation.fontFamily };
if (this.transformation.fontColor)
attr = { ...attr, style: `fill:${this.transformation.fontColor}` };
const g = new Element(xml, "g");
g.attr(attr);
(xml.get("//svg:svg", SVG_NS) as Element).addChild(
g.addChild(
new Element(xml, "text")
.attr({
x: `${viewBox.minX + i}`,
y: `${viewBox.minY + Math.floor(0.8 * h)}`,
})
.text(this.transformation.text)
)
);
}
return xml.toString();
};
}
export const HOLI_COLORS = ["#06bceb", "#9fc717", "#fbdc10", "#f00b9a", "#fa9705"]
export const HOLI_COLORS = [
"#06bceb",
"#9fc717",
"#fbdc10",
"#f00b9a",
"#fa9705",
];
export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => {
const wrap = (
icon: Icon,
rule: (clock: Clock) => boolean,
colors: Pick<Transformation, "backgroundColor" | "foregroundColor">
colors: Pick<
Transformation,
"backgroundColor" | "foregroundColor" | "fontColor"
>
) =>
new ColorOverridingIcon(
icon,
@@ -133,22 +195,176 @@ export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => {
() => colors
);
const xmas = wrap(icon, isChristmas, {
let result = icon;
const apply = (
rule: (clock: Clock) => boolean,
colors: Pick<
Transformation,
"backgroundColor" | "foregroundColor" | "fontColor"
>
) => (result = wrap(result, rule, colors));
apply(isChristmas, {
backgroundColor: "green",
foregroundColor: "red",
fontColor: "white",
});
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
const holi = wrap(xmas, isHoli, {
apply(isHoli, {
backgroundColor: randomHoliColors.pop(),
foregroundColor: randomHoliColors.pop(),
fontColor: randomHoliColors.pop(),
});
const cny = wrap(holi, isCNY, {
apply(isCNY, {
backgroundColor: "red",
foregroundColor: "yellow",
fontColor: "crimson",
});
const halloween = wrap(cny, isHalloween, {
apply(isHalloween, {
backgroundColor: "orange",
foregroundColor: "black",
fontColor: "orangered",
});
return halloween;
return result;
};
export type ICON =
| "artists"
| "albums"
| "playlists"
| "genres"
| "random"
| "starred"
| "recentlyAdded"
| "recentlyPlayed"
| "mostPlayed"
| "discover"
| "blank"
| "mushroom"
| "african"
| "rock"
| "metal"
| "punk"
| "americana"
| "guitar"
| "book"
| "oz"
| "rap"
| "horror"
| "hipHop"
| "pop"
| "blues"
| "classical"
| "comedy"
| "vinyl"
| "electronic"
| "pills"
| "trumpet"
| "conductor"
| "reggae"
| "music";
const iconFrom = (name: string) =>
new SvgIcon(
fs
.readFileSync(path.resolve(__dirname, "..", "web", "icons", name))
.toString()
).with({ viewPortIncreasePercent: 50 });
export const ICONS: Record<ICON, Icon> = {
artists: iconFrom("navidrome-artists.svg"),
albums: iconFrom("navidrome-all.svg"),
blank: iconFrom("blank.svg"),
playlists: iconFrom("navidrome-playlists.svg"),
genres: iconFrom("Theatre-Mask-111172.svg"),
random: iconFrom("navidrome-random.svg"),
starred: iconFrom("navidrome-topRated.svg"),
recentlyAdded: iconFrom("navidrome-recentlyAdded.svg"),
recentlyPlayed: iconFrom("navidrome-recentlyPlayed.svg"),
mostPlayed: iconFrom("navidrome-mostPlayed.svg"),
discover: iconFrom("Binoculars-14310.svg"),
mushroom: iconFrom("Mushroom-63864.svg"),
african: iconFrom("Africa-48087.svg"),
rock: iconFrom("Rock-Music-11007.svg"),
metal: iconFrom("Metal-Music-17763.svg"),
punk: iconFrom("Punk-40450.svg"),
americana: iconFrom("US-Capitol-104805.svg"),
guitar: iconFrom("Guitar-110433.svg"),
book: iconFrom("Book-453.svg"),
oz: iconFrom("Kangaroo-16730.svg"),
hipHop: iconFrom("Hip-Hop Music-17757.svg"),
rap: iconFrom("Rap-24851.svg"),
horror: iconFrom("Horror-4387.svg"),
pop: iconFrom("Ice-Pop Yellow-94532.svg"),
blues: iconFrom("Blues-113548.svg"),
classical: iconFrom("Classic-Music-11646.svg"),
comedy: iconFrom("Comedy-2-599.svg"),
vinyl: iconFrom("Music-Record-102104.svg"),
electronic: iconFrom("Electronic-Music-17745.svg"),
pills: iconFrom("Pills-112386.svg"),
trumpet: iconFrom("Trumpet-17823.svg"),
conductor: iconFrom("Music-Conductor-225.svg"),
reggae: iconFrom("Reggae-24843.svg"),
music: iconFrom("Music-14097.svg")
};
export type RULE = (genre: string) => boolean;
const eq =
(expected: string): RULE =>
(value: string) =>
expected.toLowerCase() === value.toLowerCase();
const contains =
(expected: string): RULE =>
(value: string) =>
value.toLowerCase().includes(expected.toLowerCase());
const containsWithAllTheNonWordCharsRemoved =
(expected: string): RULE =>
(value: string) =>
value.replace(/\W+/, " ").toLowerCase().includes(expected.toLowerCase());
const GENRE_RULES: [RULE, ICON][] = [
[eq("Acid House"), "mushroom"],
[contains("Goa"), "mushroom"],
[contains("Psy"), "mushroom"],
[eq("African"), "african"],
[eq("Americana"), "americana"],
[contains("Rock"), "rock"],
[contains("Folk"), "guitar"],
[contains("Book"), "book"],
[contains("Australian"), "oz"],
[contains("Rap"), "rap"],
[containsWithAllTheNonWordCharsRemoved("Hip Hop"), "hipHop"],
[contains("Horror"), "horror"],
[contains("Metal"), "metal"],
[contains("Punk"), "punk"],
[contains("Pop"), "pop"],
[contains("Blues"), "blues"],
[contains("Classical"), "classical"],
[contains("Comedy"), "comedy"],
[contains("Dub"), "vinyl"],
[contains("Turntable"), "vinyl"],
[contains("Electro"), "electronic"],
[contains("Trance"), "pills"],
[contains("Techno"), "pills"],
[contains("House"), "pills"],
[contains("Rave"), "pills"],
[contains("Jazz"), "trumpet"],
[contains("Orchestra"), "conductor"],
[contains("Reggae"), "reggae"],
];
export function iconForGenre(genre: string): ICON {
const [_, name] = GENRE_RULES.find(([rule, _]) => rule(genre)) || [
"music",
"music",
];
return name! as ICON;
}