Skip to content

Commit

Permalink
Merge pull request #25 from facelessuser/feature/overlay-cyl
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser authored Mar 12, 2021
2 parents 2e6709a + 831d016 commit 715bff8
Show file tree
Hide file tree
Showing 14 changed files with 54 additions and 31 deletions.
2 changes: 1 addition & 1 deletion coloraide/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,5 @@ def parse_version(ver):
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(0, 1, 0, "alpha", 7)
__version_info__ = Version(0, 1, 0, "alpha", 8)
__version__ = __version_info__._get_canonical()
4 changes: 2 additions & 2 deletions coloraide/colors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ def __eq__(self, other):

return (
other.space() == self.space() and
other.coords() == self.coords() and
other.alpha == self.alpha
util.cmp_coords(other.coords(), self.coords()) and
util.cmp_coords(other.alpha, self.alpha)
)

def is_nan(self, name):
Expand Down
33 changes: 17 additions & 16 deletions coloraide/colors/_interpolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@
from . _range import Angle


def overlay(c1, c2, a1, a2, a0):
def overlay(c1, c2, a1, a2, a0, angle=False):
"""Overlay one color channel over the other."""

if util.is_nan(c1) and util.is_nan(c2):
return 0.0
elif util.is_nan(c1):
return c2 * a2
return c2 if angle else c2 * a2
elif util.is_nan(c2):
return c1 * a1
return c1 if angle else c1 * a1

c0 = c1 * a1 + c2 * a2 * (1 - a1)
return c0 / a0 if a0 else c0
if angle:
return c1 + (c2 - c1) * (1 - a1)
else:
c0 = c1 * a1 + c2 * a2 * (1 - a1)
return c0 / a0 if a0 else c0


def interpolate(p, coords1, coords2, create, progress, outspace, premultiplied):
Expand Down Expand Up @@ -188,28 +191,26 @@ def overlay(self, background, *, space=None, in_place=False):
if this is None:
raise ValueError('Invalid colorspace value: {}'.format(space))

# Some spaces, like those that are cylindrical, will not work well,
# so a space can specify a rectangular space that is better suited.
if this.ALPHA_COMPOSITE is not None:
this = this.convert(this.ALPHA_COMPOSITE, fit=True)
background = background.convert(background.ALPHA_COMPOSITE, fit=True)
if this.space() != background.space(): # pragma: no cover
# Catch the rare event that two spaces request incompatible spaces (maybe some weird
# derived class instance).
raise ValueError('Cannot overlay space {} onto space {}'.format(this.space(), background.space()))

# Get the coordinates and indexes of valid hues
prepare_coords(this)
prepare_coords(background)

# Adjust hues if we have two valid hues
if isinstance(this, Cylindrical):
adjust_hues(this, background, util.DEF_HUE_ADJ)

