Color.Palette.Sort
(Color v0.12.1)
Copy Markdown
Sort a list of colours into a perceptually-ordered sequence.
Useful when you have a heterogeneous bag of colours — brand swatches, Material role tokens, extracted image palette, etc. — and want to lay them out as a linear strip or grid in an order a human will find natural.
Four strategies are available, each with a different contract:
:hue_lightness(default) — "rainbow" order: hue first, then lightness. Matches the usual user expectation of a ROYGBIV ramp when the input spans multiple hues. Seesort/2for the design decisions.:stepped_hue— Alan Zucconi's bucketed algorithm. Coarse rainbow order at the bucket level, with alternating-direction lightness ramps within each bucket. Produces visually continuous swatch grids at the cost of local non-monotonicity.:lightness— dark → light, hue ignored. The right choice for sequential legends or contrast demos.:material_pbr— splits%Color.Material{}inputs into dielectric and metallic buckets first, then applies hue + lightness ordering within each bucket, with roughness as a final tiebreaker. Plain colour inputs are treated as dielectrics with default roughness. The right choice for palettes that mix plastic and metal finishes.
All strategies operate in Oklch internally (perceptually uniform
hue and lightness axes). %Color.Material{} inputs are
returned as %Color.Material{} structs; other inputs are
returned as %Color.SRGB{}.
Summary
Functions
Sorts a list of colours into a perceptually-ordered sequence.
Functions
@spec sort( [Color.input()], keyword() ) :: [Color.SRGB.t()]
Sorts a list of colours into a perceptually-ordered sequence.
Arguments
colorsis a list of values accepted byColor.new/1— hex strings, CSS named colours,%Color.SRGB{}structs, Oklch structs, etc. Mixed types are allowed.
Options
:strategyis the sort algorithm. One of:hue_lightness(default),:stepped_hue, or:lightness. See the moduledoc for a description of each.:chroma_thresholdis the Oklch chroma below which a colour is treated as an achromatic gray rather than a chromatic hue. Grays are grouped into a separate bucket (see:grays) because their hue angle is numerically unstable and would otherwise scatter them randomly through the rainbow. Default0.02.:hue_originis the Oklch hue angle (in degrees, 0–360) where the rainbow "starts" — that is, where the sort cuts the hue circle. Default0.0(red/orange region). Set to270.0to start the rainbow at blue/purple, for example. Applies to:hue_lightnessand:stepped_hueonly.:grayscontrols where achromatic colours land in the output.:before(default) places them at the start of the list, before the chromatic rainbow.:afterplaces them at the end.:excludedrops them entirely. Applies to:hue_lightnessand:stepped_hueonly.:bucketsis the number of hue buckets for:stepped_hue. Default8. Only meaningful for that strategy.:metallic_thresholdis the cutoff between dielectric and metallic materials in(0.0, 1.0]. Default0.5. A material withmetallic >= thresholdsorts into the metals bucket; below goes into dielectrics. Applies to:material_pbronly.:metalscontrols whether the metallic bucket comes:beforeor:afterthe dielectric bucket. Default:after, matching typical PBR asset-browser layouts (plastics first, metals second). Applies to:material_pbronly.:roughness_orderis the tiebreaker direction when two otherwise-equal materials differ only in roughness.:glossy_first(default) puts low-roughness materials first;:matte_firstreverses. Applies to:material_pbronly.
Returns
- A list of
%Color.SRGB{}structs in sorted order.
Material-aware order (:material_pbr)
When the input mixes %Color.Material{} structs (plastic,
metal, varnish, ceramic), a flat colour sort puts a red
plastic next to a red-anodized metal — visually the same hue
but categorically different finishes. :material_pbr respects
the finish cliff by sorting as a tuple:
Dielectric vs metallic — split by
metallic >= metallic_threshold. Dielectrics go before metals by default (:metalsoption to flip).Hue, then lightness — within each bucket, apply the
:hue_lightnesslogic on the material's base colour. Near-gray materials form their own sub-bucket within each metallic group.Roughness tiebreaker — when materials tie on hue and lightness (e.g., gloss-red vs matte-red), order by roughness.
:glossy_first(default) puts mirrors before matte;:matte_firstreverses.
Plain colour inputs (hex strings, %Color.SRGB{} structs,
etc.) are treated as implicit dielectrics with roughness = 0.5.
A mixed list of materials and plain colours sorts them
together into the dielectric bucket.
Rainbow order (:hue_lightness)
The default strategy produces a linear rainbow and is worth describing precisely because several small decisions affect what a user sees.
Convert to Oklch. Hue angles come from Oklch, not HSL, because Oklch's hue axis is perceptually uniform. In HSL, yellow occupies a tiny sliver and green dominates; in Oklch the visual spacing of a sorted rainbow matches the numerical spacing of the sort.
Partition by chroma. Colours with
C < chroma_thresholdare considered achromatic. Near-grays have mathematically unstable hue (a 0.001 change in chroma can flip the angle wildly), so sorting them with the chromatic colours scatters them across the rainbow in visually random places. The gray bucket is sorted independently by lightness and placed at the start (or end, via:grays).Sort the chromatic bucket. Primary key is
(H - hue_origin) mod 360, ascending. Secondary key is lightnessL, ascending — so within a single hue, the darkest shade comes first. This produces ROYGBIV → wrap whenhue_origin = 0.The wraparound caveat. A hue circle has no start or end; the sort has to cut it somewhere. The first and last colours in the output are therefore neighbours on the hue wheel but visually far apart in a linear strip. This is intrinsic to projecting a circle onto a line — pick
hue_originto put the cut somewhere your input doesn't cross, if you can.Known surprises. Brown sits near orange and yellow in any hue-based sort (brown is dark orange/yellow); users who mentally file brown as its own category will find its placement unexpected. Magenta is a non-spectral colour and lands adjacent to blue and red in hue-angle space, which is topologically correct but may surprise users who expect a strict wavelength ordering.
Examples
iex> hexes = ["#808080", "#ff0000", "#00ff00", "#0000ff", "#ffff00"]
iex> hexes |> Color.Palette.Sort.sort() |> Enum.map(&Color.to_hex/1)
["#808080", "#ff0000", "#ffff00", "#00ff00", "#0000ff"]
iex> hexes = ["#ff0000", "#ffff00", "#00ff00"]
iex> hexes |> Color.Palette.Sort.sort(strategy: :lightness) |> Enum.map(&Color.to_hex/1)
["#ff0000", "#00ff00", "#ffff00"]
iex> hexes = ["#ff0000", "#0000ff"]
iex> hexes |> Color.Palette.Sort.sort(grays: :exclude) |> Enum.map(&Color.to_hex/1)
["#ff0000", "#0000ff"]
iex> items = [
...> Color.Material.new("#ffd700", metallic: 1.0, roughness: 0.05, name: "gold"),
...> Color.Material.new("#ff0000", metallic: 0.0, roughness: 0.6, name: "red plastic")
...> ]
iex> items
...> |> Color.Palette.Sort.sort(strategy: :material_pbr)
...> |> Enum.map(& &1.name)
["red plastic", "gold"]