title: Representing SHA-256 Hashes As Avatars url: https://francoisbest.com/posts/2021/hashvatars hash_url: be7ca5728939adfc50b9eef6818d6f63

If you follow me on Twitter, you may be aware of my weird weekend projects.

They are little challenges I give myself, usually without too many stakes involved, and with small enough a scope so that I can ship it in a day or two, while keeping spare family time.

This weekend's project is to build a transaction explorer for the Centralized Coin experiment. Something like a blockchain explorer, but without the crypto overhead.

Each node in the system is represented by a hash. Because humans are terrible at reading and quickly identifying large numbers (other than by their first or last few digits), a visual representation is needed.

There are many solutions out there: WordPress and GitHub identicons, robohash, monsterID etc..

I wanted one that still looks abstract (not as opinionated as monsters or robots), and that plays nice in a rounded avatar UI component.

This is what I ended up with:
Change the input below to see how the SHA-256 hash changes the render

If you are allergic to maths and trigonometry, feel free to play with the more detailed interactive example here. Otherwise, let's dive into how it's done.

Space Partitioning#

Many of the existing solutions produce square images, yet avatars are often displayed as circles. They would lose information on the corners when rounded, so instead of a cartesian (x-y) approach, we're going to use polar (angle-radius) coordinates instead.

A grid in cartesian space maps to concentric circles and pie-like cuts in polar space.

SHA-256 hashes have 256 bits of information that we need to represent. Dividing a circle into 256 sections would make each section too small to be visually useful, and would only leave 1 bit of "value" to represent in each section (0 or 1, black or white).

Instead, we're going to divide the circle into 32 sections:

The resulting SVG code for such a grid looks like this (in a React component):

export const SHA256Avatar = () => {
  
  const r1 = 1
  const r2 = r1 * 0.75
  const r3 = r1 * 0.5
  const r4 = r1 * 0.25
  return (
    <svg viewBox="-1 -1 2 2">
      <circle cx={0} cy={0} r={r1} />
      <circle cx={0} cy={0} r={r2} />
      <circle cx={0} cy={0} r={r3} />
      <circle cx={0} cy={0} r={r4} />
      <line x1={-r1} x2={r1} y1={0} y2={0} />
      <line y1={-r1} y2={r1} x1={0} x2={0} />
      <line
        x1={-r1 * Math.SQRT1_2}
        x2={r1 * Math.SQRT1_2}
        y1={-r1 * Math.SQRT1_2}
        y2={r1 * Math.SQRT1_2}
      />
      <line
        x1={r1 * Math.SQRT1_2}
        x2={-r1 * Math.SQRT1_2}
        y1={-r1 * Math.SQRT1_2}
        y2={r1 * Math.SQRT1_2}
      />
    </svg>
  )
}

Doing this naively, with each concentric ring's radius being 1/4th of the outermost/largest one gives us this:

There are some issues: the innermost ring sections are tiny compared to the outermost.

If we calculate the radii so that each section has an equal area, we get the following result:

Not very pleasing either. How about a mix of both ?
(Play with the slider to blend between equal radii and equal areas)

I don't know about you, but 0.42 hits the ballpark both in aesthetics and nerd-sniping satisfaction, so let's go for that.

Section Mapping#

Now that we have 32 nice looking sections on our circle, we can map each section to an 8-bit value in the hash.

As an example, let's take the following hash, the output of sha256("Hello, world!"):

315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3

We can split it in 32 blocks of 8 bits (2 hexadecimal digits), and organise them by 4 blocks of 8 to map to the rings:

12 o'clock -> clockwise
31 5f 5b db 76 d0 78 c4  Outer ring
3b 8a c0 06 4e 4a 01 64  Middle-outer ring
61 2b 1f ce 77 c8 69 34  Middle-inner ring
5b fc 94 c7 58 94 ed d3  Inner ring
315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3

In order to fill each section with a different colour, we generate an SVG <path> polygon. Each section resembles a pie/pizza slice, going from the center of the circle to a given radius, and covering 1/8th of the circle.

Since SVG uses cartesian coordinates, we'll have to convert our polar logic into cartesian SVG path commands:

interface Point {
  x: number
  y: number
}

function polarPoint(radius: number, angle: number): Point {
  
  
  
  return {
    x: radius * Math.cos(2 * Math.PI * angle - Math.PI / 2),
    y: radius * Math.sin(2 * Math.PI * angle - Math.PI / 2)
  }
}

function moveTo({ x, y }: Point) {
  return `M ${x} ${y}`
}

