
Build initial based avatar svg generator with elixir
Abstract: Here we learn how to build default avatar generator. We generate the color based on the hash algorithm of given name and display its initial on top. This kind of default avatar is popularized by google.
When the user sign up for the first time, the application usually give a default avatar before the user change it. Here’s some of those:
Can you recognize which company use those default avatar? Maybe you can recognize snap, slack, and github.
Default avatar used to be boring, just a plain male or female silhouette. But now many company getting more creative with it. In 2015 Google change their default male/female silhouette into initial based avatar in this announcement.
This new initial based avatar have some characteristic:
- Eventhough the background color seems random, they compatible each other.
- Sama initial doesn’t necessary have the same background color.
Today we will build the code to generate such avatar. If you are following previous article on How to generate pretty color programmatically, you already know how to generate random pretty color to be used as a background. So now we need to make a simple function to generate initials.
Lets specs up some little requirement for our little function:
- The function must ignore foreign character
- If name with one word given it should output the first letter of that word.
- If name with two word given it should output the first letter of both word.
- If name with more than to word is siven it should output the first letter of the first and the last word.
Given the specs, here’s a little function we can come up with:
def get_initial(name) do
clean_character =
Regex.replace(~r/[\p{P}\p{S}\p{C}\p{N}]+/, name, "")
|> String.trim()
|> String.split()
case Enum.count(clean_character) do
0 ->
"42"
1 ->
clean_character |> List.first() |> String.at(0) |> String.upcase()
other ->
first = clean_character |> List.first() |> String.at(0) |> String.upcase()
last = clean_character |> List.last() |> String.at(0) |> String.upcase()
first <> last
end
end
Little additional, it will return “42” if no string are given.
Now we already have enough ingredient to write our avatar generation function. But there’s one more we can add. I hope you find this feature interesting:
Instead of giving random background color each time, to to make the background random for different name but consistent for the same name?
Afterall, avatar is meant to be an identifier right?
Here is where we can use some sort of hash algorithm. The idea is no matter what string is given to that funtion it will consistently return back one color amongst 359 possibility. Here’s a little function requirement:
- The function will accept string and return integer between 1 to 359
- The same string should produce the same integer consistently
- Given two almost similar string should produce entirely different integer.
The last two requirement sounds like a hash algorithm but in this case it must result in very narrow possible output. So here’s the attempt.
def minihash(string) do
:crypto.hash(:md5, string)
|> Base.encode16()
|> String.slice(0..3)
|> String.graphemes()
|> string_to_int
|> rem(360)
end
defp string_to_int(list) do
Enum.reduce(list, 1, fn x, acc ->
case Integer.parse(x) do
{0, _} -> 1 * acc
{number, _} -> number * acc
:error -> letter_to_integer(x) * acc
end
end)
end
defp letter_to_integer(letter) do
case letter do
"A" -> 1
"B" -> 2
"C" -> 3
"D" -> 4
"E" -> 5
"F" -> 6
_ -> 1
end
end
As you might see, the function above obviously not optimal. We use md5 hashing algorithm only to get the first 3 character, convert into integer and multiply all 3. Lastly we use modulus/rem function to force it below 360. Let me know if you found a shorter way.
There you have it. All the ingredient required to build your very own initial based avatar generator.
Here’s the complete code including generate SVG file as an output.
defmodule HashColorAvatar do
@default_color :pastel
@default_shape :circle
@default_size 100
@default_saturation 50
@default_value 90
use Bitwise
@moduledoc """
This is a small library to generate SVG initial avatar with unique-ish color based on string hash.
"""
@doc """
Will generate random color in hex.
## Options
This color is generated by randomizing HSV (Hue Saturation Value) with default Saturation is set to 50 and Value set to 90.
``` iex> HashColorAvatar.random_color([saturation: 70, value: 100])
"""
def random_color(options \\ []) do
seed = Enum.random(1..359)
saturation = Keyword.get(options, :saturation, @default_saturation)
value = Keyword.get(options, :value, @default_value)
hsv_to_rgb(%{hue: seed, saturation: saturation, value: value}) |> rgb_to_hex
end
@doc """
This function will convert RGB to hex.
## Examples
iex> HashColorAvatar.set_color(12)
"#E58972"
Option also applied
iex> HashColorAvatar.set_color(12, [saturation: 70, value: 80])
"#CC593D"
"""
def set_color(hue_value, options \\ []) do
saturation = Keyword.get(options, :saturation, @default_saturation)
value = Keyword.get(options, :value, @default_value)
hsv_to_rgb(%{hue: hue_value, saturation: saturation, value: value}) |> rgb_to_hex
end
@doc """
This function will convert HSV map to RGB map.
## Examples
iex> HashColorAvatar.hsv_to_rgb(%{hue: 17, saturation: 50, value: 90})
%{blue: 114, green: 147, red: 229}
"""
def hsv_to_rgb(%{hue: hue, saturation: saturation, value: value} = _hsv) do
h = hue / 60
i = Float.floor(h) |> trunc()
f = h - i
sat_dec = saturation / 100
p = value * (1 - sat_dec)
q = value * (1 - sat_dec * f)
t = value * (1 - sat_dec * (1 - f))
p_rgb = get_rgb_color(p)
v_rgb = get_rgb_color(value)
t_rgb = get_rgb_color(t)
q_rgb = get_rgb_color(q)
case i do
0 -> %{red: v_rgb, green: t_rgb, blue: p_rgb}
1 -> %{red: q_rgb, green: v_rgb, blue: p_rgb}
2 -> %{red: p_rgb, green: v_rgb, blue: t_rgb}
3 -> %{red: p_rgb, green: q_rgb, blue: v_rgb}
4 -> %{red: t_rgb, green: p_rgb, blue: v_rgb}
_ -> %{red: v_rgb, green: p_rgb, blue: q_rgb}
end
end
@doc """
This function will convert RGB to hex.
## Examples
iex> HashColorAvatar.rgb_to_hex(%{red: 12, green: 23, blue: 43})
"#0C172B"
iex> HashColorAvatar.rgb_to_hex(%{red: 121, green: 13, blue: 203})
"#790DCB"
"""
def rgb_to_hex(%{red: red, green: green, blue: blue} = rgb) do
hex =
((1 <<< 24) + (red <<< 16) + (green <<< 8) + blue)
|> Integer.to_string(16)
|> String.slice(1..1500)
"#" <> hex
end
@doc """
Given some string, this function will generate SVG avatar. The main feature is the micro hasing function. Which means th ecolor given for "Frank Abraham" will be different for "Foreman Abdul" even though they both have same initials.
## Examples
iex> HashColorAvatar.gen_avatar("")
'<svg width="100" height="100"><circle cx="50.0" cy="50.0" r="50.0" stroke="white" stroke-width="4" fill="pastel" /><text fill="white" x="50%" y="67%" text-anchor="middle" style="font: bold 41.66666666666667px sans-serif;" >VK</text></circle></svg>'
## Option
**:color** can be used to specify background color. By default it will generate hash based on the text given. It will be unique-ish since there are only 359 possible color and there's a chance it looks similar one amongst the other. For the value you can choose "random", any color code recognized by CSS such as "teal", "tomato", also it accept hex code.
**:shape** by default is circle. You can also choose "rect" for rectangle avatar.
**:size** You can define how many pixel height and width. Default is 100
## Examples
iex> HashColorAvatar.gen_avatar("Samantha Johnson Abigail", [color: "tomato", shape: "rect", size: 200])
'<svg width="200" height="200"><rect width="200" height="200" fill="tomato" /><text fill="white" x="50%" y="65%" text-anchor="middle" style="font: bold 83.33333333333334px sans-serif;" >SA</text></circle></svg>'
"""
def gen_avatar(rawtext, options \\ []) do
text = if rawtext == nil, do: "V K", else: rawtext
color = Keyword.get(options, :color, @default_color)
shape = Keyword.get(options, :shape, @default_shape)
size = Keyword.get(options, :size, @default_size)
fontsize = size / 2.4
diameter = size / 2
background_color =
case color do
"grey" -> "#c3c3c3"
"black" -> "black"
"random" -> random_color()
nil -> text |> minihash |> set_color
custom -> custom
end
case shape do
"rect" ->
'<svg width="#{size}" height="#{size}"><rect width="#{size}" height="#{size}" fill="#{background_color}" /><text fill="white" x="50%" y="65%" text-anchor="middle" style="font: bold #{fontsize}px sans-serif;" >#{get_initial(text)}</text></circle></svg>'
_other ->
'<svg width="#{size}" height="#{size}"><circle cx="#{diameter}" cy="#{diameter}" r="#{diameter}" stroke="white" stroke-width="4" fill="#{
background_color}" /><text fill="white" x="50%" y="67%" text-anchor="middle" style="font: bold #{fontsize}px sans-serif;" >#{
get_initial(text)}</text></circle></svg>'
end
end
@doc """
This function generate initial from any given name. If more than 2 word given it will take initial first and last word. It will try to ignore other character.
## Examples
iex> HashColorAvatar.get_initial("sujiwo tedjo")
"ST"
iex> HashColorAvatar.get_initial("guruh soekarno putra")
"GP"
"""
def get_initial(name) do
clean_character =
Regex.replace(~r/[\p{P}\p{S}\p{C}\p{N}]+/, name, "") |> String.trim() |> String.split()
case Enum.count(clean_character) do
0 ->
"VK"
1 ->
clean_character |> List.first() |> String.at(0) |> String.upcase()
other ->
first = clean_character |> List.first() |> String.at(0) |> String.upcase()
second = clean_character |> List.last() |> String.at(0) |> String.upcase()
first <> second
end
end
@doc false
# This function will generate "hash" for any kind of string and spitting integer
# between 1 to 359. This the possible Hue range in color
defp minihash(string) do
:crypto.hash(:md5, string)
|> Base.encode16()
|> String.slice(0..3)
|> String.graphemes()
|> string_to_int
|> rem(360)
end
@doc false
# This function is private function to convert part of hashed string into interger.
defp string_to_int(list) do
Enum.reduce(list, 1, fn x, acc ->
case Integer.parse(x) do
{0, _} -> 1 * acc
{number, _} -> number * acc
:error -> letter_to_integer(x) * acc
end
end)
end
@doc false
defp get_rgb_color(color) do
(color * 255 / 100) |> trunc()
end
@doc false
defp letter_to_integer(letter) do
case letter do
"A" -> 1
"B" -> 2
"C" -> 3
"D" -> 4
"E" -> 5
"F" -> 6
other -> 1
end
end
You can also check the github repo:
https://github.com/virkillz/hash_color_avatar_elixir
And I publish it as hex.pm library for elixir. https://hexdocs.pm/hash_color_avatar/0.1.0/HashColorAvatar.html
so you can use it directly in your project by putting {:hash_color_avatar, "~> 0.1.0"}
in your mix.exs
.
If you want to use it as an API by simply embed as img
like:
<img src="https://avatar-api.org/avatar.svg?name=Amanda Vonnz" />
<img src="https://avatar-api.org/avatar.svg?name=John Doe" />
<img src="https://avatar-api.org/avatar.svg?name=Vincent Vega" />
to generate:
You can visit: https://avatar-api.org
Cheers.