Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Round the avatars and light them for the theme

5138159281e9940617a4ac399f8b50fa1f5f659b · Keenan Nemetz · 7d ago · parent 2a8c7fbf

Round the avatars and light them for the theme

The tiles are rounded squares now: the element's border-radius does the
clip, so DiceBear's circular mask and the document-wide id it carried are
gone. That also makes a mark about 130 bytes smaller.

The disc in the sky follows the theme. A dark theme gets the moon: pale
cream, near-white and a cool grey. A light theme keeps the sun: cream,
yellow and sunset orange. Switching theme in the account page redraws
the avatars in place, because theme.js writes data-theme on <html> and
the bundle watches that attribute; following the system is the same path
because theme.js resolves it to a concrete variant.

Rebuilt the bundle (10KB, 4KB gzipped) and rendered its real output at
20px and 52px on both themes with the rounded clip applied: the moon
reads against the darker skies and the sun still reads on the light one.

scripts/avatars-entry.js +22−16

@@ -15,9 +15,11 @@
15 15 * Then run `vole workflow run verify` so the licence inventory is checked. */
16 16 import { createAvatar } from "@dicebear/core";
17 17
18 // The sun, from the Everforest palette: cream, yellow, and the warm orange of a
19 // low sunset.
18 // The disc in the sky, from the Everforest palette. In a dark theme it is the
19 // moon: pale cream, near-white and a cool grey. In a light theme it is the sun:
20 // cream, yellow and the warm orange of a low sunset.
20 21 const sunColors = ["f0e6d2", "dbbc7f", "e69875"];
22 const moonColors = ["f0e6d2", "e7e8ea", "cdd6d5"];
21 23
22 24 // Full daylight would wash out at 20px and read as a pastel dot; this is the
23 25 // last hour of it. The mark is drawn in the 100-unit box DiceBear gives a style,
@@ -34,7 +36,7 @@ const shade = (hex, step) => {
34 36
35 37 // DiceBear ships no landscape style, so this one is Vole's. It uses only the
36 38 // engine's contract — a seeded prng, the declared options, and a body for the
37 // 100×100 box — so seeding, the background, and the circular mask are still
39 // 100×100 box — so seeding, the option handling and the viewBox are still
38 40 // DiceBear's.
39 41 const landscape = {
40 42 meta: {
@@ -47,19 +49,21 @@ const landscape = {
47 49 type: "object",
48 50 properties: {
49 51 skyColor: { type: "array", items: { type: "string" }, default: ["6b7561"] },
50 sunColor: { type: "array", items: { type: "string" }, default: sunColors },
52 discColor: { type: "array", items: { type: "string" }, default: sunColors },
51 53 hillCount: { type: "array", items: { type: "integer", minimum: 2, maximum: 3 }, default: [2, 3] },
52 54 },
53 55 },
54 56 create({ prng, options }) {
55 57 const sky = prng.pick(options.skyColor ?? ["6b7561"], "6b7561");
56 const suns = options.sunColor ?? sunColors;
58 const discs = options.discColor ?? sunColors;
57 59 const hills = prng.pick(options.hillCount ?? [2, 3], 2);
58 60 const body = [`<rect width="100" height="100" fill="#${sky}"/>`];
59 61
60 // The sun sits above the highest ridge: the tallest a hill may rise is 66.
62 // The sun or moon sits above the highest ridge: the tallest a hill may rise
63 // is 66, so the highest a ridge can reach is y=34 and the disc never sets
64 // behind it.
61 65 body.push(
62 `<circle cx="${prng.integer(16, 84)}" cy="${prng.integer(18, 32)}" r="${prng.integer(6, 10)}" fill="#${prng.pick(suns)}"/>`,
66 `<circle cx="${prng.integer(16, 84)}" cy="${prng.integer(18, 32)}" r="${prng.integer(6, 10)}" fill="#${prng.pick(discs)}"/>`,
63 67 );
64 68
65 69 // Each hill is a circle whose centre is below the box, so only the ridge
@@ -87,9 +91,9 @@ const ground = (el) => {
87 91 .join("");
88 92 };
89 93
90 // SVG ids are document-wide, and DiceBear names its clip mask the same way for
91 // every mark. A directory page holds fifty of them, so each gets its own id.
92 let drawn = 0;
94 // The theme is on <html> and one of the six Everforest variants, because
95 // theme.js resolves "system" to a concrete one before the first paint.
96 const moonlit = () => (document.documentElement.getAttribute("data-theme") || "").includes("dark");
93 97
94 98 const render = (el) => {
95 99 if (el.querySelector("svg")) return;
@@ -99,18 +103,20 @@ const render = (el) => {
99 103 const svg = createAvatar(landscape, {
100 104 seed,
101 105 size: 40,
102 radius: 50,
106 // The rounded square is the element's border-radius. DiceBear's clip mask
107 // would be a circle, and it is also the one thing it puts an id on.
108 clip: false,
103 109 backgroundColor: [],
104 110 skyColor: sky ? [sky] : undefined,
105 sunColor: sunColors,
111 discColor: moonlit() ? moonColors : sunColors,
106 112 hillCount: [2, 3],
107 113 }).toString();
108 114 // The RDF block names the style and its licence. It is identical for every
109 115 // avatar, so repeating it fifty times on a directory page is weight without
110 // information; the licence is in NOTICE.
111 el.innerHTML = svg
112 .replace(/<metadata[\s\S]*?<\/metadata>/g, "")
113 .replace(/viewboxMask/g, `viewboxMask-${++drawn}`);
116 // information; the licence is in NOTICE. No viewBox mask either: the square
117 // picture is rounded by the element's border-radius, so a mark carries no id
118 // that fifty neighbours would have to share.
119 el.innerHTML = svg.replace(/<metadata[\s\S]*?<\/metadata>/g, "");
114 120 };
115 121
116 122 window.VoleAvatars = {