function lineTo({ x, y }: Point) {
  return `L ${x} ${y}`
}

function arcTo({ x, y }: Point, radius: number) {
  return `A ${radius} ${radius} 0 0 0 ${x} ${y}`
}

const Section = ({ index, radius }) => {
  const angleA = index / 8
  const angleB = (index + 1) / 8

const path = [

moveTo({ x: 0, y: 0 }),

lineTo(polarPoint(radius, angleA)),

arcTo(polarPoint(radius, angleB), radius),

'Z'

].join(' ')

return <path d={path} /> }

Colour Mapping#

Now we can turn each section's byte value into a colour.

For that, we have many options. 8 bits could map directly to 256 colours like the old Windows systems, but that would require a lookup table. Instead, we can generate colours using the hsl() CSS function.

Hue is the component that has the most visual impact, while Saturation and Lightness can be used to add little variants to each section.

To map our 8 bit value to 3 components, we can divide the byte into:

function mapValueToColor(value) {
  const colorH = value >> 4
  const colorS = (value >> 2) & 0x03
  const colorL = value & 0x03
  const normalizedH = colorH / 16
  const normalizedS = colorS / 4
  const normalizedL = colorL / 4
  const h = 360 * normalizedH
  const s = 50 + 50 * normalizedS 
  const l = 40 + 30 * normalizedL 
  return `hsl(${h}, ${s}%, ${l}%)`
}

We can adjust the range for each component to get nice results:

Order, Chaos & Soul#

Our colour encoding suffers from a flaw: two hashes can look very similar, but have a few bits of difference here and there. They can go unnoticed especially if differences occur in the LSBs of hue/saturation/lightness components.

Also, the sections look random in colour, and the whole avatar lacks coherence.

It would be nice if there was some pattern to a hash that makes it random enough to be distinguished yet coherent enough within itself. A balance between order and chaos.

In order to fix that, we compute the soul of the hash, using XOR operations.

  1. We XOR all the bytes of the hash together to compute the hash soul
  2. For each ring, we XOR the bytes that form this ring's section to compute the ring's soul. (horcruxes?)

function computeSouls(bytes: string[]) {
  const ringLength = Math.round(bytes.length / 4)
  const rings = [
    bytes.slice(0, ringLength),
    bytes.slice(1 * ringLength, 2 * ringLength),
    bytes.slice(2 * ringLength, 3 * ringLength),
    bytes.slice(3 * ringLength, 4 * ringLength)
  ]
  const xorReducer = (xor: number, byte: string) => xor ^ parseInt(byte, 16)
  return {
    hashSoul: (bytes.reduce(xorReducer, 0) / 0xff) * 2 - 1,
    ringSouls: rings.map(ring => (ring.reduce(xorReducer, 0) / 0xff) * 2 - 1)
  }
}







These values give us additional parameters to play with in the colour calculation.

Notably, we can "seed" the Hue with the hash soul, and introduce hue varitions per-ring with each ring soul, and with the value itself.

export function mapValueToColor({ value, hashSoul, ringSoul }) {
  const colorH = value >> 4
  const colorS = (value >> 2) & 0x03
  const colorL = value & 0x03
  const normalizedH = colorH / 16
  const normalizedS = colorS / 4
  const normalizedL = colorL / 4

const h = 360 * hashSoul

+ 120 * ringSoul

+ 30 * normalizedH

const s = 50 + 50 * normalizedS const l = 40 + 30 * normalizedL return `hsl(${h}, ${s}%, ${l}%)` }

We can also introduce structural variations by changing each ring's starting angle based on the ring soul, to create a staggering effect:

Without souls

With souls

With souls & staggering

Not only does this help give a bit more uniqueness to the avatar, it also helps with accessibility for colour-blind people

A Bit Of Fun#

If we change the radius and flags for the arc part of the section paths and play with each ring's starting angle, we can obtain interesting variations:

Going Further#

With a bit of tweaking in the colour mapping value, we can easily extend this technique to arbitrary hash lengths (as long as said length is divisible by 32).

It so happens that when I started this project, Centralized Coin was using SHA-256, but later on switched to SHA-384, which gives 12 bits per section.

Conclusion#

You can see the hashvatars (thanks to @wzulfikar for the name) in action in the Centralized Coin Explorer, or play with the variants yourself on the playground.

I will publish the code as an NPM package later, in the mean time the source code for this article is on GitHub, as well as the component itself.

Follow me on Twitter for updates and more weekend projects.