#### <sup>[Ollin](../../index.md) → [Documentation](../index.md) → [Drawing](index.md) → `Color`</sup>

---

## Color

`Color` is an RGBA color with `Double` components in `0...1`. Most of the types below map a single number to a `Color` through the same `color(at:)` call. `Ramp`, `CosinePalette`, and `Colormap` do it smoothly, and the discrete `Palette` does it in steps. That is the usual way to drive color from a value or from time.

### Contents

- [Color](#color)
- [OKLab, OKLCH, OKHSL](#oklab)
- [CIELAB](#cielab)
- [Measuring color](#measuring): `distance(to:)`, `nearest(to:)`, `histogram(of:)`
- [Mixing](#mixing)
- [Ramp](#ramp)
- [Gradient paint](#gradient)
- [A shader as paint](#shader-paint)
- [Palette](#palette)
- [Shades whose hue drifts](#shades)
- [Loading palettes from a file](#palette-files)
- [Extracting a palette from an image](#palette-extraction)
- [Dithering an image to a palette](#dithering)
- [CosinePalette](#cosinepalette)
- [Colormap](#colormap)

<a name="color"></a>

### `Color`

```swift
Color(red: Double, green: Double, blue: Double, alpha: Double = 1)
Color(white: Double, alpha: Double = 1)
Color(hex: UInt32, alpha: Double = 1)     // 24-bit RGB: Color(hex: 0xFF0066)
Color(hex: String)                        // "#RGB" "#RGBA" "#RRGGBB" "#RRGGBBAA" → Color?
Color(hue: Double, saturation: Double, brightness: Double, alpha: Double = 1)
Color(kelvin: Double, alpha: Double = 1)  // blackbody color temperature
Color(linear: Double, green: Double, blue: Double, alpha: Double = 1)   // named by its light
```

Named constants come in two tiers. The first tier is the essentials: `.white`, `.black`, `.gray`, `.clear`, and the additive primaries `.red`, `.green`, and `.blue`, so `green` is the pure `(0, 1, 0)`. The second tier is a fuller set around them, so that common colors read by name. The names and their exact sRGB values come from the [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/#named-colors) list:

`.yellow` `.cyan` `.magenta` `.orange` `.purple` `.pink` `.brown` · `.crimson` `.tomato` `.coral` `.salmon` `.gold` · `.darkGreen` `.forestGreen` `.seaGreen` `.olive` `.teal` `.turquoise` · `.navy` `.royalBlue` `.steelBlue` `.skyBlue` `.indigo` · `.violet` `.orchid` `.plum` `.lavender` `.maroon` · `.tan` `.khaki` `.beige` `.ivory` `.silver` `.lightGray` `.darkGray` `.slateGray`

CSS defines `green` as the darker `#008000`. Ollin keeps `green` as the pure primary, so use `.darkGreen` or `.forestGreen` when you want the deeper tone.

```swift
background(.white)
fill(.coral)
fill(Color(red: 0.2, green: 0.5, blue: 0.9))
stroke(Color(white: 0.1))
```

**Hex** comes in two forms. The integer form is for literals in code. It is compile-checked, there is nothing to parse, and it is never optional. An integer carries no digit count, because `0xFFF` and `0x000FFF` are the same value. So the integer form is always six digits of RGB, and alpha is its own parameter:

```swift
background(Color(hex: 0x14171C))
fill(Color(hex: 0xFF0066, alpha: 0.5))
```

The string form takes the full grammar: `"#RGB"`, `"#RGBA"`, `"#RRGGBB"`, and `"#RRGGBBAA"`. The `#` is optional, and case is ignored. The result is an optional, so a string from a file or from the network fails cleanly instead of trapping:

```swift
let brand = Color(hex: "#ff0066")!        // a literal you know is well-formed
if let c = Color(hex: userString) { fill(c) }
```

**Hue, saturation, brightness** are each in `0...1`. Hue wraps, so 1.2 reads as 0.2. That means you can drive hue from `time` or any other unbounded value without keeping it in range yourself. Saturation and brightness clamp. The `HSBWheel` example draws the classic wheel with this initializer:

```swift
fill(Color(hue: time * 0.1, saturation: 0.8, brightness: 1))   // cycle the rainbow
```

You can read the same three values back from any color through its `hue`, `saturation`, and `brightness` properties. So you can vary a color you already hold instead of rebuilding it from numbers. The `Mixing` example labels the midpoint of every band that way:

```swift
let shifted = Color(hue: fract(base.hue + 0.1), saturation: base.saturation,
                    brightness: base.brightness)
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/02-Color/HueWheels-dark.jpg">
  <img src="../../Guide/Images/02-Color/HueWheels.jpg" alt="Left: RGB as three component bars adding up to an orange. Right: the HSB hue wheel with saturation and brightness sweeps" width="680">
</picture>

**Color temperature** describes light the way a photographer does. `Color(kelvin:)` takes a blackbody temperature. The result is warm and orange at the low end, cool and blue at the high end, and neutral white around 6600. It is an ordinary sRGB `Color`, so it works in `fill`, `background`, or as a `Light`'s color. The 3D [lighting presets](../3d/3d.md#lights) use it, so a lighting rig is written with the actual temperatures of its lights. `kelvin` clamps to `1000...40000`.

```swift
directionalLight(Color(kelvin: 5600), direction: Vector3(-0.5, -0.8, -0.4))  // daylight key
fill(Color(kelvin: 3200))                                                    // warm tungsten
```

**Linear light** is the other way to name a color. A `Color` stores sRGB components, which are what a display takes, but the middle of a frame works in light, where a number is proportional to what it stands for (see [Light and color](../concepts/light.md)). `linearRGB` is the color as light, its three components through the sRGB curve in `Double`, and `Color(linear:green:blue:)` names a color from such numbers. That is the pair a lighting calculation wants: shade in light, then hand the result to `fill`. Nothing clamps, so a value past 1 stays a highlight past white. The two curves themselves are `Color.srgbToLinear` and `Color.linearToSrgb`, one component at a time, the same curves the [shader library](../shaders/shaderlibrary.md) runs per pixel. `linearRGBA` is the same reading as `Float` with alpha, the form the GPU takes.

```swift
let lit = albedo.linearRGB * diffuse                    // shaded in light
fill(Color(linear: lit.x, green: lit.y, blue: lit.z))   // back to a color
let bright = Color(linear: 2, green: 1.8, blue: 1.5)    // a highlight past white
```

**Two component helpers** finish the type. `withAlpha(_:)` returns the same color at a different opacity and leaves the original unchanged. That is the everyday fade. `luminance` is the color's perceived brightness in `0...1`. It is the Rec. 709 weighted sum of the linearized components, so green counts most and blue least, which matches how the eye weighs them. That makes it the value to read for image-driven work. Sample a pixel with the [`Image`](images.md#pixels) subscript, then size or choose marks by `pixel.luminance`.

```swift
fill(ink.withAlpha(0.3))                  // a translucent version of a held color
let size = cell * image[x, y].luminance   // marks scaled by perceived brightness
```

**Three derived colors** come from the same held color. `lighter(by:)` and `darker(by:)` move its perceived lightness by an amount in OKLab units, where black is 0 and white is 1. The default step is a tenth. Hue and chroma stay where they were. The only thing the screen may take away is chroma it cannot show at the new lightness. A vivid red pushed all the way up ends at white. `complement` is the opposite hue at the same lightness and chroma, the color `Palette.complementary(of:)` puts beside the base. A gray has no opposite and comes back unchanged. All three keep the alpha.

```swift
stroke(ink.darker(by: 0.25))              // a quarter of the way to black, same hue
fill(ink.lighter())                       // one step up
drawCircle(x, y, r)
fill(ink.complement)                      // the opposite hue at the same weight
```

<a name="oklab"></a>

### OKLab, OKLCH, OKHSL

These are three views of one perceptual model. Each is a small value type that converts to and from `Color`, and alpha stays on the `Color`:

```swift
OKLab(l:a:b:)     OKLab(_ color: Color)     Color(_ lab: OKLab, alpha: Double = 1)
OKLCH(l:c:h:)     OKLCH(_ color: Color)     Color(_ lch: OKLCH, alpha: Double = 1)
OKHSL(h:s:l:)     OKHSL(_ color: Color)     Color(_ hsl: OKHSL, alpha: Double = 1)
```

- **`OKLab`** is the workhorse of the three, and the space for color *math*. `l` is perceived lightness in `0...1`, `a` runs from green to red, and `b` runs from blue to yellow. Mixing through it comes out visually even.
- **`OKLCH`** is OKLab in polar form (lightness, chroma, hue). It is the space for adjusting hue and chroma, because turning `h` leaves lightness and colorfulness alone. The maximum displayable chroma depends on hue and lightness, so a raised `c` can ask for a color the screen cannot show. Converting to `Color` maps such a color back by reducing chroma at constant lightness and hue. The color keeps its hue and lightness, and it is as vivid as sRGB allows.
- **`OKHSL`** fits the same model into the sRGB gamut. `s` and `l` run `0...1`, and every combination is displayable. It is the space for *generated* color, for example a random hue at the same perceived lightness.

Hue is a turn in `0...1` everywhere it appears, like the HSB initializer, and it wraps the same way.

```swift
// Twelve hues that genuinely read as the same lightness:
for i in 0..<12 {
    fill(Color(OKHSL(h: Double(i) / 12, s: 0.9, l: 0.65)))
    drawCircle(60 + Double(i) * 80, height / 2, 30)
}

var lch = OKLCH(brand)
lch.h += 0.5                 // the complementary hue, same lightness and chroma
let complement = Color(lch)  // or, in one word, brand.complement
```

<a name="cielab"></a>

### CIELAB

`CIELab` is the space printers, swatch books, and color-difference formulas are written in. `l` runs from 0 (black) to 100 (white), `a` runs from green to red, and `b` runs from blue to yellow. It converts to and from `Color` like the OKLab family:

```swift
CIELab(l:a:b:)    CIELab(_ color: Color)    Color(_ lab: CIELab, alpha: Double = 1)
```

The white it measures against is D50. ICC profiles, CSS's `lab()`, and image editors use that white too. So the Lab numbers printed on a swatch or shown in a color picker come in as they are. sRGB white converts to exactly `(100, 0, 0)`. A vivid printed or measured color is often outside sRGB. It maps back the way an OKLab color does, with its chroma reduced at constant OKLab lightness and hue (which moves L* by about a unit and the Lab hue by a few degrees).

```swift
let terracotta = Color(CIELab(l: 55, a: 32, b: 40))     // a swatch, by its numbers
let lab = CIELab(terracotta)
```

Use `CIELab` to bring measured numbers in and to compare colors with CIEDE2000, below. For mixing and for generated color, OKLab is still the better space, because it blends more evenly and holds hue steadier as chroma changes.

<a name="measuring"></a>

### Measuring color

**How different two colors look.** `distance(to:)` measures it, 0 for the same color and more the further apart they look. Alpha is not compared.

```swift
someColor.distance(to: other, measure: ColorDistance = .oklab) -> Double
```

The default `.oklab` is the straight-line distance in OKLab. On that scale black to white is 1, and about 0.02 is a difference most people just see with the two side by side. The palette checks under [color vision](../helpers/accessibility.md#whether-a-palette-holds-apart) measure their `tolerance` on the same scale. `.ciede2000` is the CIE's 2000 color-difference formula, ΔE00. On its scale about 1 is just visible and 2.3 is a commonly quoted threshold, and print and textile tolerances are written in it. It corrects CIELAB's unevenness in the blues and among near-grays. The two scales differ, so a tolerance written for one does not carry to the other.

```swift
let ink = Color(hex: 0x2F3A4A), paper = Color(hex: 0x353F4C)
if ink.distance(to: paper) < 0.05 { stroke(.white) }       // too close to read: outline it
let delta = ink.distance(to: paper, measure: .ciede2000)
```

Sharma, Wu, and Dalal published 34 test pairs in 2005 for checking implementations. Ollin's CIEDE2000 matches all of them to the four decimals they are printed with.

**The palette entry nearest a color.** `nearest(to:)` returns the swatch that looks most like a color. `nearestIndex(to:)` returns its position instead, for when the entries stand for something, such as a plate to print with. Both are `nil` for an empty palette, a tie goes to the earlier entry, and both take the same `measure:`.

```swift
let sampled = Color(red: 0.8, green: 0.45, blue: 0.3)
fill(Palette.set2.nearest(to: sampled) ?? sampled)          // snap to the palette
let plate = Palette.set2.nearestIndex(to: sampled)          // which swatch it was
```

Snapping each cell of a picture this way posterizes it to the palette. [Dithering](#dithering) is the same snap with each pixel's error carried to its neighbors, which keeps the picture's tones.

**An image's histogram.** `histogram(of:bins:in:)` counts a picture's pixels by one channel. The channels are `.red`, `.green`, `.blue`, `.alpha`, and `.lightness`, which is OKLab's `l` and the default. The result is a `Histogram` with `counts` per bin, darkest first, and `total`, the number of pixels counted. `percentile(_:)` gives the value below which a share of the pixels lie. So `0.5` is the median, and the values at `0.02` and `0.98` are a picture's working black and white, with a few stray pixels ignored.

```swift
let photo = Image(width: 64, height: 64, color: Color(hex: 0x8E6B4F))
let h = photo.histogram(of: .lightness, bins: 64)
for (i, n) in h.counts.enumerated() {
    drawRect(Double(i) * 8, 400, 7, -Double(n) / Double(h.total) * 300)
}
let floor = h.percentile(0.02), ceiling = h.percentile(0.98)    // auto levels
```

Bin `i` of `n` holds the values from `i / n` up to `(i + 1) / n`, so 256 bins hold one 8-bit level each. The region is in pixels, and it covers the same pixels as `averageColor(in:)`. Every pixel inside it counts once, so `total` is the region's pixel count. Color is read with the alpha divided out, and a fully transparent pixel counts at 0. The count runs on the CPU, so a texture-backed image (a live feed or a layer) gives an empty histogram. Count once in `setup()`, or when the picture changes, rather than in every frame.

<a name="mixing"></a>

### Mixing

```swift
Color.mix(_ a: Color, _ b: Color, Double, in: ColorSpace = .oklab) -> Color
someColor.mixed(with: other, _ t: Double, in: ColorSpace = .oklab) -> Color
```

Both calls interpolate between two colors in a chosen space. The spaces are `.rgb`, `.hsb`, `.oklab`, `.oklch`, `.okhsl`, and `.paint`. `.oklab` is the default, and it is perceptually even. `.oklch` keeps each color's own hue while the mix moves through chroma. `t` clamps to `0...1`, and alpha interpolates linearly. In the polar spaces, hue takes the shortest way around the wheel. An achromatic endpoint (gray, black, or white) adopts the other color's hue, so a fade to white does not pass through unrelated hues. `.paint` is different from the others, because it models real pigment. The two colors mix as scattering pigments, so yellow and blue meet in green, and mixes darken the way paint does. The [Spectral color](spectrum.md) page describes it in full, including the `Spectrum` type behind it.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/02-Color/MixingSpaces-dark.jpg">
  <img src="../../Guide/Images/02-Color/MixingSpaces.jpg" alt="Three rows mixing the same blue and yellow: the RGB row passes through muddy olive, the HSB row detours through bright green, the OKLab row stays even" width="680">
</picture>

```swift
let warm = Color(hex: 0xFF5500)
let cool = Color(hex: 0x0066FF)
fill(Color.mix(warm, cool, sin(time) * 0.5 + 0.5))
fill(ink.mixed(with: .white, 0.3))        // the instance form, reading as a fade
```

The `Mixing` example draws the same two colors mixed in all six spaces, band by band. The OKLab family and the gamut mapping are credited under [Influences & attribution](../../attribution.md#color).

<a name="ramp"></a>

### `Ramp`

A `Ramp` is a gradient built from a list of colors, and you sample it with `color(at:)`. The colors are spread evenly, or placed with explicit stops. Interpolation runs through a chosen `ColorSpace`, OKLab by default, which blends evenly. `t` clamps to the ends. Two stops at the same position make a hard edge.

```swift
let heat = Ramp([.black, .red, Color(hex: 0xFFCC00), .white])
fill(heat.color(at: energy))

let sky = Ramp(stops: [(0, Color(hex: 0x0B1A40)),
                       (0.8, Color(hex: 0x3C6DD0)),
                       (1, Color(hex: 0xFFD9A0))], in: .oklch)
```

The stops are `Ramp.Stop` values with a `position` and a `color`. `Ramp(stops: [Ramp.Stop])` takes them directly, so you can build a ramp from stops you edit or generate. `reversed` runs a ramp the other way, with every stop at `1 - position`. For example, `heat.reversed` cools from white to black, the way a colormap's `_r` variant does.

<a name="gradient"></a>

### Gradient paint

A `Ramp` becomes paint through `Gradient`. `fill(_:)` and `stroke(_:)` take a gradient anywhere they take a color, on every shape. A gradient is a `Ramp` laid over the canvas by one of four geometries:

```swift
fill(.linear(from: Vector2(0, 0), to: Vector2(0, height), sky))   // start → end
fill(.radial(center: sun, radius: 260, [.white, .clear]))         // center → radius
fill(.conic(center: dial, startAngle: -.pi / 2, wheel))           // once around a point
stroke(.alongPath(heat))                                          // along the stroke
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/02-Color/GradientPaint-dark.jpg">
  <img src="../../Guide/Images/02-Color/GradientPaint.jpg" alt="Four panels: a rectangle with a vertical dusk gradient, a soft radial glow, a disk whose rainbow sweeps once around its center, and a ring stroked with a rainbow that runs along it" width="680">
</picture>

Each factory takes a `Ramp` or a plain `[Color]` list. A list is spread evenly and mixed in OKLab by default, and you pass `in:` for another space. Coordinates are in drawing space, so a gradient follows the transform stack together with the shapes it paints. One gradient laid across many shapes shades them as one. `t` clamps at the ends. Alpha is part of the ramp, so a radial gradient that fades to `.clear` makes a soft-edged glow.

`.conic` sweeps the ramp once around `center`, from `startAngle` (radians from three o'clock, turning clockwise, like every angle in Ollin; `-.pi / 2` starts at twelve) back around to it. It reads by position, so it paints a fill and a stroke alike, a shape of any kind, and a merged field, and the sweep does not care where the shape's own center is: a color wheel, a dial, and a pie chart are each one call. The sweep wraps where it started, so a ramp whose last color repeats its first hides that seam.

`.alongPath` follows the shape it paints. On `drawLine`, `drawBezier`, `drawPolyline`, and stroked `drawShape` contours, the ramp runs from start to end by arc length, and each contour runs its own `0...1`. On a region shape, including its fill, the ramp sweeps once around the shape's center. The sweep starts at 12 o'clock and turns clockwise, so a ring outline becomes a color wheel. A cyclic ramp, one whose end colors match, hides the seam where the sweep wraps.

`Paint` carries either kind as one value, for a variable that holds a color or a gradient:

```swift
let paint: Paint = beat > 0 ? .gradient(.radial(center: c, radius: r, heat)) : .color(.white)
fill(paint)
```

The analytic SDF shapes (circles, rects, stars, lines, …) evaluate a gradient per pixel, so the result is exact at any size. The tessellated shapes (`drawPolygon`, `drawShape`, elliptical arcs, and outline text) shade across their vertices instead. Gradient strokes subdivide automatically, so the ramp follows the path. A fill is sampled only at its outline points, so a radial gradient centered *inside* a large polygon does not show its bullseye there. Where that matters, use an SDF shape. Vector export maps linear and radial gradients to native SVG gradients. An along-path stroke exports as short solid runs, and an along-path fill falls back to the color at the ramp's midpoint. A conic paint falls back the same way, since neither SVG nor PDF has a sweep. On the tessellated path a sweeping paint has a seam, the ray where the ramp wraps, and a triangle that crosses it is cut there so the seam stays a line.

<a name="shader-paint"></a>

### A shader as paint

A [`Shader`](../shaders/shaders.md) is paint too. `fill(_:)` and `stroke(_:)` take one anywhere they take a color, and its `shade(uv, info)` then runs at every pixel the shape covers. That holds on every shape and every path: the analytic shapes, polygons and `Shape`s, strokes, curves, and outline text. The shape clips the picture, and coverage is applied after the shader returns, so a shader-painted edge is as sharp as a flat one. A shader that returns one color draws exactly what that color draws.

```swift
let rings = Shader("""
float4 shade(float2 uv, ShaderInfo info) {
    float band = 0.5 + 0.5 * cos(-info.distance / 12.0 * 6.2831853 - info.time);
    return float4(mix(float3(0.1, 0.15, 0.4), float3(1.0, 0.85, 0.5), band), 1.0);
}
""")
fill(rings)
drawCircle(300, 300, 140)                         // rings counting inward from the edge
stroke(rings)
drawPolyline([Vector2(80, 600), Vector2(500, 520)])   // the same rings across the stroke, from its centerline
```

<picture>
  <img src="../../Guide/Images/18-YourFirstShader/ShaderPaint.jpg" alt="Three shapes painted by shaders on a dark ground: a disk ringed in gold and blue from its edge inward, a turned five-pointed star striped diagonally in coral and cream in its own frame, and a thick teal ribbon whose light pulses along it and fades toward its edges" width="680">
</picture>

Inside `shade`, `uv` runs `0...1` across the shape's own bounding box, so `(0, 0)` is the box's top-left corner and `(1, 1)` its bottom-right. The box turns and scales with the shape under the transform stack, so a pattern drawn in `uv` is the shape's own, like a texture on it. The box is the outline's for a closed shape and its fill alike; an open stroke (a line, a polyline, a curve) has no interior, so its box is the stroke's footprint. `info` carries the layer fields a generator gets (`time`, `mouse`, `resolution`, `params`) and three more:

- `info.position` is the pixel in canvas points, the same for every shape, so a grain or a pattern read from it continues across neighboring shapes, and a rotated shape does not turn it.
- `info.distance` is the signed distance to the shape's edge, in the shape's own units: zero on the outline, negative inside, positive outside. On a stroke it runs across the stroke, zero on the centerline, negative toward the inside of a closed outline. It is exact on the analytic shapes and on a stroke away from its folds: a stroked path is expanded segment by segment, so on the inside of a bend tighter than half the width, where the ribbon folds over itself, each piece reads its own segment's distance. A polygon's or a `Shape`'s *fill* has no measured edge, so there it reads far inside everywhere.
- `info.along` is the fraction `0...1` along a stroke, by arc length from its start, the same `t` an `.alongPath` gradient reads; a fill reads 0.

Each shader compiles once, on first use, and its pipeline is cached by the source text, so a `Shader` rebuilt every frame with new `params` costs nothing. Shapes painted by the same shader draw as one run. A shape between them painted another way, or by another shader, opens a new run, so a frame that alternates many shaders over many shapes pays a draw call per change. A shape whose fill and stroke are two different shaders draws its fill and then its stroke over it, as it would with one.

A **mesh** under `fill(shader)` takes the shader as its surface. The picture is baked each frame into a square layer the mesh reads as its base texture at its uvs, which is what reaches every pass a texture reaches: the lit surface, a reflection, the path tracer, a USD export. A mesh with no uvs draws white, like a gradient fill, and says so once. Where a shader cannot run, the shape still draws. A vector export (SVG, PDF, embroidery) writes it in middle gray, since a document has no program per pixel. A web page export stops and names the call. Atlas text painted by a shader takes the outline path. A line drawn with 3D points and a mesh's feature edges take one color and draw white under a shader stroke. A shader that does not compile draws nothing and reports the error at your line, the way a layer shader does. `Paint.shader(_:)` carries one as a value beside `.color` and `.gradient`, and `Paint.shader` reads it back.

<a name="palette"></a>

### `Palette`

A `Palette` is a discrete set of colors carried as one value. `palette[i]` wraps in both directions, so any counter cycles through it. `color(at:)` divides `0...1` into equal bands, so a noise value picks a swatch. `ramp(in:)` turns the set into a smooth interpolating `Ramp`.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/02-Color/PaletteShelf-dark.jpg">
  <img src="../../Guide/Images/02-Color/PaletteShelf.jpg" alt="Five rows: the set2 palette swatches, a triadic harmony, a smooth five-color ramp, the viridis colormap, and the sunset cosine palette" width="680">
</picture>

```swift
let p = Palette(.red, Color(hex: 0x1B9E77), .white)
fill(p[frameCount / 30])                   // step through, wrapping
fill(p.color(at: noise(x * 0.01)))         // band-quantized
fill(p.ramp(in: .oklch).color(at: t))      // smooth
```

A palette is a set of colors in order, so it reads as one. `for color in palette`, `palette.enumerated()`, `map`, `first`, `isEmpty`, `reversed()` and `Array(palette)` all work without reaching for `colors`, and so does a seeded pick through the sketch's [`randomness`](../generators/random.md#randomness):

```swift
for (i, color) in palette.enumerated() {
    fill(color)
    drawRect(x + Double(i) * step, y, step, 40)
}
fill(palette.randomElement(using: &randomness) ?? .black)
```

The wrapping `palette[i]` goes further than that: it takes any integer, where iteration stays inside the set. It stays read-only for the same reason, since `palette[-1] = color` could as easily mean the last swatch as a mistake. Change the set through `colors`.

**Harmonies** build a palette from one base color. They are computed in OKLCH, so the companion colors keep the base's lightness and chroma:

```swift
Palette.complementary(of: base)                         // base + opposite
Palette.splitComplementary(of: base, spread: 1.0 / 12)  // base + the pair flanking its opposite
Palette.triadic(of: base)                               // thirds of the wheel
Palette.analogous(of: base, count: 3, spread: 1.0 / 12) // neighbors centered on base
```

**Built-in sets.** The eight ColorBrewer qualitative palettes ship as data. They are `.set1`, `.set2`, `.set3`, `.paired`, `.pastel1`, `.pastel2`, `.dark2`, and `.accent`. They are credited under [Influences & attribution](../../attribution.md#color).

The `Harmonies` example follows a drifting base color through all four builders. `Swatchbook` lays out the built-in sets.

<a name="shades"></a>

### Shades whose hue drifts

A ramp that only darkens and lightens one hue reads flat. Painters and pixel artists turn the hue as they go: the lights warm toward yellow and the shadows cool toward blue or purple. `shades(of:)` builds that ramp from one base color, as a continuous `Ramp` or as a `Palette` of swatches:

```swift
Ramp.shades(of: base, lightness: 0.2...0.95, hueDrift: 1.0 / 12) -> Ramp
Palette.shades(of: base, count: 5, lightness: 0.2...0.95, hueDrift: 1.0 / 12) -> Palette
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/02-Color/ShadesThatDrift-dark.jpg">
  <img src="../../Guide/Images/02-Color/ShadesThatDrift.jpg" alt="Five rows of six swatches each, built twice. On the left each row keeps one hue from near black to near white. On the right the same rows turn: the red warms to peach in the light and cools to crimson in the dark, the green lightens toward yellow and darkens toward teal, the blue rises to sky and sinks to indigo. The base color is ringed in every row" width="680">
</picture>

The walk is in OKLCH. Lightness runs evenly between the two ends of `lightness` and through the base, which sits at the place its own lightness gives it. The hue turns toward yellow as the shades lighten and away from yellow as they darken, by `hueDrift` turns at each end. The lights stop turning if they reach yellow first. Away from yellow is toward blue or purple for every hue except yellow itself, whose shadows turn toward orange. Chroma eases to half the base's at the two ends, since a shade that dark or light reads heavy or loud at full strength. A negative `hueDrift` reverses the drift, cooling the lights and warming the shadows, and `0` keeps one hue throughout.

```swift
let skin = Palette.shades(of: Color(hex: 0xC8875E), count: 6)
for (i, color) in skin.enumerated() {
    fill(color)
    drawRect(80 + Double(i) * 150, 400, 140, 140)
}
postProcess(.gradientMap(Ramp.shades(of: Color(hex: 0x2A7F8F))))   // the frame's tones, shaded
```

The palette always includes the base itself. The other swatches step evenly in lightness on either side of it. The idea is credited under [Influences & attribution](../../attribution.md#color).

<a name="palette-files"></a>

### Loading palettes from a file

Palettes you collect elsewhere load with one call. `loadPalettes` returns every palette in a file, in file order, and `loadPalette` returns the first one. A file that holds a single palette reads as a one-element array. So `loadPalettes` is the form to use when you do not know which kind of file you have.

```swift
let sets = try loadPalettes("1000.json")      // many
let one  = try? loadPalette("sunset.hex")     // the first, or nil when it will not read

// From the sketch's own bundle. `in:` has no default: a default would
// resolve to Ollin's bundle rather than yours.
let bundled = try? loadPalettes(resource: "palettes", withExtension: "csv", in: .module)
```

Five layouts are supported, and `.auto` picks between them by looking at the bytes:

| Format | Shape | Reads as |
|---|---|---|
| hex per line | `#69d2e7` on each line | one palette |
| CSV | `#69d2e7,#a7dbd8,#e0e4cc` per line | one palette per line |
| TSV | the same, tab separated | one palette per line |
| JSON | `[["#69d2e7", …], …]`, or a flat `["#69d2e7", …]`, or `[{"colors": […]}, …]` | many, or one |
| ASE | Adobe Swatch Exchange | one palette per swatch group |

The text formats share one parser, because they differ only in what separates the colors on a line. That has two consequences. First, a file whose every line holds exactly one color is read as a single palette, not as a stack of one-color palettes. That is what makes hex-per-line work with no format argument. Second, a line that yields no colors is skipped, so a CSV header row or a line of prose drops out on its own.

A hex token is still read when extra punctuation surrounds it. That covers surrounding quotes from a CSV cell, an `0x` prefix, and the `#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA` digit forms that `Color(hex:)` accepts.

Name a `PaletteFormat` when the automatic guess is wrong:

```swift
(try? loadPalettes("swatches.txt", format: .hexLines))   // one palette, even with commas on a line
(try? loadPalettes("library.ase", format: .ase))        // Adobe's own swatch file
(try? loadPalettes("grid.csv", format: .csv))
```

**ASE** files map each swatch group onto a palette, in document order. Colors outside any group collect into a palette of their own. RGB, Gray, CMYK, and LAB swatches all decode. CMYK goes through the plain conversion. LAB goes through CIELAB on the D50 white point, which is the white point these files are written against. A truncated or malformed file yields an empty array instead of trapping, as every loader on this page does. A swatch whose numbers are not finite is skipped, since no hue can be taken of it.

**Where to find palettes.** Ollin ships the loader, and it bundles only palette data whose license permits it, which means the ColorBrewer sets above. The bitmap-font loader works the same way: it ships beside one permissively licensed default rather than a library of fonts. A good source of ready-made palettes is [nice-color-palettes](https://github.com/Experience-Monks/nice-color-palettes). Its `100.json` and `1000.json` files have exactly the JSON array-of-arrays shape in the table above. So you can `curl` one into your sketch folder, and `loadPalettes` reads it as is. Note that its palettes are scraped from COLOURlovers, whose default license is CC BY-NC-SA. That is why Ollin does not redistribute them. The MIT license on that repository covers its code, not the palette data. Fetching the file for your own work is your decision to make, but bundling it into an MIT framework is not.

That set is worth knowing for a second reason. So many people have used it that its first palette (`#69d2e7`, `#a7dbd8`, `#e0e4cc`, `#f38630`, `#fa6900`) appears in a large amount of generative art. If you want your work to have its own look, extract a palette from an image you chose, or curate your own.

The `PaletteFile` example loads a CSV of six palettes and a hex-per-line file, side by side.

<a name="palette-extraction"></a>

### Extracting a palette from an image

The colors in a picture are usually a better palette than a list someone else curated.

```swift
let photo = try loadImage("beach.jpg")
let p = Palette(extractedFrom: photo, count: 5)
fill(p[0])                                     // the color the photo is mostly made of
```

The colors come back **most-used first**, so `p[0]` is the color you would name if asked what color the picture is. Clustering runs in OKLab rather than sRGB, because distance in sRGB does not match distance as the eye sees it. Clusters formed in sRGB split greens that nobody can tell apart, and they merge blues that everybody can tell apart.

Extraction is **fully deterministic**. The same image, `count`, and `seed` always give the same palette. So an extracted palette is safe to snapshot and to carry in an export's recipe. When a result looks wrong, pass a different `seed` to move the clustering out of a local minimum:

```swift
Palette(extractedFrom: photo, count: 6, seed: 3)
extractPalette(from: photo, count: 6)          // the same thing, as a bare call
```

Three behaviors are worth knowing. Transparent pixels are ignored. An image with fewer distinct colors than `count` yields only the colors it has, and it does not invent filler. A texture-backed image, such as a video frame or a Syphon feed, has no readable pixels, so it yields an empty palette. Call `snapshot()` on the feed first.

Extraction is work for `setup()`, not for every frame. A large image is sampled on an even grid instead of being read whole, so the cost is bounded. Clustering still runs Lloyd's algorithm over the samples. So extract in `setup()` and keep the result.

The `PaletteFromImage` example paints an image from five known colors and then recovers them from the pixels alone.

<a name="dithering"></a>

### Dithering an image to a palette

Extraction pulls the colors out of a picture. Dithering rebuilds the picture from those colors.

```swift
let photo = try loadImage("beach.jpg")
let p = Palette(extractedFrom: photo, count: 6)
let poster = photo.dithered(.floydSteinberg, to: p)

drawImage(poster, in: bounds)
```

Snapping each pixel to its nearest palette color, and nothing more, gives flat bands where the picture was smooth. Dithering trades those bands for texture. For each tone it scatters the two palette colors on either side of that tone. At normal viewing distance the eye blurs them together and reads the tone that was there before. The result uses fewer colors and shows the same picture.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="../../Guide/Images/09-Pictures/Dithering-dark.jpg">
  <img src="../../Guide/Images/09-Pictures/Dithering.jpg" alt="Three panels of the same smooth color gradient reduced to five colors: the first showing wide flat bands, the second a regular crosshatch grain, the third an organic scattered grain, both of the latter reading as a smooth gradient from a distance" width="680">
</picture>

The methods come in two families.

**Threshold maps** decide each pixel by its position alone, using a repeating tile. Each pixel dithers between the two palette colors whose mix best reproduces it. Ollin considers every pair and scores it perceptually, and it prefers quiet, low-contrast pairs. So a gray field near a saturated palette color mixes the colors that average to gray, instead of tinting toward the saturated neighbor.

- `.ordered(size:)` uses a Bayer matrix that is `size` cells across, where `size` is a power of two in `2...16`. It produces the visible crosshatch of retro graphics. A larger size is finer and shows less pattern.
- `.blueNoise` uses a 64×64 tile with no structure in it, which gives an even grain with no visible pattern. The tile is generated on first use, and it repeats without a seam.

**Error diffusion** quantizes a pixel and pushes the leftover error onto the neighbors it has not reached yet, so each error is corrected nearby. It cannot run on the GPU, because each pixel depends on the one before it. It gives the organic, scattered look.

| Method | Character |
|---|---|
| `.floydSteinberg` | The classic. Four taps, sharp and detailed. |
| `.jarvisJudiceNinke` | Twelve taps over three rows. Smoother, and it softens fine detail. |
| `.stucki` | The Jarvis layout with retuned weights. Cleaner and slightly sharper. |
| `.atkinson` | Passes on only three quarters of the error, so highlights and shadows blow out to clean white and black. |
| `.burkes` | Stucki without its bottom row. Fast, and close to Stucki in look. |
| `.sierra`, `.twoRowSierra`, `.sierraLite` | Three rows, two rows, and the cheapest diffusion that is still useful. |

`.none` skips the dithering and snaps each pixel to the nearest color. Use it to show what dithering does.

A second form posterizes instead. It quantizes each channel to evenly spaced steps rather than to a palette:

```swift
photo.dithered(.atkinson, levels: 2)           // 8 colors: black, white, the primaries
photo.dithered(.ordered(size: 8), levels: 4)   // 64
```

There are two parameters, and each one is ignored by the family it does not apply to. `amount` (`0...1`) scales the grain of the threshold maps. At `1` they hold the image's tone exactly, and at `0` they band like `.none`. `serpentine` (on by default) reverses every other row of an error-diffusion scan. That breaks up the directional streaks that a straight left-to-right pass leaves behind.

Dithering is per-pixel CPU work, like extraction, so do it in `setup()` and keep the result. Alpha passes through unchanged. A fully transparent pixel passes no error to its neighbors, so the invisible background of a cutout never bleeds into the subject's edge. A texture-backed image has no readable pixels and comes back unchanged, so call `snapshot()` on it first. That covers a video frame, a Syphon feed, and an effects layer. Dithering is fully deterministic. The same image, method, and palette always give the same pixels, so a dithered result is safe to snapshot and to export.

For a cheap real-time dither over a whole layer, rather than an exact quantization of an image, use the `.dither` and `.ditherDuo` [filters](effects.md) instead. They run on the GPU, and they approximate the result rather than quantizing exactly.

The `Dithering` example reduces one painted gradient to four extracted colors six ways, side by side.

<a name="cosinepalette"></a>

### `CosinePalette`

A `CosinePalette` is a cyclic color gradient from Inigo Quilez's cosine formula, where each channel is `a + b · cos(2π · (c · t + d))`. You build one from four `(r, g, b)` coefficient triples (center, amplitude, frequency, phase), then sample it.

```swift
let p = CosinePalette(a: (0.5, 0.5, 0.5), b: (0.5, 0.5, 0.5),
                      c: (1.0, 1.0, 1.0), d: (0.0, 0.10, 0.20))
let c = p.color(at: t)        // t cycles; 0 and 1 meet for the defaults
```

Seven presets are built in. They are Quilez's example palettes, each named for how it looks:

`.rainbow`, `.dusk`, `.blush`, `.meadow`, `.sunset`, `.neon`, `.melon`.

```swift
fill(CosinePalette.sunset.color(at: time * 0.1))   // drift through the ramp over time
```

The `Palettes` example sweeps all seven. The formula is credited under [Influences & attribution](../../attribution.md#color).

<a name="colormap"></a>

### `Colormap`

A `Colormap` is a smooth ramp that maps a value in `0...1` to a color. Seven of the eight are perceptually even, so perceived brightness climbs steadily from one end to the other. `turbo` is a rainbow instead, brightest in the middle. Colormaps are the standard choice for turning a number into readable color, whether the number is a height, a density, or a field value. `color(at:)` linearly interpolates the 256-entry table and clamps `t`.

```swift
let t = noise(x * 0.01, y * 0.01)            // 0...1
fill(Colormap.magma.color(at: t))
fill(Colormap.viridis.color(cycling: time / 8))   // wrap instead of clamping
```

`color(cycling:)` treats the map as a cycle. `t` wraps by whole laps instead of clamping, so a growing value, such as an angle or the clock, runs through the map forever. A [`Palette`](#palette)'s integer subscript already wraps this way, though its `color(at:)` clamps. Most colormaps end nowhere near where they begin, so each lap still shows a seam at the wrap. For a seamless ring, use [`CosinePalette`](#cosinepalette).

There are eight cases: `viridis`, `magma`, `inferno`, `plasma`, `cividis`, `turbo`, `rocket`, and `mako`. The `Colormaps` example shows all eight. The data origins (matplotlib, Google, seaborn) are credited under [Influences & attribution](../../attribution.md#color).
