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 landscapes instead of DiceBear's Shapes style

2a8c7fbf4b2670a386ab2c203db20bb90489263a · Keenan Nemetz · 7d ago · parent 97f77001

Draw landscapes instead of DiceBear's Shapes style

DiceBear ships no landscape style, so the bundle now carries one written
against its core API. The engine that matters is still DiceBear's — the
seeded PRNG, the option handling, the 100×100 box and the circular mask
— while the style is part of Vole and carries this project's licence.
That drops the Shapes licence file and leaves DiceBear's own MIT notice.

The art is one idea: the account's tone is the sky, two or three hills
are the same colour a step darker, and the sun is drawn from Everforest's
cream, yellow or sunset orange. Each hill is a circle whose centre sits
below the box, so only its ridge shows; drawing them back to front with
one step of shade between them is what gives the depth at 17px without
an outline. The sun is placed above the highest ridge, so it is never
lost behind a hill, and a low ridge can swallow its foot like a sunset.

The bundle is now 10KB (4KB gzipped) and an avatar is about 470 bytes
of SVG, down from 18KB and 800 because Shapes was the bigger style. The
server's half is unchanged: the initial on the account's ground is drawn
first and stays if the script never runs.

scripts/avatars-entry.js +79−20

@@ -1,54 +1,113 @@
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.
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).
4 6 *
5 7 * Built with esbuild. To rebuild after changing the palette or the versions:
6 8 *
7 9 * 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
10 * npm i @dicebear/core@9.3.1 esbuild@0.25.9
9 11 * npx esbuild /path/to/scripts/avatars-entry.js \
10 12 * --bundle --minify --format=iife \
11 13 * --outfile=/path/to/server/web/static/dicebear-avatars.min.js
12 14 *
13 15 * Then run `vole workflow run verify` so the licence inventory is checked. */
14 16 import { createAvatar } from "@dicebear/core";
15 import * as shapes from "@dicebear/shapes";
17
18 // The sun, from the Everforest palette: cream, yellow, and the warm orange of a
19 // low sunset.
20 const sunColors = ["f0e6d2", "dbbc7f", "e69875"];
21
22 // Full daylight would wash out at 20px and read as a pastel dot; this is the
23 // last hour of it. The mark is drawn in the 100-unit box DiceBear gives a style,
24 // and the ground comes from the element so vole.css stays the only palette: the
25 // sky is the account's tone and the hills are steps darker than it, which is what
26 // makes a landscape read at 17px without an outline.
27 const depth = [0.34, 0.5, 0.62];
28
29 const shade = (hex, step) => {
30 const n = parseInt(hex, 16);
31 const channel = (v) => Math.round(v * (1 - step)).toString(16).padStart(2, "0");
32 return channel((n >> 16) & 255) + channel((n >> 8) & 255) + channel(n & 255);
33 };
34
35 // DiceBear ships no landscape style, so this one is Vole's. It uses only the
36 // 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
38 // DiceBear's.
39 const landscape = {
40 meta: {
41 title: "Landscape",
42 source: "https://github.com/rodent-software/vole",
43 creator: "Vole",
44 license: { name: "Apache-2.0", url: "https://www.apache.org/licenses/LICENSE-2.0" },
45 },
46 schema: {
47 type: "object",
48 properties: {
49 skyColor: { type: "array", items: { type: "string" }, default: ["6b7561"] },
50 sunColor: { type: "array", items: { type: "string" }, default: sunColors },
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 suns = options.sunColor ?? sunColors;
57 const hills = prng.pick(options.hillCount ?? [2, 3], 2);
58 const body = [`<rect width="100" height="100" fill="#${sky}"/>`];
59
60 // The sun sits above the highest ridge: the tallest a hill may rise is 66.
61 body.push(
62 `<circle cx="${prng.integer(16, 84)}" cy="${prng.integer(18, 32)}" r="${prng.integer(6, 10)}" fill="#${prng.pick(suns)}"/>`,
63 );
64
65 // Each hill is a circle whose centre is below the box, so only the ridge
66 // shows. Drawing them back to front, each a step darker, gives the depth.
67 for (let i = 0; i < hills; i++) {
68 const r = prng.integer(70, 130);
69 const rise = prng.integer(40, 66) - i * prng.integer(5, 10);
70 body.push(
71 `<circle cx="${prng.integer(-20, 120)}" cy="${100 + r - rise}" r="${r}" fill="#${shade(sky, depth[i])}"/>`,
72 );
73 }
74 return { attributes: { viewBox: "0 0 100 100" }, body: body.join("") };
75 },
76 };
16 77
17 78 // The ground comes from the element, not from this file: vole.css owns the seven
18 79 // Everforest grounds, and reading the computed colour keeps the initial the
19 // server renders and the finished mark on the same ground.
80 // server renders and the finished landscape on the same sky.
20 81 const ground = (el) => {
21 82 const parts = (getComputedStyle(el).backgroundColor.match(/\d+/g) || []).map(Number);
83 if (parts.length < 3) return "";
22 84 return parts
23 85 .slice(0, 3)
24 86 .map((n) => n.toString(16).padStart(2, "0"))
25 87 .join("");
26 88 };
27 89
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.
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.
34 92 let drawn = 0;
35 93
36 94 const render = (el) => {
37 95 if (el.querySelector("svg")) return;
38 96 const seed = el.getAttribute("data-avatar");
39 97 if (!seed) return;
40 const svg = createAvatar(shapes, {
98 const sky = ground(el);
99 const svg = createAvatar(landscape, {
41 100 seed,
42 101 size: 40,
43 102 radius: 50,
44 backgroundColor: [ground(el)],
45 shape1Color: marks,
46 shape2Color: marks,
47 shape3Color: marks,
103 backgroundColor: [],
104 skyColor: sky ? [sky] : undefined,
105 sunColor: sunColors,
106 hillCount: [2, 3],
48 107 }).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.
108 // The RDF block names the style and its licence. It is identical for every
109 // avatar, so repeating it fifty times on a directory page is weight without
110 // information; the licence is in NOTICE.
52 111 el.innerHTML = svg
53 112 .replace(/<metadata[\s\S]*?<\/metadata>/g, "")
54 113 .replace(/viewboxMask/g, `viewboxMask-${++drawn}`);