# colors

> Parse CSS colors, convert between RGB, hex and HSL, and lighten, darken and mix them.

This is the Markdown version of https://osl-lang.org/packages/colors. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:colors"
```

`std:colors` reads colors in many shapes (`#ff8800`, `rgb(255, 136, 0)`, `tomato`, `hsl(32, 100%, 50%)`), changes them the way people think about them (lighter, darker, a blend of two) and writes them back out as hex text.

A color here is one of three things, and the functions accept any of them where they take a `color`:

- a CSS color string such as `"#336699"` or `"tomato"`,
- an object with `r`, `g`, `b` and `a` from 0 to 255, which is what `colors.parse` returns,
- an `img.Color` made with `img.rgb`.

## Parsing

`colors.parse(text)` returns a [result](/packages/result.md) holding `{r, g, b, a}`:

```osl
import "std:colors"

auto orange = colors.parse("#ff8800").unwrap()
log orange
log orange.r orange.g orange.b orange.a
```

Output:

```text
{"a":255,"b":0,"g":136,"r":255}
255 136 0 255
```

Alpha `a` is opacity: 255 is solid. It reads the common CSS forms:

```osl
import "std:colors"

string[] inputs = [
  "#f80", "#ff8800", "#ff880080", "rgb(10, 20, 30)", "hsl(120, 100%, 50%)", "red"
]
for text in inputs (
  auto c = colors.parse(text).unwrap()
  log text colors.stringify(c)
)
```

Output:

```text
#f80 #ff8800
#ff8800 #ff8800
#ff880080 #ff880080
rgb(10, 20, 30) #0a141e
hsl(120, 100%, 50%) #00ff00
red #ff0000
```

Text that is not a color is a `ParseError`:

```osl
import "std:colors"

auto bad = colors.parse("not a color")
log bad.isErr()
log bad.unwrapErr().kind
```

Output:

```text
true
ParseError
```

## Writing colors out

`colors.stringify` gives `#rrggbb`, or `#rrggbbaa` when the color isn't fully opaque. It accepts any of the color forms:

```osl
import "std:colors"

log colors.stringify({r: 255, g: 99, b: 71, a: 255})
log colors.stringify({r: 1, g: 2, b: 3, a: 128})
log colors.stringify("tomato")
```

Output:

```text
#ff6347
#01020380
#ff6347
```

## Lighter, darker, mixed

`lighten` and `darken` move the lightness by an amount from 0 to 1. `mix(a, b, amount)` blends two colors, where 0 is all `a` and 1 is all `b`. All three return hex text:

```osl
import "std:colors"

string base = "#336699"
log colors.lighten(base, 0.2)
log colors.darken(base, 0.2)
log colors.mix("#ff0000", "#0000ff", 0.5)
log colors.mix("black", "white", 0.25)
```

Output:

```text
#6699cc
#1a334d
#800080
#404040
```

These work on lightness in HSL, which is how people judge "lighter", rather than adding to the RGB channels.

## HSL

Hue, saturation and lightness make some changes easy that are awkward in RGB, such as rotating the hue to find a related color. `toHSL` gives `{h, s, l, a}` with hue in degrees (0 to 360) and the others from 0 to 1. `fromHSL` goes back:

```osl
import "std:colors"

auto hsl = colors.toHSL("#ff8800")
log hsl
log hsl.h

auto shifted = colors.fromHSL({h: hsl.h + 120, s: hsl.s, l: hsl.l})
log shifted
```

Output:

```text
{"a":255,"h":32,"l":0.5,"s":1}
32
#00ff88
```

Hue 152 is a green, 120 degrees around the wheel from orange.

## Example: a palette from one color

Starting from a brand color, build a scale for a UI: three tints, the base, and two shades.

```osl
import "std:colors"

string brand = "#336699"
string[] steps = []
for amount in [0.3, 0.15] (
  steps.append(colors.lighten(brand, amount))
)
steps.append(brand)
for amount in [0.1, 0.2] (
  steps.append(colors.darken(brand, amount))
)
log steps
```

Output:

```text
["#8cb3d9","#538cc6","#336699","#264d73","#1a334d"]
```

## Colors in images

The three color shapes convert freely, so `img.rgb(255, 0, 0)` can go into `colors.lighten`, and a hex string or the object from `colors.parse` can go into `image.fill`. Use whichever is handy and let `colors.stringify` normalise to text.

**Quiz.** `colors.parse("not a color")` is called. What happens?

