Skip to content

Commit

Permalink
Use CSS algorithm for HWB instead of HSV approach
Browse files Browse the repository at this point in the history
HSV approach is correct, but CSS employs an approach that uses HSL
and sRGB that is reversible in the negative lightness direction all
while resolving the RGB values to the exact same values.

While negative lightness colors are not useful, we always prefer the
approach with better overall round tripping for predictable conversions.
  • Loading branch information
facelessuser committed Aug 28, 2023
1 parent 90036f5 commit 3f81a3d
Show file tree
Hide file tree
Showing 4 changed files with 22 additions and 30 deletions.
43 changes: 16 additions & 27 deletions coloraide/spaces/hwb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
"""HWB class."""
from __future__ import annotations
from ...spaces import Space, HWBish
from ..hsl import srgb_to_hsl, hsl_to_srgb
from ...cat import WHITES
from ... import util
from ...channels import Channel, FLG_ANGLE, FLG_OPT_PERCENT
from ...types import Vector


def hwb_to_hsv(hwb: Vector) -> Vector:
"""HWB to HSV."""
def srgb_to_hwb(srgb: Vector) -> Vector:
"""HWB to sRGB."""

h, w, b = hwb

wb = w + b
if wb >= 1:
return [h, 0.0, w / wb]

v = 1 - b
s = 0 if v == 0 else 1 - w / v
return [util.constrain_hue(h), s, v]
return [srgb_to_hsl(srgb)[0], min(srgb), 1 - max(srgb)]


def hsv_to_hwb(hsv: Vector) -> Vector:
"""HSV to HWB."""
def hwb_to_srgb(hwb: Vector) -> Vector:
"""HWB to sRGB."""

h, s, v = hsv
return [util.constrain_hue(h), v * (1 - s), 1 - v]
h, w, b = hwb
wb_sum = w + b
wb_factor = 1 - w - b
return [w / wb_sum] * 3 if wb_sum >= 1 else [c * wb_factor + w for c in hsl_to_srgb([h, 1, 0.5])]


class HWB(HWBish, Space):
"""HWB class."""

BASE = "hsv"
BASE = "srgb"
NAME = "hwb"
SERIALIZE = ("--hwb",)
CHANNELS = (
Expand All @@ -50,19 +44,14 @@ class HWB(HWBish, Space):
def is_achromatic(self, coords: Vector) -> bool:
"""Check if color is achromatic."""

if (coords[1] + coords[2]) >= (1 - 1e-07):
return True

v = 1 - coords[2]
s = 0 if v == 0 else 1 - coords[1] / v
return abs(s) < 1e-4
return (coords[1] + coords[2]) >= (1 - 1e-07)

def to_base(self, coords: Vector) -> Vector:
"""To HSV from HWB."""
"""To sRGB from HWB."""

return hwb_to_hsv(coords)
return hwb_to_srgb(coords)

def from_base(self, coords: Vector) -> Vector:
"""From HSV to HWB."""
"""From sRGB to HWB."""

return hsv_to_hwb(coords)
return srgb_to_hwb(coords)
2 changes: 2 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 2.9.1

- **FIX**: Average should allow controlling `powerless` be disabled by default for backwards compatibility.
- **FIX**: HSL should use the algorithm defined in CSS that allows for round tripping even in the negative lightness
direction. Previously we were converting directly from HSV.

## 2.9

Expand Down
3 changes: 2 additions & 1 deletion docs/src/markdown/colors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ flowchart TB
cmyk --- srgb
xyb --- srgb
hsl --- srgb
hwb --- hsv --- hsl
hsv --- hsl
hwb --- srgb
rec2020-linear --- xyz-d65
rec2020 --- rec2020-linear
Expand Down
4 changes: 2 additions & 2 deletions tools/gamut_3d_plotly.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ def cyl_disc(ColorCyl, space, gamut, location, resolution, opacity, edges):
zpos = 0.0 if location == 'bottom' else 1.0 * factor
# HWB bottom disc will have a single point in the center that is a different colors at different hues. The mesh will
# resolve one of them as the center, usually red. This will cause color averaging in the center of the disc to be
# red-ish for all colors in the center. At lower resolutions, this is more noticeable. To avoid this, interpolate
# reddish for all colors in the center. At lower resolutions, this is more noticeable. To avoid this, interpolate
# rings very close to zero radius, but not zero radius. The mesh will still connect all the points near the center,
# but it will be too small to see.
# but will leave a small hole at the center which will be too small to see.
start, end = 1.0 * factor, (1e-6 if is_hwbish and location == 'bottom' else 0.0)

# Render the two halves of the disc
Expand Down

0 comments on commit 3f81a3d

Please sign in to comment.