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.

Remove the avatars and read people by name again

28b19c7433fa6f6fe1ce826c443870385d856eb8 · Keenan Nemetz · 7d ago · parent 0fbeca85

Remove the avatars and read people by name again

The marks were harder to read than the names they replaced, so they are
gone: the account menu is back to the user icon, and the issue and
change tables, the profile, the directory and the contributor list all
say the account name.

This takes out everything that existed to draw a mark: the DiceBear
bundle and its licence file, the entry source that built it, the
Landscape style, avatar.go and its tests, the avatar template and its
three helper functions, the avatar palette in everforest.css and the
illustration rules in vole.css, and the app.js block that rendered and
re-rendered them on a theme change.

The tables keep the layout from the previous change — an author column
and an assignee column — because that part reads better with names too.
The assignee cells are the @name chips that already linked to the
filtered list.

`vole workflow run verify` passes, and the licence inventory is back to
one entry fewer.

scripts/avatars-entry.js −189

@@ -1,189 +0,0 @@
1 /*! Vole avatars: a landscape drawn by DiceBear core 9.3.1.
2 *
3 * DiceBear is MIT (see server/web/static/dicebear.LICENSE); the Landscape style
4 * below is part of Vole and carries the same licence as the rest of the program
5 * (Apache-2.0).
6 *
7 * Built with esbuild. To rebuild after changing the palette or the versions:
8 *
9 * mkdir -p /tmp/dicebear && cd /tmp/dicebear && npm init -y
10 * npm i @dicebear/core@9.3.1 esbuild@0.25.9
11 * npx esbuild /path/to/scripts/avatars-entry.js \
12 * --bundle --minify --format=iife \
13 * --outfile=/path/to/server/web/static/dicebear-avatars.min.js
14 *
15 * Then run `vole workflow run verify` so the licence inventory is checked. */
16 import { createAvatar } from "@dicebear/core";
17
18 // Back to front, each hill is a palette hue shaded a step toward the theme's ink,
19 // so the depth reads without an outline.
20 const depth = [0.4, 0.56, 0.68];
21
22 const shade = (hex, step, ink) => {
23 const a = parseInt(hex, 16);
24 const b = parseInt(ink, 16);
25 const channel = (shift) => {
26 const va = (a >> shift) & 255;
27 const vb = (b >> shift) & 255;
28 return Math.round(va + (vb - va) * step).toString(16).padStart(2, "0");
29 };
30 return channel(16) + channel(8) + channel(0);
31 };
32
33 // DiceBear ships no landscape style, so this one is Vole's. It uses only the
34 // engine's contract — a seeded prng, the declared options, and a body for the
35 // 100×100 box — so seeding, the option handling and the viewBox are still
36 // DiceBear's.
37 const landscape = {
38 meta: {
39 title: "Landscape",
40 source: "https://github.com/rodent-software/vole",
41 creator: "Vole",
42 license: { name: "Apache-2.0", url: "https://www.apache.org/licenses/LICENSE-2.0" },
43 },
44 schema: {
45 type: "object",
46 properties: {
47 skyColor: { type: "array", items: { type: "string" }, default: ["6b7561"] },
48 palette: { type: "array", items: { type: "string" }, default: [] },
49 ink: { type: "string", default: "272e33" },
50 discColor: { type: "array", items: { type: "string" }, default: ["f0e6d2"] },
51 hillCount: { type: "array", items: { type: "integer", minimum: 2, maximum: 3 }, default: [2, 3] },
52 },
53 },
54 create({ prng, options }) {
55 const sky = prng.pick(options.skyColor ?? ["6b7561"], "6b7561");
56 const ink = options.ink ?? "272e33";
57 const discs = options.discColor ?? ["f0e6d2"];
58 const hills = prng.pick(options.hillCount ?? [2, 3], 2);
59 const body = [`<rect width="100" height="100" fill="#${sky}"/>`];
60
61 // Hills take hues from the rest of the palette, never the sky's own, so every
62 // mark has at least two colours. Shuffling keeps the pairing stable per seed
63 // while making it depend on the name rather than on the sky alone.
64 const ground = (options.palette ?? []).filter((c) => c && c !== sky);
65 for (let i = ground.length - 1; i > 0; i--) {
66 const j = prng.integer(0, i);
67 [ground[i], ground[j]] = [ground[j], ground[i]];
68 }
69
70 // The sun or moon sits above the highest ridge: the tallest a hill may rise
71 // is 66, so the highest a ridge can reach is y=34 and the disc never sets
72 // behind it.
73 body.push(
74 `<circle cx="${prng.integer(16, 84)}" cy="${prng.integer(18, 32)}" r="${prng.integer(6, 10)}" fill="#${prng.pick(discs)}"/>`,
75 );
76
77 // Each hill is a circle whose centre is below the box, so only the ridge
78 // shows. Back to front, each one is a palette hue shaded toward the ink.
79 for (let i = 0; i < hills; i++) {
80 const r = prng.integer(70, 130);
81 const rise = prng.integer(40, 66) - i * prng.integer(5, 10);
82 const hue = ground.length ? ground[i % ground.length] : sky;
83 body.push(
84 `<circle cx="${prng.integer(-20, 120)}" cy="${100 + r - rise}" r="${r}" fill="#${shade(hue, depth[i], ink)}"/>`,
85 );
86 }
87 return { attributes: { viewBox: "0 0 100 100" }, body: body.join("") };
88 },
89 };
90
91 // getComputedStyle serializes a resolved colour two ways: rgb()/rgba() for a
92 // legacy sRGB value, and color(srgb r g b) with components in 0..1 when the value
93 // came through color-mix(). Reading "the first three numbers" therefore splits
94 // 0.571765 into 0 and 571765 and builds a six-digit-looking string that is not a
95 // colour; the browser then paints the sky black. Parse both forms, and clamp,
96 // because color() components are floats.
97 const toHex = (value) => {
98 const text = value.trim();
99 const srgb = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/.exec(text);
100 if (srgb) {
101 return [1, 2, 3]
102 .map((i) => Math.round(Math.min(1, Math.max(0, parseFloat(srgb[i]))) * 255))
103 .map((n) => n.toString(16).padStart(2, "0"))
104 .join("");
105 }
106 const legacy = /^rgba?\(([^)]+)\)/.exec(text);
107 if (legacy) {
108 const parts = legacy[1].split(/[\s,/]+/).filter(Boolean).map(Number);
109 if (parts.length >= 3) {
110 return parts
111 .slice(0, 3)
112 .map((n) => Math.round(Math.min(255, Math.max(0, n))))
113 .map((n) => n.toString(16).padStart(2, "0"))
114 .join("");
115 }
116 }
117 return "";
118 };
119
120 // A custom property can hold a color-mix(), and getComputedStyle reads that back
121 // as text rather than as a colour. One hidden element resolves any of them to the
122 // colour the browser actually paints, so the palette follows the theme exactly.
123 let probe;
124 const resolve = (name) => {
125 if (!probe) {
126 probe = document.createElement("span");
127 probe.hidden = true;
128 document.body.appendChild(probe);
129 }
130 probe.style.color = `var(${name})`;
131 return toHex(getComputedStyle(probe).color);
132 };
133
134 // The palette follows the active Everforest variant, so it is resolved once per
135 // theme and read again when the theme changes (app.js redraws the avatars).
136 let cached = { theme: null, palette: [], ink: "", discs: [] };
137 const themePalette = () => {
138 const theme = document.documentElement.getAttribute("data-theme") || "";
139 if (cached.theme === theme) return cached;
140 const palette = [];
141 for (let i = 1; i <= 7; i++) {
142 const colour = resolve(`--av-c${i}`);
143 if (colour) palette.push(colour);
144 }
145 cached = {
146 theme,
147 palette,
148 ink: resolve("--av-ink") || "272e33",
149 discs: [resolve("--av-disc-1"), resolve("--av-disc-2"), resolve("--av-disc-3")].filter(Boolean),
150 };
151 return cached;
152 };
153
154 // The sky comes from the element, not from this file: vole.css and everforest.css
155 // own the seven grounds, and reading the computed colour keeps the initial the
156 // server renders and the finished landscape on the same sky.
157 const ground = (el) => toHex(getComputedStyle(el).backgroundColor);
158
159 const render = (el) => {
160 if (el.querySelector("svg")) return;
161 const seed = el.getAttribute("data-avatar");
162 if (!seed) return;
163 const sky = ground(el);
164 const theme = themePalette();
165 const svg = createAvatar(landscape, {
166 seed,
167 size: 40,
168 // The rounded square is the element's border-radius. DiceBear's clip mask
169 // would be a circle, and it is also the one thing it puts an id on.
170 clip: false,
171 backgroundColor: [],
172 skyColor: sky ? [sky] : undefined,
173 palette: theme.palette,
174 ink: theme.ink,
175 discColor: theme.discs.length ? theme.discs : undefined,
176 hillCount: [2, 3],
177 }).toString();
178 // The RDF block names the style and its licence. It is identical for every
179 // avatar, so repeating it fifty times on a directory page is weight without
180 // information; the licence is in NOTICE.
181 el.innerHTML = svg.replace(/<metadata[\s\S]*?<\/metadata>/g, "");
182 };
183
184 window.VoleAvatars = {
185 render,
186 renderAll(root) {
187 root.querySelectorAll("[data-avatar]").forEach(render);
188 },
189 };