Skip to content

Commit

Permalink
Add CSS Linear mode (#367)
Browse files Browse the repository at this point in the history
* Add CSS Linear mode

Push hue fix up into interpolation plugins and provide a CSS compatible
linear interpolation plugin.

Fixes #366

* Fix lint

* Add INTERPOLATOR class override and bump version

* Docs: Add new interpolation topics and improve existing topics

* Add tests
  • Loading branch information
facelessuser authored Oct 6, 2023
1 parent e4e6af2 commit 52207b1
Show file tree
Hide file tree
Showing 37 changed files with 688 additions and 374 deletions.
2 changes: 1 addition & 1 deletion coloraide/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(2, 10, 0, "final")
__version_info__ = Version(2, 11, 0, "final")
__version__ = __version_info__._get_canonical()
7 changes: 5 additions & 2 deletions coloraide/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from .filters.cvd import Protan, Deutan, Tritan
from .interpolate import Interpolator, Interpolate
from .interpolate.linear import Linear
from .interpolate.css_linear import CSSLinear
from .interpolate.continuous import Continuous
from .interpolate.bspline import BSpline
from .interpolate.bspline_natural import NaturalBSpline
Expand Down Expand Up @@ -140,6 +141,7 @@ class Color(metaclass=ColorMeta):
PRECISION = util.DEF_PREC
FIT = util.DEF_FIT
INTERPOLATE = util.DEF_INTERPOLATE
INTERPOLATOR = util.DEF_INTERPOLATOR
DELTA_E = util.DEF_DELTA_E
HARMONY = util.DEF_HARMONY
AVERAGE = util.DEF_AVERAGE
Expand Down Expand Up @@ -1030,7 +1032,7 @@ def interpolate(
premultiplied: bool = True,
extrapolate: bool = False,
domain: list[float] | None = None,
method: str = "linear",
method: str | None = None,
padding: float | tuple[float, float] | None = None,
carryforward: bool | None = None,
powerless: bool | None = None,
Expand All @@ -1049,7 +1051,7 @@ def interpolate(
"""

return interpolate.interpolator(
method,
method if method is not None else cls.INTERPOLATOR,
cls,
colors=colors,
space=space,
Expand Down Expand Up @@ -1382,6 +1384,7 @@ def alpha(self, *, nans: bool = True) -> float:

# Interpolation
Linear(),
CSSLinear(),
Continuous(),
BSpline(),
NaturalBSpline(),
Expand Down
114 changes: 6 additions & 108 deletions coloraide/interpolate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import math
import functools
from abc import ABCMeta, abstractmethod
from .. import util
from .. import algebra as alg
from .. spaces import HSVish, HSLish, Cylindrical, RGBish, LChish, Labish
from ..types import Vector, ColorInput, Plugin
Expand Down Expand Up @@ -83,6 +82,7 @@ def __init__(
extrapolate: bool = False,
domain: Sequence[float] | None = None,
padding: float | tuple[float, float] | None = None,
hue: str = 'shorter',
**kwargs: Any
):
"""Initialize."""
Expand All @@ -100,6 +100,7 @@ def __init__(
self._out_space = out_space
self.extrapolate = extrapolate
self.current_easing = None # type: Mapping[str, Callable[..., float]] | Callable[..., float] | None
self.hue = hue
cs = self.create.CS_MAP[space]
if isinstance(cs, Cylindrical):
self.hue_index = cs.hue_index()
Expand Down Expand Up @@ -469,6 +470,7 @@ def interpolator(
extrapolate: bool = False,
domain: list[float] | None = None,
padding: float | tuple[float, float] | None = None,
hue: str = 'shorter',
**kwargs: Any
) -> Interpolator:
"""Get the interpolator object."""
Expand Down Expand Up @@ -550,93 +552,6 @@ def process_mapping(
return {aliases.get(k, k): v for k, v in progress.items()}


def adjust_shorter(h1: float, h2: float, offset: float) -> tuple[float, float]:
"""Adjust the given hues."""

d = h2 - h1
if d > 180:
h2 -= 360.0
offset -= 360.0
elif d < -180:
h2 += 360
offset += 360.0
return h2, offset


def adjust_longer(h1: float, h2: float, offset: float) -> tuple[float, float]:
"""Adjust the given hues."""

d = h2 - h1
if 0 < d < 180:
h2 -= 360.0
offset -= 360.0
elif -180 < d <= 0:
h2 += 360
offset += 360.0
return h2, offset


def adjust_increase(h1: float, h2: float, offset: float) -> tuple[float, float]:
"""Adjust the given hues."""

if h2 < h1:
h2 += 360.0
offset += 360.0
return h2, offset


def adjust_decrease(h1: float, h2: float, offset: float) -> tuple[float, float]:
"""Adjust the given hues."""

if h2 > h1:
h2 -= 360.0
offset -= 360.0
return h2, offset


def normalize_hue(
color1: Vector,
color2: Vector | None,
index: int,
offset: float,
hue: str,
fallback: float | None
) -> tuple[Vector, float]:
"""Normalize hues according the hue specifier."""

if hue == 'specified':
return (color2 or color1), offset

# Probably the first hue
if color2 is None:
color1[index] = util.constrain_hue(color1[index])
return color1, offset

if hue == 'shorter':
adjuster = adjust_shorter
elif hue == 'longer':
adjuster = adjust_longer
elif hue == 'increasing':
adjuster = adjust_increase
elif hue == 'decreasing':
adjuster = adjust_decrease
else:
raise ValueError("Unknown hue adjuster '{}'".format(hue))

c1 = color1[index] + offset
c2 = util.constrain_hue(color2[index]) + offset

# Adjust hue, handle gaps across `NaN`s
if not math.isnan(c2):
if not math.isnan(c1):
c2, offset = adjuster(c1, c2, offset)
elif fallback is not None:
c2, offset = adjuster(fallback, c2, offset)

color2[index] = c2
return color2, offset


def carryforward_convert(color: Color, space: str, hue_index: int, powerless: bool) -> None: # pragma: no cover
"""Carry forward undefined values during conversion."""

Expand Down Expand Up @@ -776,19 +691,9 @@ def interpolator(
elif powerless and is_cyl and current.is_achromatic():
current[hue_index] = math.nan

# Normalize hue
offset = 0.0
norm_coords = current[:]
fallback = None
if hue_index >= 0:
h = norm_coords[hue_index]
norm_coords, offset = normalize_hue(norm_coords, None, hue_index, offset, hue, fallback)
if not math.isnan(h):
fallback = h

easing = None # type: Any
easings = [] # type: Any
coords = [norm_coords]
coords = [current[:]]

i = 0
for x in colors[1:]:
Expand All @@ -814,16 +719,8 @@ def interpolator(
elif powerless and is_cyl and color.is_achromatic():
color[hue_index] = math.nan

# Normalize the hue
norm_coords = color[:]
if hue_index >= 0:
h = norm_coords[hue_index]
norm_coords, offset = normalize_hue(current[:], norm_coords, hue_index, offset, hue, fallback)
if not math.isnan(h):
fallback = h

# Create an entry interpolating the current color and the next color
coords.append(norm_coords)
coords.append(color[:])
easings.append(easing if easing is not None else progress)

# The "next" color is now the "current" color
Expand All @@ -836,6 +733,7 @@ def interpolator(

# Calculate stops
stops = calc_stops(stops, i)
kwargs['hue'] = hue

# Send the interpolation list along with the stop map to the Piecewise interpolator
return plugin.interpolator(
Expand Down
38 changes: 3 additions & 35 deletions coloraide/interpolate/bspline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
from .continuous import InterpolatorContinuous
from ..interpolate import Interpolator, Interpolate
from ..types import Vector
from typing import Callable, Mapping, Sequence, Any, TYPE_CHECKING

if TYPE_CHECKING: # pragma: no cover
from ..color import Color
from typing import Any


class InterpolatorBSpline(InterpolatorContinuous):
Expand Down Expand Up @@ -80,36 +77,7 @@ class BSpline(Interpolate):

NAME = "bspline"

def interpolator(
self,
coordinates: list[Vector],
channel_names: Sequence[str],
create: type[Color],
easings: list[Callable[..., float] | None],
stops: dict[int, float],
space: str,
out_space: str,
progress: Mapping[str, Callable[..., float]] | Callable[..., float] | None,
premultiplied: bool,
extrapolate: bool = False,
domain: list[float] | None = None,
padding: float | tuple[float, float] | None = None,
**kwargs: Any
) -> Interpolator:
def interpolator(self, *args: Any, **kwargs: Any) -> Interpolator:
"""Return the B-spline interpolator."""

return InterpolatorBSpline(
coordinates,
channel_names,
create,
easings,
stops,
space,
out_space,
progress,
premultiplied,
extrapolate,
domain,
padding,
**kwargs
)
return InterpolatorBSpline(*args, **kwargs)
39 changes: 3 additions & 36 deletions coloraide/interpolate/bspline_natural.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
from .. interpolate import Interpolate, Interpolator
from .bspline import InterpolatorBSpline
from .. import algebra as alg
from ..types import Vector
from typing import List, Sequence, Any, Mapping, Callable, Dict, Type, TYPE_CHECKING

if TYPE_CHECKING: # pragma: no cover
from ..color import Color
from typing import Any


class InterpolatorNaturalBSpline(InterpolatorBSpline):
Expand Down Expand Up @@ -39,36 +35,7 @@ class NaturalBSpline(Interpolate):

NAME = "natural"

def interpolator(
self,
coordinates: List[Vector],
channel_names: Sequence[str],
create: Type[Color],
easings: List[Callable[..., float] | None],
stops: Dict[int, float],
space: str,
out_space: str,
progress: Mapping[str, Callable[..., float]] | Callable[..., float] | None,
premultiplied: bool,
extrapolate: bool = False,
domain: list[float] | None = None,
padding: float | tuple[float, float] | None = None,
**kwargs: Any
) -> Interpolator:
def interpolator(self, *args: Any, **kwargs: Any) -> Interpolator:
"""Return the natural B-spline interpolator."""

return InterpolatorNaturalBSpline(
coordinates,
channel_names,
create,
easings,
stops,
space,
out_space,
progress,
premultiplied,
extrapolate,
domain,
padding,
**kwargs
)
return InterpolatorNaturalBSpline(*args, **kwargs)
39 changes: 3 additions & 36 deletions coloraide/interpolate/catmull_rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
from .bspline import InterpolatorBSpline
from ..interpolate import Interpolator, Interpolate
from .. import algebra as alg
from ..types import Vector
from typing import Callable, Mapping, Sequence, Any, TYPE_CHECKING

if TYPE_CHECKING: # pragma: no cover
from ..color import Color
from typing import Any


class InterpolatorCatmullRom(InterpolatorBSpline):
Expand All @@ -29,36 +25,7 @@ class CatmullRom(Interpolate):

NAME = "catrom"

def interpolator(
self,
coordinates: list[Vector],
channel_names: Sequence[str],
create: type[Color],
easings: list[Callable[..., float] | None],
stops: dict[int, float],
space: str,
out_space: str,
progress: Mapping[str, Callable[..., float]] | Callable[..., float] | None,
premultiplied: bool,
extrapolate: bool = False,
domain: list[float] | None = None,
padding: float | tuple[float, float] | None = None,
**kwargs: Any
) -> Interpolator:
def interpolator(self, *args: Any, **kwargs: Any) -> Interpolator:
"""Return the Catmull-Rom interpolator."""

return InterpolatorCatmullRom(
coordinates,
channel_names,
create,
easings,
stops,
space,
out_space,
progress,
premultiplied,
extrapolate,
domain,
padding,
**kwargs
)
return InterpolatorCatmullRom(*args, **kwargs)
Loading

0 comments on commit 52207b1

Please sign in to comment.