diff --git a/config/avatar.json b/config/avatar.json new file mode 100644 index 00000000..de0ed598 --- /dev/null +++ b/config/avatar.json @@ -0,0 +1,265 @@ +{ + "header": "", + "background": "", + "species": [ + "unicorn", + "pegasus", + "earthpony" + ], + "body_shapes": [ + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + } + ], + "tail_shapes": [ + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + } + ], + "hair_shapes": [ + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "path d=\"M64.335 34.675c3.358 1.584 6.716.908 10.073 1.043-.265 13.078 19.05 19.74 31.58 4.16 6.077 6.273 24.776 2.28 12.42-18.66-12.88-21.833-42.605-11.287-61-.5l-7.25 11c-29.918 14.92-16.418 45.666-.75 57.625-12.967 2.522-6.234 30.16 9.904 24.894 18.84-6.147-1.986-51.066-7.78-62.644l1.495-11.736z\" fill=\"#HAIR_FILL\"/>", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + } + ], + "extra_shapes": [ + { + "shape": "", + "species": [ + "unicorn" + ] + }, + { + "shape": "", + "species": [ + "pegasus" + ] + }, + { + "shape": "", + "species": [ + "batpony" + ] + }, + { + "shape": "", + "species": [ + "batpony" + ] + }, + { + "shape": "", + "species": [ + "unicorn", + "pegasus", + "earthpony", + "batpony" + ] + } + ], + "footer": "" +} diff --git a/lib/philomena_web/controllers/staff_controller.ex b/lib/philomena_web/controllers/staff_controller.ex index 3aa11089..755c5970 100644 --- a/lib/philomena_web/controllers/staff_controller.ex +++ b/lib/philomena_web/controllers/staff_controller.ex @@ -20,6 +20,6 @@ defmodule PhilomenaWeb.StaffController do "Assistants": Enum.filter(users, & &1.role == "assistant" and &1.secondary_role in [nil, ""]) ] - render(conn, "index.html", index: "Site Staff", categories: categories) + render(conn, "index.html", title: "Site Staff", categories: categories) end end diff --git a/lib/philomena_web/templates/user_attribution/_anon_user_avatar.html.slime b/lib/philomena_web/templates/user_attribution/_anon_user_avatar.html.slime index dccab704..8e31711f 100644 --- a/lib/philomena_web/templates/user_attribution/_anon_user_avatar.html.slime +++ b/lib/philomena_web/templates/user_attribution/_anon_user_avatar.html.slime @@ -1,4 +1,4 @@ = if !!@object.user and !anonymous?(@object) do = user_avatar(@object, assigns[:class] || "avatar--100px") - else - = anonymous_avatar(@object, assigns[:class] || "avatar--100px") + = anonymous_avatar(anonymous_name(@object), assigns[:class] || "avatar--100px") diff --git a/lib/philomena_web/templates/user_attribution/_user_avatar.html.slime b/lib/philomena_web/templates/user_attribution/_user_avatar.html.slime index 1e58fdf4..847499e7 100644 --- a/lib/philomena_web/templates/user_attribution/_user_avatar.html.slime +++ b/lib/philomena_web/templates/user_attribution/_user_avatar.html.slime @@ -1,4 +1,2 @@ = if !!@object.user do = user_avatar(@object, assigns[:class] || "avatar--100px") -- else - = anonymous_avatar(assigns[:class] || "avatar--100px") \ No newline at end of file diff --git a/lib/philomena_web/views/avatar_generator_view.ex b/lib/philomena_web/views/avatar_generator_view.ex new file mode 100644 index 00000000..44204df3 --- /dev/null +++ b/lib/philomena_web/views/avatar_generator_view.ex @@ -0,0 +1,95 @@ +defmodule PhilomenaWeb.AvatarGeneratorView do + use PhilomenaWeb, :view + use Bitwise + + alias Philomena.Servers.Config + + def generated_avatar(displayed_name) do + config = config() + + # Generate 8 pseudorandom numbers + seed = :erlang.crc32(displayed_name) + {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. + 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), + all_species(extra_shapes(config), species) |> Enum.map(&String.replace(&1["shape"], "BODY_FILL", color_bd)), + footer(config) + ] + |> List.flatten() + |> Enum.map(&raw/1) + end + + # https://en.wikipedia.org/wiki/Xorshift + # 32-bit xorshift deterministic PRNG + defp xorshift32(state) do + state = state &&& 0xffff_ffff + state = state ^^^ (state <<< 13) + state = state ^^^ (state >>> 17) + + state ^^^ (state <<< 5) + 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)) + defp all_species(styles, species), do: Enum.filter(styles, &Enum.member?(&1["species"], species)) + 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 diff --git a/lib/philomena_web/views/user_attribution_view.ex b/lib/philomena_web/views/user_attribution_view.ex index 9b1584db..f822770f 100644 --- a/lib/philomena_web/views/user_attribution_view.ex +++ b/lib/philomena_web/views/user_attribution_view.ex @@ -1,7 +1,8 @@ defmodule PhilomenaWeb.UserAttributionView do - alias Philomena.Attribution - use Bitwise use PhilomenaWeb, :view + use Bitwise + + alias Philomena.Attribution def anonymous?(object) do Attribution.anonymous?(object) @@ -23,20 +24,20 @@ defmodule PhilomenaWeb.UserAttributionView do end end - def anonymous_avatar(_object, class \\ "avatar--100px") do + def anonymous_avatar(name, class \\ "avatar--100px") do class = Enum.join(["image-constrained", class], " ") content_tag :div, [class: class] do - img_tag(Routes.static_path(PhilomenaWeb.Endpoint, "/images/no_avatar.svg")) + PhilomenaWeb.AvatarGeneratorView.generated_avatar(name) end end def user_avatar(object, class \\ "avatar--100px") def user_avatar(%{user: nil} = object, class), - do: anonymous_avatar(object, class) + do: anonymous_avatar(anonymous_name(object), class) def user_avatar(%{user: %{avatar: nil}} = object, class), - do: anonymous_avatar(object, class) + do: anonymous_avatar(object.user.name, class) def user_avatar(%{user: %{avatar: avatar}}, class) do class = Enum.join(["image-constrained", class], " ")