Packages

A comprehensive color library: 21 color spaces, chromatic adaptation, ICC rendering intents, ΔE2000 / WCAG / APCA contrast, gamut mapping, color mixing and gradients, blend modes, color harmonies, color temperature, spectral pipeline, and a full CSS Color 4 / 5 parser. Zero runtime dependencies.

Current section

Files

Jump to
color lib color palette visualizer.ex
Raw

lib/color/palette/visualizer.ex

defmodule Color.Palette.Visualizer do
@moduledoc """
A web-based visualizer for the palettes produced by
`Color.Palette`.
This module is a `Plug.Router` that can be mounted inside a
Phoenix or Plug application, or run standalone during
development via `Color.Palette.Visualizer.Standalone`.
## Three views
* `/tonal`[UI Colors](https://uicolors.app/) style. One seed
becomes a row of swatches with hex, OKLCH, and contrast
values, plus exportable CSS custom properties and Tailwind
config.
* `/theme`[Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)
style. Five tonal scales and a grid of Material Design 3
role tokens (primary / on-primary / surface / outline / …)
for light and dark schemes.
* `/contrast`[Adobe Leonardo](https://leonardocolor.io/)
style. Contrast-targeted swatches against a chosen background
and a pass/fail matrix for common text sizes.
All state lives in the URL — copy a URL and you've shared the
palette.
## Mounting in Phoenix
In your `router.ex`:
forward "/palette", Color.Palette.Visualizer
## Running standalone
Color.Palette.Visualizer.Standalone.start(port: 4001)
## Optional dependencies
The visualizer pulls in `:plug` (required for the router) and
`:bandit` (only used by the standalone helper). Both are
declared `optional: true` in this library's `mix.exs`, so you
must add them to your own project's deps to use the visualizer:
{:plug, "~> 1.15"},
{:bandit, "~> 1.5"}
The core palette algorithms have no such dependency and will
compile without either of these in place.
"""
# Deferred compile-time check: raise at use time, not at
# compile time of this library, so users who never touch the
# visualizer don't need plug installed.
if Code.ensure_loaded?(Plug.Router) do
use Plug.Router
plug(Plug.Logger, log: :debug)
plug(:match)
plug(Plug.Parsers, parsers: [:urlencoded], pass: ["text/*"])
plug(:dispatch)
alias Color.Palette.Visualizer.Assets
alias Color.Palette.Visualizer.ContrastView
alias Color.Palette.Visualizer.ThemeView
alias Color.Palette.Visualizer.TonalView
get "/" do
base = base_path(conn)
conn
|> Plug.Conn.put_resp_header("location", base <> "/tonal")
|> Plug.Conn.send_resp(302, "")
end
get "/tonal" do
params = parse_params(conn.params, :tonal)
html(conn, TonalView.render(params, base_path(conn)))
end
get "/theme" do
params = parse_params(conn.params, :theme)
html(conn, ThemeView.render(params, base_path(conn)))
end
get "/contrast" do
params = parse_params(conn.params, :contrast)
html(conn, ContrastView.render(params, base_path(conn)))
end
get "/assets/style.css" do
conn
|> Plug.Conn.put_resp_content_type("text/css")
|> Plug.Conn.put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> Plug.Conn.send_resp(200, Assets.css())
end
match _ do
send_resp(conn, 404, "Not found")
end
# ---- helpers ---------------------------------------------------------
defp html(conn, iodata) do
conn
|> Plug.Conn.put_resp_content_type("text/html")
|> Plug.Conn.send_resp(200, IO.iodata_to_binary(iodata))
end
# When mounted via `forward "/palette", ...`, Plug sets
# script_name. Rebuild the base URL from it so link hrefs
# resolve correctly whether mounted at / or at /palette.
defp base_path(%Plug.Conn{script_name: []}), do: ""
defp base_path(%Plug.Conn{script_name: segments}), do: "/" <> Enum.join(segments, "/")
defp parse_params(params, :tonal) do
%{
seed: Map.get(params, "seed") |> blank_default("#3b82f6"),
hue_drift: truthy?(Map.get(params, "hue_drift")),
name: Map.get(params, "name") |> blank_default(nil)
}
end
defp parse_params(params, :theme) do
%{
seed: Map.get(params, "seed") |> blank_default("#3b82f6"),
scheme: atom_default(Map.get(params, "scheme"), [:light, :dark], :light)
}
end
defp parse_params(params, :contrast) do
%{
seed: Map.get(params, "seed") |> blank_default("#3b82f6"),
background: Map.get(params, "background") |> blank_default("white"),
metric: atom_default(Map.get(params, "metric"), [:wcag, :apca], :wcag)
}
end
defp blank_default(nil, default), do: default
defp blank_default("", default), do: default
defp blank_default(value, _), do: value
defp truthy?(nil), do: false
defp truthy?("0"), do: false
defp truthy?("false"), do: false
defp truthy?(_), do: true
defp atom_default(nil, _allowed, default), do: default
defp atom_default(value, allowed, default) when is_binary(value) do
atom =
try do
String.to_existing_atom(value)
rescue
ArgumentError -> default
end
if atom in allowed, do: atom, else: default
end
defp atom_default(_, _, default), do: default
else
@compile_error "Color.Palette.Visualizer requires :plug. " <>
"Add `{:plug, \"~> 1.15\"}` to your project's deps."
@doc false
def init(_), do: raise(@compile_error)
@doc false
def call(_, _), do: raise(@compile_error)
end
end