Additional Icon support (#42)
@@ -4,6 +4,9 @@ export const isChristmas = (clock: Clock = SystemClock) => clock.now().month() =
|
||||
export const isHalloween = (clock: Clock = SystemClock) => clock.now().month() == 9 && clock.now().date() == 31
|
||||
export const isHoli = (clock: Clock = SystemClock) => ["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
|
||||
export const isCNY = (clock: Clock = SystemClock) => ["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
|
||||
export const isCNY_2022 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2022/02/01"))
|
||||
export const isCNY_2023 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2023/01/22"))
|
||||
export const isCNY_2024 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2024/02/10"))
|
||||
|
||||
export interface Clock {
|
||||
now(): Dayjs;
|
||||
|
||||
240
src/icon.ts
@@ -5,19 +5,15 @@ import fs from "fs";
|
||||
import {
|
||||
Clock,
|
||||
isChristmas,
|
||||
isCNY,
|
||||
isCNY_2022,
|
||||
isCNY_2023,
|
||||
isCNY_2024,
|
||||
isHalloween,
|
||||
isHoli,
|
||||
SystemClock,
|
||||
} from "./clock";
|
||||
import path from "path";
|
||||
|
||||
export type Transformation = {
|
||||
viewPortIncreasePercent: number | undefined;
|
||||
backgroundColor: string | undefined;
|
||||
foregroundColor: string | undefined;
|
||||
};
|
||||
|
||||
const SVG_NS = {
|
||||
svg: "http://www.w3.org/2000/svg",
|
||||
};
|
||||
@@ -47,55 +43,77 @@ class ViewBox {
|
||||
`${this.minX} ${this.minY} ${this.width} ${this.height}`;
|
||||
}
|
||||
|
||||
export type IconFeatures = {
|
||||
viewPortIncreasePercent: number | undefined;
|
||||
backgroundColor: string | undefined;
|
||||
foregroundColor: string | undefined;
|
||||
};
|
||||
|
||||
export type IconSpec = {
|
||||
svg: string | undefined;
|
||||
features: Partial<IconFeatures> | undefined;
|
||||
};
|
||||
|
||||
export interface Icon {
|
||||
with(newTransformation: Partial<Transformation>): Icon;
|
||||
with(spec: Partial<IconSpec>): Icon;
|
||||
apply(transformer: Transformer): Icon;
|
||||
}
|
||||
|
||||
export class ColorOverridingIcon implements Icon {
|
||||
rule: () => Boolean;
|
||||
newColors: () => Partial<
|
||||
Pick<Transformation, "backgroundColor" | "foregroundColor">
|
||||
>;
|
||||
icon: Icon;
|
||||
export type Transformer = (icon: Icon) => Icon;
|
||||
|
||||
constructor(
|
||||
icon: Icon,
|
||||
rule: () => Boolean,
|
||||
newColors: () => Partial<
|
||||
Pick<Transformation, "backgroundColor" | "foregroundColor">
|
||||
>
|
||||
) {
|
||||
this.icon = icon;
|
||||
this.rule = rule;
|
||||
this.newColors = newColors;
|
||||
}
|
||||
export function transform(spec: Partial<IconSpec>): Transformer {
|
||||
return (icon: Icon) =>
|
||||
icon.with({
|
||||
...spec,
|
||||
features: { ...spec.features },
|
||||
});
|
||||
}
|
||||
|
||||
public with = (transformation: Partial<Transformation>) =>
|
||||
this.rule()
|
||||
? this.icon.with({ ...transformation, ...this.newColors() })
|
||||
: this.icon.with(transformation);
|
||||
export function features(features: Partial<IconFeatures>): Transformer {
|
||||
return (icon: Icon) => icon.with({ features });
|
||||
}
|
||||
|
||||
public toString = () => this.with({}).toString();
|
||||
export function maybeTransform(rule: () => Boolean, transformer: Transformer) {
|
||||
return (icon: Icon) => (rule() ? transformer(icon) : icon);
|
||||
}
|
||||
|
||||
export function allOf(...transformers: Transformer[]): Transformer {
|
||||
return (icon: Icon): Icon =>
|
||||
_.inject(
|
||||
transformers,
|
||||
(current: Icon, transformer: Transformer) => transformer(current),
|
||||
icon
|
||||
);
|
||||
}
|
||||
|
||||
export class SvgIcon implements Icon {
|
||||
private svg: string;
|
||||
private transformation: Transformation;
|
||||
svg: string;
|
||||
features: IconFeatures;
|
||||
|
||||
constructor(
|
||||
svg: string,
|
||||
transformation: Transformation = {
|
||||
features: Partial<IconFeatures> = {
|
||||
viewPortIncreasePercent: undefined,
|
||||
backgroundColor: undefined,
|
||||
foregroundColor: undefined,
|
||||
}
|
||||
) {
|
||||
this.svg = svg;
|
||||
this.transformation = transformation;
|
||||
this.features = {
|
||||
viewPortIncreasePercent: undefined,
|
||||
backgroundColor: undefined,
|
||||
foregroundColor: undefined,
|
||||
...features,
|
||||
};
|
||||
}
|
||||
|
||||
public with = (newTransformation: Partial<Transformation>) =>
|
||||
new SvgIcon(this.svg, { ...this.transformation, ...newTransformation });
|
||||
public apply = (transformer: Transformer): Icon => transformer(this);
|
||||
|
||||
public with = (spec: Partial<IconSpec>) =>
|
||||
new SvgIcon(spec.svg || this.svg, {
|
||||
...this.features,
|
||||
...spec.features,
|
||||
});
|
||||
|
||||
public toString = () => {
|
||||
const xml = libxmljs.parseXmlString(this.svg, {
|
||||
@@ -105,30 +123,28 @@ export class SvgIcon implements Icon {
|
||||
const viewBoxAttr = xml.get("//svg:svg/@viewBox", SVG_NS) as Attribute;
|
||||
let viewBox = new ViewBox(viewBoxAttr.value());
|
||||
if (
|
||||
this.transformation.viewPortIncreasePercent &&
|
||||
this.transformation.viewPortIncreasePercent > 0
|
||||
this.features.viewPortIncreasePercent &&
|
||||
this.features.viewPortIncreasePercent > 0
|
||||
) {
|
||||
viewBox = viewBox.increasePercent(
|
||||
this.transformation.viewPortIncreasePercent
|
||||
);
|
||||
viewBox = viewBox.increasePercent(this.features.viewPortIncreasePercent);
|
||||
viewBoxAttr.value(viewBox.toString());
|
||||
}
|
||||
if (this.transformation.backgroundColor) {
|
||||
if (this.features.backgroundColor) {
|
||||
(xml.get("//svg:svg/*[1]", SVG_NS) as Element).addPrevSibling(
|
||||
new Element(xml, "rect").attr({
|
||||
x: `${viewBox.minX}`,
|
||||
y: `${viewBox.minY}`,
|
||||
width: `${Math.abs(viewBox.minX) + viewBox.width}`,
|
||||
height: `${Math.abs(viewBox.minY) + viewBox.height}`,
|
||||
fill: this.transformation.backgroundColor,
|
||||
fill: this.features.backgroundColor,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.transformation.foregroundColor) {
|
||||
if (this.features.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! });
|
||||
path.attr({ stroke: this.features.foregroundColor! });
|
||||
else path.attr({ fill: this.features.foregroundColor! });
|
||||
});
|
||||
}
|
||||
return xml.toString();
|
||||
@@ -143,49 +159,6 @@ export const HOLI_COLORS = [
|
||||
"#fa9705",
|
||||
];
|
||||
|
||||
export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => {
|
||||
const wrap = (
|
||||
icon: Icon,
|
||||
rule: (clock: Clock) => boolean,
|
||||
colors: Pick<Transformation, "backgroundColor" | "foregroundColor">
|
||||
) =>
|
||||
new ColorOverridingIcon(
|
||||
icon,
|
||||
() => rule(clock),
|
||||
() => colors
|
||||
);
|
||||
|
||||
let result = icon;
|
||||
|
||||
const apply = (
|
||||
rule: (clock: Clock) => boolean,
|
||||
colors: Pick<Transformation, "backgroundColor" | "foregroundColor">
|
||||
) => (result = wrap(result, rule, colors));
|
||||
|
||||
apply(isChristmas, {
|
||||
backgroundColor: "green",
|
||||
foregroundColor: "red",
|
||||
});
|
||||
|
||||
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
|
||||
apply(isHoli, {
|
||||
backgroundColor: randomHoliColors.pop(),
|
||||
foregroundColor: randomHoliColors.pop(),
|
||||
});
|
||||
|
||||
apply(isCNY, {
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
});
|
||||
|
||||
apply(isHalloween, {
|
||||
backgroundColor: "orange",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export type ICON =
|
||||
| "artists"
|
||||
| "albums"
|
||||
@@ -237,7 +210,14 @@ export type ICON =
|
||||
| "celtic"
|
||||
| "children"
|
||||
| "chillout"
|
||||
| "progressiveRock";
|
||||
| "progressiveRock"
|
||||
| "christmas"
|
||||
| "halloween"
|
||||
| "yoDragon"
|
||||
| "yoRabbit"
|
||||
| "yoTiger"
|
||||
| "chapel"
|
||||
| "audioWave";
|
||||
|
||||
const iconFrom = (name: string) =>
|
||||
new SvgIcon(
|
||||
@@ -246,7 +226,7 @@ const iconFrom = (name: string) =>
|
||||
.toString()
|
||||
);
|
||||
|
||||
export const ICONS: Record<ICON, Icon> = {
|
||||
export const ICONS: Record<ICON, SvgIcon> = {
|
||||
artists: iconFrom("navidrome-artists.svg"),
|
||||
albums: iconFrom("navidrome-all.svg"),
|
||||
blank: iconFrom("blank.svg"),
|
||||
@@ -298,6 +278,13 @@ export const ICONS: Record<ICON, Icon> = {
|
||||
celtic: iconFrom("Scottish-Thistle-108212.svg"),
|
||||
children: iconFrom("Children-78186.svg"),
|
||||
chillout: iconFrom("Sleeping-in Bed-14385.svg"),
|
||||
christmas: iconFrom("Christmas-Tree-66793.svg"),
|
||||
halloween: iconFrom("Jack-o' Lantern-66580.svg"),
|
||||
yoDragon: iconFrom("Year-of Dragon-4537.svg"),
|
||||
yoRabbit: iconFrom("Year-of Rabbit-6313.svg"),
|
||||
yoTiger: iconFrom("Year-of Tiger-22776.svg"),
|
||||
chapel: iconFrom("Chapel-69791.svg"),
|
||||
audioWave: iconFrom("Audio-Wave-1892.svg"),
|
||||
};
|
||||
|
||||
export type RULE = (genre: string) => boolean;
|
||||
@@ -332,6 +319,8 @@ const GENRE_RULES: [RULE, ICON][] = [
|
||||
[eq("Turntablism"), "vinyl"],
|
||||
[eq("Celtic"), "celtic"],
|
||||
[eq("Progressive Rock"), "progressiveRock"],
|
||||
[containsWord("Christmas"), "christmas"],
|
||||
[containsWord("Kerst"), "christmas"], // christmas in dutch
|
||||
[containsWord("Country"), "country"],
|
||||
[containsWord("Rock"), "rock"],
|
||||
[containsWord("Folk"), "guitar"],
|
||||
@@ -347,6 +336,7 @@ const GENRE_RULES: [RULE, ICON][] = [
|
||||
[eq("Classic"), "classical"],
|
||||
[containsWord("Classical"), "classical"],
|
||||
[containsWord("Comedy"), "comedy"],
|
||||
[containsWord("Komedie"), "comedy"], // dutch for Comedy
|
||||
[containsWord("Turntable"), "vinyl"],
|
||||
[containsWord("Dub"), "electronic"],
|
||||
[eq("Dubstep"), "electronic"],
|
||||
@@ -374,6 +364,9 @@ const GENRE_RULES: [RULE, ICON][] = [
|
||||
[contains("Children"), "children"],
|
||||
[contains("Chill"), "chill"],
|
||||
[contains("Old"), "old"],
|
||||
[containsWord("Christian"), "chapel"],
|
||||
[containsWord("Religious"), "chapel"],
|
||||
[containsWord("Spoken"), "audioWave"],
|
||||
];
|
||||
|
||||
export function iconForGenre(genre: string): ICON {
|
||||
@@ -383,3 +376,68 @@ export function iconForGenre(genre: string): ICON {
|
||||
];
|
||||
return name! as ICON;
|
||||
}
|
||||
|
||||
export const festivals = (clock: Clock = SystemClock): Transformer => {
|
||||
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
|
||||
return allOf(
|
||||
maybeTransform(
|
||||
() => isChristmas(clock),
|
||||
transform({
|
||||
svg: ICONS.christmas.svg,
|
||||
features: {
|
||||
backgroundColor: "green",
|
||||
foregroundColor: "red",
|
||||
},
|
||||
})
|
||||
),
|
||||
maybeTransform(
|
||||
() => isHoli(clock),
|
||||
transform({
|
||||
features: {
|
||||
backgroundColor: randomHoliColors.pop(),
|
||||
foregroundColor: randomHoliColors.pop(),
|
||||
},
|
||||
})
|
||||
),
|
||||
maybeTransform(
|
||||
() => isCNY_2022(clock),
|
||||
transform({
|
||||
svg: ICONS.yoTiger.svg,
|
||||
features: {
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
},
|
||||
})
|
||||
),
|
||||
maybeTransform(
|
||||
() => isCNY_2023(clock),
|
||||
transform({
|
||||
svg: ICONS.yoRabbit.svg,
|
||||
features: {
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
},
|
||||
})
|
||||
),
|
||||
maybeTransform(
|
||||
() => isCNY_2024(clock),
|
||||
transform({
|
||||
svg: ICONS.yoDragon.svg,
|
||||
features: {
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
},
|
||||
})
|
||||
),
|
||||
maybeTransform(
|
||||
() => isHalloween(clock),
|
||||
transform({
|
||||
svg: ICONS.halloween.svg,
|
||||
features: {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "orange",
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Clock, SystemClock } from "./clock";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { URLBuilder } from "./url_builder";
|
||||
import makeI8N, { asLANGs, KEY, keys as i8nKeys, LANG } from "./i8n";
|
||||
import { Icon, makeFestive, ICONS } from "./icon";
|
||||
import { Icon, ICONS, festivals, features } from "./icon";
|
||||
import _, { shuffle } from "underscore";
|
||||
import morgan from "morgan";
|
||||
import { takeWithRepeats } from "./utils";
|
||||
@@ -79,7 +79,7 @@ export type ServerOpts = {
|
||||
};
|
||||
applyContextPath: boolean;
|
||||
logRequests: boolean;
|
||||
version: string
|
||||
version: string;
|
||||
};
|
||||
|
||||
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||
@@ -89,7 +89,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||
iconColors: { foregroundColor: undefined, backgroundColor: undefined },
|
||||
applyContextPath: true,
|
||||
logRequests: false,
|
||||
version: "v?"
|
||||
version: "v?",
|
||||
};
|
||||
|
||||
function server(
|
||||
@@ -148,7 +148,7 @@ function server(
|
||||
removeRegistrationRoute: bonobUrl
|
||||
.append({ pathname: REMOVE_REGISTRATION_ROUTE })
|
||||
.pathname(),
|
||||
version: opts.version
|
||||
version: opts.version,
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -413,10 +413,15 @@ function server(
|
||||
};
|
||||
|
||||
return Promise.resolve(
|
||||
makeFestive(
|
||||
icon.with({ viewPortIncreasePercent: 80, ...serverOpts.iconColors }),
|
||||
clock
|
||||
).toString()
|
||||
icon
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 80,
|
||||
...serverOpts.iconColors,
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock))
|
||||
.toString()
|
||||
)
|
||||
.then(spec.responseFormatter)
|
||||
.then((data) => res.status(200).type(spec.mimeType).send(data));
|
||||
@@ -428,7 +433,12 @@ function server(
|
||||
icons: Object.keys(ICONS).map((k) => [
|
||||
k,
|
||||
((ICONS as any)[k] as Icon)
|
||||
.with({ viewPortIncreasePercent: 80, ...serverOpts.iconColors })
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 80,
|
||||
...serverOpts.iconColors,
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.replace('<?xml version="1.0" encoding="UTF-8"?>', ""),
|
||||
]),
|
||||
|
||||
@@ -2,16 +2,22 @@ import dayjs from "dayjs";
|
||||
import libxmljs from "libxmljs2";
|
||||
|
||||
import {
|
||||
ColorOverridingIcon,
|
||||
contains,
|
||||
containsWord,
|
||||
eq,
|
||||
HOLI_COLORS,
|
||||
Icon,
|
||||
iconForGenre,
|
||||
makeFestive,
|
||||
SvgIcon,
|
||||
Transformation,
|
||||
IconFeatures,
|
||||
IconSpec,
|
||||
ICONS,
|
||||
Transformer,
|
||||
transform,
|
||||
maybeTransform,
|
||||
festivals,
|
||||
allOf,
|
||||
features,
|
||||
} from "../src/icon";
|
||||
|
||||
describe("SvgIcon", () => {
|
||||
@@ -34,7 +40,7 @@ describe("SvgIcon", () => {
|
||||
</svg>
|
||||
`;
|
||||
|
||||
describe("with no transformation", () => {
|
||||
describe("with no features", () => {
|
||||
it("should be the same", () => {
|
||||
expect(new SvgIcon(svgIcon24).toString()).toEqual(xmlTidy(svgIcon24));
|
||||
});
|
||||
@@ -46,7 +52,7 @@ describe("SvgIcon", () => {
|
||||
it("should resize the viewPort", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ viewPortIncreasePercent: 50 })
|
||||
.with({ features: { viewPortIncreasePercent: 50 } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -63,7 +69,7 @@ describe("SvgIcon", () => {
|
||||
it("should resize the viewPort", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon128)
|
||||
.with({ viewPortIncreasePercent: 50 })
|
||||
.with({ features: { viewPortIncreasePercent: 50 } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -81,7 +87,9 @@ describe("SvgIcon", () => {
|
||||
describe("of 0%", () => {
|
||||
it("should do nothing", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24).with({ viewPortIncreasePercent: 0 }).toString()
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ features: { viewPortIncreasePercent: 0 } })
|
||||
.toString()
|
||||
).toEqual(xmlTidy(svgIcon24));
|
||||
});
|
||||
});
|
||||
@@ -91,7 +99,9 @@ describe("SvgIcon", () => {
|
||||
describe("with no viewPort increase", () => {
|
||||
it("should add a rectangle the same size as the original viewPort", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24).with({ backgroundColor: "red" }).toString()
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ features: { backgroundColor: "red" } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
@@ -109,7 +119,12 @@ describe("SvgIcon", () => {
|
||||
it("should add a rectangle the same size as the original viewPort", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ backgroundColor: "pink", viewPortIncreasePercent: 50 })
|
||||
.with({
|
||||
features: {
|
||||
backgroundColor: "pink",
|
||||
viewPortIncreasePercent: 50,
|
||||
},
|
||||
})
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -127,7 +142,9 @@ describe("SvgIcon", () => {
|
||||
describe("of undefined", () => {
|
||||
it("should not do anything", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24).with({ backgroundColor: undefined }).toString()
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ features: { backgroundColor: undefined } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
@@ -144,8 +161,8 @@ describe("SvgIcon", () => {
|
||||
it("should use the most recent", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ backgroundColor: "green" })
|
||||
.with({ backgroundColor: "red" })
|
||||
.with({ features: { backgroundColor: "green" } })
|
||||
.with({ features: { backgroundColor: "red" } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -165,7 +182,9 @@ describe("SvgIcon", () => {
|
||||
describe("with no viewPort increase", () => {
|
||||
it("should add a rectangle the same size as the original viewPort", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24).with({ foregroundColor: "red" }).toString()
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ features: { foregroundColor: "red" } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
@@ -182,7 +201,12 @@ describe("SvgIcon", () => {
|
||||
it("should add a rectangle the same size as the original viewPort", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ foregroundColor: "pink", viewPortIncreasePercent: 50 })
|
||||
.with({
|
||||
features: {
|
||||
foregroundColor: "pink",
|
||||
viewPortIncreasePercent: 50,
|
||||
},
|
||||
})
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -199,7 +223,9 @@ describe("SvgIcon", () => {
|
||||
describe("of undefined", () => {
|
||||
it("should not do anything", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24).with({ foregroundColor: undefined }).toString()
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ features: { foregroundColor: undefined } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
@@ -216,8 +242,8 @@ describe("SvgIcon", () => {
|
||||
it("should use the most recent", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24)
|
||||
.with({ foregroundColor: "blue" })
|
||||
.with({ foregroundColor: "red" })
|
||||
.with({ features: { foregroundColor: "blue" } })
|
||||
.with({ features: { foregroundColor: "red" } })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -231,143 +257,306 @@ describe("SvgIcon", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("swapping the svg", () => {
|
||||
describe("with no other changes", () => {
|
||||
it("should swap out the svg, but maintain the IconFeatures", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24, {
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "green",
|
||||
viewPortIncreasePercent: 50,
|
||||
})
|
||||
.with({ svg: svgIcon128 })
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-21 -21 170 170">
|
||||
<rect x="-21" y="-21" width="191" height="191" fill="green"/>
|
||||
<path d="path1" fill="blue"/>
|
||||
<path d="path2" fill="none" stroke="blue"/>
|
||||
<path d="path3" fill="blue"/>
|
||||
</svg>
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with no other changes", () => {
|
||||
it("should swap out the svg, but maintain the IconFeatures", () => {
|
||||
expect(
|
||||
new SvgIcon(svgIcon24, {
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "green",
|
||||
viewPortIncreasePercent: 50,
|
||||
})
|
||||
.with({
|
||||
svg: svgIcon128,
|
||||
features: {
|
||||
foregroundColor: "pink",
|
||||
backgroundColor: "red",
|
||||
viewPortIncreasePercent: 0,
|
||||
},
|
||||
})
|
||||
.toString()
|
||||
).toEqual(
|
||||
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect x="0" y="0" width="128" height="128" fill="red"/>
|
||||
<path d="path1" fill="pink"/>
|
||||
<path d="path2" fill="none" stroke="pink"/>
|
||||
<path d="path3" fill="pink"/>
|
||||
</svg>
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class DummyIcon implements Icon {
|
||||
transformation: Partial<Transformation>;
|
||||
constructor(transformation: Partial<Transformation>) {
|
||||
this.transformation = transformation;
|
||||
svg: string;
|
||||
features: Partial<IconFeatures>;
|
||||
constructor(svg: string, features: Partial<IconFeatures>) {
|
||||
this.svg = svg;
|
||||
this.features = features;
|
||||
}
|
||||
public with = (newTransformation: Partial<Transformation>) =>
|
||||
new DummyIcon({ ...this.transformation, ...newTransformation });
|
||||
|
||||
public toString = () => JSON.stringify(this);
|
||||
public apply = (transformer: Transformer): Icon => transformer(this);
|
||||
|
||||
public with = ({ svg, features }: Partial<IconSpec>) => {
|
||||
return new DummyIcon(svg || this.svg, {
|
||||
...this.features,
|
||||
...(features || {}),
|
||||
});
|
||||
};
|
||||
|
||||
public toString = () =>
|
||||
JSON.stringify({ svg: this.svg, features: this.features });
|
||||
}
|
||||
|
||||
describe("ColorOverridingIcon", () => {
|
||||
describe("transform", () => {
|
||||
describe("when the features contains no svg", () => {
|
||||
it("should apply the overriding transform ontop of the requested transform", () => {
|
||||
const original = new DummyIcon("original", {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
const result = original
|
||||
.with({
|
||||
features: {
|
||||
viewPortIncreasePercent: 100,
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "blue",
|
||||
},
|
||||
})
|
||||
.apply(
|
||||
transform({
|
||||
features: {
|
||||
foregroundColor: "override1",
|
||||
backgroundColor: "override2",
|
||||
},
|
||||
})
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.svg).toEqual("original");
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 100,
|
||||
foregroundColor: "override1",
|
||||
backgroundColor: "override2",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the features contains an svg", () => {
|
||||
it("should use the newly provided svg", () => {
|
||||
const original = new DummyIcon("original", {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
const result = original
|
||||
.with({
|
||||
features: {
|
||||
viewPortIncreasePercent: 100,
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "blue",
|
||||
},
|
||||
})
|
||||
.apply(
|
||||
transform({
|
||||
svg: "new",
|
||||
})
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.svg).toEqual("new");
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 100,
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "blue",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("features", () => {
|
||||
it("should apply the features", () => {
|
||||
const original = new DummyIcon("original", {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
const result = original.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 100,
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "blue",
|
||||
})
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 100,
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "blue",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("allOf", () => {
|
||||
it("should apply all composed transforms", () => {
|
||||
const result = new DummyIcon("original", {
|
||||
foregroundColor: "black",
|
||||
backgroundColor: "black",
|
||||
viewPortIncreasePercent: 0,
|
||||
}).apply(
|
||||
allOf(
|
||||
(icon: Icon) => icon.with({ svg: "foo" }),
|
||||
(icon: Icon) => icon.with({ features: { backgroundColor: "red" } }),
|
||||
(icon: Icon) => icon.with({ features: { foregroundColor: "blue" } })
|
||||
)
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.svg).toEqual("foo");
|
||||
expect(result.features).toEqual({
|
||||
foregroundColor: "blue",
|
||||
backgroundColor: "red",
|
||||
viewPortIncreasePercent: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeTransform", () => {
|
||||
describe("when the rule matches", () => {
|
||||
const icon = new DummyIcon({
|
||||
const original = new DummyIcon("original", {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
|
||||
describe("overriding some options", () => {
|
||||
const overriding = new ColorOverridingIcon(
|
||||
icon,
|
||||
() => true,
|
||||
() => ({ backgroundColor: "blue", foregroundColor: "red" })
|
||||
);
|
||||
|
||||
describe("with", () => {
|
||||
it("should be the with of the underlieing icon with the overriden colors", () => {
|
||||
const result = overriding.with({
|
||||
describe("transforming the color", () => {
|
||||
const result = original
|
||||
.with({
|
||||
features: {
|
||||
viewPortIncreasePercent: 99,
|
||||
backgroundColor: "shouldBeIgnored",
|
||||
foregroundColor: "shouldBeIgnored",
|
||||
}) as DummyIcon;
|
||||
},
|
||||
})
|
||||
.apply(
|
||||
maybeTransform(
|
||||
() => true,
|
||||
transform({
|
||||
features: {
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
},
|
||||
})
|
||||
)
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
describe("with", () => {
|
||||
it("should be the with of the underlieing icon with the overriden colors", () => {
|
||||
expect(result.svg).toEqual("original");
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 99,
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toString", () => {
|
||||
it("should be the toString of the underlieing icon with the overriden colors", () => {
|
||||
expect(overriding.toString()).toEqual(
|
||||
new DummyIcon({
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
}).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("overriding all options", () => {
|
||||
const overriding = new ColorOverridingIcon(
|
||||
icon,
|
||||
() => true,
|
||||
() => ({
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
})
|
||||
);
|
||||
|
||||
describe("with", () => {
|
||||
it("should be the with of the underlieing icon with the overriden colors", () => {
|
||||
const result = overriding.with({
|
||||
const result = original
|
||||
.with({
|
||||
features: {
|
||||
viewPortIncreasePercent: 99,
|
||||
backgroundColor: "shouldBeIgnored",
|
||||
foregroundColor: "shouldBeIgnored",
|
||||
}) as DummyIcon;
|
||||
},
|
||||
})
|
||||
.apply(
|
||||
maybeTransform(
|
||||
() => true,
|
||||
transform({
|
||||
features: {
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
},
|
||||
})
|
||||
)
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
describe("with", () => {
|
||||
it("should be the with of the underlieing icon with the overriden colors", () => {
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 99,
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toString", () => {
|
||||
it("should be the toString of the underlieing icon with the overriden colors", () => {
|
||||
expect(overriding.toString()).toEqual(
|
||||
new DummyIcon({
|
||||
backgroundColor: "blue",
|
||||
foregroundColor: "red",
|
||||
}).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the rule doesnt match", () => {
|
||||
const icon = new DummyIcon({
|
||||
const original = new DummyIcon("original", {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
const overriding = new ColorOverridingIcon(
|
||||
icon,
|
||||
const result = original
|
||||
.with({
|
||||
features: {
|
||||
viewPortIncreasePercent: 88,
|
||||
backgroundColor: "shouldBeUsed",
|
||||
foregroundColor: "shouldBeUsed",
|
||||
},
|
||||
})
|
||||
.apply(
|
||||
maybeTransform(
|
||||
() => false,
|
||||
() => ({ backgroundColor: "blue", foregroundColor: "red" })
|
||||
);
|
||||
transform({
|
||||
features: { backgroundColor: "blue", foregroundColor: "red" },
|
||||
})
|
||||
)
|
||||
) as DummyIcon;
|
||||
|
||||
describe("with", () => {
|
||||
it("should use the provided transformation", () => {
|
||||
const result = overriding.with({
|
||||
viewPortIncreasePercent: 88,
|
||||
backgroundColor: "shouldBeUsed",
|
||||
foregroundColor: "shouldBeUsed",
|
||||
}) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
it("should use the provided features", () => {
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 88,
|
||||
backgroundColor: "shouldBeUsed",
|
||||
foregroundColor: "shouldBeUsed",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toString", () => {
|
||||
it("should be the toString of the unchanged icon", () => {
|
||||
expect(overriding.toString()).toEqual(icon.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeFestive", () => {
|
||||
const icon = new DummyIcon({
|
||||
describe("festivals", () => {
|
||||
const original = new DummyIcon("original", {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
let now = dayjs();
|
||||
|
||||
const festiveIcon = makeFestive(icon, { now: () => now });
|
||||
const clock = { now: () => now };
|
||||
|
||||
describe("on a day that isn't festive", () => {
|
||||
beforeEach(() => {
|
||||
@@ -375,17 +564,23 @@ describe("makeFestive", () => {
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
const result = festiveIcon.with({
|
||||
const result = original
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 88,
|
||||
backgroundColor: "shouldBeUsed",
|
||||
foregroundColor: "shouldBeUsed",
|
||||
}) as DummyIcon;
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock)) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
viewPortIncreasePercent: 88,
|
||||
expect(result.toString()).toEqual(
|
||||
new DummyIcon("original", {
|
||||
backgroundColor: "shouldBeUsed",
|
||||
foregroundColor: "shouldBeUsed",
|
||||
});
|
||||
viewPortIncreasePercent: 88,
|
||||
}).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -395,16 +590,22 @@ describe("makeFestive", () => {
|
||||
});
|
||||
|
||||
it("should use the christmas theme colors", () => {
|
||||
const result = festiveIcon.with({
|
||||
const result = original.apply(
|
||||
allOf(
|
||||
features({
|
||||
viewPortIncreasePercent: 25,
|
||||
backgroundColor: "shouldNotBeUsed",
|
||||
foregroundColor: "shouldNotBeUsed",
|
||||
}) as DummyIcon;
|
||||
}),
|
||||
festivals(clock)
|
||||
)
|
||||
) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
viewPortIncreasePercent: 25,
|
||||
expect(result.svg).toEqual(ICONS.christmas.svg);
|
||||
expect(result.features).toEqual({
|
||||
backgroundColor: "green",
|
||||
foregroundColor: "red",
|
||||
viewPortIncreasePercent: 25,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -415,33 +616,44 @@ describe("makeFestive", () => {
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
const result = festiveIcon.with({
|
||||
const result = original
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "shouldNotBeUsed",
|
||||
foregroundColor: "shouldNotBeUsed",
|
||||
}) as DummyIcon;
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock)) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
expect(result.svg).toEqual(ICONS.halloween.svg);
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "orange",
|
||||
foregroundColor: "black",
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "orange",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("on cny", () => {
|
||||
describe("2022", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/02/01");
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
const result = festiveIcon.with({
|
||||
it("should use the cny theme", () => {
|
||||
const result = original
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "shouldNotBeUsed",
|
||||
foregroundColor: "shouldNotBeUsed",
|
||||
}) as DummyIcon;
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock)) as DummyIcon;
|
||||
|
||||
expect(result.transformation).toEqual({
|
||||
expect(result.svg).toEqual(ICONS.yoTiger.svg);
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
@@ -449,27 +661,82 @@ describe("makeFestive", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("2023", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2023/01/22");
|
||||
});
|
||||
|
||||
it("should use the cny theme", () => {
|
||||
const result = original
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "shouldNotBeUsed",
|
||||
foregroundColor: "shouldNotBeUsed",
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock)) as DummyIcon;
|
||||
|
||||
expect(result.svg).toEqual(ICONS.yoRabbit.svg);
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("2024", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2024/02/10");
|
||||
});
|
||||
|
||||
it("should use the cny theme", () => {
|
||||
const result = original
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "shouldNotBeUsed",
|
||||
foregroundColor: "shouldNotBeUsed",
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock)) as DummyIcon;
|
||||
|
||||
expect(result.svg).toEqual(ICONS.yoDragon.svg);
|
||||
expect(result.features).toEqual({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("on holi", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/03/18");
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
const result = festiveIcon.with({
|
||||
const result = original
|
||||
.apply(
|
||||
features({
|
||||
viewPortIncreasePercent: 12,
|
||||
backgroundColor: "shouldNotBeUsed",
|
||||
foregroundColor: "shouldNotBeUsed",
|
||||
}) as DummyIcon;
|
||||
})
|
||||
)
|
||||
.apply(festivals(clock)) as DummyIcon;
|
||||
|
||||
expect(result.transformation.viewPortIncreasePercent).toEqual(12);
|
||||
expect(
|
||||
HOLI_COLORS.includes(result.transformation.backgroundColor!)
|
||||
).toEqual(true);
|
||||
expect(
|
||||
HOLI_COLORS.includes(result.transformation.foregroundColor!)
|
||||
).toEqual(true);
|
||||
expect(result.transformation.backgroundColor).not.toEqual(
|
||||
result.transformation.foregroundColor
|
||||
expect(result.features.viewPortIncreasePercent).toEqual(12);
|
||||
expect(HOLI_COLORS.includes(result.features.backgroundColor!)).toEqual(
|
||||
true
|
||||
);
|
||||
expect(HOLI_COLORS.includes(result.features.foregroundColor!)).toEqual(
|
||||
true
|
||||
);
|
||||
expect(result.features.backgroundColor).not.toEqual(
|
||||
result.features.foregroundColor
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -509,7 +776,6 @@ describe("containsWord", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("iconForGenre", () => {
|
||||
[
|
||||
["Acid House", "mushroom"],
|
||||
|
||||
@@ -1723,16 +1723,28 @@ describe("server", () => {
|
||||
expect(svg).toContain(`fill="brightpink"`);
|
||||
});
|
||||
|
||||
it("should return a christmas icon on christmas day", async () => {
|
||||
function itShouldBeFestive(theme: string, date: string, id: string, color1: string, color2: string) {
|
||||
it(`should return a ${theme} icon on ${date}`, async () => {
|
||||
const response = await request(
|
||||
server({ now: () => dayjs("2022/12/25") })
|
||||
server({ now: () => dayjs(date) })
|
||||
).get(`/icon/${type}/size/180`);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
const svg = Buffer.from(response.body).toString();
|
||||
expect(svg).toContain(`fill="red"`);
|
||||
expect(svg).toContain(`fill="green"`);
|
||||
expect(svg).toContain(`id="${id}"`);
|
||||
expect(svg).toContain(`fill="${color1}"`);
|
||||
expect(svg).toContain(`fill="${color2}"`);
|
||||
});
|
||||
}
|
||||
|
||||
itShouldBeFestive("christmas '22", "2022/12/25", "christmas", "red", "green")
|
||||
itShouldBeFestive("christmas '23", "2023/12/25", "christmas", "red", "green")
|
||||
|
||||
itShouldBeFestive("halloween", "2022/10/31", "halloween", "black", "orange")
|
||||
itShouldBeFestive("halloween", "2023/10/31", "halloween", "black", "orange")
|
||||
|
||||
itShouldBeFestive("cny '22", "2022/02/01", "yoTiger", "red", "yellow")
|
||||
itShouldBeFestive("cny '23", "2023/01/22", "yoRabbit", "red", "yellow")
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
3
web/icons/Audio-Wave-1892.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.9" d="M19 3L19 22M22 9L22 16M25 11L25 14M16 7L16 18M10 9L10 16M13 11L13 14M7 4L7 21M1 11L1 14M4 8L4 17"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
6
web/icons/Chapel-69791.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M10.5 7.125L10.494 3.529 7.494 0.5 4.5 3.5 4.5 7.125"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M2.5 13.5L2.5 8.5 7.5 5.5 12.5 8.5 12.5 13.5M7 13.5L2 13.5"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M8.5 13.5v-3c0-.552-.448-1-1-1s-1 .448-1 1v3M13 13.5L8 13.5"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M10.5 10.5L10.5 13.5M4.5 10.5L4.5 13.5M7.5 3.5L7.5 5.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 665 B |
4
web/icons/Christmas-Tree-66793.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" id="christmas">
|
||||
<path d="M17 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C18 51.448 17.552 51 17 51zM22 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C23 51.448 22.552 51 22 51zM27 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C28 51.448 27.552 51 27 51zM32 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C33 51.448 32.552 51 32 51zM37 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C38 51.448 37.552 51 37 51zM42 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C43 51.448 42.552 51 42 51zM48 52c0-.552-.448-1-1-1s-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1V52z"/>
|
||||
<path d="M54.641,55.858L46.417,44h1.576c0.776,0,1.469-0.434,1.807-1.131c0.336-0.695,0.247-1.503-0.233-2.109L41.036,30h1.946c0.806,0,1.51-0.453,1.84-1.182c0.325-0.721,0.202-1.539-0.321-2.133l-10.769-12.23l2.399,1.359c0.28,0.175,0.625,0.229,0.945,0.146c0.321-0.082,0.601-0.296,0.761-0.582c0.146-0.255,0.196-0.556,0.143-0.848c-0.01-0.053-0.023-0.104-0.042-0.155l-1.492-4.18l3.097-2.849c0.378-0.307,0.541-0.81,0.416-1.283c-0.004-0.016-0.009-0.031-0.014-0.046c-0.148-0.468-0.558-0.804-1.051-0.855l-4.198-0.401L33.143,0.77c-0.125-0.31-0.374-0.558-0.682-0.682c-0.307-0.123-0.653-0.115-0.945,0.017c-0.293,0.129-0.521,0.366-0.648,0.678L29.32,4.76l-4.218,0.4c-0.667,0.068-1.159,0.673-1.097,1.343c0.027,0.306,0.166,0.589,0.395,0.802l3.148,2.895l-1.45,4.185c-0.015,0.043-0.027,0.088-0.036,0.133c-0.13,0.658,0.292,1.309,0.94,1.452C27.092,15.99,27.183,16,27.271,16c0.221,0,0.434-0.059,0.606-0.167l2.373-1.36L19.5,26.685c-0.524,0.595-0.647,1.413-0.321,2.133c0.33,0.729,1.035,1.182,1.84,1.182h1.895l-8.482,10.764c-0.478,0.605-0.566,1.413-0.229,2.107C14.54,43.567,15.231,44,16.006,44h1.544L9.356,55.862c-0.424,0.614-0.472,1.408-0.125,2.069C9.577,58.591,10.254,59,10.998,59H24v3c0,1.103,0.897,2,2,2h12c1.103,0,2-0.897,2-2v-3h13.002c0.744,0,1.422-0.41,1.768-1.071C55.116,57.267,55.067,56.473,54.641,55.858z M28.669,13.075l0.983-2.839c0.13-0.376,0.025-0.794-0.268-1.063L27.01,6.989l3.113-0.295c0.376-0.036,0.701-0.281,0.838-0.633l1.047-2.691l1.047,2.691c0.137,0.353,0.46,0.597,0.837,0.633l3.088,0.295l-2.375,2.184c-0.296,0.272-0.4,0.694-0.265,1.073l1.01,2.828l-2.862-1.622c-0.308-0.173-0.684-0.173-0.991,0.002L28.669,13.075z M26,62v-3h12l0.002,3H26z M10.969,57.046L19.98,44H28c0.552,0,1-0.447,1-1s-0.448-1-1-1l-11.998,0.002L25.46,30h3.492c0.552,0,1-0.448,1-1s-0.448-1-1-1L21,28.006l10.999-12.492L42.982,28h-3.871c-0.152,0-0.292,0.039-0.421,0.1c-0.109,0.036-0.216,0.083-0.311,0.159c-0.433,0.343-0.506,0.972-0.162,1.405L47.992,42h-5.975c-0.553,0-1,0.447-1,1s0.447,1,1,1h1.965l9.019,13L10.969,57.046z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
4
web/icons/Jack-o' Lantern-107512.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="halloween" viewBox="0 0 30 30">
|
||||
<path d="M17.532,5c-2.517,0-3.596,0.966-4.435,1.265c-0.779,0.3-2.037,0.12-4.375,0.479c-6.472,1.079-5.693,8.648-5.693,8.648C3.029,23.124,10.64,26,12.498,26c1.258,0,1.318-0.599,1.918-0.599c0.599,0,1.258,0.599,2.517,0.599C21.307,26,27,22.165,27,14.554C27,6.284,20.049,5,17.532,5z M19.721,7.572l1.765,2.353l-1.176,1.765l-4.118,0.588L19.721,7.572z M16.191,14.042l-1.765,1.765l-1.765-1.765H16.191z M9.132,9.336l3.529,2.941H8.544l-1.176-1.176L9.132,9.336z M23.25,20.513c0-0.588-0.588-1.765-0.588-1.765c0,1.176-0.588,2.353-1.177,2.941c-0.176-0.588-0.588-1.176-0.588-1.176c-0.588,1.176-1.177,1.765-1.177,1.765c-0.412-0.647-1.176-1.176-1.176-1.176c0,0.882-0.588,1.765-0.588,1.765c-0.706-0.471-1.177-1.177-1.177-1.177s-0.588,0.824-0.588,1.765c-0.353-0.412-0.941-1.235-1.177-1.765c0,0-0.588,0.588-0.588,1.765c-0.588-0.471-1.765-1.765-1.765-1.765s-0.588,1.177-0.588,1.765c0,0-0.765-0.706-1.176-1.765c0,0-0.588,0.588-0.588,1.177c-1.765-1.177-1.765-2.353-1.765-2.353s-0.588,0.588-0.588,1.765c-1.765-1.765-2.941-3.529-2.941-6.471c0-1.118,0.588-2.941,1.177-3.529c-0.588,4.118,0.588,5.294,0.588,5.294c0-0.235,0-1.177,1.177-2.353c0,2.353,0.588,3.529,0.588,3.529c0-0.588,0.588-1.765,1.176-2.353c0,2.353,1.765,3.529,1.765,3.529c0-0.588,0.588-1.765,1.177-2.353c0,0.588,1.765,2.941,1.765,2.941s0-1.765,0.588-2.941c0.588,1.176,1.765,2.353,1.765,2.353s0.588-1.176,0.588-2.941c0,0,1.765,1.176,2.353,2.941c0,0,1.177-1.176,1.177-4.118c0,0,1.176,1.176,1.176,2.353c0,0,1.176-2.941,0.588-4.118c0,0,1.176,1.176,1.176,2.353c0,0,0.588-2.353-0.588-5.882c1.235,1.176,1.765,2.941,1.765,5.294C25.015,18.16,23.838,19.925,23.25,20.513z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M12.795,7.572C10.998,3.317,15,3,15,3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
5
web/icons/Jack-o' Lantern-66580.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="halloween" viewBox="0 0 16 16">
|
||||
<path fill="#ffab40" d="M4.41,8.67c0-0.008-0.001-0.014-0.001-0.022c-0.006,0.006-0.01,0.012-0.016,0.018C4.399,8.668,4.405,8.669,4.41,8.67z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M5.496,2.5C7.5,2.5,7.5,4.25,7.5,4.25"/>
|
||||
<path d="M9.274,3c-1.26,0-1.819,0.553-2.221,0.71C6.65,3.867,6.039,3.779,4.863,3.943C1.619,4.482,2.015,8.736,2.015,8.736C1.999,12.575,5.808,14,6.754,14c0.63,0,0.63-0.297,0.945-0.297c0.315,0,0.63,0.297,1.26,0.297C11.164,14,14,12.097,14,8.326C14,4.226,10.534,3,9.274,3z M11,5l1,2h-1H9L11,5z M9.5,8L8,9.5L6.5,8H9.5z M5,5l2,2H5H4L5,5z M11,12h-1v-1H9v1H7v-1H6v1H5c0,0-1-0.75-1-2h1v1h1v-1h2v1h1v-1h1v1h1v-1h1C12,11.25,11,12,11,12z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 807 B |
10
web/icons/Year-of Dragon-4537.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="yoDragon" viewBox="0 0 50 50">
|
||||
<path d="M36.9 33c.6 0 4.3 0 4.3 0s-2.7 1.9-3.7 2.3L36.9 33zM37.1 13c1.4-.4 1.4-3.9 1.4-3.9s3 4.1 3.1 7.4L37.1 13zM41.8 17.1c1.2-.2 1.5-3.1 1.5-3.1s2.1 3.7 1.8 6.4L41.8 17.1zM44.3 28c-.2.5-2 4.6-2 4.6s3-2.3 3.9-3.1L44.3 28z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M22.4 28.6c3 3.5 5.6 9 5.6 20.4M9 49c0 0 2-3 2-8 0-7.9-5-11-5-18 0-5.6 2.9-10.8 8.2-12.7"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M42.6,22.1c1.7,1.2,2.6,2.4,2.6,2.4"/>
|
||||
<path d="M32.7,14.4c0,0,4,2.9,5,4.2c0,0,0.2,1-0.2,1.1c-0.6,0.2-3.1,0.5-4-0.2l0.7-2.4l-2.2,2c-1.2-0.4-2.2-1.1-2.2-1.1S29.5,15.4,32.7,14.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M6.5 6.1c1.7 4-1.2 7.3-5.3 7.6 2.3 2.8 3 9.3-.2 12.1 2.9 2.3 4.2 5.3 1 10 4.4 1.1 5.1 6.4 2 9.3 3.9 1.3 5.1 3.5 5.1 3.5M36.1 13.2c0 0-4.1-3.6-4.3-8.9-1.5 1.2-6.4 2.6-7.7-3-1.1 2.7-5.8 6-9.8.5-.2 1.1-1.7 1.6-1.7 1.6M38.1 37c0 0-1.1 1-2.8 1.5-1.7.4-4.5.8-4.5.8l2.5-3.7L26.5 29c-2.1 1.1-12-3.3-7.4-11.5M37 24.2c-.6-.1-1.2 0-1.8.4-.5.3-.8.7-.9 1.2"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M37.2,18.8C28,8.4,7.5,14.8,3.6,1C9.5,10.2,23.8,6.1,37,13.7c10,5.8,13.7,14.9,11.2,16.4"/>
|
||||
<path d="M36.9 24.6l-.1 1.5 1.6-.9-.1 1.6 1.7-.5L40 27.6l1.2-.6.1 1.5 1.6-.7c0 0-.4 1.4-.2 1.3.2-.1 1.7-.5 1.7-.5l.1 1.3 1.3-.3.1 1.3 1.3-.6v1.2l1.1-.4-.1-2.2c0 0-2.2-1.2-4.2-2.3s-5.2-3.2-6.6-3.4C36.5 23.1 36.9 24.6 36.9 24.6zM38.6 36.3l1-1.1-1.9-.5 1.1-1-1.7-.6.9-1-1.3-.3.9-1.2L36 30.1c0 0 1.1-.9 1-.9-.1-.1-1.6-.6-1.6-.6l.7-1-1.2-.5.7-1.1-1.8-1.4c0 0-.6.8-.6 2.1 0 1.1 1.4 4 1.4 4l2.4 6L38.6 36.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23.7 2.7c0 0 0 1.6.1 2.9M14.4 3.3L14.3 4.5M31.4 5c0 0-1 1.5-1.4 2.4M2.2 14.3L4.3 15M1.4 25.7l2.1-.3M2.8 35.5c0 0 1.5-.3 3-1.1M4.8 44.9c0 0 1.3-.4 3.1-1.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
5
web/icons/Year-of Rabbit-6313.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="yoRabbit" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M12,22c0-6.4,4.6,0.1,6.3-5.5c0,0,0.7-0.2,0.7-0.8c0-0.4-1.4-4.7-7.6-4.7c0-5.4-3.7-7-4.8-7C6.2,4,6,4.1,6,4.5c0,3.3,1.1,5.4,3,7.1C6.5,12.3,7.5,18,5,18"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M9,5.1C9,5.1,8.8,3,9.9,3c0.5,0,4.1,1.9,4.1,8.3"/>
|
||||
<path d="M14 13.299999999999999A0.8 0.8 0 1 0 14 14.9A0.8 0.8 0 1 0 14 13.299999999999999Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 586 B |
10
web/icons/Year-of Tiger-22776.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="yoTiger" viewBox="0 0 32 32">
|
||||
<path fill="none" stroke="#000" stroke-linejoin="bevel" stroke-miterlimit="10" stroke-width="2" d="M12,9c2.242,0,4,2,4,2s1.758-2,4-2" id="yoTiger"/>
|
||||
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M4 15c0 0 2.319 2 5 2M3 19c0 0 3.319 2 6 2"/>
|
||||
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M13.871,14.871c0,0-2.871,3.519-2.871,10.129c-4.506,0-8-2-8-2c0-6.665,2.917-11,2.917-11S4,10.443,4,7.927S5.831,4,6.917,4c0.901,1.478,3,3,3,3"/>
|
||||
<path d="M19 21c0 .552-3 2-3 2s-3-1.448-3-2 1.895-1 3-1S19 20.448 19 21zM10 13l5 2-.725 1.001c0 0-.335-.001-2.275-.001S10 13 10 13z"/>
|
||||
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M25.738 11.508C24.157 6.477 20.797 5 19.083 5s-3 1-3 1h-.167c0 0-1.287-1-3-1s-5.073 1.477-6.655 6.508M28 15c0 0-2.319 2-5 2M29 19c0 0-3.319 2-6 2"/>
|
||||
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M18.129 14.871c0 0 2.871 3.519 2.871 10.129 4.506 0 8-2 8-2 0-6.665-2.917-11-2.917-11S28 10.443 28 7.927 26.169 4 25.083 4c-.901 1.478-3 3-3 3M21 25c0 2.792-5 3-5 3s-5-.208-5-3"/>
|
||||
<path d="M22,13l-5,2l0.725,1.001c0,0,0.335-0.001,2.275-0.001S22,13,22,13z"/>
|
||||
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M19.207,27.349C17.549,27.549,16.15,25,16.15,25h-0.3c0,0-1.399,2.549-3.057,2.349"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |