Current section
Files
Jump to
Current section
Files
lib/tiny_color.ex
defmodule TinyColor do
@moduledoc """
Documentation for `TinyColor`.
"""
@white %TinyColor.RGB{red: 255, green: 255, blue: 255}
@black %TinyColor.RGB{red: 0, green: 0, blue: 0}
@typedoc """
A representation of a color in hue, saturation, lightness and alpha.
"""
@type hsl_color :: %TinyColor.HSL{
hue: :float,
saturation: :float,
lightness: :float,
alpha: :float
}
@typedoc """
A representation of a color in hue, saturation, value and alpha.
"""
@type hsv_color :: %TinyColor.HSV{
hue: :float,
saturation: :float,
value: :float,
alpha: :float
}
@typedoc """
A representation of a color in red, green, blue and alpha.
"""
@type rgb_color :: %TinyColor.RGB{
red: :float,
green: :float,
blue: :float,
alpha: :float
}
@typedoc """
A representation of a color in the oklab color space with optional alpha
"""
@type oklab_color :: %TinyColor.OKLab{
l: :float,
a: :float,
b: :float,
alpha: :float
}
@typedoc """
A representation of a color in any supported system.
"""
@type color :: hsl_color | hsv_color | rgb_color
@doc ~S"""
Parses the given values into an HSL struct
## Examples
iex> TinyColor.hsl(128, 0.41, 0.13)
%TinyColor.HSL{hue: 128.0, saturation: 41.0, lightness: 13.0, alpha: 1.0}
iex> TinyColor.hsl(450, 0.41, 0.13)
%TinyColor.HSL{alpha: 1.0, hue: 90.0, lightness: 13.0, saturation: 41.0}
iex> TinyColor.hsl(128, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSL{hue: 128.0, saturation: 26.5, lightness: 54.0, alpha: 1.0}
iex> TinyColor.hsl(450, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSL{alpha: 1.0, hue: 90.0, lightness: 54.0, saturation: 26.5}
iex> TinyColor.hsl({0.54, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSL{hue: 194.4, saturation: 26.5, lightness: 54.0, alpha: 1.0}
iex> TinyColor.hsl({1.4, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSL{alpha: 1.0, hue: 143.99999999999994, lightness: 54.0, saturation: 26.5}
iex> TinyColor.hsl(128, 0.41, 0.13, 0.5)
%TinyColor.HSL{hue: 128.0, saturation: 41.0, lightness: 13.0, alpha: 0.5}
iex> TinyColor.hsl(450, 0.41, 0.13, 0.5)
%TinyColor.HSL{alpha: 0.5, hue: 90.0, lightness: 13.0, saturation: 41.0}
iex> TinyColor.hsl(128, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSL{hue: 128.0, saturation: 26.5, lightness: 54.0, alpha: 0.5}
iex> TinyColor.hsl(450, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSL{alpha: 0.5, hue: 90.0, lightness: 54.0, saturation: 26.5}
iex> TinyColor.hsl({0.54, :percent}, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSL{hue: 194.4, saturation: 26.5, lightness: 54.0, alpha: 0.5}
iex> TinyColor.hsl({1.4, :percent}, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSL{alpha: 0.5, hue: 143.99999999999994, lightness: 54.0, saturation: 26.5}
"""
def hsl(hue, saturation, lightness, alpha \\ 1.0) do
TinyColor.HSL.new(hue, saturation, lightness, alpha)
end
@doc ~S"""
Parses the given values into an HSL struct
## Examples
iex> TinyColor.hsv(128, 0.41, 0.13)
%TinyColor.HSV{hue: 128.0, saturation: 41.0, value: 13.0}
iex> TinyColor.hsv(450, 0.41, 0.13)
%TinyColor.HSV{hue: 90.0, saturation: 41.0, value: 13.0}
iex> TinyColor.hsv(128, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{hue: 128.0, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv(450, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{hue: 90.0, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv({0.54, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{hue: 194.4, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv({1.4, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{hue: 143.99999999999994, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv(128, 0.41, 0.13)
%TinyColor.HSV{hue: 128.0, saturation: 41.0, value: 13.0, alpha: 1.0}
iex> TinyColor.hsv(450, 0.41, 0.13)
%TinyColor.HSV{alpha: 1.0, hue: 90.0, saturation: 41.0, value: 13.0}
iex> TinyColor.hsv(128, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{hue: 128.0, saturation: 26.5, value: 54.0, alpha: 1.0}
iex> TinyColor.hsv(450, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{alpha: 1.0, hue: 90.0, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv({0.54, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{hue: 194.4, saturation: 26.5, value: 54.0, alpha: 1.0}
iex> TinyColor.hsv({1.4, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.HSV{alpha: 1.0, hue: 143.99999999999994, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv(128, 0.41, 0.13, 0.5)
%TinyColor.HSV{hue: 128.0, saturation: 41.0, value: 13.0, alpha: 0.5}
iex> TinyColor.hsv(450, 0.41, 0.13, 0.5)
%TinyColor.HSV{alpha: 0.5, hue: 90.0, saturation: 41.0, value: 13.0}
iex> TinyColor.hsv(128, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSV{hue: 128.0, saturation: 26.5, value: 54.0, alpha: 0.5}
iex> TinyColor.hsv(450, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSV{alpha: 0.5, hue: 90.0, saturation: 26.5, value: 54.0}
iex> TinyColor.hsv({0.54, :percent}, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSV{hue: 194.4, saturation: 26.5, value: 54.0, alpha: 0.5}
iex> TinyColor.hsv({1.4, :percent}, {0.265, :percent}, {0.54, :percent}, 0.5)
%TinyColor.HSV{alpha: 0.5, hue: 143.99999999999994, saturation: 26.5, value: 54.0}
"""
def hsv(hue, saturation, value, alpha \\ 1.0) do
TinyColor.HSV.new(hue, saturation, value, alpha)
end
@doc ~S"""
Parses the given values into an RGB struct
## Examples
iex> TinyColor.rgb(128, 129, 130)
%TinyColor.RGB{red: 128.0, green: 129.0, blue: 130.0, alpha: 1.0}
iex> TinyColor.rgb(333, 129, 130)
%TinyColor.RGB{alpha: 1.0, blue: 130.0, green: 129.0, red: 255.0}
iex> TinyColor.rgb(128, 129, 130, 0.5)
%TinyColor.RGB{red: 128.0, green: 129.0, blue: 130.0, alpha: 0.5}
iex> TinyColor.rgb(128, 129, 130, -0.5)
%TinyColor.RGB{alpha: 0.0, blue: 130.0, green: 129.0, red: 128.0}
iex> TinyColor.rgb({0.125, :percent}, {0.265, :percent}, {0.525, :percent})
%TinyColor.RGB{red: 31.875, green: 67.575, blue: 133.875, alpha: 1.0}
iex> TinyColor.rgb({1.4, :percent}, {0.265, :percent}, {0.54, :percent})
%TinyColor.RGB{alpha: 1.0, blue: 137.70000000000002, green: 67.575, red: 255.0}
iex> TinyColor.rgb({0.125, :percent}, {0.265, :percent}, {0.525, :percent}, 0.5)
%TinyColor.RGB{red: 31.875, green: 67.575, blue: 133.875, alpha: 0.5}
iex> TinyColor.rgb({0.54, :percent}, {0.265, :percent}, {0.54, :percent}, -0.5)
%TinyColor.RGB{alpha: 0.0, blue: 137.70000000000002, green: 67.575, red: 137.70000000000002}
"""
def rgb(red, green, blue, alpha \\ 1.0) do
TinyColor.RGB.new(red, green, blue, alpha)
end
def oklab(l, a, b, alpha \\ 1.0) do
TinyColor.OKLab.new(l, a, b, alpha)
end
def light?(color) do
not dark?(color)
end
def dark?(color) do
brightness(color) < 128
end
def brightness(color) do
%{red: r, blue: b, green: g} = TinyColor.Conversions.to_rgb(color)
(r * 299 + g * 587 + b * 114) / 1000
end
@spec luminance(color()) :: float
def luminance(color) do
{rs_rgb, gs_rgb, bs_rgb, _} =
color
|> TinyColor.Conversions.to_rgb()
|> TinyColor.RGB.percentages()
r =
if rs_rgb <= 0.03928 do
rs_rgb / 12.92
else
:math.pow((rs_rgb + 0.055) / 1.055, 2.4)
end
g =
if gs_rgb <= 0.03928 do
gs_rgb / 12.92
else
:math.pow((gs_rgb + 0.055) / 1.055, 2.4)
end
b =
if bs_rgb <= 0.03928 do
bs_rgb / 12.92
else
:math.pow((bs_rgb + 0.055) / 1.055, 2.4)
end
0.2126 * r + 0.7152 * g + 0.0722 * b
end
def lighten(color, amount \\ 10) do
color = TinyColor.Conversions.to_hsl(color)
TinyColor.hsl(color.hue, color.lightness + amount, color.saturation, color.alpha)
end
def brighten(color, amount \\ 10) do
%{red: r, green: g, blue: b, alpha: a} = TinyColor.Conversions.to_rgb(color)
TinyColor.rgb(
r - round(255 * -(amount / 100)),
g - round(255 * -(amount / 100)),
b - round(255 * -(amount / 100)),
a
)
end
def darken(color, amount \\ 10) do
color = TinyColor.Conversions.to_hsl(color)
TinyColor.hsl(color.hue, color.lightness - amount, color.saturation, color.alpha)
end
def tint(color, amount \\ 10) do
mix(color, TinyColor.Named.get("white"), amount)
end
def shade(color, amount \\ 10) do
mix(color, TinyColor.Named.get("black"), amount)
end
def desaturate(color, amount \\ 10) do
color = TinyColor.Conversions.to_hsl(color)
TinyColor.hsl(color.hue, color.lightness, color.saturation - amount, color.alpha)
end
def saturate(color, amount \\ 10) do
color = TinyColor.Conversions.to_hsl(color)
TinyColor.hsl(color.hue, color.lightness, color.saturation + amount, color.alpha)
end
def grayscale(color), do: desaturate(color, 100)
def spin(color, amount) do
color = TinyColor.Conversions.to_hsl(color)
TinyColor.hsl(color.hue + amount, color.lightness, color.saturation, color.alpha)
end
def mix(self, color, amount \\ 50) do
%{red: r1, green: g1, blue: b1, alpha: a1} = TinyColor.Conversions.to_rgb(self)
%{red: r2, green: g2, blue: b2, alpha: a2} = TinyColor.Conversions.to_rgb(color)
percentage = amount / 100
TinyColor.rgb(
(r2 - r1) * percentage + r1,
(g2 - g1) * percentage + g1,
(b2 - b1) * percentage + b1,
(a2 - a1) * percentage + a1
)
end
@spec contrast(color(), color()) :: float()
def contrast(color1, color2) do
c1_luminance = luminance(color1)
c2_luminance = luminance(color2)
(max(c1_luminance, c2_luminance) + 0.05) / (min(c1_luminance, c2_luminance) + 0.05)
end
@spec readable?(color(), color(), Keyword.t([{:level, :AA | :AAA}, {:size, :small | :large}])) ::
boolean()
def readable?(color1, color2, opts \\ []) do
level = Keyword.get(opts, :level, :AA)
size = Keyword.get(opts, :size, :small)
case {level, size, contrast(color1, color2)} do
{:AAA, :small, level} -> level >= 7
{:AAA, :large, level} -> level >= 4.5
{:AA, :small, level} -> level >= 4.5
{:AA, :large, level} -> level >= 3
_ -> false
end
end
@spec most_readable(
color(),
list(color()),
[{atom, [any]}]
) :: color()
def most_readable(base, choices, opts \\ []) do
best_choice = Enum.max_by(choices, fn choice -> contrast(base, choice) end)
include_fallbacks = Keyword.get(opts, :include_fallback_colors, false)
if readable?(base, best_choice, opts) or not include_fallbacks do
best_choice
else
most_readable(base, [@white, @black])
end
end
@doc """
Checks for equality of two colors.
"""
@spec equal?(any, any) :: boolean
def equal?(nil, nil), do: true
def equal?(nil, _), do: false
def equal?(_, nil), do: false
def equal?(%TinyColor.RGB{} = a, %TinyColor.RGB{} = b), do: a == b
def equal?(%TinyColor.HSL{} = a, %TinyColor.HSL{} = b), do: a == b
def equal?(%TinyColor.HSV{} = a, %TinyColor.HSV{} = b), do: a == b
def equal?(color1, color2) do
a = color1 |> TinyColor.Conversions.to_rgb() |> to_string()
b = color2 |> TinyColor.Conversions.to_rgb() |> to_string()
a == b
end
end