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 = { | |