coords1 = this.coords()
coords2 = background.coords()
a1 = this.alpha
a2 = background.alpha
a0 = a1 + a2 * (1.0 - a1)
gamut = this._range
coords = []
# Avoid multiplying angles and don't mix them the same as non-angles
for i, value in enumerate(coords1):
coords.append(overlay(coords1[i], coords2[i], a1, a2, a0))
g = gamut[i][0]
is_angle = isinstance(g, Angle)
coords.append(overlay(coords1[i], coords2[i], a1, a2, a0, angle=is_angle))
this._coords = coords
this.alpha = a0
else:
Expand Down
2 changes: 0 additions & 2 deletions coloraide/colors/_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ class Space(contrast.Contrast, interpolate.Interpolate, distance.Distance, gamut
GAMUT = None
# White point
WHITE = convert.WHITES["D50"]
# When doing alpha composition, select an alternative space to perform the compositing in.
ALPHA_COMPOSITE = None

def __init__(self, color=None):
"""Initialize."""
Expand Down
1 change: 0 additions & 1 deletion coloraide/colors/hsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class HSL(Cylindrical, Space):
DEF_VALUE = "color(hsl 0 0 0 / 1)"
CHANNEL_NAMES = frozenset(["hue", "saturation", "lightness", "alpha"])
DEFAULT_MATCH = re.compile(RE_DEFAULT_MATCH.format(color_space=SPACE))
ALPHA_COMPOSITE = "srgb"
WHITE = convert.WHITES["D65"]

_range = (
Expand Down
1 change: 0 additions & 1 deletion coloraide/colors/hsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ class HSV(Cylindrical, Space):
CHANNEL_NAMES = frozenset(["hue", "saturation", "value", "alpha"])
DEFAULT_MATCH = re.compile(RE_DEFAULT_MATCH.format(color_space=SPACE))
GAMUT = "hsl"
ALPHA_COMPOSITE = "srgb"
WHITE = convert.WHITES["D65"]

_range = (
Expand Down
1 change: 0 additions & 1 deletion coloraide/colors/hwb.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class HWB(Cylindrical, Space):
CHANNEL_NAMES = frozenset(["hue", "blackness", "whiteness", "alpha"])
DEFAULT_MATCH = re.compile(RE_DEFAULT_MATCH.format(color_space=SPACE))
GAMUT = "hsl"
ALPHA_COMPOSITE = "srgb"
WHITE = convert.WHITES["D65"]

_range = (
Expand Down
1 change: 0 additions & 1 deletion coloraide/colors/lch.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ class LCH(Cylindrical, Space):
DEF_VALUE = "color(lch 0 0 0 / 1)"
CHANNEL_NAMES = frozenset(["lightness", "chroma", "hue", "alpha"])
DEFAULT_MATCH = re.compile(RE_DEFAULT_MATCH.format(color_space=SPACE))
ALPHA_COMPOSITE = "lab"
WHITE = convert.WHITES["D50"]

_range = (
Expand Down
9 changes: 9 additions & 0 deletions coloraide/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def no_nan(value):
return [(0.0 if is_nan(x) else x) for x in value]


def cmp_coords(c1, c2):
"""Compare coordinates."""

if is_number(c1):
return (math.isnan(c1) and math.isnan(c2)) or c1 == c2
else:
return all(map(lambda a, b: (math.isnan(a) and math.isnan(b)) or a == b, c1, c2))


def dot(a, b):
"""Get dot product of simple numbers, vectors, and 2D matrices and/or numbers."""

Expand Down
7 changes: 7 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.1.0a8

- **NEW**: Remove workaround to force cylindrical colors to overlay in non-cylindrical spaces. Allow colors to be
overlaid in any color space. Original issues related to allowing cylindrical spaces as been fixed. Overlaying in
cylindrical spaces may not make sense, but it is no longer prohibited.
- **FIX**: Ensure color comparison will yield true if two channels have `NaN`.

## 0.1.0a7

- **FIX**: Fix issue with translation of an input that is compressed hex with a specified alpha.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/markdown/color.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ or color names that are part of color variables (`#!css var(--color-red)`).
import re
from coloraide import Color
RE_COLOR_START = re.compile(r"(?i)(?:\b(?<![-#&])(?:color|hsla?|lch|lab|hwb|rgba?)\(|\b(?<![-#&])[\w]{3,}(?!\()\b|(?<![&])#)")
RE_COLOR_START = re.compile(r"(?i)(?:\b(?<![-#&$])(?:color|hsla?|lch|lab|hwb|rgba?)\(|\b(?<![-#&$])[\w]{3,}(?![(-])\b|(?<![&])#)")
text = """Red and yellow are colors. So are #000088 and lch(75% 50 50)."""
Expand Down
6 changes: 2 additions & 4 deletions docs/src/markdown/interpolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,8 @@ The `overlay` method allows a transparent color to be overlaid on top of another
two. To perform an overlay, a background color must be provided to the color along with an optional color space. If a
color is to be overlaid within a smaller color space, the colors will be mapped to the smaller space.

!!! Note "Cylindrical Spaces"
Certain color spaces, like cylindrical spaces (HSV, HSL, HWB, and LCH), will not be overlaid in their own space.
This is because these spaces do not work well with the overlay algorithm. Instead, such spaces will be mapped to
more suitable spaces; such as, HSL, HSV, and HWB to sRGB and LCH to LAB.
!!! tip "Cylindrical Spaces"
It is generally recommended to overlay in non-cylindrical spaces, but there is no limitation to do so.

In the example below, we take the `#!color rgb(100% 0% 0% / 0.5)` and overlay it on the color `#!color black`. This
yields the color: `#!color rgb(127.5 0 0)`.
Expand Down
14 changes: 14 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,17 @@ def test_function_set(self):
c1 = Color('orange')
c1.set("red", lambda x: x * 0.3)
self.assertEqual(c1.get("red"), 0.3)

def test_overlay(self):
"""Test overlay logic."""

c1 = Color('blue').set('alpha', 0.5)
c2 = Color('yellow')
self.assertEqual(c1.overlay(c2), Color('color(srgb 0.5 0.5 0.5)'))

def test_overlay_cyl(self):
"""Test overlay logic."""

c1 = Color('blue').set('alpha', 0.5)
c2 = Color('yellow')
self.assertEqual(c1.overlay(c2, space="hsl"), Color('color(srgb 0 1 0.5)'))
2 changes: 1 addition & 1 deletion tools/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
AST_BLOCKS = (ast.If, ast.For, ast.While, ast.Try, ast.With, ast.FunctionDef, ast.ClassDef)

RE_COLOR_START = re.compile(
r"(?i)(?:\b(?<![-#&])(?:color|hsla?|lch|lab|hwb|rgba?)\(|\b(?<![-#&])[\w]{3,}(?!\()\b|(?<![&])#)"
r"(?i)(?:\b(?<![-#&$])(?:color|hsla?|lch|lab|hwb|rgba?)\(|\b(?<![-#&$])[\w]{3,}(?![(-])\b|(?<![&])#)"
)


Expand Down

0 comments on commit 715bff8

Please sign in to comment.