Additional Icon support (#42)

This commit is contained in:
Simon J
2021-09-03 10:26:49 +10:00
committed by GitHub
parent 9dcac1f324
commit f8f8224213
13 changed files with 653 additions and 257 deletions

View File

@@ -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;

View File

@@ -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",
},
})
)
);
};

View File

@@ -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"?>', ""),
]),

View File

@@ -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;
}
public with = (newTransformation: Partial<Transformation>) =>
new DummyIcon({ ...this.transformation, ...newTransformation });
public toString = () => JSON.stringify(this);
svg: string;
features: Partial<IconFeatures>;
constructor(svg: string, features: Partial<IconFeatures>) {
this.svg = svg;
this.features = features;
}
describe("ColorOverridingIcon", () => {
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("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"],

View File

@@ -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")
});
});
});

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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