Skip to content

Commit

Permalink
Add new random color method (#208)
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser authored Jul 16, 2022
1 parent 574daa5 commit c7bc20d
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 0 deletions.
29 changes: 29 additions & 0 deletions coloraide/color.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Colors."""
import abc
import functools
import random
from . import distance
from . import convert
from . import gamut
Expand Down Expand Up @@ -475,6 +476,34 @@ def deregister(cls, plugin: Union[str, Sequence[str]], silent: bool = False) ->
if reset_convert_cache:
cls._get_convert_chain.cache_clear()

@classmethod
def random(cls, space: str, *, limits: Optional[Sequence[Optional[Sequence[float]]]] = None) -> 'Color':
"""Get a random color."""

# Get the color space and number of channels
cs = cls.CS_MAP[space]
num_chan = len(cs.CHANNELS)

# Initialize constraints if none were provided
if limits is None:
limits = []

# Acquire the minimum and maximum for the channel and get a random value value between
length = len(limits)
coords = []
for i in range(num_chan):
chan = limits[i] if i < length else None # type: Any
if chan is None:
chan = cs.get_channel(i)
a, b = chan.low, chan.high
else:
a, b = chan

coords.append(random.uniform(a, b))

# Create the color
return cls(space, coords)

def to_dict(self) -> Mapping[str, Any]:
"""Return color as a data object."""

Expand Down
1 change: 1 addition & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- **NEW**: Added new color spaces: ACES 2065-1, ACEScg, ACEScc, and ACEScct.
- **NEW**: Contrast is now exposed as a plugin to allow for future expansion of approaches. While there is currently
only one approach, methods can be selected via the `method` attribute.
- **NEW**: Add new `random` method for generating a random color for a given color space.

## 1.0b1

Expand Down
29 changes: 29 additions & 0 deletions docs/src/markdown/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,35 @@ Return
:
Returns a [`Color`](#color) object.

## `Color.random` {#random}

```py3
@classmethod
def random(
cls,
space,
*,
limits=None
):
```

Description
:
Generate a random color in the provided `space`. The color space's channel range will be used as a limit for the
channel. For color spaces with no clearly defined gamut, these values can be arbitrary. In such cases, it may be
advisable to fit the returned color space to a displayable gamut.

Parameters
:
Parameters | Defaults | Description
---------- | -------------| -----------
`space` | | The color space name in which to generate a random color in.
`limits` | `#!py3 None` | An optional list of constraints for various color channels. Each entry should either be a sequence contain a minimum and maximum value, or should be `#!py3 None`. `#!py3 None` values will be ignored and the color space's specified channel range will be used instead. Any missing entries will be treated as `#!py3 None`.

Return
:
Returns a [`Color`](#color) object.

## `Color.clone` {#clone}

```py3
Expand Down
26 changes: 26 additions & 0 deletions docs/src/markdown/color.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,32 @@ except ValueError:
Color("hsl(130 30% 75%)", filters=["hsl"])
```

## Random

If you'd like to generate a random color, simply call `Color.random` with a given color space and one will be generated.

```playground
[Color.random('srgb') for _ in range(10)]
```

Ranges are based on the color space's defined channel range. For color spaces with defined gamuts, the values will be
confined to appropriate ranges. For color space's without defined gamuts, the ranges may be quite arbitrary. For color
spaces with no hard, defined gamut, it is recommend to fit the colors to whatever gamut you'd like, or simply use a
target space with a clear defined gamut.

```playground
Color.random('lab').fit('srgb')
```

Lastly, if you'd like to further constrain the limits, you can provide a list of constraints. A constraint should be
a sequence of two values specifying the minimum and maximum for the channel. If `#!py None` is provided, that constraint
will be ignored. If the list doesn't have enough values, those missing indexes will be ignored. If the list has too many
values, those extra values will be ignored.

```playground
Color.random('srgb', limits=[(0.25, 0.75)] * 3)
```

## Cloning

The `clone` method is an easy way to duplicate the current color object.
Expand Down
26 changes: 26 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,29 @@ def test_parse_float(self):
self.assertColorEqual(Color("color(srgb 3.2e-2 0.1e+1 0.1e1 / 0.5e-)"), Color("color(srgb 0.032 1 1 / 0.5)"))
self.assertColorEqual(Color("color(srgb +3.2e-2 +0.1e+1 +0.1e1 / 0.5e+)"), Color("color(srgb 0.032 1 1 / 0.5)"))
self.assertColorEqual(Color("color(srgb 0.032e 1e 1e / 0.5e)"), Color("color(srgb 0.032 1 1 / 0.5)"))

def test_random_space(self):
"""Test that random colors are generated in the specified space."""

c = Color.random('srgb')
self.assertEqual(c.space(), 'srgb')

c = Color.random('display-p3')
self.assertEqual(c.space(), 'display-p3')

def test_random_range(self):
"""Test that random colors are generated within the space's range."""

for _ in range(10):
for c in Color.random('srgb'):
self.assertTrue(0 <= c <= 1)

def test_random_limits(self):
"""Test random limits."""

for _ in range(10):
for i, c in enumerate(Color.random('srgb', limits=[None, (0, 0.5)])):
if i == 1:
self.assertTrue(0 <= c <= 0.5)
else:
self.assertTrue(0 <= c <= 1)

0 comments on commit c7bc20d

Please sign in to comment.