Expresso.Palette (expresso v0.1.0)

Copy Markdown View Source

Holds the colors of a theme, and makes sure that they meet the minimums of WCAG and APCA

A palette has the 16 colors of a base16 scheme, base00 to base0F. The scheme gives each color a purpose: base00 is the background, base05 the text, base03 the comments, and base08 to base0F the accents. new/4 makes a palette, and it gives each role of the theme one of the colors. A role is a color that the style sheet reads as a custom property, such as --text or --code-keyword. roles/0 lists them.

problems/1 returns each role that does not meet a minimum. Each minimum has two measures: the contrast ratio of WCAG 2.2, and the lightness contrast Lc of APCA. The contrast ratio is too high for a color on a dark background, and APCA corrects this. Thus a color must meet both. These are the minimums:

  • Each role of text, on its background: 4.5:1 and Lc 60. 4.5:1 is the minimum of WCAG 2.2 for normal text, criterion 1.4.3. Lc 60 is the minimum of APCA for the text of content. A slide shows large text, but a projector and the light of a room lower the contrast, so a theme does not use the smaller minimums of large text.
  • Each role of text with the dim opacity of the palette, on its background: 3:1 and Lc 30. 3:1 is the minimum of criterion 1.4.11 for a part that the reader must see. Lc 30 is the minimum of APCA for any text that the reader must be able to read, such as a text that the presenter does not talk about now. A dimmed element is less important, and it must stay readable. The dimming applies to each color of the element, such as a comment of a dimmed line of code, so the role with the lowest contrast sets the opacity. 3:1 is also the minimum of WCAG for large text, and not for normal text. The dim_contrast option of a deck raises it, such as to 4.5:1 for criterion 1.4.3. See with_dim_contrast/2.

docs/architecture.md gives the formulas, and it tells why the dimming of a theme is small.

The accent also colors the progress bar and the frame of the selected page in the overview, and 4.5:1 is more than the 3:1 that criterion 1.4.11 gives for them. adjust/1 changes only the lightness of each role that fails, until it meets the minimum, and it never changes a background. Expresso.Palette.Builtin adjusts each built-in theme, and Expresso.ThemeVerifier gives a warning for a palette of a deck that fails.

Summary

Types

A slot of a base16 scheme, from :base00 to :base0F

t()

A palette: its name, its variant, its 16 colors, and the color of each role

The value of the theme option of a deck

Functions

Change the lightness of each role that fails, until each role meets its minimum

Return the colors of the series of a chart for a variant, in order

Return the custom properties of the palette, as declarations for a rule

Make a palette from the 16 colors of a base16 scheme

Return the palette of one theme of the theme option of a deck

Return each role that does not meet a minimum, with the measure, its value and the minimum

Return each role, the slot of its color, and the role of its background

Return the slots of a base16 scheme, in order

Return a color that passes for each slot with a role of text that fails

Validate the value of the theme option, for the schema of the DSL

Return the palette of each variant of the theme option of a deck

Dim the palette to a higher minimum contrast ratio

Types

slot()

@type slot() :: atom()

A slot of a base16 scheme, from :base00 to :base0F

t()

@type t() :: %Expresso.Palette{
  change: float(),
  colors: %{required(slot()) => Expresso.Color.t()},
  dim_opacity: float(),
  dim_ratio: float(),
  dimmed: %{required(atom()) => {float(), Expresso.Color.t()}},
  name: String.t(),
  roles: %{required(atom()) => Expresso.Color.t()},
  variant: :dark | :light
}

A palette: its name, its variant, its 16 colors, and the color of each role

change is the largest change of lightness that adjust/1 made, in OKLab, from 0 to 1. dimmed holds, for each role of text, its dimmed color and the opacity that gives it. dim_opacity is one opacity that dims each role of text to its minimums. dim_ratio is the minimum contrast ratio of a dimmed role: 3.0 by default, and the dim_contrast option of a deck can raise it. See with_dim_contrast/2. The style sheet dims an image, an SVG file and an embed with it, because their colors are not roles.

theme()

@type theme() :: atom() | %{required(slot()) => Expresso.Color.t()} | keyword()

The value of the theme option of a deck

A name of a built-in theme, a map with a color for each slot, or a keyword list. The keyword list has colors for one theme, or dark and light for two variants, and adjust to adjust each map.

Functions

adjust(palette)

@spec adjust(t()) :: {:ok, t()} | :error

Change the lightness of each role that fails, until each role meets its minimum

The function keeps the hue of each color and each background. It returns :error when no lightness gives a role its minimum.

chart_colors(atom)

@spec chart_colors(:light | :dark) :: [String.t()]

Return the colors of the series of a chart for a variant, in order

iex> Expresso.Palette.chart_colors(:light) |> length()
8

declarations(palette)

@spec declarations(t()) :: String.t()

Return the custom properties of the palette, as declarations for a rule

