101 lines
2.8 KiB
Elixir
101 lines
2.8 KiB
Elixir
|
|
defmodule Berrypod.Theme.Contrast do
|
||
|
|
@moduledoc """
|
||
|
|
WCAG contrast ratio calculations for accessibility checking.
|
||
|
|
|
||
|
|
Used to warn shop owners when header background images might make text hard to read.
|
||
|
|
"""
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Calculate relative luminance from sRGB values (0-255).
|
||
|
|
Uses the WCAG 2.1 formula.
|
||
|
|
"""
|
||
|
|
def relative_luminance([r, g, b]) do
|
||
|
|
[r, g, b]
|
||
|
|
|> Enum.map(fn c ->
|
||
|
|
c = c / 255
|
||
|
|
|
||
|
|
if c <= 0.03928 do
|
||
|
|
c / 12.92
|
||
|
|
else
|
||
|
|
:math.pow((c + 0.055) / 1.055, 2.4)
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
|> then(fn [r, g, b] -> 0.2126 * r + 0.7152 * g + 0.0722 * b end)
|
||
|
|
end
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Calculate WCAG contrast ratio between two colors.
|
||
|
|
Returns a ratio >= 1.0 (higher = better contrast).
|
||
|
|
"""
|
||
|
|
def contrast_ratio(color1, color2) do
|
||
|
|
l1 = relative_luminance(color1)
|
||
|
|
l2 = relative_luminance(color2)
|
||
|
|
{lighter, darker} = if l1 > l2, do: {l1, l2}, else: {l2, l1}
|
||
|
|
(lighter + 0.05) / (darker + 0.05)
|
||
|
|
end
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Analyze header image colors against text color, return warning level.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
- `:ok` - All dominant colors have acceptable contrast
|
||
|
|
- `:caution` - Some colors may be problematic
|
||
|
|
- `:poor` - Most colors have poor contrast
|
||
|
|
"""
|
||
|
|
def analyze_header_contrast(nil, _text_color), do: :ok
|
||
|
|
def analyze_header_contrast([], _text_color), do: :ok
|
||
|
|
|
||
|
|
def analyze_header_contrast(dominant_colors, text_color) when is_list(dominant_colors) do
|
||
|
|
text_rgb = hex_to_rgb(text_color)
|
||
|
|
|
||
|
|
ratios = Enum.map(dominant_colors, &contrast_ratio(&1, text_rgb))
|
||
|
|
min_ratio = Enum.min(ratios)
|
||
|
|
avg_ratio = Enum.sum(ratios) / length(ratios)
|
||
|
|
|
||
|
|
cond do
|
||
|
|
# All colors have good contrast (3:1 is WCAG AA for large text)
|
||
|
|
Enum.all?(ratios, &(&1 >= 3.0)) -> :ok
|
||
|
|
# Most colors are problematic
|
||
|
|
avg_ratio < 3.0 -> :poor
|
||
|
|
# Some colors might be problematic
|
||
|
|
min_ratio < 2.0 -> :caution
|
||
|
|
true -> :ok
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Parse JSON-encoded dominant colors string.
|
||
|
|
"""
|
||
|
|
def parse_dominant_colors(nil), do: nil
|
||
|
|
def parse_dominant_colors(""), do: nil
|
||
|
|
|
||
|
|
def parse_dominant_colors(json) when is_binary(json) do
|
||
|
|
case Jason.decode(json) do
|
||
|
|
{:ok, colors} when is_list(colors) -> colors
|
||
|
|
_ -> nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Get the text color for a given mood setting.
|
||
|
|
"""
|
||
|
|
def text_color_for_mood("dark"), do: "#fafafa"
|
||
|
|
def text_color_for_mood("neutral"), do: "#171717"
|
||
|
|
def text_color_for_mood("warm"), do: "#1c1917"
|
||
|
|
def text_color_for_mood("cool"), do: "#0f172a"
|
||
|
|
def text_color_for_mood(_), do: "#171717"
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Convert hex color to RGB list.
|
||
|
|
"""
|
||
|
|
def hex_to_rgb("#" <> hex), do: hex_to_rgb(hex)
|
||
|
|
|
||
|
|
def hex_to_rgb(hex) when byte_size(hex) == 6 do
|
||
|
|
[
|
||
|
|
String.slice(hex, 0..1) |> Integer.parse(16) |> elem(0),
|
||
|
|
String.slice(hex, 2..3) |> Integer.parse(16) |> elem(0),
|
||
|
|
String.slice(hex, 4..5) |> Integer.parse(16) |> elem(0)
|
||
|
|
]
|
||
|
|
end
|
||
|
|
end
|