Valstar.dev

Procedurally Generated Avatars - @mikevalstar/gridavatar

12 min read


For many websites I’ve worked on over the years I’ve needed to have some randomly generated avatars. Sometimes we use a service like Gravatar or Libravatar if we’re looking for something quick, or we’ll allow users to upload an image but have a simple default like a circle image with your initials in the center and some random background color.

Both of these approaches work great as a quick solution, however they have some problems:

So I created an image generator with some simple shapes, you can see it being used in the header above. This also allows me a fallback for when I don’t have an image and I’m too lazy to use Midjourney or something.

Generating Some Colors

The first step we need is to generate some colors to make our images with. For this there is a nice little library randomColor that will work great. This library will help us by generating a few starting colors:

// So all our colors are similar lets get teh hue of the original, 
// but limit to the range supported by randomColor
const starterHue = randomNumber(1, 334);
const similarColors = randomColor({
  seed,
  hue: starterHue,
  format: 'hslArray',
  luminosity: opts.luminosity,
  count: 16,
});
// Set a background color for our design
const baseColor = similarColors.pop();

This will give us a quick and set of 16 colors to work with, 1 of those being the background. note: the randomNumber() function will be created with our PRNG below

Making it Deterministic

To make our avatars be deterministic in how they look, we will want to use something unique about our users (e.g. email) and put that into a PRNG (pseudo random number generator) so that our output is always the same. So taking an example from StackOverflow we can create a quick instantiable random number generator:

export function cyrb128 (str) {
  let h1 = 1779033703; let h2 = 3144134277;
  let h3 = 1013904242; let h4 = 2773480762;
  for (let i = 0, k; i < str.length; i++) {
    k = str.charCodeAt(i);
    h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
    h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
    h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
    h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
  }
  h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
  h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
  h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
  h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
  return [(h1 ^ h2 ^ h3 ^ h4) >>> 0, (h2 ^ h1) >>> 0, (h3 ^ h1) >>> 0, (h4 ^ h1) >>> 0];
}

export function mulberry32 (a) {
  return function () {
    let t = a += 0x6D2B79F5;
    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

const randomGenInit = (seed) => {
  const pseed = typeof seed === 'string' ? cyrb128(seed) : [seed];

  const rand = mulberry32(pseed[0]);

  return rand;
};

export default randomGenInit;

And we’ll need some helper random functions for our drawing:

// Random number generator variations
const randomGen = randomGenInit('mike@valstar.dev'); // Sets the seed for the prng; this is predictable
const randomNumber = (minValue, maxValue) => {
  const max = maxValue || 1;
  const min = minValue || 0;
  const rnd = randomGen();

  return min + rnd * (max - min);
};
// Used to grab a random color
const randomArrayItem = items => items[Math.floor(randomGen() * items.length)];
// allows or to have our shapes be random sizes within a range
const getRandomSizeVariance = (b, v) => (randomNumber() * b) - (b * v);

Drawing It Out

So now that we have some colors and some predictable random numbers. Lets draw out an image:

const getHSLStyle = (hsl) => `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;

const drawHexBoard = (ctx, similarColors, width, height, sideLength, pixelVariance, getRandomSizeVariance, randomArrayItem) => {
  let i, j, x, y;

  const hexagonAngle = 0.523598776; // 30 degrees in radians
  const offset = getRandomSizeVariance(sideLength * 1.3, 0);
  const hexHeight = Math.sin(hexagonAngle) * sideLength;
  const hexRadius = Math.cos(hexagonAngle) * sideLength;
  const hexRectangleHeight = sideLength + 2 * hexHeight;
  const hexRectangleWidth = 2 * hexRadius;

  for (i = 0; i < width / sideLength + 1; ++i) {
    for (j = 0; j < height / sideLength + 1; ++j) {
      x = i * hexRectangleWidth + ((j % 2) * hexRadius) - offset + getRandomSizeVariance(pixelVariance, 0.5);
      y = j * (sideLength + hexHeight) - offset + getRandomSizeVariance(pixelVariance, 0.5);
      ctx.beginPath();
      ctx.moveTo(x + hexRadius, y);
      ctx.lineTo(x + hexRectangleWidth, y + hexHeight);
      ctx.lineTo(x + hexRectangleWidth, y + hexHeight + sideLength);
      ctx.lineTo(x + hexRadius, y + hexRectangleHeight);
      ctx.lineTo(x, y + sideLength + hexHeight);
      ctx.lineTo(x, y + hexHeight);
      ctx.closePath();
      const newHSL = randomArrayItem(similarColors); // Random color
      ctx.fillStyle = getHSLStyle(newHSL);
      ctx.fill();
    }
  }
};

// Create the canvas
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');

// Set a backtround color and set the font size
ctx.fillStyle = getHSLStyle(baseColor);
ctx.fillRect(0, 0, 128, 128);
ctx.font = 64 + 'px sans-serif';
ctx.textAlign = 'center';

// Draw out some random hexes
drawHexBoard(ctx, similarColors, 128, 128, 16, 0, getRandomSizeVariance, randomArrayItem);

// Add some tech to the center of the image
ctx.fillStyle = '#000';
ctx.fillText('MV', 128 / 2, (128 / 2) + (64 / 3));

const image = new Image();
image.src = canvas.toDataURL('image/png');
return image;

There is quite a bit above, but what we’re basically doing is creating a canvas object, then looping over the canvas in a grid like fashion and every “step” of the loop placing in a hex at that center point. Each hex will pick a “random” color from the array of colors and place it. Because we used a PRNG function to both generate the colors and to also pick which one to grab at each round this will be repeatable given the same seed.

Extending This Idea

This is just a basic setup and can easily be extended to generate your own avatars. Maybe bring in some SVGs from a list, add in some gradients or any other cool drawing tools.

Final Thoughts

Altough there has been less of a focus on bandwidth usage over the last 5 or so years, this generator if implemented properly can reduce the amount of bandwidth used when displaying avatars as the package I created for this is smaller then a lot of avatar image’s I’m seeing on the internet right now. (3.5kb gzipped).

For the 6 or 7 hours this took me to create this would be a nice simple addition to any website that needs some unique avatars for people that they don’t need to upload themselves.

The Package - @mikevalstar/gridavatar

I have compiled all of this (and added in some options) into a node package that makes it nice and simple for anyone to copy from and use:

gridavatar('mike@valstar.dev', {
  height: 128,
  width: 128,
  luminosity: 'light',
  type: 'square',
  text: 'MV'
})

Example avatar

If you’re interested in the code you can check it out here: https://gitlab.com/mikevalstar/gridavatar

Features to Add