Repository: facelessuser/coloraide (language: python)

Issue:
Make functions that operate on two or more colors more intuitive?
There are a couple methods that operate on one or more colors:

- mix
- interpolate
- steps
- contrast
- compose
- delta_e
- distance
- closest

All of these methods have the behavior where the current color is assumed as the first input. And for things like mix, this seems intuitive:

```py
color1.mix(color2)
```

But when we look at things like interpolating, which can have multiple colors, this can feel awkward:

```py
Color('red').interpolate(['blue', 'green'])
```

Often, it may be desirable to do something like.

```py
Color.interpolate(['red', 'blue', 'green'])
```

It may be useful to provide a slightly more dynamic interface, such that you can do something like.

```py
Color('red').mix('currentcolor', 'blue')
```
or

```py
Color.mix('red', 'blue')
```

Here the instance method will not assume you want to mix itself and instead requires two colors. Only if you specify specify "currentcolor", will it use `self`. And if you use the classmethod, it requires two explicit colors.

An alternative is simply instance methods will assume self, but classmethod will require explicit inputs.

This is all interesting, but it'd be nice to prototype it and see how it feels and performs.

Create classmethod variants of certain instancemethods
Create classmethods for methods that apply operations on multiple
colors.

Resolves #186 


Discussion / hints:
Have a local prototype. It allows a way to operate on the multiple colors directly from the classmethods without changing the signature of instance calls. So for interpolation, you'd just give the classmethod a list of 2 or more colors. The instancemethod would take a single color or a list of colors and the current color would be included, basically what is done currently:

```py
>>> Color.mix('red', 'blue')
color(srgb 0.55044 0.32562 0.6365 / 1)
>>> Color.steps(['green', 'red', 'blue'], steps=5)
[color(srgb 0 0.50196 0 / 1), color(srgb 0.6516 0.41238 0.00082 / 1), color(srgb 1 0 0 / 1), color(srgb 0.55044 0.32562 0.6365 / 1), color(srgb 0 0 1 / 1)]
>>> Color.contrast('red', 'blue')
2.149390533243867
```

And we can see the old methods are still usable in the same way from the instance:

```py
>>> Color('red').steps(['red', 'blue'], steps=5)
[color(srgb 1 0 0 / 1), color(srgb 1 0 0 / 1), color(srgb 1 0 0 / 1), color(srgb 0.55044 0.32562 0.6365 / 1), color(srgb 0 0 1 / 1)]
```

I think will pass on the `currentColor` stuff. That would alter the current behavior to act like a classmethod, with the ability to optionally include the current instance's color. This would be a breaking change for little additional value and doesn't make sense for things like contrast, distance, etc.

I think this will allow people to work with colors how they want to.


I am questioning if we need to make all of these functions with class methods. Do too many, then the question arises, why don't we do this to the rest:`filter`, `harmony`, `convert`, etc.

There is also the question of how pythonic the prototype is. For all intents and purposes, it works great currently, but would it be surprising to the user...

I really think it makes piecewise interpolation more intuitive, but is it really needed for `mix`?

I could see an argument that this would work fine for `interpolate`, `steps`, and maybe `compose`, but maybe it isn't expressly needed for things like distancing, contrast, and two color mixing.

What I like about this approach with interpolation and steps is that it doesn't actually change the signature, not really. The instance takes a single color or a list of colors and interpolates them all with the base as the first in the list.  With the class method, it does the same except the base color is obviously excluded. In the event that an empty list is provided, meaning no colors, there would be a failure. Piecewise wrappers can then be applied to the first color.

I guess the question could be raised, why not just add a new function instead of creating this dual method? This is also a good question. 
The more I'm thinking about this, the more I'm thinking we should only address the problem areas. We can always add more after the fact if requested with a real reason backing the request.

I think the only problem areas are `interpolate` and `steps`.

`mix`, `contrast`, `delta_e` and `distance` are all operations that only work on two colors, so the idea of simply doing the following seems fine:

```py
color1.operation(color2)
```

In some operations the order matters, like some detla_e methods, but swapping the colors is no different than if we provided a method that accepted two inputs. The second color is always the sample. We should make a note of this. Mixing is obviously order-dependent as well, but this is noted.

Contrast is not currently order-dependent, but I suspect it will be when the WCG publishes (officially) the new approach.

I think `compose` is also not a problem, the idea is simply that you take a source (the base) color, and overlay it on a stack of color(s) in the background. You specify the blend modes and porter duff operator to use, and you get color back. Source has significance, so I think it makes sense.

But with `steps` and `interpolate`, especially when doing piecewise interpolation, all colors under consideration are being interpolated, and the special cases of the first color, since it is the caller, are weird. If I want to interpolate a list of colors, I have to do something odd like below:

```py
colors = ['red', 'yellow', 'blue']
Color(colors[0]).interpolate(colors.[1:])
```

If I want to add color stops it is fine until I need to add it to the first:

```py
colors = [Piecewise('red', 0.3), Piecewise('yellow', 0.5), 'blue']
# Nope, can't do that
stop = colors[0].stop
colors[0] = colors[0].color
Color(colors[0]).interpolate(colors.[1:], stop=stop)
```

When what we really want to do is something simple like:

```py
colors = [Piecewise('red', 0.3), Piecewise('yellow', 0.5), 'blue']
Color.interpolate(colors)
```

So I may prototype some other options and see what is best. I think the hybrid method works well, but if we use it, I want to scale it back to only `steps` and `interpolate`, but maybe we'll add a dedicated `piecewise_steps` and `piecewise` that are classmethods. They'll always take a list and never consider the base...


So after thinking about this some more, the plan is to separate piecewise logic from basic interpolation. The goal is to make the interface as easy to understand as possible. I always found the current way awkward, and if we are going to roll out the 1.0 version sometime soon, I'd like to try and get it right. This of course will cause some breakage, but alas, there is no better way.

The plan is to have `interpolate` and `steps` only operate on two colors. The current instance and the provided input. If piecewise interpolation is desired, we will provide `piecewise` and `piecewise_steps`. These methods are both classmethod only, so if you call them on an instance, they will not auto-include the current instance into the interpolation. This will allow very straightforward, multi-color interpolations:

```py
Color.piecewise([Piecewise('red', 0.3), Piecewise('yellow', 0.5), 'blue'])
```

Yeah, I wish I got it right the first time, but that's software development, especially when you don't have feedback.
I think we need to stop and re-evaluate interpolation in general. I was playing around with creating interpolation with bezier curves:

<img width="722" alt="Screen Shot 2022-07-08 at 6 54 46 AM" src="https://user-images.githubusercontent.com/1055125/177996193-bc4f6499-1443-4d1d-8a58-8d3333f78231.png">

My concern is that color stops aren't general enough to work in this scenario. We've tied them to piecewise interpolation...I think we just to make sure we've dotted our "i"s and crossed our "t"s. If we need to rework all of this, so be it.

I meant to post this one:

<img width="1245" alt="Screen Shot 2022-07-08 at 8 51 42 PM" src="https://user-images.githubusercontent.com/1055125/178088936-02d9f953-9565-45dd-82e1-cdb698fd8689.png">

We can have easing functions directly inserted between colors and wrap colors with stops. The function would be a class method and the user could switch to different interpolation approaches such as bezier.




Fix patch:
diff --git a/coloraide/__init__.py b/coloraide/__init__.py
index 08ddcda9b..2c04e9fa8 100644
--- a/coloraide/__init__.py
+++ b/coloraide/__init__.py
@@ -1,7 +1,7 @@
 """ColorAide Library."""
 from .__meta__ import __version_info__, __version__  # noqa: F401
 from .color import Color, ColorMatch
-from .interpolate import Piecewise, Lerp
+from .interpolate import stop, hint
 from .algebra import NaN
 
