diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index d1797aeef..000000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] -omit=coloraide/gamut/fit_css_color_4.py - -[report] -omit=coloraide/gamut/fit_css_color_4.py -exclude_lines = - pragma: no cover - @overload diff --git a/coloraide/everything.py b/coloraide/everything.py index 480e22cfe..2a3ed5489 100644 --- a/coloraide/everything.py +++ b/coloraide/everything.py @@ -29,7 +29,6 @@ from .spaces.acescg import ACEScg from .spaces.acescc import ACEScc from .spaces.acescct import ACEScct -from .spaces.cam16 import CAM16 from .spaces.cam16_jmh import CAM16JMh from .spaces.cam16_ucs import CAM16UCS, CAM16LCD, CAM16SCD from .spaces.hct import HCT @@ -89,7 +88,6 @@ class ColorAll(Base): ACEScg(), ACEScc(), ACEScct(), - CAM16(), CAM16JMh(), CAM16UCS(), CAM16SCD(), diff --git a/coloraide/spaces/cam16_ucs.py b/coloraide/spaces/cam16_ucs.py index c0fb87d72..e31ed3799 100644 --- a/coloraide/spaces/cam16_ucs.py +++ b/coloraide/spaces/cam16_ucs.py @@ -7,10 +7,12 @@ """ from __future__ import annotations import math -from . cam16 import CAM16 -from ..types import Vector +from .cam16_jmh import CAM16JMh +from ..spaces import Space, Labish +from ..cat import WHITES +from .. import util from ..channels import Channel, FLG_MIRROR_PERCENT -from .. import algebra as alg +from ..types import Vector COEFFICENTS = { 'lcd': (0.77, 0.007, 0.0053), @@ -19,7 +21,7 @@ } -def cam16_to_cam16_ucs(jab: Vector, model: str) -> Vector: +def cam16_jmh_to_cam16_ucs(jmh: Vector, model: str) -> Vector: """ CAM16 (Jab) to CAM16 UCS (Jab). @@ -27,17 +29,13 @@ def cam16_to_cam16_ucs(jab: Vector, model: str) -> Vector: and then adding the new adjusted multiplier. Then we can just adjust lightness. """ - J, a, b = jab - M = math.sqrt(a ** 2 + b ** 2) + J, M, h = jmh c1, c2 = COEFFICENTS[model][1:] - if M != 0: - a /= M - b /= M - M = math.log(1 + c2 * M) / c2 - a *= M - b *= M + M = math.log(1 + c2 * M) / c2 + a = M * math.cos(math.radians(h)) + b = M * math.sin(math.radians(h)) return [ (1 + 100 * c1) * J / (1 + c1 * J), @@ -46,7 +44,7 @@ def cam16_to_cam16_ucs(jab: Vector, model: str) -> Vector: ] -def cam16_ucs_to_cam16(ucs: Vector, model: str) -> Vector: +def cam16_ucs_to_cam16_jmh(ucs: Vector, model: str) -> Vector: """ CAM16 UCS (Jab) to CAM16 (Jab). @@ -55,53 +53,67 @@ def cam16_ucs_to_cam16(ucs: Vector, model: str) -> Vector: """ J, a, b = ucs - M = math.sqrt(a ** 2 + b ** 2) c1, c2 = COEFFICENTS[model][1:] - if M != 0: - a /= M - b /= M - M = (math.exp(M * c2) - 1) / c2 - a *= M - b *= M + M = math.sqrt(a ** 2 + b ** 2) + M = (math.exp(M * c2) - 1) / c2 + h = math.degrees(math.atan2(b, a)) return [ J / (1 - c1 * (J - 100)), - a, - b + M, + util.constrain_hue(h) ] -class CAM16UCS(CAM16): +class CAM16UCS(Labish, Space): """CAM16 UCS (Jab) class.""" - BASE = "cam16" + BASE = "cam16-jmh" NAME = "cam16-ucs" SERIALIZE = ("--cam16-ucs",) MODEL = 'ucs' CHANNELS = ( - Channel("j", 0.0, 100.0, limit=(0.0, None)), + Channel("j", 0.0, 100.0), Channel("a", -50.0, 50.0, flags=FLG_MIRROR_PERCENT), Channel("b", -50.0, 50.0, flags=FLG_MIRROR_PERCENT) ) + CHANNEL_ALIASES = { + "lightness": "j" + } + WHITE = WHITES['2deg']['D65'] + # Use the same environment as CAM16JMh + ENV = CAM16JMh.ENV + ACHROMATIC = CAM16JMh.ACHROMATIC + + def resolve_channel(self, index: int, coords: Vector) -> float: + """Resolve channels.""" + + if index in (1, 2): + if not math.isnan(coords[index]): + return coords[index] + + return self.ACHROMATIC.get_ideal_ab(coords[0])[index - 1] + + value = coords[index] + return self.channels[index].nans if math.isnan(value) else value def is_achromatic(self, coords: Vector) -> bool: """Check if color is achromatic.""" - j, a, b = cam16_ucs_to_cam16(coords, self.MODEL) - m, h = alg.rect_to_polar(a, b) - return coords[0] == 0.0 or self.ACHROMATIC.test(j, m, h) + j, m, h = cam16_ucs_to_cam16_jmh(coords, self.MODEL) + return j <= 0.0 or self.ACHROMATIC.test(j, m, h) def to_base(self, coords: Vector) -> Vector: - """To XYZ from CAM16.""" + """To CAM16 JMh from CAM16.""" - return cam16_ucs_to_cam16(coords, self.MODEL) + return cam16_ucs_to_cam16_jmh(coords, self.MODEL) def from_base(self, coords: Vector) -> Vector: - """From XYZ to CAM16.""" + """From CAM16 JMh to CAM16.""" - return cam16_to_cam16_ucs(coords, self.MODEL) + return cam16_jmh_to_cam16_ucs(coords, self.MODEL) class CAM16LCD(CAM16UCS): diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md index 9a92c2058..e6a507369 100644 --- a/docs/src/markdown/about/changelog.md +++ b/docs/src/markdown/about/changelog.md @@ -2,6 +2,9 @@ ## Unreleased +- **NEW**: Deprecate non-standard CAM16 (Jab) space. People should use the standard CAM16 JMh or the CAM16 UCS, SCD, + or LCD Jab spaces. The non-standard Jab is still available via `coloraide.spaces.cam16.CAM16`, but it is no longer + available in `coloraide.everything` and will be removed at a future time. - **FIX**: Much more accurate ICtCp matrices. - **FIX**: Fix typing of deeply nested arrays in `algebra`. - **FIX**: Fix issue with HCT undefined channel resolver. diff --git a/docs/src/markdown/colors/cam16.md b/docs/src/markdown/colors/cam16.md deleted file mode 100644 index f164660a6..000000000 --- a/docs/src/markdown/colors/cam16.md +++ /dev/null @@ -1,92 +0,0 @@ -# CAM16 - -/// failure | The CAM16 color space is not registered in `Color` by default -/// - -/// html | div.info-container -//// info | Properties - attrs: {class: inline end} - -**Name:** `cam16` - -**White Point:** D65 / 2˚ - -**Coordinates:** - -Name | Range^\*^ ----- | ----- -`j` | [0, 100] -`a` | [-90, 90] -`b` | [-90, 90] - -^\*^ Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in -relation to the Display P3 color space. -//// - -//// html | figure -![CAM16](../images/cam16-3d.png) - -///// html | figcaption -The sRGB gamut represented within the CAM16 color space. -///// -//// - -A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color -vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical -measurement of the stimulus source. - -CAM16 is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous different -attributes: - -Name | Description ----- | ----------- -J | Lightness -C | Chroma -h | hue -s | saturation -Q | Brightness -M | Colorfulness -H | Hue Quadrature - -A color space can be constructed of using a subset of these attributes: JCh, JMh, Jsh, QCh, QMh, Qsh, etc. You can also -construct Lab like spaces taking using the hue and either C, M, or s. The `cam16` color space in ColorAide represents -a Jab configuration based off M (Colorfulness). - -[Learn more](https://doi.org/10.1002/col.22131). -/// - -## Channel Aliases - -Channels | Aliases --------- | ------- -`j` | `lightness` -`a` | -`b` | - -## Input/Output - -The CAM16 UCS space is not currently supported in the CSS spec, the parsed input and string output formats use -the `#!css-color color()` function format using the custom name `#!css-color --cam16`: - -```css-color -color(--cam16 j a b / a) // Color function -``` - -The string representation of the color object and the default string output use the -`#!css-color color(--cam16 j a b / a)` form. - -```py play -Color("cam16", [46.026, 72.143, 37.385], 1) -Color("cam16", [68.056, 13.955, 41.212], 1).to_string() -``` - -## Registering - -```py -from coloraide import Color as Base -from coloraide_extras.spaces.cam16 import CAM16 - -class Color(Base): ... - -Color.register(CAM16()) -``` diff --git a/docs/src/markdown/colors/cam16_jmh.md b/docs/src/markdown/colors/cam16_jmh.md index 5b23120ae..10f3ac9f8 100644 --- a/docs/src/markdown/colors/cam16_jmh.md +++ b/docs/src/markdown/colors/cam16_jmh.md @@ -35,8 +35,8 @@ A color appearance model (CAM) is a mathematical model that seeks to describe th vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source. -[CAM16](./cam16.md) is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous -different attributes: +CAM16 is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous different +attributes: Name | Description ---- | ----------- diff --git a/docs/src/markdown/colors/index.md b/docs/src/markdown/colors/index.md index e2e190d5a..e6694f7a9 100644 --- a/docs/src/markdown/colors/index.md +++ b/docs/src/markdown/colors/index.md @@ -78,10 +78,10 @@ flowchart TB xyz-d65 --- lab-d65 --- lch-d65 - xyz-d65 --- cam16-jmh --- cam16 - cam16 --- cam16-ucs - cam16 --- cam16-scd - cam16 --- cam16-lcd + xyz-d65 --- cam16-jmh + cam16-jmh --- cam16-ucs + cam16-jmh --- cam16-scd + cam16-jmh --- cam16-lcd xyz-d65 --- hct @@ -157,7 +157,6 @@ flowchart TB acescg(ACEScg) acescc(ACEScc) acescct(ACEScct) - cam16(CAM16) cam16-jmh(CAM16 JMh) cam16-ucs(CAM16 UCS) cam16-scd(CAM16 SCD) @@ -217,7 +216,6 @@ flowchart TB click acescg "./acescg/" _self click acescc "./acescc/" _self click acescct "./acescct/" _self - click cam16 "./cam16/" _self click cam16-jmh "./cam16_jmh/" _self click cam16-ucs "./cam16_ucs/" _self click cam16-scd "./cam16_scd/" _self diff --git a/docs/src/markdown/contrast.md b/docs/src/markdown/contrast.md index 013e34871..0551c40b5 100644 --- a/docs/src/markdown/contrast.md +++ b/docs/src/markdown/contrast.md @@ -111,7 +111,7 @@ Color("blue").contrast("red", method='wcag21') /// Google's Material Design uses a new color space called [HCT](./colors/hct.md). It uses the hue and chroma from -[CAM16](./colors/cam16.md) and the tone/lightness from CIELab. For contrast, they determined using tones that are +[CAM16](./colors/cam16_jmh.md) and the tone/lightness from CIELab. For contrast, they determined using tones that are "far enough apart" in the HCT color space was a good indication of sufficient contrast. Since HCT tone is exactly the same as CIELab's lightness (also known as L\*), we've referred to this approach as Lstar. diff --git a/docs/src/markdown/gamut.md b/docs/src/markdown/gamut.md index 90adbf100..ab8bb369f 100644 --- a/docs/src/markdown/gamut.md +++ b/docs/src/markdown/gamut.md @@ -276,8 +276,8 @@ Much like the other LCh chroma reduction algorithms, HCT Chroma performs gamut m [LCh Chroma](#lch-chroma) with the exception that it uses the HCT color space as the working LCh color space. Google's Material Design uses a new color space called [HCT](./colors/hct.md). It uses the hue and chroma from -[CAM16 (JMh)](./colors/cam16.md) space and the tone/lightness from the [CIELab](./colors/lab_d65.md) space. HCT takes -advantage of the good hue preservation of CAM16 and has the better lightness predictability of CIELab. Using these +[CAM16 (JMh)](./colors/cam16_jmh.md) space and the tone/lightness from the [CIELab](./colors/lab_d65.md) space. HCT +takes advantage of the good hue preservation of CAM16 and has the better lightness predictability of CIELab. Using these characteristics, the color space is adept at generating tonal palettes with predictable lightness. This makes it easier to construct UIs with decent contrast. But to do this well, you must work in HCT and gamut map in HCT. For this reason, the HCT Chroma gamut mapping method was added. diff --git a/docs/src/markdown/manipulation.md b/docs/src/markdown/manipulation.md index cd03f53b5..b47dfeef4 100644 --- a/docs/src/markdown/manipulation.md +++ b/docs/src/markdown/manipulation.md @@ -338,8 +338,8 @@ always be true. This behavior can be seen in non cylindrical spaces as well, like the Lab form of CAM16. ```py play - Color('gray').convert('cam16') - Color('cam16', [43.042, NaN, NaN]).normalize(nans=False) + Color('gray').convert('cam16-ucs') + Color('cam16-ucs', [56.23, NaN, NaN]).normalize(nans=False) ``` The selected values may not always perfectly precise, but they are much better than blindly assuming zero. diff --git a/docs/src/mkdocs.yml b/docs/src/mkdocs.yml index ed2fd8c85..cb094854d 100644 --- a/docs/src/mkdocs.yml +++ b/docs/src/mkdocs.yml @@ -99,7 +99,6 @@ nav: - IPT: colors/ipt.md - ICtCp: colors/ictcp.md - IgPgTg: colors/igpgtg.md - - CAM16: colors/cam16.md - CAM16 UCS: colors/cam16_ucs.md - CAM16 SCD: colors/cam16_scd.md - CAM16 LCD: colors/cam16_lcd.md diff --git a/mkdocs.yml b/mkdocs.yml index 7ae6810cf..0651d4244 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,7 +99,6 @@ nav: - IPT: colors/ipt.md - ICtCp: colors/ictcp.md - IgPgTg: colors/igpgtg.md - - CAM16: colors/cam16.md - CAM16 UCS: colors/cam16_ucs.md - CAM16 SCD: colors/cam16_scd.md - CAM16 LCD: colors/cam16_lcd.md diff --git a/pyproject.toml b/pyproject.toml index 2f27f95cf..3421add7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,17 @@ ignore = [ "RUF100" ] +[tool.coverage.report] +# Regexes for lines to exclude from consideration +omit = [ + "coloraide/spaces/cam16.py" +] + +exclude_lines = [ + "pragma: no cover", + "@overload" +] + [tool.tox] legacy_tox_ini = """ [tox] diff --git a/tests/test_cam16.py b/tests/test_cam16.py deleted file mode 100644 index b0849c0ee..000000000 --- a/tests/test_cam16.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Test CAM16 UCS.""" -import unittest -from . import util -from coloraide.everything import ColorAll as Color, NaN -import pytest - - -class TestCAM16CAM16(util.ColorAssertsPyTest): - """Test CAM16.""" - - COLORS = [ - ('red', 'color(--cam16 46.026 72.143 37.385)'), - ('orange', 'color(--cam16 68.056 13.955 41.212)'), - ('yellow', 'color(--cam16 94.682 -19.662 50.83)'), - ('green', 'color(--cam16 33.976 -40.301 31.147)'), - ('blue', 'color(--cam16 25.066 13.785 -60.901)'), - ('indigo', 'color(--cam16 16.046 28.336 -32.712)'), - ('violet', 'color(--cam16 63.507 41.066 -22.402)'), - ('white', 'color(--cam16 100 -1.9463 -1.1026)'), - ('gray', 'color(--cam16 43.042 -1.2764 -0.72317)'), - ('black', 'color(--cam16 0 0 0)'), - # Test color - ('color(--cam16 50 10 -10)', 'color(--cam16 50 10 -10)'), - ('color(--cam16 50 10 -10 / 0.5)', 'color(--cam16 50 10 -10 / 0.5)'), - ('color(--cam16 50% 50% -50% / 50%)', 'color(--cam16 50 45 -45 / 0.5)'), - ('color(--cam16 none none none / none)', 'color(--cam16 none none none / none)'), - # Test percent ranges - ('color(--cam16 0% 0% 0%)', 'color(--cam16 0 0 0)'), - ('color(--cam16 100% 100% 100%)', 'color(--cam16 100 90 90)'), - ('color(--cam16 -100% -100% -100%)', 'color(--cam16 -100 -90 -90)') - ] - - @pytest.mark.parametrize('color1,color2', COLORS) - def test_colors(self, color1, color2): - """Test colors.""" - - self.assertColorEqual(Color(color1).convert('cam16'), Color(color2)) - - -class TestCAM16Serialize(util.ColorAssertsPyTest): - """Test CAM16 serialization.""" - - COLORS = [ - # Test color - ('color(--cam16 75 10 -10 / 0.5)', {}, 'color(--cam16 75 10 -10 / 0.5)'), - # Test alpha - ('color(--cam16 75 10 -10)', {'alpha': True}, 'color(--cam16 75 10 -10 / 1)'), - ('color(--cam16 75 10 -10 / 0.5)', {'alpha': False}, 'color(--cam16 75 10 -10)'), - # Test None - ('color(--cam16 none 10 -10)', {}, 'color(--cam16 0 10 -10)'), - ('color(--cam16 none 10 -10)', {'none': True}, 'color(--cam16 none 10 -10)'), - # Test Fit (not bound) - ('color(--cam16 120 10 -10)', {}, 'color(--cam16 120 10 -10)'), - ('color(--cam16 120 10 -10)', {'fit': False}, 'color(--cam16 120 10 -10)') - ] - - @pytest.mark.parametrize('color1,options,color2', COLORS) - def test_colors(self, color1, options, color2): - """Test colors.""" - - self.assertEqual(Color(color1).to_string(**options), color2) - - -class TestCAM16Poperties(util.ColorAsserts, unittest.TestCase): - """Test CAM16.""" - - def test_j(self): - """Test `j`.""" - - c = Color('color(--cam16 0.51332 0.92781 1.076)') - self.assertEqual(c['j'], 0.51332) - c['j'] = 0.2 - self.assertEqual(c['j'], 0.2) - - def test_a(self): - """Test `a`.""" - - c = Color('color(--cam16 0.51332 0.92781 1.076)') - self.assertEqual(c['a'], 0.92781) - c['a'] = 0.1 - self.assertEqual(c['a'], 0.1) - - def test_b(self): - """Test `b`.""" - - c = Color('color(--cam16 0.51332 0.92781 1.076)') - self.assertEqual(c['b'], 1.076) - c['b'] = 0.1 - self.assertEqual(c['b'], 0.1) - - def test_alpha(self): - """Test `alpha`.""" - - c = Color('color(--cam16 0.51332 0.92781 1.076)') - self.assertEqual(c['alpha'], 1) - c['alpha'] = 0.5 - self.assertEqual(c['alpha'], 0.5) - - -class TestsAchromatic(util.ColorAsserts, unittest.TestCase): - """Test achromatic.""" - - def test_achromatic(self): - """Test when color is achromatic.""" - - self.assertEqual(Color('#222222').convert('cam16').is_achromatic(), True) - self.assertEqual(Color('srgb', [0.000000001] * 3).convert('cam16').set('j', NaN).is_achromatic(), True) - self.assertEqual(Color('cam16', [0, NaN, NaN]).is_achromatic(), True) - self.assertEqual(Color('cam16', [0, NaN, NaN]).is_achromatic(), True) - self.assertEqual(Color('cam16', [0, 3, -4]).is_achromatic(), True) - self.assertEqual(Color('cam16', [NaN, 0, -3]).is_achromatic(), True) - self.assertEqual(Color('cam16', [30, NaN, 0]).is_achromatic(), False) - self.assertEqual(Color('cam16', [NaN, NaN, 0]).is_achromatic(), True) diff --git a/tests/test_cam16_ucs.py b/tests/test_cam16_ucs.py index 4a21d1331..5ce302a2c 100644 --- a/tests/test_cam16_ucs.py +++ b/tests/test_cam16_ucs.py @@ -287,7 +287,7 @@ class TestsAchromatic(util.ColorAsserts, unittest.TestCase): def test_achromatic(self): """Test when color is achromatic.""" - self.assertEqual(Color('#222222').convert('cam16').is_achromatic(), True) + self.assertEqual(Color('#222222').convert('cam16-ucs').is_achromatic(), True) self.assertEqual(Color('srgb', [0.000000001] * 3).convert('cam16-ucs').set('j', NaN).is_achromatic(), True) self.assertEqual(Color('cam16-ucs', [0, NaN, NaN]).is_achromatic(), True) self.assertEqual(Color('cam16-ucs', [0, NaN, NaN]).is_achromatic(), True)