Generating SVG Avatars From Identifiers
Table Of Contents
When you create a new social service on the internet, one of the things you need to think about is how to make a user’s space feel like their own. Services like GitHub, Roll20, or Reddit generate — or allow you to generate — a custom avatar (i.e. Identicons) for your account. Avatar auto-generation is especially neat, as it does not require any engagement from the user. Let’s try to figure that out on our own.
Standing on the shoulders of giants
I think this blog post is one of those things which are easier to understand if you’d see the end result first. So, for those interested here’s a spoiler.
Spoiler!
This is the end result:
The avatar has 24 segments and consists of 24 <path>
tags. The coordinates describing paths are static, but their
colors are generated from SHA-256 of the identifier.
1SHA256("Foo") == [ 44, 38, 180, 107, 104, 255, 198, 143,
2 249, 155, 69, 60, 29, 48, 65, 52,
3 19, 66, 45, 112, 100, 131, 191, 160,
4 249, 138, 94, 136, 98, 102, 231, 174]
5
6Base64(SHA256("Foo")) == LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=
The rest of this blog post focuses on translating the byte array into an avatar that looks nice.
SVG scaffolding
Let’s think how to prepare the SVG’s structure. We know that each segment must be its own <path>
element, so we can
color them individually with the fill property. Generating annuluses sectors can be challenging — it would require
using two(!) arcs, which is way too much — instead, we can cheat a little and create overlapping
circle sectors. It will make paths simpler and requires only one(!) arc.
Starting with the <svg>
tag we get:
Paths generation gets much easier if the center of “circles” is in $(0, 0)$. We can do that by setting the viewBox
property as shown above: top-left corner is $(-1, -1)$, width is $2$, and height is $2$.
OK. Now we need to create circle sectors. SVG has some tags for constructing basic shapes (like circles, rectangles,
or ellipses). Sadly, pizza slices are not considered as “basic”, so there’s no <pizza-slice>
to help us here.
Instead, we need to use the <path>
tag.
Making pizza slices 🍕
The <path>
tag is nice, because it allows you to construct any complex shape. For it to work we need to describe how
we want our shape to look like with its special path data syntax. It’s fairly complex, but we only need to grasp 4 of
its commands:
- the absolute “moveto” command — which establishes a new initial point,
- the absolute “lineto” command — which draws a line from the current point to the given $(x, y)$,
- the absolute elliptical arc curve command — which draws an elliptical arc from the current point to the given $(x, y)$,
- the “closepath” command — which ends the current path by connecting it back to its initial point.
To start easy, let’s consider an example with 4 pizza slices:
1<svg overflow="visible" viewBox="-1 -1 2 2" xmlns="http://www.w3.org/2000/svg">
2 <path d="M 0,0 L 0,-1 A 1,1,0,0,1,1,0 z" fill="IndianRed" stroke="black" stroke-width="0.01" />
3 <path d="M 0,0 L 1,0 A 1,1,0,0,1,0,1 z" fill="LightCoral" stroke="black" stroke-width="0.01" />
4 <path d="M 0,0 L 0,1 A 1,1,0,0,1,-1,0 z" fill="Salmon" stroke="black" stroke-width="0.01" />
5 <path d="M 0,0 L -1,0 A 1,1,0,0,1,0,-1 z" fill="DarkSalmon" stroke="black" stroke-width="0.01" />
6</svg>
It renders to:
$$ r_1 = 1 $$
Note
overflow="visible"
to the SVG tag, because — with a non-zero stroke — the image is slightly bigger than
its viewBox
and the stroke lines get cut off at $(-1, 0)$, $(1, 0)$, $(0, 1)$, and $(-1, 0)$.We can see an interesting patter here: coordinates of the “lineto” command are the same, as the coordinates of the elliptical arc curve command of the previous element.
Ok, but how to divide these slices into halves? — you may ask.
To do that we need to find the points in the middle of the arcs.
$$ r_1 = 1 $$
Fortunately — since the circle’s middle point is $(0, 0)$ and the arc points are in the middle of their arcs — we know that the absolute value of their coordinates is the same. So, our diagram simplifies to:
$$ r_1 = 1 $$
Great. 👌
Now we can use the equation of a circle to find the middle points.
$$ \begin{aligned} (x - x_0)^2 + (y - y_0)^2 &= r^2 \\[0.25em] (a - 0)^2 + (a - 0)^2 &= 1^2 \\[0.25em] 2a^2 &= 1 \\[0.25em] a^2 &= \frac{1}{2} \\[0.25em] a &= \sqrt{\frac{1}{2}} \end{aligned} $$
By adding four more pizza slices we get:
$$ r_1 = 1 $$
Layers of pizza
The next step is to create additional (smaller) layers of circle sectors. Let’s say we want three circles; there’re two obvious ways of selecting division points: equal radii, or equal areas. Equal radii is simpler, so let’s try this one first.
If the outermost circle has a radius of $1$, then the middle circle will have a radius of $0.\overline{6}$, and the smallest one will have $0.\overline{3}$.
$$ r_1 = 1 \quad\text{and}\quad r_2 = 0.\overline{6} \quad\text{and}\quad r_3 = 0.\overline{3} $$
Not that it matters, but the smallest circle is a little too small for my liking. But, before we decide, we need to see circles with equal areas first.
$$ \begin{aligned} \Pi r_1^2 - \Pi r_2^2 = \Pi r_2^2 - \Pi r_3^2 \quad&\text{and}\quad \Pi r_2^2 - \Pi r_3^2 = \Pi r_3^2 \\[0.25em] 1 - r_2^2 = r_2^2 - r_3^2 \quad&\text{and}\quad r_2^2 - r_3^2 = r_3^2 \\[0.25em] 1 = 2r_2^2 - r_3^2 \quad&\text{and}\quad r_2^2 = 2r_3^2 \\[0.25em] 1 = 3r_3^2 \quad&\text{and}\quad r_2^2 = 2r_3^2 \\[0.25em] r_3 = \sqrt{\frac{1}{3}} \quad&\text{and}\quad r_2 = \sqrt{\frac{2}{3}} \end{aligned} $$
Which gives us this avatar:
$$ r_1 = 1 \quad\text{and}\quad r_2 = \sqrt{\frac{2}{3}} \quad\text{and}\quad r_3 = \sqrt{\frac{1}{3}} $$
Which is also not ideal, but the other way around… I was tinkering with the radii a bit and I think I found a middle ground with which I’m happy.
$$ r_1 = 1 \quad\text{and}\quad r_2 = \frac{0.\overline{6} + \sqrt{\frac{2}{3}}}{2} \quad\text{and}\quad r_3 = \frac{0.\overline{3} + \sqrt{\frac{1}{3}}}{2} $$
Radii in the circles above are the arithmetic average of the equal radii variant and the equal areas variant.
Working with colors
With the SVG structure out of the way we can focus on selecting quasi-random colors for the circle segments based on the identifier’s hash. SHA-256 hashes have 32 bytes; it’s more than we need — assuming we need a single byte per circle sector — which gives us another benefit: we can slide in the 4th circle, if we want to.
For now though, let’s talk about the colors. If we’d use HSL, we can split each byte into 4 pieces: 4 bits for hue (since it’s the most dominant), 2 bits for saturation, and 2 bits for lightness. Path’s fill property accepts any paint value, which basically means we need to format our color to a standard CSS’s hue.
1fn to_color(byte: u8) -> String {
2 let h = byte >> 4;
3 let s = (byte >> 2) & 0x03;
4 let l = byte & 0x03;
5
6 let normalized_h = (h as f32) / 16.0;
7 let normalized_s = (s as f32) / 4.0;
8 let normalized_l = (l as f32) / 4.0;
9
10 let h = 360.0 * normalized_h;
11 let s = 20.0 + 80.0 * normalized_s;
12 let l = 40.0 + 50.0 * normalized_l;
13
14 format!("hsl({h}, {s}%, {l}%)")
15}
Alright, let’s generate an avatar for "foo"
and see how it looks like.
Well, it does not look terrible, but it’s not eye-catching either. To be perfectly honest, we should expect something like this; SHA-256 returns (and for that matter all other hash functions too) a quasi-random values. If we convert those raw bytes to fill colors, we’ll get quasi-random colors. To make it nicer we need to modify our algorithm slightly; we need to calculate a global theme, if you will, for the avatar, so that a part of circle segments' colors come from that theme. We can do that by folding all hash bytes into a single one with XOR.
1fn to_color(normalized_theme: f32, byte: u8) -> String {
2 let h = byte >> 4;
3 let s = (byte >> 2) & 0x03;
4 let l = byte & 0x03;
5
6 let normalized_h = (h as f32) / 16.0;
7 let normalized_s = (s as f32) / 4.0;
8 let normalized_l = (l as f32) / 4.0;
9
10 let h = 360.0 * normalized_theme + 120.0 * normalized_h;
11 let s = 20.0 + 80.0 * normalized_s;
12 let l = 40.0 + 50.0 * normalized_l;
13
14 format!("hsl({h}, {s}%, {l}%)")
15}
16
17fn calculate_theme(bytes: &[u8]) -> f32 {
18 let theme = bytes.iter()
19 .fold(0u8, |acc, byte| acc ^ byte);
20 (theme as f32) / (u8::MAX as f32)
21}
22
23fn generate_paths(hash: [u8; 32]) {
24 let theme = calculate_theme(&hash);
25
26 let colors = hash.iter()
27 .cloned()
28 .map(|byte| to_color(theme, byte))
29 .collect::<Vec<_>>();
30
31 unimplemented!("Generate SVG paths.");
32}
This code renders:
It looks rather good, if I say so myself. The solution works as it’s suppose to: it most probably produces different avatars for different identifiers (hash collisions do happen) and they look acceptable. However, the rings seem to phase into each other — their colors are too similar. We could experiment with another theme: a ring theme — which would be a XOR-fold of all bytes that represent a single ring — and check the results.
1fn to_color(normalized_theme: f32, normalized_ring_theme: f32, byte: u8) -> String {
2 let h = byte >> 4;
3 let s = (byte >> 2) & 0x03;
4 let l = byte & 0x03;
5
6 let normalized_h = (h as f32) / 16.0;
7 let normalized_s = (s as f32) / 4.0;
8 let normalized_l = (l as f32) / 4.0;
9
10 let h = 360.0 * normalized_theme
11 + 120.0 * normalized_ring_theme
12 + 30.0 * normalized_h;
13 let s = 20.0 + 80.0 * normalized_s;
14 let l = 40.0 + 50.0 * normalized_l;
15
16 format!("hsl({h}, {s}%, {l}%)")
17}
18
19fn calculate_theme(bytes: &[u8]) -> f32 {
20 let theme = bytes.iter().fold(0u8, |acc, byte| acc ^ byte);
21 (theme as f32) / (u8::MAX as f32)
22}
23
24fn generate_paths(hash: [u8; 32]) {
25 let theme = calculate_theme(&hash);
26 let ring_themes = hash
27 .windows(8)
28 .map(calculate_theme)
29 .collect::<Vec<_>>();
30
31 let colors = hash
32 .iter()
33 .cloned()
34 .enumerate()
35 .map(|(idx, byte)| to_color(theme, ring_themes[idx % 8], byte))
36 .collect::<Vec<_>>();
37
38 unimplemented!("Generate SVG paths.");
39}
With the final touch we get:
Which is exactly the thing that hides in the spoiler block at the top. If you restrained yourself and didn’t check it out: congratulations. Now feel free to do so. 😃
Conclusions
Working on this blog post has been an effort I wanted to make to better understand how SVGs work from the inside, as well as, a challenge to reverse-engineer ideas from Representing SHA-256 Hashes As Avatars.
I’m not working on any service which would benefit from these avatars. If I happen to work on one in the future I’ll for
sure like to try them out. To make this solution easier to re-use in other places, I’ve made a Rust crate called
svg_avatars
, which implements this exact solution. The crate is fairly minimal, so if anyone has any idea on how to
enhance it, I’d love to hear from you/check out your PRs.
As a closing thought, one of the parameters of the absolute elliptical arc curve command is the
sweep-flag
parameter. It allows you to determine, whether an arc should be a smiley face or a sad face. If the
parameter is 1
— as it is in our case — then the arc is a sad face. However, if you’d flip the flags to be 0
,
then all arcs become smiley faces…
…and the avatar emerges as a spiderweb. Who does not love a single-line-change feature for Spooktober.
Interested in my work?
Consider subscribing to the RSS Feed or joining my mailing list: madebyme-notifications on Google Groups .
Disclaimer: Only group owner (i.e. me) can view e-mail addresses of group members. I will not share your e-mail with any third-parties — it will be used exclusively to notify you about new blog posts.