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
This blog post has been inspired by Representing SHA-256 Hashes As Avatars by François Best. I’m re-creating ideas presented by François in their article — with slight modifications — in Rust.

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:

Avatar for identifier "Foo".

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:

1<svg viewBox="-1 -1 2 2" xmlns="http://www.w3.org/2000/svg">
2    <!-- ... -->
3</svg>

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:

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
I added 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.

(ax, ay)(bx, by)(cx, cy)(dx, dy)

$$ 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:

(a, -a)(a, a)(-a, -a)(-a, a)

$$ 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.

Avatar for "foo".

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:

Avatar for "foo" with a theme.

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:

Avatar for "foo" with a global theme and a ring theme.

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…

Smiley-face avatar for "foo".

…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.