3.1 KiB · text 5af55d9
defmodule GitGudWeb.AvatarComponent do
@moduledoc """
Deterministic identicon-style avatars that pull their colors from the
active daisyUI theme.
No image storage. The avatar is an inline SVG whose `fill`s reference
CSS custom properties (`var(--color-primary)`, etc.) — so switching
themes recolors every avatar on the page without re-rendering.
Pattern: 5×5 grid of cells, left half + center driven by 15 bits from
a SHA-256 of the seed name; right half mirrors the left. Identicon
shape used by GitHub, picked because it stays legible at small sizes.
Usage:
<.avatar name="alice" size="md" />
<.avatar name={@user.handle} size="lg" alt={@user.handle} />
"""
use Phoenix.Component
@sizes %{
"xs" => "size-4",
"sm" => "size-6",
"md" => "size-8",
"lg" => "size-12",
"xl" => "size-16",
"2xl" => "size-24"
}
# daisyUI color slots we'll pick from for the foreground. Background
# is fixed to `base-200` so the avatar reads against any panel.
@palette ~w(primary secondary accent info success warning)
attr :name, :string, required: true, doc: "Seed string — handle, repo name, anything stable"
attr :size, :string, default: "md", values: Map.keys(@sizes)
attr :alt, :string, default: ""
attr :class, :string, default: ""
def avatar(assigns) do
{fg_color, cells} = render_spec(assigns.name || "?")
assigns =
assigns
|> assign(:size_class, Map.get(@sizes, assigns.size, "size-8"))
|> assign(:fg_color, fg_color)
|> assign(:cells, cells)
~H"""
<span
class={[
"inline-block overflow-hidden rounded-full bg-base-200 align-middle shrink-0",
@size_class,
@class
]}
role="img"
aria-label={@alt}
>
<svg viewBox="0 0 5 5" xmlns="http://www.w3.org/2000/svg" class="w-full h-full block">
<rect
:for={{x, y} <- @cells}
x={x}
y={y}
width="1"
height="1"
style={"fill: var(--color-" <> @fg_color <> ")"}
/>
</svg>
</span>
"""
end
@doc """
For a given seed name, return `{fg_color, cells}` where `fg_color` is
the chosen daisyUI palette slot (without the `--color-` prefix) and
`cells` is the list of `{x, y}` grid positions to fill.
"""
@spec render_spec(String.t()) :: {String.t(), [{0..4, 0..4}]}
def render_spec(name) when is_binary(name) do
<<color_byte, bits1, bits2, _rest::binary>> = :crypto.hash(:sha256, name)
fg = Enum.at(@palette, rem(color_byte, length(@palette)))
# 15 bits — 5 rows × 3 left/center columns. Take the low 8 of bits1
# and the low 7 of bits2 as a 15-bit field.
bit_field = bits1 * 128 + Bitwise.band(bits2, 0x7F)
cells =
for row <- 0..4, col <- 0..2, bit?(bit_field, row * 3 + col), reduce: [] do
acc ->
left = {col, row}
mirror = {4 - col, row}
if left == mirror, do: [left | acc], else: [left, mirror | acc]
end
{fg, cells}
end
defp bit?(field, position),
do: Bitwise.band(Bitwise.bsr(field, position), 1) == 1
@doc false
def palette, do: @palette
@doc false
def sizes, do: @sizes
end