-__all__ = ("Color", "ColorMatch", "NaN", "Piecewise", "Lerp")
+__all__ = ("Color", "ColorMatch", "NaN", "stop", "hint")
diff --git a/coloraide/color.py b/coloraide/color.py
index 753bf0b65..1ea54e462 100644
--- a/coloraide/color.py
+++ b/coloraide/color.py
@@ -710,30 +710,6 @@ def mask(self, channel: Union[str, Sequence[str]], *, invert: bool = False, in_p
                 this[name] = alg.NaN
         return this
 
-    def steps(
-        self,
-        color: Union[Union[ColorInput, interpolate.Piecewise], Sequence[Union[ColorInput, interpolate.Piecewise]]],
-        *,
-        steps: int = 2,
-        max_steps: int = 1000,
-        max_delta_e: float = 0,
-        delta_e: Optional[str] = None,
-        **interpolate_args: Any
-    ) -> List['Color']:
-        """
-        Discrete steps.
-
-        This is built upon the interpolate function, and will return a list of
-        colors containing a minimum of colors equal to `steps` or steps as specified
-        derived from the `max_delta_e` parameter (whichever is greatest).
-
-        Number of colors can be capped with `max_steps`.
-
-        Default delta E method used is delta E 76.
-        """
-
-        return self.interpolate(color, **interpolate_args).steps(steps, max_steps, max_delta_e, delta_e)
-
     def mix(
         self,
         color: ColorInput,
@@ -749,22 +725,38 @@ def mix(
         The basic mixing logic is outlined in the CSS level 5 draft.
         """
 
-        if not self._is_color(color) and not isinstance(color, (str, interpolate.Piecewise, Mapping)):
+        if not self._is_color(color) and not isinstance(color, (str, Mapping)):
             raise TypeError("Unexpected type '{}'".format(type(color)))
-        mixed = self.interpolate(color, **interpolate_args)(percent)
+        mixed = self.interpolate([self, color], **interpolate_args)(percent)
         return self.mutate(mixed) if in_place else mixed
 
+    @classmethod
+    def steps(
+        cls,
+        colors: Sequence[Union[ColorInput, interpolate.common.stop, Callable[..., float]]],
+        *,
+        steps: int = 2,
+        max_steps: int = 1000,
+        max_delta_e: float = 0,
+        delta_e: Optional[str] = None,
+        **interpolate_args: Any
+    ) -> List['Color']:
+        """Discrete steps."""
+
+        return cls.interpolate(colors, **interpolate_args).steps(steps, max_steps, max_delta_e, delta_e)
+
+    @classmethod
     def interpolate(
-        self,
-        color: Union[Union[ColorInput, interpolate.Piecewise], Sequence[Union[ColorInput, interpolate.Piecewise]]],
+        cls,
+        colors: Sequence[Union[ColorInput, interpolate.common.stop, Callable[..., float]]],
         *,
         space: Optional[str] = None,
         out_space: Optional[str] = None,
-        stop: float = 0,
         progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]] = None,
         hue: str = util.DEF_HUE_ADJ,
-        premultiplied: bool = True
-    ) -> interpolate.Interpolator:
+        premultiplied: bool = True,
+        method: str = "linear"
+    ) -> interpolate.common.Interpolator:
         """
         Return an interpolation function.
 
@@ -777,43 +769,15 @@ def interpolate(
         mixing occurs.
         """
 
-        if space is None:
-            space = self.INTERPOLATE
-
-        if out_space is None:
-            out_space = self.space()
-
-        # A piecewise object was provided, so treat it as such,
-        # or we've changed the stop of the base color, so run it through piecewise.
-        if (
-            isinstance(color, interpolate.Piecewise) or
-            (stop != 0 and (isinstance(color, (str, Mapping)) or self._is_color(color)))
-        ):
-            color = cast(Sequence[Union['Color', str, Mapping[str, Any], interpolate.Piecewise]], [color])
-
-        if not isinstance(color, str) and isinstance(color, Sequence):
-            # We have a sequence, so use piecewise interpolation
-            colors = list(color)
-            colors.insert(0, interpolate.Piecewise(self, stop=stop))
-            return interpolate.color_piecewise_lerp(
-                colors,
-                space,
-                out_space,
-                progress,
-                hue,
-                premultiplied
-            )
-        else:
-            # We have a sequence, so use piecewise interpolation
-            return interpolate.color_lerp(
-                self,
-                color,
-                space,
-                out_space,
-                progress,
-                hue,
-                premultiplied
-            )
+        return interpolate.get_interpolator(method)(
+            cls,
+            colors=colors,
+            space=space,
+            out_space=out_space,
+            progress=progress,
+            hue=hue,
+            premultiplied=premultiplied
+        )
 
     def filter(  # noqa: A003
         self,
diff --git a/coloraide/harmonies.py b/coloraide/harmonies.py
index 0b8a809b4..bd9541900 100644
--- a/coloraide/harmonies.py
+++ b/coloraide/harmonies.py
@@ -77,23 +77,23 @@ def harmonize(cls, color: 'Color', space: Optional[str]) -> List['Color']:
         if steps_b <= 1:
             left = []
             if steps_b == 1:
-                left.extend(b.steps(color, steps=steps_b, space=space, out_space=orig_space))
+                left.extend(color.steps([b, color], steps=steps_b, space=space, out_space=orig_space))
             steps = min(cls.RANGE - (1 + steps_b), steps_w)
-            right = color0.steps(w, steps=steps, space=space, out_space=orig_space)[rtrim]
+            right = color.steps([color0, w], steps=steps, space=space, out_space=orig_space)[rtrim]
 
         # Very close to white or is white, no need to interpolate from current color to white
         elif steps_w <= 1:
             right = []
             if steps_w == 1:
-                right.extend(color0.steps(w, steps=steps_w, space=space, out_space=orig_space))
+                right.extend(color.steps([color0, w], steps=steps_w, space=space, out_space=orig_space))
             steps = min(cls.RANGE - (1 + steps_w), steps_b)
             right.insert(0, color.clone())
-            left = b.steps(color, steps=steps, space=space, out_space=orig_space)[ltrim]
+            left = color.steps([b, color], steps=steps, space=space, out_space=orig_space)[ltrim]
 
         else:
             # Anything else in between
-            left = b.steps(color, steps=steps_b, space=space, out_space=orig_space)[ltrim]
-            right = color0.steps(w, steps=steps_w, space=space, out_space=orig_space)[rtrim]
+            left = color.steps([b, color], steps=steps_b, space=space, out_space=orig_space)[ltrim]
+            right = color.steps([color0, w], steps=steps_w, space=space, out_space=orig_space)[rtrim]
 
         # Extract a subset of the results
         len_l = len(left)
diff --git a/coloraide/interpolate.py b/coloraide/interpolate.py
deleted file mode 100644
index c51d50974..000000000
--- a/coloraide/interpolate.py
+++ /dev/null
@@ -1,501 +0,0 @@
-"""
-Interpolation methods.
-
-Originally, the base code for `interpolate`, `mix` and `steps` was ported from the
-https://colorjs.io project. Since that time, there has been significant modifications
-that add additional features etc. The base logic though is attributed to the original
-authors.
-
-In general, the logic mimics in many ways the `color-mix` function as outlined in the Level 5
-color draft (Oct 2020), but the initial approach was modeled directly off of the work done in
-color.js.
----
-Original Authors: Lea Verou, Chris Lilley
-License: MIT (As noted in https://github.com/LeaVerou/color.js/blob/master/package.json)
-"""
-from abc import ABCMeta, abstractmethod
-from collections import namedtuple
-from . import algebra as alg
-from .types import Vector
-from .spaces import Cylindrical
-from .channels import FLG_ANGLE
-from typing import Optional, Callable, Sequence, Mapping, Type, Dict, List, Union, cast, TYPE_CHECKING
-from .types import ColorInput
-
-if TYPE_CHECKING:  # pragma: no cover
-    from .color import Color
-
-
-class Lerp:
-    """Linear interpolation."""
-
-    def __init__(self, progress: Optional[Callable[..., float]]) -> None:
-        """Initialize."""
-
-        self.progress = progress
-
-    def __call__(self, a: float, b: float, t: float) -> float:
-        """Interpolate with period."""
-
-        return alg.lerp(a, b, t if self.progress is None else self.progress(t))
-
-
-class Piecewise(namedtuple('Piecewise', ['color', 'stop', 'progress', 'hue', 'premultiplied'])):
-    """Piecewise interpolation input."""
-
-    __slots__ = ()
-
-    def __new__(
-        cls,
-        color: ColorInput,
-        stop: Optional[float] = None,
-        progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]] = None,
-        hue: Optional[str] = None,
-        premultiplied: Optional[bool] = None
-    ) -> 'Piecewise':
-        """Initialize."""
-
-        return super().__new__(cls, color, stop, progress, hue, premultiplied)
-
-
-class Interpolator(metaclass=ABCMeta):
-    """Interpolator."""
-
-    @abstractmethod
-    def __init__(self) -> None:
-        """Initialize."""
-
-    @abstractmethod
-    def __call__(self, p: float) -> 'Color':
-        """Call the interpolator."""
-
-    def steps(
-        self,
-        steps: int = 2,
-        max_steps: int = 1000,
-        max_delta_e: float = 0,
-        delta_e: Optional[str] = None
-    ) -> List['Color']:
-        """Steps."""
-
-        return color_steps(self, steps, max_steps, max_delta_e, delta_e)
-
-
-class InterpolateSingle(Interpolator):
-    """Interpolate a single range of two colors."""
-
-    def __init__(
-        self,
-        channels1: Vector,
-        channels2: Vector,
-        names: Sequence[str],
-        create: Type['Color'],
-        progress: Optional[Union[Callable[..., float], Mapping[str, Callable[..., float]]]],
-        space: str,
-        outspace: str,
-        premultiplied: bool
-    ) -> None:
-        """Initialize."""
-
-        self.names = names
-        self.channels1 = channels1
-        self.channels2 = channels2
-        self.create = create
-        self.progress = progress
-        self.space = space
-        self.outspace = outspace
-        self.premultiplied = premultiplied
-
-    def __call__(self, p: float) -> 'Color':
-        """Run through the coordinates and run the interpolation on them."""
-
-        channels = []
-        for i, c1 in enumerate(self.channels1):
-            name = self.names[i]
-            c2 = self.channels2[i]
-            if alg.is_nan(c1) and alg.is_nan(c2):
-                value = alg.NaN
-            elif alg.is_nan(c1):
-                value = c2
-            elif alg.is_nan(c2):
-                value = c1
-            else:
-                progress = None
-                if isinstance(self.progress, Mapping):
-                    progress = self.progress.get(name)
-                    if progress is None:
-                        progress = self.progress.get('all')
-                else:
-                    progress = self.progress
-                lerper = progress if isinstance(progress, Lerp) else Lerp(progress)
-                value = lerper(c1, c2, p)
-            channels.append(value)
-        color = self.create(self.space, channels[:-1], channels[-1])
-        if self.premultiplied:
-            postdivide(color)
-        return color.convert(self.outspace, in_place=True) if self.outspace != color.space() else color
-
-
-class InterpolatePiecewise(Interpolator):
-    """Interpolate multiple ranges of colors."""
-
-    def __init__(self, stops: Dict[int, float], interpolators: List[InterpolateSingle]):
-        """Initialize."""
-
-        self.start = stops[0]
-        self.end = stops[len(stops) - 1]
-        self.stops = stops
-        self.interpolators = interpolators
-
-    def __call__(self, p: float) -> 'Color':
-        """Interpolate."""
-
-        percent = p
-        if percent > self.end:
-            # Beyond range, just interpolate the last colors
-            return self.interpolators[-1](1 + abs(p - self.end) if p > 1 else 1)
-
-        elif percent < self.start:
-            # Beyond range, just interpolate the last colors
-            return self.interpolators[0](0 - abs(self.start - p) if p < 0 else 0)
-
-        else:
-            last = self.start
-            for i, interpolator in enumerate(self.interpolators, 1):
-                stop = self.stops[i]
-                if percent <= stop:
-                    r = stop - last
-                    p2 = (percent - last) / r if r else 1
-                    return interpolator(p2)
-                last = stop
-
-            # We shouldn't ever hit this, but provided for typing.
-            # If we do hit this, it would be a bug.
-            raise RuntimeError('Iterpolation could not be found for {}'.format(percent))  # pragma: no cover
-
-
-def calc_stops(stops: Dict[int, float], count: int) -> Dict[int, float]:
-    """Calculate stops."""
-
-    # Ensure the first stop is set to zero if not explicitly set
-    if 0 not in stops:
-        stops[0] = 0
-
-    last = stops[0] * 100
-    highest = last
-    empty = None
-    final = {}
-
-    # Build up normalized stops
-    for i in range(count):
-        value = stops.get(i)
-        if value is not None:
-            value *= 100
-
-        # Found an empty hole, track the start
-        if value is None and empty is None:
-            empty = i - 1
-            continue
-        elif value is None:
-            continue
-
-        # We can't have a stop decrease in progression
-        if value < last:
-            value = last
-
-        # Track the largest explicit value set
-        if value > highest:
-            highest = value
-
-        # Fill in hole if one exists.
-        # Holes will be evenly space between the
-        # current and last stop.
-        if empty is not None:
-            r = i - empty
-            increment = (value - last) / r
-            for j in range(empty + 1, i):
-                last += increment
-                final[j] = last / 100
-            empty = None
-
-        # Set the stop and track it as the last
-        last = value
-        final[i] = last / 100
-
-    # If there is a hole at the end, fill in the hole,
-    # equally spacing the stops from the last to 100%.
-    # If the last is greater than 100%, then all will
-    # be equal to the last.
-    if empty is not None:
-        r = (count - 1) - empty
-        if highest > 100:
-            increment = 0
-        else:
-            increment = (100 - last) / r
-        for j in range(empty + 1, count):
-            last += increment
-            final[j] = last / 100
-
-    return final
-
-
-def process_mapping(
-    progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
-    aliases: Mapping[str, str]
-) -> Optional[Union[Callable[..., float], Mapping[str, Callable[..., float]]]]:
-    """Process a mapping, such that it is not using aliases."""
-
-    if not isinstance(progress, Mapping):
-        return progress
-    return {aliases.get(k, k): v for k, v in progress.items()}
-
-
-def postdivide(color: 'Color') -> None:
-    """Premultiply the given transparent color."""
-
-    alpha = color[-1]
-
-    if alg.is_nan(alpha) or alpha in (0.0, 1.0):
-        return
-
-    channels = color._space.CHANNELS
-    for i, value in enumerate(color[:-1]):
-
-        # Wrap the angle
-        if channels[i].flags & FLG_ANGLE:
-            continue
-        color[i] = value / alpha
-
-
-def premultiply(color: 'Color') -> None:
-    """Premultiply the given transparent color."""
-
-    alpha = color[-1]
-
-    if alg.is_nan(alpha) or alpha == 1.0:
-        return
-
-    channels = color._space.CHANNELS
-    for i, value in enumerate(color[:-1]):
-
-        # Wrap the angle
-        if channels[i].flags & FLG_ANGLE:
-            continue
-        color[i] = value * alpha
-
-
-def adjust_hues(color1: 'Color', color2: 'Color', hue: str) -> None:
-    """Adjust hues."""
-
-    if hue == "specified":
-        return
-
-    name = cast(Type[Cylindrical], color1._space).hue_name()
-    c1 = color1.get(name)
-    c2 = color2.get(name)
-
-    c1 = c1 % 360
-    c2 = c2 % 360
-
-    if alg.is_nan(c1) or alg.is_nan(c2):
-        color1.set(name, c1)
-        color2.set(name, c2)
-        return
-
-    if hue == "shorter":
-        if c2 - c1 > 180:
-            c1 += 360
-        elif c2 - c1 < -180:
-            c2 += 360
-
-    elif hue == "longer":
-        if 0 < (c2 - c1) < 180:
-            c1 += 360
-        elif -180 < (c2 - c1) <= 0:
-            c2 += 360
-
-    elif hue == "increasing":
-        if c2 < c1:
-            c2 += 360
-
-    elif hue == "decreasing":
-        if c1 < c2:
-            c1 += 360
-
-    else:
-        raise ValueError("Unknown hue adjuster '{}'".format(hue))
-
-    color1.set(name, c1)
-    color2.set(name, c2)
-
-
-def color_steps(
-    interpolator: Interpolator,
-    steps: int = 2,
-    max_steps: int = 1000,
-    max_delta_e: float = 0,
-    delta_e: Optional[str] = None
-) -> List['Color']:
-    """Color steps."""
-
-    actual_steps = steps
-
-    # Allocate at least two steps if we are doing a maximum delta E,
-    if max_delta_e != 0 and actual_steps < 2:
-        actual_steps = 2
-
-    # Make sure we don't start out allocating too many colors
-    if max_steps is not None:
-        actual_steps = min(actual_steps, max_steps)
-
-    ret = []
-    if actual_steps == 1:
-        ret = [{"p": 0.5, "color": interpolator(0.5)}]
-    elif actual_steps > 1:
-        step = 1 / (actual_steps - 1)
-        for i in range(actual_steps):
-            p = i * step
-            ret.append({'p': p, 'color': interpolator(p)})
-
-    # Iterate over all the stops inserting stops in between all colors
-    # if we have any two colors with a max delta greater than what was requested.
-    # We inject between every stop to ensure the midpoint does not shift.
-    if max_delta_e > 0:
-        # Initial check to see if we need to insert more stops
-        m_delta = 0.0
-        for i in range(1, len(ret)):
-            m_delta = max(
-                m_delta,
-                cast('Color', ret[i - 1]['color']).delta_e(
-                    cast('Color', ret[i]['color']),
-                    method=delta_e
-                )
-            )
-
-        # If we currently have delta over our limit inject more stops.
-        # If inserting between every color would push us over the max_steps, halt.
-        while m_delta > max_delta_e and (len(ret) * 2 - 1 <= max_steps):
-            # Inject stops while measuring again to see if it was sufficient
-            m_delta = 0.0
-            i = 1
-            offset = 0
-            for i in range(1, len(ret)):
-                index = i + offset
-                prev = ret[index - 1]
-                cur = ret[index]
-                p = (cast(float, cur['p']) + cast(float, prev['p'])) / 2
-                color = interpolator(p)
-                m_delta = max(
-                    m_delta,
-                    color.delta_e(cast('Color', prev['color']), method=delta_e),
-                    color.delta_e(cast('Color', cur['color']), method=delta_e)
-                )
-                ret.insert(index, {'p': p, 'color': color})
-                offset += 1
-
-    return [cast('Color', i['color']) for i in ret]
-
-
-def color_piecewise_lerp(
-    pw: List[Union[ColorInput, Piecewise]],
-    space: str,
-    out_space: str,
-    progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
-    hue: str,
-    premultiplied: bool
-) -> InterpolatePiecewise:
-    """Piecewise Interpolation."""
-
-    # Ensure we have something we can interpolate with
-    count = len(pw)
-    if count == 1:
-        pw.append(pw[0])
-        count += 1
-
-    # Construct piecewise interpolation object
-    stops = {}
-    color_map = []
-    for i, x in enumerate(pw, 0):
-
-        # Normalize all colors as Piecewise objects
-        if isinstance(x, Piecewise):
-            stops[i] = x.stop
-            p = x
-        else:
-            p = Piecewise(x)
-
-        # The first is the calling color which is already a Color object
-        if i == 0:
-            current = p.color
-            continue
-
-        # Ensure input provided via Piecewise object is a Color object
-        color = current._handle_color_input(p.color)
-
-        # Create an entry interpolating the current color and the next color
-        color_map.append(
-            current.interpolate(
-                color,
-                space=space,
-                out_space=out_space,
-                progress=p.progress if p.progress is not None else progress,
-                hue=p.hue if p.hue is not None else hue,
-                premultiplied=p.premultiplied if p.premultiplied is not None else premultiplied
-            )
-        )
-
-        # The "next" color is now the "current" color
-        current = color
-
-    # Calculate stops
-    stops = calc_stops(stops, count)
-
-    # Send the interpolation list along with the stop map to the Piecewise interpolator
-    return InterpolatePiecewise(stops, color_map)
-
-
-def color_lerp(
-    color1: 'Color',
-    color2: ColorInput,
-    space: str,
-    out_space: str,
-    progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
-    hue: str,
-    premultiplied: bool
-) -> InterpolateSingle:
-    """Color interpolation."""
-
-    # Convert to the color space and ensure the color fits inside
-    color1 = color1.convert(space)
-    color2 = color1._handle_color_input(color2).convert(space)
-    if not color1.CS_MAP[space].EXTENDED_RANGE:
-        if not color1.in_gamut():
-            color1.fit()
-        if not color2.in_gamut():
-            color2.fit()
-
-    # Adjust hues if we have two valid hues
-    if issubclass(color1._space, Cylindrical):
-        adjust_hues(color1, color2, hue)
-
-    if premultiplied:
-        premultiply(color1)
-        premultiply(color2)
-
-    channels1 = color1[:-1]
-    channels2 = color2[:-1]
-
-    # Include alpha
-    channels1.append(color1[-1])
-    channels2.append(color2[-1])
-
-    return InterpolateSingle(
-        channels1=channels1,
-        channels2=channels2,
-        names=color1._space.CHANNELS + ('alpha',),
-        create=type(color1),
-        progress=process_mapping(progress, color1._space.CHANNEL_ALIASES),
-        space=space,
-        outspace=out_space,
-        premultiplied=premultiplied
-    )
diff --git a/coloraide/interpolate/__init__.py b/coloraide/interpolate/__init__.py
new file mode 100644
index 000000000..0d9c38367
--- /dev/null
+++ b/coloraide/interpolate/__init__.py
@@ -0,0 +1,36 @@
+"""
+Interpolation methods.
+
+Originally, the base code for `interpolate`, `mix` and `steps` was ported from the
+https://colorjs.io project. Since that time, there has been significant modifications
+that add additional features etc. The base logic though is attributed to the original
+authors.
+
+In general, the logic mimics in many ways the `color-mix` function as outlined in the Level 5
+color draft (Oct 2020), but the initial approach was modeled directly off of the work done in
+color.js.
+---
+Original Authors: Lea Verou, Chris Lilley
+License: MIT (As noted in https://github.com/LeaVerou/color.js/blob/master/package.json)
+"""
+from .bezier import color_bezier_lerp
+from .piecewise import color_piecewise_lerp
+from .common import Interpolator, hint, stop  # noqa: F401
+from typing import Callable, Dict
+
+__all__ = ('stop', 'hint', 'get_interpolator')
+
+
+SUPPORTED = {
+    "linear": color_piecewise_lerp,
+    "bezier": color_bezier_lerp
+}  # type: Dict[str, Callable[..., Interpolator]]
+
+
+def get_interpolator(interpolator: str) -> Callable[..., Interpolator]:
+    """Get desired blend mode."""
+
+    try:
+        return SUPPORTED[interpolator]
+    except KeyError:
+        raise ValueError("'{}' is not a recognized interpolator".format(interpolator))
diff --git a/coloraide/interpolate/bezier.py b/coloraide/interpolate/bezier.py
new file mode 100644
index 000000000..a19b4bd1e
--- /dev/null
+++ b/coloraide/interpolate/bezier.py
@@ -0,0 +1,248 @@
+"""Bezier interpolation."""
+from .. import algebra as alg
+from ..spaces import Cylindrical
+from ..types import Vector, ColorInput
+from typing import Optional, Callable, Sequence, Mapping, Type, Dict, List, Union, cast, Any, TYPE_CHECKING
+from .common import stop, Interpolator, calc_stops, process_mapping, premultiply, postdivide
+
+if TYPE_CHECKING:  # pragma: no cover
+    from ..color import Color
+
+
+def binomial_row(n: int) -> List[int]:
+    """
+    Binomial row.
+
+    Return row in Pascal's triangle.
+    """
+
+    row = [1, 1]
+    for i in range(n - 1):
+        r = [1]
+        x = 0
+        for x in range(1, len(row)):
+            r.append(row[x] + row[x - 1])
+        r.append(row[x])
+        row = r
+    return row
+
+
+class InterpolateBezier(Interpolator):
+    """Interpolate Bezier."""
+
+    def __init__(
+        self,
+        coordinates: List[Vector],
+        names: Sequence[str],
+        create: Type['Color'],
+        easings: List[Optional[Callable[..., float]]],
+        stops: Dict[int, float],
+        space: str,
+        out_space: str,
+        progress: Optional[Union[Callable[..., float], Mapping[str, Callable[..., float]]]],
+        premultiplied: bool
+    ) -> None:
+        """Initialize."""
+
+        self.start = stops[0]
+        self.end = stops[len(stops) - 1]
+        self.stops = stops
+        self.easings = easings
+        self.coordinates = coordinates
+        self.length = len(self.coordinates)
+        self.names = names
+        self.create = create
+        self.progress = progress
+        self.space = space
+        self.out_space = out_space
+        self.premultiplied = premultiplied
+
+    def handle_undefined(self, coords: Vector) -> Vector:
+        """Handle null values."""
+
+        backfill = None
+        for x in range(1, len(coords)):
+            a = coords[x - 1]
+            b = coords[x]
+            if alg.is_nan(a) and not alg.is_nan(b):
+                coords[x - 1] = b
+            elif alg.is_nan(b) and not alg.is_nan(a):
+                coords[x] = a
+            elif alg.is_nan(a) and alg.is_nan(b):
+                # Multiple undefined values, mark the start
+                backfill = x - 1
+                continue
+
+            # Replace all undefined values that occurred prior to
+            # finding the current defined value
+            if backfill is not None:
+                coords[backfill:x - 1] = [b] * (x - 1 - backfill)
+                backfill = None
+
+        return coords
+
+    def interpolate(
+        self,
+        easing: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
+        p2: float,
+        first: float,
+        last: float
+    ) -> 'Color':
+        """Interpolate."""
+
+        n = self.length - 1
+        row = binomial_row(n)
+        channels = []
+        for i, coords in enumerate(zip(*self.coordinates)):
+            name = self.names[i]
+            progress = None
+            if isinstance(easing, Mapping):
+                progress = easing.get(name)
+                if progress is None:
+                    progress = easing.get('all')
+            else:
+                progress = easing
+
+            # Apply easing and scale properly between the colors
+            t = alg.clamp(p2 if progress is None else progress(p2), 0.0, 1.0)
+            t = t * (last - first) + first
+
+            # Find new points using a bezier curve
+            x = 1 - t
+            s = 0.0
+            for j, c in enumerate(self.handle_undefined(list(coords)), 0):
+                s += row[j] * (x ** (n - j)) * (t ** j) * c
+
+            channels.append(s)
+        color = self.create(self.space, channels[:-1], channels[-1])
+        if self.premultiplied:
+            postdivide(color)
+        return color.convert(self.out_space, in_place=True) if self.out_space != color.space() else color
+
+    def __call__(self, p: float) -> 'Color':
+        """Interpolate."""
+
+        percent = alg.clamp(p, 0.0, 1.0)
+        if percent > self.end:
+            percent = self.end
+        elif percent < self.start:
+            percent = self.start
+        last = self.start
+        for i in range(1, self.length):
+            s = self.stops[i]
+            if percent <= s:
+                r = s - last
+                p2 = (percent - last) / r if r else 1
+                easing = self.easings[i - 1]  # type: Any
+                if easing is None:
+                    easing = self.progress
+                piece = 1 / (self.length - 1)
+                return self.interpolate(easing, p2, (i - 1) * piece, i * piece)
+            last = s
+
+        # We shouldn't ever hit this, but provided for typing.
+        # If we do hit this, it would be a bug.
+        raise RuntimeError('Iterpolation could not be found for {}'.format(percent))  # pragma: no cover
+
+
+def normalize_color(color: 'Color', space: str, premultiplied: bool) -> None:
+    """Normalize color."""
+
+    # Adjust to color to space and ensure it fits
+    if not color.CS_MAP[space].EXTENDED_RANGE:
+        if not color.in_gamut():
+            color.fit()
+
+    # Premultiply
+    if premultiplied:
+        premultiply(color)
+
+    # Normalize hue
+    if issubclass(color._space, Cylindrical):
+        name = cast(Type[Cylindrical], color._space).hue_name()
+        color.set(name, lambda h: cast(float, h % 360))
+
+
+def color_bezier_lerp(
+    create: Type['Color'],
+    colors: List[ColorInput],
+    space: str,
+    out_space: str,
+    progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
+    premultiplied: bool,
+    **kwargs: Any
+) -> InterpolateBezier:
+    """Bezier interpolation."""
+
+    # Construct piecewise interpolation object
+    stops = {}  # type: Any
+
+    if space is None:
+        space = create.INTERPOLATE
+
+    if isinstance(colors[0], stop):
+        current = create(colors[0].color)
+        stops[0] = colors[0].stop
+    elif not callable(colors[0]):
+        current = create(colors[0])
+        stops[0] = None
+    else:
+        raise ValueError('Cannot have an easing function as the first item in an interpolation list')
+
+    if out_space is None:
+        out_space = current.space()
+
+    current.convert(space, in_place=True)
+    normalize_color(current, space, premultiplied)
+
+    easing = None  # type: Any
+    easings = []  # type: Any
+    coords = [current[:]]
+
+    i = 0
+    for x in colors[1:]:
+
+        # Normalize all colors as Piecewise objects
+        if isinstance(x, stop):
+            i += 1
+            stops[i] = x.stop
+            color = current._handle_color_input(x.color)
+        elif callable(x):
+            easing = x
+            continue
+        else:
+            i += 1
+            color = current._handle_color_input(x)
+            stops[i] = None
+
+        # Adjust to color to space and ensure it fits
+        color = color.convert(space)
+        normalize_color(color, space, premultiplied)
+
+        # Create an entry interpolating the current color and the next color
+        coords.append(color[:])
+        easings.append(easing if easing is not None else progress)
+
+        # The "next" color is now the "current" color
+        easing = None
+        current = color
+
+    i += 1
+    if i < 2:
+        raise ValueError('Need at least two colors to interpolate')
+
+    # Calculate stops
+    stops = calc_stops(stops, i)
+
+    # Send the interpolation list along with the stop map to the Piecewise interpolator
+    return InterpolateBezier(
+        coords,
+        [str(c) for c in current._space.get_all_channels()],
+        create,
+        easings,
+        stops,
+        space,
+        out_space,
+        process_mapping(progress, current._space.CHANNEL_ALIASES),
+        premultiplied
+    )
diff --git a/coloraide/interpolate/common.py b/coloraide/interpolate/common.py
new file mode 100644
index 000000000..7ab13ea12
--- /dev/null
+++ b/coloraide/interpolate/common.py
@@ -0,0 +1,235 @@
+"""Common tools."""
+import math
+import functools
+from abc import ABCMeta, abstractmethod
+from .. import algebra as alg
+from ..channels import FLG_ANGLE
+from ..types import ColorInput
+from typing import Optional, Callable, Mapping, Dict, List, Union, cast, TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover
+    from ..color import Color
+
+
+def midpoint(t: float, h: float = 0.5) -> float:
+    """Midpoint easing function."""
+
+    return 0.0 if h <= 0 or h >= 1 else math.pow(t, math.log(0.5) / math.log(h))
+
+
+def hint(mid: float) -> Callable[..., float]:
+    """A generate a midpoint easing function."""
+
+    return functools.partial(midpoint, h=mid)
+
+
+class stop:
+    """Color stop."""
+
+    __slots__ = ('color', 'stop')
+
+    def __init__(self, color: ColorInput, value: float) -> None:
+        """Color stops."""
+
+        self.color = color
+        self.stop = value
+
+
+class Interpolator(metaclass=ABCMeta):
+    """Interpolator."""
+
+    @abstractmethod
+    def __init__(self) -> None:
+        """Initialize."""
+
+    @abstractmethod
+    def __call__(self, p: float) -> 'Color':
+        """Call the interpolator."""
+
+    def steps(
+        self,
+        steps: int = 2,
+        max_steps: int = 1000,
+        max_delta_e: float = 0,
+        delta_e: Optional[str] = None
+    ) -> List['Color']:
+        """Steps."""
+
+        return color_steps(self, steps, max_steps, max_delta_e, delta_e)
+
+
+def calc_stops(stops: Dict[int, float], count: int) -> Dict[int, float]:
+    """Calculate stops."""
+
+    # Ensure the first stop is set to zero if not explicitly set
+    if 0 not in stops or stops[0] is None:
+        stops[0] = 0
+
+    last = stops[0] * 100
+    highest = last
+    empty = None
+    final = {}
+
+    # Build up normalized stops
+    for i in range(count):
+        value = stops.get(i)
+        if value is not None:
+            value *= 100
+
+        # Found an empty hole, track the start
+        if value is None and empty is None:
+            empty = i - 1
+            continue
+        elif value is None:
+            continue
+
+        # We can't have a stop decrease in progression
+        if value < last:
+            value = last
+
+        # Track the largest explicit value set
+        if value > highest:
+            highest = value
+
+        # Fill in hole if one exists.
+        # Holes will be evenly space between the
+        # current and last stop.
+        if empty is not None:
+            r = i - empty
+            increment = (value - last) / r
+            for j in range(empty + 1, i):
+                last += increment
+                final[j] = last / 100
+            empty = None
+
+        # Set the stop and track it as the last
+        last = value
+        final[i] = last / 100
+
+    # If there is a hole at the end, fill in the hole,
+    # equally spacing the stops from the last to 100%.
+    # If the last is greater than 100%, then all will
+    # be equal to the last.
+    if empty is not None:
+        r = (count - 1) - empty
+        if highest > 100:
+            increment = 0
+        else:
+            increment = (100 - last) / r
+        for j in range(empty + 1, count):
+            last += increment
+            final[j] = last / 100
+
+    return final
+
+
+def process_mapping(
+    progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
+    aliases: Mapping[str, str]
+) -> Optional[Union[Callable[..., float], Mapping[str, Callable[..., float]]]]:
+    """Process a mapping, such that it is not using aliases."""
+
+    if not isinstance(progress, Mapping):
+        return progress
+    return {aliases.get(k, k): v for k, v in progress.items()}
+
+
+def postdivide(color: 'Color') -> None:
+    """Premultiply the given transparent color."""
+
+    alpha = color[-1]
+
+    if alg.is_nan(alpha) or alpha in (0.0, 1.0):
+        return
+
+    channels = color._space.CHANNELS
+    for i, value in enumerate(color[:-1]):
+
+        # Wrap the angle
+        if channels[i].flags & FLG_ANGLE:
+            continue
+        color[i] = value / alpha
+
+
+def premultiply(color: 'Color') -> None:
+    """Premultiply the given transparent color."""
+
+    alpha = color[-1]
+
+    if alg.is_nan(alpha) or alpha == 1.0:
+        return
+
+    channels = color._space.CHANNELS
+    for i, value in enumerate(color[:-1]):
+
+        # Wrap the angle
+        if channels[i].flags & FLG_ANGLE:
+            continue
+        color[i] = value * alpha
+
+
+def color_steps(
+    interpolator: Interpolator,
+    steps: int = 2,
+    max_steps: int = 1000,
+    max_delta_e: float = 0,
+    delta_e: Optional[str] = None
+) -> List['Color']:
+    """Color steps."""
+
+    actual_steps = steps
+
+    # Allocate at least two steps if we are doing a maximum delta E,
+    if max_delta_e != 0 and actual_steps < 2:
+        actual_steps = 2
+
+    # Make sure we don't start out allocating too many colors
+    if max_steps is not None:
+        actual_steps = min(actual_steps, max_steps)
+
+    ret = []
+    if actual_steps == 1:
+        ret = [{"p": 0.5, "color": interpolator(0.5)}]
+    elif actual_steps > 1:
+        step = 1 / (actual_steps - 1)
+        for i in range(actual_steps):
+            p = i * step
+            ret.append({'p': p, 'color': interpolator(p)})
+
+    # Iterate over all the stops inserting stops in between all colors
+    # if we have any two colors with a max delta greater than what was requested.
+    # We inject between every stop to ensure the midpoint does not shift.
+    if max_delta_e > 0:
+        # Initial check to see if we need to insert more stops
+        m_delta = 0.0
+        for i in range(1, len(ret)):
+            m_delta = max(
+                m_delta,
+                cast('Color', ret[i - 1]['color']).delta_e(
+                    cast('Color', ret[i]['color']),
+                    method=delta_e
+                )
+            )
+
+        # If we currently have delta over our limit inject more stops.
+        # If inserting between every color would push us over the max_steps, halt.
+        while m_delta > max_delta_e and (len(ret) * 2 - 1 <= max_steps):
+            # Inject stops while measuring again to see if it was sufficient
+            m_delta = 0.0
+            i = 1
+            offset = 0
+            for i in range(1, len(ret)):
+                index = i + offset
+                prev = ret[index - 1]
+                cur = ret[index]
+                p = (cast(float, cur['p']) + cast(float, prev['p'])) / 2
+                color = interpolator(p)
+                m_delta = max(
+                    m_delta,
+                    color.delta_e(cast('Color', prev['color']), method=delta_e),
+                    color.delta_e(cast('Color', cur['color']), method=delta_e)
+                )
+                ret.insert(index, {'p': p, 'color': color})
+                offset += 1
+
+    return [cast('Color', i['color']) for i in ret]
diff --git a/coloraide/interpolate/piecewise.py b/coloraide/interpolate/piecewise.py
new file mode 100644
index 000000000..45f242ebd
--- /dev/null
+++ b/coloraide/interpolate/piecewise.py
@@ -0,0 +1,263 @@
+"""Piecewise linear interpolation."""
+from .. import algebra as alg
+from ..spaces import Cylindrical
+from ..types import Vector, ColorInput
+from typing import Optional, Callable, Sequence, Mapping, Type, Dict, List, Union, cast, Tuple, Any, TYPE_CHECKING
+from .common import stop, Interpolator, calc_stops, process_mapping, premultiply, postdivide
+
+if TYPE_CHECKING:  # pragma: no cover
+    from ..color import Color
+
+
+def adjust_hues(color1: 'Color', color2: 'Color', hue: str) -> None:
+    """Adjust hues."""
+
+    if hue == "specified":
+        return
+
+    name = cast(Type[Cylindrical], color1._space).hue_name()
+    c1 = color1.get(name)
+    c2 = color2.get(name)
+
+    c1 = c1 % 360
+    c2 = c2 % 360
+
+    if alg.is_nan(c1) or alg.is_nan(c2):
+        color1.set(name, c1)
+        color2.set(name, c2)
+        return
+
+    if hue == "shorter":
+        if c2 - c1 > 180:
+            c1 += 360
+        elif c2 - c1 < -180:
+            c2 += 360
+
+    elif hue == "longer":
+        if 0 < (c2 - c1) < 180:
+            c1 += 360
+        elif -180 < (c2 - c1) <= 0:
+            c2 += 360
+
+    elif hue == "increasing":
+        if c2 < c1:
+            c2 += 360
+
+    elif hue == "decreasing":
+        if c1 < c2:
+            c1 += 360
+
+    else:
+        raise ValueError("Unknown hue adjuster '{}'".format(hue))
+
+    color1.set(name, c1)
+    color2.set(name, c2)
+
+
+class Lerp:
+    """Linear interpolation."""
+
+    def __init__(self, progress: Optional[Callable[..., float]]) -> None:
+        """Initialize."""
+
+        self.progress = progress
+
+    def __call__(self, a: float, b: float, t: float) -> float:
+        """Interpolate with period."""
+
+        if self.progress is not None:
+            t = alg.clamp(self.progress(t), 0.0, 1.0)
+
+        return alg.lerp(a, b, t)
+
+
+class InterpolatePiecewise(Interpolator):
+    """Interpolate multiple ranges of colors."""
+
+    def __init__(
+        self,
+        color_map: List[Tuple[Vector, Vector]],
+        names: Sequence[str],
+        create: Type['Color'],
+        easings: List[Optional[Callable[..., float]]],
+        stops: Dict[int, float],
+        space: str,
+        out_space: str,
+        progress: Optional[Union[Callable[..., float], Mapping[str, Callable[..., float]]]],
+        premultiplied: bool
+    ):
+        """Initialize."""
+
+        self.start = stops[0]
+        self.end = stops[len(stops) - 1]
+        self.stops = stops
+        self.color_map = color_map
+        self.names = names
+        self.create = create
+        self.easings = easings
+        self.space = space
+        self.out_space = out_space
+        self.progress = progress
+        self.premultiplied = premultiplied
+
+    def interpolate(
+        self,
+        colors: Tuple[Vector, Vector],
+        easing: Optional[Union[Callable[..., float], Mapping[str, Callable[..., float]]]],
+        p: float
+    ) -> 'Color':
+        """Interpolate."""
+
+        channels = []
+        for i, values in enumerate(zip(*colors)):
+            c1, c2 = values
+            name = self.names[i]
+            if alg.is_nan(c1) and alg.is_nan(c2):
+                value = alg.NaN
+            elif alg.is_nan(c1):
+                value = c2
+            elif alg.is_nan(c2):
+                value = c1
+            else:
+                progress = None
+                if isinstance(easing, Mapping):
+                    progress = easing.get(name)
+                    if progress is None:
+                        progress = easing.get('all')
+                else:
+                    progress = easing
+                lerper = progress if isinstance(progress, Lerp) else Lerp(progress)
+                value = lerper(c1, c2, p)
+            channels.append(value)
+        color = self.create(self.space, channels[:-1], channels[-1])
+        if self.premultiplied:
+            postdivide(color)
+        return color.convert(self.out_space, in_place=True) if self.out_space != color.space() else color
+
+    def __call__(self, p: float) -> 'Color':
+        """Interpolate."""
+
+        percent = alg.clamp(p, 0.0, 1.0)
+        if percent > self.end:
+            percent = self.end
+        elif percent < self.start:
+            percent = self.start
+        last = self.start
+        for i, colors in enumerate(self.color_map, 1):
+            s = self.stops[i]
+            if percent <= s:
+                r = s - last
+                p2 = (percent - last) / r if r else 1
+                easing = self.easings[i - 1]  # type: Any
+                if easing is None:
+                    easing = self.progress
+                return self.interpolate(colors, easing, p2)
+            last = s
+
+        # We shouldn't ever hit this, but provided for typing.
+        # If we do hit this, it would be a bug.
+        raise RuntimeError('Iterpolation could not be found for {}'.format(percent))  # pragma: no cover
+
+
+def normalize_color(color: 'Color', space: str, premultiplied: bool) -> None:
+    """Normalize the color."""
+
+    # Adjust to color to space and ensure it fits
+    if not color.CS_MAP[space].EXTENDED_RANGE:
+        if not color.in_gamut():
+            color.fit()
+
+    # Premultiply
+    if premultiplied:
+        premultiply(color)
+
+
+def color_piecewise_lerp(
+    create: Type['Color'],
+    colors: List[Union[ColorInput, stop, Callable[..., float]]],
+    space: str,
+    out_space: str,
+    progress: Optional[Union[Mapping[str, Callable[..., float]], Callable[..., float]]],
+    hue: str,
+    premultiplied: bool,
+    **kwargs: Any
+) -> InterpolatePiecewise:
+    """Piecewise Interpolation."""
+
+    # Construct piecewise interpolation object
+    stops = {}  # type: Any
+    color_map = []
+
+    if space is None:
+        space = create.INTERPOLATE
+
+    if isinstance(colors[0], stop):
+        current = create(colors[0].color)
+        stops[0] = colors[0].stop
+    elif not callable(colors[0]):
+        current = create(colors[0])
+        stops[0] = None
+    else:
+        raise ValueError('Cannot have an easing function as the first item in an interpolation list')
+
+    if out_space is None:
+        out_space = current.space()
+
+    current.convert(space, in_place=True)
+    normalize_color(current, space, premultiplied)
+
+    easing = None  # type: Any
+    easings = []  # type: Any
+
+    i = 0
+    for x in colors[1:]:
+
+        # Normalize all colors as Piecewise objects
+        if isinstance(x, stop):
+            i += 1
+            stops[i] = x.stop
+            color = current._handle_color_input(x.color)
+        elif callable(x):
+            easing = x
+            continue
+        else:
+            i += 1
+            color = current._handle_color_input(x)
+            stops[i] = None
+
+        # Adjust to color to space and ensure it fits
+        color = color.convert(space)
+        normalize_color(color, space, premultiplied)
+
+        # Adjust hues if we have two valid hues
+        color2 = color.clone()
+        if issubclass(current._space, Cylindrical):
+            adjust_hues(current, color2, hue)
+
+        # Create an entry interpolating the current color and the next color
+        color_map.append((current[:], color2[:]))
+        easings.append(easing if easing is not None else progress)
+
+        # The "next" color is now the "current" color
+        easing = None
+        current = color
+
+    i += 1
+    if i < 2:
+        raise ValueError('Need at least two colors to interpolate')
+
+    # Calculate stops
+    stops = calc_stops(stops, i)
+
+    # Send the interpolation list along with the stop map to the Piecewise interpolator
+    return InterpolatePiecewise(
+        color_map,
+        [str(c) for c in current._space.get_all_channels()],
+        create,
+        easings,
+        stops,
+        space,
+        out_space,
+        process_mapping(progress, current._space.CHANNEL_ALIASES),
+        premultiplied
+    )
diff --git a/docs/src/dictionary/en-custom.txt b/docs/src/dictionary/en-custom.txt
index dcf3ebba1..ea5d8d2d7 100644
--- a/docs/src/dictionary/en-custom.txt
+++ b/docs/src/dictionary/en-custom.txt
@@ -5,6 +5,7 @@ Achromatopsia
 Alvy
 Az
 BT
+Bezier
 Björn
 Boronine
 Brettel
@@ -144,6 +145,7 @@ achromatomaly
 achromatopsia
 async
 az
+bezier
 boolean
 broadcasted
 bz
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index f0615161e..282b27fab 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -2,6 +2,21 @@
 
 ## 0.19.0
 
+- **BREAK**: Reworked interpolation:
+
+    - `interpolate` and `steps` functions are now `@classmethod`s. This alleviates the awkward handling of interpolating
+      colors greater than 2. Before, the first color always had to be an instance and then the rest had to be fed into
+      that instance, now the the methods can be called from the base class or an instance with all the colors fed in
+      via a list. Only the colors in the list will be evaluated during interpolation.
+    - `Piecewise` object has been removed.
+    - `stop` objects are used to wrap colors to apply a new color stop.
+    - easing functions can be supplied in the middle of two colors via the list input.
+    - `hint` function has been provided to simulate CSS color hinting. `hint` returns an easing function that modifies
+      the midpoint to the specified point between two color stops.
+    - A new bezier interpolation method has been provided. When using `interpolate`, `steps`, or `mix` the interpolation
+      style can be changed via the `method` parameter. `bezier` and `linear` are available with `linear` being the
+      default.
+
 - **BREAK**: Dictionary input/output now matches the following format (where alpha is optional):
 
     ```py
@@ -30,9 +45,9 @@
   the color first: `#!py color.clone().clip()`.
 
 - **NEW**: Update `lch()`, `lab()`, `oklch()`, and `oklab()` to optionally support percentages for lightness, chroma, a,
-  and b. Lightness is no longer enforced to be a percentage in the CSS syntax and these spaces and will serialize as a
-  number by default instead. Optionally, these forms can force a percentage output with via the `to_string` method when
-  using the `percentage` option. Percent ranges roughly correspond with the Display P3 gamut per the CSS specification.
+  and b. Lightness is no longer enforced to be a percentage in the CSS syntax and these spaces will serialize as a
+  number by default instead. Optionally, these forms can force a percentage output via the `to_string` method when using
+  the `percentage` option. Percent ranges roughly correspond with the Display P3 gamut per the CSS specification.
 
   Additionally, CSS color spaces using the `color()` format as an input will translate using these same ranges if the
   channels are percentages. `hue` will also be respected and treated as 0 - 360 when using a percentage.
diff --git a/docs/src/markdown/api/index.md b/docs/src/markdown/api/index.md
index 29e6bc278..defc794e2 100644
--- a/docs/src/markdown/api/index.md
+++ b/docs/src/markdown/api/index.md
@@ -13,41 +13,60 @@ Import path
     from coloraide import NaN
     ```
 
-## `coloraide.Piecewise` {#piecewise}
+## `coloraide.stop` {#stop}
 
 ```py3
-class Piecewise(
-    namedtuple('Piecewise',
-        [
-            'color',
-            'stop',
-            'progress',
-            'hue',
-            'premultiplied'
-        ]
-    )
+class stop(
+    color,
+    s
 ):
 ```
 
 Description
 : 
-    `Piecewise` objects are used in [`interpolate`](#interpolate) methods. They allow a user to control interpolation
-    `stops`, `progress`, `hue`, or `premultiplied` options for a specific interpolation piece when doing piecewise
-    interpolation.
+    `stop` objects are used in [`interpolate`](#interpolate) methods. They allow a user to control specify a color stop
+    for a given color during the interpolation process.
 
 Import Path
 : 
-    `Piecewise` is imported from `coloraide` library:
+    `stop` is imported from `coloraide` library:
 
     ```py3
-    from coloraide import Piecewise
+    from coloraide import stop
     ```
 
 Parameters
 : 
-   Input parameters match [`interpolate`](#interpolate) parameters of the same name. Only `color` is required and all
-   other parameters default to `#!py3 None`. If a parameter is `#!py3 None`, it will be ignored by
-   [`interpolate`](#interpolate).
+    Parameters | Defaults     | Description
+    ---------- | -------------| -----------
+    `color`    |              | A color string, a dictionary describing the color, or another `Color` class object.
+    `value`    |              | A numerical value specifying the new color stop for the given color.
+
+## `coloraide.hint` {#hint}
+
+```py3
+class hint(
+    mid,
+):
+```
+
+Description
+: 
+    `hint` returns an easing function that adjust the midpoint between two color stops.
+
+Import Path
+: 
+    `hint` is imported from `coloraide` library:
+
+    ```py3
+    from coloraide import hint
+    ```
+
+Parameters
+: 
+    Parameters | Defaults     | Description
+    ---------- | -------------| -----------
+    `mid`      |              | A numerical value, relative to the two color stops it occurs between, that will be used as the new midpoint.
 
 ## `coloraide.Color` {#color}
 
@@ -590,16 +609,18 @@ Return
 ## `Color.interpolate` {#interpolate}
 
 ```py3
+@classmethod
 def interpolate(
-    self,
-    color,
+    cls,
+    colors,
     *,
     stop=0,
     space="lab",
     progress=None,
     out_space=None,
     hue=util.DEF_HUE_ADJ,
-    premultiplied=True
+    premultiplied=True,
+    method='linear'
 ):
 ```
 
@@ -614,8 +635,8 @@ Description
     Interpolation can be customized by limiting the interpolation to specific color channels, providing custom
     interpolation functions, and even adjusting the hue logic used.
 
-    [`Piecewise`](#piecewise) objects can be used to specify stops or adjust the interpolation for itself and the
-    preceding color.
+    [`stop`](#stop) objects can wrapped around colors to specify new color stops and easing functions can be placed
+    between colors to alter the transition progress between the two colors.
 
     Hue\ Evaluation | Description
     --------------- | -----------
@@ -625,16 +646,24 @@ Description
     `decreasing`    | Angles are adjusted so that θ₂ - θ₁ ∈ (-360, 0]
     `specified`     | No fixup is performed. Angles are interpolated in the same way as every other component.
 
+    The method of interpolation to can also be selected via the `method` parameter.
+
+    Method   | Description
+    -------- | -----------
+    `linear` | An linear interpolation that employs piecewise logic to interpolate between two or more colors.
+    `bezier` | An interpolation method that employs Bezier curves to calculate an interpolation path through multiple colors.
+
 Parameters
 : 
     Parameters      | Defaults          | Description
     --------------- | ----------------- | -----------
-    `color`         |                   | A color string, [`Color`](#color) object, or [`Piecewise`](#piecewise) object representing a color. Also, multiple can be provided via a list.
+    `color`         |                   | A list of color strings, [`Color`](#color) objects, dictionaries representing a color, [`stop`](#piecewise) objects, or easing functions.
     `space`         | `#!py3 "lab"`     | Color space to interpolate in.
     `progress`      | `#!py3 None`      | An optional function that that allows for custom logic to perform non-linear interpolation.
     `out_space`     | `#!py3 None`      | Color space that the new color should be in. If `#!py3 None`, the color will be in the same color space as the base color.
     `hue`           | `#!py3 "shorter"` | Define how color spaces which have hue angles are interpolated. Default evaluates between the shortest angle.
     `premultiplied` | `#!py3 True`      | Use premultiplied alpha when interpolating.
+    `method`        | `#!py3 "linear"`  | The interpolation method to use.
 
 Return
 : 
@@ -673,7 +702,7 @@ Parameters
 : 
     Parameters                 | Defaults                           | Description
     -------------------------- | ---------------------------------- | -----------
-    `color`                    |                                    | A color string or [`Color`](#color) object representing a color. Also, multiple can be provided via a list.
+    `color`                    |                                    | A list of color strings, [`Color`](#color) objects, dictionaries representing a color, [`stop`](#piecewise) objects, or easing functions.
     `steps`                    | `#!py3 2`                          | Minimum number of steps.
     `max_steps`                | `#!py3 1000`                       | Maximum number of steps.
     `max_delta_e`              | `#!py3 0`                          | Maximum delta E distance between the color stops. A value of `0` or less will be ignored.
diff --git a/docs/src/markdown/filters.md b/docs/src/markdown/filters.md
index 882e1348a..b5948b43a 100644
--- a/docs/src/markdown/filters.md
+++ b/docs/src/markdown/filters.md
@@ -51,7 +51,7 @@ according to the W3C spec will be used instead.
 
 ```playground
 inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
-colors = Color(inputs[0]).steps(inputs[1:], steps=10, space='srgb')
+colors = Color.steps(inputs, steps=10, space='srgb')
 HtmlRow(colors)
 HtmlRow([c.filter('brightness', 0.5).clip() for c in colors])
 HtmlRow([c.filter('saturate', 0.5).clip() for c in colors])
@@ -107,7 +107,7 @@ probably the best approach for all cases, Viénot is much faster and does quite
 
 ```playground
 inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
-colors = Color(inputs[0]).steps(inputs[1:], steps=10, space='srgb')
+colors = Color.steps(inputs, steps=10, space='srgb')
 HtmlRow(colors)
 HtmlRow([c.filter('protan').clip() for c in colors])
 HtmlRow([c.filter('deutan').clip() for c in colors])
@@ -121,7 +121,7 @@ tritanopia.
 
 ```playground
 inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
-colors = Color(inputs[0]).steps(inputs[1:], steps=10, space='srgb')
+colors = Color.steps(inputs, steps=10, space='srgb')
 HtmlRow(colors)
 HtmlRow([c.filter('tritan', method='brettel').clip() for c in colors])
 HtmlRow([c.filter('tritan', method='vienot').clip() for c in colors])
@@ -165,7 +165,7 @@ results. With that said, the `method` can always be overridden to use something
 
 ```playground
 inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
-colors = Color(inputs[0]).steps(inputs[1:], steps=10, space='srgb')
+colors = Color.steps(inputs, steps=10, space='srgb')
 HtmlRow(colors)
 HtmlRow([c.filter('protan', 0.75).clip() for c in colors])
 HtmlRow([c.filter('deutan', 0.75).clip() for c in colors])
@@ -198,7 +198,7 @@ designed to be applied in the Linear sRGB space, and cannot be used in any other
 
 ```playground
 inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
-colors = Color(inputs[0]).steps(inputs[1:], steps=10, space='srgb')
+colors = Color.steps(inputs, steps=10, space='srgb')
 HtmlRow(colors)
 HtmlRow([c.filter('sepia', 1, space='srgb-linear').clip() for c in colors])
 HtmlRow([c.filter('sepia', 1, space='srgb').clip() for c in colors])
diff --git a/docs/src/markdown/gamut.md b/docs/src/markdown/gamut.md
index 24f229e83..93c6b9257 100644
--- a/docs/src/markdown/gamut.md
+++ b/docs/src/markdown/gamut.md
@@ -233,12 +233,12 @@ class ColorClip(Color):
 # Gamut mapping in Lch
 yellow = Color('color(display-p3 1 1 0)')
 lightness_mask = Color('lch(0% none none)')
-HtmlRow(yellow.steps(lightness_mask, steps=10, space='lch'))
+HtmlRow(Color.steps([yellow, lightness_mask], steps=10, space='lch'))
 
 # Clipping
 yellow = ColorClip('color(display-p3 1 1 0)')
 lightness_mask = Color('lch(0% none none)')
-HtmlRow(yellow.steps(lightness_mask, steps=10, space='lch'))
+HtmlRow(Color.steps([yellow, lightness_mask], steps=10, space='lch'))
 ```
 
 There are times when clipping is simply preferred. It is fast, and if you are just trimming noise off channels, it is
diff --git a/docs/src/markdown/harmonies.md b/docs/src/markdown/harmonies.md
index 136d140ad..d78a5bd48 100644
--- a/docs/src/markdown/harmonies.md
+++ b/docs/src/markdown/harmonies.md
@@ -67,9 +67,9 @@ mainly on the fact it keeps hue more consistent than some other options, and it
 options like HSL.
 
 ```playground
-HtmlSteps(Color('black').steps(['blue', 'white'], steps=11, space='oklch'))
-HtmlSteps(Color('black').steps(['blue', 'white'], steps=11, space='hsl'))
-HtmlSteps(Color('black').steps(['blue', 'white'], steps=11, space='lch'))
+HtmlSteps(Color.steps(['black', 'blue', 'white'], steps=11, space='oklch'))
+HtmlSteps(Color.steps(['black', 'blue', 'white'], steps=11, space='hsl'))
+HtmlSteps(Color.steps(['black', 'blue', 'white'], steps=11, space='lch'))
 ```
 
 While Oklch is the default, we understand that there are many reasons to use other spaces, so use what you like, we
diff --git a/docs/src/markdown/index.md b/docs/src/markdown/index.md
index b9c132769..97b0b31d3 100644
--- a/docs/src/markdown/index.md
+++ b/docs/src/markdown/index.md
@@ -11,7 +11,7 @@ ColorAide is a color library for Python with the intent of providing an easy to
 
 ```playground
 from coloraide import Color
-Color('lch(75% 50 0)').steps('lch(75% 50 300)', steps=8, space='lch', hue='longer')
+Color.steps(['lch(75% 50 0)', 'lch(75% 50 300)'], steps=8, space='lch', hue='longer')
 ```
 
 ColorAide is a continually evolving project, but was created with a number of specific goals in mind:
diff --git a/docs/src/markdown/interpolation.md b/docs/src/markdown/interpolation.md
index 5a9f67243..2f0c1dbfc 100644
--- a/docs/src/markdown/interpolation.md
+++ b/docs/src/markdown/interpolation.md
@@ -12,36 +12,36 @@ To further illustrate this point, the example below shows a slice of the Oklab c
 this 2D plane, we select two colors: `#!color oklab(70% 0.2 -0.1)` and `#!color oklab(70% 0 0.1)`. We then connect these
 two colors with a line. We can then select any point on the line to simulate the mixing of these colors. 0% would yield
 the first color, 100% would yield the second color, and 50% would yield a new color:
-`#!color Color('oklab(70% 0.2 -0.1)').interpolate('oklab(70% 0 0.1)')(0.5)`.
+`#!color Color.interpolate(['oklab(70% 0.2 -0.1)', 'oklab(70% 0 0.1)'])(0.5)`.
 
 ![Interpolation Cartesian](images/interp.png)
 
 Using this simple method as a foundation, ColorAide provides a number of useful utilities to mix colors.
 
-## Interpolating
+## Linear Interpolation
 
 The `interpolate` method allows a user to create a linear interpolation function using two or more colors. A returned
 interpolation function accepts an input between 0 - 1 and will cause a new color between the specified colors to be
-returned. If a an input value exceeds the range, a color will be returned that is extrapolated on the imaginary line
-that continues beyond the specified colors which may be surprising.
+returned.
 
 By default, colors are interpolated in the perceptually uniform Oklab color space, though any supported color space can
-be used instead. This also applies to all methods that use interpolation, such as [steps](#steps), [mix](#mixing), etc.
+be used instead. This also applies to all methods that use interpolation, such as [`steps`](#steps), [`mix`](#mixing),
+etc.
 
 As an example, below we create an interpolation between `#!color rebeccapurple` and `#!color lch(85% 100 85)`. We then
 step through values of `0.1`, `0.2`, `0.3`, etc. This returns colors at various positions on the line that connects
 the two colors, `0` returning `#!color rebeccapurple` and `1` returning `#!color lch(85% 100 85)`.
 
 ```playground
-i = Color("rebeccapurple").interpolate("lch(85% 100 85)", space='lch')
-[i(x/10).to_string() for x in range(10)]
+i = Color.interpolate(["rebeccapurple", "lch(85% 100 85)"], space='lch')
+[i(x / 10).to_string() for x in range(10 + 1)]
 ```
 
 If we create enough steps, we can create a gradient.
 
 ```playground
-Color("rebeccapurple").interpolate(
-    "lch(85% 100 85)",
+Color.interpolate(
+    ["rebeccapurple", "lch(85% 100 85)"],
     space='lch'
 )
 ```
@@ -53,15 +53,42 @@ Color("rebeccapurple").interpolate(
     in the requested color space and cannot properly be represented in that space or model, the colors will be gamut
     mapped before interpolation.
 
-Interpolation can also be done across multiple colors. The function, just like when interpolating between two colors,
-takes a range of 0 - 1, only this range now applies to the range that spans all the colors, not just two. As it may be
-impossible to draw a straight line that passes through a series of colors, [piecewise interpolation](#piecewise-interpolation)
-is used at anytime there is more than one color. As this type of interpolation will be cover more later, suffice it to
-say that this method breaks up the interpolation into individual segments/pieces consisting of only two of the colors
-in the series and applies linear interpolation to these segments/pieces individually.
+## Piecewise Interpolation
+
+Interpolation is not just limited to two colors, if more colors are provided, the interpolation will span all the
+colors. The function, just like when interpolating between two colors, still takes a range of 0 - 1, only it will now
+apply to the entire range that spans all the colors.
+
+When using more than two colors, it becomes far less likely that a straight line could pass through all the colors, so
+in order to interpolate between all the colors, [piecewise interpolation](#piecewise-interpolation) is used.
+
+Piecewise interpolation is simply a method of breaking up a data set and performing interpolation over small segments.
+In the context of color, when more than two colors are given, a separate interpolation (in our case linear) is performed
+between each color.
+
+```playground
+Color.interpolate(['black', 'red', 'white'])
+```
+
+This approach generally works well, but since the placement of colors may not be in a straight line, you will often
+have pivot points and the transition may not be quite as smooth.
+
+## Bezier Interpolation
+
+There are many non-linear alternatives for interpolation. One way is to use a Bezier curve instead of using multiple
+straight lines between sets of colors. A Bezier curve will essentially draw a line that roughly follows the shape of the
+points. It is not guarantee that this curve will pass through all the points, but this allows for smoother transitions
+across all the colors.
+
+By simply passing the `method='bezier`, we can interpolate using this method. Below we can see the difference between
+linear and the Bezier method. Notice in the linear interpolation the pivot point where the gradient fully transitions to
+purple and then begins the transition to green  the linear example the the point. The Bezier curve doesn't have the
+pivot point as the curve bends towards purple smoothly, without passing directly through it, and then away towards
+green.
 
 ```playground
-Color('black').interpolate(['red', 'white'])
+Color.interpolate(['orange', 'purple', 'green'])
+Color.interpolate(['orange', 'purple', 'green'], method='bezier')
 ```
 
 ## Hue Interpolation
@@ -72,13 +99,13 @@ of different ways: `shorter`, `longer`, `increasing`, `decreasing`, and `specifi
 interpolation varies using `shorter` vs `longer` (interpolate between the largest angle).
 
 ```playground
-i = Color("lch(52% 58.1 22.7)").interpolate(
-    Color("lch(56% 49.1 257.1)").mask("hue", invert=True),
+i = Color.interpolate(
+    ["lch(52% 58.1 22.7)", Color("lch(56% 49.1 257.1)").mask("hue", invert=True)],
     space="lch"
 )
 i(0.2477).to_string()
-i = Color("lch(52% 58.1 22.7)").interpolate(
-    Color("lch(56% 49.1 257.1)").mask("hue", invert=True),
+i = Color.interpolate(
+    ["lch(52% 58.1 22.7)", Color("lch(56% 49.1 257.1)").mask("hue", invert=True)],
     space="lch",
     hue="longer"
 )
@@ -89,10 +116,14 @@ To help visualize the different hue methods, consider the following evaluation b
 `#!color lch(85% 85 805)`. Below we will demonstrate each of the different hue evaluations. To learn more, check out the
 [CSS level 4 specification](https://drafts.csswg.org/css-color-4/#hue-interpolation) which describes each one.
 
+!!! warning "Hue Fix-ups"
+    Hue "fix-ups" via the `hue` parameter currently only apply to linear interpolation, not Bezier. Bezier interpolation
+    will simply normalize the hues to be between 0˚ - 360˚.
+
 === "shorter"
     ```playground
-    Color("rebeccapurple").interpolate(
-        "lch(85% 100 805)",
+    Color.interpolate(
+        ["rebeccapurple", "lch(85% 100 805)"],
         space='lch',
         hue="shorter"
     )
@@ -100,8 +131,8 @@ To help visualize the different hue methods, consider the following evaluation b
 
 === "longer"
     ```playground
-    Color("rebeccapurple").interpolate(
-        "lch(85% 100 805)",
+    Color.interpolate(
+        ["rebeccapurple", "lch(85% 100 805)"],
         space='lch',
         hue="longer"
     )
@@ -109,8 +140,8 @@ To help visualize the different hue methods, consider the following evaluation b
 
 === "increasing"
     ```playground
-    Color("rebeccapurple").interpolate(
-        "lch(85% 100 805)",
+    Color.interpolate(
+        ["rebeccapurple", "lch(85% 100 805)"],
         space='lch',
         hue="increasing"
     )
@@ -118,8 +149,8 @@ To help visualize the different hue methods, consider the following evaluation b
 
 === "decreasing"
     ```playground
-    Color("rebeccapurple").interpolate(
-        "lch(85% 100 805)",
+    Color.interpolate(
+        ["rebeccapurple", "lch(85% 100 805)"],
         space='lch',
         hue="decreasing"
     )
@@ -127,8 +158,8 @@ To help visualize the different hue methods, consider the following evaluation b
 
 === "specified"
     ```playground
-    Color("rebeccapurple").interpolate(
-        "lch(85% 100 805)",
+    Color.interpolate(
+        ["rebeccapurple", "lch(85% 100 805)"],
         space='lch',
         hue="specified"
     )
@@ -153,8 +184,8 @@ non-premultiplied results easy to see.
 <div style="--swatch-bg-color: hsl(0, 0%, 100%); --swatch-bg-alt-color: hsl(0, 0%, 90%) " markdown="1">
 
 ```playground
-Color('white').interpolate('transparent', space='srgb', premultiplied=False)
-Color('white').interpolate('transparent', space='srgb')
+Color.interpolate(['white', 'transparent'], space='srgb', premultiplied=False)
+Color.interpolate(['white', 'transparent'], space='srgb')
 ```
 
 </div>
@@ -176,8 +207,8 @@ Color('orange').mix(Color('blue').set('alpha', 0.25), space='srgb')
 If we interpolate it, we can see the difference in transition.
 
 ```playground
-Color('orange').interpolate(Color('blue').set('alpha', 0.25), space='srgb', premultiplied=False)
-Color('orange').interpolate(Color('blue').set('alpha', 0.25), space='srgb')
+Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb', premultiplied=False)
+Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb')
 ```
 
 There may be some cases where it is desired to use a non-premultiplied. There are many reasons, one could simply be that
@@ -201,8 +232,8 @@ will end up with a range of colors that maintains the same lightness and chroma
 hues. We can see as we step through the colors that only the hue is interpolated.
 
 ```playground
-i = Color("lch(52% 58.1 22.7)").interpolate(
-    Color("lch(56% 49.1 257.1)").mask(['lightness', 'chroma', 'alpha']),
+i = Color.interpolate(
+    ["lch(52% 58.1 22.7)", Color("lch(56% 49.1 257.1)").mask(['lightness', 'chroma', 'alpha'])],
     space="lch"
 )
 [i(x/10).to_string() for x in range(10)]
@@ -211,8 +242,8 @@ i = Color("lch(52% 58.1 22.7)").interpolate(
 You can also create inverted masks. An inverted mask will mask all *except* the specified channel.
 
 ```playground
-i = Color("lch(52% 58.1 22.7)").interpolate(
-    Color("lch(56% 49.1 257.1)").mask('hue', invert=True),
+i = Color.interpolate(
+    ["lch(52% 58.1 22.7)", Color("lch(56% 49.1 257.1)").mask('hue', invert=True)],
     space="lch"
 )
 [i(x/10).to_string() for x in range(10)]
@@ -222,15 +253,13 @@ i = Color("lch(52% 58.1 22.7)").interpolate(
     Masking actually clones the color, setting the specified channels to undefined values. To learn more about masking
     and undefined values, check out [Undefined Handling](#null-handling).
 
-## Using Easing Functions
-
-Linear interpolation is not only linear in terms of the straightness of the imaginary line connecting two colors, but
-also in regards to the rate we travel along that line to return colors. Normally, if we were to request an interpolation
-point at `#!py3 0.5`, we'd get a color exactly in the middle between the two colors.
+## Easing Functions
 
-With an easing function we can actually control the rate at which we travel the interpolation line, compressing the rate
-at the start or end of the line, elongating the rate in the middle, or compress the rate periodically. The possibilities
-are endless.
+When interpolating, whether it using linear interpolation or something like Bezier interpolation, the transitioning of
+a color from one to another is done in linear time. For example, if you are translating between 2 colors and you request
+the `#!py3 0.5` point in the interpolation process, you will get a color exactly in the middle of the transition. With
+easing functions, you can completely change the progress in relation to time by compressing the rate of change at the
+start or end of the interpolation process or even elongating them; the possibilities are endless.
 
 Sometimes, it is easier to visualize what something means than to just have it explained. Here we apply an easing
 function by setting `progress`. Here we change the rate of progress along the line using different easing functions.
@@ -238,26 +267,34 @@ function by setting `progress`. Here we change the rate of progress along the li
 ```playground
 import math
 
-Color("green").interpolate(
-    "blue",
+Color.interpolate(
+    ["green", "blue"],
     progress=lambda t: t ** 3
 )
-Color("green").interpolate(
-    "blue",
+Color.interpolate(
+    ["green", "blue"],
     progress=lambda t: 8 * t ** 4 if t < 0.5 else 1 - ((-2 * t + 2) ** 4) / 2
 )
-Color("green").interpolate(
-    "blue",
+Color.interpolate(
+    ["green", "blue"],
     progress=lambda t: math.sin((t * math.pi) / 2)
 )
 ```
 
-ColorAide even lets you apply easing functions to specific channels. This can be done to one or more channels at a time.
-Below, we apply `#!py3 t ** 3` to `alpha` while allowing all other channels to interpolate normally.
+While `progress` sets an easing function for the all interpolation operation, we can set easing functions between two
+specific colors as well:
 
 ```playground
-Color("lch(50% 50 0)").interpolate(
-    "lch(90% 50 20 / 0)",
+Color.interpolate(["red", "green", lambda t: t ** 3, "blue"])
+```
+
+ColorAide even lets you apply easing functions to specific channels, though they can only be done this way for the
+entire operation. This can be done to one or more channels at a time. Below, we apply `#!py3 t ** 3` to `alpha` while
+allowing all other channels to interpolate normally.
+
+```playground
+Color.interpolate(
+    ["lch(50% 50 0)", "lch(90% 50 20 / 0)"],
     progress={
         'alpha': lambda t: t ** 3
     }
@@ -270,8 +307,8 @@ distance between the two colors, but then target `red` to ramp all the way up fr
 of `#!color color(srgb 1 0.5 0.5)`.
 
 ```playground
-Color("color(srgb 0 1 1)").interpolate(
-    "color(srgb 1 0 0)",
+Color.interpolate(
+    ["color(srgb 0 1 1)", "color(srgb 1 0 0)"],
     progress={
         'all': lambda t: t / 2,
         'r': lambda t: t
@@ -280,15 +317,45 @@ Color("color(srgb 0 1 1)").interpolate(
 )
 ```
 
+## Color Stops and Hints
+
+Color stops are the position where the transition to and from a color starts and ends. By default, color stops are
+evenly distributed between 0 - 1, but if desired, these color stops can be shifted.
+
+To specify color stops, simply wrap a color in a `coloraide.stop` object and specify the stop position. Stop positions
+will then cause the transition of the targeted color to be moved.
+
+```playground
+from coloraide import stop
+Color.interpolate(['orange', 'purple', 'green'])
+Color.interpolate(['orange', stop('purple', 0.25), 'green'])
+```
+
+Color stops follow the rules as laid out in the [CSS spec](https://drafts.csswg.org/css-images-4/#color-stop-fixup).
+
+CSS gradients also have a concept of "hints". Hints essentially define the midpoint between two colors. Instead of
+reinventing the wheel, and further complicating the interface, we've decided to just demonstrate color hints with
+easing functions. The logic comes directly from the [CSS spec](https://drafts.csswg.org/css-images-4/#coloring-gradient-line).
+
+Using the `hint` function, we can generate a midpoint easing method that moves the middle of the interpolation
+transition to the specified point which is relative to the two color stops it is between.
+
+```playground
+from coloraide import hint
+
+Color.interpolate(['orange', 'purple', 'green'])
+Color.interpolate(['orange', hint(0.75), 'purple', 'green'])
+```
+
 ## Mixing
 
 !!! tip "Interpolation Options"
     Any options not consumed by `mix` will be passed to the underlying `interpolation` function. This includes options
     like `hue`, `progress`, etc.
 
-The `mix` function is built on top of the [`interpolate`](#interpolating) function and provides a simple, intuitive
-interface for mixing two colors. Simply pass in a color to mix with the base color, and you'll get an equal mix of the
-two.
+The `mix` function is built on top of the [`interpolate`](#linear-interpolation) function and provides a simple, quick, and
+intuitive simple mixing of two colors. Just pass in a color to mix with the base color, and you'll get an equal mix of
+the two.
 
 ```playground
 Color("red").mix(Color("blue"))
@@ -302,11 +369,11 @@ Color("red").mix(Color("blue"), 0.2)
 ```
 
 As with all interpolation based functions, if needed, a different color space can be specified with the `space`
-parameter. Notice below that this creates a different color. The results of mixing in a different color space may be
-more desirable as color mixing may be more natural.
+parameter or even a different interpolation method via `method`. `mix` accepts all the same parameters used in
+`interpolate`, though concepts like [stops and hints](#color-stops-and-hints) are not allowed with mixing.
 
 ```playground
-Color("red").mix(Color("blue"), space="hsl")
+Color("red").mix(Color("blue"), space="hsl", method='bezier')
 ```
 
 Mix can also accept a string and will create the color for us which is great if we don't need to work with the second
@@ -325,10 +392,10 @@ Mixing will always return a new color unless `in_place` is set `#!py3 True`.
     like `hue`, `progress`, etc.
 
 The `steps` method provides an intuitive interface to create lists of discrete colors. Like mixing, it is also built on
-[`interpolate`](#interpolating). Just provide two colors, and specify how many `steps` are wanted.
+[`interpolate`](#linear-interpolation). Just provide two colors, and specify how many `steps` are wanted.
 
 ```playground
-Color("red").steps("blue", steps=10)
+Color.steps(["red", "blue"], steps=10)
 ```
 
 If desired, multiple colors can be provided, and steps will be returned for all the interpolated segments. When
@@ -336,7 +403,7 @@ interpolating multiple colors, [piecewise interpolation](#piecewise-interpolatio
 detail later).
 
 ```playground
-Color("red").steps(["orange", "yellow", "green"], steps=10)
+Color.steps(["red", "orange", "yellow", "green"], steps=10)
 ```
 
 Steps can also be configured to return colors based on a maximum Delta E distance. This means you can ensure the
@@ -347,8 +414,8 @@ The result gives us an array of colors, where the distance between any two color
 result of 10.
 
 ```playground
-Color("display-p3", [0, 1, 0]).steps(
-    "red",
+Color.steps(
+    [Color("display-p3", [0, 1, 0]), "red"],
     space="lch",
     out_space="srgb",
     max_delta_e=10
@@ -363,8 +430,8 @@ than the `max_steps` limit. `max_steps` is set to `#!py3 1000` by default, but c
 desired.
 
 ```playground
-Color("display-p3", [0, 1, 0]).steps(
-    "red",
+Color.steps(
+    [Color("display-p3", [0, 1, 0]), "red"],
     space="lch",
     out_space="srgb",
     max_delta_e=10,
@@ -376,8 +443,8 @@ When specifying a `max_delta_e`, `steps` will function as a minimum required ste
 if the required steps is greater than the calculated steps via the maximum Delta E limit.
 
 ```playground
-Color("display-p3", [0, 1, 0]).steps(
-    "red",
+Color.steps(
+    [Color("display-p3", [0, 1, 0]), "red"],
     space="lch",
     out_space="srgb",
     max_delta_e=10,
@@ -393,8 +460,8 @@ specifying the method via the `delta_e` parameter.
 
 === "∆E^\*^~ab~."
     ```playground
-    Color("display-p3", [0, 1, 0]).steps(
-        "red",
+    Color.steps(
+        [Color("display-p3", [0, 1, 0]), "red"],
         space="lch",
         out_space="srgb",
         max_delta_e=10,
@@ -404,8 +471,8 @@ specifying the method via the `delta_e` parameter.
 
 === "∆E^\*^~00~"
     ```playground
-    Color("display-p3", [0, 1, 0]).steps(
-        "red",
+    Color.steps(
+        [Color("display-p3", [0, 1, 0]), "red"],
         space="lch",
         out_space="srgb",
         max_delta_e=10,
@@ -413,56 +480,11 @@ specifying the method via the `delta_e` parameter.
     )
     ```
 
-## Piecewise Interpolation
-
-The [`interploate`](#interpolating) and [`steps`](#steps) methods allow for piecewise interpolation across multiple
-color ranges. Anytime, multiple colors are provided via a list, the piecewise logic will be applied to the various
-segments.
-
-```playground
-Color('red').interpolate(['white', 'black', 'blue'])
-```
-
-When interpolating between two colors, we showed that you can control the transition by setting easing functions to
-the `progress` parameter or control hue interpolation with the `hue` parameter. For piecewise interpolation, when
-`progress`, `hue`, or `premultiplied` are set via the function parameters, that will be the defaults used between all
-the provided colors, but you can also setup specific interpolation configurations between any two colors by using the
-[`Piecewise`](./api/index.md#piecewise) object. For instance, in the example below, we can apply an easing between just
-the `#!color white` and `#!color black` colors. Notice that we wrap `#!color black` in a `Piecewise` object so that the
-easing function is applied to `#!color black` and the color immediately before it (`#!color white`).
-
-```playground
-Color('red').interpolate(['white', Piecewise('black', progress=lambda t: t * (2 - t)), 'blue'])
-```
-
-Additionally, you can set color stops using the `Piecewise` object's `stop` parameter. This will ensure that the given
-color is interpolated at 100% at that percentage of the total interpolation. In the example below, we specify that in
-the entire gradient that at 75% the color will be `#!color green`.
-
-```playground
-Color('orange').interpolate([Piecewise('green', 0.75), 'blue'])
-```
-
-As the base color cannot be wrapped in a `Piecewise` object, the `steps` and `interpolation` method provide a `stop`
-parameter that specifically sets a stop for the base color. In the example below, we specify that the base color's stop
-will be at 75%, but since the base is always the first color, what it really means is that the color will remain as the
-base color until 75% and then begin the transition to the next color. In this case, the gradient remains
-`#!color orange` until it reaches 75% and then transitions to `#!color green` completing the full transition at 100%.
-
-```playground
-Color('orange').interpolate('green', stop=0.75)
-```
-
-And when we put it all together:
-
-```playground
-Color('red').interpolate([Piecewise('white', 0.6), Piecewise('black', 0.8), 'blue'], stop=0.4)
-```
-
-As previously mentioned, this can also be applied to steps as well.
+And much like [`interpolate`](#linear-interpolation), we can use [`stops` and `hints`](#color-stops-and-hints) and of
+the supported `interpolate` parameters as well.
 
 ```playground
-Color('red').steps([Piecewise('white', 0.6), Piecewise('black', 0.8), 'blue'], stop=0.4, steps=15)
+Color.steps(['orange', stop('purple', 0.25), 'green'], method='bezier', steps=10)
 ```
 
 ## Undefined/NaN Handling {#null-handling}
diff --git a/docs/src/py/notebook.py b/docs/src/py/notebook.py
index 142c5abaf..aff45b49f 100644
--- a/docs/src/py/notebook.py
+++ b/docs/src/py/notebook.py
@@ -155,7 +155,8 @@ def execute(cmd, no_except=True, inline=False):
         'Color': coloraide.Color,
         'coloraide': coloraide,
         'NaN': coloraide.NaN,
-        'Piecewise': coloraide.Piecewise,
+        'stop': coloraide.stop,
+        'hint': coloraide.hint,
         'HtmlRow': HtmlRow,
         'HtmlSteps': HtmlSteps,
         'HtmlGradient': HtmlGradient
diff --git a/docs/theme/assets/coloraide-extras/extra-notebook-1e54adfd.js b/docs/theme/assets/coloraide-extras/extra-notebook-1e54adfd.js
new file mode 100644
index 000000000..57e7fa669
--- /dev/null
+++ b/docs/theme/assets/coloraide-extras/extra-notebook-1e54adfd.js
@@ -0,0 +1,4 @@
+function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}var runtime=function(e){"use strict";var t,n=Object.prototype,o=n.hasOwnProperty,r="function"==typeof Symbol?Symbol:{},a=r.iterator||"@@iterator",i=r.asyncIterator||"@@asyncIterator",s=r.toStringTag||"@@toStringTag";function c(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{c({},"")}catch(e){c=function(e,t,n){return e[t]=n}}function l(e,t,n,o){var r=t&&t.prototype instanceof y?t:y,a=Object.create(r.prototype),i=new T(o||[]);return a._invoke=function(e,t,n){var o=d;return function(r,a){if(o===f)throw new Error("Generator is already running");if(o===m){if("throw"===r)throw a;return P()}for(n.method=r,n.arg=a;;){var i=n.delegate;if(i){var s=L(i,n);if(s){if(s===h)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===d)throw o=m,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=f;var c=u(e,t,n);if("normal"===c.type){if(o=n.done?m:p,c.arg===h)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(o=m,n.method="throw",n.arg=c.arg)}}}(e,n,i),a}function u(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}e.wrap=l;var d="suspendedStart",p="suspendedYield",f="executing",m="completed",h={};function y(){}function g(){}function v(){}var w={};c(w,a,(function(){return this}));var _=Object.getPrototypeOf,x=_&&_(_(I([])));x&&x!==n&&o.call(x,a)&&(w=x);var b=v.prototype=y.prototype=Object.create(w);function E(e){["next","throw","return"].forEach((function(t){c(e,t,(function(e){return this._invoke(t,e)}))}))}function k(e,t){function n(r,a,i,s){var c=u(e[r],e,a);if("throw"!==c.type){var l=c.arg,d=l.value;return d&&"object"===_typeof(d)&&o.call(d,"__await")?t.resolve(d.__await).then((function(e){n("next",e,i,s)}),(function(e){n("throw",e,i,s)})):t.resolve(d).then((function(e){l.value=e,i(l)}),(function(e){return n("throw",e,i,s)}))}s(c.arg)}var r;this._invoke=function(e,o){function a(){return new t((function(t,r){n(e,o,t,r)}))}return r=r?r.then(a,a):a()}}function L(e,n){var o=e.iterator[n.method];if(o===t){if(n.delegate=null,"throw"===n.method){if(e.iterator.return&&(n.method="return",n.arg=t,L(e,n),"throw"===n.method))return h;n.method="throw",n.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var r=u(o,e.iterator,n.arg);if("throw"===r.type)return n.method="throw",n.arg=r.arg,n.delegate=null,h;var a=r.arg;return a?a.done?(n[e.resultName]=a.value,n.next=e.nextLoc,"return"!==n.method&&(n.method="next",n.arg=t),n.delegate=null,h):a:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,h)}function S(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function C(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function T(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(S,this),this.reset(!0)}function I(e){if(e){var n=e[a];if(n)return n.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,i=function n(){for(;++r<e.length;)if(o.call(e,r))return n.value=e[r],n.done=!1,n;return n.value=t,n.done=!0,n};return i.next=i}}return{next:P}}function P(){return{value:t,done:!0}}return g.prototype=v,c(b,"constructor",v),c(v,"constructor",g),g.displayName=c(v,s,"GeneratorFunction"),e.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===g||"GeneratorFunction"===(t.displayName||t.name))},e.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,v):(e.__proto__=v,c(e,s,"GeneratorFunction")),e.prototype=Object.create(b),e},e.awrap=function(e){return{__await:e}},E(k.prototype),c(k.prototype,i,(function(){return this})),e.AsyncIterator=k,e.async=function(t,n,o,r,a){void 0===a&&(a=Promise);var i=new k(l(t,n,o,r),a);return e.isGeneratorFunction(n)?i:i.next().then((function(e){return e.done?e.value:i.next()}))},E(b),c(b,s,"Generator"),c(b,a,(function(){return this})),c(b,"toString",(function(){return"[object Generator]"})),e.keys=function(e){var t=[];for(var n in e)t.push(n);return t.reverse(),function n(){for(;t.length;){var o=t.pop();if(o in e)return n.value=o,n.done=!1,n}return n.done=!0,n}},e.values=I,T.prototype={constructor:T,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(C),!e)for(var n in this)"t"===n.charAt(0)&&o.call(this,n)&&!isNaN(+n.slice(1))&&(this[n]=t)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var n=this;function r(o,r){return s.type="throw",s.arg=e,n.next=o,r&&(n.method="next",n.arg=t),!!r}for(var a=this.tryEntries.length-1;a>=0;--a){var i=this.tryEntries[a],s=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var c=o.call(i,"catchLoc"),l=o.call(i,"finallyLoc");if(c&&l){if(this.prev<i.catchLoc)return r(i.catchLoc,!0);if(this.prev<i.finallyLoc)return r(i.finallyLoc)}else if(c){if(this.prev<i.catchLoc)return r(i.catchLoc,!0)}else{if(!l)throw new Error("try statement without catch or finally");if(this.prev<i.finallyLoc)return r(i.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var a=r;break}}a&&("break"===e||"continue"===e)&&a.tryLoc<=t&&t<=a.finallyLoc&&(a=null);var i=a?a.completion:{};return i.type=e,i.arg=t,a?(this.method="next",this.next=a.finallyLoc,h):this.complete(i)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),h},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),C(n),h}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var o=n.completion;if("throw"===o.type){var r=o.arg;C(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(e,n,o){return this.delegate={iterator:I(e),resultName:n,nextLoc:o},"next"===this.method&&(this.arg=t),h}},e}("object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports:{});try{regeneratorRuntime=runtime}catch(e){"object"===("undefined"==typeof globalThis?"undefined":_typeof(globalThis))?globalThis.regeneratorRuntime=runtime:Function("r","regeneratorRuntime = r")(runtime)}!function(){"use strict";function e(){
+/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
+e=function(){return t};var t={},n=Object.prototype,o=n.hasOwnProperty,r="function"==typeof Symbol?Symbol:{},a=r.iterator||"@@iterator",i=r.asyncIterator||"@@asyncIterator",s=r.toStringTag||"@@toStringTag";function c(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{c({},"")}catch(e){c=function(e,t,n){return e[t]=n}}function l(e,t,n,o){var r=t&&t.prototype instanceof p?t:p,a=Object.create(r.prototype),i=new k(o||[]);return a._invoke=function(e,t,n){var o="suspendedStart";return function(r,a){if("executing"===o)throw new Error("Generator is already running");if("completed"===o){if("throw"===r)throw a;return S()}for(n.method=r,n.arg=a;;){var i=n.delegate;if(i){var s=x(i,n);if(s){if(s===d)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if("suspendedStart"===o)throw o="completed",n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o="executing";var c=u(e,t,n);if("normal"===c.type){if(o=n.done?"completed":"suspendedYield",c.arg===d)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(o="completed",n.method="throw",n.arg=c.arg)}}}(e,n,i),a}function u(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}t.wrap=l;var d={};function p(){}function f(){}function m(){}var h={};c(h,a,(function(){return this}));var y=Object.getPrototypeOf,g=y&&y(y(L([])));g&&g!==n&&o.call(g,a)&&(h=g);var v=m.prototype=p.prototype=Object.create(h);function w(e){["next","throw","return"].forEach((function(t){c(e,t,(function(e){return this._invoke(t,e)}))}))}function _(e,t){function n(r,a,i,s){var c=u(e[r],e,a);if("throw"!==c.type){var l=c.arg,d=l.value;return d&&"object"==_typeof(d)&&o.call(d,"__await")?t.resolve(d.__await).then((function(e){n("next",e,i,s)}),(function(e){n("throw",e,i,s)})):t.resolve(d).then((function(e){l.value=e,i(l)}),(function(e){return n("throw",e,i,s)}))}s(c.arg)}var r;this._invoke=function(e,o){function a(){return new t((function(t,r){n(e,o,t,r)}))}return r=r?r.then(a,a):a()}}function x(e,t){var n=e.iterator[t.method];if(void 0===n){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=void 0,x(e,t),"throw"===t.method))return d;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return d}var o=u(n,e.iterator,t.arg);if("throw"===o.type)return t.method="throw",t.arg=o.arg,t.delegate=null,d;var r=o.arg;return r?r.done?(t[e.resultName]=r.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,d):r:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,d)}function b(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function E(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function k(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(b,this),this.reset(!0)}function L(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,r=function t(){for(;++n<e.length;)if(o.call(e,n))return t.value=e[n],t.done=!1,t;return t.value=void 0,t.done=!0,t};return r.next=r}}return{next:S}}function S(){return{value:void 0,done:!0}}return f.prototype=m,c(v,"constructor",m),c(m,"constructor",f),f.displayName=c(m,s,"GeneratorFunction"),t.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===f||"GeneratorFunction"===(t.displayName||t.name))},t.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,m):(e.__proto__=m,c(e,s,"GeneratorFunction")),e.prototype=Object.create(v),e},t.awrap=function(e){return{__await:e}},w(_.prototype),c(_.prototype,i,(function(){return this})),t.AsyncIterator=_,t.async=function(e,n,o,r,a){void 0===a&&(a=Promise);var i=new _(l(e,n,o,r),a);return t.isGeneratorFunction(n)?i:i.next().then((function(e){return e.done?e.value:i.next()}))},w(v),c(v,s,"Generator"),c(v,a,(function(){return this})),c(v,"toString",(function(){return"[object Generator]"})),t.keys=function(e){var t=[];for(var n in e)t.push(n);return t.reverse(),function n(){for(;t.length;){var o=t.pop();if(o in e)return n.value=o,n.done=!1,n}return n.done=!0,n}},t.values=L,k.prototype={constructor:k,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=void 0,this.done=!1,this.delegate=null,this.method="next",this.arg=void 0,this.tryEntries.forEach(E),!e)for(var t in this)"t"===t.charAt(0)&&o.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=void 0)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var t=this;function n(n,o){return i.type="throw",i.arg=e,t.next=n,o&&(t.method="next",t.arg=void 0),!!o}for(var r=this.tryEntries.length-1;r>=0;--r){var a=this.tryEntries[r],i=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=o.call(a,"catchLoc"),c=o.call(a,"finallyLoc");if(s&&c){if(this.prev<a.catchLoc)return n(a.catchLoc,!0);if(this.prev<a.finallyLoc)return n(a.finallyLoc)}else if(s){if(this.prev<a.catchLoc)return n(a.catchLoc,!0)}else{if(!c)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return n(a.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var a=r;break}}a&&("break"===e||"continue"===e)&&a.tryLoc<=t&&t<=a.finallyLoc&&(a=null);var i=a?a.completion:{};return i.type=e,i.arg=t,a?(this.method="next",this.next=a.finallyLoc,d):this.complete(i)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),d},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),E(n),d}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var o=n.completion;if("throw"===o.type){var r=o.arg;E(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:L(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=void 0),d}},t}function t(e,t,n,o,r,a,i){try{var s=e[a](i),c=s.value}catch(e){return void n(e)}s.done?t(c):Promise.resolve(c).then(o,r)}function n(e){return function(){var n=this,o=arguments;return new Promise((function(r,a){var i=e.apply(n,o);function s(e){t(i,r,a,s,c,"next",e)}function c(e){t(i,r,a,s,c,"throw",e)}s(void 0)}))}}function o(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null==n)return;var o,r,a=[],i=!0,s=!1;try{for(n=n.call(e);!(i=(o=n.next()).done)&&(a.push(o.value),!t||a.length!==t);i=!0);}catch(e){s=!0,r=e}finally{try{i||null==n.return||n.return()}finally{if(s)throw r}}return a}(e,t)||r(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function r(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n<t;n++)o[n]=e[n];return o}var i,s,c,l,u,d,p,f,m,h,y,g,v,w,_,x,b,E,k,L,S,C,T,I;i=null,s=!1,c="",l="",u={},d=/.*?_(\d+)$/,p=!1,f="",m=!1,h=/^( {1,4}|\t)/,y="\n\"\"\"\nExecute Python code in code blocks (color previews added specifically for ColorAide).\n\nThis can be executed in either a Pyodide environment of a normal Python environment.\n3rd party libraries (that are not available directly from Pyodide) are only loaded\nwhen needed so that Pyodide will have a chance to load them if necessary. This is also\nhardcoded to work with ColorAide.\n\nThis is meant to be executed by Pyodide on preformatted HTML to allow for live execution of\ncode snippets using `coloraide`.\n\nTransform Python code by executing it, transforming to a Python console output,\nand finding and outputting color previews.\n\"\"\"\nimport xml.etree.ElementTree as Etree\nfrom collections.abc import Sequence\nfrom collections import namedtuple\nimport ast\nfrom io import StringIO\nimport contextlib\nimport sys\nimport re\n\nWEBSPACE = \"srgb\"\nAST_BLOCKS = (ast.If, ast.For, ast.While, ast.Try, ast.With, ast.FunctionDef, ast.ClassDef)\n\nRE_COLOR_START = re.compile(\n    r\"(?i)(?:\\b(?<![-#&$])(?:color|hsla?|lch|lab|hwb|rgba?)\\(|\\b(?<![-#&$])[\\w]{3,}(?![(-])\\b|(?<![&])#)\"\n)\n\ntemplate = '''<div class=\"playground\" id=\"__playground_{el_id}\">\n<div class=\"playground-results\" id=\"__playground-results_{el_id}\">\n{results}\n</div>\n<div class=\"playground-code hidden\" id=\"__playground-code_{el_id}\">\n<form autocomplete=\"off\">\n<textarea class=\"playground-inputs\" id=\"__playground-inputs_{el_id}\" spellcheck=\"false\">{raw_source}</textarea>\n</form>\n</div>\n\n<button id=\"__playground-edit_{el_id}\" class=\"playground-edit\" title=\"Edit the current snippet\">Edit</button>\n<button id=\"__playground-share_{el_id}\" class=\"playground-share\" title=\"Copy URL to current snippet\">Share</button>\n<button id=\"__playground-run_{el_id}\" class=\"playground-run hidden\" title=\"Run code (Ctrl + Enter)\">Run</button>\n<button id=\"__playground-cancel_{el_id}\" class=\"playground-cancel hidden\" title=\"Cancel edit (Escape)\">Cancel</button>\n</div>'''\n\ncode_id = 0\n\n\nclass HtmlGradient(list):\n    \"\"\"HTML color gradient.\"\"\"\n\n\nclass HtmlSteps(list):\n    \"\"\"HTML color steps.\"\"\"\n\n\nclass ColorTuple(namedtuple('ColorTuple', ['string', 'color'])):\n    \"\"\"Color tuple.\"\"\"\n\n\nclass HtmlRow(list):\n    \"\"\"Create a row with the given colors.\"\"\"\n\n\ndef _escape(txt):\n    \"\"\"Basic HTML escaping.\"\"\"\n\n    txt = txt.replace('&', '&amp;')\n    txt = txt.replace('<', '&lt;')\n    txt = txt.replace('>', '&gt;')\n    return txt\n\n\n@contextlib.contextmanager\ndef std_output(stdout=None):\n    \"\"\"Capture standard out.\"\"\"\n    old = sys.stdout\n    if stdout is None:\n        stdout = StringIO()\n    sys.stdout = stdout\n    yield stdout\n    sys.stdout = old\n\n\ndef get_colors(result):\n    \"\"\"Get color from results.\"\"\"\n\n    from coloraide import Color\n    from coloraide.interpolate import Interpolator\n    try:\n        from coloraide_extras import Color as Color2\n    except ImportError:\n        Color2 = Color\n\n    colors = []\n    if isinstance(result, HtmlRow):\n        colors = HtmlRow(\n            [\n                ColorTuple(c.to_string(fit=False), c.clone()) if isinstance(c, Color) else ColorTuple(c, Color2(c))\n                for c in result\n            ]\n        )\n    elif isinstance(result, (HtmlSteps, HtmlGradient)):\n        t = type(result)\n        colors = t([c.clone() if isinstance(c, Color) else Color2(c) for c in result])\n    elif isinstance(result, Color):\n        colors.append(ColorTuple(result.to_string(fit=False), result.clone()))\n    elif isinstance(result, Interpolator):\n        colors = HtmlGradient(result.steps(steps=5, max_delta_e=2.3))\n    elif isinstance(result, str):\n        try:\n            colors.append(ColorTuple(result, Color2(result)))\n        except Exception:\n            pass\n    elif isinstance(result, Sequence):\n        for x in result:\n            if isinstance(x, Color):\n                colors.append(ColorTuple(x.to_string(fit=False), x.clone()))\n            elif isinstance(x, str):\n                try:\n                    colors.append(ColorTuple(x, Color2(x)))\n                except Exception:\n                    pass\n    return colors\n\n\ndef find_colors(text):\n    \"\"\"Find colors in text buffer.\"\"\"\n\n    try:\n        from coloraide_extras import Color\n    except ImportError:\n        from coloraide import Color\n\n    colors = []\n    for m in RE_COLOR_START.finditer(text):\n        start = m.start()\n        mcolor = Color.match(text, start=start)\n        if mcolor is not None:\n            colors.append(ColorTuple(text[mcolor.start:mcolor.end], mcolor.color))\n    return colors\n\n\ndef execute(cmd, no_except=True, inline=False):\n    \"\"\"Execute color commands.\"\"\"\n\n    import coloraide\n    try:\n        import coloraide_extras\n    except ImportError:\n        coloraide_extras = None\n\n    g = {\n        'Color': coloraide.Color,\n        'coloraide': coloraide,\n        'NaN': coloraide.NaN,\n        'stop': coloraide.stop,\n        'HtmlRow': HtmlRow,\n        'HtmlSteps': HtmlSteps,\n        'HtmlGradient': HtmlGradient\n    }\n\n    if coloraide_extras is not None:\n        g['coloraide_extras'] = coloraide_extras\n        g['Color'] = coloraide_extras.Color\n\n    console = ''\n    colors = []\n\n    # Build AST tree\n    src = cmd.strip()\n    lines = src.split('\\n')\n    try:\n        tree = ast.parse(src)\n    except Exception as e:\n        if no_except:\n            if not inline:\n                from pymdownx.superfences import SuperFencesException\n                raise SuperFencesException from e\n            else:\n                from pymdownx.inlinehilite import InlineHiliteException\n                raise InlineHiliteException from e\n        import traceback\n        return '{}'.format(traceback.format_exc()), colors\n\n    for node in tree.body:\n        result = None\n\n        # Format source as Python console statements\n        start = node.lineno\n        end = node.end_lineno\n        stmt = lines[start - 1: end]\n        command = ''\n        for i, line in enumerate(stmt, 0):\n            if i == 0:\n                stmt[i] = '>>> ' + line\n            else:\n                stmt[i] = '... ' + line\n        command += '\\n'.join(stmt)\n        if isinstance(node, AST_BLOCKS):\n            command += '\\n... '\n\n        try:\n            # Capture anything sent to standard out\n            text = ''\n            with std_output() as s:\n                # Execute code\n                if isinstance(node, ast.Expr):\n                    _eval = ast.Expression(node.value)\n                    result = eval(compile(_eval, '<string>', 'eval'), g)\n                else:\n                    _exec = ast.Module([node], [])\n                    exec(compile(_exec, '<string>', 'exec'), g)\n\n                # Execution went well, so append command\n                console += command\n\n                # Output captured standard out after statements\n                text = s.getvalue()\n                if text:\n                    clist = find_colors(text)\n                    if clist:\n                        colors.append(clist)\n                    console += '\\n{}'.format(text)\n                s.flush()\n        except Exception as e:\n            if no_except:\n                if not inline:\n                    from pymdownx.superfences import SuperFencesException\n                    raise SuperFencesException from e\n                else:\n                    from pymdownx.inlinehilite import InlineHiliteException\n                    raise InlineHiliteException from e\n            import traceback\n            console += '{}\\n{}'.format(command, traceback.format_exc())\n            # Failed for some reason, so quit\n            break\n\n        # If we got a result, output it as well\n        if result is not None:\n            clist = get_colors(result)\n            if clist:\n                colors.append(clist)\n            console += '{}{}\\n'.format('\\n' if not text else '', str(result))\n        else:\n            console += '\\n' if not text else ''\n\n    return console, colors\n\n\ndef colorize(src, lang, **options):\n    \"\"\"Colorize.\"\"\"\n\n    from pygments import highlight\n    from pygments.lexers import get_lexer_by_name\n    from pygments.formatters import find_formatter_class\n    HtmlFormatter = find_formatter_class('html')\n\n    lexer = get_lexer_by_name(lang, **options)\n    formatter = HtmlFormatter(cssclass=\"highlight\", wrapcode=True)\n    return highlight(src, lexer, formatter).strip()\n\n\ndef color_command_validator(language, inputs, options, attrs, md):\n    \"\"\"Color validator.\"\"\"\n\n    valid_inputs = set(['exceptions'])\n\n    for k, v in inputs.items():\n        if k in valid_inputs:\n            options[k] = True\n            continue\n        attrs[k] = v\n    return True\n\n\ndef _color_command_console(colors):\n    \"\"\"Color command formatter.\"\"\"\n\n    el = ''\n    bar = False\n    values = []\n    for item in colors:\n        if isinstance(item, (HtmlGradient, HtmlSteps)):\n            current = total = percent = last = 0\n            if isinstance(item, HtmlSteps):\n                total = len(item)\n                percent = 100 / total\n                current = percent\n\n            if bar:\n                el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n                values = []\n            sub_el1 = '<div class=\"swatch-bar\"><span class=\"swatch swatch-gradient\">{}</span></div>'\n            style = \"--swatch-stops: \"\n            stops = []\n            for e, color in enumerate(item):\n                color.fit(WEBSPACE)\n                if current:\n                    stops.append('{} {}%'.format(color.convert(WEBSPACE).to_string(), str(last)))\n                    stops.append('{} {}%'.format(color.convert(WEBSPACE).to_string(), str(current)))\n                    last = current\n                    if e < (total - 1):\n                        current += percent\n                    else:\n                        current = 100\n                else:\n                    stops.append(color.convert(WEBSPACE).to_string())\n            if not stops:\n                stops.extend(['transparent'] * 2)\n            if len(stops) == 1:\n                stops.append(stops[0])\n            style += ','.join(stops)\n            sub_el2 = '<span class=\"swatch-color\" style=\"{}\"></span>'.format(style)\n            el += sub_el1.format(sub_el2)\n            bar = False\n        else:\n            is_row = False\n            if isinstance(item, HtmlRow):\n                is_row = True\n                if bar and values:\n                    el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n                    values = []\n                bar = False\n\n            bar = True\n            base_classes = \"swatch\"\n            for color in item:\n                if not color.color.in_gamut(WEBSPACE):\n                    base_classes += \" out-of-gamut\"\n                color.color.fit(WEBSPACE)\n                srgb = color.color.convert(WEBSPACE)\n                value1 = srgb.to_string(alpha=False)\n                value2 = srgb.to_string()\n                style = \"--swatch-stops: {} 50%, {} 50%\".format(value1, value2)\n                title = color.string\n                classes = base_classes\n                c = '<span class=\"swatch-color\" style=\"{style}\"></span>'.format(style=style)\n                c = '<span class=\"{classes}\" title=\"{title}&#013;Copy to clipboard\">{color}</span>'.format(\n                    classes=classes,\n                    color=c,\n                    title=title\n                )\n                values.append(c)\n\n            if is_row and values:\n                el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n                values = []\n                bar = False\n    if bar:\n        el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n        values = []\n\n    return el\n\n\ndef color_command_formatter(src=\"\", language=\"\", class_name=None, options=None, md=\"\", **kwargs):\n    \"\"\"Formatter wrapper.\"\"\"\n\n    global code_id\n    from pymdownx.superfences import SuperFencesException\n\n    try:\n        if len(md.preprocessors['fenced_code_block'].extension.stash) == 0:\n            code_id = 0\n\n        # Check if we should allow exceptions\n        exceptions = options.get('exceptions', False) if options is not None else False\n\n        console, colors = execute(src.strip(), not exceptions)\n        el = _color_command_console(colors)\n\n        el += md.preprocessors['fenced_code_block'].extension.superfences[0]['formatter'](\n            src=console,\n            class_name=\"highlight\",\n            language='pycon',\n            md=md,\n            options=options,\n            **kwargs\n        )\n        el = '<div class=\"color-command\">{}</div>'.format(el)\n        el = template.format(el_id=code_id, raw_source=_escape(src), results=el)\n        code_id += 1\n    except SuperFencesException:\n        raise\n    except Exception:\n        from pymdownx import superfences\n        import traceback\n        print(traceback.format_exc())\n        return superfences.fence_code_format(src, 'text', class_name, options, md, **kwargs)\n    return el\n\n\ndef color_formatter(src=\"\", language=\"\", class_name=None, md=\"\", exceptions=True):\n    \"\"\"Formatter wrapper.\"\"\"\n\n    from pymdownx.inlinehilite import InlineHiliteException\n    try:\n        from coloraide_extras import Color\n    except ImportError:\n        from coloraide import Color\n\n    try:\n        result = src.strip()\n\n        try:\n            color = Color(result.strip())\n        except Exception:\n            console, colors = execute(result, exceptions, inline=True)\n            if len(colors) != 1 or len(colors[0]) != 1:\n                if exceptions:\n                    raise InlineHiliteException('Only one color allowed')\n                else:\n                    raise ValueError('Only one color allowed')\n            color = colors[0][0].color\n            result = colors[0][0].string\n\n        el = Etree.Element('span')\n        stops = []\n        if not color.in_gamut(WEBSPACE):\n            color.fit(WEBSPACE)\n            attributes = {'class': \"swatch out-of-gamut\", \"title\": result}\n            sub_el = Etree.SubElement(el, 'span', attributes)\n            stops.append(color.convert(WEBSPACE).to_string(hex=True, alpha=False))\n            if color[-1] < 1.0:\n                stops[-1] += ' 50%'\n                stops.append(color.convert(WEBSPACE).to_string(hex=True) + ' 50%')\n        else:\n            attributes = {'class': \"swatch\", \"title\": result}\n            sub_el = Etree.SubElement(el, 'span', attributes)\n            stops.append(color.convert(WEBSPACE).to_string(hex=True, alpha=False))\n            if color[-1] < 1.0:\n                stops[-1] += ' 50%'\n                stops.append(color.convert(WEBSPACE).to_string(hex=True) + ' 50%')\n\n        if not stops:\n            stops.extend(['transparent'] * 2)\n        if len(stops) == 1:\n            stops.append(stops[0])\n\n        Etree.SubElement(\n            sub_el,\n            'span',\n            {\n                \"class\": \"swatch-color\",\n                \"style\": \"--swatch-stops: {};\".format(','.join(stops))\n            }\n        )\n\n        el.append(md.inlinePatterns['backtick'].handle_code('css-color', result))\n    except InlineHiliteException:\n        raise\n    except Exception:\n        import traceback\n        print(traceback.format_exc())\n        el = md.inlinePatterns['backtick'].handle_code('text', src)\n    return el\n\n\n#############################\n# Pyodide specific code\n#############################\ndef live_color_command_formatter(src):\n    \"\"\"Formatter wrapper.\"\"\"\n\n    try:\n        console, colors = execute(src.strip(), False)\n        el = _color_command_console(colors)\n\n        if not colors:\n            el += '<div class=\"swatch-bar\"></div>'\n\n        el += colorize(console, 'pycon', **{'python3': True, 'stripnl': False})\n        el = '<div class=\"color-command\">{}</div>'.format(el)\n    except Exception:\n        return '<div class=\"color-command\"><div class=\"swatch-bar\"></div>{}</div>'.format(colorize('', 'text'))\n    return el\n\n\ndef live_color_command_validator(language, inputs, options, attrs, md):\n    \"\"\"Color validator.\"\"\"\n\n    value = color_command_validator(language, inputs, options, attrs, md)\n    # Live edit, we always allow exceptions so not to crash the service.\n    options['exceptions'] = True\n    return value\n\n\ndef live_color_formatter(src=\"\", language=\"\", class_name=None, md=\"\"):\n    \"\"\"Color formatter for a live environment.\"\"\"\n\n    return color_formatter(src, language, class_name, md, exceptions=False)\n\n\ndef render_console(*args):\n    \"\"\"Render console update.\"\"\"\n\n    from js import document\n\n    try:\n        # Run code\n        inputs = document.getElementById(\"__playground-inputs_{}\".format(globals()['id_num']))\n        results = document.getElementById(\"__playground-results_{}\".format(globals()['id_num']))\n        results.innerHTML = live_color_command_formatter(inputs.value)\n        scrollingElement = results.querySelector('code')\n        scrollingElement.scrollTop = scrollingElement.scrollHeight\n    except Exception as e:\n        print(e)\n\n\ndef render_notebook(*args):\n    \"\"\"Render notebook.\"\"\"\n\n    import markdown\n    from pymdownx import slugs\n    from js import document\n\n    text = globals().get('content', '')\n    extensions = [\n        'markdown.extensions.toc',\n        'markdown.extensions.admonition',\n        'markdown.extensions.smarty',\n        'pymdownx.betterem',\n        'markdown.extensions.attr_list',\n        'markdown.extensions.def_list',\n        'markdown.extensions.tables',\n        'markdown.extensions.abbr',\n        'markdown.extensions.footnotes',\n        'markdown.extensions.md_in_html',\n        'pymdownx.superfences',\n        'pymdownx.highlight',\n        'pymdownx.inlinehilite',\n        'pymdownx.magiclink',\n        'pymdownx.tilde',\n        'pymdownx.caret',\n        'pymdownx.smartsymbols',\n        'pymdownx.emoji',\n        'pymdownx.escapeall',\n        'pymdownx.tasklist',\n        'pymdownx.striphtml',\n        'pymdownx.snippets',\n        'pymdownx.keys',\n        'pymdownx.details',\n        'pymdownx.saneheaders',\n        'pymdownx.tabbed'\n    ]\n    extension_configs = {\n        'markdown.extensions.toc': {\n            'slugify': slugs.slugify(case=\"lower\"),\n            'permalink': \"\"\n        },\n        'markdown.extensions.smarty': {\n            \"smart_quotes\": False,\n        },\n        'pymdownx.superfences': {\n            'preserve_tabs': True,\n            'custom_fences': [\n                {\n                    \"name\": 'playground',\n                    \"class\": 'playground',\n                    \"format\": color_command_formatter,\n                    \"validator\": live_color_command_validator\n                }\n            ]\n        },\n        'pymdownx.inlinehilite': {\n            'custom_inline': [\n                {\n                    'name': 'color',\n                    'class': 'color',\n                    'format': live_color_formatter\n                }\n            ]\n        },\n        'pymdownx.magiclink': {\n            'repo_url_shortener': True,\n            'repo_url_shorthand': True,\n            'social_url_shorthand': True,\n            'user': 'facelessuser',\n            'repo': 'coloraide'\n        },\n        'pymdownx.keys': {\n            'separator': \"\\uff0b\"\n        }\n    }\n\n    try:\n        html = markdown.markdown(text, extensions=extensions, extension_configs=extension_configs)\n    except Exception:\n        html = ''\n    content = document.getElementById(\"__notebook-render\")\n    content.innerHTML = html\n\n\nimport micropip\nfrom js import location\n\naction = globals().get('action')\nif action == 'notebook':\n    callback = render_notebook\nelse:\n    callback = render_console\n\nbase = location.pathname.lstrip('/').split('/')[0]\nawait micropip.install([location.origin + '/{}/playground/{}'.format(base, w) for w in wheels])\ncallback()\n",g=window.color_notebook.default_playground,v=function(e){return'\n!!! new "This notebook is powered by [Pyodide](https://github.com/pyodide/pyodide). Learn more [here](?notebook=https://gist.githubusercontent.com/facelessuser/7c819668b5eb248ecb9ac608d91391cf/raw/playground.md). Preview, convert, interpolate, and explore!"\n\n````````playground\n'.concat(e,"\n````````\n")},w=function(){m=!0,window.document.dispatchEvent(new Event("DOMContentLoaded",{bubbles:!0,cancelable:!0}))},_=function(e){var t=window.pageXOffset||(document.documentElement||document.body.parentNode||document.body).scrollLeft,n=window.pageYOffset||(document.documentElement||document.body.parentNode||document.body).scrollTop;e.style.height="5px",e.style.height="".concat(e.scrollHeight,"px"),window.scrollTo(t,n)},x=function(e){return encodeURIComponent(e).replace(/[.!'()*]/g,(function(e){return"%".concat(e.charCodeAt(0).toString(16))}))},b=function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return(o=document.getElementById("__playground-inputs_".concat(n))).setAttribute("readonly",""),i.globals.set("id_num",n),i.globals.set("action","playground"),i.globals.set("wheels",window.color_notebook.playground_wheels),e.next=7,i.runPythonAsync(y);case 7:o.removeAttribute("readonly");case 8:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),E=function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return i.globals.set("content",n),i.globals.set("action","notebook"),i.globals.set("wheels",window.color_notebook.notebook_wheels),e.next=5,i.runPythonAsync(y);case 5:(o=document.getElementById("__notebook-input"))&&(c=n,o.value=n),window.location.hash&&(window.location.href=window.location.href);case 8:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),k=function(){var t=n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(p){e.next=7;break}return p=!0,e.next=4,loadPyodide({indexURL:"https://cdn.jsdelivr.net/pyodide/v0.20.0/full/",fullStdLib:!1});case 4:return i=e.sent,e.next=7,i.loadPackage(["micropip"]);case 7:case"end":return e.stop()}}),t)})));return function(){return t.apply(this,arguments)}}(),L=function(e,t,n){var o=null==t?"Loading...":t,r=n?"loading relative":"loading",a=document.createElement("template");a.innerHTML='<div class="'.concat(r,'"><div class="loader"></div><div>').concat(o,"</div></div>"),e.appendChild(a.content.firstChild)},S=function(e){e.querySelector(".loading")&&e.removeChild(e.querySelector(".loading"))},C=function(e){if("Tab"===e.key){var t=e.target;if(t.selectionStart!==t.selectionEnd){e.preventDefault();for(var n=t.selectionStart,o=t.selectionEnd,r=t.value;n>0&&"\n"!==r[n-1];)n--;for(;o>0&&"\n"!==r[o-1]&&o<r.length;)o++;for(var a=r.substr(n,o-n).split("\n"),i=0;i<a.length;i++)i===a.length-1&&0===a[i].length||(e.shiftKey?a[i]=a[i].replace(h,""):a[i]="    ".concat(a[i]));a=a.join("\n"),t.value=r.substr(0,n)+a+r.substr(o),t.selectionStart=n,t.selectionEnd=n+a.length}}},T=function(){var t=n(e().mark((function t(o){var r;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:r=document.getElementById("__notebook-source"),document.querySelectorAll(".playground").forEach((function(t){var a=t.id.replace(d,"$1"),i=document.getElementById("__playground-inputs_".concat(a)),p=document.getElementById("__playground-results_".concat(a)),f=document.getElementById("__playground-code_".concat(a)),m=document.querySelector("button#__playground-edit_".concat(a)),h=document.querySelector("button#__playground-share_".concat(a)),y=document.querySelector("button#__playground-run_".concat(a)),g=document.querySelector("button#__playground-cancel_".concat(a));if(i.addEventListener("input",(function(){_(i)})),i.addEventListener("keydown",C),r&&o){var v=document.getElementById("__notebook-input");v.addEventListener("input",(function(e){_(e.target)})),v.addEventListener("keydown",C),document.getElementById("__notebook-edit").addEventListener("click",(function(){u[v.id]=v.value,document.getElementById("__notebook-render").classList.toggle("hidden"),document.getElementById("__notebook-source").classList.toggle("hidden"),_(document.getElementById("__notebook-input"))})),document.getElementById("__notebook-md-gist").addEventListener("click",function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:null!==(o=prompt("Please enter link to the Markdown page source:",l))&&(o=x(o),n.preventDefault(),history.pushState({notebook:o},"","?".concat(new URLSearchParams("notebook=".concat(o)).toString())),I(!1));case 2:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()),document.getElementById("__notebook-py-gist").addEventListener("click",function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:null!==(o=prompt("Please enter the link to the Python code source:",l))&&(o=x(o),n.preventDefault(),history.pushState({source:o},"","?".concat(new URLSearchParams("source=".concat(o)).toString())),I(!1));case 2:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()),document.getElementById("__notebook-input").value=c,document.getElementById("__notebook-cancel").addEventListener("click",(function(){v.value=u[v.id],delete u[v.id],document.getElementById("__notebook-render").classList.toggle("hidden"),document.getElementById("__notebook-source").classList.toggle("hidden")})),document.getElementById("__notebook-submit").addEventListener("click",n(e().mark((function t(){var n,o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=document.getElementById("__notebook-render"),c=document.getElementById("__notebook-input").value,n.classList.toggle("hidden"),document.getElementById("__notebook-source").classList.toggle("hidden"),o=document.querySelector("article"),L(o,"Loading Notebook..."),n.innerHTML="",u={},e.next=10,k();case 10:return e.next=12,E(c);case 12:return e.next=14,T();case 14:S(o);case 15:case"end":return e.stop()}}),t)}))))}i.addEventListener("touchmove",(function(e){e.stopPropagation()})),p.addEventListener("click",(function(e){var t=e.target;if(t.matches("span.swatch-color")){var n="",o=t.parentNode;if(!o.matches("span.swatch-gradient")){if(n=(n=o.getAttribute("title").replace("Copy to clipboard","")).replace("\n",""),window.clipboardData&&window.clipboardData.setData)return window.clipboardData.setData("Text",n);if(document.queryCommandSupported&&document.queryCommandSupported("copy")){var r=document.createElement("textarea");r.textContent=n,r.style.position="fixed",document.body.appendChild(r),r.select();try{return document.execCommand("copy")}catch(e){return prompt("Copy to clipboard: Ctrl+C, Enter",n)}finally{document.body.removeChild(r)}}}}})),m.addEventListener("click",n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:u[a]=i.value,f.classList.toggle("hidden"),p.classList.toggle("hidden"),y.classList.toggle("hidden"),g.classList.toggle("hidden"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),_(i),i.focus();case 9:case"end":return e.stop()}}),t)})))),h.addEventListener("click",n(e().mark((function t(){var o,r,a,s,c;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:o=window.location.pathname.split("/")[1],r=x(i.value),a=window.location,s="/playground/",a.pathname.startsWith("/".concat(o,"/"))&&(s="/".concat(o,"/playground/")),c="".concat(a.protocol,"//").concat(a.host).concat(s,"?code=").concat(r),r.length>1e3?alert("Code must be under a 1000 characters to generate a URL!"):navigator.clipboard.writeText(c).then(n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:alert("Link copied to clipboard :)");case 1:case"end":return e.stop()}}),t)}))),n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:alert("Failed to copy link clipboard!");case 1:case"end":return e.stop()}}),t)}))));case 7:case"end":return t.stop()}}),t)})))),y.addEventListener("click",n(e().mark((function t(){var n,o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!s){e.next=2;break}return e.abrupt("return");case 2:return s=!0,n=f.querySelector("form"),L(n,null,!0),(o=document.querySelectorAll(".playground .playground-run"))&&o.forEach((function(e){e.setAttribute("disabled","")})),e.next=9,k();case 9:return p.querySelector("code").innerHTML="",e.next=12,b(a);case 12:o&&o.forEach((function(e){e.removeAttribute("disabled")})),S(n),f.classList.toggle("hidden"),p.classList.toggle("hidden"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),y.classList.toggle("hidden"),g.classList.toggle("hidden"),delete u[a],s=!1;case 22:case"end":return e.stop()}}),t)})))),g.addEventListener("click",(function(){i.value=u[a],delete u[a],f.classList.toggle("hidden"),p.classList.toggle("hidden"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),y.classList.toggle("hidden"),g.classList.toggle("hidden")}))}));case 3:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),I=function(){var t=n(e().mark((function t(o){var r,a,i,s,c,d,p,m,h;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(u={},!window.location.pathname.endsWith("/playground/")){t.next=32;break}if(r=new URLSearchParams(window.location.search),a="Loading Pyodide...",i="Loading Notebook...",s=r.has("source")?r.get("source"):r.get("notebook"),c=document.querySelector("article"),null===s||!s.trim()){t.next=16;break}return L(c,a),t.next=11,k();case 11:S(c),L(c,i);try{d=r.has("source")?"source":"notebook",f=decodeURIComponent(r.toString()),p="",m=new XMLHttpRequest,l=s,m.open("GET",s,!0),m.onload=n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return 4===m.readyState&&200===m.status&&(p=m.responseText),"source"===d&&(p=v(p)),e.next=4,E(p);case 4:return e.next=6,T(o);case 6:S(c),w();case 8:case"end":return e.stop()}}),t)}))),m.send()}catch(e){}t.next=30;break;case 16:return l="",h=v(r.has("code")?r.get("code"):g),f=decodeURIComponent(r.toString()),L(c,a),t.next=22,k();case 22:return S(c),L(c,i),t.next=26,E(h);case 26:return t.next=28,T(o);case 28:S(c),w();case 30:t.next=35;break;case 32:l="",f="",T(o);case 35:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),document.addEventListener("click",(function(e){var t=window.location.pathname.split("/")[1],n=e.target||e.srcElement;if("A"===n.tagName&&I&&n.getAttribute("href")&&n.host===window.location.host&&window.location.pathname==="/".concat(t,'/playground/"')&&window.location.pathname===n.pathname&&window.location.search!==n.search){e.preventDefault();var a,i={},s=function(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=r(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var o=0,a=function(){};return{s:a,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,c=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){c=!0,i=e},f:function(){try{s||null==n.return||n.return()}finally{if(c)throw i}}}}(new URLSearchParams(n.search));try{for(s.s();!(a=s.n()).done;){var c=o(a.value,2),l=c[0],u=c[1];i[l]=u}}catch(e){s.e(e)}finally{s.f()}history.pushState(i,"",n.href),I(!1)}})),window.addEventListener("popstate",(function(){var e=window.location.pathname.split("/")[1];window.location.pathname==="/".concat(e,"/playground/")&&decodeURIComponent(new URLSearchParams(window.location.search).toString())!==f&&I(!1)})),window.addEventListener("unload",(function(){m=!0})),window.document$.subscribe((function(){m?m=!1:I(!0)}))}();
+//# sourceMappingURL=extra-notebook-1e54adfd.js.map
diff --git a/docs/theme/assets/coloraide-extras/extra-notebook-a80f0a52.js.map b/docs/theme/assets/coloraide-extras/extra-notebook-1e54adfd.js.map
similarity index 99%
rename from docs/theme/assets/coloraide-extras/extra-notebook-a80f0a52.js.map
rename to docs/theme/assets/coloraide-extras/extra-notebook-1e54adfd.js.map
index c1fd7515d..330e698e7 100644
--- a/docs/theme/assets/coloraide-extras/extra-notebook-a80f0a52.js.map
+++ b/docs/theme/assets/coloraide-extras/extra-notebook-1e54adfd.js.map
@@ -1,1 +1,1 @@
-{"version":3,"file":"extra-notebook-a80f0a52.js","sources":["../../../src/js/extra-notebook.js"],"sourcesContent":["(() => {\n  let pyodide = null\n  let busy = false\n  let raw = \"\"\n  let gist = \"\"\n  let editTemp = {}\n  const reIdNum = /.*?_(\\d+)$/\n  let initialized = false\n  let lastSearch = \"\"\n  let fake = false\n  const tabStart = /^( {1,4}|\\t)/\n  // This is the Python payload that will be executed when the user\n  // presses the `Run` button. It will execute the code, create a\n  // Python console output, find color references, steps, and interpolation\n  // references and render the appropriate preview.\n  const pycode = `\n{{pycode}}\n\nimport micropip\nfrom js import location\n\naction = globals().get('action')\nif action == 'notebook':\n    callback = render_notebook\nelse:\n    callback = render_console\n\nbase = location.pathname.lstrip('/').split('/')[0]\nawait micropip.install([location.origin + '/{}/playground/{}'.format(base, w) for w in wheels])\ncallback()\n`\n\n  const defContent = window.color_notebook.default_playground\n\n  const getContent = content => {\n    return `\n!!! new \"This notebook is powered by [Pyodide](https://github.com/pyodide/pyodide). \\\nLearn more [here](\\\n?notebook=https://gist.githubusercontent.com/facelessuser/7c819668b5eb248ecb9ac608d91391cf/raw/playground.md\\\n). Preview, convert, interpolate, and explore!\"\n\n\\`\\`\\`\\`\\`\\`\\`\\`playground\n${content}\n\\`\\`\\`\\`\\`\\`\\`\\`\n`\n  }\n\n  const fakeDOMContentLoaded = () => {\n    // Send a fake `DOMContentLoaded`\n    fake = true\n    window.document.dispatchEvent(new Event(\"DOMContentLoaded\", {\n      bubbles: true,\n      cancelable: true\n    }))\n  }\n\n  const textResize = inpt => {\n    // Resize inputs based on text height.\n\n    const scrollLeft = window.pageXOffset ||\n      (document.documentElement || document.body.parentNode || document.body).scrollLeft\n\n    const scrollTop  = window.pageYOffset ||\n      (document.documentElement || document.body.parentNode || document.body).scrollTop\n\n    inpt.style.height = \"5px\"\n    inpt.style.height = `${inpt.scrollHeight}px`\n\n    window.scrollTo(scrollLeft, scrollTop)\n  }\n\n  const encodeuri = uri => {\n    // Encode the URI component.\n\n    return encodeURIComponent(uri).replace(/[.!'()*]/g, c => {\n      return `%${c.charCodeAt(0).toString(16)}`\n    })\n  }\n\n  const pyexecute = async currentID => {\n    // Execute Python code inside a playground\n\n    const currentInputs = document.getElementById(`__playground-inputs_${currentID}`)\n    currentInputs.setAttribute(\"readonly\", \"\")\n    pyodide.globals.set(\"id_num\", currentID)\n    pyodide.globals.set(\"action\", \"playground\")\n    pyodide.globals.set('wheels', window.color_notebook.playground_wheels)\n    await pyodide.runPythonAsync(pycode)\n    currentInputs.removeAttribute(\"readonly\")\n  }\n\n  const pyrender = async text => {\n    // Render an entire notebook page\n\n    pyodide.globals.set(\"content\", text)\n    pyodide.globals.set(\"action\", \"notebook\")\n    pyodide.globals.set('wheels', window.color_notebook.notebook_wheels)\n    await pyodide.runPythonAsync(pycode)\n    const src = document.getElementById(\"__notebook-input\")\n    if (src) {\n      raw = text\n      src.value = text\n    }\n    if (window.location.hash) {\n      // Force jumping to hashes\n      window.location.href = window.location.href // eslint-disable-line no-self-assign\n    }\n  }\n\n  const setupPyodide = async() => {\n    // Load `Pyodide` and the any default packages we can need and can load.\n\n    if (!initialized) {\n      initialized = true\n      pyodide = await loadPyodide({ // eslint-disable-line no-undef\n        indexURL: \"https://cdn.jsdelivr.net/pyodide/v0.20.0/full/\",\n        fullStdLib: false\n      })\n      await pyodide.loadPackage([\"micropip\"])\n    }\n  }\n\n  const showBusy = (target, label, relative) => {\n    // Show busy indicator\n\n    const loaderLabel = (typeof label === \"undefined\" || label === null) ? \"Loading...\" : label\n    const classes = relative ? \"loading relative\" : \"loading\"\n    const template = document.createElement(\"template\")\n    template.innerHTML = `<div class=\"${classes}\"><div class=\"loader\"></div><div>${loaderLabel}</div></div>`\n    target.appendChild(template.content.firstChild)\n  }\n\n  const hideBusy = target => {\n    // Hide busy indicator\n\n    const loading = target.querySelector(\".loading\")\n    if (loading) {\n      target.removeChild(target.querySelector(\".loading\"))\n    }\n  }\n\n  const popState = () => {\n    // Handle notebook history\n\n    const base = window.location.pathname.split('/')[1]\n    if (\n      window.location.pathname === `/${base}/playground/`\n    ) {\n      const current = decodeURIComponent(new URLSearchParams(window.location.search).toString())\n      if (current !== lastSearch) {\n        main(false) // eslint-disable-line no-use-before-define\n      }\n    }\n  }\n\n  const interceptClickEvent = e => {\n    // Catch links to other notebook pages and handle them\n\n    const base = window.location.pathname.split('/')[1]\n    const target = e.target || e.srcElement\n    if (target.tagName === \"A\" && main) { // eslint-disable-line no-use-before-define\n      if (\n        target.getAttribute(\"href\") &&\n        target.host === window.location.host &&\n        window.location.pathname === `/${base}/playground/\"` &&\n        window.location.pathname === target.pathname &&\n        window.location.search !== target.search\n      ) {\n        e.preventDefault()\n        const search = new URLSearchParams(target.search)\n        const state = {}\n        for (const [key, value] of search) {\n          state[key] = value\n        }\n        history.pushState(state, \"\", target.href)\n        main(false) // eslint-disable-line no-use-before-define\n      }\n    }\n  }\n\n  const handleTab = e => {\n    // Prevent tab from tabbing out.\n\n    if (e.key === 'Tab') {\n      const target = e.target\n\n      if (target.selectionStart !== target.selectionEnd) {\n        e.preventDefault()\n\n        let start = target.selectionStart\n        let end = target.selectionEnd\n\n        const text = target.value\n\n        while (start > 0 && text[start - 1] !== '\\n') {\n          start--\n        }\n        while (end > 0 && text[end - 1] !== '\\n' && end < text.length) {\n          end++\n        }\n\n        let lines = text.substr(start, end - start).split('\\n')\n\n        for (let i = 0; i < lines.length; i++) {\n\n          // Don't indent last line if cursor at start of line\n          if (i === lines.length - 1 && lines[i].length === 0) {\n            continue\n          }\n\n          // Indent or deindent\n          if (e.shiftKey) {\n            lines[i] = lines[i].replace(tabStart, '')\n          } else {\n            lines[i] = `    ${lines[i]}`\n          }\n        }\n        lines = lines.join('\\n')\n\n        // Update the text area\n        target.value = text.substr(0, start) + lines + text.substr(end)\n        target.selectionStart = start\n        target.selectionEnd = start + lines.length\n      }\n    }\n  }\n\n  const init = async first => {\n    // Setup input highlighting and events to run Python code blocks.\n\n    const notebook = document.getElementById(\"__notebook-source\")\n    const playgrounds = document.querySelectorAll(\".playground\")\n    playgrounds.forEach(pg => {\n\n      const currentID = pg.id.replace(reIdNum, \"$1\")\n      const inputs = document.getElementById(`__playground-inputs_${currentID}`)\n      const results = document.getElementById(`__playground-results_${currentID}`)\n      const pgcode = document.getElementById(`__playground-code_${currentID}`)\n      const buttonEdit = document.querySelector(`button#__playground-edit_${currentID}`)\n      const buttonShare = document.querySelector(`button#__playground-share_${currentID}`)\n      const buttonRun = document.querySelector(`button#__playground-run_${currentID}`)\n      const buttonCancel = document.querySelector(`button#__playground-cancel_${currentID}`)\n\n      inputs.addEventListener(\"input\", () => {\n        // Adjust textarea height on text input.\n\n        textResize(inputs)\n      })\n\n      inputs.addEventListener('keydown', handleTab)\n\n      if (notebook && first) {\n        const notebookInput = document.getElementById(\"__notebook-input\")\n\n        notebookInput.addEventListener(\"input\", e => {\n          // Adjust textarea height on text input.\n\n          textResize(e.target)\n        })\n\n        notebookInput.addEventListener('keydown', handleTab)\n\n        const editPage = document.getElementById(\"__notebook-edit\")\n        editPage.addEventListener(\"click\", () => {\n          editTemp[notebookInput.id] = notebookInput.value\n          document.getElementById(\"__notebook-render\").classList.toggle(\"hidden\")\n          document.getElementById(\"__notebook-source\").classList.toggle(\"hidden\")\n          textResize(document.getElementById(\"__notebook-input\"))\n        })\n\n        document.getElementById(\"__notebook-md-gist\").addEventListener(\"click\", async e => {\n          let uri = prompt(\"Please enter link to the Markdown page source:\", gist) // eslint-disable-line no-alert\n          if (uri !== null) {\n            uri = encodeuri(uri)\n            e.preventDefault()\n            history.pushState({notebook: uri}, \"\", `?${new URLSearchParams(`notebook=${uri}`).toString()}`)\n            main(false) // eslint-disable-line no-use-before-define\n          }\n        })\n\n        document.getElementById(\"__notebook-py-gist\").addEventListener(\"click\", async e => {\n          let uri = prompt(\"Please enter the link to the Python code source:\", gist) // eslint-disable-line no-alert\n          if (uri !== null) {\n            uri = encodeuri(uri)\n            e.preventDefault()\n            history.pushState({source: uri}, \"\", `?${new URLSearchParams(`source=${uri}`).toString()}`)\n            main(false) // eslint-disable-line no-use-before-define\n          }\n        })\n\n        document.getElementById(\"__notebook-input\").value = raw\n        document.getElementById(\"__notebook-cancel\").addEventListener(\"click\", () => {\n          notebookInput.value = editTemp[notebookInput.id]\n          delete editTemp[notebookInput.id]\n          document.getElementById(\"__notebook-render\").classList.toggle(\"hidden\")\n          document.getElementById(\"__notebook-source\").classList.toggle(\"hidden\")\n        })\n\n        document.getElementById(\"__notebook-submit\").addEventListener(\"click\", async() => {\n          const render = document.getElementById(\"__notebook-render\")\n          raw = document.getElementById(\"__notebook-input\").value\n          render.classList.toggle(\"hidden\")\n          document.getElementById(\"__notebook-source\").classList.toggle(\"hidden\")\n          const article = document.querySelector(\"article\")\n          showBusy(article, \"Loading Notebook...\")\n          render.innerHTML = \"\"\n          editTemp = {}\n          await setupPyodide()\n          await pyrender(raw)\n          await init()\n          hideBusy(article)\n        })\n      }\n\n      inputs.addEventListener(\"touchmove\", e => {\n        // Stop propagation on \"touchmove\".\n\n        e.stopPropagation()\n      })\n\n      results.addEventListener(\"click\", e => {\n        // Handle clicks on results and copies color from single color swatch when clicked.\n\n        const el = e.target\n        if (el.matches('span.swatch-color')) {\n          let content = ''\n          const parent = el.parentNode\n          if (!parent.matches('span.swatch-gradient')) {\n            content = parent.getAttribute('title').replace('Copy to clipboard', '')\n            content = content.replace('\\n', '')\n            if (window.clipboardData && window.clipboardData.setData) {\n              // Old `IE`` handling, do we really need this?\n              return window.clipboardData.setData(\"Text\", content)\n            } else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n              const textarea = document.createElement(\"textarea\")\n              textarea.textContent = content\n              textarea.style.position = \"fixed\"\n              document.body.appendChild(textarea)\n              textarea.select()\n              try {\n                return document.execCommand(\"copy\")\n              } catch (ex) {\n                return prompt(\"Copy to clipboard: Ctrl+C, Enter\", content) // eslint-disable-line no-alert\n              } finally {\n                document.body.removeChild(textarea)\n              }\n            }\n          }\n        }\n      })\n\n      buttonEdit.addEventListener(\"click\", async() => {\n        // Handle the button click: show source or execute source.\n\n        editTemp[currentID] = inputs.value\n        pgcode.classList.toggle(\"hidden\")\n        results.classList.toggle(\"hidden\")\n        buttonRun.classList.toggle(\"hidden\")\n        buttonCancel.classList.toggle(\"hidden\")\n        buttonEdit.classList.toggle(\"hidden\")\n        buttonShare.classList.toggle(\"hidden\")\n        textResize(inputs)\n        inputs.focus()\n      })\n\n      buttonShare.addEventListener(\"click\", async() => {\n        // Handle the share click: copy URL with code as parameter.\n\n        const base = window.location.pathname.split('/')[1]\n        const uri = encodeuri(inputs.value)\n        const loc = window.location\n        let pathname = \"/playground/\"\n        if (loc.pathname.startsWith(`/${base}/`)) {\n          pathname = `/${base}/playground/`\n        }\n        const path = `${loc.protocol}//${loc.host}${pathname}?code=${uri}`\n        if (uri.length > 1000) {\n          alert(\"Code must be under a 1000 characters to generate a URL!\") // eslint-disable-line no-alert\n        } else {\n          navigator.clipboard.writeText(path).then(async() => {\n            alert(\"Link copied to clipboard :)\") // eslint-disable-line no-alert\n          }, async() => {\n            alert(\"Failed to copy link clipboard!\") // eslint-disable-line no-alert\n          })\n        }\n      })\n\n      buttonRun.addEventListener(\"click\", async() => {\n        // Handle the button click: show source or execute source.\n\n        if (busy) {\n          return\n        }\n\n        busy = true\n        // Load Pyodide and related packages.\n        const form = pgcode.querySelector(\"form\")\n        showBusy(form, null, true)\n        const buttons = document.querySelectorAll(\".playground .playground-run\")\n        if (buttons) {\n          buttons.forEach(b => {\n            b.setAttribute(\"disabled\", \"\")\n          })\n        }\n        await setupPyodide()\n        results.querySelector(\"code\").innerHTML = \"\"\n        await pyexecute(currentID)\n        if (buttons) {\n          buttons.forEach(b => {\n            b.removeAttribute(\"disabled\")\n          })\n        }\n        hideBusy(form)\n        pgcode.classList.toggle(\"hidden\")\n        results.classList.toggle(\"hidden\")\n        buttonEdit.classList.toggle(\"hidden\")\n        buttonShare.classList.toggle(\"hidden\")\n        buttonRun.classList.toggle(\"hidden\")\n        buttonCancel.classList.toggle(\"hidden\")\n\n        delete editTemp[currentID]\n        busy = false\n      })\n\n      buttonCancel.addEventListener(\"click\", () => {\n        // Cancel edit.\n\n        inputs.value = editTemp[currentID]\n        delete editTemp[currentID]\n        pgcode.classList.toggle(\"hidden\")\n        results.classList.toggle(\"hidden\")\n        buttonEdit.classList.toggle(\"hidden\")\n        buttonShare.classList.toggle(\"hidden\")\n        buttonRun.classList.toggle(\"hidden\")\n        buttonCancel.classList.toggle(\"hidden\")\n      })\n    })\n  }\n\n  const main = async first => {\n    // Load external source to render in a playground.\n    // This can be something like a file on a gist we must read in (?source=)\n    // or raw code (?code=).\n\n    editTemp = {}\n\n    if (window.location.pathname.endsWith(\"/playground/\")) {\n      const params = new URLSearchParams(window.location.search)\n      const loadMsg = \"Loading Pyodide...\"\n      const pageMsg = \"Loading Notebook...\"\n      const uri = params.has(\"source\") ? params.get(\"source\") : params.get(\"notebook\")\n      const article = document.querySelector(\"article\")\n      if (uri !== null && uri.trim()) {\n        // A source was specified, so load it.\n        showBusy(article, loadMsg)\n        await setupPyodide()\n        hideBusy(article)\n        showBusy(article, pageMsg)\n        try {\n          const gistType = params.has(\"source\") ? \"source\" : \"notebook\"\n          lastSearch = decodeURIComponent(params.toString())\n          let value = \"\"\n          const xhr = new XMLHttpRequest()\n          gist = uri\n          xhr.open(\"GET\", uri, true)\n          xhr.onload = async() => {\n            // Try and load the requested content\n            if (xhr.readyState === 4) {\n              if (xhr.status === 200) {\n                value = xhr.responseText\n              }\n            }\n\n            if (gistType === \"source\") {\n              value = getContent(value)\n            }\n            await pyrender(value)\n            await init(first)\n            hideBusy(article)\n            fakeDOMContentLoaded()\n          }\n          xhr.send()\n        } catch (err) {} // eslint-disable-line no-empty\n      } else {\n        gist = \"\"\n        const content = getContent(params.has(\"code\") ? params.get(\"code\") : defContent)\n        lastSearch = decodeURIComponent(params.toString())\n        showBusy(article, loadMsg)\n        await setupPyodide()\n        hideBusy(article)\n        showBusy(article, pageMsg)\n        await pyrender(content)\n        await init(first)\n        hideBusy(article)\n        fakeDOMContentLoaded()\n      }\n    } else {\n      gist = \"\"\n      lastSearch = \"\"\n      init(first)\n    }\n  }\n\n  // Capture links in notebook pages so that we can make playgound links load instantly\n  document.addEventListener(\"click\", interceptClickEvent)\n\n  // Handle history of notebook pages as they are loaded dynamically\n  window.addEventListener(\"popstate\", popState)\n\n  // Before leaving, turn off fake, just in case we navigated away before finished\n  window.addEventListener(\"unload\", () => {\n    fake = true\n  })\n\n  // Attach main via subscribe (subscribes to Materials on page load and instant page loads)\n  window.document$.subscribe(() => {\n    // To get other libraries to to reload, we may create a fake `DOMContentLoaded`\n    // No need to process these events.\n    if (fake) {\n      fake = false\n      return\n    }\n    main(true)\n  })\n})()\n"],"names":["pyodide","busy","raw","gist","editTemp","reIdNum","initialized","lastSearch","fake","tabStart","pycode","defContent","getContent","fakeDOMContentLoaded","textResize","encodeuri","pyexecute","pyrender","setupPyodide","showBusy","hideBusy","handleTab","init","main","window","color_notebook","default_playground","content","concat","document","dispatchEvent","Event","bubbles","cancelable","inpt","scrollLeft","pageXOffset","documentElement","body","parentNode","scrollTop","pageYOffset","style","height","scrollHeight","scrollTo","uri","encodeURIComponent","replace","c","charCodeAt","toString","_ref","_asyncToGenerator","_regeneratorRuntime","mark","currentID","currentInputs","wrap","_context","prev","next","getElementById","setAttribute","globals","set","playground_wheels","runPythonAsync","removeAttribute","stop","_callee","_x","apply","this","arguments","_ref2","text","src","_context2","notebook_wheels","value","location","hash","href","_callee2","_x2","_ref3","_callee3","_context3","loadPyodide","indexURL","fullStdLib","sent","loadPackage","target","label","relative","loaderLabel","classes","template","createElement","innerHTML","appendChild","firstChild","querySelector","removeChild","e","key","selectionStart","selectionEnd","preventDefault","start","end","length","lines","substr","split","i","shiftKey","join","_ref4","first","notebook","_context12","querySelectorAll","forEach","pg","id","inputs","results","pgcode","buttonEdit","buttonShare","buttonRun","buttonCancel","addEventListener","notebookInput","classList","toggle","_ref5","_context4","prompt","history","pushState","URLSearchParams","_callee4","_x4","_ref6","_context5","source","_callee5","_x5","_callee6","render","article","_context6","stopPropagation","el","matches","parent","getAttribute","clipboardData","setData","queryCommandSupported","textarea","textContent","position","select","execCommand","ex","_callee7","_context7","focus","_callee10","base","loc","pathname","path","_context10","startsWith","protocol","host","alert","navigator","clipboard","writeText","then","_callee8","_context8","_callee9","_context9","_callee11","form","buttons","_context11","abrupt","b","_callee12","_x3","_ref13","params","loadMsg","pageMsg","gistType","xhr","_context14","endsWith","search","has","get","trim","decodeURIComponent","XMLHttpRequest","open","onload","_callee13","_context13","readyState","status","responseText","send","err","_callee14","_x6","srcElement","tagName","_step","state","_iterator","_createForOfIteratorHelper","s","n","done","_step$value","_slicedToArray","f","document$","subscribe"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"extra-notebook-1e54adfd.js","sources":["../../../src/js/extra-notebook.js"],"sourcesContent":["(() => {\n  let pyodide = null\n  let busy = false\n  let raw = \"\"\n  let gist = \"\"\n  let editTemp = {}\n  const reIdNum = /.*?_(\\d+)$/\n  let initialized = false\n  let lastSearch = \"\"\n  let fake = false\n  const tabStart = /^( {1,4}|\\t)/\n  // This is the Python payload that will be executed when the user\n  // presses the `Run` button. It will execute the code, create a\n  // Python console output, find color references, steps, and interpolation\n  // references and render the appropriate preview.\n  const pycode = `\n{{pycode}}\n\nimport micropip\nfrom js import location\n\naction = globals().get('action')\nif action == 'notebook':\n    callback = render_notebook\nelse:\n    callback = render_console\n\nbase = location.pathname.lstrip('/').split('/')[0]\nawait micropip.install([location.origin + '/{}/playground/{}'.format(base, w) for w in wheels])\ncallback()\n`\n\n  const defContent = window.color_notebook.default_playground\n\n  const getContent = content => {\n    return `\n!!! new \"This notebook is powered by [Pyodide](https://github.com/pyodide/pyodide). \\\nLearn more [here](\\\n?notebook=https://gist.githubusercontent.com/facelessuser/7c819668b5eb248ecb9ac608d91391cf/raw/playground.md\\\n). Preview, convert, interpolate, and explore!\"\n\n\\`\\`\\`\\`\\`\\`\\`\\`playground\n${content}\n\\`\\`\\`\\`\\`\\`\\`\\`\n`\n  }\n\n  const fakeDOMContentLoaded = () => {\n    // Send a fake `DOMContentLoaded`\n    fake = true\n    window.document.dispatchEvent(new Event(\"DOMContentLoaded\", {\n      bubbles: true,\n      cancelable: true\n    }))\n  }\n\n  const textResize = inpt => {\n    // Resize inputs based on text height.\n\n    const scrollLeft = window.pageXOffset ||\n      (document.documentElement || document.body.parentNode || document.body).scrollLeft\n\n    const scrollTop  = window.pageYOffset ||\n      (document.documentElement || document.body.parentNode || document.body).scrollTop\n\n    inpt.style.height = \"5px\"\n    inpt.style.height = `${inpt.scrollHeight}px`\n\n    window.scrollTo(scrollLeft, scrollTop)\n  }\n\n  const encodeuri = uri => {\n    // Encode the URI component.\n\n    return encodeURIComponent(uri).replace(/[.!'()*]/g, c => {\n      return `%${c.charCodeAt(0).toString(16)}`\n    })\n  }\n\n  const pyexecute = async currentID => {\n    // Execute Python code inside a playground\n\n    const currentInputs = document.getElementById(`__playground-inputs_${currentID}`)\n    currentInputs.setAttribute(\"readonly\", \"\")\n    pyodide.globals.set(\"id_num\", currentID)\n    pyodide.globals.set(\"action\", \"playground\")\n    pyodide.globals.set('wheels', window.color_notebook.playground_wheels)\n    await pyodide.runPythonAsync(pycode)\n    currentInputs.removeAttribute(\"readonly\")\n  }\n\n  const pyrender = async text => {\n    // Render an entire notebook page\n\n    pyodide.globals.set(\"content\", text)\n    pyodide.globals.set(\"action\", \"notebook\")\n    pyodide.globals.set('wheels', window.color_notebook.notebook_wheels)\n    await pyodide.runPythonAsync(pycode)\n    const src = document.getElementById(\"__notebook-input\")\n    if (src) {\n      raw = text\n      src.value = text\n    }\n    if (window.location.hash) {\n      // Force jumping to hashes\n      window.location.href = window.location.href // eslint-disable-line no-self-assign\n    }\n  }\n\n  const setupPyodide = async() => {\n    // Load `Pyodide` and the any default packages we can need and can load.\n\n    if (!initialized) {\n      initialized = true\n      pyodide = await loadPyodide({ // eslint-disable-line no-undef\n        indexURL: \"https://cdn.jsdelivr.net/pyodide/v0.20.0/full/\",\n        fullStdLib: false\n      })\n      await pyodide.loadPackage([\"micropip\"])\n    }\n  }\n\n  const showBusy = (target, label, relative) => {\n    // Show busy indicator\n\n    const loaderLabel = (typeof label === \"undefined\" || label === null) ? \"Loading...\" : label\n    const classes = relative ? \"loading relative\" : \"loading\"\n    const template = document.createElement(\"template\")\n    template.innerHTML = `<div class=\"${classes}\"><div class=\"loader\"></div><div>${loaderLabel}</div></div>`\n    target.appendChild(template.content.firstChild)\n  }\n\n  const hideBusy = target => {\n    // Hide busy indicator\n\n    const loading = target.querySelector(\".loading\")\n    if (loading) {\n      target.removeChild(target.querySelector(\".loading\"))\n    }\n  }\n\n  const popState = () => {\n    // Handle notebook history\n\n    const base = window.location.pathname.split('/')[1]\n    if (\n      window.location.pathname === `/${base}/playground/`\n    ) {\n      const current = decodeURIComponent(new URLSearchParams(window.location.search).toString())\n      if (current !== lastSearch) {\n        main(false) // eslint-disable-line no-use-before-define\n      }\n    }\n  }\n\n  const interceptClickEvent = e => {\n    // Catch links to other notebook pages and handle them\n\n    const base = window.location.pathname.split('/')[1]\n    const target = e.target || e.srcElement\n    if (target.tagName === \"A\" && main) { // eslint-disable-line no-use-before-define\n      if (\n        target.getAttribute(\"href\") &&\n        target.host === window.location.host &&\n        window.location.pathname === `/${base}/playground/\"` &&\n        window.location.pathname === target.pathname &&\n        window.location.search !== target.search\n      ) {\n        e.preventDefault()\n        const search = new URLSearchParams(target.search)\n        const state = {}\n        for (const [key, value] of search) {\n          state[key] = value\n        }\n        history.pushState(state, \"\", target.href)\n        main(false) // eslint-disable-line no-use-before-define\n      }\n    }\n  }\n\n  const handleTab = e => {\n    // Prevent tab from tabbing out.\n\n    if (e.key === 'Tab') {\n      const target = e.target\n\n      if (target.selectionStart !== target.selectionEnd) {\n        e.preventDefault()\n\n        let start = target.selectionStart\n        let end = target.selectionEnd\n\n        const text = target.value\n\n        while (start > 0 && text[start - 1] !== '\\n') {\n          start--\n        }\n        while (end > 0 && text[end - 1] !== '\\n' && end < text.length) {\n          end++\n        }\n\n        let lines = text.substr(start, end - start).split('\\n')\n\n        for (let i = 0; i < lines.length; i++) {\n\n          // Don't indent last line if cursor at start of line\n          if (i === lines.length - 1 && lines[i].length === 0) {\n            continue\n          }\n\n          // Indent or deindent\n          if (e.shiftKey) {\n            lines[i] = lines[i].replace(tabStart, '')\n          } else {\n            lines[i] = `    ${lines[i]}`\n          }\n        }\n        lines = lines.join('\\n')\n\n        // Update the text area\n        target.value = text.substr(0, start) + lines + text.substr(end)\n        target.selectionStart = start\n        target.selectionEnd = start + lines.length\n      }\n    }\n  }\n\n  const init = async first => {\n    // Setup input highlighting and events to run Python code blocks.\n\n    const notebook = document.getElementById(\"__notebook-source\")\n    const playgrounds = document.querySelectorAll(\".playground\")\n    playgrounds.forEach(pg => {\n\n      const currentID = pg.id.replace(reIdNum, \"$1\")\n      const inputs = document.getElementById(`__playground-inputs_${currentID}`)\n      const results = document.getElementById(`__playground-results_${currentID}`)\n      const pgcode = document.getElementById(`__playground-code_${currentID}`)\n      const buttonEdit = document.querySelector(`button#__playground-edit_${currentID}`)\n      const buttonShare = document.querySelector(`button#__playground-share_${currentID}`)\n      const buttonRun = document.querySelector(`button#__playground-run_${currentID}`)\n      const buttonCancel = document.querySelector(`button#__playground-cancel_${currentID}`)\n\n      inputs.addEventListener(\"input\", () => {\n        // Adjust textarea height on text input.\n\n        textResize(inputs)\n      })\n\n      inputs.addEventListener('keydown', handleTab)\n\n      if (notebook && first) {\n        const notebookInput = document.getElementById(\"__notebook-input\")\n\n        notebookInput.addEventListener(\"input\", e => {\n          // Adjust textarea height on text input.\n\n          textResize(e.target)\n        })\n\n        notebookInput.addEventListener('keydown', handleTab)\n\n        const editPage = document.getElementById(\"__notebook-edit\")\n        editPage.addEventListener(\"click\", () => {\n          editTemp[notebookInput.id] = notebookInput.value\n          document.getElementById(\"__notebook-render\").classList.toggle(\"hidden\")\n          document.getElementById(\"__notebook-source\").classList.toggle(\"hidden\")\n          textResize(document.getElementById(\"__notebook-input\"))\n        })\n\n        document.getElementById(\"__notebook-md-gist\").addEventListener(\"click\", async e => {\n          let uri = prompt(\"Please enter link to the Markdown page source:\", gist) // eslint-disable-line no-alert\n          if (uri !== null) {\n            uri = encodeuri(uri)\n            e.preventDefault()\n            history.pushState({notebook: uri}, \"\", `?${new URLSearchParams(`notebook=${uri}`).toString()}`)\n            main(false) // eslint-disable-line no-use-before-define\n          }\n        })\n\n        document.getElementById(\"__notebook-py-gist\").addEventListener(\"click\", async e => {\n          let uri = prompt(\"Please enter the link to the Python code source:\", gist) // eslint-disable-line no-alert\n          if (uri !== null) {\n            uri = encodeuri(uri)\n            e.preventDefault()\n            history.pushState({source: uri}, \"\", `?${new URLSearchParams(`source=${uri}`).toString()}`)\n            main(false) // eslint-disable-line no-use-before-define\n          }\n        })\n\n        document.getElementById(\"__notebook-input\").value = raw\n        document.getElementById(\"__notebook-cancel\").addEventListener(\"click\", () => {\n          notebookInput.value = editTemp[notebookInput.id]\n          delete editTemp[notebookInput.id]\n          document.getElementById(\"__notebook-render\").classList.toggle(\"hidden\")\n          document.getElementById(\"__notebook-source\").classList.toggle(\"hidden\")\n        })\n\n        document.getElementById(\"__notebook-submit\").addEventListener(\"click\", async() => {\n          const render = document.getElementById(\"__notebook-render\")\n          raw = document.getElementById(\"__notebook-input\").value\n          render.classList.toggle(\"hidden\")\n          document.getElementById(\"__notebook-source\").classList.toggle(\"hidden\")\n          const article = document.querySelector(\"article\")\n          showBusy(article, \"Loading Notebook...\")\n          render.innerHTML = \"\"\n          editTemp = {}\n          await setupPyodide()\n          await pyrender(raw)\n          await init()\n          hideBusy(article)\n        })\n      }\n\n      inputs.addEventListener(\"touchmove\", e => {\n        // Stop propagation on \"touchmove\".\n\n        e.stopPropagation()\n      })\n\n      results.addEventListener(\"click\", e => {\n        // Handle clicks on results and copies color from single color swatch when clicked.\n\n        const el = e.target\n        if (el.matches('span.swatch-color')) {\n          let content = ''\n          const parent = el.parentNode\n          if (!parent.matches('span.swatch-gradient')) {\n            content = parent.getAttribute('title').replace('Copy to clipboard', '')\n            content = content.replace('\\n', '')\n            if (window.clipboardData && window.clipboardData.setData) {\n              // Old `IE`` handling, do we really need this?\n              return window.clipboardData.setData(\"Text\", content)\n            } else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n              const textarea = document.createElement(\"textarea\")\n              textarea.textContent = content\n              textarea.style.position = \"fixed\"\n              document.body.appendChild(textarea)\n              textarea.select()\n              try {\n                return document.execCommand(\"copy\")\n              } catch (ex) {\n                return prompt(\"Copy to clipboard: Ctrl+C, Enter\", content) // eslint-disable-line no-alert\n              } finally {\n                document.body.removeChild(textarea)\n              }\n            }\n          }\n        }\n      })\n\n      buttonEdit.addEventListener(\"click\", async() => {\n        // Handle the button click: show source or execute source.\n\n        editTemp[currentID] = inputs.value\n        pgcode.classList.toggle(\"hidden\")\n        results.classList.toggle(\"hidden\")\n        buttonRun.classList.toggle(\"hidden\")\n        buttonCancel.classList.toggle(\"hidden\")\n        buttonEdit.classList.toggle(\"hidden\")\n        buttonShare.classList.toggle(\"hidden\")\n        textResize(inputs)\n        inputs.focus()\n      })\n\n      buttonShare.addEventListener(\"click\", async() => {\n        // Handle the share click: copy URL with code as parameter.\n\n        const base = window.location.pathname.split('/')[1]\n        const uri = encodeuri(inputs.value)\n        const loc = window.location\n        let pathname = \"/playground/\"\n        if (loc.pathname.startsWith(`/${base}/`)) {\n          pathname = `/${base}/playground/`\n        }\n        const path = `${loc.protocol}//${loc.host}${pathname}?code=${uri}`\n        if (uri.length > 1000) {\n          alert(\"Code must be under a 1000 characters to generate a URL!\") // eslint-disable-line no-alert\n        } else {\n          navigator.clipboard.writeText(path).then(async() => {\n            alert(\"Link copied to clipboard :)\") // eslint-disable-line no-alert\n          }, async() => {\n            alert(\"Failed to copy link clipboard!\") // eslint-disable-line no-alert\n          })\n        }\n      })\n\n      buttonRun.addEventListener(\"click\", async() => {\n        // Handle the button click: show source or execute source.\n\n        if (busy) {\n          return\n        }\n\n        busy = true\n        // Load Pyodide and related packages.\n        const form = pgcode.querySelector(\"form\")\n        showBusy(form, null, true)\n        const buttons = document.querySelectorAll(\".playground .playground-run\")\n        if (buttons) {\n          buttons.forEach(b => {\n            b.setAttribute(\"disabled\", \"\")\n          })\n        }\n        await setupPyodide()\n        results.querySelector(\"code\").innerHTML = \"\"\n        await pyexecute(currentID)\n        if (buttons) {\n          buttons.forEach(b => {\n            b.removeAttribute(\"disabled\")\n          })\n        }\n        hideBusy(form)\n        pgcode.classList.toggle(\"hidden\")\n        results.classList.toggle(\"hidden\")\n        buttonEdit.classList.toggle(\"hidden\")\n        buttonShare.classList.toggle(\"hidden\")\n        buttonRun.classList.toggle(\"hidden\")\n        buttonCancel.classList.toggle(\"hidden\")\n\n        delete editTemp[currentID]\n        busy = false\n      })\n\n      buttonCancel.addEventListener(\"click\", () => {\n        // Cancel edit.\n\n        inputs.value = editTemp[currentID]\n        delete editTemp[currentID]\n        pgcode.classList.toggle(\"hidden\")\n        results.classList.toggle(\"hidden\")\n        buttonEdit.classList.toggle(\"hidden\")\n        buttonShare.classList.toggle(\"hidden\")\n        buttonRun.classList.toggle(\"hidden\")\n        buttonCancel.classList.toggle(\"hidden\")\n      })\n    })\n  }\n\n  const main = async first => {\n    // Load external source to render in a playground.\n    // This can be something like a file on a gist we must read in (?source=)\n    // or raw code (?code=).\n\n    editTemp = {}\n\n    if (window.location.pathname.endsWith(\"/playground/\")) {\n      const params = new URLSearchParams(window.location.search)\n      const loadMsg = \"Loading Pyodide...\"\n      const pageMsg = \"Loading Notebook...\"\n      const uri = params.has(\"source\") ? params.get(\"source\") : params.get(\"notebook\")\n      const article = document.querySelector(\"article\")\n      if (uri !== null && uri.trim()) {\n        // A source was specified, so load it.\n        showBusy(article, loadMsg)\n        await setupPyodide()\n        hideBusy(article)\n        showBusy(article, pageMsg)\n        try {\n          const gistType = params.has(\"source\") ? \"source\" : \"notebook\"\n          lastSearch = decodeURIComponent(params.toString())\n          let value = \"\"\n          const xhr = new XMLHttpRequest()\n          gist = uri\n          xhr.open(\"GET\", uri, true)\n          xhr.onload = async() => {\n            // Try and load the requested content\n            if (xhr.readyState === 4) {\n              if (xhr.status === 200) {\n                value = xhr.responseText\n              }\n            }\n\n            if (gistType === \"source\") {\n              value = getContent(value)\n            }\n            await pyrender(value)\n            await init(first)\n            hideBusy(article)\n            fakeDOMContentLoaded()\n          }\n          xhr.send()\n        } catch (err) {} // eslint-disable-line no-empty\n      } else {\n        gist = \"\"\n        const content = getContent(params.has(\"code\") ? params.get(\"code\") : defContent)\n        lastSearch = decodeURIComponent(params.toString())\n        showBusy(article, loadMsg)\n        await setupPyodide()\n        hideBusy(article)\n        showBusy(article, pageMsg)\n        await pyrender(content)\n        await init(first)\n        hideBusy(article)\n        fakeDOMContentLoaded()\n      }\n    } else {\n      gist = \"\"\n      lastSearch = \"\"\n      init(first)\n    }\n  }\n\n  // Capture links in notebook pages so that we can make playgound links load instantly\n  document.addEventListener(\"click\", interceptClickEvent)\n\n  // Handle history of notebook pages as they are loaded dynamically\n  window.addEventListener(\"popstate\", popState)\n\n  // Before leaving, turn off fake, just in case we navigated away before finished\n  window.addEventListener(\"unload\", () => {\n    fake = true\n  })\n\n  // Attach main via subscribe (subscribes to Materials on page load and instant page loads)\n  window.document$.subscribe(() => {\n    // To get other libraries to to reload, we may create a fake `DOMContentLoaded`\n    // No need to process these events.\n    if (fake) {\n      fake = false\n      return\n    }\n    main(true)\n  })\n})()\n"],"names":["pyodide","busy","raw","gist","editTemp","reIdNum","initialized","lastSearch","fake","tabStart","pycode","defContent","getContent","fakeDOMContentLoaded","textResize","encodeuri","pyexecute","pyrender","setupPyodide","showBusy","hideBusy","handleTab","init","main","window","color_notebook","default_playground","content","concat","document","dispatchEvent","Event","bubbles","cancelable","inpt","scrollLeft","pageXOffset","documentElement","body","parentNode","scrollTop","pageYOffset","style","height","scrollHeight","scrollTo","uri","encodeURIComponent","replace","c","charCodeAt","toString","_ref","_asyncToGenerator","_regeneratorRuntime","mark","currentID","currentInputs","wrap","_context","prev","next","getElementById","setAttribute","globals","set","playground_wheels","runPythonAsync","removeAttribute","stop","_callee","_x","apply","this","arguments","_ref2","text","src","_context2","notebook_wheels","value","location","hash","href","_callee2","_x2","_ref3","_callee3","_context3","loadPyodide","indexURL","fullStdLib","sent","loadPackage","target","label","relative","loaderLabel","classes","template","createElement","innerHTML","appendChild","firstChild","querySelector","removeChild","e","key","selectionStart","selectionEnd","preventDefault","start","end","length","lines","substr","split","i","shiftKey","join","_ref4","first","notebook","_context12","querySelectorAll","forEach","pg","id","inputs","results","pgcode","buttonEdit","buttonShare","buttonRun","buttonCancel","addEventListener","notebookInput","classList","toggle","_ref5","_context4","prompt","history","pushState","URLSearchParams","_callee4","_x4","_ref6","_context5","source","_callee5","_x5","_callee6","render","article","_context6","stopPropagation","el","matches","parent","getAttribute","clipboardData","setData","queryCommandSupported","textarea","textContent","position","select","execCommand","ex","_callee7","_context7","focus","_callee10","base","loc","pathname","path","_context10","startsWith","protocol","host","alert","navigator","clipboard","writeText","then","_callee8","_context8","_callee9","_context9","_callee11","form","buttons","_context11","abrupt","b","_callee12","_x3","_ref13","params","loadMsg","pageMsg","gistType","xhr","_context14","endsWith","search","has","get","trim","decodeURIComponent","XMLHttpRequest","open","onload","_callee13","_context13","readyState","status","responseText","send","err","_callee14","_x6","srcElement","tagName","_step","state","_iterator","_createForOfIteratorHelper","s","n","done","_step$value","_slicedToArray","f","document$","subscribe"],"mappings":"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"}
\ No newline at end of file
diff --git a/docs/theme/assets/coloraide-extras/extra-notebook-a80f0a52.js b/docs/theme/assets/coloraide-extras/extra-notebook-a80f0a52.js
deleted file mode 100644
index 1b1cfad99..000000000
--- a/docs/theme/assets/coloraide-extras/extra-notebook-a80f0a52.js
+++ /dev/null
@@ -1,4 +0,0 @@
-function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}var runtime=function(e){"use strict";var t,n=Object.prototype,o=n.hasOwnProperty,r="function"==typeof Symbol?Symbol:{},a=r.iterator||"@@iterator",i=r.asyncIterator||"@@asyncIterator",s=r.toStringTag||"@@toStringTag";function c(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{c({},"")}catch(e){c=function(e,t,n){return e[t]=n}}function l(e,t,n,o){var r=t&&t.prototype instanceof y?t:y,a=Object.create(r.prototype),i=new T(o||[]);return a._invoke=function(e,t,n){var o=d;return function(r,a){if(o===f)throw new Error("Generator is already running");if(o===m){if("throw"===r)throw a;return I()}for(n.method=r,n.arg=a;;){var i=n.delegate;if(i){var s=L(i,n);if(s){if(s===h)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===d)throw o=m,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=f;var c=u(e,t,n);if("normal"===c.type){if(o=n.done?m:p,c.arg===h)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(o=m,n.method="throw",n.arg=c.arg)}}}(e,n,i),a}function u(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}e.wrap=l;var d="suspendedStart",p="suspendedYield",f="executing",m="completed",h={};function y(){}function g(){}function v(){}var w={};c(w,a,(function(){return this}));var _=Object.getPrototypeOf,x=_&&_(_(P([])));x&&x!==n&&o.call(x,a)&&(w=x);var b=v.prototype=y.prototype=Object.create(w);function E(e){["next","throw","return"].forEach((function(t){c(e,t,(function(e){return this._invoke(t,e)}))}))}function k(e,t){function n(r,a,i,s){var c=u(e[r],e,a);if("throw"!==c.type){var l=c.arg,d=l.value;return d&&"object"===_typeof(d)&&o.call(d,"__await")?t.resolve(d.__await).then((function(e){n("next",e,i,s)}),(function(e){n("throw",e,i,s)})):t.resolve(d).then((function(e){l.value=e,i(l)}),(function(e){return n("throw",e,i,s)}))}s(c.arg)}var r;this._invoke=function(e,o){function a(){return new t((function(t,r){n(e,o,t,r)}))}return r=r?r.then(a,a):a()}}function L(e,n){var o=e.iterator[n.method];if(o===t){if(n.delegate=null,"throw"===n.method){if(e.iterator.return&&(n.method="return",n.arg=t,L(e,n),"throw"===n.method))return h;n.method="throw",n.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var r=u(o,e.iterator,n.arg);if("throw"===r.type)return n.method="throw",n.arg=r.arg,n.delegate=null,h;var a=r.arg;return a?a.done?(n[e.resultName]=a.value,n.next=e.nextLoc,"return"!==n.method&&(n.method="next",n.arg=t),n.delegate=null,h):a:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,h)}function S(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function C(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function T(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(S,this),this.reset(!0)}function P(e){if(e){var n=e[a];if(n)return n.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,i=function n(){for(;++r<e.length;)if(o.call(e,r))return n.value=e[r],n.done=!1,n;return n.value=t,n.done=!0,n};return i.next=i}}return{next:I}}function I(){return{value:t,done:!0}}return g.prototype=v,c(b,"constructor",v),c(v,"constructor",g),g.displayName=c(v,s,"GeneratorFunction"),e.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===g||"GeneratorFunction"===(t.displayName||t.name))},e.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,v):(e.__proto__=v,c(e,s,"GeneratorFunction")),e.prototype=Object.create(b),e},e.awrap=function(e){return{__await:e}},E(k.prototype),c(k.prototype,i,(function(){return this})),e.AsyncIterator=k,e.async=function(t,n,o,r,a){void 0===a&&(a=Promise);var i=new k(l(t,n,o,r),a);return e.isGeneratorFunction(n)?i:i.next().then((function(e){return e.done?e.value:i.next()}))},E(b),c(b,s,"Generator"),c(b,a,(function(){return this})),c(b,"toString",(function(){return"[object Generator]"})),e.keys=function(e){var t=[];for(var n in e)t.push(n);return t.reverse(),function n(){for(;t.length;){var o=t.pop();if(o in e)return n.value=o,n.done=!1,n}return n.done=!0,n}},e.values=P,T.prototype={constructor:T,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(C),!e)for(var n in this)"t"===n.charAt(0)&&o.call(this,n)&&!isNaN(+n.slice(1))&&(this[n]=t)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var n=this;function r(o,r){return s.type="throw",s.arg=e,n.next=o,r&&(n.method="next",n.arg=t),!!r}for(var a=this.tryEntries.length-1;a>=0;--a){var i=this.tryEntries[a],s=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var c=o.call(i,"catchLoc"),l=o.call(i,"finallyLoc");if(c&&l){if(this.prev<i.catchLoc)return r(i.catchLoc,!0);if(this.prev<i.finallyLoc)return r(i.finallyLoc)}else if(c){if(this.prev<i.catchLoc)return r(i.catchLoc,!0)}else{if(!l)throw new Error("try statement without catch or finally");if(this.prev<i.finallyLoc)return r(i.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var a=r;break}}a&&("break"===e||"continue"===e)&&a.tryLoc<=t&&t<=a.finallyLoc&&(a=null);var i=a?a.completion:{};return i.type=e,i.arg=t,a?(this.method="next",this.next=a.finallyLoc,h):this.complete(i)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),h},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),C(n),h}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var o=n.completion;if("throw"===o.type){var r=o.arg;C(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(e,n,o){return this.delegate={iterator:P(e),resultName:n,nextLoc:o},"next"===this.method&&(this.arg=t),h}},e}("object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports:{});try{regeneratorRuntime=runtime}catch(e){"object"===("undefined"==typeof globalThis?"undefined":_typeof(globalThis))?globalThis.regeneratorRuntime=runtime:Function("r","regeneratorRuntime = r")(runtime)}!function(){"use strict";function e(){
-/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
-e=function(){return t};var t={},n=Object.prototype,o=n.hasOwnProperty,r="function"==typeof Symbol?Symbol:{},a=r.iterator||"@@iterator",i=r.asyncIterator||"@@asyncIterator",s=r.toStringTag||"@@toStringTag";function c(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{c({},"")}catch(e){c=function(e,t,n){return e[t]=n}}function l(e,t,n,o){var r=t&&t.prototype instanceof p?t:p,a=Object.create(r.prototype),i=new k(o||[]);return a._invoke=function(e,t,n){var o="suspendedStart";return function(r,a){if("executing"===o)throw new Error("Generator is already running");if("completed"===o){if("throw"===r)throw a;return S()}for(n.method=r,n.arg=a;;){var i=n.delegate;if(i){var s=x(i,n);if(s){if(s===d)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if("suspendedStart"===o)throw o="completed",n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o="executing";var c=u(e,t,n);if("normal"===c.type){if(o=n.done?"completed":"suspendedYield",c.arg===d)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(o="completed",n.method="throw",n.arg=c.arg)}}}(e,n,i),a}function u(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}t.wrap=l;var d={};function p(){}function f(){}function m(){}var h={};c(h,a,(function(){return this}));var y=Object.getPrototypeOf,g=y&&y(y(L([])));g&&g!==n&&o.call(g,a)&&(h=g);var v=m.prototype=p.prototype=Object.create(h);function w(e){["next","throw","return"].forEach((function(t){c(e,t,(function(e){return this._invoke(t,e)}))}))}function _(e,t){function n(r,a,i,s){var c=u(e[r],e,a);if("throw"!==c.type){var l=c.arg,d=l.value;return d&&"object"==_typeof(d)&&o.call(d,"__await")?t.resolve(d.__await).then((function(e){n("next",e,i,s)}),(function(e){n("throw",e,i,s)})):t.resolve(d).then((function(e){l.value=e,i(l)}),(function(e){return n("throw",e,i,s)}))}s(c.arg)}var r;this._invoke=function(e,o){function a(){return new t((function(t,r){n(e,o,t,r)}))}return r=r?r.then(a,a):a()}}function x(e,t){var n=e.iterator[t.method];if(void 0===n){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=void 0,x(e,t),"throw"===t.method))return d;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return d}var o=u(n,e.iterator,t.arg);if("throw"===o.type)return t.method="throw",t.arg=o.arg,t.delegate=null,d;var r=o.arg;return r?r.done?(t[e.resultName]=r.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,d):r:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,d)}function b(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function E(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function k(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(b,this),this.reset(!0)}function L(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,r=function t(){for(;++n<e.length;)if(o.call(e,n))return t.value=e[n],t.done=!1,t;return t.value=void 0,t.done=!0,t};return r.next=r}}return{next:S}}function S(){return{value:void 0,done:!0}}return f.prototype=m,c(v,"constructor",m),c(m,"constructor",f),f.displayName=c(m,s,"GeneratorFunction"),t.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===f||"GeneratorFunction"===(t.displayName||t.name))},t.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,m):(e.__proto__=m,c(e,s,"GeneratorFunction")),e.prototype=Object.create(v),e},t.awrap=function(e){return{__await:e}},w(_.prototype),c(_.prototype,i,(function(){return this})),t.AsyncIterator=_,t.async=function(e,n,o,r,a){void 0===a&&(a=Promise);var i=new _(l(e,n,o,r),a);return t.isGeneratorFunction(n)?i:i.next().then((function(e){return e.done?e.value:i.next()}))},w(v),c(v,s,"Generator"),c(v,a,(function(){return this})),c(v,"toString",(function(){return"[object Generator]"})),t.keys=function(e){var t=[];for(var n in e)t.push(n);return t.reverse(),function n(){for(;t.length;){var o=t.pop();if(o in e)return n.value=o,n.done=!1,n}return n.done=!0,n}},t.values=L,k.prototype={constructor:k,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=void 0,this.done=!1,this.delegate=null,this.method="next",this.arg=void 0,this.tryEntries.forEach(E),!e)for(var t in this)"t"===t.charAt(0)&&o.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=void 0)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var t=this;function n(n,o){return i.type="throw",i.arg=e,t.next=n,o&&(t.method="next",t.arg=void 0),!!o}for(var r=this.tryEntries.length-1;r>=0;--r){var a=this.tryEntries[r],i=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=o.call(a,"catchLoc"),c=o.call(a,"finallyLoc");if(s&&c){if(this.prev<a.catchLoc)return n(a.catchLoc,!0);if(this.prev<a.finallyLoc)return n(a.finallyLoc)}else if(s){if(this.prev<a.catchLoc)return n(a.catchLoc,!0)}else{if(!c)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return n(a.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var a=r;break}}a&&("break"===e||"continue"===e)&&a.tryLoc<=t&&t<=a.finallyLoc&&(a=null);var i=a?a.completion:{};return i.type=e,i.arg=t,a?(this.method="next",this.next=a.finallyLoc,d):this.complete(i)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),d},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),E(n),d}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var o=n.completion;if("throw"===o.type){var r=o.arg;E(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:L(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=void 0),d}},t}function t(e,t,n,o,r,a,i){try{var s=e[a](i),c=s.value}catch(e){return void n(e)}s.done?t(c):Promise.resolve(c).then(o,r)}function n(e){return function(){var n=this,o=arguments;return new Promise((function(r,a){var i=e.apply(n,o);function s(e){t(i,r,a,s,c,"next",e)}function c(e){t(i,r,a,s,c,"throw",e)}s(void 0)}))}}function o(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null==n)return;var o,r,a=[],i=!0,s=!1;try{for(n=n.call(e);!(i=(o=n.next()).done)&&(a.push(o.value),!t||a.length!==t);i=!0);}catch(e){s=!0,r=e}finally{try{i||null==n.return||n.return()}finally{if(s)throw r}}return a}(e,t)||r(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function r(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n<t;n++)o[n]=e[n];return o}var i,s,c,l,u,d,p,f,m,h,y,g,v,w,_,x,b,E,k,L,S,C,T,P;i=null,s=!1,c="",l="",u={},d=/.*?_(\d+)$/,p=!1,f="",m=!1,h=/^( {1,4}|\t)/,y="\n\"\"\"\nExecute Python code in code blocks (color previews added specifically for ColorAide).\n\nThis can be executed in either a Pyodide environment of a normal Python environment.\n3rd party libraries (that are not available directly from Pyodide) are only loaded\nwhen needed so that Pyodide will have a chance to load them if necessary. This is also\nhardcoded to work with ColorAide.\n\nThis is meant to be executed by Pyodide on preformatted HTML to allow for live execution of\ncode snippets using `coloraide`.\n\nTransform Python code by executing it, transforming to a Python console output,\nand finding and outputting color previews.\n\"\"\"\nimport xml.etree.ElementTree as Etree\nfrom collections.abc import Sequence\nfrom collections import namedtuple\nimport ast\nfrom io import StringIO\nimport contextlib\nimport sys\nimport re\n\nWEBSPACE = \"srgb\"\nAST_BLOCKS = (ast.If, ast.For, ast.While, ast.Try, ast.With, ast.FunctionDef, ast.ClassDef)\n\nRE_COLOR_START = re.compile(\n    r\"(?i)(?:\\b(?<![-#&$])(?:color|hsla?|lch|lab|hwb|rgba?)\\(|\\b(?<![-#&$])[\\w]{3,}(?![(-])\\b|(?<![&])#)\"\n)\n\ntemplate = '''<div class=\"playground\" id=\"__playground_{el_id}\">\n<div class=\"playground-results\" id=\"__playground-results_{el_id}\">\n{results}\n</div>\n<div class=\"playground-code hidden\" id=\"__playground-code_{el_id}\">\n<form autocomplete=\"off\">\n<textarea class=\"playground-inputs\" id=\"__playground-inputs_{el_id}\" spellcheck=\"false\">{raw_source}</textarea>\n</form>\n</div>\n\n<button id=\"__playground-edit_{el_id}\" class=\"playground-edit\" title=\"Edit the current snippet\">Edit</button>\n<button id=\"__playground-share_{el_id}\" class=\"playground-share\" title=\"Copy URL to current snippet\">Share</button>\n<button id=\"__playground-run_{el_id}\" class=\"playground-run hidden\" title=\"Run code (Ctrl + Enter)\">Run</button>\n<button id=\"__playground-cancel_{el_id}\" class=\"playground-cancel hidden\" title=\"Cancel edit (Escape)\">Cancel</button>\n</div>'''\n\ncode_id = 0\n\n\nclass HtmlGradient(list):\n    \"\"\"HTML color gradient.\"\"\"\n\n\nclass HtmlSteps(list):\n    \"\"\"HTML color steps.\"\"\"\n\n\nclass ColorTuple(namedtuple('ColorTuple', ['string', 'color'])):\n    \"\"\"Color tuple.\"\"\"\n\n\nclass HtmlRow(list):\n    \"\"\"Create a row with the given colors.\"\"\"\n\n\ndef _escape(txt):\n    \"\"\"Basic HTML escaping.\"\"\"\n\n    txt = txt.replace('&', '&amp;')\n    txt = txt.replace('<', '&lt;')\n    txt = txt.replace('>', '&gt;')\n    return txt\n\n\n@contextlib.contextmanager\ndef std_output(stdout=None):\n    \"\"\"Capture standard out.\"\"\"\n    old = sys.stdout\n    if stdout is None:\n        stdout = StringIO()\n    sys.stdout = stdout\n    yield stdout\n    sys.stdout = old\n\n\ndef get_colors(result):\n    \"\"\"Get color from results.\"\"\"\n\n    from coloraide import Color\n    from coloraide.interpolate import Interpolator\n    try:\n        from coloraide_extras import Color as Color2\n    except ImportError:\n        Color2 = Color\n\n    colors = []\n    if isinstance(result, HtmlRow):\n        colors = HtmlRow(\n            [\n                ColorTuple(c.to_string(fit=False), c.clone()) if isinstance(c, Color) else ColorTuple(c, Color2(c))\n                for c in result\n            ]\n        )\n    elif isinstance(result, (HtmlSteps, HtmlGradient)):\n        t = type(result)\n        colors = t([c.clone() if isinstance(c, Color) else Color2(c) for c in result])\n    elif isinstance(result, Color):\n        colors.append(ColorTuple(result.to_string(fit=False), result.clone()))\n    elif isinstance(result, Interpolator):\n        colors = HtmlGradient(result.steps(steps=5, max_delta_e=2.3))\n    elif isinstance(result, str):\n        try:\n            colors.append(ColorTuple(result, Color2(result)))\n        except Exception:\n            pass\n    elif isinstance(result, Sequence):\n        for x in result:\n            if isinstance(x, Color):\n                colors.append(ColorTuple(x.to_string(fit=False), x.clone()))\n            elif isinstance(x, str):\n                try:\n                    colors.append(ColorTuple(x, Color2(x)))\n                except Exception:\n                    pass\n    return colors\n\n\ndef find_colors(text):\n    \"\"\"Find colors in text buffer.\"\"\"\n\n    try:\n        from coloraide_extras import Color\n    except ImportError:\n        from coloraide import Color\n\n    colors = []\n    for m in RE_COLOR_START.finditer(text):\n        start = m.start()\n        mcolor = Color.match(text, start=start)\n        if mcolor is not None:\n            colors.append(ColorTuple(text[mcolor.start:mcolor.end], mcolor.color))\n    return colors\n\n\ndef execute(cmd, no_except=True, inline=False):\n    \"\"\"Execute color commands.\"\"\"\n\n    import coloraide\n    try:\n        import coloraide_extras\n    except ImportError:\n        coloraide_extras = None\n\n    g = {\n        'Color': coloraide.Color,\n        'coloraide': coloraide,\n        'NaN': coloraide.NaN,\n        'Piecewise': coloraide.Piecewise,\n        'HtmlRow': HtmlRow,\n        'HtmlSteps': HtmlSteps,\n        'HtmlGradient': HtmlGradient\n    }\n\n    if coloraide_extras is not None:\n        g['coloraide_extras'] = coloraide_extras\n        g['Color'] = coloraide_extras.Color\n\n    console = ''\n    colors = []\n\n    # Build AST tree\n    src = cmd.strip()\n    lines = src.split('\\n')\n    try:\n        tree = ast.parse(src)\n    except Exception as e:\n        if no_except:\n            if not inline:\n                from pymdownx.superfences import SuperFencesException\n                raise SuperFencesException from e\n            else:\n                from pymdownx.inlinehilite import InlineHiliteException\n                raise InlineHiliteException from e\n        import traceback\n        return '{}'.format(traceback.format_exc()), colors\n\n    for node in tree.body:\n        result = None\n\n        # Format source as Python console statements\n        start = node.lineno\n        end = node.end_lineno\n        stmt = lines[start - 1: end]\n        command = ''\n        for i, line in enumerate(stmt, 0):\n            if i == 0:\n                stmt[i] = '>>> ' + line\n            else:\n                stmt[i] = '... ' + line\n        command += '\\n'.join(stmt)\n        if isinstance(node, AST_BLOCKS):\n            command += '\\n... '\n\n        try:\n            # Capture anything sent to standard out\n            text = ''\n            with std_output() as s:\n                # Execute code\n                if isinstance(node, ast.Expr):\n                    _eval = ast.Expression(node.value)\n                    result = eval(compile(_eval, '<string>', 'eval'), g)\n                else:\n                    _exec = ast.Module([node], [])\n                    exec(compile(_exec, '<string>', 'exec'), g)\n\n                # Execution went well, so append command\n                console += command\n\n                # Output captured standard out after statements\n                text = s.getvalue()\n                if text:\n                    clist = find_colors(text)\n                    if clist:\n                        colors.append(clist)\n                    console += '\\n{}'.format(text)\n                s.flush()\n        except Exception as e:\n            if no_except:\n                if not inline:\n                    from pymdownx.superfences import SuperFencesException\n                    raise SuperFencesException from e\n                else:\n                    from pymdownx.inlinehilite import InlineHiliteException\n                    raise InlineHiliteException from e\n            import traceback\n            console += '{}\\n{}'.format(command, traceback.format_exc())\n            # Failed for some reason, so quit\n            break\n\n        # If we got a result, output it as well\n        if result is not None:\n            clist = get_colors(result)\n            if clist:\n                colors.append(clist)\n            console += '{}{}\\n'.format('\\n' if not text else '', str(result))\n        else:\n            console += '\\n' if not text else ''\n\n    return console, colors\n\n\ndef colorize(src, lang, **options):\n    \"\"\"Colorize.\"\"\"\n\n    from pygments import highlight\n    from pygments.lexers import get_lexer_by_name\n    from pygments.formatters import find_formatter_class\n    HtmlFormatter = find_formatter_class('html')\n\n    lexer = get_lexer_by_name(lang, **options)\n    formatter = HtmlFormatter(cssclass=\"highlight\", wrapcode=True)\n    return highlight(src, lexer, formatter).strip()\n\n\ndef color_command_validator(language, inputs, options, attrs, md):\n    \"\"\"Color validator.\"\"\"\n\n    valid_inputs = set(['exceptions'])\n\n    for k, v in inputs.items():\n        if k in valid_inputs:\n            options[k] = True\n            continue\n        attrs[k] = v\n    return True\n\n\ndef _color_command_console(colors):\n    \"\"\"Color command formatter.\"\"\"\n\n    el = ''\n    bar = False\n    values = []\n    for item in colors:\n        if isinstance(item, (HtmlGradient, HtmlSteps)):\n            current = total = percent = last = 0\n            if isinstance(item, HtmlSteps):\n                total = len(item)\n                percent = 100 / total\n                current = percent\n\n            if bar:\n                el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n                values = []\n            sub_el1 = '<div class=\"swatch-bar\"><span class=\"swatch swatch-gradient\">{}</span></div>'\n            style = \"--swatch-stops: \"\n            stops = []\n            for e, color in enumerate(item):\n                color.fit(WEBSPACE)\n                if current:\n                    stops.append('{} {}%'.format(color.convert(WEBSPACE).to_string(), str(last)))\n                    stops.append('{} {}%'.format(color.convert(WEBSPACE).to_string(), str(current)))\n                    last = current\n                    if e < (total - 1):\n                        current += percent\n                    else:\n                        current = 100\n                else:\n                    stops.append(color.convert(WEBSPACE).to_string())\n            if not stops:\n                stops.extend(['transparent'] * 2)\n            if len(stops) == 1:\n                stops.append(stops[0])\n            style += ','.join(stops)\n            sub_el2 = '<span class=\"swatch-color\" style=\"{}\"></span>'.format(style)\n            el += sub_el1.format(sub_el2)\n            bar = False\n        else:\n            is_row = False\n            if isinstance(item, HtmlRow):\n                is_row = True\n                if bar and values:\n                    el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n                    values = []\n                bar = False\n\n            bar = True\n            base_classes = \"swatch\"\n            for color in item:\n                if not color.color.in_gamut(WEBSPACE):\n                    base_classes += \" out-of-gamut\"\n                color.color.fit(WEBSPACE)\n                srgb = color.color.convert(WEBSPACE)\n                value1 = srgb.to_string(alpha=False)\n                value2 = srgb.to_string()\n                style = \"--swatch-stops: {} 50%, {} 50%\".format(value1, value2)\n                title = color.string\n                classes = base_classes\n                c = '<span class=\"swatch-color\" style=\"{style}\"></span>'.format(style=style)\n                c = '<span class=\"{classes}\" title=\"{title}&#013;Copy to clipboard\">{color}</span>'.format(\n                    classes=classes,\n                    color=c,\n                    title=title\n                )\n                values.append(c)\n\n            if is_row and values:\n                el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n                values = []\n                bar = False\n    if bar:\n        el += '<div class=\"swatch-bar\">{}</div>'.format(' '.join(values))\n        values = []\n\n    return el\n\n\ndef color_command_formatter(src=\"\", language=\"\", class_name=None, options=None, md=\"\", **kwargs):\n    \"\"\"Formatter wrapper.\"\"\"\n\n    global code_id\n    from pymdownx.superfences import SuperFencesException\n\n    try:\n        if len(md.preprocessors['fenced_code_block'].extension.stash) == 0:\n            code_id = 0\n\n        # Check if we should allow exceptions\n        exceptions = options.get('exceptions', False) if options is not None else False\n\n        console, colors = execute(src.strip(), not exceptions)\n        el = _color_command_console(colors)\n\n        el += md.preprocessors['fenced_code_block'].extension.superfences[0]['formatter'](\n            src=console,\n            class_name=\"highlight\",\n            language='pycon',\n            md=md,\n            options=options,\n            **kwargs\n        )\n        el = '<div class=\"color-command\">{}</div>'.format(el)\n        el = template.format(el_id=code_id, raw_source=_escape(src), results=el)\n        code_id += 1\n    except SuperFencesException:\n        raise\n    except Exception:\n        from pymdownx import superfences\n        import traceback\n        print(traceback.format_exc())\n        return superfences.fence_code_format(src, 'text', class_name, options, md, **kwargs)\n    return el\n\n\ndef color_formatter(src=\"\", language=\"\", class_name=None, md=\"\", exceptions=True):\n    \"\"\"Formatter wrapper.\"\"\"\n\n    from pymdownx.inlinehilite import InlineHiliteException\n    try:\n        from coloraide_extras import Color\n    except ImportError:\n        from coloraide import Color\n\n    try:\n        result = src.strip()\n\n        try:\n            color = Color(result.strip())\n        except Exception:\n            console, colors = execute(result, exceptions, inline=True)\n            if len(colors) != 1 or len(colors[0]) != 1:\n                if exceptions:\n                    raise InlineHiliteException('Only one color allowed')\n                else:\n                    raise ValueError('Only one color allowed')\n            color = colors[0][0].color\n            result = colors[0][0].string\n\n        el = Etree.Element('span')\n        stops = []\n        if not color.in_gamut(WEBSPACE):\n            color.fit(WEBSPACE)\n            attributes = {'class': \"swatch out-of-gamut\", \"title\": result}\n            sub_el = Etree.SubElement(el, 'span', attributes)\n            stops.append(color.convert(WEBSPACE).to_string(hex=True, alpha=False))\n            if color[-1] < 1.0:\n                stops[-1] += ' 50%'\n                stops.append(color.convert(WEBSPACE).to_string(hex=True) + ' 50%')\n        else:\n            attributes = {'class': \"swatch\", \"title\": result}\n            sub_el = Etree.SubElement(el, 'span', attributes)\n            stops.append(color.convert(WEBSPACE).to_string(hex=True, alpha=False))\n            if color[-1] < 1.0:\n                stops[-1] += ' 50%'\n                stops.append(color.convert(WEBSPACE).to_string(hex=True) + ' 50%')\n\n        if not stops:\n            stops.extend(['transparent'] * 2)\n        if len(stops) == 1:\n            stops.append(stops[0])\n\n        Etree.SubElement(\n            sub_el,\n            'span',\n            {\n                \"class\": \"swatch-color\",\n                \"style\": \"--swatch-stops: {};\".format(','.join(stops))\n            }\n        )\n\n        el.append(md.inlinePatterns['backtick'].handle_code('css-color', result))\n    except InlineHiliteException:\n        raise\n    except Exception:\n        import traceback\n        print(traceback.format_exc())\n        el = md.inlinePatterns['backtick'].handle_code('text', src)\n    return el\n\n\n#############################\n# Pyodide specific code\n#############################\ndef live_color_command_formatter(src):\n    \"\"\"Formatter wrapper.\"\"\"\n\n    try:\n        console, colors = execute(src.strip(), False)\n        el = _color_command_console(colors)\n\n        if not colors:\n            el += '<div class=\"swatch-bar\"></div>'\n\n        el += colorize(console, 'pycon', **{'python3': True, 'stripnl': False})\n        el = '<div class=\"color-command\">{}</div>'.format(el)\n    except Exception:\n        return '<div class=\"color-command\"><div class=\"swatch-bar\"></div>{}</div>'.format(colorize('', 'text'))\n    return el\n\n\ndef live_color_command_validator(language, inputs, options, attrs, md):\n    \"\"\"Color validator.\"\"\"\n\n    value = color_command_validator(language, inputs, options, attrs, md)\n    # Live edit, we always allow exceptions so not to crash the service.\n    options['exceptions'] = True\n    return value\n\n\ndef live_color_formatter(src=\"\", language=\"\", class_name=None, md=\"\"):\n    \"\"\"Color formatter for a live environment.\"\"\"\n\n    return color_formatter(src, language, class_name, md, exceptions=False)\n\n\ndef render_console(*args):\n    \"\"\"Render console update.\"\"\"\n\n    from js import document\n\n    try:\n        # Run code\n        inputs = document.getElementById(\"__playground-inputs_{}\".format(globals()['id_num']))\n        results = document.getElementById(\"__playground-results_{}\".format(globals()['id_num']))\n        results.innerHTML = live_color_command_formatter(inputs.value)\n        scrollingElement = results.querySelector('code')\n        scrollingElement.scrollTop = scrollingElement.scrollHeight\n    except Exception as e:\n        print(e)\n\n\ndef render_notebook(*args):\n    \"\"\"Render notebook.\"\"\"\n\n    import markdown\n    from pymdownx import slugs\n    from js import document\n\n    text = globals().get('content', '')\n    extensions = [\n        'markdown.extensions.toc',\n        'markdown.extensions.admonition',\n        'markdown.extensions.smarty',\n        'pymdownx.betterem',\n        'markdown.extensions.attr_list',\n        'markdown.extensions.def_list',\n        'markdown.extensions.tables',\n        'markdown.extensions.abbr',\n        'markdown.extensions.footnotes',\n        'markdown.extensions.md_in_html',\n        'pymdownx.superfences',\n        'pymdownx.highlight',\n        'pymdownx.inlinehilite',\n        'pymdownx.magiclink',\n        'pymdownx.tilde',\n        'pymdownx.caret',\n        'pymdownx.smartsymbols',\n        'pymdownx.emoji',\n        'pymdownx.escapeall',\n        'pymdownx.tasklist',\n        'pymdownx.striphtml',\n        'pymdownx.snippets',\n        'pymdownx.keys',\n        'pymdownx.details',\n        'pymdownx.saneheaders',\n        'pymdownx.tabbed'\n    ]\n    extension_configs = {\n        'markdown.extensions.toc': {\n            'slugify': slugs.slugify(case=\"lower\"),\n            'permalink': \"\"\n        },\n        'markdown.extensions.smarty': {\n            \"smart_quotes\": False,\n        },\n        'pymdownx.superfences': {\n            'preserve_tabs': True,\n            'custom_fences': [\n                {\n                    \"name\": 'playground',\n                    \"class\": 'playground',\n                    \"format\": color_command_formatter,\n                    \"validator\": live_color_command_validator\n                }\n            ]\n        },\n        'pymdownx.inlinehilite': {\n            'custom_inline': [\n                {\n                    'name': 'color',\n                    'class': 'color',\n                    'format': live_color_formatter\n                }\n            ]\n        },\n        'pymdownx.magiclink': {\n            'repo_url_shortener': True,\n            'repo_url_shorthand': True,\n            'social_url_shorthand': True,\n            'user': 'facelessuser',\n            'repo': 'coloraide'\n        },\n        'pymdownx.keys': {\n            'separator': \"\\uff0b\"\n        }\n    }\n\n    try:\n        html = markdown.markdown(text, extensions=extensions, extension_configs=extension_configs)\n    except Exception:\n        html = ''\n    content = document.getElementById(\"__notebook-render\")\n    content.innerHTML = html\n\n\nimport micropip\nfrom js import location\n\naction = globals().get('action')\nif action == 'notebook':\n    callback = render_notebook\nelse:\n    callback = render_console\n\nbase = location.pathname.lstrip('/').split('/')[0]\nawait micropip.install([location.origin + '/{}/playground/{}'.format(base, w) for w in wheels])\ncallback()\n",g=window.color_notebook.default_playground,v=function(e){return'\n!!! new "This notebook is powered by [Pyodide](https://github.com/pyodide/pyodide). Learn more [here](?notebook=https://gist.githubusercontent.com/facelessuser/7c819668b5eb248ecb9ac608d91391cf/raw/playground.md). Preview, convert, interpolate, and explore!"\n\n````````playground\n'.concat(e,"\n````````\n")},w=function(){m=!0,window.document.dispatchEvent(new Event("DOMContentLoaded",{bubbles:!0,cancelable:!0}))},_=function(e){var t=window.pageXOffset||(document.documentElement||document.body.parentNode||document.body).scrollLeft,n=window.pageYOffset||(document.documentElement||document.body.parentNode||document.body).scrollTop;e.style.height="5px",e.style.height="".concat(e.scrollHeight,"px"),window.scrollTo(t,n)},x=function(e){return encodeURIComponent(e).replace(/[.!'()*]/g,(function(e){return"%".concat(e.charCodeAt(0).toString(16))}))},b=function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return(o=document.getElementById("__playground-inputs_".concat(n))).setAttribute("readonly",""),i.globals.set("id_num",n),i.globals.set("action","playground"),i.globals.set("wheels",window.color_notebook.playground_wheels),e.next=7,i.runPythonAsync(y);case 7:o.removeAttribute("readonly");case 8:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),E=function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return i.globals.set("content",n),i.globals.set("action","notebook"),i.globals.set("wheels",window.color_notebook.notebook_wheels),e.next=5,i.runPythonAsync(y);case 5:(o=document.getElementById("__notebook-input"))&&(c=n,o.value=n),window.location.hash&&(window.location.href=window.location.href);case 8:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),k=function(){var t=n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(p){e.next=7;break}return p=!0,e.next=4,loadPyodide({indexURL:"https://cdn.jsdelivr.net/pyodide/v0.20.0/full/",fullStdLib:!1});case 4:return i=e.sent,e.next=7,i.loadPackage(["micropip"]);case 7:case"end":return e.stop()}}),t)})));return function(){return t.apply(this,arguments)}}(),L=function(e,t,n){var o=null==t?"Loading...":t,r=n?"loading relative":"loading",a=document.createElement("template");a.innerHTML='<div class="'.concat(r,'"><div class="loader"></div><div>').concat(o,"</div></div>"),e.appendChild(a.content.firstChild)},S=function(e){e.querySelector(".loading")&&e.removeChild(e.querySelector(".loading"))},C=function(e){if("Tab"===e.key){var t=e.target;if(t.selectionStart!==t.selectionEnd){e.preventDefault();for(var n=t.selectionStart,o=t.selectionEnd,r=t.value;n>0&&"\n"!==r[n-1];)n--;for(;o>0&&"\n"!==r[o-1]&&o<r.length;)o++;for(var a=r.substr(n,o-n).split("\n"),i=0;i<a.length;i++)i===a.length-1&&0===a[i].length||(e.shiftKey?a[i]=a[i].replace(h,""):a[i]="    ".concat(a[i]));a=a.join("\n"),t.value=r.substr(0,n)+a+r.substr(o),t.selectionStart=n,t.selectionEnd=n+a.length}}},T=function(){var t=n(e().mark((function t(o){var r;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:r=document.getElementById("__notebook-source"),document.querySelectorAll(".playground").forEach((function(t){var a=t.id.replace(d,"$1"),i=document.getElementById("__playground-inputs_".concat(a)),p=document.getElementById("__playground-results_".concat(a)),f=document.getElementById("__playground-code_".concat(a)),m=document.querySelector("button#__playground-edit_".concat(a)),h=document.querySelector("button#__playground-share_".concat(a)),y=document.querySelector("button#__playground-run_".concat(a)),g=document.querySelector("button#__playground-cancel_".concat(a));if(i.addEventListener("input",(function(){_(i)})),i.addEventListener("keydown",C),r&&o){var v=document.getElementById("__notebook-input");v.addEventListener("input",(function(e){_(e.target)})),v.addEventListener("keydown",C),document.getElementById("__notebook-edit").addEventListener("click",(function(){u[v.id]=v.value,document.getElementById("__notebook-render").classList.toggle("hidden"),document.getElementById("__notebook-source").classList.toggle("hidden"),_(document.getElementById("__notebook-input"))})),document.getElementById("__notebook-md-gist").addEventListener("click",function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:null!==(o=prompt("Please enter link to the Markdown page source:",l))&&(o=x(o),n.preventDefault(),history.pushState({notebook:o},"","?".concat(new URLSearchParams("notebook=".concat(o)).toString())),P(!1));case 2:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()),document.getElementById("__notebook-py-gist").addEventListener("click",function(){var t=n(e().mark((function t(n){var o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:null!==(o=prompt("Please enter the link to the Python code source:",l))&&(o=x(o),n.preventDefault(),history.pushState({source:o},"","?".concat(new URLSearchParams("source=".concat(o)).toString())),P(!1));case 2:case"end":return e.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()),document.getElementById("__notebook-input").value=c,document.getElementById("__notebook-cancel").addEventListener("click",(function(){v.value=u[v.id],delete u[v.id],document.getElementById("__notebook-render").classList.toggle("hidden"),document.getElementById("__notebook-source").classList.toggle("hidden")})),document.getElementById("__notebook-submit").addEventListener("click",n(e().mark((function t(){var n,o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=document.getElementById("__notebook-render"),c=document.getElementById("__notebook-input").value,n.classList.toggle("hidden"),document.getElementById("__notebook-source").classList.toggle("hidden"),o=document.querySelector("article"),L(o,"Loading Notebook..."),n.innerHTML="",u={},e.next=10,k();case 10:return e.next=12,E(c);case 12:return e.next=14,T();case 14:S(o);case 15:case"end":return e.stop()}}),t)}))))}i.addEventListener("touchmove",(function(e){e.stopPropagation()})),p.addEventListener("click",(function(e){var t=e.target;if(t.matches("span.swatch-color")){var n="",o=t.parentNode;if(!o.matches("span.swatch-gradient")){if(n=(n=o.getAttribute("title").replace("Copy to clipboard","")).replace("\n",""),window.clipboardData&&window.clipboardData.setData)return window.clipboardData.setData("Text",n);if(document.queryCommandSupported&&document.queryCommandSupported("copy")){var r=document.createElement("textarea");r.textContent=n,r.style.position="fixed",document.body.appendChild(r),r.select();try{return document.execCommand("copy")}catch(e){return prompt("Copy to clipboard: Ctrl+C, Enter",n)}finally{document.body.removeChild(r)}}}}})),m.addEventListener("click",n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:u[a]=i.value,f.classList.toggle("hidden"),p.classList.toggle("hidden"),y.classList.toggle("hidden"),g.classList.toggle("hidden"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),_(i),i.focus();case 9:case"end":return e.stop()}}),t)})))),h.addEventListener("click",n(e().mark((function t(){var o,r,a,s,c;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:o=window.location.pathname.split("/")[1],r=x(i.value),a=window.location,s="/playground/",a.pathname.startsWith("/".concat(o,"/"))&&(s="/".concat(o,"/playground/")),c="".concat(a.protocol,"//").concat(a.host).concat(s,"?code=").concat(r),r.length>1e3?alert("Code must be under a 1000 characters to generate a URL!"):navigator.clipboard.writeText(c).then(n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:alert("Link copied to clipboard :)");case 1:case"end":return e.stop()}}),t)}))),n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:alert("Failed to copy link clipboard!");case 1:case"end":return e.stop()}}),t)}))));case 7:case"end":return t.stop()}}),t)})))),y.addEventListener("click",n(e().mark((function t(){var n,o;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!s){e.next=2;break}return e.abrupt("return");case 2:return s=!0,n=f.querySelector("form"),L(n,null,!0),(o=document.querySelectorAll(".playground .playground-run"))&&o.forEach((function(e){e.setAttribute("disabled","")})),e.next=9,k();case 9:return p.querySelector("code").innerHTML="",e.next=12,b(a);case 12:o&&o.forEach((function(e){e.removeAttribute("disabled")})),S(n),f.classList.toggle("hidden"),p.classList.toggle("hidden"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),y.classList.toggle("hidden"),g.classList.toggle("hidden"),delete u[a],s=!1;case 22:case"end":return e.stop()}}),t)})))),g.addEventListener("click",(function(){i.value=u[a],delete u[a],f.classList.toggle("hidden"),p.classList.toggle("hidden"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),y.classList.toggle("hidden"),g.classList.toggle("hidden")}))}));case 3:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),P=function(){var t=n(e().mark((function t(o){var r,a,i,s,c,d,p,m,h;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(u={},!window.location.pathname.endsWith("/playground/")){t.next=32;break}if(r=new URLSearchParams(window.location.search),a="Loading Pyodide...",i="Loading Notebook...",s=r.has("source")?r.get("source"):r.get("notebook"),c=document.querySelector("article"),null===s||!s.trim()){t.next=16;break}return L(c,a),t.next=11,k();case 11:S(c),L(c,i);try{d=r.has("source")?"source":"notebook",f=decodeURIComponent(r.toString()),p="",m=new XMLHttpRequest,l=s,m.open("GET",s,!0),m.onload=n(e().mark((function t(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return 4===m.readyState&&200===m.status&&(p=m.responseText),"source"===d&&(p=v(p)),e.next=4,E(p);case 4:return e.next=6,T(o);case 6:S(c),w();case 8:case"end":return e.stop()}}),t)}))),m.send()}catch(e){}t.next=30;break;case 16:return l="",h=v(r.has("code")?r.get("code"):g),f=decodeURIComponent(r.toString()),L(c,a),t.next=22,k();case 22:return S(c),L(c,i),t.next=26,E(h);case 26:return t.next=28,T(o);case 28:S(c),w();case 30:t.next=35;break;case 32:l="",f="",T(o);case 35:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}(),document.addEventListener("click",(function(e){var t=window.location.pathname.split("/")[1],n=e.target||e.srcElement;if("A"===n.tagName&&P&&n.getAttribute("href")&&n.host===window.location.host&&window.location.pathname==="/".concat(t,'/playground/"')&&window.location.pathname===n.pathname&&window.location.search!==n.search){e.preventDefault();var a,i={},s=function(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=r(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var o=0,a=function(){};return{s:a,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,c=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){c=!0,i=e},f:function(){try{s||null==n.return||n.return()}finally{if(c)throw i}}}}(new URLSearchParams(n.search));try{for(s.s();!(a=s.n()).done;){var c=o(a.value,2),l=c[0],u=c[1];i[l]=u}}catch(e){s.e(e)}finally{s.f()}history.pushState(i,"",n.href),P(!1)}})),window.addEventListener("popstate",(function(){var e=window.location.pathname.split("/")[1];window.location.pathname==="/".concat(e,"/playground/")&&decodeURIComponent(new URLSearchParams(window.location.search).toString())!==f&&P(!1)})),window.addEventListener("unload",(function(){m=!0})),window.document$.subscribe((function(){m?m=!1:P(!0)}))}();
-//# sourceMappingURL=extra-notebook-a80f0a52.js.map
diff --git a/mkdocs.yml b/mkdocs.yml
index a3497076e..3fab83191 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -126,7 +126,7 @@ extra_css:
 extra_javascript:
   - playground-config-6cf8f015.js
   - https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js
-  - assets/coloraide-extras/extra-notebook-a80f0a52.js
+  - assets/coloraide-extras/extra-notebook-1e54adfd.js
 
 extra:
   social:


Test patch:
diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py
index 320e76fc5..d156ba228 100644
--- a/tests/test_interpolation.py
+++ b/tests/test_interpolation.py
@@ -1,6 +1,6 @@
 """Test Interpolation."""
 import unittest
-from coloraide import Color, NaN, Piecewise
+from coloraide import Color, NaN, stop, hint
 from . import util
 
 
@@ -34,9 +34,8 @@ def test_bad_mix_input(self):
     def test_mix_input_piecewise(self):
         """Test mix with piecewise."""
 
-        self.assertColorEqual(
-            Color('red').mix(Piecewise('blue'), 0.5, space="srgb"), Color("srgb", [0.5, 0, 0.5])
-        )
+        with self.assertRaises(TypeError):
+            Color('red').mix(stop('blue', 0.0), 0.5, space="srgb")
 
     def test_mix_space(self):
         """Test color mix in different space."""
@@ -301,56 +300,101 @@ def test_mix_progress(self):
         progress = lambda x: x * 3  # noqa: E731
         self.assertColorEqual(
             Color('red').mix('blue', 1, out_space="lab", space="lab", progress=progress),
-            Color("lab(-19.876% 43.252 -475.87)")
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
             Color('red').mix('blue', 0.75, out_space="lab", space="lab", progress=progress),
-            Color("lab(-1.3345% 52.64 -339.43)")
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
             Color('red').mix('blue', 0.5, out_space="lab", space="lab", progress=progress),
-            Color("lab(17.207% 62.029 -202.99)")
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
             Color('red').mix('blue', 0.25, out_space="lab", space="lab", progress=progress),
-            Color("lab(35.749% 71.417 -66.55)")
+            Color("lab(35.749 71.417 -66.55)")
         )
         self.assertColorEqual(
             Color('red').mix('blue', 0, out_space="lab", space="lab", progress=progress),
-            Color("lab(54.291% 80.805 69.891)")
+            Color("lab(54.291 80.805 69.891)")
         )
 
     def test_interpolate_fit_required(self):
         """Test interpolation case that requires fitting."""
 
         self.assertColorEqual(
-            Color('color(display-p3 0 1 1)').interpolate('color(display-p3 0 0 1)', space='hsl')(0.5),
+            Color.interpolate(['color(display-p3 0 1 1)', 'color(display-p3 0 0 1)'], space='hsl')(0.5),
             Color('color(display-p3 0.21789 0.49793 0.96522)')
         )
 
     def test_interpolate(self):
         """Test interpolation."""
 
-        self.assertColorEqual(Color('red').interpolate('blue', space="srgb")(1), Color("srgb", [0, 0, 1]))
-        self.assertColorEqual(Color('red').interpolate('blue', space="srgb")(0.75), Color("srgb", [0.25, 0, 0.75]))
-        self.assertColorEqual(Color('red').interpolate('blue', space="srgb")(0.5), Color("srgb", [0.5, 0, 0.5]))
-        self.assertColorEqual(Color('red').interpolate('blue', space="srgb")(0.25), Color("srgb", [0.75, 0, 0.25]))
-        self.assertColorEqual(Color('red').interpolate('blue', space="srgb")(0), Color("srgb", [1, 0, 0]))
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space="srgb")(1), Color("srgb", [0, 0, 1]))
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space="srgb")(0.75), Color("srgb", [0.25, 0, 0.75]))
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space="srgb")(0.5), Color("srgb", [0.5, 0, 0.5]))
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space="srgb")(0.25), Color("srgb", [0.75, 0, 0.25]))
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space="srgb")(0), Color("srgb", [1, 0, 0]))
+
+    def test_interpolate_bezier(self):
+        """Test interpolation bezier."""
+
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space="srgb", method="bezier")(1), Color("srgb", [0, 0, 1])
+        )
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space="srgb", method="bezier")(0.75), Color("srgb", [0.25, 0, 0.75])
+        )
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space="srgb", method="bezier")(0.5), Color("srgb", [0.5, 0, 0.5])
+        )
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space="srgb", method="bezier")(0.25), Color("srgb", [0.75, 0, 0.25])
+        )
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space="srgb", method="bezier")(0), Color("srgb", [1, 0, 0])
+        )
 
     def test_interpolate_channel(self):
         """Test interpolating a specific channel differently."""
 
         self.assertColorEqual(
-            Color('red').interpolate(Color('blue').set('alpha', 0), progress={'alpha': lambda t: t ** 3})(0.5),
+            Color.interpolate(['red', Color('blue').set('alpha', 0)], progress={'alpha': lambda t: t ** 3})(0.5),
+            Color('rgb(119.63 0 0 / 0.875)')
+        )
+
+    def test_interpolate_channel_bezier(self):
+        """Test interpolating a specific channel differently."""
+
+        self.assertColorEqual(
+            Color.interpolate(
+                ['red', Color('blue').set('alpha', 0)], progress={'alpha': lambda t: t ** 3}, method='bezier'
+            )(0.5),
             Color('rgb(119.63 0 0 / 0.875)')
         )
 
+    def test_interpolate_easing_inline(self):
+        """Test interpolating a specific channel differently."""
+
+        self.assertColorEqual(
+            Color.interpolate(['red', lambda t: t ** 3, 'blue'])(0.5),
+            Color('rgb(226.44 55.886 74.779)')
+        )
+
+    def test_interpolate_color_hint(self):
+        """Test interpolating with color hints."""
+
+        self.assertColorEqual(
+            Color.interpolate(['red', hint(0.75), 'blue'])(0.5),
+            Color('rgb(212 66.119 93.278)')
+        )
+
     def test_interpolate_channel_all(self):
         """Test interpolating a specific channel differently, but setting the others via all."""
 
         self.assertColorEqual(
-            Color('red').interpolate(
-                Color('blue').set('alpha', 0),
+            Color.interpolate(
+                ['red', Color('blue').set('alpha', 0)],
                 progress={
                     'alpha': lambda t: t ** 3,
                     'all': lambda t: 0,
@@ -364,78 +408,107 @@ def test_interpolate_channel_aliases(self):
         """Test interpolating a specific channel using a color's channel alias."""
 
         self.assertColorEqual(
-            Color('orange').interpolate(
-                Color('purple'),
+            Color.interpolate(
+                ['orange', Color('purple')],
                 progress={
                     'red': lambda t: t ** 3
                 },
                 space='srgb'
             )(0.5),
-            Color('rgb(239.13 82.5 64)')
+            Color('rgb(191.5 82.5 64)')
         )
 
     def test_interpolate_input_piecewise(self):
         """Test interpolation with piecewise."""
 
         self.assertColorEqual(
-            Color('red').interpolate(Piecewise('blue'), space="srgb")(0.5), Color("srgb", [0.5, 0, 0.5])
+            Color.interpolate(['red', stop('blue', 0.5)], space="srgb")(0.5), Color("srgb", [0, 0, 1])
+        )
+
+    def test_interpolate_input_bezier(self):
+        """Test interpolation with piecewise."""
+
+        self.assertColorEqual(
+            Color.interpolate(['red', stop('blue', 0.5)], space="srgb", method='bezier')(0.5), Color("srgb", [0, 0, 1])
         )
 
     def test_interpolate_stop(self):
         """Test interpolation with piecewise."""
 
         self.assertColorEqual(
-            Color('red').interpolate('blue', space="srgb", stop=0.6)(0.5), Color('red')
+            Color.interpolate([stop('red', 0.6), 'blue'], space="srgb")(0.5), Color('red')
+        )
+        self.assertColorEqual(
+            Color.interpolate([stop('red', 0.6), 'blue'], space="srgb")(0.7), Color('rgb(191.25 0 63.75)')
         )
+
+    def test_interpolate_stop_bezier(self):
+        """Test interpolation with piecewise."""
+
         self.assertColorEqual(
-            Color('red').interpolate('blue', space="srgb", stop=0.6)(0.7), Color('rgb(191.25 0 63.75)')
+            Color.interpolate([stop('red', 0.6), 'blue'], space="srgb", method='bezier')(0.5), Color('red')
         )
 
     def test_interpolate_space(self):
         """Test color mix in different space."""
 
-        self.assertColorEqual(Color('red').interpolate('blue', space='lab')(1), Color("rgb(0 0 255)"))
-        self.assertColorEqual(Color('red').interpolate('blue', space='lab')(0.75), Color("rgb(144.85 -24.864 194.36)"))
-        self.assertColorEqual(Color('red').interpolate('blue', space='lab')(0.5), Color("rgb(192.99 -29.503 136.17)"))
-        self.assertColorEqual(Color('red').interpolate('blue', space='lab')(0.25), Color("rgb(226.89 -24.304 79.188)"))
-        self.assertColorEqual(Color('red').interpolate('blue', space='lab')(0), Color("rgb(255 0 0)"))
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space='lab')(1), Color("rgb(0 0 255)"))
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space='lab')(0.75), Color("rgb(144.85 -24.864 194.36)")
+        )
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space='lab')(0.5), Color("rgb(192.99 -29.503 136.17)"))
+        self.assertColorEqual(
+            Color.interpolate(['red', 'blue'], space='lab')(0.25), Color("rgb(226.89 -24.304 79.188)")
+        )
+        self.assertColorEqual(Color.interpolate(['red', 'blue'], space='lab')(0), Color("rgb(255 0 0)"))
 
     def test_interpolate_empty_list(self):
         """Test interpolate with empty list."""
 
-        self.assertColorEqual(Color('green').interpolate([])(0.5), Color('green'))
+        with self.assertRaises(IndexError):
+            Color('green').interpolate([])(0.5)
 
     def test_interpolate_piecewise(self):
         """Test multiple inputs for interpolation."""
 
-        func = Color('white').interpolate(['red', 'black'])
+        func = Color.interpolate(['white', 'red', 'black'])
         self.assertColorEqual(func(0), Color('white'))
         self.assertColorEqual(func(0.5), Color('red'))
         self.assertColorEqual(func(1), Color('black'))
-        self.assertColorEqual(func(-0.1), Color('rgb(248.66 273.11 277.36)'))
-        self.assertColorEqual(func(1.1), Color('rgb(-3.2946 0 0)'))
+        self.assertColorEqual(func(-0.1), Color('rgb(255 255 255)'))
+        self.assertColorEqual(func(1.1), Color('rgb(0 0 0)'))
+
+    def test_interpolate_multi_bezier(self):
+        """Test multiple inputs for bezier interpolation."""
+
+        func = Color.interpolate(['white', 'red', 'black'], method='bezier')
+        self.assertColorEqual(func(0), Color('white'))
+        self.assertColorEqual(func(0.5), Color('rgb(180.88 83.809 71.264)'))
+        self.assertColorEqual(func(1), Color('black'))
+        self.assertColorEqual(func(-0.1), Color('rgb(255 255 255)'))
+        self.assertColorEqual(func(1.1), Color('rgb(0 0 0)'))
 
     def test_interpolate_out_space(self):
         """Test interpolation."""
 
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab")(1),
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab")(1),
             Color("lab(29.568% 68.287 -112.03)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab")(0.75),
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab")(0.75),
             Color("lab(35.749% 71.417 -66.55)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab")(0.5),
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab")(0.5),
             Color("lab(41.929% 74.546 -21.069)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab")(0.25),
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab")(0.25),
             Color("lab(48.11% 77.676 24.411)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab")(0),
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab")(0),
             Color("lab(54.291% 80.805 69.891)")
         )
 
