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.

Draw avatars with DiceBear instead of by hand

97f77001eed76cc287aeba532c4d670488121a18 · Keenan Nemetz · 7d ago · parent ca098f62

Draw avatars with DiceBear instead of by hand

The marks this package drew itself were a design I kept losing arguments
with. Avatars are now DiceBear's Shapes style, which is what the request
asked for.

DiceBear is JavaScript and Vole is a Go binary, so the split is: the
server renders the account's initial on its Everforest ground, and an
embedded bundle replaces it with the mark when the page runs. The
initial is what a browser without scripts keeps, so nothing depends on
the bundle arriving; the seed is the account name, so the same person
draws the same mark everywhere; and the ground is read from the element,
so vole.css remains the only palette.

The bundle is core 9.3.1 plus Shapes 9.4.2 (MIT code, CC0 design), built
with esbuild and committed like the other vendored scripts: 18KB, 5.5KB
gzipped. Its source is scripts/avatars-entry.js, and the licence files
beside it are inventoried in NOTICE, so the verify workflow's licence
check keeps passing. No request leaves the instance and no avatar is
stored.

Two details worth naming. Each mark gets a unique clip-mask id, because
SVG ids are document-wide and a directory page holds fifty of them. And
the RDF metadata block DiceBear embeds is stripped: it is identical for
every avatar and its licence is in NOTICE, so repeating it in the DOM
would be weight without information.

scripts/avatars-entry.js +62

@@ -0,0 +1,62 @@
1 /*! Vole avatars: DiceBear core 9.3.1 + Shapes 9.4.2, bundled into one script.
2 * MIT (code) and CC0 1.0 (Shapes design). See server/web/static/dicebear.LICENSE
3 * and server/web/static/dicebear-shapes.LICENSE.
4 *
5 * Built with esbuild. To rebuild after changing the palette or the versions:
6 *
7 * mkdir -p /tmp/dicebear && cd /tmp/dicebear && npm init -y
8 * npm i @dicebear/core@9.3.1 @dicebear/shapes@9.4.2 esbuild@0.25.9
9 * npx esbuild /path/to/scripts/avatars-entry.js \
10 * --bundle --minify --format=iife \
11 * --outfile=/path/to/server/web/static/dicebear-avatars.min.js
12 *
13 * Then run `vole workflow run verify` so the licence inventory is checked. */
14 import { createAvatar } from "@dicebear/core";
15 import * as shapes from "@dicebear/shapes";
16
17 // The ground comes from the element, not from this file: vole.css owns the seven
18 // Everforest grounds, and reading the computed colour keeps the initial the
19 // server renders and the finished mark on the same ground.
20 const ground = (el) => {
21 const parts = (getComputedStyle(el).backgroundColor.match(/\d+/g) || []).map(Number);
22 return parts
23 .slice(0, 3)
24 .map((n) => n.toString(16).padStart(2, "0"))
25 .join("");
26 };
27
28 // Everforest's cream, dark ground, green, blue and yellow. The ground makes the
29 // sixth colour, so a mark is at most two of these plus its ground.
30 const marks = ["f0e6d2", "2d353b", "a7c080", "7fbbb3", "dbbc7f"];
31
32 // SVG ids are document-wide, and every DiceBear mark names its clip mask the same
33 // way. A directory page holds fifty of them, so each mark gets its own id.
34 let drawn = 0;
35
36 const render = (el) => {
37 if (el.querySelector("svg")) return;
38 const seed = el.getAttribute("data-avatar");
39 if (!seed) return;
40 const svg = createAvatar(shapes, {
41 seed,
42 size: 40,
43 radius: 50,
44 backgroundColor: [ground(el)],
45 shape1Color: marks,
46 shape2Color: marks,
47 shape3Color: marks,
48 }).toString();
49 // The RDF metadata block names the style and its licence. It is identical for
50 // every avatar and the licence is in NOTICE, so repeating it fifty times in a
51 // directory page would be weight without information.
52 el.innerHTML = svg
53 .replace(/<metadata[\s\S]*?<\/metadata>/g, "")
54 .replace(/viewboxMask/g, `viewboxMask-${++drawn}`);
55 };
56
57 window.VoleAvatars = {
58 render,
59 renderAll(root) {
60 root.querySelectorAll("[data-avatar]").forEach(render);
61 },
62 };