diff --git a/coloraide/__meta__.py b/coloraide/__meta__.py
index b234edd22..13ab05874 100644
--- a/coloraide/__meta__.py
+++ b/coloraide/__meta__.py
@@ -204,5 +204,5 @@ def parse_version(ver: str) -> Version:
return Version(major, minor, micro, release, pre, post, dev)
-__version_info__ = Version(8, 8, 1, "final")
+__version_info__ = Version(8, 9, 0, "final")
__version__ = __version_info__._get_canonical()
diff --git a/coloraide/interpolate/__init__.py b/coloraide/interpolate/__init__.py
index 1f82d35d2..2bcfd3896 100644
--- a/coloraide/interpolate/__init__.py
+++ b/coloraide/interpolate/__init__.py
@@ -829,6 +829,9 @@ def carryforward_convert(color: Color, space: str, hue_index: int, powerless: bo
'L': False, 'V': False, 'a': False, 'b': False
}
+ # Analogous components: all undefined
+ all_undefined = all(math.isnan(c) for c in color[:-1])
+
# Gather undefined channels
if isinstance(cs1, RGBish):
for i, name in zip(cs1.indexes(), ('R', 'G', 'B')):
@@ -838,18 +841,34 @@ def carryforward_convert(color: Color, space: str, hue_index: int, powerless: bo
for i, name in zip(cs1.indexes(), ('L', 'C', 'H')):
if math.isnan(color[i]):
channels[name] = True
+ # Analogous components: chroma and hue
+ if channels['C'] and channels['H']:
+ channels['a'] = True
+ channels['b'] = True
elif isinstance(cs1, Labish):
for i, name in zip(cs1.indexes(), ('L', 'a', 'b')):
if math.isnan(color[i]):
channels[name] = True
+ # Analogous components: chroma and hue
+ if channels['a'] and channels['b']:
+ channels['C'] = True
+ channels['H'] = True
elif isinstance(cs1, HSLish):
for i, name in zip(cs1.indexes(), ('H', 'C', 'L')):
if math.isnan(color[i]):
channels[name] = True
+ # Analogous components: chroma and hue
+ if channels['C'] and channels['H']:
+ channels['a'] = True
+ channels['b'] = True
elif isinstance(cs1, HSVish):
for i, name in zip(cs1.indexes(), ('H', 'C', 'V')):
if math.isnan(color[i]):
channels[name] = True
+ # Analogous components: chroma and hue
+ if channels['C'] and channels['H']:
+ channels['a'] = True
+ channels['b'] = True
elif cs1.is_polar():
if math.isnan(color[cs1.hue_index()]): # type: ignore[attr-defined]
channels['H'] = True
@@ -859,7 +878,9 @@ def carryforward_convert(color: Color, space: str, hue_index: int, powerless: bo
carry.append(-1)
# Channels that need to be carried forward
- if isinstance(cs2, RGBish):
+ if all_undefined:
+ carry.extend(cs2.indexes())
+ elif isinstance(cs2, RGBish):
indexes = cs2.indexes()
for e, name in enumerate(('R', 'G', 'B')):
if channels[name]:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 38d1c5ec5..713a6f104 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -3,6 +3,10 @@ icon: lucide/scroll-text
---
# Changelog
+## 8.9
+
+- **NEW**: Add support for CSS analogous component sets in interpolation and mixing.
+
## 8.8.1
- **ENHANCE**: Minor speed improvements related to chromatic adaptation.
diff --git a/docs/src/markdown/examples/3d_models.html b/docs/src/markdown/examples/3d_models.html
index 9dd98c2d7..ea2677ed2 100644
--- a/docs/src/markdown/examples/3d_models.html
+++ b/docs/src/markdown/examples/3d_models.html
@@ -1241,7 +1241,7 @@
ColorAide Color Space Models
let colorSpaces = null
let colorGamuts = null
let lastModel = null
-let package = 'coloraide-8.8.1-py3-none-any.whl'
+let package = 'coloraide-8.9-py3-none-any.whl'
const defaultSpace = 'lab'
const defaultGamut = 'srgb'
const exceptions = new Set(['hwb', 'ryb', 'ryb-biased'])
diff --git a/docs/src/markdown/examples/colorpicker.html b/docs/src/markdown/examples/colorpicker.html
index adf588b2c..14c74afaa 100644
--- a/docs/src/markdown/examples/colorpicker.html
+++ b/docs/src/markdown/examples/colorpicker.html
@@ -421,7 +421,7 @@ ColorAide Color Picker
let pyodide = null
let webspace = ''
let initial = 'oklab(0.69 0.13 -0.1 / 0.85)'
- let package = 'coloraide-8.8.1-py3-none-any.whl'
+ let package = 'coloraide-8.9-py3-none-any.whl'
const base = `${window.location.origin}/${window.location.pathname.split('/')[1]}/playground/`
package = base + package
diff --git a/docs/src/markdown/interpolation.md b/docs/src/markdown/interpolation.md
index d48deb4c0..27c05ec64 100644
--- a/docs/src/markdown/interpolation.md
+++ b/docs/src/markdown/interpolation.md
@@ -1323,8 +1323,8 @@ derived from `RGBish`, so `x`, `y`, and `z` is treated like super saturated `r`,
Space\ Type | Channel\ Equivalents
------------ | --------
`RGBish` | `r`, `g`, `b`
-`LABish` | `l`
-`LCHish` | `l`, `c`, `h`
+`Labish` | `l`
+`LChish` | `l`, `c`, `h`
`HSLish` | `h`, `s`, `l`
`HSVish` | `h`, `s`, `v`
`Cylindrical`| `h`
@@ -1343,6 +1343,11 @@ Opponent a | `a`
Opponent b | `b`
Value | `v`
+Additionally, `carryforward` is applied to analogous sets as well. This means that a combination of components are
+considered equivalent to another set of components. For instance, if an `LChish` space has an undefined chroma and hue,
+when converting to a `Labish` space, `a` and `b` would be considered undefined (and vice versa). If all color components
+are undefined, then when converted, all components in the new space would also be undefined.
+
## Powerless Hues
> [!example] Experimental
diff --git a/docs/src/zensical.yml b/docs/src/zensical.yml
index 94a3c76f4..0fd03aeb8 100644
--- a/docs/src/zensical.yml
+++ b/docs/src/zensical.yml
@@ -354,7 +354,7 @@ extra_javascript:
- assets/pymdownx-extras/extra-loader-Ccztcqfq.js
- https://cdn.jsdelivr.net/npm/ace-builds@1.43.0/src-min-noconflict/ace.js
- https://cdn.jsdelivr.net/npm/mermaid@11.12.1/dist/mermaid.min.js
- - playground-config-3ba60a2e.js
+ - playground-config-7363a2a9.js
- https://cdn.jsdelivr.net/pyodide/v0.29.0/full/pyodide.js
- assets/coloraide-extras/extra-notebook.js
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
diff --git a/docs/theme/playground-config-3ba60a2e.js b/docs/theme/playground-config-7363a2a9.js
similarity index 71%
rename from docs/theme/playground-config-3ba60a2e.js
rename to docs/theme/playground-config-7363a2a9.js
index 188089712..1dd2a7cdf 100644
--- a/docs/theme/playground-config-3ba60a2e.js
+++ b/docs/theme/playground-config-7363a2a9.js
@@ -1,5 +1,5 @@
var colorNotebook = {
- "playgroundWheels": ['micropip', 'pygments-2.19.2-py3-none-any.whl', 'coloraide-8.8.1-py3-none-any.whl'],
- "notebookWheels": ['pyyaml', 'markdown-3.10.1-py3-none-any.whl', 'pymdown_extensions-10.20-py3-none-any.whl', 'micropip', 'pygments-2.19.2-py3-none-any.whl', 'coloraide-8.8.1-py3-none-any.whl'],
+ "playgroundWheels": ['micropip', 'pygments-2.19.2-py3-none-any.whl', 'coloraide-8.9-py3-none-any.whl'],
+ "notebookWheels": ['pyyaml', 'markdown-3.10.1-py3-none-any.whl', 'pymdown_extensions-10.20-py3-none-any.whl', 'micropip', 'pygments-2.19.2-py3-none-any.whl', 'coloraide-8.9-py3-none-any.whl'],
"defaultPlayground": "import coloraide\ncoloraide.__version__\nColor('red')"
}
diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py
index e10ec2d6a..f83b22d9c 100644
--- a/tests/test_interpolation.py
+++ b/tests/test_interpolation.py
@@ -2457,3 +2457,34 @@ def test_spectral_no_colors(self):
with self.assertRaises(ValueError):
Color.weighted_mix([], method='spectral')
+
+ def test_carryforward_sets(self):
+ """Test analogous sets."""
+
+ result = Color.interpolate(
+ ['color(srgb none none none)', 'oklch(70% 0.1 240)'],
+ space='oklch',
+ carryforward=True
+ )(0.5)
+ self.assertColorEqual(result, Color('oklch(0.7 0.1 240)'))
+
+ result = Color.interpolate(
+ ['oklab(40% none none)', 'oklch(70% 0.1 240)'],
+ space='oklch',
+ carryforward=True
+ )(0.5)
+ self.assertColorEqual(result, Color('oklch(0.55 0.1 240)'))
+
+ result = Color.interpolate(
+ ['oklch(40% none none)', 'oklab(70% 0.1 -1.5)'],
+ space='oklab',
+ carryforward=True
+ )(0.5)
+ self.assertColorEqual(result, Color('oklab(0.55 0.1 -1.5)'))
+
+ result = Color.interpolate(
+ ['hsl(none none 40%)', 'oklab(70% 0.1 -1.5)'],
+ space='oklab',
+ carryforward=True
+ )(0.5)
+ self.assertColorEqual(result, Color('oklab(0.60514 0.1 -1.5)'))
diff --git a/zensical.yml b/zensical.yml
index d24d65626..cf9403f25 100644
--- a/zensical.yml
+++ b/zensical.yml
@@ -354,7 +354,7 @@ extra_javascript:
- assets/pymdownx-extras/extra-loader-Ccztcqfq.js
- https://cdn.jsdelivr.net/npm/ace-builds@1.43.0/src-min-noconflict/ace.js
- https://cdn.jsdelivr.net/npm/mermaid@11.12.1/dist/mermaid.min.js
- - playground-config-3ba60a2e.js
+ - playground-config-7363a2a9.js
- https://cdn.jsdelivr.net/pyodide/v0.29.0/full/pyodide.js
- assets/coloraide-extras/extra-notebook-CU9d9Z53.js
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js