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 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. */