- [ ] It throws a `ParseError`.
- [x] It returns an error result, so `isErr()` is `true`.
- [ ] It returns black.

Answer: `parse` returns a [result](/packages/result.md), so bad input is a value to check, not a crash.

### Exercise: The opposite color

Finish `complement` so it returns the color opposite on the wheel: the same saturation and lightness with the hue turned 180 degrees (wrap past 360 with `% 360`).

Starting code:

```osl
import "std:colors"

def complement(string color) string -> color

log complement("#ff8800")
log complement("#ff0000")
```

It should print:

```text
#0077ff
#00ffff
```

Hint 1: `colors.toHSL(color)` gives `{h, s, l, a}`.

Hint 2: Build the answer with `colors.fromHSL({h: ..., s: hsl.s, l: hsl.l})`.

A solution:

```osl
import "std:colors"

def complement(string color) string (
  auto hsl = colors.toHSL(color)
  return colors.fromHSL({h: (hsl.h + 180) % 360, s: hsl.s, l: hsl.l})
)

log complement("#ff8800")
log complement("#ff0000")
```

## API reference

## API reference

Every member of `std:colors`, generated from the source of OSL.

### Functions

#### `colors.darken(any color, number amount) string`

darken lowers color's HSL lightness by amount (0 to 1, where 1 is the
whole range, so darken(c, 1) is black). Returns "#rrggbb" or
"#rrggbbaa". Throws ValueError for an invalid color or amount.

#### `colors.fromHSL(object hsl) string`

fromHSL converts {h, s, l, a} (as toHSL returns it; a is optional and
defaults to 255) to a "#rrggbb" or "#rrggbbaa" color. The hue wraps
around 360 degrees. Throws ValueError for a missing key, saturation or
lightness outside 0 to 1, or alpha outside 0-255.

#### `colors.gray(any v) object`

**Deprecated.** Deprecated: use {r: v, g: v, b: v} instead.

gray builds a grayscale {Y} from a channel that wraps modulo 256.

#### `colors.hex(any hex) object`

**Deprecated.** Deprecated: use colors.parse(text) instead, which reports invalid input.

hex parses "#rrggbb" or "#rrggbbaa" into {R, G, B, A}, giving opaque black
for anything else.

#### `colors.lighten(any color, number amount) string`

lighten raises color's HSL lightness by amount (0 to 1, where 1 is the
whole range, so lighten(c, 1) is white). Returns "#rrggbb" or
"#rrggbbaa". Throws ValueError for an invalid color or amount.

#### `colors.mix(any a, any b, number amount) string`

mix blends two colors channel by channel, alpha included: amount 0 gives
a, 1 gives b and 0.5 the midpoint. Returns "#rrggbb" or "#rrggbbaa".
Throws ValueError for an invalid color or an amount outside 0 to 1.

#### `colors.nrgba(any r, any g, any b, any a) object`

**Deprecated.** Deprecated: use {r: r, g: g, b: b, a: a} instead.

nrgba builds {R, G, B, A} from four channels, which wrap modulo 256.

#### `colors.parse(string text) result<{a: int, b: int, g: int, r: int}, error>`

parse reads a CSS color and returns it as {r, g, b, a} with 0-255
channels, which every OSL color argument accepts. It understands
"#rgb", "#rgba", "#rrggbb", "#rrggbbaa", the CSS color names (such as
"red" and "rebeccapurple"), "transparent", rgb()/rgba() and
hsl()/hsla() with commas or spaces, percentages and an optional
"/ alpha", in any case. Fails with ParseError for anything else.

#### `colors.rgb(any r, any g, any b) object`

**Deprecated.** Deprecated: use {r: r, g: g, b: b} or colors.stringify instead.

rgb builds an opaque {R, G, B, A} from three channels, which wrap modulo
256.

#### `colors.rgba(any r, any g, any b, any a) object`

**Deprecated.** Deprecated: use {r: r, g: g, b: b, a: a} or colors.stringify instead.

rgba builds {R, G, B, A} from four channels, which wrap modulo 256.

#### `colors.stringify(any color) string`

stringify returns color as "#rrggbb", or "#rrggbbaa" when it isn't fully
opaque. color is a CSS color string or {r, g, b, a}. Throws ValueError
for an invalid color.

#### `colors.toHSL(any color) {a: int, h: number, l: number, s: number}`

toHSL converts color to {h, s, l, a}: hue in degrees from 0 to 360,
saturation and lightness from 0 to 1, and alpha 0-255. Throws ValueError
for an invalid color.
