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 a vole portrait for every name
753cd703960a1f024b3e5819ae6330bb2ab1cd08 · Keenan Nemetz · 7d ago · parent a6b42433
Draw a vole portrait for every name The wordmark is a vole's face, so a name gets one too: the profile, the directory, comment cards, inline review notes, approvals and the contributor list all render a small vole drawn from the account name. The portrait is derived with SHA-256 rather than stored. The same name draws the same ears, eyes, whiskers, fur shade and ground colour on every page and between visits, and the instance keeps no image, accepts no upload, and calls no avatar service. The seven grounds are the Everforest accents pulled toward the page, the head and features use the palette's cream and dark ground, and the picture is inline SVG: the CSP, the single binary, and an instance with no outbound requests are all unchanged. The name never reaches the markup. The builder takes only integer traits, which is what makes it safe to hand the template as HTML, and a test holds that a hostile name cannot appear in the output. Two shapes were tried and dropped. Separating the ears with a ground-coloured gap, the way the wordmark does, collapses into noise below 24px, so the ears and head are one silhouette. Cheek patches and brow strokes read as smudges at the same size, so a blaze down the forehead is the only marking left; the rest of the variation is colour.
server/web/static/vole.css +32−16
| @@ -1082,7 +1082,7 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; } | |||
| 1082 | 1082 | color: var(--fg-muted); | |
| 1083 | 1083 | font-size: calc(12.65px * var(--font-scale)); | |
| 1084 | 1084 | } | |
| 1085 | .inline-note-head .avatar { width: 17px; height: 17px; font-size: calc(8.8px * var(--font-scale)); } | ||
| 1085 | .inline-note-head .avatar { width: 17px; height: 17px; } | ||
| 1086 | 1086 | .inline-note-head .comment-author { color: var(--fg); font-size: calc(13.2px * var(--font-scale)); } | |
| 1087 | 1087 | .inline-note .comment-body { | |
| 1088 | 1088 | padding: 0; | |
| @@ -1160,6 +1160,12 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; } | |||
| 1160 | 1160 | .contributors .cname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: calc(13.75px * var(--font-scale)); } | |
| 1161 | 1161 | .contributors .muted { font-size: calc(12.65px * var(--font-scale)); } | |
| 1162 | 1162 | ||
| 1163 | /* A name's portrait is a vole drawn from the name (see server/web/avatar.go). | ||
| 1164 | The sheet owns the palette and the SVG owns the geometry, so the face follows | ||
| 1165 | the theme: the seven grounds are the Everforest accents pulled toward the page, | ||
| 1166 | and the fur and ink are the palette's own cream and dark ground. The tones are | ||
| 1167 | the same in every Everforest variant on purpose; a person who is moss green on | ||
| 1168 | one theme must not be clay on another. */ | ||
| 1163 | 1169 | .avatar { | |
| 1164 | 1170 | display: inline-grid; | |
| 1165 | 1171 | place-items: center; | |
| @@ -1167,20 +1173,30 @@ pre code { background: none; border: 0; padding: 0; font-size: inherit; } | |||
| 1167 | 1173 | width: 20px; | |
| 1168 | 1174 | height: 20px; | |
| 1169 | 1175 | border-radius: 50%; | |
| 1170 | font-family: var(--font); | ||
| 1171 | font-size: calc(11px * var(--font-scale)); | ||
| 1172 | font-weight: 600; | ||
| 1173 | line-height: 1; | ||
| 1174 | color: #fff; | ||
| 1175 | background: #4b5563; | ||
| 1176 | } | ||
| 1177 | .avatar.c1 { background: #5b67d8; } | ||
| 1178 | .avatar.c2 { background: #2f7d6d; } | ||
| 1179 | .avatar.c3 { background: #9a5b3c; } | ||
| 1180 | .avatar.c4 { background: #8a4b78; } | ||
| 1181 | .avatar.c5 { background: #3c6e9a; } | ||
| 1182 | .avatar.c6 { background: #7a6a2f; } | ||
| 1183 | .avatar.c7 { background: #5f5b6e; } | ||
| 1176 | overflow: hidden; | ||
| 1177 | background: var(--av-bg, #6b7561); | ||
| 1178 | --av-fur: #f0e6d2; | ||
| 1179 | --av-ink: #2d353b; | ||
| 1180 | --av-mark: #d8c5a3; | ||
| 1181 | } | ||
| 1182 | .avatar.c1 { --av-bg: #6f8a5f; } | ||
| 1183 | .avatar.c2 { --av-bg: #a2734f; } | ||
| 1184 | .avatar.c3 { --av-bg: #a05f66; } | ||
| 1185 | .avatar.c4 { --av-bg: #5b7f92; } | ||
| 1186 | .avatar.c5 { --av-bg: #816a9b; } | ||
| 1187 | .avatar.c6 { --av-bg: #94843f; } | ||
| 1188 | .avatar.c7 { --av-bg: #7a6b5f; } | ||
| 1189 | .avatar svg { display: block; width: 100%; height: 100%; } | ||
| 1190 | .avatar .fur-2 { --av-fur: #e6d5b3; } | ||
| 1191 | .avatar .fur-3 { --av-fur: #f7f0de; } | ||
| 1192 | .avatar .av-fur { fill: var(--av-fur); } | ||
| 1193 | .avatar .av-head { fill: var(--av-fur); } | ||
| 1194 | .avatar .av-ink { fill: var(--av-ink); } | ||
| 1195 | .avatar .av-mark { fill: var(--av-mark); } | ||
| 1196 | .avatar .av-ring { fill: none; stroke: var(--av-ink); stroke-width: 1.1; } | ||
| 1197 | .avatar .av-nose { fill: var(--av-ink); stroke: var(--av-ink); stroke-width: 0.7; stroke-linejoin: round; } | ||
| 1198 | .avatar .av-line { fill: none; stroke: var(--av-ink); stroke-width: 0.95; stroke-linecap: round; stroke-linejoin: round; } | ||
| 1199 | .avatar .av-whisker { fill: none; stroke: var(--av-ink); stroke-width: 0.75; stroke-linecap: round; opacity: 0.8; } | ||
| 1184 | 1200 | ||
| 1185 | 1201 | .lang { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; margin-bottom: 8px; font-size: calc(13.2px * var(--font-scale)); } | |
| 1186 | 1202 | .lang .muted { font-size: calc(12.65px * var(--font-scale)); } | |
| @@ -2892,7 +2908,7 @@ textarea:focus-visible { | |||
| 2892 | 2908 | .profile-joined { margin: 4px 0 0; font-size: calc(12.65px * var(--font-scale)); } | |
| 2893 | 2909 | .profile-scope { margin: 8px 0 0; font-size: calc(12.65px * var(--font-scale)); } | |
| 2894 | 2910 | ||
| 2895 | .avatar-lg { width: 52px; height: 52px; font-size: calc(22px * var(--font-scale)); } | ||
| 2911 | .profile-who .avatar { width: 52px; height: 52px; } | ||
| 2896 | 2912 | ||
| 2897 | 2913 | /* The four numbers a profile leads with. Each one is a link to the feed it | |
| 2898 | 2914 | counts, so a number is a door rather than a dead end. */ | |