Each role gets a property, such as --text. Each role of text also gets the property of its dimmed color, such as --text-dim. --dim-opacity holds the dim opacity of the palette, and --chart-1 to --chart-8 hold the colors of the series of a chart for the variant of the palette.

iex> palette = Expresso.Palette.Builtin.fetch!(:default)
iex> Expresso.Palette.declarations(palette) =~ "--text: #000000;"
true
iex> Expresso.Palette.declarations(palette) =~ "--text-dim: #"
true

new(name, variant, colors)

@spec new(String.t(), :dark | :light, %{required(slot()) => Expresso.Color.t()}) ::
  t()

Make a palette from the 16 colors of a base16 scheme

colors holds a #rrggbb color for each slot. Each role of text gets a dimmed color: the role blended on its background at the smallest multiple of 0.05 that keeps the role at 3:1 and at Lc 30. Each role dims as far as its own contrast lets it.

of(name)

@spec of(theme() | nil) :: t()

Return the palette of one theme of the theme option of a deck

The option is the name of a built-in theme, or a map with a #rrggbb color for each slot of base16. A palette of a map is not adjusted. Its variant is dark when its background is dark. A keyword list returns its first variant from variants/1.

problems(palette)

@spec problems(t()) :: [{atom(), :wcag | :apca, float(), number()}]

Return each role that does not meet a minimum, with the measure, its value and the minimum

The measure is :wcag for the contrast ratio, or :apca for the lightness contrast. A role can fail both. The role :dimmed_text stands for the role of text with the lowest contrast at the dim opacity. A palette that meets each minimum returns an empty list.

roles()

@spec roles() :: [{atom(), {slot(), atom() | nil}}]

Return each role, the slot of its color, and the role of its background

A background has nil in place of a background.

slots()

@spec slots() :: [slot()]

Return the slots of a base16 scheme, in order

suggestions(palette)

@spec suggestions(t()) :: %{required(slot()) => Expresso.Color.t()}

Return a color that passes for each slot with a role of text that fails

A slot can color more than one role, such as base05 for text on base00 and for code_text on base01. The color of the slot changes its lightness with Expresso.Color.adjust/4 against each background in turn, and it must then meet the minimums on each one. A slot with no such color is not in the map. Expresso.ThemeVerifier puts the color into its warning.

iex> colors = Expresso.Palette.Builtin.fetch!(:default).colors
iex> palette = Expresso.Palette.of(%{colors | base03: "#dddddd"})
iex> %{base03: color} = Expresso.Palette.suggestions(palette)
iex> problems = Expresso.Palette.problems(Expresso.Palette.of(%{colors | base03: color}))
iex> Enum.filter(problems, &(elem(&1, 0) == :code_comment))
[]

validate(options)

@spec validate(term()) :: {:ok, theme()} | {:error, String.t()}

Validate the value of the theme option, for the schema of the DSL

The value is the name of a built-in theme, or a map with exactly the 16 slots of base16 and a #rrggbb color for each one. The contrast is not part of the validation: Expresso.ThemeVerifier gives a warning for it.

The value can also be a keyword list:

  • colors with a name or a map, for one theme, or

  • dark and light, each with a name or a map, for two variants,

  • and adjust with true or false, optional.

    iex> Expresso.Palette.validate(dark: :dracula, light: :default, adjust: true)

    iex> {:error, message} = Expresso.Palette.validate(dark: :dracula) iex> message =~ "needs colors, or both dark and light" true

    iex> Expresso.Palette.validate(:dracula)

    iex> {:error, message} = Expresso.Palette.validate(:plaid) iex> message =~ "is not a built-in theme" true

variants(options)

@spec variants(theme() | nil) :: [{:light | :dark | nil, t()}]

Return the palette of each variant of the theme option of a deck

A theme with one variant returns [{nil, palette}]. A keyword list with dark and light returns [light: palette, dark: palette]. With adjust: true, the palette of each map gets adjust/1, as a built-in theme does. A map that no lightness can adjust keeps its colors, and Expresso.ThemeVerifier then gives a warning for each role that fails.

iex> [{nil, palette}] = Expresso.Palette.variants(:dracula)
iex> palette.name
"Dracula"

iex> Expresso.Palette.variants(dark: :dracula, light: :default) |> Keyword.keys()
[:light, :dark]

with_dim_contrast(palette, ratio)

@spec with_dim_contrast(t(), number() | nil) :: t()

Dim the palette to a higher minimum contrast ratio

Each role of text then dims only as far as the role keeps ratio on its background, and the dim opacity follows. The minimum of APCA stays at Lc 30. A ratio of 4.5 is the minimum of WCAG 2.2 for normal text, criterion 1.4.3. nil keeps the palette as it is.

iex> palette = Expresso.Palette.Builtin.fetch!(:default)
iex> Expresso.Palette.with_dim_contrast(palette, 4.5).dim_opacity >= palette.dim_opacity
true