@@ -443,7 +516,7 @@ def test_interpolate_alpha(self):
         """Test mixing alpha."""
 
         self.assertColorEqual(
-            Color('color(srgb 1 0 0 / 0.75)').interpolate('color(srgb 0 0 1 / 0.25)', space="srgb")(0.5),
+            Color.interpolate(['color(srgb 1 0 0 / 0.75)', 'color(srgb 0 0 1 / 0.25)'], space="srgb")(0.5),
             Color('rgb(191.25 0 63.75 / 0.5)')
         )
 
@@ -451,8 +524,8 @@ def test_interpolate_premultiplied_alpha(self):
         """Test premultiplied alpha."""
 
         self.assertColorEqual(
-            Color('color(srgb 1 0 0 / 0.75)').interpolate(
-                'color(srgb 0 0 1 / 0.25)', space="srgb", premultiplied=True
+            Color.interpolate(
+                ['color(srgb 1 0 0 / 0.75)', 'color(srgb 0 0 1 / 0.25)'], space="srgb", premultiplied=True
             )(0.5),
             Color('rgb(191.25 0 63.75 / 0.5)')
         )
@@ -461,8 +534,8 @@ def test_interpolate_premultiplied_alpha_none(self):
         """Test premultiplied alpha when alphas are none."""
 
         self.assertColorEqual(
-            Color('color(srgb 0 0 0 / none)').interpolate(
-                'color(srgb 0 1 0 / none)', space="srgb", premultiplied=True
+            Color.interpolate(
+                ['color(srgb 0 0 0 / none)', 'color(srgb 0 1 0 / none)'], space="srgb", premultiplied=True
             )(0.5),
             Color('rgb(0 127.5 0 / 0)')
         )
@@ -471,8 +544,8 @@ def test_interpolate_premultiplied_no_alpha(self):
         """Test premultiplied alpha."""
 
         self.assertColorEqual(
-            Color('color(srgb 1 0 0)').interpolate('color(srgb 0 0 1)', space="srgb", premultiplied=True)(0.5),
-            Color('color(srgb 1 0 0)').interpolate('color(srgb 0 0 1)', space="srgb")(0.5)
+            Color.interpolate(['color(srgb 1 0 0)', 'color(srgb 0 0 1)'], space="srgb", premultiplied=True)(0.5),
+            Color.interpolate(['color(srgb 1 0 0)', 'color(srgb 0 0 1)'], space="srgb")(0.5)
         )
 
     def test_interpolate_nan(self):
@@ -480,13 +553,13 @@ def test_interpolate_nan(self):
 
         c1 = Color("srgb", [NaN, 1, 1])
         c2 = Color("srgb", [0.75, 0, 0])
-        self.assertColorEqual(c1.interpolate(c2, space="srgb")(0.5), Color("srgb", [0.75, 0.5, 0.5]))
+        self.assertColorEqual(Color.interpolate([c1, c2], space="srgb")(0.5), Color("srgb", [0.75, 0.5, 0.5]))
         c1 = Color("srgb", [0.25, 1, 1])
         c2 = Color("srgb", [NaN, 0, 0])
-        self.assertColorEqual(c1.interpolate(c2, space="srgb")(0.5), Color("srgb", [0.25, 0.5, 0.5]))
+        self.assertColorEqual(Color.interpolate([c1, c2], space="srgb")(0.5), Color("srgb", [0.25, 0.5, 0.5]))
         c1 = Color("srgb", [NaN, 1, 1])
         c2 = Color("srgb", [NaN, 0, 0])
-        self.assertColorEqual(c1.interpolate(c2, space="srgb")(0.5), Color("srgb", [0, 0.5, 0.5]))
+        self.assertColorEqual(Color.interpolate([c1, c2], space="srgb")(0.5), Color("srgb", [0, 0.5, 0.5]))
 
     def test_interpolate_adjust(self):
         """Test mix adjust method."""
@@ -494,7 +567,7 @@ def test_interpolate_adjust(self):
         c1 = Color("color(srgb 0.25 1 1)")
         c2 = Color("color(srgb 0.75 0 0)")
         self.assertColorEqual(
-            c1.interpolate(c2.mask("red"), space="srgb")(0.5),
+            Color.interpolate([c1, c2.mask("red")], space="srgb")(0.5),
             Color("srgb", [0.25, 0.5, 0.5])
         )
 
@@ -504,23 +577,23 @@ def test_interpolate_hue_adjust(self):
         c1 = Color('rebeccapurple')
         c2 = Color('lch(85% 100 805)')
         self.assertColorEqual(
-            c1.interpolate(c2.mask("hue", invert=True), hue="shorter", space="lch")(0.25),
+            Color.interpolate([c1, c2.mask("hue", invert=True)], hue="shorter", space="lch")(0.25),
             Color("rgb(146.72 -3.9233 106.41)")
         )
         self.assertColorEqual(
-            c1.interpolate(c2.mask("hue", invert=True), hue="longer", space="lch")(0.25),
+            Color.interpolate([c1, c2.mask("hue", invert=True)], hue="longer", space="lch")(0.25),
             Color("rgb(-86.817 87.629 170)")
         )
         self.assertColorEqual(
-            c1.interpolate(c2.mask("hue", invert=True), hue="increasing", space="lch")(0.25),
+            Color.interpolate([c1, c2.mask("hue", invert=True)], hue="increasing", space="lch")(0.25),
             Color("rgb(146.72 -3.9233 106.41)")
         )
         self.assertColorEqual(
-            c1.interpolate(c2.mask("hue", invert=True), hue="decreasing", space="lch")(0.25),
+            Color.interpolate([c1, c2.mask("hue", invert=True)], hue="decreasing", space="lch")(0.25),
             Color("rgb(-86.817 87.629 170)")
         )
         self.assertColorEqual(
-            c1.interpolate(c2.mask("hue", invert=True), hue="specified", space="lch")(0.25),
+            Color.interpolate([c1, c2.mask("hue", invert=True)], hue="specified", space="lch")(0.25),
             Color("rgb(112.83 63.969 -28.821)")
         )
 
@@ -529,30 +602,30 @@ def test_interpolate_progress(self):
 
         progress = lambda x: x * 3  # noqa: E731
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab", progress=progress)(1),
-            Color("lab(-19.876% 43.252 -475.87)")
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab", progress=progress)(1),
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab", progress=progress)(0.75),
-            Color("lab(-1.3345% 52.64 -339.43)")
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab", progress=progress)(0.75),
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab", progress=progress)(0.5),
-            Color("lab(17.207% 62.029 -202.99)")
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab", progress=progress)(0.5),
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab", progress=progress)(0.25),
-            Color("lab(35.749% 71.417 -66.55)")
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab", progress=progress)(0.25),
+            Color("lab(35.749 71.417 -66.55)")
         )
         self.assertColorEqual(
-            Color('red').interpolate('blue', out_space="lab", space="lab", progress=progress)(0),
-            Color("lab(54.291% 80.805 69.891)")
+            Color.interpolate(['red', 'blue'], out_space="lab", space="lab", progress=progress)(0),
+            Color("lab(54.291 80.805 69.891)")
         )
 
     def test_steps(self):
         """Test steps."""
 
-        colors = Color('red').steps('blue', space="srgb", steps=5)
+        colors = Color.steps(['red', 'blue'], space="srgb", steps=5)
         self.assertColorEqual(colors[4], Color("srgb", [0, 0, 1]))
         self.assertColorEqual(colors[3], Color("srgb", [0.25, 0, 0.75]))
         self.assertColorEqual(colors[2], Color("srgb", [0.5, 0, 0.5]))
@@ -563,13 +636,20 @@ def test_steps_input_piecewise(self):
         """Test steps with piecewise."""
 
         self.assertColorEqual(
-            Color('red').steps(Piecewise('blue'), space="srgb", steps=5)[2], Color("srgb", [0.5, 0, 0.5])
+            Color.steps(['red', stop('blue', 0.5)], space="srgb", steps=5)[2], Color("srgb", [0, 0, 1])
+        )
+
+    def test_steps_input_bezier(self):
+        """Test steps with bezier."""
+
+        self.assertColorEqual(
+            Color.steps(['red', stop('blue', 0.5)], space="srgb", steps=5, method='bezier')[2], Color("srgb", [0, 0, 1])
         )
 
     def test_steps_multi(self):
         """Test steps with multiple color ranges."""
 
-        colors = Color('white').steps(['red', 'black'], steps=3)
+        colors = Color('white').steps(['white', 'red', 'black'], steps=3)
         self.assertColorEqual(colors[0], Color('white'))
         self.assertColorEqual(colors[1], Color('red'))
         self.assertColorEqual(colors[2], Color('black'))
@@ -577,13 +657,13 @@ def test_steps_multi(self):
     def test_steps_multi_max_delta_e(self):
         """Test steps with multiple color ranges and max_delta_e."""
 
-        colors = Color('red').steps(['green', 'blue'], space="srgb", max_delta_e=10)
+        colors = Color.steps(['red', 'green', 'blue'], space="srgb", max_delta_e=10)
         for index, color in enumerate(colors, 0):
             if not index:
                 continue
             self.assertTrue(color.delta_e(colors[index - 1]) <= 10)
 
-        colors = Color('red').steps(['green', 'blue'], space="srgb", max_delta_e=3)
+        colors = Color.steps(['red', 'green', 'blue'], space="srgb", max_delta_e=3)
         for index, color in enumerate(colors, 0):
             if not index:
                 continue
@@ -592,13 +672,13 @@ def test_steps_multi_max_delta_e(self):
     def test_steps_custom_delta_e(self):
         """Test a custom delta E input."""
 
-        colors = Color('red').steps(['green', 'blue'], space="srgb", max_delta_e=10, delta_e='99o')
+        colors = Color.steps(['red', 'green', 'blue'], space="srgb", max_delta_e=10, delta_e='99o')
         for index, color in enumerate(colors, 0):
             if not index:
                 continue
             self.assertTrue(color.delta_e(colors[index - 1]) <= 10)
 
-        colors = Color('red').steps(['green', 'blue'], space="srgb", max_delta_e=3, delta_e='99o')
+        colors = Color.steps(['red', 'green', 'blue'], space="srgb", max_delta_e=3, delta_e='99o')
         for index, color in enumerate(colors, 0):
             if not index:
                 continue
@@ -607,20 +687,21 @@ def test_steps_custom_delta_e(self):
     def test_steps_custom_delta_compare(self):
         """Test a custom delta E input."""
 
-        colors1 = len(Color('orange').steps('red', space="srgb", max_delta_e=0.2, delta_e='76'))
-        colors2 = len(Color('orange').steps('red', space="srgb", max_delta_e=0.2, delta_e='ok'))
+        colors1 = len(Color.steps(['orange', 'red'], space="srgb", max_delta_e=0.2, delta_e='76'))
+        colors2 = len(Color.steps(['orange', 'red'], space="srgb", max_delta_e=0.2, delta_e='ok'))
 
         self.assertNotEqual(colors1, colors2)
 
     def test_steps_empty_list(self):
         """Test steps with empty list."""
 
-        self.assertColorEqual(Color('green').steps([], steps=3)[1], Color('green'))
+        with self.assertRaises(IndexError):
+            Color.steps([], steps=3)
 
     def test_steps_space(self):
         """Test steps different space."""
 
-        colors = Color('red').steps('blue', space="lab", steps=5)
+        colors = Color.steps(['red', 'blue'], space="lab", steps=5)
         self.assertColorEqual(colors[4], Color("rgb(0 0 255)"))
         self.assertColorEqual(colors[3], Color("rgb(144.85 -24.864 194.36)"))
         self.assertColorEqual(colors[2], Color("rgb(192.99 -29.503 136.17)"))
@@ -630,7 +711,7 @@ def test_steps_space(self):
     def test_steps_out_space(self):
         """Test steps with output in different space."""
 
-        colors = Color('red').steps('blue', space="srgb", steps=5, out_space="lab")
+        colors = Color.steps(['red', 'blue'], space="srgb", steps=5, out_space="lab")
         self.assertColorEqual(
             colors[4],
             Color("lab(29.568% 68.287 -112.03)")
@@ -656,7 +737,7 @@ def test_steps_alpha(self):
         """Test mixing alpha."""
 
         self.assertColorEqual(
-            Color('color(srgb 1 0 0 / 0.75)').steps('color(srgb 0 0 1 / 0.25)', space="srgb", steps=1)[0],
+            Color.steps(['color(srgb 1 0 0 / 0.75)', 'color(srgb 0 0 1 / 0.25)'], space="srgb", steps=1)[0],
             Color('rgb(191.25 0 63.75 / 0.5)')
         )
 
@@ -664,8 +745,8 @@ def test_steps_premultiplied_alpha(self):
         """Test premultiplied alpha."""
 
         self.assertColorEqual(
-            Color('color(srgb 1 0 0 / 0.75)').steps(
-                'color(srgb 0 0 1 / 0.25)', space="srgb", steps=1, premultiplied=True
+            Color.steps(
+                ['color(srgb 1 0 0 / 0.75)', 'color(srgb 0 0 1 / 0.25)'], space="srgb", steps=1, premultiplied=True
             )[0],
             Color('rgb(191.25 0 63.75 / 0.5)')
         )
@@ -674,8 +755,8 @@ def test_steps_premultiplied_no_alpha(self):
         """Test premultiplied alpha."""
 
         self.assertColorEqual(
-            Color('color(srgb 1 0 0)').steps('color(srgb 0 0 1)', space="srgb", steps=1, premultiplied=True)[0],
-            Color('color(srgb 1 0 0)').steps('color(srgb 0 0 1)', space="srgb", steps=1)[0]
+            Color.steps(['color(srgb 1 0 0)', 'color(srgb 0 0 1)'], space="srgb", steps=1, premultiplied=True)[0],
+            Color.steps(['color(srgb 1 0 0)', 'color(srgb 0 0 1)'], space="srgb", steps=1)[0]
         )
 
     def test_steps_nan(self):
@@ -683,13 +764,13 @@ def test_steps_nan(self):
 
         c1 = Color("srgb", [NaN, 1, 1])
         c2 = Color("srgb", [0.75, 0, 0])
-        self.assertColorEqual(c1.steps(c2, space="srgb", steps=1)[0], Color("srgb", [0.75, 0.5, 0.5]))
+        self.assertColorEqual(Color.steps([c1, c2], space="srgb", steps=1)[0], Color("srgb", [0.75, 0.5, 0.5]))
         c1 = Color("srgb", [0.25, 1, 1])
         c2 = Color("srgb", [NaN, 0, 0])
-        self.assertColorEqual(c1.steps(c2, space="srgb", steps=1)[0], Color("srgb", [0.25, 0.5, 0.5]))
+        self.assertColorEqual(Color.steps([c1, c2], space="srgb", steps=1)[0], Color("srgb", [0.25, 0.5, 0.5]))
         c1 = Color("srgb", [NaN, 1, 1])
         c2 = Color("srgb", [NaN, 0, 0])
-        self.assertColorEqual(c1.steps(c2, space="srgb", steps=1)[0], Color("srgb", [0, 0.5, 0.5]))
+        self.assertColorEqual(Color.steps([c1, c2], space="srgb", steps=1)[0], Color("srgb", [0, 0.5, 0.5]))
 
     def test_steps_adjust(self):
         """Test steps with adjust method."""
@@ -697,7 +778,7 @@ def test_steps_adjust(self):
         c1 = Color("color(srgb 0.25 1 1)")
         c2 = Color("color(srgb 0.75 0 0)")
         self.assertColorEqual(
-            c1.steps(c2.mask("red"), space="srgb", steps=1)[0],
+            Color.steps([c1, c2.mask("red")], space="srgb", steps=1)[0],
             Color("srgb", [0.25, 0.5, 0.5])
         )
 
@@ -705,32 +786,47 @@ def test_steps_hue_adjust(self):
         """Test steps with hue adjusting."""
 
         self.assertColorEqual(
-            Color('rebeccapurple').steps(
-                Color('lch(85% 100 805)').mask("hue", invert=True), space="lch", steps=5, hue="shorter"
+            Color.steps(
+                ['rebeccapurple', Color('lch(85% 100 805)').mask("hue", invert=True)],
+                space="lch",
+                steps=5,
+                hue="shorter"
             )[1],
             Color("rgb(146.72 -3.9233 106.41)")
         )
         self.assertColorEqual(
-            Color('rebeccapurple').steps(
-                Color('lch(85% 100 805)').mask("hue", invert=True), space="lch", steps=5, hue="longer"
+            Color.steps(
+                ['rebeccapurple', Color('lch(85% 100 805)').mask("hue", invert=True)],
+                space="lch",
+                steps=5,
+                hue="longer"
             )[1],
             Color("rgb(-86.817 87.629 170)")
         )
         self.assertColorEqual(
-            Color('rebeccapurple').steps(
-                Color('lch(85% 100 805)').mask("hue", invert=True), space="lch", steps=5, hue="increasing"
+            Color.steps(
+                ['rebeccapurple', Color('lch(85% 100 805)').mask("hue", invert=True)],
+                space="lch",
+                steps=5,
+                hue="increasing"
             )[1],
             Color("rgb(146.72 -3.9233 106.41)")
         )
         self.assertColorEqual(
-            Color('rebeccapurple').steps(
-                Color('lch(85% 100 805)').mask("hue", invert=True), space="lch", steps=5, hue="decreasing"
+            Color.steps(
+                ['rebeccapurple', Color('lch(85% 100 805)').mask("hue", invert=True)],
+                space="lch",
+                steps=5,
+                hue="decreasing"
             )[1],
             Color("rgb(-86.817 87.629 170)")
         )
         self.assertColorEqual(
-            Color('rebeccapurple').steps(
-                Color('lch(85% 100 805)').mask("hue", invert=True), space="lch", steps=5, hue="specified"
+            Color.steps(
+                ['rebeccapurple', Color('lch(85% 100 805)').mask("hue", invert=True)],
+                space="lch",
+                steps=5,
+                hue="specified"
             )[1],
             Color("rgb(112.83 63.969 -28.821)")
         )
@@ -739,38 +835,38 @@ def test_steps_progress(self):
         """Test custom progress."""
 
         progress = lambda x: x * 3  # noqa: E731
-        colors = Color('red').steps('blue', steps=5, out_space="lab", space="lab", progress=progress)
+        colors = Color.steps(['red', 'blue'], steps=5, out_space="lab", space="lab", progress=progress)
         self.assertColorEqual(
             colors[4],
-            Color("lab(-19.876% 43.252 -475.87)")
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
             colors[3],
-            Color("lab(-1.3345% 52.64 -339.43)")
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
             colors[2],
-            Color("lab(17.207% 62.029 -202.99)")
+            Color("lab(29.568 68.287 -112.03)")
         )
         self.assertColorEqual(
             colors[1],
-            Color("lab(35.749% 71.417 -66.55)")
+            Color("lab(35.749 71.417 -66.55)")
         )
         self.assertColorEqual(
             colors[0],
-            Color("lab(54.291% 80.805 69.891)")
+            Color("lab(54.291 80.805 69.891)")
         )
 
     def test_steps_max_delta_e(self):
         """Test steps with a max delta e."""
 
-        colors = Color('red').steps('blue', space="srgb", max_delta_e=10)
+        colors = Color.steps(['red', 'blue'], space="srgb", max_delta_e=10)
         for index, color in enumerate(colors, 0):
             if not index:
                 continue
             self.assertTrue(color.delta_e(colors[index - 1]) <= 10)
 
-        colors = Color('red').steps('blue', space="srgb", max_delta_e=3)
+        colors = Color.steps(['red', 'blue'], space="srgb", max_delta_e=3)
         for index, color in enumerate(colors, 0):
             if not index:
                 continue
@@ -779,13 +875,103 @@ def test_steps_max_delta_e(self):
     def test_max_delta_min_step_less_than_two(self):
         """Test that when a minimum step less than 2 is given that `max_delta_e` won't break."""
 
-        colors = Color('lightblue').steps('blue', space="srgb", steps=1, max_delta_e=10)
+        colors = Color.steps(['lightblue', 'blue'], space="srgb", steps=1, max_delta_e=10)
         self.assertTrue(len(colors) > 2)
 
     def test_steps_max_delta_e_steps(self):
         """Test steps with a max delta e."""
 
-        colors = Color('red').steps('blue', space="srgb", max_delta_e=10)
+        colors = Color.steps(['red', 'blue'], space="srgb", max_delta_e=10)
         self.assertTrue(len(colors) > 5)
-        colors = Color('red').steps('blue', space="srgb", max_delta_e=10, max_steps=5)
+        colors = Color.steps(['red', 'blue'], space="srgb", max_delta_e=10, max_steps=5)
         self.assertTrue(len(colors) == 5)
+
+    def test_too_few_colors_linear(self):
+        """Test too few colors during linear interpolation."""
+
+        with self.assertRaises(ValueError):
+            Color.interpolate(['green', lambda t: t * 3])
+
+    def test_too_few_colors_bezier(self):
+        """Test too few colors during bezier interpolation."""
+
+        with self.assertRaises(ValueError):
+            Color.interpolate(['green', lambda t: t * 3], method='bezier')
+
+    def test_bad_method(self):
+        """Test bad interpolation method."""
+
+        with self.assertRaises(ValueError):
+            Color.interpolate(['green', lambda t: t * 3], method='bad')
+
+    def test_bad_easing(self):
+        """Test bad color easing linear."""
+
+        with self.assertRaises(ValueError):
+            Color.interpolate([lambda t: t * 3, 'green'])
+
+    def test_bad_color_input_bezier(self):
+        """Test bad color easing bezier."""
+
+        with self.assertRaises(ValueError):
+            Color.interpolate([lambda t: t * 3, 'green'], method='bezier')
+
+    def test_bezier_all_none(self):
+        """Test multiple bezier inputs with the same channel all none."""
+
+        self.assertColorEqual(
+            Color.interpolate(
+                ['Color(srgb 1 none 0.5)', 'Color(srgb 0.1 none 0.7)', 'Color(srgb 0.2 none 0.9)'],
+                space='srgb',
+                method='bezier'
+            )(0.5),
+            Color('rgb(89.25 none 178.5)')
+        )
+
+    def test_bezier_most_none(self):
+        """Test multiple bezier inputs with the same channel with most none."""
+
+        self.assertColorEqual(
+            Color.interpolate(
+                ['Color(srgb 1 none 0.5)', 'Color(srgb 0.1 none 0.7)', 'Color(srgb 0.2 0.8 0.9)'],
+                space='srgb',
+                method='bezier'
+            )(0.5),
+            Color('rgb(89.25 204 178.5)')
+        )
+
+    def test_bezier_none_after(self):
+        """Test multiple bezier inputs with the same channel with most none but not none at the start."""
+
+        self.assertColorEqual(
+            Color.interpolate(
+                ['Color(srgb 1 0.8 0.5)', 'Color(srgb 0.1 none 0.7)', 'Color(srgb 0.2 none 0.9)'],
+                space='srgb',
+                method='bezier'
+            )(0.5),
+            Color('rgb(89.25 204 178.5)')
+        )
+
+    def test_bezier_cylindrical(self):
+        """Test bezier with a cylindrical space."""
+
+        self.assertColorEqual(
+            Color.interpolate(
+                ['hsl(250 50% 30%)', 'hsl(none 0% 10%)', 'hsl(120 75% 75%)'],
+                space='hsl',
+                method='bezier'
+            )(0.75),
+            Color('hsl(176.88 45.313% 47.813%)')
+        )
+
+    def test_bezier_cylindrical_gamut(self):
+        """Test bezier with a cylindrical space with at least one color out of gamut."""
+
+        self.assertColorEqual(
+            Color.interpolate(
+                ['hsl(250 50% 30%)', 'hsl(none 0% 110%)'],
+                space='hsl',
+                method='bezier'
+            )(0.75),
+            Color('hsl(62.5 12.5% 82.5%)')
+        )
diff --git a/tests/test_stops.py b/tests/test_stops.py
index 3ac674dca..e02da807a 100644
--- a/tests/test_stops.py
+++ b/tests/test_stops.py
@@ -1,5 +1,5 @@
 """Test API."""
-from coloraide.interpolate import calc_stops
+from coloraide.interpolate.common import calc_stops
 import pytest