2019-12-25 19:32:24 +01:00
|
|
|
defmodule PhilomenaWeb.AvatarGeneratorView do
|
|
|
|
use PhilomenaWeb, :view
|
|
|
|
use Bitwise
|
|
|
|
|
2020-10-27 04:03:08 +01:00
|
|
|
alias Philomena.Config
|
2019-12-25 19:32:24 +01:00
|
|
|
|
|
|
|
def generated_avatar(displayed_name) do
|
|
|
|
config = config()
|
|
|
|
|
|
|
|
# Generate 8 pseudorandom numbers
|
|
|
|
seed = :erlang.crc32(displayed_name)
|
2020-01-11 05:20:19 +01:00
|
|
|
|
2019-12-25 19:32:24 +01:00
|
|
|
{rand, _acc} =
|
|
|
|
Enum.map_reduce(1..8, seed, fn _elem, acc ->
|
|
|
|
value = xorshift32(acc)
|
|
|
|
{value, value}
|
|
|
|
end)
|
|
|
|
|
|
|
|
# Set species
|
|
|
|
{species, rand} = at(species(config), rand)
|
|
|
|
|
|
|
|
# Set the ranges for the colors we are going to make
|
|
|
|
color_range = 128
|
|
|
|
color_brightness = 72
|
|
|
|
|
|
|
|
{body_r, body_g, body_b, rand} = rgb(0..color_range, color_brightness, rand)
|
|
|
|
{hair_r, hair_g, hair_b, rand} = rgb(0..color_range, color_brightness, rand)
|
|
|
|
{style_hr, _rand} = at(all_species(hair_shapes(config), species), rand)
|
|
|
|
|
|
|
|
# Creates bounded hex color strings
|
|
|
|
color_bd = format("~2.16.0B~2.16.0B~2.16.0B", [body_r, body_g, body_b])
|
|
|
|
color_hr = format("~2.16.0B~2.16.0B~2.16.0B", [hair_r, hair_g, hair_b])
|
|
|
|
|
|
|
|
# Make a character
|
|
|
|
avatar_svg(config, color_bd, color_hr, species, style_hr)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Build the final SVG for the character.
|
2021-04-01 18:49:41 +02:00
|
|
|
#
|
|
|
|
# Inputs to raw/1 are not user-generated.
|
|
|
|
# sobelow_skip ["XSS.Raw"]
|
2019-12-25 19:32:24 +01:00
|
|
|
defp avatar_svg(config, color_bd, color_hr, species, style_hr) do
|
|
|
|
[
|
|
|
|
header(config),
|
|
|
|
background(config),
|
|
|
|
for_species(tail_shapes(config), species)["shape"] |> String.replace("HAIR_FILL", color_hr),
|
|
|
|
for_species(body_shapes(config), species)["shape"] |> String.replace("BODY_FILL", color_bd),
|
|
|
|
style_hr["shape"] |> String.replace("HAIR_FILL", color_hr),
|
2020-01-11 05:20:19 +01:00
|
|
|
all_species(extra_shapes(config), species)
|
|
|
|
|> Enum.map(&String.replace(&1["shape"], "BODY_FILL", color_bd)),
|
2019-12-25 19:32:24 +01:00
|
|
|
footer(config)
|
|
|
|
]
|
|
|
|
|> List.flatten()
|
|
|
|
|> Enum.map(&raw/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
# https://en.wikipedia.org/wiki/Xorshift
|
|
|
|
# 32-bit xorshift deterministic PRNG
|
|
|
|
defp xorshift32(state) do
|
2020-01-11 05:20:19 +01:00
|
|
|
state = state &&& 0xFFFF_FFFF
|
2021-05-22 22:18:24 +02:00
|
|
|
state = bxor(state, (state <<< 13))
|
|
|
|
state = bxor(state, (state >>> 17))
|
2019-12-25 19:32:24 +01:00
|
|
|
|
2021-05-22 22:18:24 +02:00
|
|
|
bxor(state, (state <<< 5))
|
2019-12-25 19:32:24 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# Generate pseudorandom, clamped RGB values with a specified
|
|
|
|
# brightness and random source
|
|
|
|
defp rgb(range, brightness, rand) do
|
|
|
|
{r, rand} = at(range, rand)
|
|
|
|
{g, rand} = at(range, rand)
|
|
|
|
{b, rand} = at(range, rand)
|
|
|
|
|
|
|
|
{r + brightness, g + brightness, b + brightness, rand}
|
|
|
|
end
|
|
|
|
|
|
|
|
# Pick an element from an enumerable at the specified position,
|
|
|
|
# wrapping around as appropriate.
|
|
|
|
defp at(list, [position | rest]) do
|
|
|
|
length = Enum.count(list)
|
|
|
|
position = rem(position, length)
|
|
|
|
|
|
|
|
{Enum.at(list, position), rest}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp for_species(styles, species), do: hd(all_species(styles, species))
|
2020-01-11 05:20:19 +01:00
|
|
|
|
|
|
|
defp all_species(styles, species),
|
|
|
|
do: Enum.filter(styles, &Enum.member?(&1["species"], species))
|
|
|
|
|
2019-12-25 19:32:24 +01:00
|
|
|
defp format(format_string, args), do: to_string(:io_lib.format(format_string, args))
|
|
|
|
|
|
|
|
defp species(%{"species" => species}), do: species
|
|
|
|
defp header(%{"header" => header}), do: header
|
|
|
|
defp background(%{"background" => background}), do: background
|
|
|
|
defp tail_shapes(%{"tail_shapes" => tail_shapes}), do: tail_shapes
|
|
|
|
defp body_shapes(%{"body_shapes" => body_shapes}), do: body_shapes
|
|
|
|
defp hair_shapes(%{"hair_shapes" => hair_shapes}), do: hair_shapes
|
|
|
|
defp extra_shapes(%{"extra_shapes" => extra_shapes}), do: extra_shapes
|
|
|
|
defp footer(%{"footer" => footer}), do: footer
|
|
|
|
|
|
|
|
defp config, do: Config.get(:avatar)
|
|
|
|
end
|